Skip to content

Commit

Permalink
Merge branch 'main' into formatting
Browse files Browse the repository at this point in the history
* main:
  Add CSS toGamut algorithm (color-js#344)
  Accept "Lightness" for lab space first channel name (color-js#348)
  Fix type def for MixOptions (color-js#347)
  Add CJS file to /fn entry and fix legacy builds (color-js#349)
  • Loading branch information
jgerigmeyer committed Nov 16, 2023
2 parents db5521d + 496102a commit 6a6e7ee
Show file tree
Hide file tree
Showing 17 changed files with 238 additions and 108 deletions.
8 changes: 1 addition & 7 deletions api/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -249,19 +249,13 @@
"name": "options.method",
"optional": true,
"type": "string",
"description": "How to force into gamut. If \"clip\", coordinates are just clipped to their reference range (don't do that unless you have a reason, it produces very poor results). If in the form [colorSpaceId].[coordName], that coordinate is reduced until the color is in gamut. Please note that this may produce nonsensical results for certain coordinates (e.g. hue) or infinite loops if reducing the coordinate never brings the color in gamut. Defaults to `\"lch.c\"`."
"description": "How to force into gamut. If \"css\", chroma is reduced in the Oklch space using the algorithm defined by CSS Color 4. If \"clip\", coordinates are just clipped to their reference range (don't do that unless you have a reason, it produces very poor results). If in the form [colorSpaceId].[coordName], that coordinate is reduced until the color is in gamut. Please note that this may produce nonsensical results for certain coordinates (e.g. hue) or infinite loops if reducing the coordinate never brings the color in gamut. Defaults to `\"css\"`."
},
{
"name": "options.space",
"optional": true,
"type": "ColorSpace",
"description": "Color space whose gamut we are mapping to. Defaults to the current color space."
},
{
"name": "options.inPlace",
"optional": true,
"type": "boolean",
"description": "If true, modifies the current color instead of returning a new one. Defaults to false."
}
],
"returnType": "Color",
Expand Down
11 changes: 8 additions & 3 deletions docs/gamut-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,20 @@ sRGB_lime; // still out of gamut
```

Perhaps most important is the `method` parameter, which controls the algorithm used for gamut mapping.

The default method is `"css"`, which uses the binary search algorithm from [CSS Color Module Level 4](https://drafts.csswg.org/css-color/#css-gamut-mapping). The mapping is done in the Oklch space, and works by finding a chroma value where there is minimal difference between the mapped color and a clipped version. This difference is called the just noticeable difference, and is calculated in deltaEOK.

If the Oklch representation of the color has a lightness of less than or equal to 0, black is returned. Similarly, if the color has a lightness of greater than or equal to 1, white is returned.

You can pass `"clip"` to use simple clipping (not recommended), or any coordinate of any imported color space, which will make Color.js reduce that coordinate until the color is in gamut.

The default method is `"lch.c"` which means LCH hue and lightness remain constant while chroma is reduced until the color fits in gamut.
The default method before implementing the CSS Color 4 algorithm was `"lch.c"` which means LCH hue and lightness remain constant while chroma is reduced until the color fits in gamut.
Simply reducing chroma tends to produce good results for most colors, but most notably fails on yellows:

![chroma-reduction](images/p3-yellow-lab.svg)

Here is P3 yellow, with LCH Chroma reduced to the neutral axis. The RGB values are linear-light P3. The color wedge shows sRGB values, if in gamut; salmon, if outside sRGB and red if outside P3. Notice the red curve goes up (so, out of gamut) before finally dropping again. The chroma of P3 yellow is 123, while the chroma of the gamut-mapped result is far to low, only 25!
Here is P3 yellow, with LCH Chroma reduced to the neutral axis. The RGB values are linear-light P3. The color wedge shows sRGB values, if in gamut; salmon, if outside sRGB and red if outside P3. Notice the red curve goes up (so, out of gamut) before finally dropping again. The chroma of P3 yellow is 123, while the chroma of the gamut-mapped result is far too low, only 25!

Instead, the default algorithm reduces chroma (by binary search) and also, at each stage, calculates the deltaE2000 between the current estimate and a channel-clipped version of that color. If the deltaE is less than 2, the clipped color is displayed. Notice the red curve hugs the top edge now because clipping to sRGB also means it is inside P3 gamut. Notice how we get an in-gamut color much earlier. This method produces an in-gamut color with chroma 103.
Instead, the `"lch.c"` method reduces chroma (by binary search) and also, at each stage, calculates the deltaE2000 between the current estimate and a channel-clipped version of that color. If the deltaE is less than 2, the clipped color is displayed. Notice the red curve hugs the top edge now because clipping to sRGB also means it is inside P3 gamut. Notice how we get an in-gamut color much earlier. This method produces an in-gamut color with chroma 103.

![chroma-reduction-clip](images/p3-yellow-lab-clip.svg)
12 changes: 3 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,7 @@
"version": "0.4.5",
"description": "Let’s get serious about color",
"files": [
"dist/color.cjs",
"dist/color.cjs.map",
"dist/color.js",
"dist/color.js.map",
"dist/color.legacy.cjs",
"dist/color.legacy.cjs.map",
"dist/color.legacy.js",
"dist/color.legacy.js.map",
"dist/",
"src/",
"types/dist/",
"types/src/",
Expand All @@ -24,7 +17,8 @@
},
"./fn": {
"types": "./types/src/index-fn.d.ts",
"import": "./src/index-fn.js"
"import": "./src/index-fn.js",
"require": "./dist/color-fn.cjs"
},
"./dist/*": "./dist/*"
},
Expand Down
54 changes: 38 additions & 16 deletions rollup.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import terser from "@rollup/plugin-terser";

let bundles = [
const bundles = [
{
file: "dist/color.global.js",
format: "iife",
Expand All @@ -20,22 +20,44 @@ let bundles = [
},
];

const fnBundles = [
{
file: "dist/color-fn.cjs",
format: "cjs",
sourcemap: true,
exports: "named",
},
];

// Add minified versions of every bundle
bundles = bundles.flatMap((bundle) => {
let minBundle = Object.assign({}, bundle);
minBundle.file = minBundle.file.replace(/\.\w+$/, ".min$&");
minBundle.plugins ||= [];
minBundle.plugins.push(terser());
const addMinBundle = (bundles) => {
return bundles.flatMap((bundle) => {
const minBundle = Object.assign({}, bundle);
minBundle.file = minBundle.file.replace(/\.\w+$/, ".min$&");
minBundle.plugins ||= [];
minBundle.plugins.push(terser());

return [bundle, minBundle];
});
return [bundle, minBundle];
});
};

export default {
input: "src/index.js",
output: bundles,
onwarn(warning, rollupWarn) {
if (warning.code !== "CIRCULAR_DEPENDENCY") {
rollupWarn(warning);
}
export default [
{
input: "src/index.js",
output: addMinBundle(bundles),
onwarn(warning, rollupWarn) {
if (warning.code !== "CIRCULAR_DEPENDENCY") {
rollupWarn(warning);
}
},
},
};
{
input: "src/index-fn.js",
output: addMinBundle(fnBundles),
onwarn(warning, rollupWarn) {
if (warning.code !== "CIRCULAR_DEPENDENCY") {
rollupWarn(warning);
}
},
},
];
20 changes: 11 additions & 9 deletions rollup.legacy.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@ import { nodeResolve } from "@rollup/plugin-node-resolve";
import defaultConfig from "./rollup.config.js";

const legacyPlugins = [
commonjs(),
nodeResolve(),
commonjs({ strictRequires: true }),
nodeResolve({ ignoreSideEffectsForRoot: true }),
babel({ babelHelpers: "bundled", exclude: "node_modules/**" }),
];

export default Object.assign(defaultConfig, {
output: defaultConfig.output.map((bundle) => ({
...bundle,
file: bundle.file.replace(/\.(?:min\.)?\w+$/, ".legacy$&"),
})),
plugins: [...(defaultConfig.plugins || []), ...legacyPlugins],
});
export default defaultConfig.map((config) =>
Object.assign(config, {
output: config.output.map((bundle) => ({
...bundle,
file: bundle.file.replace(/\.(?:min\.)?\w+$/, ".legacy$&"),
})),
plugins: [...(config.plugins || []), ...legacyPlugins],
}),
);
2 changes: 1 addition & 1 deletion src/defaults.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Global defaults one may want to configure
export default {
gamut_mapping: "lch.c",
gamut_mapping: "css",
precision: 5,
deltaE: "76", // Default deltaE method
};
2 changes: 1 addition & 1 deletion src/index-fn.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export { default as to } from "./to.js";
export { default as serialize } from "./serialize.js";
export { default as display } from "./display.js";
export { default as inGamut } from "./inGamut.js";
export { default as toGamut } from "./toGamut.js";
export { default as toGamut, toGamutCSS } from "./toGamut.js";
export { default as distance } from "./distance.js";
export { default as equals } from "./equals.js";
export { default as contrast } from "./contrast.js";
Expand Down
2 changes: 1 addition & 1 deletion src/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const noneTypes = new Set(["<number>", "<percentage>", "<angle>"]);
* @param {string} str
* @param {object} [options]
* @param {object} [options.meta] - Object for additional information about the parsing
* @returns { Color }
* @returns {Color}
*/
export default function parse(str, { meta } = {}) {
let env = { str: String(str)?.trim() };
Expand Down
4 changes: 4 additions & 0 deletions src/space.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ export default class ColorSpace {
});
}

get isUnbounded() {
return Object.values(this.coords).every((coord) => !("range" in coord));
}

get cssId() {
return this.formats.functions?.color?.id || this.id;
}
Expand Down
2 changes: 1 addition & 1 deletion src/spaces/lab-d65.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default new ColorSpace({
coords: {
l: {
refRange: [0, 100],
name: "L",
name: "Lightness",
},
a: {
refRange: [-125, 125],
Expand Down
2 changes: 1 addition & 1 deletion src/spaces/lab.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default new ColorSpace({
coords: {
l: {
refRange: [0, 100],
name: "L",
name: "Lightness",
},
a: {
refRange: [-125, 125],
Expand Down
2 changes: 1 addition & 1 deletion src/spaces/oklab.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default new ColorSpace({
coords: {
l: {
refRange: [0, 1],
name: "L",
name: "Lightness",
},
a: {
refRange: [-0.4, 0.4],
Expand Down
Loading

0 comments on commit 6a6e7ee

Please sign in to comment.