@omi-io/color-gamut 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -0
- package/dist/cjs/index.cjs +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/types/delta-e-ok.d.ts +2 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/is-in-gamut.d.ts +5 -0
- package/dist/types/map-to-gamut.d.ts +5 -0
- package/dist/types/max-chroma.d.ts +2 -0
- package/package.json +66 -0
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# `@omi-io/color-gamut`
|
|
2
|
+
|
|
3
|
+
Gamut queries and mapping for RGB display gamuts (`sRGB`, `Display P3`,
|
|
4
|
+
`Rec.2020`, …): is a color displayable, what is the closest displayable
|
|
5
|
+
fallback, and how much chroma survives at a given lightness and hue.
|
|
6
|
+
|
|
7
|
+
Conversions in `@omi-io/color-convert` never clamp — out-of-gamut input
|
|
8
|
+
yields out-of-gamut RGB. This package is the explicit opt-in layer on top:
|
|
9
|
+
the CSS Color 4 `css-gamut-map` algorithm (what browsers do for
|
|
10
|
+
`color(srgb …)` fallback, and what oklch.com shows as "closest fallback by
|
|
11
|
+
chroma"), a naive clip, and slider-bound helpers.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
yarn add @omi-io/color-gamut
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Example — sRGB fallback for an out-of-gamut Oklch color
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { unsafeAsOklch } from "@omi-io/color-models";
|
|
23
|
+
import { isOklchInGamut, mapToGamut, clipToGamut } from "@omi-io/color-gamut";
|
|
24
|
+
|
|
25
|
+
const vivid = unsafeAsOklch([0.35, 0.35, 150] as const);
|
|
26
|
+
|
|
27
|
+
isOklchInGamut(vivid, "sRGB"); // false — not displayable as-is
|
|
28
|
+
|
|
29
|
+
// CSS Color 4 gamut mapping (default strategy "css"):
|
|
30
|
+
mapToGamut(vivid, "sRGB"); // ~[0.3590, 0.1140, 144.68] — closest fallback
|
|
31
|
+
clipToGamut(vivid, "sRGB"); // encoded sRGB of the naive per-channel clamp
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## API
|
|
35
|
+
|
|
36
|
+
| Export | Signature | What it does |
|
|
37
|
+
| --- | --- | --- |
|
|
38
|
+
| `isInGamut` | `(rgb, space?) => boolean` | Channels of an encoded-RGB tuple (already in `space`) inside `[0, 1] ± GAMUT_EPSILON` |
|
|
39
|
+
| `isOklchInGamut` | `(oklch, space?) => boolean` | `oklchToRgb(…, space)` then the same channel check |
|
|
40
|
+
| `mapToGamut` | `(oklch, space?, strategy?) => Oklch` | Nearest displayable color; strategies `"css"` (default, CSS Color 4), `"clip"`, `"cusp"` (reserved, throws) |
|
|
41
|
+
| `clipToGamut` | `(oklch, space?) => RGB` | Naive per-channel clamp, encoded in `space` — ready to render |
|
|
42
|
+
| `maxChromaForLh` | `(L, h, space?) => number` | Largest displayable Oklch chroma at fixed lightness/hue (chroma slider bound) |
|
|
43
|
+
| `deltaEOK` | `(a: Oklab, b: Oklab) => number` | Euclidean distance in Oklab — the metric `css-gamut-map` requires (JND `0.02`) |
|
|
44
|
+
| `GAMUT_EPSILON` | `1e-6` | Boundary tolerance for encoded channels |
|
|
45
|
+
|
|
46
|
+
`space` is any D65 `RGBColorspaceId` and defaults to `"sRGB"`; non-D65
|
|
47
|
+
spaces (ACES/ACEScg) throw in the underlying Oklch conversions.
|
|
48
|
+
|
|
49
|
+
The `"css"` strategy is verified against colorjs.io (the CSS Color 4
|
|
50
|
+
reference implementation): max deviation on a 90-point grid is under
|
|
51
|
+
`0.0014` per sRGB channel. Note the spec returns the *clipped* converged
|
|
52
|
+
candidate, so the mapped result may differ from the input by up to the JND
|
|
53
|
+
(`0.02` deltaEOK) in lightness and hue as well, not only in chroma.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var I=Object.defineProperty;var A=Object.getOwnPropertyDescriptor;var S=Object.getOwnPropertyNames;var M=Object.prototype.hasOwnProperty;var N=(r,o)=>{for(var t in o)I(r,t,{get:o[t],enumerable:!0})},L=(r,o,t,n)=>{if(o&&typeof o=="object"||typeof o=="function")for(let e of S(o))!M.call(r,e)&&e!==t&&I(r,e,{get:()=>o[e],enumerable:!(n=A(o,e))||n.enumerable});return r};var _=r=>L(I({},"__esModule",{value:!0}),r);var D={};N(D,{GAMUT_EPSILON:()=>l,clipToGamut:()=>p,deltaEOK:()=>u,isInGamut:()=>R,isOklchInGamut:()=>c,mapToGamut:()=>T,maxChromaForLh:()=>g});module.exports=_(D);function u(r,o){return Math.hypot(r[0]-o[0],r[1]-o[1],r[2]-o[2])}var k=require("@omi-io/color-core"),y=require("@omi-io/color-convert"),l=1e-6;function R(r,o=k.DEFAULTS.rgbColorspace){return r[0]>=-l&&r[0]<=1+l&&r[1]>=-l&&r[1]<=1+l&&r[2]>=-l&&r[2]<=1+l}function c(r,o=k.DEFAULTS.rgbColorspace){return R((0,y.oklchToRgb)(r,o),o)}var s=require("@omi-io/color-core"),m=require("@omi-io/color-convert"),O=require("@omi-io/color-models");var d=.02,b=1e-4;function p(r,o=s.DEFAULTS.rgbColorspace){let t=(0,m.oklchToRgb)(r,o);return[(0,s.clampUnit)(t[0]),(0,s.clampUnit)(t[1]),(0,s.clampUnit)(t[2])]}function U(r,o){let[t,n,e]=r;if(t>=1)return(0,O.unsafeAsOklch)([1,0,0]);if(t<=0)return(0,O.unsafeAsOklch)([0,0,0]);if(c(r,o))return r;let i=0,C=n,E=!0,a=r,f=p(a,o);if(u((0,m.rgbToOklab)(f,o),(0,m.oklchToOklab)(a))<d)return(0,m.rgbToOklch)(f,o);for(;C-i>b;){let G=(i+C)/2;if(a=(0,O.unsafeAsOklch)([t,G,e]),E&&c(a,o)){i=G;continue}f=p(a,o);let x=u((0,m.rgbToOklab)(f,o),(0,m.oklchToOklab)(a));if(x<d){if(d-x<b)break;E=!1,i=G}else C=G}return(0,m.rgbToOklch)(p(a,o),o)}function T(r,o=s.DEFAULTS.rgbColorspace,t="css"){switch(t){case"css":return U(r,o);case"clip":return(0,m.rgbToOklch)(p(r,o),o);case"cusp":throw new Error('mapToGamut strategy "cusp" is not implemented')}}var B=require("@omi-io/color-core"),h=require("@omi-io/color-models");var w=.5,F=1e-7;function g(r,o,t=B.DEFAULTS.rgbColorspace){if(!c((0,h.unsafeAsOklch)([r,0,o]),t))return 0;let n=0,e=w;if(c((0,h.unsafeAsOklch)([r,e,o]),t))return e;for(;e-n>F;){let i=(n+e)/2;c((0,h.unsafeAsOklch)([r,i,o]),t)?n=i:e=i}return n}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function u(r,o){return Math.hypot(r[0]-o[0],r[1]-o[1],r[2]-o[2])}import{DEFAULTS as d}from"@omi-io/color-core";import{oklchToRgb as B}from"@omi-io/color-convert";var i=1e-6;function E(r,o=d.rgbColorspace){return r[0]>=-i&&r[0]<=1+i&&r[1]>=-i&&r[1]<=1+i&&r[2]>=-i&&r[2]<=1+i}function m(r,o=d.rgbColorspace){return E(B(r,o),o)}import{clampUnit as G,DEFAULTS as T}from"@omi-io/color-core";import{oklchToOklab as x,oklchToRgb as g,rgbToOklab as y,rgbToOklch as C}from"@omi-io/color-convert";import{unsafeAsOklch as O}from"@omi-io/color-models";var h=.02,b=1e-4;function a(r,o=T.rgbColorspace){let t=g(r,o);return[G(t[0]),G(t[1]),G(t[2])]}function A(r,o){let[t,l,n]=r;if(t>=1)return O([1,0,0]);if(t<=0)return O([0,0,0]);if(m(r,o))return r;let e=0,f=l,k=!0,c=r,s=a(c,o);if(u(y(s,o),x(c))<h)return C(s,o);for(;f-e>b;){let p=(e+f)/2;if(c=O([t,p,n]),k&&m(c,o)){e=p;continue}s=a(c,o);let R=u(y(s,o),x(c));if(R<h){if(h-R<b)break;k=!1,e=p}else f=p}return C(a(c,o),o)}function S(r,o=T.rgbColorspace,t="css"){switch(t){case"css":return A(r,o);case"clip":return C(a(r,o),o);case"cusp":throw new Error('mapToGamut strategy "cusp" is not implemented')}}import{DEFAULTS as M}from"@omi-io/color-core";import{unsafeAsOklch as I}from"@omi-io/color-models";var N=.5,L=1e-7;function _(r,o,t=M.rgbColorspace){if(!m(I([r,0,o]),t))return 0;let l=0,n=N;if(m(I([r,n,o]),t))return n;for(;n-l>L;){let e=(l+n)/2;m(I([r,e,o]),t)?l=e:n=e}return l}export{i as GAMUT_EPSILON,a as clipToGamut,u as deltaEOK,E as isInGamut,m as isOklchInGamut,S as mapToGamut,_ as maxChromaForLh};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { RGB, RGBColorspaceId } from "@omi-io/color-core/types";
|
|
2
|
+
import type { Oklch } from "@omi-io/color-models";
|
|
3
|
+
export declare const GAMUT_EPSILON = 0.000001;
|
|
4
|
+
export declare function isInGamut(rgb: RGB, _space?: RGBColorspaceId): boolean;
|
|
5
|
+
export declare function isOklchInGamut(oklch: Oklch, space?: RGBColorspaceId): boolean;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { RGB, RGBColorspaceId } from "@omi-io/color-core/types";
|
|
2
|
+
import { type Oklch } from "@omi-io/color-models";
|
|
3
|
+
export type GamutMapStrategy = "css" | "clip" | "cusp";
|
|
4
|
+
export declare function clipToGamut(oklch: Oklch, space?: RGBColorspaceId): RGB;
|
|
5
|
+
export declare function mapToGamut(oklch: Oklch, space?: RGBColorspaceId, strategy?: GamutMapStrategy): Oklch;
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@omi-io/color-gamut",
|
|
3
|
+
"scopeCommitName": "@:gamut",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Gamut queries and mapping for RGB display gamuts: in-gamut checks, CSS Color 4 gamut mapping, naive clipping, max chroma per (L, h), and the deltaEOK metric.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"color",
|
|
8
|
+
"gamut",
|
|
9
|
+
"gamut-mapping",
|
|
10
|
+
"css-color-4",
|
|
11
|
+
"oklch",
|
|
12
|
+
"oklab",
|
|
13
|
+
"delta-e",
|
|
14
|
+
"srgb",
|
|
15
|
+
"display-p3",
|
|
16
|
+
"rec2020",
|
|
17
|
+
"wide-gamut",
|
|
18
|
+
"colorimetry"
|
|
19
|
+
],
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/a-omi-io/color.git",
|
|
24
|
+
"directory": "packages/color-gamut"
|
|
25
|
+
},
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"type": "module",
|
|
31
|
+
"types": "./dist/types/index.d.ts",
|
|
32
|
+
"main": "./dist/cjs/index.cjs",
|
|
33
|
+
"module": "./dist/esm/index.js",
|
|
34
|
+
"scripts": {
|
|
35
|
+
"clean": "omi-io-pkg clean",
|
|
36
|
+
"build": "yarn clean && omi-io-pkg build && tsc && omi-io-pkg alias",
|
|
37
|
+
"test": "jest",
|
|
38
|
+
"lint": "eslint ./src",
|
|
39
|
+
"tsc-check": "tsc --noEmit --emitDeclarationOnly false"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@omi-io/color-convert": "^0.2.0",
|
|
43
|
+
"@omi-io/color-core": "^0.1.0",
|
|
44
|
+
"@omi-io/color-models": "^0.2.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@omi-io/pkg-scripts": "v2.5.0",
|
|
48
|
+
"@types/jest": "^29.5.0",
|
|
49
|
+
"@types/node": "^20.11.30",
|
|
50
|
+
"eslint": "^8.57.0",
|
|
51
|
+
"jest": "^29.5.0",
|
|
52
|
+
"ts-jest": "^29.0.5",
|
|
53
|
+
"typescript": "~5.4.3"
|
|
54
|
+
},
|
|
55
|
+
"exports": {
|
|
56
|
+
".": {
|
|
57
|
+
"types": "./dist/types/index.d.ts",
|
|
58
|
+
"require": "./dist/cjs/index.cjs",
|
|
59
|
+
"default": "./dist/esm/index.js"
|
|
60
|
+
},
|
|
61
|
+
"./package.json": "./package.json"
|
|
62
|
+
},
|
|
63
|
+
"files": [
|
|
64
|
+
"dist"
|
|
65
|
+
]
|
|
66
|
+
}
|