@oxyhq/bloom 0.36.0 → 0.37.1
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/lib/commonjs/theme/build-theme.js +31 -43
- package/lib/commonjs/theme/build-theme.js.map +1 -1
- package/lib/commonjs/theme/color-engine/cam16.js +161 -0
- package/lib/commonjs/theme/color-engine/cam16.js.map +1 -0
- package/lib/commonjs/theme/color-engine/color-role.js +183 -0
- package/lib/commonjs/theme/color-engine/color-role.js.map +1 -0
- package/lib/commonjs/theme/color-engine/color-roles.js +416 -0
- package/lib/commonjs/theme/color-engine/color-roles.js.map +1 -0
- package/lib/commonjs/theme/color-engine/color-utils.js +157 -0
- package/lib/commonjs/theme/color-engine/color-utils.js.map +1 -0
- package/lib/commonjs/theme/color-engine/contrast-curve.js +32 -0
- package/lib/commonjs/theme/color-engine/contrast-curve.js.map +1 -0
- package/lib/commonjs/theme/color-engine/contrast.js +70 -0
- package/lib/commonjs/theme/color-engine/contrast.js.map +1 -0
- package/lib/commonjs/theme/color-engine/dislike-analyzer.js +26 -0
- package/lib/commonjs/theme/color-engine/dislike-analyzer.js.map +1 -0
- package/lib/commonjs/theme/color-engine/dynamic-scheme.js +70 -0
- package/lib/commonjs/theme/color-engine/dynamic-scheme.js.map +1 -0
- package/lib/commonjs/theme/color-engine/hct-solver.js +236 -0
- package/lib/commonjs/theme/color-engine/hct-solver.js.map +1 -0
- package/lib/commonjs/theme/color-engine/hct.js +67 -0
- package/lib/commonjs/theme/color-engine/hct.js.map +1 -0
- package/lib/commonjs/theme/color-engine/index.js +173 -0
- package/lib/commonjs/theme/color-engine/index.js.map +1 -0
- package/lib/commonjs/theme/color-engine/lab-point-provider.js +32 -0
- package/lib/commonjs/theme/color-engine/lab-point-provider.js.map +1 -0
- package/lib/commonjs/theme/color-engine/math-utils.js +77 -0
- package/lib/commonjs/theme/color-engine/math-utils.js.map +1 -0
- package/lib/commonjs/theme/color-engine/quantizer-map.js +24 -0
- package/lib/commonjs/theme/color-engine/quantizer-map.js.map +1 -0
- package/lib/commonjs/theme/color-engine/quantizer-wu.js +266 -0
- package/lib/commonjs/theme/color-engine/quantizer-wu.js.map +1 -0
- package/lib/commonjs/theme/color-engine/scheme-variants.js +102 -0
- package/lib/commonjs/theme/color-engine/scheme-variants.js.map +1 -0
- package/lib/commonjs/theme/color-engine/score.js +80 -0
- package/lib/commonjs/theme/color-engine/score.js.map +1 -0
- package/lib/commonjs/theme/color-engine/seed-from-image.js +73 -0
- package/lib/commonjs/theme/color-engine/seed-from-image.js.map +1 -0
- package/lib/commonjs/theme/color-engine/tonal-palette.js +95 -0
- package/lib/commonjs/theme/color-engine/tonal-palette.js.map +1 -0
- package/lib/commonjs/theme/color-engine/tone-delta-pair.js +26 -0
- package/lib/commonjs/theme/color-engine/tone-delta-pair.js.map +1 -0
- package/lib/commonjs/theme/color-engine/variant.js +26 -0
- package/lib/commonjs/theme/color-engine/variant.js.map +1 -0
- package/lib/commonjs/theme/color-engine/viewing-conditions.js +61 -0
- package/lib/commonjs/theme/color-engine/viewing-conditions.js.map +1 -0
- package/lib/commonjs/theme/color-presets.js +44 -572
- package/lib/commonjs/theme/color-presets.js.map +1 -1
- package/lib/commonjs/theme/preset-vars.js +69 -49
- package/lib/commonjs/theme/preset-vars.js.map +1 -1
- package/lib/module/theme/build-theme.js +32 -44
- package/lib/module/theme/build-theme.js.map +1 -1
- package/lib/module/theme/color-engine/cam16.js +155 -0
- package/lib/module/theme/color-engine/cam16.js.map +1 -0
- package/lib/module/theme/color-engine/color-role.js +177 -0
- package/lib/module/theme/color-engine/color-role.js.map +1 -0
- package/lib/module/theme/color-engine/color-roles.js +411 -0
- package/lib/module/theme/color-engine/color-roles.js.map +1 -0
- package/lib/module/theme/color-engine/color-utils.js +135 -0
- package/lib/module/theme/color-engine/color-utils.js.map +1 -0
- package/lib/module/theme/color-engine/contrast-curve.js +26 -0
- package/lib/module/theme/color-engine/contrast-curve.js.map +1 -0
- package/lib/module/theme/color-engine/contrast.js +61 -0
- package/lib/module/theme/color-engine/contrast.js.map +1 -0
- package/lib/module/theme/color-engine/dislike-analyzer.js +20 -0
- package/lib/module/theme/color-engine/dislike-analyzer.js.map +1 -0
- package/lib/module/theme/color-engine/dynamic-scheme.js +65 -0
- package/lib/module/theme/color-engine/dynamic-scheme.js.map +1 -0
- package/lib/module/theme/color-engine/hct-solver.js +231 -0
- package/lib/module/theme/color-engine/hct-solver.js.map +1 -0
- package/lib/module/theme/color-engine/hct.js +61 -0
- package/lib/module/theme/color-engine/hct.js.map +1 -0
- package/lib/module/theme/color-engine/index.js +57 -0
- package/lib/module/theme/color-engine/index.js.map +1 -0
- package/lib/module/theme/color-engine/lab-point-provider.js +25 -0
- package/lib/module/theme/color-engine/lab-point-provider.js.map +1 -0
- package/lib/module/theme/color-engine/math-utils.js +65 -0
- package/lib/module/theme/color-engine/math-utils.js.map +1 -0
- package/lib/module/theme/color-engine/quantizer-map.js +20 -0
- package/lib/module/theme/color-engine/quantizer-map.js.map +1 -0
- package/lib/module/theme/color-engine/quantizer-wu.js +261 -0
- package/lib/module/theme/color-engine/quantizer-wu.js.map +1 -0
- package/lib/module/theme/color-engine/scheme-variants.js +93 -0
- package/lib/module/theme/color-engine/scheme-variants.js.map +1 -0
- package/lib/module/theme/color-engine/score.js +75 -0
- package/lib/module/theme/color-engine/score.js.map +1 -0
- package/lib/module/theme/color-engine/seed-from-image.js +67 -0
- package/lib/module/theme/color-engine/seed-from-image.js.map +1 -0
- package/lib/module/theme/color-engine/tonal-palette.js +89 -0
- package/lib/module/theme/color-engine/tonal-palette.js.map +1 -0
- package/lib/module/theme/color-engine/tone-delta-pair.js +21 -0
- package/lib/module/theme/color-engine/tone-delta-pair.js.map +1 -0
- package/lib/module/theme/color-engine/variant.js +22 -0
- package/lib/module/theme/color-engine/variant.js.map +1 -0
- package/lib/module/theme/color-engine/viewing-conditions.js +55 -0
- package/lib/module/theme/color-engine/viewing-conditions.js.map +1 -0
- package/lib/module/theme/color-presets.js +44 -572
- package/lib/module/theme/color-presets.js.map +1 -1
- package/lib/module/theme/preset-vars.js +69 -48
- package/lib/module/theme/preset-vars.js.map +1 -1
- package/lib/typescript/commonjs/theme/build-theme.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/color-engine/cam16.d.ts +26 -0
- package/lib/typescript/commonjs/theme/color-engine/cam16.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/color-engine/color-role.d.ts +36 -0
- package/lib/typescript/commonjs/theme/color-engine/color-role.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/color-engine/color-roles.d.ts +58 -0
- package/lib/typescript/commonjs/theme/color-engine/color-roles.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/color-engine/color-utils.d.ts +32 -0
- package/lib/typescript/commonjs/theme/color-engine/color-utils.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/color-engine/contrast-curve.d.ts +9 -0
- package/lib/typescript/commonjs/theme/color-engine/contrast-curve.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/color-engine/contrast.d.ts +12 -0
- package/lib/typescript/commonjs/theme/color-engine/contrast.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/color-engine/dislike-analyzer.d.ts +11 -0
- package/lib/typescript/commonjs/theme/color-engine/dislike-analyzer.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/color-engine/dynamic-scheme.d.ts +47 -0
- package/lib/typescript/commonjs/theme/color-engine/dynamic-scheme.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/color-engine/hct-solver.d.ts +6 -0
- package/lib/typescript/commonjs/theme/color-engine/hct-solver.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/color-engine/hct.d.ts +20 -0
- package/lib/typescript/commonjs/theme/color-engine/hct.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/color-engine/index.d.ts +44 -0
- package/lib/typescript/commonjs/theme/color-engine/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/color-engine/lab-point-provider.d.ts +6 -0
- package/lib/typescript/commonjs/theme/color-engine/lab-point-provider.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/color-engine/math-utils.d.ts +46 -0
- package/lib/typescript/commonjs/theme/color-engine/math-utils.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/color-engine/quantizer-map.d.ts +3 -0
- package/lib/typescript/commonjs/theme/color-engine/quantizer-map.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/color-engine/quantizer-wu.d.ts +3 -0
- package/lib/typescript/commonjs/theme/color-engine/quantizer-wu.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/color-engine/scheme-variants.d.ts +23 -0
- package/lib/typescript/commonjs/theme/color-engine/scheme-variants.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/color-engine/score.d.ts +8 -0
- package/lib/typescript/commonjs/theme/color-engine/score.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/color-engine/seed-from-image.d.ts +13 -0
- package/lib/typescript/commonjs/theme/color-engine/seed-from-image.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/color-engine/tonal-palette.d.ts +24 -0
- package/lib/typescript/commonjs/theme/color-engine/tonal-palette.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/color-engine/tone-delta-pair.d.ts +19 -0
- package/lib/typescript/commonjs/theme/color-engine/tone-delta-pair.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/color-engine/variant.d.ts +19 -0
- package/lib/typescript/commonjs/theme/color-engine/variant.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/color-engine/viewing-conditions.d.ts +18 -0
- package/lib/typescript/commonjs/theme/color-engine/viewing-conditions.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/color-presets.d.ts +25 -20
- package/lib/typescript/commonjs/theme/color-presets.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/preset-vars.d.ts +16 -12
- package/lib/typescript/commonjs/theme/preset-vars.d.ts.map +1 -1
- package/lib/typescript/module/theme/build-theme.d.ts.map +1 -1
- package/lib/typescript/module/theme/color-engine/cam16.d.ts +26 -0
- package/lib/typescript/module/theme/color-engine/cam16.d.ts.map +1 -0
- package/lib/typescript/module/theme/color-engine/color-role.d.ts +36 -0
- package/lib/typescript/module/theme/color-engine/color-role.d.ts.map +1 -0
- package/lib/typescript/module/theme/color-engine/color-roles.d.ts +58 -0
- package/lib/typescript/module/theme/color-engine/color-roles.d.ts.map +1 -0
- package/lib/typescript/module/theme/color-engine/color-utils.d.ts +32 -0
- package/lib/typescript/module/theme/color-engine/color-utils.d.ts.map +1 -0
- package/lib/typescript/module/theme/color-engine/contrast-curve.d.ts +9 -0
- package/lib/typescript/module/theme/color-engine/contrast-curve.d.ts.map +1 -0
- package/lib/typescript/module/theme/color-engine/contrast.d.ts +12 -0
- package/lib/typescript/module/theme/color-engine/contrast.d.ts.map +1 -0
- package/lib/typescript/module/theme/color-engine/dislike-analyzer.d.ts +11 -0
- package/lib/typescript/module/theme/color-engine/dislike-analyzer.d.ts.map +1 -0
- package/lib/typescript/module/theme/color-engine/dynamic-scheme.d.ts +47 -0
- package/lib/typescript/module/theme/color-engine/dynamic-scheme.d.ts.map +1 -0
- package/lib/typescript/module/theme/color-engine/hct-solver.d.ts +6 -0
- package/lib/typescript/module/theme/color-engine/hct-solver.d.ts.map +1 -0
- package/lib/typescript/module/theme/color-engine/hct.d.ts +20 -0
- package/lib/typescript/module/theme/color-engine/hct.d.ts.map +1 -0
- package/lib/typescript/module/theme/color-engine/index.d.ts +44 -0
- package/lib/typescript/module/theme/color-engine/index.d.ts.map +1 -0
- package/lib/typescript/module/theme/color-engine/lab-point-provider.d.ts +6 -0
- package/lib/typescript/module/theme/color-engine/lab-point-provider.d.ts.map +1 -0
- package/lib/typescript/module/theme/color-engine/math-utils.d.ts +46 -0
- package/lib/typescript/module/theme/color-engine/math-utils.d.ts.map +1 -0
- package/lib/typescript/module/theme/color-engine/quantizer-map.d.ts +3 -0
- package/lib/typescript/module/theme/color-engine/quantizer-map.d.ts.map +1 -0
- package/lib/typescript/module/theme/color-engine/quantizer-wu.d.ts +3 -0
- package/lib/typescript/module/theme/color-engine/quantizer-wu.d.ts.map +1 -0
- package/lib/typescript/module/theme/color-engine/scheme-variants.d.ts +23 -0
- package/lib/typescript/module/theme/color-engine/scheme-variants.d.ts.map +1 -0
- package/lib/typescript/module/theme/color-engine/score.d.ts +8 -0
- package/lib/typescript/module/theme/color-engine/score.d.ts.map +1 -0
- package/lib/typescript/module/theme/color-engine/seed-from-image.d.ts +13 -0
- package/lib/typescript/module/theme/color-engine/seed-from-image.d.ts.map +1 -0
- package/lib/typescript/module/theme/color-engine/tonal-palette.d.ts +24 -0
- package/lib/typescript/module/theme/color-engine/tonal-palette.d.ts.map +1 -0
- package/lib/typescript/module/theme/color-engine/tone-delta-pair.d.ts +19 -0
- package/lib/typescript/module/theme/color-engine/tone-delta-pair.d.ts.map +1 -0
- package/lib/typescript/module/theme/color-engine/variant.d.ts +19 -0
- package/lib/typescript/module/theme/color-engine/variant.d.ts.map +1 -0
- package/lib/typescript/module/theme/color-engine/viewing-conditions.d.ts +18 -0
- package/lib/typescript/module/theme/color-engine/viewing-conditions.d.ts.map +1 -0
- package/lib/typescript/module/theme/color-presets.d.ts +25 -20
- package/lib/typescript/module/theme/color-presets.d.ts.map +1 -1
- package/lib/typescript/module/theme/preset-vars.d.ts +16 -12
- package/lib/typescript/module/theme/preset-vars.d.ts.map +1 -1
- package/package.json +12 -1
- package/src/__tests__/BloomThemeProvider.test.tsx +9 -4
- package/src/__tests__/native-root-vars.test.ts +8 -4
- package/src/__tests__/preset-vars.test.ts +66 -37
- package/src/__tests__/theme.test.ts +20 -12
- package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +509 -509
- package/src/theme/__tests__/__snapshots__/visual-gallery.test.tsx.snap +613 -613
- package/src/theme/__tests__/color-engine-golden.test.ts +81 -0
- package/src/theme/__tests__/fixtures/color-engine-golden.json +1 -0
- package/src/theme/__tests__/golden-tokens.test.ts +6 -7
- package/src/theme/__tests__/runtime-contract.test.ts +8 -8
- package/src/theme/__tests__/theme-colors-parity.test.ts +44 -88
- package/src/theme/build-theme.ts +32 -44
- package/src/theme/color-engine/cam16.ts +179 -0
- package/src/theme/color-engine/color-role.ts +217 -0
- package/src/theme/color-engine/color-roles.ts +435 -0
- package/src/theme/color-engine/color-utils.ts +157 -0
- package/src/theme/color-engine/contrast-curve.ts +25 -0
- package/src/theme/color-engine/contrast.ts +59 -0
- package/src/theme/color-engine/dislike-analyzer.ts +19 -0
- package/src/theme/color-engine/dynamic-scheme.ts +90 -0
- package/src/theme/color-engine/hct-solver.ts +263 -0
- package/src/theme/color-engine/hct.ts +69 -0
- package/src/theme/color-engine/index.ts +73 -0
- package/src/theme/color-engine/lab-point-provider.ts +26 -0
- package/src/theme/color-engine/math-utils.ts +69 -0
- package/src/theme/color-engine/quantizer-map.ts +17 -0
- package/src/theme/color-engine/quantizer-wu.ts +320 -0
- package/src/theme/color-engine/scheme-variants.ts +113 -0
- package/src/theme/color-engine/score.ts +82 -0
- package/src/theme/color-engine/seed-from-image.ts +64 -0
- package/src/theme/color-engine/tonal-palette.ts +94 -0
- package/src/theme/color-engine/tone-delta-pair.ts +21 -0
- package/src/theme/color-engine/variant.ts +18 -0
- package/src/theme/color-engine/viewing-conditions.ts +77 -0
- package/src/theme/color-presets.ts +54 -631
- package/src/theme/preset-vars.ts +70 -51
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bloom colour engine — dynamic scheme.
|
|
3
|
+
*
|
|
4
|
+
* Faithful port of Material Color Utilities' `DynamicScheme` core (Apache-2.0).
|
|
5
|
+
* Holds the five tonal palettes (primary/secondary/tertiary/neutral/
|
|
6
|
+
* neutralVariant) + error that a scheme variant derives from the seed, plus the
|
|
7
|
+
* dark/contrast state a `DynamicColor` role reads to resolve into an ARGB. The
|
|
8
|
+
* ~50 convenience role getters from the reference are omitted — Bloom reads
|
|
9
|
+
* roles directly through `materialRoles`.
|
|
10
|
+
*/
|
|
11
|
+
import type { ColorRole } from './color-role';
|
|
12
|
+
import { Hct } from './hct';
|
|
13
|
+
import { sanitizeDegreesDouble } from './math-utils';
|
|
14
|
+
import { TonalPalette } from './tonal-palette';
|
|
15
|
+
import { Variant } from './variant';
|
|
16
|
+
|
|
17
|
+
export interface DynamicSchemeArgs {
|
|
18
|
+
sourceColorArgb: number;
|
|
19
|
+
variant: Variant;
|
|
20
|
+
contrastLevel: number;
|
|
21
|
+
isDark: boolean;
|
|
22
|
+
primaryPalette: TonalPalette;
|
|
23
|
+
secondaryPalette: TonalPalette;
|
|
24
|
+
tertiaryPalette: TonalPalette;
|
|
25
|
+
neutralPalette: TonalPalette;
|
|
26
|
+
neutralVariantPalette: TonalPalette;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class DynamicScheme {
|
|
30
|
+
readonly sourceColorArgb: number;
|
|
31
|
+
readonly variant: Variant;
|
|
32
|
+
readonly contrastLevel: number;
|
|
33
|
+
readonly isDark: boolean;
|
|
34
|
+
readonly sourceColorHct: Hct;
|
|
35
|
+
readonly primaryPalette: TonalPalette;
|
|
36
|
+
readonly secondaryPalette: TonalPalette;
|
|
37
|
+
readonly tertiaryPalette: TonalPalette;
|
|
38
|
+
readonly neutralPalette: TonalPalette;
|
|
39
|
+
readonly neutralVariantPalette: TonalPalette;
|
|
40
|
+
readonly errorPalette: TonalPalette;
|
|
41
|
+
|
|
42
|
+
constructor(args: DynamicSchemeArgs) {
|
|
43
|
+
this.sourceColorArgb = args.sourceColorArgb;
|
|
44
|
+
this.variant = args.variant;
|
|
45
|
+
this.contrastLevel = args.contrastLevel;
|
|
46
|
+
this.isDark = args.isDark;
|
|
47
|
+
this.sourceColorHct = Hct.fromInt(args.sourceColorArgb);
|
|
48
|
+
this.primaryPalette = args.primaryPalette;
|
|
49
|
+
this.secondaryPalette = args.secondaryPalette;
|
|
50
|
+
this.tertiaryPalette = args.tertiaryPalette;
|
|
51
|
+
this.neutralPalette = args.neutralPalette;
|
|
52
|
+
this.neutralVariantPalette = args.neutralVariantPalette;
|
|
53
|
+
this.errorPalette = TonalPalette.fromHueAndChroma(25.0, 84.0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Rotate the seed hue by a designer-specified amount that depends on which
|
|
58
|
+
* hue "breakpoint" band the seed falls in.
|
|
59
|
+
*/
|
|
60
|
+
static getRotatedHue(sourceColor: Hct, hues: number[], rotations: number[]): number {
|
|
61
|
+
const sourceHue = sourceColor.hue;
|
|
62
|
+
if (hues.length !== rotations.length) {
|
|
63
|
+
throw new Error(`mismatch between hue length ${hues.length} & rotations ${rotations.length}`);
|
|
64
|
+
}
|
|
65
|
+
if (rotations.length === 1) {
|
|
66
|
+
const firstRotation = rotations[0];
|
|
67
|
+
if (firstRotation === undefined) throw new Error('rotations[0] missing');
|
|
68
|
+
return sanitizeDegreesDouble(sourceColor.hue + firstRotation);
|
|
69
|
+
}
|
|
70
|
+
const size = hues.length;
|
|
71
|
+
for (let i = 0; i <= size - 2; i++) {
|
|
72
|
+
const thisHue = hues[i];
|
|
73
|
+
const nextHue = hues[i + 1];
|
|
74
|
+
const rotation = rotations[i];
|
|
75
|
+
if (thisHue === undefined || nextHue === undefined || rotation === undefined) continue;
|
|
76
|
+
if (thisHue < sourceHue && sourceHue < nextHue) {
|
|
77
|
+
return sanitizeDegreesDouble(sourceHue + rotation);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return sourceHue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
getArgb(role: ColorRole): number {
|
|
84
|
+
return role.getArgb(this);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getHct(role: ColorRole): Hct {
|
|
88
|
+
return role.getHct(this);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bloom colour engine — the HCT solver.
|
|
3
|
+
*
|
|
4
|
+
* Faithful port of Material Color Utilities' `HctSolver` (Apache-2.0) into
|
|
5
|
+
* Bloom's own code. Given a desired hue, chroma and tone (L*), it finds the
|
|
6
|
+
* closest sRGB colour — an analytic Newton solve with a geometric bisection
|
|
7
|
+
* fallback that maximises chroma when the exact colour is out of gamut. This is
|
|
8
|
+
* what makes tonal palettes possible. Precision-critical; verified by golden
|
|
9
|
+
* tests. Constants are the reference values verbatim.
|
|
10
|
+
*/
|
|
11
|
+
import { argbFromLinrgb, argbFromLstar, yFromLstar } from './color-utils';
|
|
12
|
+
import { matrixMultiply, sanitizeDegreesDouble, signum } from './math-utils';
|
|
13
|
+
import { ViewingConditions } from './viewing-conditions';
|
|
14
|
+
|
|
15
|
+
type Vec3 = [number, number, number];
|
|
16
|
+
type Mat3 = [Vec3, Vec3, Vec3];
|
|
17
|
+
|
|
18
|
+
const SCALED_DISCOUNT_FROM_LINRGB: Mat3 = [
|
|
19
|
+
[0.001200833568784504, 0.002389694492170889, 0.0002795742885861124],
|
|
20
|
+
[0.0005891086651375999, 0.0029785502573438758, 0.0003270666104008398],
|
|
21
|
+
[0.00010146692491640572, 0.0005364214359186694, 0.0032979401770712076],
|
|
22
|
+
];
|
|
23
|
+
const LINRGB_FROM_SCALED_DISCOUNT: Mat3 = [
|
|
24
|
+
[1373.2198709594231, -1100.4251190754821, -7.278681089101213],
|
|
25
|
+
[-271.815969077903, 559.6580465940733, -32.46047482791194],
|
|
26
|
+
[1.9622899599665666, -57.173814538844006, 308.7233197812385],
|
|
27
|
+
];
|
|
28
|
+
const Y_FROM_LINRGB: Vec3 = [0.2126, 0.7152, 0.0722];
|
|
29
|
+
|
|
30
|
+
// prettier-ignore
|
|
31
|
+
const CRITICAL_PLANES: number[] = [
|
|
32
|
+
0.015176349177441876, 0.045529047532325624, 0.07588174588720938, 0.10623444424209313, 0.13658714259697685, 0.16693984095186062, 0.19729253930674434, 0.2276452376616281, 0.2579979360165119, 0.28835063437139563, 0.3188300904430532, 0.350925934958123, 0.3848314933096426, 0.42057480301049466, 0.458183274052838, 0.4976837250274023, 0.5391024159806381, 0.5824650784040898, 0.6277969426914107, 0.6751227633498623, 0.7244668422128921, 0.775853049866786, 0.829304845476233, 0.8848452951698498, 0.942497089126609, 1.0022825574869039, 1.0642236851973577, 1.1283421258858297, 1.1946592148522128, 1.2631959812511864, 1.3339731595349034, 1.407011200216447, 1.4823302800086415, 1.5599503113873272, 1.6398909516233677, 1.7221716113234105, 1.8068114625156377, 1.8938294463134073, 1.9832442801866852, 2.075074464868551, 2.1693382909216234, 2.2660538449872063, 2.36523901573795, 2.4669114995532007, 2.5710888059345764, 2.6777882626779785, 2.7870270208169257, 2.898822059350997, 3.0131901897720907, 3.1301480604002863, 3.2497121605402226, 3.3718988244681087, 3.4967242352587946, 3.624204428461639, 3.754355295633311, 3.887192587735158, 4.022731918402185, 4.160988767090289, 4.301978482107941, 4.445716283538092, 4.592217266055746, 4.741496401646282, 4.893568542229298, 5.048448422192488, 5.20615066083972, 5.3666897647573375, 5.5300801301023865, 5.696336044816294, 5.865471690767354, 6.037501145825082, 6.212438385869475, 6.390297286737924, 6.571091626112461, 6.7548350853498045, 6.941541251256611, 7.131223617812143, 7.323895587840543, 7.5195704746346665, 7.7182615035334345, 7.919981813454504, 8.124744458384042, 8.332562408825165, 8.543448553206703, 8.757415699253682, 8.974476575321063, 9.194643831691977, 9.417930041841839, 9.644347703669503, 9.873909240696694, 10.106627003236781, 10.342513269534024, 10.58158024687427, 10.8238400726681, 11.069304815507364, 11.317986476196008, 11.569896988756009, 11.825048221409341, 12.083451977536606, 12.345119996613247, 12.610063955123938, 12.878295467455942, 13.149826086772048, 13.42466730586372, 13.702830557985108, 13.984327217668513, 14.269168601521828, 14.55736596900856, 14.848930523210871, 15.143873411576273, 15.44220572664832, 15.743938506781891, 16.04908273684337, 16.35764934889634, 16.66964922287304, 16.985093187232053, 17.30399201960269, 17.62635644741625, 17.95219714852476, 18.281524751807332, 18.614349837764564, 18.95068293910138, 19.290534541298456, 19.633915083172692, 19.98083495742689, 20.331304511189067, 20.685334046541502, 21.042933821039977, 21.404114048223256, 21.76888489811322, 22.137256497705877, 22.50923893145328, 22.884842241736916, 23.264076429332462, 23.6469514538663, 24.033477234264016, 24.42366364919083, 24.817520537484558, 25.21505769858089, 25.61628489293138, 26.021211842414342, 26.429848230738664, 26.842203703840827, 27.258287870275353, 27.678110301598522, 28.10168053274597, 28.529008062403893, 28.96010235337422, 29.39497283293396, 29.83362889318845, 30.276079891419332, 30.722335150426627, 31.172403958865512, 31.62629557157785, 32.08401920991837, 32.54558406207592, 33.010999283389665, 33.4802739966603, 33.953417292456834, 34.430438229418264, 34.911345834551085, 35.39614910352207, 35.88485700094671, 36.37747846067349, 36.87402238606382, 37.37449765026789, 37.87891309649659, 38.38727753828926, 38.89959975977785, 39.41588851594697, 39.93615253289054, 40.460400508064545, 40.98864111053629, 41.520882981230194, 42.05713473317016, 42.597404951718396, 43.141702194811224, 43.6900349931913, 44.24241185063697, 44.798841244188324, 45.35933162437017, 45.92389141541209, 46.49252901546552, 47.065252796817916, 47.64207110610409, 48.22299226451468, 48.808024568002054, 49.3971762874833, 49.9904556690408, 50.587870934119984, 51.189430279724725, 51.79514187861014, 52.40501387947288, 53.0190544071392, 53.637271562750364, 54.259673423945976, 54.88626804504493, 55.517063457223934, 56.15206766869424, 56.79128866487574, 57.43473440856916, 58.08241284012621, 58.734331877617365, 59.39049941699807, 60.05092333227251, 60.715611475655585, 61.38457167773311, 62.057811747619894, 62.7353394731159, 63.417162620860914, 64.10328893648692, 64.79372614476921, 65.48848194977529, 66.18756403501224, 66.89098006357258, 67.59873767827808, 68.31084450182222, 69.02730813691093, 69.74813616640164, 70.47333615344107, 71.20291564160104, 71.93688215501312, 72.67524319850172, 73.41800625771542, 74.16517879925733, 74.9167682708136, 75.67278210128072, 76.43322770089146, 77.1981124613393, 77.96744375590167, 78.74122893956174, 79.51947534912904, 80.30219030335869, 81.08938110306934, 81.88105503125999, 82.67721935322541, 83.4778813166706, 84.28304815182372, 85.09272707154808, 85.90692527145302, 86.72564993000343, 87.54890820862819, 88.3767072518277, 89.2090541872801, 90.04595612594655, 90.88742016217518, 91.73345337380438, 92.58406282226491, 93.43925555268066, 94.29903859396902, 95.16341895893969, 96.03240364439274, 96.9059996312159, 97.78421388448044, 98.6670533535366, 99.55452497210776,
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
function sanitizeRadians(angle: number): number {
|
|
36
|
+
return (angle + Math.PI * 8) % (Math.PI * 2);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function trueDelinearized(rgbComponent: number): number {
|
|
40
|
+
const normalized = rgbComponent / 100.0;
|
|
41
|
+
const delin =
|
|
42
|
+
normalized <= 0.0031308 ? normalized * 12.92 : 1.055 * Math.pow(normalized, 1.0 / 2.4) - 0.055;
|
|
43
|
+
return delin * 255.0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function chromaticAdaptation(component: number): number {
|
|
47
|
+
const af = Math.pow(Math.abs(component), 0.42);
|
|
48
|
+
return (signum(component) * 400.0 * af) / (af + 27.13);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function hueOf(linrgb: Vec3): number {
|
|
52
|
+
const scaledDiscount = matrixMultiply(linrgb, SCALED_DISCOUNT_FROM_LINRGB);
|
|
53
|
+
const rA = chromaticAdaptation(scaledDiscount[0]);
|
|
54
|
+
const gA = chromaticAdaptation(scaledDiscount[1]);
|
|
55
|
+
const bA = chromaticAdaptation(scaledDiscount[2]);
|
|
56
|
+
const a = (11.0 * rA + -12.0 * gA + bA) / 11.0;
|
|
57
|
+
const b = (rA + gA - 2.0 * bA) / 9.0;
|
|
58
|
+
return Math.atan2(b, a);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function areInCyclicOrder(a: number, b: number, c: number): boolean {
|
|
62
|
+
const deltaAB = sanitizeRadians(b - a);
|
|
63
|
+
const deltaAC = sanitizeRadians(c - a);
|
|
64
|
+
return deltaAB < deltaAC;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function intercept(source: number, mid: number, target: number): number {
|
|
68
|
+
return (mid - source) / (target - source);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function lerpPoint(source: Vec3, t: number, target: Vec3): Vec3 {
|
|
72
|
+
return [
|
|
73
|
+
source[0] + (target[0] - source[0]) * t,
|
|
74
|
+
source[1] + (target[1] - source[1]) * t,
|
|
75
|
+
source[2] + (target[2] - source[2]) * t,
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Read a Vec3 lane by a runtime axis index. The axis is always 0–2 in the
|
|
81
|
+
* callers below; this asserts that invariant honestly under
|
|
82
|
+
* `noUncheckedIndexedAccess` rather than masking it.
|
|
83
|
+
*/
|
|
84
|
+
function coord(v: Vec3, axis: number): number {
|
|
85
|
+
const value = v[axis];
|
|
86
|
+
if (value === undefined) throw new RangeError(`Vec3 axis ${axis} out of range`);
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function setCoordinate(source: Vec3, coordinate: number, target: Vec3, axis: number): Vec3 {
|
|
91
|
+
const t = intercept(coord(source, axis), coordinate, coord(target, axis));
|
|
92
|
+
return lerpPoint(source, t, target);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isBounded(x: number): boolean {
|
|
96
|
+
return 0.0 <= x && x <= 100.0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function nthVertex(y: number, n: number): Vec3 {
|
|
100
|
+
const kR = Y_FROM_LINRGB[0];
|
|
101
|
+
const kG = Y_FROM_LINRGB[1];
|
|
102
|
+
const kB = Y_FROM_LINRGB[2];
|
|
103
|
+
const coordA = n % 4 <= 1 ? 0.0 : 100.0;
|
|
104
|
+
const coordB = n % 2 === 0 ? 0.0 : 100.0;
|
|
105
|
+
if (n < 4) {
|
|
106
|
+
const g = coordA;
|
|
107
|
+
const b = coordB;
|
|
108
|
+
const r = (y - g * kG - b * kB) / kR;
|
|
109
|
+
return isBounded(r) ? [r, g, b] : [-1.0, -1.0, -1.0];
|
|
110
|
+
} else if (n < 8) {
|
|
111
|
+
const b = coordA;
|
|
112
|
+
const r = coordB;
|
|
113
|
+
const g = (y - r * kR - b * kB) / kG;
|
|
114
|
+
return isBounded(g) ? [r, g, b] : [-1.0, -1.0, -1.0];
|
|
115
|
+
} else {
|
|
116
|
+
const r = coordA;
|
|
117
|
+
const g = coordB;
|
|
118
|
+
const b = (y - r * kR - g * kG) / kB;
|
|
119
|
+
return isBounded(b) ? [r, g, b] : [-1.0, -1.0, -1.0];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function bisectToSegment(y: number, targetHue: number): [Vec3, Vec3] {
|
|
124
|
+
let left: Vec3 = [-1.0, -1.0, -1.0];
|
|
125
|
+
let right: Vec3 = left;
|
|
126
|
+
let leftHue = 0.0;
|
|
127
|
+
let rightHue = 0.0;
|
|
128
|
+
let initialized = false;
|
|
129
|
+
let uncut = true;
|
|
130
|
+
for (let n = 0; n < 12; n++) {
|
|
131
|
+
const mid = nthVertex(y, n);
|
|
132
|
+
if (mid[0] < 0) continue;
|
|
133
|
+
const midHue = hueOf(mid);
|
|
134
|
+
if (!initialized) {
|
|
135
|
+
left = mid;
|
|
136
|
+
right = mid;
|
|
137
|
+
leftHue = midHue;
|
|
138
|
+
rightHue = midHue;
|
|
139
|
+
initialized = true;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (uncut || areInCyclicOrder(leftHue, midHue, rightHue)) {
|
|
143
|
+
uncut = false;
|
|
144
|
+
if (areInCyclicOrder(leftHue, targetHue, midHue)) {
|
|
145
|
+
right = mid;
|
|
146
|
+
rightHue = midHue;
|
|
147
|
+
} else {
|
|
148
|
+
left = mid;
|
|
149
|
+
leftHue = midHue;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return [left, right];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function midpoint(a: Vec3, b: Vec3): Vec3 {
|
|
157
|
+
return [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const criticalPlaneBelow = (x: number): number => Math.floor(x - 0.5);
|
|
161
|
+
const criticalPlaneAbove = (x: number): number => Math.ceil(x - 0.5);
|
|
162
|
+
|
|
163
|
+
function bisectToLimit(y: number, targetHue: number): Vec3 {
|
|
164
|
+
const segment = bisectToSegment(y, targetHue);
|
|
165
|
+
let left = segment[0];
|
|
166
|
+
let leftHue = hueOf(left);
|
|
167
|
+
let right = segment[1];
|
|
168
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
169
|
+
const leftAxis = coord(left, axis);
|
|
170
|
+
const rightAxis = coord(right, axis);
|
|
171
|
+
if (leftAxis !== rightAxis) {
|
|
172
|
+
let lPlane = -1;
|
|
173
|
+
let rPlane = 255;
|
|
174
|
+
if (leftAxis < rightAxis) {
|
|
175
|
+
lPlane = criticalPlaneBelow(trueDelinearized(leftAxis));
|
|
176
|
+
rPlane = criticalPlaneAbove(trueDelinearized(rightAxis));
|
|
177
|
+
} else {
|
|
178
|
+
lPlane = criticalPlaneAbove(trueDelinearized(leftAxis));
|
|
179
|
+
rPlane = criticalPlaneBelow(trueDelinearized(rightAxis));
|
|
180
|
+
}
|
|
181
|
+
for (let i = 0; i < 8; i++) {
|
|
182
|
+
if (Math.abs(rPlane - lPlane) <= 1) break;
|
|
183
|
+
const mPlane = Math.floor((lPlane + rPlane) / 2.0);
|
|
184
|
+
const midPlaneCoordinate = CRITICAL_PLANES[mPlane];
|
|
185
|
+
if (midPlaneCoordinate === undefined) throw new RangeError(`critical plane ${mPlane} out of range`);
|
|
186
|
+
const mid = setCoordinate(left, midPlaneCoordinate, right, axis);
|
|
187
|
+
const midHue = hueOf(mid);
|
|
188
|
+
if (areInCyclicOrder(leftHue, targetHue, midHue)) {
|
|
189
|
+
right = mid;
|
|
190
|
+
rPlane = mPlane;
|
|
191
|
+
} else {
|
|
192
|
+
left = mid;
|
|
193
|
+
leftHue = midHue;
|
|
194
|
+
lPlane = mPlane;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return midpoint(left, right);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function inverseChromaticAdaptation(adapted: number): number {
|
|
203
|
+
const adaptedAbs = Math.abs(adapted);
|
|
204
|
+
const base = Math.max(0, (27.13 * adaptedAbs) / (400.0 - adaptedAbs));
|
|
205
|
+
return signum(adapted) * Math.pow(base, 1.0 / 0.42);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function findResultByJ(hueRadians: number, chroma: number, y: number): number {
|
|
209
|
+
let j = Math.sqrt(y) * 11.0;
|
|
210
|
+
// Read the CAM16 default viewing conditions directly (no magic constants).
|
|
211
|
+
const vc = ViewingConditions.DEFAULT;
|
|
212
|
+
const tInnerCoeff = 1 / Math.pow(1.64 - Math.pow(0.29, vc.n), 0.73);
|
|
213
|
+
const eHue = 0.25 * (Math.cos(hueRadians + 2.0) + 3.8);
|
|
214
|
+
const p1 = eHue * (50000.0 / 13.0) * vc.nc * vc.ncb;
|
|
215
|
+
const hSin = Math.sin(hueRadians);
|
|
216
|
+
const hCos = Math.cos(hueRadians);
|
|
217
|
+
for (let iterationRound = 0; iterationRound < 5; iterationRound++) {
|
|
218
|
+
const jNormalized = j / 100.0;
|
|
219
|
+
const alpha = chroma === 0.0 || j === 0.0 ? 0.0 : chroma / Math.sqrt(jNormalized);
|
|
220
|
+
const t = Math.pow(alpha * tInnerCoeff, 1.0 / 0.9);
|
|
221
|
+
const ac = vc.aw * Math.pow(jNormalized, 1.0 / vc.c / vc.z);
|
|
222
|
+
const p2 = ac / vc.nbb;
|
|
223
|
+
const gamma = (23.0 * (p2 + 0.305) * t) / (23.0 * p1 + 11 * t * hCos + 108.0 * t * hSin);
|
|
224
|
+
const a = gamma * hCos;
|
|
225
|
+
const b = gamma * hSin;
|
|
226
|
+
const rA = (460.0 * p2 + 451.0 * a + 288.0 * b) / 1403.0;
|
|
227
|
+
const gA = (460.0 * p2 - 891.0 * a - 261.0 * b) / 1403.0;
|
|
228
|
+
const bA = (460.0 * p2 - 220.0 * a - 6300.0 * b) / 1403.0;
|
|
229
|
+
const rCScaled = inverseChromaticAdaptation(rA);
|
|
230
|
+
const gCScaled = inverseChromaticAdaptation(gA);
|
|
231
|
+
const bCScaled = inverseChromaticAdaptation(bA);
|
|
232
|
+
const linrgb = matrixMultiply([rCScaled, gCScaled, bCScaled], LINRGB_FROM_SCALED_DISCOUNT);
|
|
233
|
+
if (linrgb[0] < 0 || linrgb[1] < 0 || linrgb[2] < 0) return 0;
|
|
234
|
+
const kR = Y_FROM_LINRGB[0];
|
|
235
|
+
const kG = Y_FROM_LINRGB[1];
|
|
236
|
+
const kB = Y_FROM_LINRGB[2];
|
|
237
|
+
const fnj = kR * linrgb[0] + kG * linrgb[1] + kB * linrgb[2];
|
|
238
|
+
if (fnj <= 0) return 0;
|
|
239
|
+
if (iterationRound === 4 || Math.abs(fnj - y) < 0.002) {
|
|
240
|
+
if (linrgb[0] > 100.01 || linrgb[1] > 100.01 || linrgb[2] > 100.01) return 0;
|
|
241
|
+
return argbFromLinrgb(linrgb);
|
|
242
|
+
}
|
|
243
|
+
j = j - ((fnj - y) * j) / (2 * fnj);
|
|
244
|
+
}
|
|
245
|
+
return 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Find the sRGB ARGB with the given hue (deg), chroma, and L*. When the exact
|
|
250
|
+
* colour is out of gamut, hue + L* are preserved and chroma is maximised.
|
|
251
|
+
*/
|
|
252
|
+
export function solveToInt(hueDegrees: number, chroma: number, lstar: number): number {
|
|
253
|
+
if (chroma < 0.0001 || lstar < 0.0001 || lstar > 99.9999) {
|
|
254
|
+
return argbFromLstar(lstar);
|
|
255
|
+
}
|
|
256
|
+
hueDegrees = sanitizeDegreesDouble(hueDegrees);
|
|
257
|
+
const hueRadians = (hueDegrees / 180) * Math.PI;
|
|
258
|
+
const y = yFromLstar(lstar);
|
|
259
|
+
const exactAnswer = findResultByJ(hueRadians, chroma, y);
|
|
260
|
+
if (exactAnswer !== 0) return exactAnswer;
|
|
261
|
+
const linrgb = bisectToLimit(y, hueRadians);
|
|
262
|
+
return argbFromLinrgb(linrgb);
|
|
263
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bloom colour engine — HCT (Hue · Chroma · Tone).
|
|
3
|
+
*
|
|
4
|
+
* Faithful port of Material Color Utilities' `Hct` (Apache-2.0) into Bloom's own
|
|
5
|
+
* code. HCT pairs CAM16 hue + chroma with L* as "tone", giving a perceptually
|
|
6
|
+
* even lightness axis: a tone difference of 40 guarantees a ≥3:1 contrast ratio,
|
|
7
|
+
* 50 guarantees ≥4.5:1. Every tonal palette and colour role is built on it.
|
|
8
|
+
*/
|
|
9
|
+
import { Cam16 } from './cam16';
|
|
10
|
+
import { lstarFromArgb } from './color-utils';
|
|
11
|
+
import { solveToInt } from './hct-solver';
|
|
12
|
+
|
|
13
|
+
export class Hct {
|
|
14
|
+
private argb: number;
|
|
15
|
+
private internalHue: number;
|
|
16
|
+
private internalChroma: number;
|
|
17
|
+
private internalTone: number;
|
|
18
|
+
|
|
19
|
+
private constructor(argb: number) {
|
|
20
|
+
this.argb = argb;
|
|
21
|
+
const cam = Cam16.fromInt(argb);
|
|
22
|
+
this.internalHue = cam.hue;
|
|
23
|
+
this.internalChroma = cam.chroma;
|
|
24
|
+
this.internalTone = lstarFromArgb(argb);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Build an HCT colour from hue (deg), chroma, and tone (L*, 0–100). */
|
|
28
|
+
static from(hue: number, chroma: number, tone: number): Hct {
|
|
29
|
+
return new Hct(solveToInt(hue, chroma, tone));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Wrap an existing ARGB colour as HCT. */
|
|
33
|
+
static fromInt(argb: number): Hct {
|
|
34
|
+
return new Hct(argb);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
toInt(): number {
|
|
38
|
+
return this.argb;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get hue(): number {
|
|
42
|
+
return this.internalHue;
|
|
43
|
+
}
|
|
44
|
+
set hue(newHue: number) {
|
|
45
|
+
this.setInternalState(solveToInt(newHue, this.internalChroma, this.internalTone));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get chroma(): number {
|
|
49
|
+
return this.internalChroma;
|
|
50
|
+
}
|
|
51
|
+
set chroma(newChroma: number) {
|
|
52
|
+
this.setInternalState(solveToInt(this.internalHue, newChroma, this.internalTone));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get tone(): number {
|
|
56
|
+
return this.internalTone;
|
|
57
|
+
}
|
|
58
|
+
set tone(newTone: number) {
|
|
59
|
+
this.setInternalState(solveToInt(this.internalHue, this.internalChroma, newTone));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private setInternalState(argb: number): void {
|
|
63
|
+
const cam = Cam16.fromInt(argb);
|
|
64
|
+
this.internalHue = cam.hue;
|
|
65
|
+
this.internalChroma = cam.chroma;
|
|
66
|
+
this.internalTone = lstarFromArgb(argb);
|
|
67
|
+
this.argb = argb;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bloom colour engine — public surface.
|
|
3
|
+
*
|
|
4
|
+
* A dependency-free tonal colour system (HCT + tonal palettes + dynamic roles)
|
|
5
|
+
* reimplemented in Bloom from the Material Color Utilities algorithm (Apache-2.0)
|
|
6
|
+
* and verified byte-for-byte against it. Give it ANY seed colour and it derives a
|
|
7
|
+
* full, self-consistent, WCAG-legible set of roles for light and dark at any
|
|
8
|
+
* contrast level — the same dynamic theming MaterialKolor does, but ours.
|
|
9
|
+
*/
|
|
10
|
+
export { Hct } from './hct';
|
|
11
|
+
export { TonalPalette } from './tonal-palette';
|
|
12
|
+
export { DynamicScheme } from './dynamic-scheme';
|
|
13
|
+
export { buildScheme, schemeVibrant, schemeExpressive, schemeTonalSpot, schemeNeutral } from './scheme-variants';
|
|
14
|
+
export type { SchemeVariant } from './scheme-variants';
|
|
15
|
+
export { Roles } from './color-roles';
|
|
16
|
+
export type { RoleName } from './color-roles';
|
|
17
|
+
export { ColorRole } from './color-role';
|
|
18
|
+
export { quantizeWu } from './quantizer-wu';
|
|
19
|
+
export { score } from './score';
|
|
20
|
+
export type { ScoreOptions } from './score';
|
|
21
|
+
export { quantizeImage, seedsFromImagePixels, seedHexFromImagePixels } from './seed-from-image';
|
|
22
|
+
export { argbFromRgb, redFromArgb, greenFromArgb, blueFromArgb } from './color-utils';
|
|
23
|
+
|
|
24
|
+
import { blueFromArgb, greenFromArgb, redFromArgb } from './color-utils';
|
|
25
|
+
import { Roles, type RoleName } from './color-roles';
|
|
26
|
+
import { Hct } from './hct';
|
|
27
|
+
import { buildScheme, type SchemeVariant } from './scheme-variants';
|
|
28
|
+
|
|
29
|
+
/** Parse a `#rrggbb` (or `#aarrggbb`) hex string into an ARGB int. */
|
|
30
|
+
export function argbFromHex(hex: string): number {
|
|
31
|
+
const h = hex.replace('#', '');
|
|
32
|
+
const v = h.length === 6 ? `ff${h}` : h;
|
|
33
|
+
return parseInt(v, 16) >>> 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Format an ARGB int as `#rrggbb`. */
|
|
37
|
+
export function hexFromArgb(argb: number): string {
|
|
38
|
+
const r = redFromArgb(argb).toString(16).padStart(2, '0');
|
|
39
|
+
const g = greenFromArgb(argb).toString(16).padStart(2, '0');
|
|
40
|
+
const b = blueFromArgb(argb).toString(16).padStart(2, '0');
|
|
41
|
+
return `#${r}${g}${b}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Every colour role as a modern `rgb(r g b)` string. */
|
|
45
|
+
export type RoleColors = Record<RoleName, string>;
|
|
46
|
+
|
|
47
|
+
export interface GenerateOptions {
|
|
48
|
+
/** The brand/seed colour, `#rrggbb`. */
|
|
49
|
+
seed: string;
|
|
50
|
+
variant?: SchemeVariant;
|
|
51
|
+
isDark?: boolean;
|
|
52
|
+
/** −1 (low) … 0 (normal) … 1 (high). */
|
|
53
|
+
contrastLevel?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Generate the full role set from ANY seed colour — the dynamic-theming entry
|
|
58
|
+
* point. Presets are just fixed seeds; a user-picked colour works identically.
|
|
59
|
+
*/
|
|
60
|
+
export function generateRoleColors(opts: GenerateOptions): RoleColors {
|
|
61
|
+
const scheme = buildScheme(
|
|
62
|
+
opts.variant ?? 'vibrant',
|
|
63
|
+
Hct.fromInt(argbFromHex(opts.seed)),
|
|
64
|
+
opts.isDark ?? false,
|
|
65
|
+
opts.contrastLevel ?? 0,
|
|
66
|
+
);
|
|
67
|
+
const out = {} as RoleColors;
|
|
68
|
+
for (const name of Object.keys(Roles) as RoleName[]) {
|
|
69
|
+
const argb = Roles[name].getArgb(scheme);
|
|
70
|
+
out[name] = `rgb(${redFromArgb(argb)} ${greenFromArgb(argb)} ${blueFromArgb(argb)})`;
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bloom colour engine — L*a*b* point provider.
|
|
3
|
+
*
|
|
4
|
+
* Faithful port of Material Color Utilities' `LabPointProvider` (Apache-2.0).
|
|
5
|
+
* Quantization clusters colours in L*a*b* (perceptually even), so pixels map to
|
|
6
|
+
* points and back through here, with squared-distance as the metric.
|
|
7
|
+
*/
|
|
8
|
+
import { argbFromLab, labFromArgb } from './color-utils';
|
|
9
|
+
|
|
10
|
+
export type LabPoint = [number, number, number];
|
|
11
|
+
|
|
12
|
+
export function labFromInt(argb: number): LabPoint {
|
|
13
|
+
return labFromArgb(argb);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function intFromLab(point: LabPoint): number {
|
|
17
|
+
return argbFromLab(point[0], point[1], point[2]);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Squared Euclidean distance in L*a*b* (cheaper than the real distance). */
|
|
21
|
+
export function labDistance(from: LabPoint, to: LabPoint): number {
|
|
22
|
+
const dL = from[0] - to[0];
|
|
23
|
+
const dA = from[1] - to[1];
|
|
24
|
+
const dB = from[2] - to[2];
|
|
25
|
+
return dL * dL + dA * dA + dB * dB;
|
|
26
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bloom colour engine — scalar/vector maths.
|
|
3
|
+
*
|
|
4
|
+
* Self-contained port of the Material Color Utilities maths helpers (the same
|
|
5
|
+
* primitives MaterialKolor uses), reimplemented in Bloom's own TypeScript so the
|
|
6
|
+
* theme has ZERO external colour dependencies. Algorithm © Google, Apache-2.0;
|
|
7
|
+
* this is a faithful derivative kept byte-compatible via golden tests.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** 1 if `n > 0`, -1 if `n < 0`, 0 if `n === 0`. */
|
|
11
|
+
export function signum(n: number): number {
|
|
12
|
+
return n < 0 ? -1 : n === 0 ? 0 : 1;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Linear interpolation: `start` at `amount = 0`, `stop` at `amount = 1`. */
|
|
16
|
+
export function lerp(start: number, stop: number, amount: number): number {
|
|
17
|
+
return (1.0 - amount) * start + amount * stop;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Clamp `input` into `[min, max]`. */
|
|
21
|
+
export function clampInt(min: number, max: number, input: number): number {
|
|
22
|
+
return input < min ? min : input > max ? max : input;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Clamp `input` into `[min, max]` (floating point). */
|
|
26
|
+
export function clampDouble(min: number, max: number, input: number): number {
|
|
27
|
+
return input < min ? min : input > max ? max : input;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Wrap a degree measure into `[0, 360)` as an integer. */
|
|
31
|
+
export function sanitizeDegreesInt(degrees: number): number {
|
|
32
|
+
degrees = degrees % 360;
|
|
33
|
+
return degrees < 0 ? degrees + 360 : degrees;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Wrap a degree measure into `[0, 360)` as a float. */
|
|
37
|
+
export function sanitizeDegreesDouble(degrees: number): number {
|
|
38
|
+
degrees = degrees % 360.0;
|
|
39
|
+
return degrees < 0 ? degrees + 360.0 : degrees;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Sign of the shortest rotation from `from` to `to` on a circle.
|
|
44
|
+
* @returns 1 if increasing `from` is shortest, -1 if decreasing is.
|
|
45
|
+
*/
|
|
46
|
+
export function rotationDirection(from: number, to: number): number {
|
|
47
|
+
const increasingDifference = sanitizeDegreesDouble(to - from);
|
|
48
|
+
return increasingDifference <= 180.0 ? 1.0 : -1.0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Shortest angular distance (degrees) between two hues. */
|
|
52
|
+
export function differenceDegrees(a: number, b: number): number {
|
|
53
|
+
return 180.0 - Math.abs(Math.abs(a - b) - 180.0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Multiply a 1×3 row vector by a 3×3 matrix. */
|
|
57
|
+
export function matrixMultiply(
|
|
58
|
+
row: [number, number, number],
|
|
59
|
+
matrix: [
|
|
60
|
+
[number, number, number],
|
|
61
|
+
[number, number, number],
|
|
62
|
+
[number, number, number],
|
|
63
|
+
],
|
|
64
|
+
): [number, number, number] {
|
|
65
|
+
const a = row[0] * matrix[0][0] + row[1] * matrix[0][1] + row[2] * matrix[0][2];
|
|
66
|
+
const b = row[0] * matrix[1][0] + row[1] * matrix[1][1] + row[2] * matrix[1][2];
|
|
67
|
+
const c = row[0] * matrix[2][0] + row[1] * matrix[2][1] + row[2] * matrix[2][2];
|
|
68
|
+
return [a, b, c];
|
|
69
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bloom colour engine — exact colour histogram.
|
|
3
|
+
*
|
|
4
|
+
* Faithful port of Material Color Utilities' `QuantizerMap` (Apache-2.0). Counts
|
|
5
|
+
* how many opaque pixels are each exact colour — the input to Wu quantization.
|
|
6
|
+
*/
|
|
7
|
+
import { alphaFromArgb } from './color-utils';
|
|
8
|
+
|
|
9
|
+
/** Count opaque pixels by exact ARGB. Fully-transparent pixels are dropped. */
|
|
10
|
+
export function countByColor(pixels: number[]): Map<number, number> {
|
|
11
|
+
const counts = new Map<number, number>();
|
|
12
|
+
for (const pixel of pixels) {
|
|
13
|
+
if (alphaFromArgb(pixel) < 255) continue;
|
|
14
|
+
counts.set(pixel, (counts.get(pixel) ?? 0) + 1);
|
|
15
|
+
}
|
|
16
|
+
return counts;
|
|
17
|
+
}
|