Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ node_modules/
# Ignore generated HTML files
*.html
!*.tpl.html
!*/*.html
!*/*.html
!*/**/*.html
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ They are useful in their own right, but also serve as [Color.js](https://colorjs
- [Convert across everything](convert)
- [Gamut Mapping Gradients](gamut-mapping/gradients)
- [Gamut Mapping Playground](gamut-mapping)
- [Gamut Mapping Comparison](gamut-mapping/compare)
- [Gradient interpolation](gradients)
- [Named color proximity](named)

Expand Down
57 changes: 57 additions & 0 deletions gamut-mapping/compare/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Color GMA Test</title>
<script type="module" src="script.js"></script>
<link rel="modulepreload" href="worker.js" />
</head>
<body>
<h1>GMA Comparison</h1>
<p>
This compares the output of different gamut mapping algorithms when
mapping to <code>rec2020</code>, and then clipping to <code>p3</code> and
<code>srgb</code> to approximate clipping to the display device.
</p>
<p>
It steps through the <code>xyz-d65</code> space with the provided delta,
and checks a color if it is outside of <code>rec2020</code>.
</p>
<dl>
<dt>Scales:</dt>
<dd>
<ul>
<li>L = 0-100 (So a 1 would be equivalent to 0.01 or 1%)</li>
<li>C = 0-0.4 (Most of the mapped colors had very high chroma to start with, so we're seeing large shifts here, as expected)</li>
<li>h = 0-360</li>

</ul>
</dd>
<dt>Algorithms:</dt>
<dd>
<ul>
<li>edge: Edge Tracer, mapping to rec2020</li>
<li>chromium: Chromium "baked in" implementation, mapping to rec2020</li>
<li>bjorn: Björn Ottosson method, mapping to rec2020</li>
<li>raytrace: Raytrace, mapping to rec2020</li>
<li>clip: Clipping directly to p3, as a comparison</li>
</ul>
</dl>



<button id="run">Run</button>
<label for="delta">Delta</label>
<input
type="number"
id="delta"
value="0.03"
step="0.01"
min="0.01"
max="1"
/>

<pre id="output"></pre>
</body>
</html>
21 changes: 21 additions & 0 deletions gamut-mapping/compare/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// @ts-check
const worker = new Worker("./worker.js", { type: "module" });

const output = document.getElementById("output");
worker.onmessage = (message) => {
const data = JSON.parse(message.data);
if (data.results) {
output.innerHTML = JSON.stringify(data, null, 2) + "<br/><br/><br/><br/>";
}
};
worker.onerror = (...error) => {
console.error("Worker error:", error);
output.innerHTML = "Error: " + error.message;
};

const run = document.getElementById("run");
run.addEventListener("click", () => {
const delta = parseFloat(document.getElementById("delta").value);
console.log("Running worker");
worker.postMessage(["run", { delta }]);
});
68 changes: 68 additions & 0 deletions gamut-mapping/compare/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// @ts-check
import Color from "https://colorjs.io/dist/color.js";
import methods from "../methods.js";

export function* xyzColorGenerator(settings) {
let count = 0;
for (var x = settings.x.min; x <= settings.x.max; x = x + settings.x.delta) {
for (
var y = settings.y.min;
y <= settings.y.max;
y = y + settings.y.delta
) {
for (
var z = settings.z.min;
z <= settings.z.max;
z = z + settings.z.delta
) {
yield [`color(xyz ${x} ${y} ${z})`, count++];
}
}
}
}

export const deltas = (original, mapped) => {
let deltas = {};
["L", "C", "H"].forEach((c, i) => {
let delta = mapped.to("oklch").coords[i] - original.to("oklch").coords[i];

if (c === "L") {
// L is percentage
delta *= 100;
} else if (c === "H") {
// Hue is angular, so we need to normalize it
delta = ((delta % 360) + 720) % 360;
delta = Math.min(360 - delta, delta);
// Check: Is this hiding cases where only one value is NaN?
if (isNaN(delta)) delta = 0;
} else {
// debugger;
}

// delta = Color.util.toPrecision(delta, 2);
// Use absolute because we are interested in magnitude, not direction
deltas[c] = Math.abs(delta);
});
deltas.delta2000 = original.deltaE(mapped, { method: "2000" });
return deltas;
};

export const clipP3 = (color) =>
color.clone().toGamut({ space: "p3", method: "clip" });
export const clipSrgb = (color) =>
color.clone().toGamut({ space: "srgb", method: "clip" });

export const edgeSeekerColor = (color) => methods["edge-seeker-rec2020"].compute(color);

export const runningAverage = (average, newValue, count) => {
if (count === 1) {
return newValue;
}
return (average * (count - 1)) / count + newValue / count;
};

export const chromiumColor = (color) => methods.chromium.compute(color);

export const bjornColor = (color) => methods.bjornRec2020.compute(color);

export const raytraceColor = (color) => methods.raytraceRec2020.compute(color);
158 changes: 158 additions & 0 deletions gamut-mapping/compare/worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// @ts-check
import Color from "https://colorjs.io/dist/color.js";

import {
xyzColorGenerator,
deltas,
edgeSeekerColor,
clipP3,
clipSrgb,
runningAverage,
chromiumColor,
bjornColor,
raytraceColor,
} from "./utils.js";

const settings = (delta)=> ({
x: { min: 0, max: 1, delta },
y: { min: 0, max: 1, delta },
z: { min: 0, max: 1, delta },
});
const results = {
edgeToP3: { L: 0, C: 0, H: 0, delta2000: 0 },
edgeToSrgb: { L: 0, C: 0, H: 0, delta2000: 0 },
chromiumToP3: { L: 0, C: 0, H: 0, delta2000: 0 },
chromiumToSrgb: { L: 0, C: 0, H: 0, delta2000: 0 },
bjornToP3: { L: 0, C: 0, H: 0, delta2000: 0 },
bjornToSrgb: { L: 0, C: 0, H: 0, delta2000: 0 },
clipToP3: { L: 0, C: 0, H: 0, delta2000: 0 },
raytraceToP3: { L: 0, C: 0, H: 0, delta2000: 0 },
raytraceToSrgb: { L: 0, C: 0, H: 0, delta2000: 0 },
delta: null
};

const rawResults = [];

// methods:
// - edge to rec2020, clip to p3
// - edge to rec2020, clip to srgb
// - clip to p3
// - clip to srgb

const run = ({delta}) => {
const colors = xyzColorGenerator(settings(delta));
results.delta = delta;
let color = colors.next();
let count = 0;

while (!color.done) {
const [colorString, index] = color.value;
count = index;
let _color = new Color(colorString);
if (_color.inGamut("rec2020", { epsilon: 0 })) {
color = colors.next();
continue; // Skip colors in rec2020 gamut
}

const colorData = processColor(_color);
// rawResults.push({ colorString, colorData });
aggregate(colorData, index);

color = colors.next();
if (index % 100 === 0) {
postMessage(JSON.stringify({ results, count: index }));
}
}
postMessage(JSON.stringify({ results, count }));
};

const processColor = (color) => {
const edgeSeeker = edgeSeekerColor(color);
const edgeClippedToP3 = clipP3(edgeSeeker);
const edgeClippedToSrgb = clipSrgb(edgeSeeker);
const edgeP3Delta = deltas(color, edgeClippedToP3);
const edgeSrgbDelta = deltas(color, edgeClippedToSrgb);

const clippedStraightToP3 = clipP3(color);
const clippedStraightToP3Delta = deltas(color, clippedStraightToP3);

const chromiumClippedToP3 = chromiumColor(color);
const chromiumClippedToSrgb = clipSrgb(chromiumClippedToP3);
const chromiumP3Delta = deltas(color, chromiumClippedToP3);
const chromiumSrgbDelta = deltas(color, chromiumClippedToSrgb);

const bjorn = bjornColor(color);
const bjornClippedToP3 = clipP3(bjorn);
const bjornClippedToSrgb = clipSrgb(bjorn);
const bjornP3Delta = deltas(color, bjornClippedToP3);
const bjornSrgbDelta = deltas(color, bjornClippedToSrgb);

const raytrace = raytraceColor(color);
const raytraceClippedToP3 = clipP3(raytrace);
const raytraceClippedToSrgb = clipSrgb(raytrace);
const raytraceP3Delta = deltas(color, raytraceClippedToP3);
const raytraceSrgbDelta = deltas(color, raytraceClippedToSrgb);

return {
color,
edgeSeeker,
edgeClippedToP3,
edgeClippedToSrgb,
clippedStraightToP3,
edgeP3Delta,
edgeSrgbDelta,
clippedStraightToP3Delta,
chromiumClippedToP3,
chromiumClippedToSrgb,
chromiumP3Delta,
chromiumSrgbDelta,
bjornClippedToP3,
bjornClippedToSrgb,
bjornP3Delta,
bjornSrgbDelta,
raytraceP3Delta,
raytraceSrgbDelta,
};
};

const aggregate = (colorData, index) => {
const {
edgeP3Delta,
clippedStraightToP3Delta,
edgeSrgbDelta,
chromiumP3Delta,
chromiumSrgbDelta,
bjornP3Delta,
bjornSrgbDelta,
raytraceP3Delta,
raytraceSrgbDelta,
} = colorData;

const map = [
["edgeToP3", edgeP3Delta],
["edgeToSrgb", edgeSrgbDelta],
["chromiumToP3", chromiumP3Delta],
["chromiumToSrgb", chromiumSrgbDelta],
["bjornToP3", bjornP3Delta],
["bjornToSrgb", bjornSrgbDelta],
["raytraceToP3", raytraceP3Delta],
["raytraceToSrgb", raytraceSrgbDelta],
["clipToP3", clippedStraightToP3Delta],
];

map.forEach(([key, delta]) => {
const res = results[key];
["L", "C", "H", "delta2000"].forEach((d) => {
res[d] = runningAverage(res[d], delta[d], index);
});
});

return results;
};

onmessage = (event) => {
console.log("Message received from main script - compare", event, event.data);
if (event.data[0] === "run") {
run(event.data[1]);
}
};
5 changes: 5 additions & 0 deletions gamut-mapping/gradients.css
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,10 @@ details.timing-info {
justify-content: space-between;
gap: 1em;
}
}

#algorithms{
label{
margin-inline-end: 1em;
}
}
11 changes: 9 additions & 2 deletions gamut-mapping/gradients.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ <h1>Gamut Mapping Gradients</h1>
<input v-model="maxDeltaE" type="number" min="1" name="maxDeltaE"/><br/>
<div>{{steps.length}} Gradient Steps</div>
<label for="flush">Flush:</label> <input type="checkbox" v-model="flush" name="flush">
<details id="algorithms">
<summary>Algorithms</summary>
<label v-for="method in methods" :key="method">
<input type="checkbox" v-model="enabledMethods" :value="method" />
{{ method }}
</label>
</details>
</div>
<div class="color-inputs">
<color-swatch size="large" @colorchange="colorChangeFrom" :value="from">
Expand All @@ -46,8 +53,8 @@ <h1>Gamut Mapping Gradients</h1>
<div v-for="[title, step] in oogSteps" :style="{'--step-color': step}" :title="title"></div>
</div>
</div>
<div :class="{flush, 'gradients': true}">
<article class="method" v-for="(i, method) of methods">
<div :class="{flush, 'gradients': true}" :key="refresh">
<article class="method" v-for="(i, method) of enabledMethods">
<mapped-gradient :key="i" :steps="steps" :method="i" @report-time="reportTime"/>
</article>
</div>
Expand Down
15 changes: 12 additions & 3 deletions gamut-mapping/gradients.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ let app = createApp({
const urlToColor = params.get("to");
const from = urlFromColor || "oklch(90% .4 250)";
const to = urlToColor || "oklch(40% .1 20)";
const methods = ["none", "clip", "scale-lh", "css", "css-rec2020", "raytrace", "bjorn", "edge-seeker", "chromium"];
const methods = ["none", "clip", "scale-lh", "css", "css-rec2020", "raytrace", "raytraceRec2020", "bjorn", "bjornRec2020", "edge-seeker", "edge-seeker-rec2020", "chromium"];
const enabledMethods = ["clip", "css-rec2020", "raytraceRec2020", "bjornRec2020", "edge-seeker-rec2020", "chromium"];
const runResults = {};
methods.forEach(method => runResults[method] = []);
enabledMethods.forEach(method => runResults[method] = []);
return {
methods: methods,
methods,
enabledMethods,
from: from,
to: to,
parsedFrom: this.tryParse(from),
Expand All @@ -27,6 +29,7 @@ let app = createApp({
params: params,
interpolationSpaces: ["oklch", "oklab", "p3", "rec2020", "lab"],
runResults: runResults,
refresh: 0,
};
},

Expand Down Expand Up @@ -97,6 +100,12 @@ let app = createApp({
deep: true,
immediate: true,
},
enabledMethods(newValue) {
const runResults = {};
newValue.forEach(method => runResults[method] = []);
this.runResults = runResults;
this.refresh++;
}
},

components: {
Expand Down
Loading