@newtonedev/colors 0.0.1 → 1.0.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 +22 -23
- package/dist/__tests__/key-color.test.d.ts +2 -0
- package/dist/__tests__/key-color.test.d.ts.map +1 -0
- package/dist/config.d.ts +23 -11
- package/dist/config.d.ts.map +1 -1
- package/dist/contrast/apca.d.ts +16 -0
- package/dist/contrast/apca.d.ts.map +1 -1
- package/dist/gamut/max-chroma.d.ts +8 -3
- package/dist/gamut/max-chroma.d.ts.map +1 -1
- package/dist/index.cjs +74 -77
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +4 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +66 -70
- package/dist/index.js.map +1 -1
- package/dist/scale/generate.d.ts +44 -34
- package/dist/scale/generate.d.ts.map +1 -1
- package/dist/scale/hue-grade.d.ts +36 -29
- package/dist/scale/hue-grade.d.ts.map +1 -1
- package/dist/scale/key-color.d.ts +31 -27
- package/dist/scale/key-color.d.ts.map +1 -1
- package/dist/scale/resolve-color.d.ts +6 -26
- package/dist/scale/resolve-color.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,26 +33,25 @@ const hexValues = scale.map(oklchToHex);
|
|
|
33
33
|
|
|
34
34
|
| Option | Type | Default | Description |
|
|
35
35
|
|---|---|---|---|
|
|
36
|
+
| `contrast.light` | `number` | `1` | How light the lightest step is (0–1). 0 = near-white, 1 = pure white. |
|
|
37
|
+
| `contrast.dark` | `number` | `1` | How dark the darkest step is (0–1). 0 = near-black, 1 = pure black. |
|
|
36
38
|
| `hue` | `number` | — | Hue angle in degrees (0–360) |
|
|
37
|
-
| `
|
|
38
|
-
| `chroma.
|
|
39
|
-
| `
|
|
40
|
-
| `gamut` | `"srgb" \| "display-p3"` | `"srgb"` | Target gamut for chroma boundary. |
|
|
41
|
-
| `lightest` | `number` | `1` | Lightness of the lightest step. |
|
|
42
|
-
| `darkest` | `number` | `0` | Lightness of the darkest step. |
|
|
39
|
+
| `chroma.amount` | `number` | `0` | How much of the gamut to use (0–1). 0 = achromatic, 1 = maximum. |
|
|
40
|
+
| `chroma.balance` | `number` | `0.5` | How chroma is distributed along the scale (0 = toward lightest, 1 = toward darkest). |
|
|
41
|
+
| `isP3` | `boolean` | `false` | Use Display P3 gamut instead of sRGB. |
|
|
43
42
|
| `grading` | `Grading` | — | Global hue grading shared across all palettes. |
|
|
44
|
-
| `
|
|
43
|
+
| `shift` | `Shift` | — | Per-palette one-sided hue shift. Defaults to the dark end; set `light: true` for the light end. |
|
|
45
44
|
|
|
46
45
|
---
|
|
47
46
|
|
|
48
47
|
## Key color workflow
|
|
49
48
|
|
|
50
|
-
Derive scale parameters from a
|
|
49
|
+
Derive scale parameters from a known color, then locate it in the generated scale:
|
|
51
50
|
|
|
52
51
|
```ts
|
|
53
52
|
import { keyColor, generateScale, findNearest, oklchToHex } from "newtone-colors";
|
|
54
53
|
|
|
55
|
-
const key = keyColor("#3B82F6", {
|
|
54
|
+
const key = keyColor("#3B82F6", { contrast: { light: 0, dark: 0 } });
|
|
56
55
|
const scale = generateScale({ ...key, steps: 11 });
|
|
57
56
|
const { index } = findNearest(key.resolved.oklch, scale);
|
|
58
57
|
|
|
@@ -60,7 +59,7 @@ const { index } = findNearest(key.resolved.oklch, scale);
|
|
|
60
59
|
console.log(oklchToHex(scale[index]));
|
|
61
60
|
```
|
|
62
61
|
|
|
63
|
-
`keyColor` derives `hue` and `chroma` (
|
|
62
|
+
`keyColor` derives `hue` and `chroma` (amount + balance) from the color and returns them ready to spread into `ScaleOptions`. The `chroma.balance` is computed from where the color's lightness falls within the scale range, so the chroma distribution is centered around the key color's position.
|
|
64
63
|
|
|
65
64
|
The reverse direction — step to hex — is already covered by `oklchToHex(scale[index])`.
|
|
66
65
|
|
|
@@ -72,22 +71,22 @@ Shift hues at the light or dark end of the scale toward a target hue, using vect
|
|
|
72
71
|
|
|
73
72
|
```ts
|
|
74
73
|
import { generateScale } from "newtone-colors";
|
|
75
|
-
import type { Grading,
|
|
74
|
+
import type { Grading, Shift } from "newtone-colors";
|
|
76
75
|
|
|
77
76
|
// Global: affects all palettes equally
|
|
78
77
|
const grading: Grading = {
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
light: { hue: 145, amount: 0.15 }, // light end shifts toward 145°
|
|
79
|
+
dark: { hue: 25, amount: 0.15 }, // dark end shifts toward 25°
|
|
81
80
|
};
|
|
82
81
|
|
|
83
|
-
// Per-palette: one-sided, stronger
|
|
84
|
-
const
|
|
82
|
+
// Per-palette: one-sided, stronger. Omit `light` or set false for dark end.
|
|
83
|
+
const shift: Shift = {
|
|
85
84
|
hue: 200,
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
amount: 0.3,
|
|
86
|
+
// light: true ← uncomment to target the light end instead
|
|
88
87
|
};
|
|
89
88
|
|
|
90
|
-
const scale = generateScale({ hue: 264,
|
|
89
|
+
const scale = generateScale({ hue: 264, grading, shift });
|
|
91
90
|
```
|
|
92
91
|
|
|
93
92
|
---
|
|
@@ -99,10 +98,10 @@ Map slider values (0–1) to lightness bounds. The defaults keep the scale away
|
|
|
99
98
|
```ts
|
|
100
99
|
import { resolveLightest, resolveDarkest } from "newtone-colors";
|
|
101
100
|
|
|
102
|
-
const
|
|
103
|
-
const
|
|
101
|
+
const lightL = resolveLightest(0); // → 0.96 (MIN_LIGHTEST_L)
|
|
102
|
+
const darkL = resolveDarkest(0); // → 0.16 (MAX_DARKEST_L)
|
|
104
103
|
|
|
105
|
-
const scale = generateScale({ hue: 264, steps: 11
|
|
104
|
+
const scale = generateScale({ contrast: { light: 0, dark: 0 }, hue: 264, steps: 11 });
|
|
106
105
|
```
|
|
107
106
|
|
|
108
107
|
See [docs/constants.md](docs/constants.md) for all hardcoded values and their rationale.
|
|
@@ -145,9 +144,9 @@ Resolve any hex or OKLCH input to scale-ready parameters, with automatic gamut m
|
|
|
145
144
|
```ts
|
|
146
145
|
import { resolveColor } from "newtone-colors";
|
|
147
146
|
|
|
148
|
-
const result = resolveColor("#E74C3C"
|
|
147
|
+
const result = resolveColor("#E74C3C");
|
|
149
148
|
result.hue; // extracted hue
|
|
150
|
-
result.chromaRatio; // chroma as fraction of gamut boundary → ScaleOptions.chroma.
|
|
149
|
+
result.chromaRatio; // chroma as fraction of gamut boundary → ScaleOptions.chroma.amount
|
|
151
150
|
result.wasRemapped; // true if the color was outside the target gamut
|
|
152
151
|
result.original; // original OKLCH before mapping
|
|
153
152
|
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"key-color.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/key-color.test.ts"],"names":[],"mappings":""}
|
package/dist/config.d.ts
CHANGED
|
@@ -4,6 +4,18 @@
|
|
|
4
4
|
* Edit this file to adjust the perceptual behavior of every scale and
|
|
5
5
|
* grading operation across the library.
|
|
6
6
|
*/
|
|
7
|
+
/**
|
|
8
|
+
* Default number of steps in a generated scale.
|
|
9
|
+
* Fixed at 26 — enough for a full design token set with fine lightness
|
|
10
|
+
* increments without redundancy.
|
|
11
|
+
*/
|
|
12
|
+
export declare const DEFAULT_SCALE_STEPS = 26;
|
|
13
|
+
/**
|
|
14
|
+
* Default hue when none is provided.
|
|
15
|
+
* 0° is the start of the OKLCH hue wheel (red region). Arbitrary but
|
|
16
|
+
* deterministic — callers should always supply an explicit hue.
|
|
17
|
+
*/
|
|
18
|
+
export declare const DEFAULT_HUE = 0;
|
|
7
19
|
/**
|
|
8
20
|
* Lightest step's lightness when the whites slider is at 0.
|
|
9
21
|
* slider 0 → L = MIN_LIGHTEST_L, slider 1 → L = 1.0 (pure white).
|
|
@@ -15,20 +27,20 @@ export declare const MIN_LIGHTEST_L = 0.96;
|
|
|
15
27
|
*/
|
|
16
28
|
export declare const MAX_DARKEST_L = 0.16;
|
|
17
29
|
/**
|
|
18
|
-
* How far
|
|
19
|
-
* Light grading fades to zero at t =
|
|
20
|
-
* Dark grading fades to zero at t = 1 −
|
|
30
|
+
* How far global grading reaches into the scale from each end (0–1).
|
|
31
|
+
* Light grading fades to zero at t = GRADING_REACH.
|
|
32
|
+
* Dark grading fades to zero at t = 1 − GRADING_REACH.
|
|
21
33
|
*/
|
|
22
|
-
export declare const
|
|
23
|
-
/** Maximum
|
|
24
|
-
export declare const
|
|
34
|
+
export declare const GRADING_REACH: number;
|
|
35
|
+
/** Maximum amount for global grading. Inputs above this are clamped. */
|
|
36
|
+
export declare const MAX_GRADING_AMOUNT = 0.25;
|
|
25
37
|
/**
|
|
26
|
-
* How far
|
|
27
|
-
* Slightly wider than
|
|
38
|
+
* How far per-palette hue shift reaches into the scale from each end (0–1).
|
|
39
|
+
* Slightly wider than GRADING_REACH to give per-palette shifts more room.
|
|
28
40
|
*/
|
|
29
|
-
export declare const
|
|
30
|
-
/** Maximum
|
|
31
|
-
export declare const
|
|
41
|
+
export declare const SHIFT_REACH: number;
|
|
42
|
+
/** Maximum amount for per-palette hue shift. Inputs above this are clamped. */
|
|
43
|
+
export declare const MAX_SHIFT_AMOUNT = 0.5;
|
|
32
44
|
/**
|
|
33
45
|
* Just-noticeable difference threshold (deltaEOK).
|
|
34
46
|
* Colors with distance below this are considered perceptually identical.
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;;GAGG;AACH,eAAO,MAAM,cAAc,OAAO,CAAC;AAEnC;;;GAGG;AACH,eAAO,MAAM,aAAa,OAAO,CAAC;AAIlC;;;;GAIG;AACH,eAAO,MAAM,
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC;;;;GAIG;AACH,eAAO,MAAM,WAAW,IAAI,CAAC;AAI7B;;;GAGG;AACH,eAAO,MAAM,cAAc,OAAO,CAAC;AAEnC;;;GAGG;AACH,eAAO,MAAM,aAAa,OAAO,CAAC;AAIlC;;;;GAIG;AACH,eAAO,MAAM,aAAa,QAAQ,CAAC;AAEnC,wEAAwE;AACxE,eAAO,MAAM,kBAAkB,OAAO,CAAC;AAEvC;;;GAGG;AACH,eAAO,MAAM,WAAW,QAAQ,CAAC;AAEjC,+EAA+E;AAC/E,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAIpC;;;;GAIG;AACH,eAAO,MAAM,cAAc,OAAO,CAAC"}
|
package/dist/contrast/apca.d.ts
CHANGED
|
@@ -10,5 +10,21 @@ import type { Srgb } from "../types.js";
|
|
|
10
10
|
* IMPORTANT: Uses its own linearization (simple 2.4 gamma),
|
|
11
11
|
* NOT the IEC piecewise sRGB transfer function.
|
|
12
12
|
*/
|
|
13
|
+
/**
|
|
14
|
+
* Practical maximum APCA Lc magnitude used for normalization.
|
|
15
|
+
* BoW tops out at ~+106, WoB at ~-108; we use 108 as a symmetric ceiling.
|
|
16
|
+
*/
|
|
17
|
+
export declare const APCA_LC_MAX = 108;
|
|
18
|
+
/**
|
|
19
|
+
* Normalize a raw APCA Lc value to the range [-1, +1].
|
|
20
|
+
* +1 = maximum dark-on-light contrast (BoW)
|
|
21
|
+
* -1 = maximum light-on-dark contrast (WoB)
|
|
22
|
+
* 0 = no meaningful contrast
|
|
23
|
+
*/
|
|
24
|
+
export declare function apcaToNormalized(lc: number): number;
|
|
25
|
+
/**
|
|
26
|
+
* Convert a normalized contrast value [-1, +1] back to a raw APCA Lc value.
|
|
27
|
+
*/
|
|
28
|
+
export declare function normalizedToApca(normalized: number): number;
|
|
13
29
|
export declare function apcaContrast(textColor: Srgb, bgColor: Srgb): number;
|
|
14
30
|
//# sourceMappingURL=apca.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apca.d.ts","sourceRoot":"","sources":["../../src/contrast/apca.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAoBxC;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,GAAG,MAAM,CAmCnE"}
|
|
1
|
+
{"version":3,"file":"apca.d.ts","sourceRoot":"","sources":["../../src/contrast/apca.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAoBxC;;;;;;;;;;GAUG;AACH;;;GAGG;AACH,eAAO,MAAM,WAAW,MAAM,CAAC;AAE/B;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED,wBAAgB,YAAY,CAAC,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,GAAG,MAAM,CAmCnE"}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import type { Gamut } from "../types.js";
|
|
2
2
|
/**
|
|
3
|
-
* Find the maximum gamut chroma for a given OKLCH lightness and hue.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* Find the maximum in-gamut chroma for a given OKLCH lightness and hue.
|
|
4
|
+
*
|
|
5
|
+
* Binary search over [0, upperBound], converging to 1e-6 precision.
|
|
6
|
+
* Upper bounds are empirically safe ceilings — no in-gamut color at any
|
|
7
|
+
* lightness or hue exceeds C = 0.4 in sRGB or C = 0.5 in Display P3.
|
|
8
|
+
* Searching beyond these would waste iterations with no benefit.
|
|
9
|
+
*
|
|
10
|
+
* Returns 0 for L ≤ 0 or L ≥ 1 (pure black and white carry no chroma).
|
|
6
11
|
*/
|
|
7
12
|
export declare function maxChroma(L: number, h: number, gamut?: Gamut): number;
|
|
8
13
|
//# sourceMappingURL=max-chroma.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"max-chroma.d.ts","sourceRoot":"","sources":["../../src/gamut/max-chroma.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAS,MAAM,aAAa,CAAC;AAKhD
|
|
1
|
+
{"version":3,"file":"max-chroma.d.ts","sourceRoot":"","sources":["../../src/gamut/max-chroma.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAS,MAAM,aAAa,CAAC;AAKhD;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,GAAE,KAAc,GAAG,MAAM,CAsB7E"}
|
package/dist/index.cjs
CHANGED
|
@@ -304,6 +304,13 @@ function contrastTextHex(background) {
|
|
|
304
304
|
}
|
|
305
305
|
|
|
306
306
|
// src/contrast/apca.ts
|
|
307
|
+
var APCA_LC_MAX = 108;
|
|
308
|
+
function apcaToNormalized(lc) {
|
|
309
|
+
return Math.max(-1, Math.min(1, lc / APCA_LC_MAX));
|
|
310
|
+
}
|
|
311
|
+
function normalizedToApca(normalized) {
|
|
312
|
+
return normalized * APCA_LC_MAX;
|
|
313
|
+
}
|
|
307
314
|
function apcaContrast(textColor, bgColor) {
|
|
308
315
|
let txtY = APCA_SRGB_R * textColor.r ** APCA_MAIN_TRC + APCA_SRGB_G * textColor.g ** APCA_MAIN_TRC + APCA_SRGB_B * textColor.b ** APCA_MAIN_TRC;
|
|
309
316
|
let bgY = APCA_SRGB_R * bgColor.r ** APCA_MAIN_TRC + APCA_SRGB_G * bgColor.g ** APCA_MAIN_TRC + APCA_SRGB_B * bgColor.b ** APCA_MAIN_TRC;
|
|
@@ -345,19 +352,20 @@ function mix(a, b, t) {
|
|
|
345
352
|
}
|
|
346
353
|
|
|
347
354
|
// src/config.ts
|
|
355
|
+
var DEFAULT_SCALE_STEPS = 26;
|
|
356
|
+
var DEFAULT_HUE = 0;
|
|
348
357
|
var MIN_LIGHTEST_L = 0.96;
|
|
349
358
|
var MAX_DARKEST_L = 0.16;
|
|
350
|
-
var
|
|
351
|
-
var
|
|
352
|
-
var
|
|
353
|
-
var
|
|
359
|
+
var GRADING_REACH = 3 / 5;
|
|
360
|
+
var MAX_GRADING_AMOUNT = 0.25;
|
|
361
|
+
var SHIFT_REACH = 2 / 3;
|
|
362
|
+
var MAX_SHIFT_AMOUNT = 0.5;
|
|
354
363
|
var PERCEPTUAL_JND = 0.02;
|
|
355
364
|
|
|
356
365
|
// src/scale/hue-grade.ts
|
|
357
|
-
function gradeHue(baseHue, t, grade, reach =
|
|
358
|
-
const
|
|
359
|
-
const
|
|
360
|
-
const di = Math.max(0, Math.min(maxIntensity, darkValue));
|
|
366
|
+
function gradeHue(baseHue, t, grade, reach = GRADING_REACH, maxIntensity = MAX_GRADING_AMOUNT) {
|
|
367
|
+
const li = Math.max(0, Math.min(maxIntensity, grade.light?.amount ?? 0));
|
|
368
|
+
const di = Math.max(0, Math.min(maxIntensity, grade.dark?.amount ?? 0));
|
|
361
369
|
if (li === 0 && di === 0) return baseHue;
|
|
362
370
|
const lightInfluence = t <= reach ? 0.5 * (1 + Math.cos(Math.PI * t / reach)) : 0;
|
|
363
371
|
const darkInfluence = t >= 1 - reach ? 0.5 * (1 + Math.cos(Math.PI * (1 - t) / reach)) : 0;
|
|
@@ -369,13 +377,13 @@ function gradeHue(baseHue, t, grade, reach = GRADE_REACH, maxIntensity = MAX_GRA
|
|
|
369
377
|
let dy = 0;
|
|
370
378
|
if (lightInfluence > 0 && li > 0) {
|
|
371
379
|
const blend = lightInfluence * li;
|
|
372
|
-
const lRad =
|
|
380
|
+
const lRad = grade.light.hue * toRad;
|
|
373
381
|
dx += blend * (Math.cos(lRad) - bx);
|
|
374
382
|
dy += blend * (Math.sin(lRad) - by);
|
|
375
383
|
}
|
|
376
384
|
if (darkInfluence > 0 && di > 0) {
|
|
377
385
|
const blend = darkInfluence * di;
|
|
378
|
-
const dRad =
|
|
386
|
+
const dRad = grade.dark.hue * toRad;
|
|
379
387
|
dx += blend * (Math.cos(dRad) - bx);
|
|
380
388
|
dy += blend * (Math.sin(dRad) - by);
|
|
381
389
|
}
|
|
@@ -386,35 +394,49 @@ function gradeHue(baseHue, t, grade, reach = GRADE_REACH, maxIntensity = MAX_GRA
|
|
|
386
394
|
}
|
|
387
395
|
function resolveGradedHue(baseHue, t, globalGrade, localGrade) {
|
|
388
396
|
let h = baseHue;
|
|
397
|
+
if (localGrade) h = gradeHue(h, t, localGrade, SHIFT_REACH, MAX_SHIFT_AMOUNT);
|
|
389
398
|
if (globalGrade) h = gradeHue(h, t, globalGrade);
|
|
390
|
-
if (localGrade) h = gradeHue(h, t, localGrade, LOCAL_GRADE_REACH, MAX_LOCAL_GRADE_INTENSITY);
|
|
391
399
|
return h;
|
|
392
400
|
}
|
|
393
|
-
function buildOneSidedGrade(hue,
|
|
394
|
-
return
|
|
401
|
+
function buildOneSidedGrade(hue, amount, light = false) {
|
|
402
|
+
return light ? { light: { hue, amount } } : { dark: { hue, amount } };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// src/scale/dynamic-range.ts
|
|
406
|
+
function resolveLightest(slider) {
|
|
407
|
+
const s = Math.max(0, Math.min(1, slider));
|
|
408
|
+
return MIN_LIGHTEST_L + s * (1 - MIN_LIGHTEST_L);
|
|
409
|
+
}
|
|
410
|
+
function resolveDarkest(slider) {
|
|
411
|
+
const s = Math.max(0, Math.min(1, slider));
|
|
412
|
+
return MAX_DARKEST_L * (1 - s);
|
|
413
|
+
}
|
|
414
|
+
function lightnessToScaleT(L, lightestL, darkestL) {
|
|
415
|
+
const range = lightestL - darkestL;
|
|
416
|
+
if (range <= 0) return 0.5;
|
|
417
|
+
return Math.max(0, Math.min(1, (lightestL - L) / range));
|
|
395
418
|
}
|
|
396
419
|
|
|
397
420
|
// src/scale/generate.ts
|
|
398
|
-
function generateScale(options) {
|
|
421
|
+
function generateScale(options = {}) {
|
|
399
422
|
const {
|
|
400
|
-
|
|
401
|
-
|
|
423
|
+
contrast,
|
|
424
|
+
hue = DEFAULT_HUE,
|
|
402
425
|
chroma,
|
|
403
|
-
|
|
404
|
-
lightest = 1,
|
|
405
|
-
darkest = 0,
|
|
426
|
+
isP3 = false,
|
|
406
427
|
grading,
|
|
407
|
-
|
|
428
|
+
shift
|
|
408
429
|
} = options;
|
|
409
|
-
const
|
|
410
|
-
const
|
|
430
|
+
const gamut = isP3 ? "display-p3" : "srgb";
|
|
431
|
+
const steps = DEFAULT_SCALE_STEPS;
|
|
432
|
+
const chromaRatio = chroma?.amount ?? 0;
|
|
433
|
+
const chromaPeak = chroma?.balance ?? 0.5;
|
|
411
434
|
const hueGrade = grading;
|
|
412
|
-
const localHueGrade =
|
|
413
|
-
if (steps < 2) return [];
|
|
435
|
+
const localHueGrade = shift ? buildOneSidedGrade(shift.hue, shift.amount, shift.light) : void 0;
|
|
414
436
|
const ratio = Math.max(0, Math.min(1, chromaRatio));
|
|
415
437
|
const peak = Math.max(0, Math.min(1, chromaPeak));
|
|
416
|
-
const lightestL =
|
|
417
|
-
const darkestL =
|
|
438
|
+
const lightestL = resolveLightest(contrast?.light ?? 1);
|
|
439
|
+
const darkestL = resolveDarkest(contrast?.dark ?? 1);
|
|
418
440
|
const hueAt = (t) => resolveGradedHue(hue, t, hueGrade, localHueGrade);
|
|
419
441
|
if (peak === 0.5 || ratio === 0 || ratio >= 1) {
|
|
420
442
|
const scale2 = [];
|
|
@@ -422,7 +444,7 @@ function generateScale(options) {
|
|
|
422
444
|
const t = i / (steps - 1);
|
|
423
445
|
const L = lightestL - t * (lightestL - darkestL);
|
|
424
446
|
const h = hueAt(t);
|
|
425
|
-
const C = maxChroma(L,
|
|
447
|
+
const C = Math.min(maxChroma(L, hue, gamut) * ratio, maxChroma(L, h, gamut));
|
|
426
448
|
scale2.push({ L, C, h });
|
|
427
449
|
}
|
|
428
450
|
return scale2;
|
|
@@ -434,7 +456,7 @@ function generateScale(options) {
|
|
|
434
456
|
for (let i = 0; i <= N; i++) {
|
|
435
457
|
const t = i / N;
|
|
436
458
|
const L = lightestL - t * (lightestL - darkestL);
|
|
437
|
-
const C = maxChroma(L,
|
|
459
|
+
const C = maxChroma(L, hue, gamut);
|
|
438
460
|
boundarySamples.push({ t, C });
|
|
439
461
|
if (C > peakBoundaryC) {
|
|
440
462
|
peakBoundaryC = C;
|
|
@@ -471,7 +493,7 @@ function generateScale(options) {
|
|
|
471
493
|
tWarped = peakT + (t - targetT) * ((1 - peakT) / (1 - targetT));
|
|
472
494
|
}
|
|
473
495
|
const Lwarped = lightestL - tWarped * (lightestL - darkestL);
|
|
474
|
-
const warpedC = maxChroma(Lwarped,
|
|
496
|
+
const warpedC = maxChroma(Lwarped, hue, gamut) * ratio;
|
|
475
497
|
const boundaryC = maxChroma(L, h, gamut);
|
|
476
498
|
const C = Math.min(warpedC, boundaryC);
|
|
477
499
|
scale.push({ L, C, h });
|
|
@@ -480,7 +502,8 @@ function generateScale(options) {
|
|
|
480
502
|
}
|
|
481
503
|
|
|
482
504
|
// src/scale/resolve-color.ts
|
|
483
|
-
function resolveColor(input,
|
|
505
|
+
function resolveColor(input, isP3 = false) {
|
|
506
|
+
const gamut = isP3 ? "display-p3" : "srgb";
|
|
484
507
|
const isHex = typeof input === "string";
|
|
485
508
|
const original = isHex ? hexToOklch(input) : input;
|
|
486
509
|
let inGamut;
|
|
@@ -503,67 +526,43 @@ function resolveColor(input, gamut = "srgb") {
|
|
|
503
526
|
chromaRatio: Math.min(1, chromaRatio)
|
|
504
527
|
};
|
|
505
528
|
}
|
|
506
|
-
function findNearest(target, scale) {
|
|
507
|
-
if (scale.length === 0) {
|
|
508
|
-
throw new Error("findNearest: scale must not be empty");
|
|
509
|
-
}
|
|
510
|
-
let bestIndex = 0;
|
|
511
|
-
let bestDistance = Infinity;
|
|
512
|
-
for (let i = 0; i < scale.length; i++) {
|
|
513
|
-
const d = deltaEOK(target, scale[i]);
|
|
514
|
-
if (d < bestDistance) {
|
|
515
|
-
bestDistance = d;
|
|
516
|
-
bestIndex = i;
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
return {
|
|
520
|
-
index: bestIndex,
|
|
521
|
-
color: scale[bestIndex],
|
|
522
|
-
distance: bestDistance
|
|
523
|
-
};
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// src/scale/dynamic-range.ts
|
|
527
|
-
function resolveLightest(slider) {
|
|
528
|
-
const s = Math.max(0, Math.min(1, slider));
|
|
529
|
-
return MIN_LIGHTEST_L + s * (1 - MIN_LIGHTEST_L);
|
|
530
|
-
}
|
|
531
|
-
function resolveDarkest(slider) {
|
|
532
|
-
const s = Math.max(0, Math.min(1, slider));
|
|
533
|
-
return MAX_DARKEST_L * (1 - s);
|
|
534
|
-
}
|
|
535
|
-
function lightnessToScaleT(L, lightestL, darkestL) {
|
|
536
|
-
const range = lightestL - darkestL;
|
|
537
|
-
if (range <= 0) return 0.5;
|
|
538
|
-
return Math.max(0, Math.min(1, (lightestL - L) / range));
|
|
539
|
-
}
|
|
540
529
|
|
|
541
530
|
// src/scale/key-color.ts
|
|
542
531
|
function keyColor(color, options) {
|
|
543
|
-
const {
|
|
544
|
-
const resolved = resolveColor(color,
|
|
545
|
-
const
|
|
532
|
+
const { isP3 = false, contrast, steps = DEFAULT_SCALE_STEPS } = options ?? {};
|
|
533
|
+
const resolved = resolveColor(color, isP3);
|
|
534
|
+
const lightestL = resolveLightest(contrast?.light ?? 1);
|
|
535
|
+
const darkestL = resolveDarkest(contrast?.dark ?? 1);
|
|
536
|
+
const balance = lightnessToScaleT(resolved.oklch.L, lightestL, darkestL);
|
|
537
|
+
const stepIndex = Math.round(balance * Math.max(0, steps - 1));
|
|
546
538
|
return {
|
|
547
539
|
hue: resolved.hue,
|
|
548
|
-
chroma: {
|
|
549
|
-
|
|
540
|
+
chroma: { amount: resolved.chromaRatio, balance },
|
|
541
|
+
stepIndex,
|
|
542
|
+
hex: resolved.hex,
|
|
543
|
+
oklch: resolved.oklch,
|
|
544
|
+
wasRemapped: resolved.wasRemapped,
|
|
545
|
+
original: resolved.original
|
|
550
546
|
};
|
|
551
547
|
}
|
|
552
548
|
|
|
553
|
-
exports.
|
|
554
|
-
exports.
|
|
549
|
+
exports.APCA_LC_MAX = APCA_LC_MAX;
|
|
550
|
+
exports.DEFAULT_HUE = DEFAULT_HUE;
|
|
551
|
+
exports.DEFAULT_SCALE_STEPS = DEFAULT_SCALE_STEPS;
|
|
552
|
+
exports.GRADING_REACH = GRADING_REACH;
|
|
555
553
|
exports.MAX_DARKEST_L = MAX_DARKEST_L;
|
|
556
|
-
exports.
|
|
557
|
-
exports.
|
|
554
|
+
exports.MAX_GRADING_AMOUNT = MAX_GRADING_AMOUNT;
|
|
555
|
+
exports.MAX_SHIFT_AMOUNT = MAX_SHIFT_AMOUNT;
|
|
558
556
|
exports.MIN_LIGHTEST_L = MIN_LIGHTEST_L;
|
|
559
557
|
exports.PERCEPTUAL_JND = PERCEPTUAL_JND;
|
|
558
|
+
exports.SHIFT_REACH = SHIFT_REACH;
|
|
560
559
|
exports.apcaContrast = apcaContrast;
|
|
560
|
+
exports.apcaToNormalized = apcaToNormalized;
|
|
561
561
|
exports.buildOneSidedGrade = buildOneSidedGrade;
|
|
562
562
|
exports.clampSrgb = clampSrgb;
|
|
563
563
|
exports.contrastTextHex = contrastTextHex;
|
|
564
564
|
exports.deltaEOK = deltaEOK;
|
|
565
565
|
exports.deltaEOKLab = deltaEOKLab;
|
|
566
|
-
exports.findNearest = findNearest;
|
|
567
566
|
exports.gamutMap = gamutMap;
|
|
568
567
|
exports.generateScale = generateScale;
|
|
569
568
|
exports.gradeHue = gradeHue;
|
|
@@ -578,6 +577,7 @@ exports.linearSrgbToOklab = linearSrgbToOklab;
|
|
|
578
577
|
exports.linearSrgbToSrgb = linearSrgbToSrgb;
|
|
579
578
|
exports.maxChroma = maxChroma;
|
|
580
579
|
exports.mix = mix;
|
|
580
|
+
exports.normalizedToApca = normalizedToApca;
|
|
581
581
|
exports.oklabToLinearP3 = oklabToLinearP3;
|
|
582
582
|
exports.oklabToLinearSrgb = oklabToLinearSrgb;
|
|
583
583
|
exports.oklabToOklch = oklabToOklch;
|
|
@@ -587,10 +587,7 @@ exports.oklchToOklab = oklchToOklab;
|
|
|
587
587
|
exports.oklchToP3 = oklchToP3;
|
|
588
588
|
exports.oklchToSrgb = oklchToSrgb;
|
|
589
589
|
exports.p3ToOklch = p3ToOklch;
|
|
590
|
-
exports.resolveColor = resolveColor;
|
|
591
|
-
exports.resolveDarkest = resolveDarkest;
|
|
592
590
|
exports.resolveGradedHue = resolveGradedHue;
|
|
593
|
-
exports.resolveLightest = resolveLightest;
|
|
594
591
|
exports.srgbChannelToLinear = srgbChannelToLinear;
|
|
595
592
|
exports.srgbToHex = srgbToHex;
|
|
596
593
|
exports.srgbToLinearSrgb = srgbToLinearSrgb;
|