diff --git a/.gitignore b/.gitignore
index b8a5f34..1aa7ff0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,5 @@ node_modules/
# Ignore generated HTML files
*.html
!*.tpl.html
-!*/*.html
\ No newline at end of file
+!*/*.html
+!*/**/*.html
\ No newline at end of file
diff --git a/README.md b/README.md
index ed3f3bc..96f5e91 100644
--- a/README.md
+++ b/README.md
@@ -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)
diff --git a/gamut-mapping/compare/index.html b/gamut-mapping/compare/index.html
new file mode 100644
index 0000000..239c98f
--- /dev/null
+++ b/gamut-mapping/compare/index.html
@@ -0,0 +1,57 @@
+
+
+
+
+
+ Color GMA Test
+
+
+
+
+ GMA Comparison
+
+ This compares the output of different gamut mapping algorithms when
+ mapping to rec2020, and then clipping to p3 and
+ srgb to approximate clipping to the display device.
+
+
+ It steps through the xyz-d65 space with the provided delta,
+ and checks a color if it is outside of rec2020.
+
+
+ Scales:
+
+
+ L = 0-100 (So a 1 would be equivalent to 0.01 or 1%)
+ 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)
+ h = 0-360
+
+
+
+ Algorithms:
+
+
+ edge: Edge Tracer, mapping to rec2020
+ chromium: Chromium "baked in" implementation, mapping to rec2020
+ bjorn: Björn Ottosson method, mapping to rec2020
+ raytrace: Raytrace, mapping to rec2020
+ clip: Clipping directly to p3, as a comparison
+
+
+
+
+
+ Run
+ Delta
+
+
+
+
+
diff --git a/gamut-mapping/compare/script.js b/gamut-mapping/compare/script.js
new file mode 100644
index 0000000..b7011ee
--- /dev/null
+++ b/gamut-mapping/compare/script.js
@@ -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) + " ";
+ }
+};
+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 }]);
+});
diff --git a/gamut-mapping/compare/utils.js b/gamut-mapping/compare/utils.js
new file mode 100644
index 0000000..0e22b74
--- /dev/null
+++ b/gamut-mapping/compare/utils.js
@@ -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);
diff --git a/gamut-mapping/compare/worker.js b/gamut-mapping/compare/worker.js
new file mode 100644
index 0000000..0df63c6
--- /dev/null
+++ b/gamut-mapping/compare/worker.js
@@ -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]);
+ }
+};
diff --git a/gamut-mapping/gradients.css b/gamut-mapping/gradients.css
index 7bd3bc8..06c3c73 100644
--- a/gamut-mapping/gradients.css
+++ b/gamut-mapping/gradients.css
@@ -79,5 +79,10 @@ details.timing-info {
justify-content: space-between;
gap: 1em;
}
+}
+#algorithms{
+ label{
+ margin-inline-end: 1em;
+ }
}
\ No newline at end of file
diff --git a/gamut-mapping/gradients.html b/gamut-mapping/gradients.html
index ed7553d..35fd7ef 100644
--- a/gamut-mapping/gradients.html
+++ b/gamut-mapping/gradients.html
@@ -23,6 +23,13 @@ Gamut Mapping Gradients
{{steps.length}} Gradient Steps
Flush:
+
+ Algorithms
+
+
+ {{ method }}
+
+
-
-
+
diff --git a/gamut-mapping/gradients.js b/gamut-mapping/gradients.js
index c789f62..1a92191 100644
--- a/gamut-mapping/gradients.js
+++ b/gamut-mapping/gradients.js
@@ -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),
@@ -27,6 +29,7 @@ let app = createApp({
params: params,
interpolationSpaces: ["oklch", "oklab", "p3", "rec2020", "lab"],
runResults: runResults,
+ refresh: 0,
};
},
@@ -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: {
diff --git a/gamut-mapping/methods.js b/gamut-mapping/methods.js
index beafeaa..d7bfa80 100644
--- a/gamut-mapping/methods.js
+++ b/gamut-mapping/methods.js
@@ -11,6 +11,10 @@ const p3EdgeSeeker = makeEdgeSeeker((r, g, b) => {
const [l, c, h = 0] = new Color("p3", [r, g, b]).to("oklch").coords;
return { l, c, h };
});
+const rec2020EdgeSeeker = makeEdgeSeeker((r, g, b) => {
+ const [l, c, h = 0] = new Color("rec2020", [r, g, b]).to("oklch").coords;
+ return { l, c, h };
+});
const methods = {
"clip": {
@@ -192,7 +196,7 @@ const methods = {
},
},
"bjorn" : {
- label: "Björn Ottosson",
+ label: "Björn Ottosson P3",
description: "Approach using Oklab as defined by the creator of Oklab, Bjorn Ottosson. Projected toward constant lightness.",
lmsToP3Linear: [
[ 3.1277689713618737, -2.2571357625916377, 0.1293667912297650],
@@ -270,6 +274,85 @@ const methods = {
return oklab.to('p3').toGamut({method: 'clip'});
},
},
+ "bjornRec2020" : {
+ label: "Björn Ottosson Rec2020",
+ description: "Approach using Oklab as defined by the creator of Oklab, Bjorn Ottosson. Projected toward constant lightness.",
+ lmsToRec2020Linear: [
+ [2.1399067304346517, -1.2463894937606181, 0.10648276332596672],
+ [-0.8847358357577675, 2.1632309383612007, -0.27849510260343346],
+ [-0.04857374640044416, -0.454503149714096, 1.5030768961145402],
+ ],
+ rec2020Coeff: [
+ // Red
+ [
+ // Limit
+ [-1.36834899, -0.46664773],
+ // `Kn` coefficients
+ [1.2572445, 1.71580176, 0.5648733, 0.79507316, 0.58716363],
+ ],
+ // Green
+ [
+ // Limit
+ [2.01150796, -2.0379096],
+ // `Kn` coefficients
+ [0.74087755, -0.4586733, 0.08182977, 0.12598705, -0.14570327],
+ ],
+ // Blue
+ [
+ // Limit
+ [0.06454093, 2.29709336],
+ // `Kn` coefficients
+ [1.36920484e+00, -1.64666735e-02, -1.14197870e+00, -5.01064768e-01, 1.19905985e-03],
+ ],
+ ],
+ compute: (color) => {
+ // Approach described in https://bottosson.github.io/posts/gamutclipping/
+ // For comparison against CSS approaches, constant lightness was used.
+ let oklab = color.to("oklab");
+
+ // Clamp lightness and see if we are in gamut.
+ oklab.l = util.clamp(0.0, oklab.l, 1.0); // If doing adaptive lightness, this might not be wanted.
+ if (oklab.inGamut("rec2020", { epsilon: 0 })) {
+ return oklab.to("rec2020");
+ }
+
+ // Get coordinates and calculate chroma
+ let [l, a, b] = oklab.coords;
+ // Bjorn used 0.00001, are there issues with 0.0?
+ const epsilon = 0.0
+ let c = Math.max(epsilon, Math.sqrt(a ** 2 + b ** 2));
+
+ // Normalize a and b
+ if (c) {
+ a /= c;
+ b /= c;
+ }
+
+ // Get gamut specific transform from LMS to RGB and related coefficients
+ const lmsToLinear = methods.bjornRec2020.lmsToRec2020Linear;
+ const coeff = methods.bjornRec2020.rec2020Coeff;
+
+ // Find the lightness and chroma for the cusp.
+ let cusp = findCusp(a, b, lmsToLinear, coeff);
+
+ // Set the target lightness towards which chroma reduction will take place.
+ // `cusp[0]` is approximate lightness of cusp, l is current lightness.
+ // One could apply some adaptive lightness if desired.
+ const target = l; // cusp[0];
+ const t = findGamutIntersection(a, b, l, c, target, lmsToLinear, coeff, cusp);
+
+ // Adjust lightness and chroma
+ if (target !== l) {
+ oklab.l = target * (1 - t) + t * l;
+ }
+ c *= t;
+ oklab.a = c * a;
+ oklab.b = c * b;
+
+ // Convert back to rec2020 and clip.
+ return oklab.to("rec2020").toGamut({method: "clip"});
+ },
+ },
"raytrace": {
label: "Raytrace",
description: "Uses ray tracing to find a color with reduced chroma on the RGB surface.",
@@ -486,9 +569,170 @@ const methods = {
];
},
},
+ "raytraceRec2020": {
+ label: "Raytrace Rec2020",
+ description: "Uses ray tracing to find a color with reduced chroma on the RGB surface.",
+ compute: (color) => {
+ // An approached originally designed for ColorAide.
+ // https://facelessuser.github.io/coloraide/gamut/#ray-tracing-chroma-reduction
+ if (color.inGamut("rec2020", { epsilon: 0 })) {
+ return color.to("rec2020");
+ }
+
+ let mapColor = color.to("oklch");
+ let lightness = mapColor.coords[0];
+
+ if (lightness >= 1) {
+ return new Color({ space: "xyz-d65", coords: WHITES["D65"] }).to("p3");
+ }
+ else if (lightness <= 0) {
+ return new Color({ space: "xyz-d65", coords: [0, 0, 0] }).to("p3");
+ }
+ return methods.raytraceRec2020.trace(mapColor);
+ },
+
+ oklchToLinearRGB (lch) {
+ // Convert from Oklab to linear RGB.
+ //
+ // Can be any gamut as long as `lmsToRgb` is a matrix
+ // that transform the LMS values to the linear RGB space.
+
+ let c = lch[1];
+ let h = lch[2];
+ // to lab
+ let result = [
+ lch[0],
+ c * Math.cos((h * Math.PI) / 180),
+ c * Math.sin((h * Math.PI) / 180)
+ ];
+
+ // To LMS
+ util.multiply_v3_m3x3(
+ result,
+ [
+ [ 1.0000000000000000, 0.3963377773761749, 0.2158037573099136 ],
+ [ 1.0000000000000000, -0.1055613458156586, -0.0638541728258133 ],
+ [ 1.0000000000000000, -0.0894841775298119, -1.2914855480194092 ]
+ ],
+ result
+ );
+ result[0] = result[0] ** 3;
+ result[1] = result[1] ** 3;
+ result[2] = result[2] ** 3;
+
+ // To RGB
+ util.multiply_v3_m3x3(
+ result,
+ [
+ [2.1399067304346517, -1.2463894937606181, 0.10648276332596672],
+ [-0.8847358357577675, 2.1632309383612007, -0.27849510260343346],
+ [-0.04857374640044416, -0.454503149714096, 1.5030768961145402]
+ ],
+ result
+ );
+ return result;
+ },
+
+ LinearRGBtoOklch (rgb) {
+ // Convert from Oklch to linear RGB.
+ //
+ // Can be any gamut as long as `lmsToRgb` is a matrix
+ // that transform the LMS values to the linear RGB space.
+
+ // To LMS
+ let result = util.multiply_v3_m3x3(
+ rgb,
+ [
+ [0.6167557848654442, 0.3601984012264634, 0.023045813908092294],
+ [0.2651330593926367, 0.6358393720678492, 0.0990275685395141],
+ [0.10010262952034829, 0.20390652261661446, 0.6959908478630372]
+ ]
+ );
+
+ result[0] = Math.cbrt(result[0]);
+ result[1] = Math.cbrt(result[1]);
+ result[2] = Math.cbrt(result[2]);
+
+ util.multiply_v3_m3x3(
+ result,
+ [
+ [ 0.2104542683093140, 0.7936177747023054, -0.0040720430116193 ],
+ [ 1.9779985324311684, -2.4285922420485799, 0.4505937096174110 ],
+ [ 0.0259040424655478, 0.7827717124575296, -0.8086757549230774 ]
+ ],
+ result
+ );
+
+ let a = result[1];
+ let b = result[2];
+ return [
+ result[0],
+ Math.sqrt(a ** 2 + b ** 2),
+ constrainAngle((Math.atan2(b, a) * 180) / Math.PI)
+ ];
+ },
+
+ trace: (orig) => {
+ let coords = orig.coords;
+ let [light, chroma, hue] = coords;
+ coords[1] = 0;
+
+ let anchor = methods.raytraceRec2020.oklchToLinearRGB(coords);
+ coords[1] = chroma;
+ let mapColor = methods.raytraceRec2020.oklchToLinearRGB(coords);
+
+ let raytrace = methods.raytrace.raytrace_box;
+ // Assume an RGB range between 0 - 1.
+ // This could be different depending on the RGB max luminance and could
+ // be calculated to be different depending on needs.
+ // We'll use this to adjust our anchor point closer to the gamut surface.
+ let low = 1e-6;
+ let high = 1 - low;
+
+ // Cast a ray from the zero chroma color to the target color.
+ // Trace the line to the RGB cube edge and find where it intersects.
+ // Correct L and h within the perceptual OkLCh after each attempt.
+ for (let i = 0; i < 4; i++) {
+ if (i) {
+ const oklch = methods.raytraceRec2020.LinearRGBtoOklch(mapColor);
+ oklch[0] = light;
+ oklch[2] = hue;
+ mapColor = methods.raytraceRec2020.oklchToLinearRGB(oklch);
+ }
+ const current = mapColor.slice();
+ const intersection = raytrace(anchor, current);
+
+ // Adjust anchor point closer to surface, when possible, to improve results for some spaces.
+ // But not too close to the surface.
+ if (i && current.every((x) => low < x && x < high)) {
+ anchor = current;
+ }
+
+ if (intersection.length) {
+ mapColor = intersection.slice();
+ continue;
+ }
+
+ // If there was no change, we are done
+ break;
+ }
+
+ // Remove noise from floating point math by clipping
+ orig.setAll(
+ 'rec2020-linear',
+ [
+ util.clamp(0.0, mapColor[0], 1.0),
+ util.clamp(0.0, mapColor[1], 1.0),
+ util.clamp(0.0, mapColor[2], 1.0),
+ ]
+ );
+
+ return orig.to("rec2020");
+ },
+ },
"edge-seeker": {
label: "Edge Seeker",
- description: "Using a LUT to detect edges of the gamut and reduce chroma accordingly.",
+ description: "Using a LUT to detect edges of the p3 gamut and reduce chroma accordingly.",
compute: (color) => {
let [l, c, h] = color.to("oklch").coords;
if (l <= 0) {
@@ -505,6 +749,25 @@ const methods = {
return new Color("oklch", [l, c, h]).toGamut({ space: "p3", method: "clip" });
},
},
+ "edge-seeker-rec2020": {
+ label: "Edge Seeker rec2020",
+ description: "Using a LUT to detect edges of the rec2020 gamut and reduce chroma accordingly.",
+ compute: (color) => {
+ let [l, c, h] = color.to("oklch").coords;
+ if (l <= 0) {
+ return new Color("oklch", [0, 0, h]);
+ }
+ if (l >= 1) {
+ return new Color("oklch", [1, 0, h]);
+ }
+ let maxChroma = rec2020EdgeSeeker(l, h || 0);
+ if (c > maxChroma) {
+ c = maxChroma;
+ }
+ // Don't clip, and return rec2020 gamut
+ return new Color("oklch", [l, c, h]);
+ },
+ },
// "scale125": {
// label: "Scale from 0.125",
// description: "Using a midpoint of 0.125 (perceptual midpoint per Oklab), scale the color to fit within the linear P3 gamut using different scaling factors for values below and above the midpoint.",
diff --git a/gamut-mapping/timing-info.js b/gamut-mapping/timing-info.js
index ff16bfa..e3ea856 100644
--- a/gamut-mapping/timing-info.js
+++ b/gamut-mapping/timing-info.js
@@ -8,10 +8,11 @@ export default {
computed: {
results () {
- const [none, ...methodsTested] = Object.keys(this.runResults);
+ const methodsTested = Object.keys(this.runResults).filter(method => method !== "none");
+
return methodsTested.map(method => {
- const data = this.runResults[method];
- const total = data.reduce((acc, value) => acc + value);
+ const data = this.runResults[method] ?? [];
+ const total = data.reduce((acc, value) => acc + value, 0);
return {
id: method,