Skip to content

Commit

Permalink
deltas() function, closes #437 (#532)
Browse files Browse the repository at this point in the history
* `deltas()` function, closes #437

* Handle hue angles

- `hue` option to customize how hues are handled
- `shorter` by default

* Proper `none` handling

* Add tests for hues + none

* Fix none handling

* Lint
  • Loading branch information
LeaVerou authored May 31, 2024
1 parent 1b3ec7a commit 25f1c0f
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/color.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
inGamut,
toGamut,
distance,
deltas,
equals,
get,
getAll,
Expand Down Expand Up @@ -189,6 +190,7 @@ Color.defineFunctions({
inGamut,
toGamut,
distance,
deltas,
toString: serialize,
});

Expand Down
49 changes: 49 additions & 0 deletions src/deltas.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import getColor from "./getColor.js";
import ColorSpace from "./space.js";
import to from "./to.js";
import { adjust } from "./angles.js";
import { isNone } from "./util.js";

/**
* Get color differences per-component, on any color space
* @param {Color} c1
* @param {Color} c2
* @param {object} options
* @param {string | ColorSpace} [options.space=c1.space] - The color space to use for the delta calculation. Defaults to the color space of the first color.
* @param {string} [options.hue="shorter"] - How to handle hue differences. Same as hue interpolation option.
* @returns {number[]} - An array of differences per component.
* If one of the components is none, the difference will be 0.
* If both components are none, the difference will be none.
*/
export default function deltas (c1, c2, {space, hue = "shorter"} = {}) {
c1 = getColor(c1);
space ||= c1.space;
space = ColorSpace.get(space);
let spaceCoords = Object.values(space.coords);

[c1, c2] = [c1, c2].map(c => to(c, space));
let [coords1, coords2] = [c1, c2].map(c => c.coords);

let coords = coords1.map((coord1, i) => {
let coordMeta = spaceCoords[i];
let coord2 = coords2[i];

if (coordMeta.type === "angle") {
[coord1, coord2] = adjust(hue, [coord1, coord2]);
}

return subtractCoords(coord1, coord2);
});

let alpha = subtractCoords(c1.alpha, c2.alpha);

return { space: c1.space, spaceId: c1.space.id, coords, alpha };
}

function subtractCoords (c1, c2) {
if (isNone(c1) || isNone(c2)) {
return c1 === c2 ? null : 0;
}

return c1 - c2;
}
1 change: 1 addition & 0 deletions src/index-fn.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export {default as display} from "./display.js";
export {default as inGamut} from "./inGamut.js";
export {default as toGamut, toGamutCSS} from "./toGamut.js";
export {default as distance} from "./distance.js";
export {default as deltas} from "./deltas.js";
export {default as equals} from "./equals.js";
export {default as contrast} from "./contrast.js";
export {default as clone} from "./clone.js";
Expand Down
80 changes: 80 additions & 0 deletions test/deltas.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import "../src/spaces/index.js";
import deltas from "../src/deltas.js";
import * as check from "../node_modules/htest.dev/src/check.js";

export default {
name: "deltas() tests",
description: "These tests test the various Delta E algorithms.",
run (c1, c2, o) {
return deltas(c1, c2, o);
},
check: check.deep(check.shallowEquals({ epsilon: .0001, subset: true })),
tests: [
{
name: "Same color space",
tests: [
{
name: "Same color",
args: ["red", "red"],
expect: { spaceId: "srgb", coords: [0, 0, 0], alpha: 0 },
},
{
args: ["white", "black"],
expect: { spaceId: "srgb", coords: [1, 1, 1], alpha: 0 },
},
{
name: "Hues should never have a difference > 180 by default",
args: [
{spaceId: "oklch", coords: [.5, .2, -180]},
{spaceId: "oklch", coords: [.5, .2, 720]},
],
expect: { spaceId: "oklch", coords: [0, 0, 180], alpha: 0 },
},
{
name: "If both coords are none, the delta should be none.",
args: [
{spaceId: "oklch", coords: [null, null, null], alpha: null},
{spaceId: "oklch", coords: [null, null, null], alpha: null},
],
expect: { spaceId: "oklch", coords: [null, null, null], alpha: null },
},
{
name: "If one coord is none, the delta should be 0.",
args: [
{spaceId: "oklch", coords: [.5, .2, -180], alpha: null},
{spaceId: "oklch", coords: [null, null, null], alpha: .5},
],
expect: { spaceId: "oklch", coords: [0, 0, 0], alpha: 0 },
},
],
},
{
name: "Different color space",
tests: [
{
name: "Same color",
args: ["red", "hsl(0 100% 50%)"],
expect: { spaceId: "srgb", coords: [0, 0, 0], alpha: 0 },
},
{
args: ["white", "hsl(0 100% 0%)"],
expect: { spaceId: "srgb", coords: [1, 1, 1], alpha: 0 },
},
],
},
{
name: "Forced color space",
tests: [
{
name: "Same color",
args: ["red", "hsl(0 100% 50%)", { space: "oklch" }],
expect: { spaceId: "oklch", coords: [0, 0, 0], alpha: 0 },
},
{
args: [{space: "srgb", coords: [1, 0, 0]}, {space: "srgb", coords: [.5, 0, 0]}, { space: "oklch" }],
expect: { spaceId: "oklch", coords: [0.2523245655926571, 0.10354211689049864, 0], alpha: 0 },
},
],
},
],
};

0 comments on commit 25f1c0f

Please sign in to comment.