236 lines
8.3 KiB
HTML
236 lines
8.3 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<title>Perceptual Color Nudging</title>
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<style>
|
|
html {
|
|
background-color: #0c0c0c;
|
|
color: #cccccc;
|
|
font-family: "Cascadia Code", "Cascadia Mono", monospace;
|
|
}
|
|
|
|
body {
|
|
display: flex;
|
|
margin: 0;
|
|
white-space: nowrap;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
body>div {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 2rem;
|
|
}
|
|
|
|
form,
|
|
h2 {
|
|
margin: 1rem;
|
|
}
|
|
|
|
p,
|
|
pre {
|
|
margin: 0.5rem;
|
|
}
|
|
|
|
table {
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
table td {
|
|
padding: 0.5rem;
|
|
}
|
|
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div style="flex: 2; align-items: flex-start; background-color: #0c0c0c">
|
|
<form>
|
|
<input id="background-color" name="background-color" type="color" value="#0c0c0c" />
|
|
<label for="background-color">background color</label>
|
|
</form>
|
|
<table>
|
|
<tr>
|
|
<td>Input</td>
|
|
<td>WCAG21:<br>APCA:</td>
|
|
<td id="stats-input"></td>
|
|
</tr>
|
|
<tr>
|
|
<td>ΔE2000<br>(ConEmu)</td>
|
|
<td>WCAG21:<br>APCA:</td>
|
|
<td id="stats-cielab"></td>
|
|
</tr>
|
|
<tr>
|
|
<td>ΔEOK</td>
|
|
<td>WCAG21:<br>APCA:</td>
|
|
<td id="stats-oklab"></td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
<div id="input" style="flex: 1">
|
|
<h2>Input</h2>
|
|
<pre style="font-size: 8pt">猫</pre>
|
|
<pre style="font-size: 10pt">猫</pre>
|
|
<pre style="font-size: 12pt">猫</pre>
|
|
<pre style="font-size: 14pt">猫</pre>
|
|
<pre style="font-size: 16pt">猫</pre>
|
|
<pre style="font-size: 32pt">猫</pre>
|
|
<pre style="font-size: 64pt">猫</pre>
|
|
</div>
|
|
<div id="cielab" style="flex: 1">
|
|
<h2>ΔE2000 (ConEmu)</h2>
|
|
<pre style="font-size: 8pt">猫</pre>
|
|
<pre style="font-size: 10pt">猫</pre>
|
|
<pre style="font-size: 12pt">猫</pre>
|
|
<pre style="font-size: 14pt">猫</pre>
|
|
<pre style="font-size: 16pt">猫</pre>
|
|
<pre style="font-size: 32pt">猫</pre>
|
|
<pre style="font-size: 64pt">猫</pre>
|
|
</div>
|
|
<div id="oklab" style="flex: 1">
|
|
<h2>ΔEOK</h2>
|
|
<pre style="font-size: 8pt">猫</pre>
|
|
<pre style="font-size: 10pt">猫</pre>
|
|
<pre style="font-size: 12pt">猫</pre>
|
|
<pre style="font-size: 14pt">猫</pre>
|
|
<pre style="font-size: 16pt">猫</pre>
|
|
<pre style="font-size: 32pt">猫</pre>
|
|
<pre style="font-size: 64pt">猫</pre>
|
|
</div>
|
|
<script type="module">
|
|
import Color from "https://cdn.jsdelivr.net/npm/colorjs.io@0.4.3/+esm";
|
|
|
|
window.Color = Color;
|
|
|
|
const input = document.getElementById("input");
|
|
const cielab = document.getElementById("cielab");
|
|
const oklab = document.getElementById("oklab");
|
|
|
|
const statsInput = document.getElementById("stats-input");
|
|
const statsCielab = document.getElementById("stats-cielab");
|
|
const statsOklab = document.getElementById("stats-oklab");
|
|
|
|
let backgroundColor = new Color("#0c0c0c");
|
|
let foregroundColor = new Color("#0c0c0c");
|
|
let foregroundColorRange = null;
|
|
let previousSecsIntegral = -1;
|
|
|
|
function saturate(val) {
|
|
return val < 0 ? 0 : val > 1 ? 1 : val;
|
|
}
|
|
|
|
function clipToSrgb(color) {
|
|
return color.to("srgb").toGamut({ method: "clip" });
|
|
}
|
|
|
|
function nudgeCielab(backgroundColor, foregroundColor) {
|
|
const backgroundCielab = backgroundColor.to("lab-d65");
|
|
const foregroundCielab = foregroundColor.to("lab-d65");
|
|
|
|
const de1 = Color.deltaE(foregroundColor, backgroundCielab, "2000");
|
|
if (de1 >= 12.0) {
|
|
return foregroundColor;
|
|
}
|
|
|
|
for (let i = 0; i <= 1; i++) {
|
|
const step = (i == 0) ? 5.0 : -5.0;
|
|
foregroundCielab.l += step;
|
|
|
|
while (((i == 0) && foregroundCielab.l <= 100) || (i == 1 && foregroundCielab.l >= 0)) {
|
|
const de2 = Color.deltaE(foregroundCielab, backgroundCielab, "2000");
|
|
if (de2 >= 20.0) {
|
|
return clipToSrgb(foregroundCielab);
|
|
}
|
|
foregroundCielab.l += step;
|
|
}
|
|
}
|
|
}
|
|
|
|
function nudgeOklab(backgroundColor, foregroundColor) {
|
|
const backgroundOklab = backgroundColor.to("oklab");
|
|
const foregroundOklab = foregroundColor.to("oklab");
|
|
const deltaSquared = {
|
|
l: (backgroundOklab.l - foregroundOklab.l) ** 2,
|
|
a: (backgroundOklab.a - foregroundOklab.a) ** 2,
|
|
b: (backgroundOklab.b - foregroundOklab.b) ** 2,
|
|
};
|
|
const distance = deltaSquared.l + deltaSquared.a + deltaSquared.b;
|
|
|
|
if (distance >= 0.25) {
|
|
return foregroundColor;
|
|
}
|
|
|
|
let deltaL = Math.sqrt(0.25 - deltaSquared.a - deltaSquared.b);
|
|
if (foregroundOklab.l < backgroundOklab.l)
|
|
{
|
|
deltaL = -deltaL;
|
|
}
|
|
|
|
foregroundOklab.l = backgroundOklab.l + deltaL;
|
|
if (foregroundOklab.l < 0 || foregroundOklab.l > 1)
|
|
{
|
|
foregroundOklab.l = backgroundOklab.l - deltaL;
|
|
}
|
|
|
|
return clipToSrgb(foregroundOklab);
|
|
}
|
|
|
|
function contrastStringLevels(num, level0, level1) {
|
|
const str = num.toFixed(1);
|
|
if (num < level0) {
|
|
return `<span style="color:crimson">${str}</span>`;
|
|
}
|
|
if (num < level1) {
|
|
return `<span style="color:coral">${str}</span>`;
|
|
}
|
|
return str;
|
|
}
|
|
|
|
function contrastString(foregroundColor) {
|
|
const contrastWCAG21 = contrastStringLevels(foregroundColor.contrast(backgroundColor, "WCAG21"), 3, 4.5);
|
|
const contrastAPCA = contrastStringLevels(Math.abs(foregroundColor.contrast(backgroundColor, "APCA")), 45, 60);
|
|
return `${contrastWCAG21}<br/>${contrastAPCA}`;
|
|
}
|
|
|
|
function animate(time) {
|
|
const timeScale = time / 1000;
|
|
const secsIntegral = Math.trunc(timeScale);
|
|
const secsFractional = timeScale % 1;
|
|
|
|
if (previousSecsIntegral != secsIntegral) {
|
|
const foregroundColorTarget = new Color("srgb", backgroundColor.coords.map(c => saturate(c + Math.random() - 0.5)));
|
|
foregroundColorRange = foregroundColor.range(foregroundColorTarget, { space: "srgb" });
|
|
previousSecsIntegral = secsIntegral;
|
|
}
|
|
|
|
foregroundColor = foregroundColorRange(secsFractional);
|
|
input.style.color = foregroundColor.toString({ inGamut: false });
|
|
|
|
const foregroundCielabNudged = nudgeCielab(backgroundColor, foregroundColor);
|
|
const foregroundOklabNudged = nudgeOklab(backgroundColor, foregroundColor);
|
|
|
|
cielab.style.color = foregroundCielabNudged;
|
|
oklab.style.color = foregroundOklabNudged;
|
|
|
|
statsInput.innerHTML = contrastString(foregroundColor);
|
|
statsCielab.innerHTML = contrastString(foregroundCielabNudged);
|
|
statsOklab.innerHTML = contrastString(foregroundOklabNudged);
|
|
|
|
requestAnimationFrame(animate);
|
|
}
|
|
requestAnimationFrame(animate);
|
|
|
|
document.getElementById("background-color").addEventListener("input", event => {
|
|
backgroundColor = new Color(event.target.value);
|
|
document.documentElement.style.backgroundColor = backgroundColor;
|
|
}, false);
|
|
|
|
document.documentElement.style.backgroundColor = backgroundColor;
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|