diff --git a/src/color.js b/src/color.js index 593bb8a43..255aa1f82 100644 --- a/src/color.js +++ b/src/color.js @@ -19,6 +19,7 @@ import { inGamut, toGamut, distance, + deltas, equals, get, getAll, @@ -183,6 +184,7 @@ Color.defineFunctions({ inGamut, toGamut, distance, + deltas, toString: serialize, }); diff --git a/src/deltas.js b/src/deltas.js new file mode 100644 index 000000000..9e2720703 --- /dev/null +++ b/src/deltas.js @@ -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; +} diff --git a/src/index-fn.js b/src/index-fn.js index 88a4046db..f5003f415 100644 --- a/src/index-fn.js +++ b/src/index-fn.js @@ -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"; diff --git a/test/deltas.js b/test/deltas.js new file mode 100644 index 000000000..b21e55f79 --- /dev/null +++ b/test/deltas.js @@ -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 }, + }, + ], + }, + ], +};