@ankhorage/color-theory 0.0.8 → 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/CHANGELOG.md +6 -0
- package/README.md +7 -57
- package/dist/contrast.d.ts +25 -0
- package/dist/contrast.d.ts.map +1 -1
- package/dist/contrast.js +74 -5
- package/dist/contrast.js.map +1 -1
- package/dist/harmony.d.ts.map +1 -1
- package/dist/harmony.js +30 -20
- package/dist/harmony.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/neutral.d.ts.map +1 -1
- package/dist/neutral.js +6 -12
- package/dist/neutral.js.map +1 -1
- package/dist/selection.d.ts +42 -0
- package/dist/selection.d.ts.map +1 -0
- package/dist/selection.js +146 -0
- package/dist/selection.js.map +1 -0
- package/dist/semantic-status.d.ts +6 -0
- package/dist/semantic-status.d.ts.map +1 -1
- package/dist/semantic-status.js +31 -13
- package/dist/semantic-status.js.map +1 -1
- package/dist/swatches.d.ts.map +1 -1
- package/dist/swatches.js +122 -77
- package/dist/swatches.js.map +1 -1
- package/package.json +9 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @ankhorage/color-theory
|
|
2
2
|
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- be8317b: Add public contrast measurement, caller-defined foreground selection, and deterministic swatch-step selection with perceptual and contrast diagnostics.
|
|
8
|
+
|
|
3
9
|
## 0.0.8
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
# @ankhorage/color-theory
|
|
5
5
|
|
|
6
|
-
         
|
|
7
7
|
|
|
8
8
|
Standalone color theory, harmony, swatch, contrast, and theme color generation utilities.
|
|
9
9
|
|
|
@@ -16,62 +16,12 @@ Standalone color theory, harmony, swatch, contrast, and theme color generation u
|
|
|
16
16
|
- [Module relationships](././paradox/diagrams/module-relationships.mmd)
|
|
17
17
|
- [Export graph](././paradox/diagrams/export-graph.mmd)
|
|
18
18
|
- [assertHexColor sequence](././paradox/diagrams/sequences/assert-hex-color.mmd)
|
|
19
|
+
- [createDefaultSemanticStatusSwatches sequence](././paradox/diagrams/sequences/create-default-semantic-status-swatches.mmd)
|
|
20
|
+
- [createSemanticStatusSwatches sequence](././paradox/diagrams/sequences/create-semantic-status-swatches.mmd)
|
|
21
|
+
- [generateColorSwatch sequence](././paradox/diagrams/sequences/generate-color-swatch.mmd)
|
|
22
|
+
- [getContrastRatio sequence](././paradox/diagrams/sequences/get-contrast-ratio.mmd)
|
|
19
23
|
- [getReadableForeground sequence](././paradox/diagrams/sequences/get-readable-foreground.mmd)
|
|
20
24
|
- [getThemeModePrimaryHex sequence](././paradox/diagrams/sequences/get-theme-mode-primary-hex.mmd)
|
|
21
25
|
- [parseHexColorOrThrow sequence](././paradox/diagrams/sequences/parse-hex-color-or-throw.mmd)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
<details>
|
|
26
|
-
<summary>Architecture overview</summary>
|
|
27
|
-
|
|
28
|
-
```mermaid
|
|
29
|
-
graph TD
|
|
30
|
-
package__ankhorage_color_theory["@ankhorage/color-theory"]
|
|
31
|
-
entrypoint_src_index_ts["src/index.ts"]
|
|
32
|
-
package__ankhorage_color_theory --> entrypoint_src_index_ts
|
|
33
|
-
module_src_contrast_ts["src/contrast.ts"]
|
|
34
|
-
package__ankhorage_color_theory -.-> module_src_contrast_ts
|
|
35
|
-
module_src_contrast_ts --> module_src_hex_ts
|
|
36
|
-
module_src_contrast_ts --> module_src_internal_culori_ts
|
|
37
|
-
module_src_culori_fn_ts["src/culori-fn.ts"]
|
|
38
|
-
package__ankhorage_color_theory -.-> module_src_culori_fn_ts
|
|
39
|
-
module_src_culori_ts["src/culori.ts"]
|
|
40
|
-
package__ankhorage_color_theory -.-> module_src_culori_ts
|
|
41
|
-
module_src_harmony_ts["src/harmony.ts"]
|
|
42
|
-
package__ankhorage_color_theory -.-> module_src_harmony_ts
|
|
43
|
-
module_src_harmony_ts --> module_src_hex_ts
|
|
44
|
-
module_src_harmony_ts --> module_src_internal_culori_ts
|
|
45
|
-
module_src_hex_ts["src/hex.ts"]
|
|
46
|
-
package__ankhorage_color_theory -.-> module_src_hex_ts
|
|
47
|
-
module_src_index_ts["src/index.ts"]
|
|
48
|
-
module_src_internal_culori_ts["src/internal-culori.ts"]
|
|
49
|
-
package__ankhorage_color_theory -.-> module_src_internal_culori_ts
|
|
50
|
-
module_src_internal_culori_ts --> module_src_hex_ts
|
|
51
|
-
module_src_neutral_ts["src/neutral.ts"]
|
|
52
|
-
package__ankhorage_color_theory -.-> module_src_neutral_ts
|
|
53
|
-
module_src_neutral_ts --> module_src_harmony_ts
|
|
54
|
-
module_src_neutral_ts --> module_src_hex_ts
|
|
55
|
-
module_src_neutral_ts --> module_src_internal_culori_ts
|
|
56
|
-
module_src_neutral_ts --> module_src_swatches_ts
|
|
57
|
-
module_src_semantic_status_ts["src/semantic-status.ts"]
|
|
58
|
-
package__ankhorage_color_theory -.-> module_src_semantic_status_ts
|
|
59
|
-
module_src_semantic_status_ts --> module_src_hex_ts
|
|
60
|
-
module_src_semantic_status_ts --> module_src_swatches_ts
|
|
61
|
-
module_src_semantics_ts["src/semantics.ts"]
|
|
62
|
-
package__ankhorage_color_theory -.-> module_src_semantics_ts
|
|
63
|
-
module_src_semantics_ts --> module_src_harmony_ts
|
|
64
|
-
module_src_semantics_ts --> module_src_swatches_ts
|
|
65
|
-
module_src_swatches_ts["src/swatches.ts"]
|
|
66
|
-
package__ankhorage_color_theory -.-> module_src_swatches_ts
|
|
67
|
-
module_src_swatches_ts --> module_src_hex_ts
|
|
68
|
-
module_src_swatches_ts --> module_src_internal_culori_ts
|
|
69
|
-
module_src_theme_colors_ts["src/theme-colors.ts"]
|
|
70
|
-
package__ankhorage_color_theory -.-> module_src_theme_colors_ts
|
|
71
|
-
module_src_theme_colors_ts --> module_src_harmony_ts
|
|
72
|
-
module_src_theme_colors_ts --> module_src_hex_ts
|
|
73
|
-
module_src_theme_colors_ts --> module_src_neutral_ts
|
|
74
|
-
module_src_theme_colors_ts --> module_src_swatches_ts
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
</details>
|
|
26
|
+
- [selectColorSwatchStep sequence](././paradox/diagrams/sequences/select-color-swatch-step.mmd)
|
|
27
|
+
- [selectReadableForeground sequence](././paradox/diagrams/sequences/select-readable-foreground.mmd)
|
package/dist/contrast.d.ts
CHANGED
|
@@ -3,6 +3,31 @@ export interface ReadableForegroundResult {
|
|
|
3
3
|
foreground: HexColor;
|
|
4
4
|
contrast: number;
|
|
5
5
|
}
|
|
6
|
+
export type ForegroundTiePolicy = 'first' | 'last';
|
|
7
|
+
export interface ForegroundSelectionCandidate {
|
|
8
|
+
foreground: HexColor;
|
|
9
|
+
contrast: number;
|
|
10
|
+
passesMinimum: boolean;
|
|
11
|
+
selected: boolean;
|
|
12
|
+
rejectionReason?: 'minimum-contrast' | 'weaker-contrast' | 'tie-break';
|
|
13
|
+
}
|
|
14
|
+
export interface ForegroundSelectionResult {
|
|
15
|
+
selected: ForegroundSelectionCandidate | null;
|
|
16
|
+
candidates: readonly ForegroundSelectionCandidate[];
|
|
17
|
+
minimumContrast: number;
|
|
18
|
+
tieBreak: {
|
|
19
|
+
policy: ForegroundTiePolicy;
|
|
20
|
+
applied: boolean;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/***
|
|
24
|
+
Measure the WCAG contrast ratio between two validated hex colors.
|
|
25
|
+
*/
|
|
26
|
+
export declare function getContrastRatio(colorA: HexColor, colorB: HexColor): number;
|
|
27
|
+
/***
|
|
28
|
+
Select the strongest caller-provided foreground that meets a minimum WCAG contrast ratio.
|
|
29
|
+
*/
|
|
30
|
+
export declare function selectReadableForeground(background: HexColor, foregrounds: readonly HexColor[], minimumContrast: number, tiePolicy: ForegroundTiePolicy): ForegroundSelectionResult;
|
|
6
31
|
/***
|
|
7
32
|
Return the readable black or white foreground color with the stronger contrast against a background color.
|
|
8
33
|
*/
|
package/dist/contrast.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contrast.d.ts","sourceRoot":"","sources":["../src/contrast.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAItC,MAAM,WAAW,wBAAwB;IACvC,UAAU,EAAE,QAAQ,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAKD;;EAEE;AACF,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,QAAQ,GAAG,wBAAwB,
|
|
1
|
+
{"version":3,"file":"contrast.d.ts","sourceRoot":"","sources":["../src/contrast.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAItC,MAAM,WAAW,wBAAwB;IACvC,UAAU,EAAE,QAAQ,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,mBAAmB,GAAG,OAAO,GAAG,MAAM,CAAC;AAEnD,MAAM,WAAW,4BAA4B;IAC3C,UAAU,EAAE,QAAQ,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB,eAAe,CAAC,EAAE,kBAAkB,GAAG,iBAAiB,GAAG,WAAW,CAAC;CACxE;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,EAAE,4BAA4B,GAAG,IAAI,CAAC;IAC9C,UAAU,EAAE,SAAS,4BAA4B,EAAE,CAAC;IACpD,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE;QACR,MAAM,EAAE,mBAAmB,CAAC;QAC5B,OAAO,EAAE,OAAO,CAAC;KAClB,CAAC;CACH;AAKD;;EAEE;AACF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,GAAG,MAAM,CAE3E;AAED;;EAEE;AACF,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,QAAQ,EACpB,WAAW,EAAE,SAAS,QAAQ,EAAE,EAChC,eAAe,EAAE,MAAM,EACvB,SAAS,EAAE,mBAAmB,GAC7B,yBAAyB,CA8C3B;AAED;;EAEE;AACF,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,QAAQ,GAAG,wBAAwB,CAUpF"}
|
package/dist/contrast.js
CHANGED
|
@@ -2,15 +2,84 @@ import { parseHexColorOrThrow } from './hex';
|
|
|
2
2
|
import { contrastRatio } from './internal-culori';
|
|
3
3
|
const BLACK = parseHexColorOrThrow('#000000');
|
|
4
4
|
const WHITE = parseHexColorOrThrow('#FFFFFF');
|
|
5
|
+
/***
|
|
6
|
+
Measure the WCAG contrast ratio between two validated hex colors.
|
|
7
|
+
*/
|
|
8
|
+
export function getContrastRatio(colorA, colorB) {
|
|
9
|
+
return contrastRatio(colorA, colorB);
|
|
10
|
+
}
|
|
11
|
+
/***
|
|
12
|
+
Select the strongest caller-provided foreground that meets a minimum WCAG contrast ratio.
|
|
13
|
+
*/
|
|
14
|
+
export function selectReadableForeground(background, foregrounds, minimumContrast, tiePolicy) {
|
|
15
|
+
assertMinimumContrast(minimumContrast);
|
|
16
|
+
if (foregrounds.length === 0) {
|
|
17
|
+
throw new Error('[color-theory] Foreground candidates must not be empty.');
|
|
18
|
+
}
|
|
19
|
+
const normalizedBackground = parseHexColorOrThrow(background);
|
|
20
|
+
const measured = foregrounds.map((foreground, index) => {
|
|
21
|
+
const normalizedForeground = parseHexColorOrThrow(foreground);
|
|
22
|
+
return {
|
|
23
|
+
foreground: normalizedForeground,
|
|
24
|
+
contrast: getContrastRatio(normalizedBackground, normalizedForeground),
|
|
25
|
+
index,
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
const passing = measured.filter(({ contrast }) => contrast >= minimumContrast);
|
|
29
|
+
const strongestContrast = Math.max(...passing.map(({ contrast }) => contrast));
|
|
30
|
+
const strongest = passing.filter(({ contrast }) => contrast === strongestContrast);
|
|
31
|
+
const selectedMeasured = tiePolicy === 'first' ? strongest.at(0) : strongest.at(-1);
|
|
32
|
+
const tieBreakApplied = strongest.length > 1;
|
|
33
|
+
const candidates = measured.map(({ foreground, contrast, index }) => {
|
|
34
|
+
const selected = selectedMeasured?.index === index;
|
|
35
|
+
return {
|
|
36
|
+
foreground,
|
|
37
|
+
contrast,
|
|
38
|
+
passesMinimum: contrast >= minimumContrast,
|
|
39
|
+
selected,
|
|
40
|
+
...(!selected
|
|
41
|
+
? {
|
|
42
|
+
rejectionReason: getForegroundRejectionReason(contrast, minimumContrast, strongestContrast),
|
|
43
|
+
}
|
|
44
|
+
: {}),
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
selected: candidates.find(({ selected }) => selected) ?? null,
|
|
49
|
+
candidates,
|
|
50
|
+
minimumContrast,
|
|
51
|
+
tieBreak: { policy: tiePolicy, applied: tieBreakApplied },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
5
54
|
/***
|
|
6
55
|
Return the readable black or white foreground color with the stronger contrast against a background color.
|
|
7
56
|
*/
|
|
8
57
|
export function getReadableForeground(background) {
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return { foreground: BLACK, contrast: contrastOnBlack };
|
|
58
|
+
const result = selectReadableForeground(background, [BLACK, WHITE], 1, 'first');
|
|
59
|
+
if (!result.selected) {
|
|
60
|
+
throw new Error('[color-theory] Expected black or white to be a readable foreground.');
|
|
13
61
|
}
|
|
14
|
-
return {
|
|
62
|
+
return {
|
|
63
|
+
foreground: result.selected.foreground,
|
|
64
|
+
contrast: result.selected.contrast,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/***
|
|
68
|
+
Validate a WCAG contrast threshold.
|
|
69
|
+
*/
|
|
70
|
+
function assertMinimumContrast(minimumContrast) {
|
|
71
|
+
if (!Number.isFinite(minimumContrast) || minimumContrast < 1 || minimumContrast > 21) {
|
|
72
|
+
throw new Error(`[color-theory] Minimum contrast must be a finite ratio from 1 through 21: ${minimumContrast}.`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/***
|
|
76
|
+
Explain why a foreground candidate was not selected.
|
|
77
|
+
*/
|
|
78
|
+
function getForegroundRejectionReason(contrast, minimumContrast, strongestContrast) {
|
|
79
|
+
if (contrast < minimumContrast)
|
|
80
|
+
return 'minimum-contrast';
|
|
81
|
+
if (contrast < strongestContrast)
|
|
82
|
+
return 'weaker-contrast';
|
|
83
|
+
return 'tie-break';
|
|
15
84
|
}
|
|
16
85
|
//# sourceMappingURL=contrast.js.map
|
package/dist/contrast.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contrast.js","sourceRoot":"","sources":["../src/contrast.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"contrast.js","sourceRoot":"","sources":["../src/contrast.ts"],"names":[],"mappings":"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","sourcesContent":["import type { HexColor } from './hex';\nimport { parseHexColorOrThrow } from './hex';\nimport { contrastRatio } from './internal-culori';\n\nexport interface ReadableForegroundResult {\n foreground: HexColor;\n contrast: number;\n}\n\nexport type ForegroundTiePolicy = 'first' | 'last';\n\nexport interface ForegroundSelectionCandidate {\n foreground: HexColor;\n contrast: number;\n passesMinimum: boolean;\n selected: boolean;\n rejectionReason?: 'minimum-contrast' | 'weaker-contrast' | 'tie-break';\n}\n\nexport interface ForegroundSelectionResult {\n selected: ForegroundSelectionCandidate | null;\n candidates: readonly ForegroundSelectionCandidate[];\n minimumContrast: number;\n tieBreak: {\n policy: ForegroundTiePolicy;\n applied: boolean;\n };\n}\n\nconst BLACK = parseHexColorOrThrow('#000000');\nconst WHITE = parseHexColorOrThrow('#FFFFFF');\n\n/***\n Measure the WCAG contrast ratio between two validated hex colors.\n*/\nexport function getContrastRatio(colorA: HexColor, colorB: HexColor): number {\n return contrastRatio(colorA, colorB);\n}\n\n/***\n Select the strongest caller-provided foreground that meets a minimum WCAG contrast ratio.\n*/\nexport function selectReadableForeground(\n background: HexColor,\n foregrounds: readonly HexColor[],\n minimumContrast: number,\n tiePolicy: ForegroundTiePolicy,\n): ForegroundSelectionResult {\n assertMinimumContrast(minimumContrast);\n if (foregrounds.length === 0) {\n throw new Error('[color-theory] Foreground candidates must not be empty.');\n }\n\n const normalizedBackground = parseHexColorOrThrow(background);\n const measured = foregrounds.map((foreground, index) => {\n const normalizedForeground = parseHexColorOrThrow(foreground);\n return {\n foreground: normalizedForeground,\n contrast: getContrastRatio(normalizedBackground, normalizedForeground),\n index,\n };\n });\n const passing = measured.filter(({ contrast }) => contrast >= minimumContrast);\n const strongestContrast = Math.max(...passing.map(({ contrast }) => contrast));\n const strongest = passing.filter(({ contrast }) => contrast === strongestContrast);\n const selectedMeasured = tiePolicy === 'first' ? strongest.at(0) : strongest.at(-1);\n const tieBreakApplied = strongest.length > 1;\n\n const candidates = measured.map(({ foreground, contrast, index }) => {\n const selected = selectedMeasured?.index === index;\n return {\n foreground,\n contrast,\n passesMinimum: contrast >= minimumContrast,\n selected,\n ...(!selected\n ? {\n rejectionReason: getForegroundRejectionReason(\n contrast,\n minimumContrast,\n strongestContrast,\n ),\n }\n : {}),\n } satisfies ForegroundSelectionCandidate;\n });\n\n return {\n selected: candidates.find(({ selected }) => selected) ?? null,\n candidates,\n minimumContrast,\n tieBreak: { policy: tiePolicy, applied: tieBreakApplied },\n };\n}\n\n/***\n Return the readable black or white foreground color with the stronger contrast against a background color.\n*/\nexport function getReadableForeground(background: HexColor): ReadableForegroundResult {\n const result = selectReadableForeground(background, [BLACK, WHITE], 1, 'first');\n if (!result.selected) {\n throw new Error('[color-theory] Expected black or white to be a readable foreground.');\n }\n\n return {\n foreground: result.selected.foreground,\n contrast: result.selected.contrast,\n };\n}\n\n/***\n Validate a WCAG contrast threshold.\n*/\nfunction assertMinimumContrast(minimumContrast: number): void {\n if (!Number.isFinite(minimumContrast) || minimumContrast < 1 || minimumContrast > 21) {\n throw new Error(\n `[color-theory] Minimum contrast must be a finite ratio from 1 through 21: ${minimumContrast}.`,\n );\n }\n}\n\n/***\n Explain why a foreground candidate was not selected.\n*/\nfunction getForegroundRejectionReason(\n contrast: number,\n minimumContrast: number,\n strongestContrast: number,\n): NonNullable<ForegroundSelectionCandidate['rejectionReason']> {\n if (contrast < minimumContrast) return 'minimum-contrast';\n if (contrast < strongestContrast) return 'weaker-contrast';\n return 'tie-break';\n}\n"]}
|
package/dist/harmony.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"harmony.d.ts","sourceRoot":"","sources":["../src/harmony.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAGtC,eAAO,MAAM,eAAe,uGAOlB,CAAC;AAEX,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAE5D,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,WAAW,GAAG,UAAU,GAAG,YAAY,CAAC;AAErF,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,kBAAkB,CAAC;IACzB,GAAG,EAAE,QAAQ,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,UAAU,GAAG,WAAW,CAAC;CAClC;AAED,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,YAAY,CAAC;IACtB,MAAM,EAAE,SAAS,yBAAyB,EAAE,CAAC;IAC7C,OAAO,EAAE,yBAAyB,CAAC;IACnC,SAAS,CAAC,EAAE,yBAAyB,CAAC;IACtC,QAAQ,CAAC,EAAE,yBAAyB,CAAC;IACrC,UAAU,CAAC,EAAE,yBAAyB,CAAC;CACxC;
|
|
1
|
+
{"version":3,"file":"harmony.d.ts","sourceRoot":"","sources":["../src/harmony.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAGtC,eAAO,MAAM,eAAe,uGAOlB,CAAC;AAEX,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAE5D,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,WAAW,GAAG,UAAU,GAAG,YAAY,CAAC;AAErF,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,kBAAkB,CAAC;IACzB,GAAG,EAAE,QAAQ,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,UAAU,GAAG,WAAW,CAAC;CAClC;AAED,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,YAAY,CAAC;IACtB,MAAM,EAAE,SAAS,yBAAyB,EAAE,CAAC;IAC7C,OAAO,EAAE,yBAAyB,CAAC;IACnC,SAAS,CAAC,EAAE,yBAAyB,CAAC;IACtC,QAAQ,CAAC,EAAE,yBAAyB,CAAC;IACrC,UAAU,CAAC,EAAE,yBAAyB,CAAC;CACxC;AAmCD;;EAEE;AACF,wBAAgB,yBAAyB,CACvC,YAAY,EAAE,QAAQ,EACtB,OAAO,EAAE,YAAY,GACpB,0BAA0B,CAqC5B"}
|
package/dist/harmony.js
CHANGED
|
@@ -7,36 +7,46 @@ export const COLOR_HARMONIES = [
|
|
|
7
7
|
'tetradic',
|
|
8
8
|
'splitComplementary',
|
|
9
9
|
];
|
|
10
|
-
const ROLE_ORDER_BY_HARMONY =
|
|
11
|
-
monochromatic
|
|
12
|
-
complementary
|
|
13
|
-
analogous
|
|
14
|
-
splitComplementary
|
|
15
|
-
triadic
|
|
16
|
-
tetradic
|
|
17
|
-
|
|
18
|
-
const OFFSETS_BY_HARMONY =
|
|
19
|
-
monochromatic
|
|
20
|
-
analogous
|
|
21
|
-
complementary
|
|
22
|
-
splitComplementary
|
|
23
|
-
triadic
|
|
24
|
-
tetradic
|
|
25
|
-
|
|
10
|
+
const ROLE_ORDER_BY_HARMONY = new Map([
|
|
11
|
+
['monochromatic', ['primary']],
|
|
12
|
+
['complementary', ['primary', 'secondary']],
|
|
13
|
+
['analogous', ['primary', 'secondary', 'tertiary']],
|
|
14
|
+
['splitComplementary', ['primary', 'secondary', 'tertiary']],
|
|
15
|
+
['triadic', ['primary', 'secondary', 'tertiary']],
|
|
16
|
+
['tetradic', ['primary', 'secondary', 'tertiary', 'quaternary']],
|
|
17
|
+
]);
|
|
18
|
+
const OFFSETS_BY_HARMONY = new Map([
|
|
19
|
+
['monochromatic', [0]],
|
|
20
|
+
['analogous', [0, -30, 30]],
|
|
21
|
+
['complementary', [0, 180]],
|
|
22
|
+
['splitComplementary', [0, 150, 210]],
|
|
23
|
+
['triadic', [0, 120, 240]],
|
|
24
|
+
['tetradic', [0, 90, 180, 270]],
|
|
25
|
+
]);
|
|
26
|
+
/***
|
|
27
|
+
Resolve a complete harmony definition from the canonical maps.
|
|
28
|
+
*/
|
|
29
|
+
function getHarmonyDefinition(harmony) {
|
|
30
|
+
const roles = ROLE_ORDER_BY_HARMONY.get(harmony);
|
|
31
|
+
const offsets = OFFSETS_BY_HARMONY.get(harmony);
|
|
32
|
+
if (!roles || !offsets) {
|
|
33
|
+
throw new Error(`[color-theory] Missing definition for harmony ${harmony}.`);
|
|
34
|
+
}
|
|
35
|
+
return { offsets, roles };
|
|
36
|
+
}
|
|
26
37
|
/***
|
|
27
38
|
Generate role-based harmony colors from a primary color and harmony strategy.
|
|
28
39
|
*/
|
|
29
40
|
export function generateHarmonyRoleColors(primaryColor, harmony) {
|
|
30
41
|
const base = parseHexToOklch(primaryColor);
|
|
31
|
-
const roles =
|
|
32
|
-
const offsets = OFFSETS_BY_HARMONY[harmony];
|
|
42
|
+
const { offsets, roles } = getHarmonyDefinition(harmony);
|
|
33
43
|
const baseHue = normalizeHueDegrees(base.h);
|
|
34
44
|
const colors = [];
|
|
35
45
|
for (let index = 0; index < roles.length; index++) {
|
|
36
|
-
const role = roles
|
|
46
|
+
const role = roles.at(index);
|
|
37
47
|
if (!role)
|
|
38
48
|
continue;
|
|
39
|
-
const hueDegrees = normalizeHueDegrees(baseHue + (offsets
|
|
49
|
+
const hueDegrees = normalizeHueDegrees(baseHue + (offsets.at(index) ?? 0));
|
|
40
50
|
colors.push({
|
|
41
51
|
role,
|
|
42
52
|
hex: role === 'primary' ? primaryColor : oklchToHex({ ...base, h: hueDegrees }),
|
package/dist/harmony.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"harmony.js","sourceRoot":"","sources":["../src/harmony.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAErF,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,eAAe;IACf,WAAW;IACX,eAAe;IACf,SAAS;IACT,UAAU;IACV,oBAAoB;CACZ,CAAC;AAsBX,MAAM,qBAAqB,
|
|
1
|
+
{"version":3,"file":"harmony.js","sourceRoot":"","sources":["../src/harmony.ts"],"names":[],"mappings":"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","sourcesContent":["import type { HexColor } from './hex';\nimport { normalizeHueDegrees, oklchToHex, parseHexToOklch } from './internal-culori';\n\nexport const COLOR_HARMONIES = [\n 'monochromatic',\n 'analogous',\n 'complementary',\n 'triadic',\n 'tetradic',\n 'splitComplementary',\n] as const;\n\nexport type ColorHarmony = (typeof COLOR_HARMONIES)[number];\n\nexport type GeneratedColorRole = 'primary' | 'secondary' | 'tertiary' | 'quaternary';\n\nexport interface GeneratedHarmonyRoleColor {\n role: GeneratedColorRole;\n hex: HexColor;\n hueDegrees: number;\n source: 'selected' | 'generated';\n}\n\nexport interface GeneratedHarmonyRoleColors {\n harmony: ColorHarmony;\n colors: readonly GeneratedHarmonyRoleColor[];\n primary: GeneratedHarmonyRoleColor;\n secondary?: GeneratedHarmonyRoleColor;\n tertiary?: GeneratedHarmonyRoleColor;\n quaternary?: GeneratedHarmonyRoleColor;\n}\n\nconst ROLE_ORDER_BY_HARMONY = new Map<ColorHarmony, readonly GeneratedColorRole[]>([\n ['monochromatic', ['primary']],\n ['complementary', ['primary', 'secondary']],\n ['analogous', ['primary', 'secondary', 'tertiary']],\n ['splitComplementary', ['primary', 'secondary', 'tertiary']],\n ['triadic', ['primary', 'secondary', 'tertiary']],\n ['tetradic', ['primary', 'secondary', 'tertiary', 'quaternary']],\n]);\n\nconst OFFSETS_BY_HARMONY = new Map<ColorHarmony, readonly number[]>([\n ['monochromatic', [0]],\n ['analogous', [0, -30, 30]],\n ['complementary', [0, 180]],\n ['splitComplementary', [0, 150, 210]],\n ['triadic', [0, 120, 240]],\n ['tetradic', [0, 90, 180, 270]],\n]);\n\n/***\n Resolve a complete harmony definition from the canonical maps.\n*/\nfunction getHarmonyDefinition(harmony: ColorHarmony): {\n readonly offsets: readonly number[];\n readonly roles: readonly GeneratedColorRole[];\n} {\n const roles = ROLE_ORDER_BY_HARMONY.get(harmony);\n const offsets = OFFSETS_BY_HARMONY.get(harmony);\n if (!roles || !offsets) {\n throw new Error(`[color-theory] Missing definition for harmony ${harmony}.`);\n }\n return { offsets, roles };\n}\n\n/***\n Generate role-based harmony colors from a primary color and harmony strategy.\n*/\nexport function generateHarmonyRoleColors(\n primaryColor: HexColor,\n harmony: ColorHarmony,\n): GeneratedHarmonyRoleColors {\n const base = parseHexToOklch(primaryColor);\n const { offsets, roles } = getHarmonyDefinition(harmony);\n const baseHue = normalizeHueDegrees(base.h);\n\n const colors: GeneratedHarmonyRoleColor[] = [];\n\n for (let index = 0; index < roles.length; index++) {\n const role = roles.at(index);\n if (!role) continue;\n\n const hueDegrees = normalizeHueDegrees(baseHue + (offsets.at(index) ?? 0));\n colors.push({\n role,\n hex: role === 'primary' ? primaryColor : oklchToHex({ ...base, h: hueDegrees }),\n hueDegrees,\n source: role === 'primary' ? 'selected' : 'generated',\n });\n }\n\n const primary = colors.find((color) => color.role === 'primary');\n if (!primary) {\n throw new Error('[color-theory] Expected generated harmony role colors to include primary.');\n }\n\n const secondary = colors.find((color) => color.role === 'secondary');\n const tertiary = colors.find((color) => color.role === 'tertiary');\n const quaternary = colors.find((color) => color.role === 'quaternary');\n\n return {\n harmony,\n colors,\n primary,\n ...(secondary ? { secondary } : {}),\n ...(tertiary ? { tertiary } : {}),\n ...(quaternary ? { quaternary } : {}),\n };\n}\n"]}
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,OAAO,CAAC;AACtB,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,OAAO,CAAC;AACtB,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,OAAO,CAAC;AACtB,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC","sourcesContent":["export * from './contrast';\nexport * from './harmony';\nexport * from './hex';\nexport * from './neutral';\nexport * from './semantic-status';\nexport * from './semantics';\nexport * from './swatches';\nexport * from './theme-colors';\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,OAAO,CAAC;AACtB,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC","sourcesContent":["export * from './contrast';\nexport * from './harmony';\nexport * from './hex';\nexport * from './neutral';\nexport * from './selection';\nexport * from './semantic-status';\nexport * from './semantics';\nexport * from './swatches';\nexport * from './theme-colors';\n"]}
|
package/dist/neutral.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"neutral.d.ts","sourceRoot":"","sources":["../src/neutral.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAgB,0BAA0B,EAAE,MAAM,WAAW,CAAC;AAC1E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEtC,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,sBAAsB,EAE5B,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,iBAAiB,QAAQ,CAAC;AAEvC,MAAM,WAAW,mBAAmB;IAClC,eAAe,EAAE,QAAQ,CAAC;IAC1B,OAAO,EAAE,WAAW,CAAC;IACrB,WAAW,EAAE,sBAAsB,CAAC;CACrC;AAED,MAAM,WAAW,wBAAwB;IACvC,eAAe,EAAE,QAAQ,CAAC;IAC1B,WAAW,EAAE,sBAAsB,CAAC;CACrC;
|
|
1
|
+
{"version":3,"file":"neutral.d.ts","sourceRoot":"","sources":["../src/neutral.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAgB,0BAA0B,EAAE,MAAM,WAAW,CAAC;AAC1E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEtC,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,sBAAsB,EAE5B,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,iBAAiB,QAAQ,CAAC;AAEvC,MAAM,WAAW,mBAAmB;IAClC,eAAe,EAAE,QAAQ,CAAC;IAC1B,OAAO,EAAE,WAAW,CAAC;IACrB,WAAW,EAAE,sBAAsB,CAAC;CACrC;AAED,MAAM,WAAW,wBAAwB;IACvC,eAAe,EAAE,QAAQ,CAAC;IAC1B,WAAW,EAAE,sBAAsB,CAAC;CACrC;AAwBD;;EAEE;AACF,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,0BAA0B,GAAG,mBAAmB,CAuBjG"}
|
package/dist/neutral.js
CHANGED
|
@@ -5,18 +5,12 @@ export const MIN_HUEFUL_CHROMA = 0.015;
|
|
|
5
5
|
Pick the harmony color that should tint the generated neutral swatch.
|
|
6
6
|
*/
|
|
7
7
|
function pickTintSourceHex(roleColors, harmony) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
tetradic: roleColors.tertiary?.hex,
|
|
15
|
-
};
|
|
16
|
-
return (byMapping[harmony] ??
|
|
17
|
-
roleColors.tertiary?.hex ??
|
|
18
|
-
roleColors.secondary?.hex ??
|
|
19
|
-
roleColors.primary.hex);
|
|
8
|
+
if (harmony === 'monochromatic')
|
|
9
|
+
return roleColors.primary.hex;
|
|
10
|
+
if (harmony === 'complementary') {
|
|
11
|
+
return roleColors.secondary?.hex ?? roleColors.primary.hex;
|
|
12
|
+
}
|
|
13
|
+
return roleColors.tertiary?.hex ?? roleColors.secondary?.hex ?? roleColors.primary.hex;
|
|
20
14
|
}
|
|
21
15
|
/***
|
|
22
16
|
Clamp a number to a finite minimum and maximum range.
|
package/dist/neutral.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"neutral.js","sourceRoot":"","sources":["../src/neutral.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACrF,OAAO,EACL,2BAA2B,EAG3B,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAEpB,MAAM,CAAC,MAAM,iBAAiB,GAAG,KAAK,CAAC;AAavC;;EAEE;AACF,SAAS,iBAAiB,CACxB,UAAsC,EACtC,OAAqB;IAErB,
|
|
1
|
+
{"version":3,"file":"neutral.js","sourceRoot":"","sources":["../src/neutral.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACrF,OAAO,EACL,2BAA2B,EAG3B,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAEpB,MAAM,CAAC,MAAM,iBAAiB,GAAG,KAAK,CAAC;AAavC;;EAEE;AACF,SAAS,iBAAiB,CACxB,UAAsC,EACtC,OAAqB;IAErB,IAAI,OAAO,KAAK,eAAe;QAAE,OAAO,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC;IAC/D,IAAI,OAAO,KAAK,eAAe,EAAE,CAAC;QAChC,OAAO,UAAU,CAAC,SAAS,EAAE,GAAG,IAAI,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC;IAC7D,CAAC;IACD,OAAO,UAAU,CAAC,QAAQ,EAAE,GAAG,IAAI,UAAU,CAAC,SAAS,EAAE,GAAG,IAAI,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC;AACzF,CAAC;AAED;;EAEE;AACF,SAAS,WAAW,CAAC,KAAa,EAAE,GAAW,EAAE,GAAW;IAC1D,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,GAAG,CAAC;IACxC,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;AAC7C,CAAC;AAED;;EAEE;AACF,MAAM,UAAU,qBAAqB,CAAC,UAAsC;IAC1E,MAAM,aAAa,GAAG,iBAAiB,CAAC,UAAU,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC;IACxE,MAAM,UAAU,GAAG,eAAe,CAAC,aAAa,CAAC,CAAC;IAElD,MAAM,OAAO,GAAG,eAAe,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAExD,MAAM,UAAU,GAAG,UAAU,CAAC,CAAC,GAAG,iBAAiB,CAAC;IAEpD,MAAM,GAAG,GAAG,UAAU,CAAC,OAAO,KAAK,eAAe,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;IAC9E,MAAM,YAAY,GAAG,UAAU,CAAC,OAAO,KAAK,eAAe,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;IACvF,MAAM,UAAU,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC;IAC7D,MAAM,aAAa,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,YAAY,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;IAEtF,MAAM,eAAe,GAAG,UAAU,CAAC;QACjC,IAAI,EAAE,OAAO;QACb,CAAC,EAAE,2BAA2B;QAC9B,CAAC,EAAE,aAAa;QAChB,CAAC,EAAE,UAAU;KACd,CAAC,CAAC;IAEH,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,mBAAmB,CAAC,eAAe,CAAC,CAAC;IAErE,OAAO,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;AAC3D,CAAC","sourcesContent":["import type { ColorHarmony, GeneratedHarmonyRoleColors } from './harmony';\nimport type { HexColor } from './hex';\nimport { normalizeHueDegrees, oklchToHex, parseHexToOklch } from './internal-culori';\nimport {\n COLOR_SWATCH_BASE_LIGHTNESS,\n type ColorSwatch,\n type ColorSwatchDiagnostics,\n generateColorSwatch,\n} from './swatches';\n\nexport const MIN_HUEFUL_CHROMA = 0.015;\n\nexport interface NeutralSwatchResult {\n neutralKeyColor: HexColor;\n neutral: ColorSwatch;\n diagnostics: ColorSwatchDiagnostics;\n}\n\nexport interface GeneratedNeutralMetadata {\n neutralKeyColor: HexColor;\n diagnostics: ColorSwatchDiagnostics;\n}\n\n/***\n Pick the harmony color that should tint the generated neutral swatch.\n*/\nfunction pickTintSourceHex(\n roleColors: GeneratedHarmonyRoleColors,\n harmony: ColorHarmony,\n): HexColor {\n if (harmony === 'monochromatic') return roleColors.primary.hex;\n if (harmony === 'complementary') {\n return roleColors.secondary?.hex ?? roleColors.primary.hex;\n }\n return roleColors.tertiary?.hex ?? roleColors.secondary?.hex ?? roleColors.primary.hex;\n}\n\n/***\n Clamp a number to a finite minimum and maximum range.\n*/\nfunction clampNumber(value: number, min: number, max: number): number {\n if (!Number.isFinite(value)) return min;\n return Math.min(max, Math.max(min, value));\n}\n\n/***\n Generate a softly tinted neutral swatch from generated harmony role colors.\n*/\nexport function generateNeutralSwatch(roleColors: GeneratedHarmonyRoleColors): NeutralSwatchResult {\n const tintSourceHex = pickTintSourceHex(roleColors, roleColors.harmony);\n const tintSource = parseHexToOklch(tintSourceHex);\n\n const primary = parseHexToOklch(roleColors.primary.hex);\n\n const preferGray = tintSource.c < MIN_HUEFUL_CHROMA;\n\n const hue = roleColors.harmony === 'monochromatic' ? primary.h : tintSource.h;\n const sourceChroma = roleColors.harmony === 'monochromatic' ? primary.c : tintSource.c;\n const neutralHue = preferGray ? 0 : normalizeHueDegrees(hue);\n const neutralChroma = preferGray ? 0 : clampNumber(sourceChroma * 0.06, 0.004, 0.012);\n\n const neutralKeyColor = oklchToHex({\n mode: 'oklch',\n l: COLOR_SWATCH_BASE_LIGHTNESS,\n c: neutralChroma,\n h: neutralHue,\n });\n\n const { swatch, diagnostics } = generateColorSwatch(neutralKeyColor);\n\n return { neutralKeyColor, neutral: swatch, diagnostics };\n}\n"]}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { HexColor } from './hex';
|
|
2
|
+
import { type ColorSwatch, type ColorSwatchStep } from './swatches';
|
|
3
|
+
export type ColorSwatchTiePolicy = 'lower-step' | 'higher-step';
|
|
4
|
+
export interface ColorSelectionTarget {
|
|
5
|
+
lightness: number;
|
|
6
|
+
chroma: number;
|
|
7
|
+
hueDegrees?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface ColorContrastContext {
|
|
10
|
+
id: string;
|
|
11
|
+
against: HexColor;
|
|
12
|
+
minimumContrast: number;
|
|
13
|
+
}
|
|
14
|
+
export interface ColorContrastMeasurement extends ColorContrastContext {
|
|
15
|
+
contrast: number;
|
|
16
|
+
passes: boolean;
|
|
17
|
+
}
|
|
18
|
+
export type ColorSwatchCandidateRejectionReason = 'contrast' | 'target-distance' | 'tie-break';
|
|
19
|
+
export interface ColorSwatchSelectionCandidate {
|
|
20
|
+
step: ColorSwatchStep;
|
|
21
|
+
hex: HexColor;
|
|
22
|
+
targetDistance: number;
|
|
23
|
+
contrasts: readonly ColorContrastMeasurement[];
|
|
24
|
+
selected: boolean;
|
|
25
|
+
rejectionReasons: readonly ColorSwatchCandidateRejectionReason[];
|
|
26
|
+
}
|
|
27
|
+
export interface ColorSwatchSelectionResult {
|
|
28
|
+
selected: ColorSwatchSelectionCandidate | null;
|
|
29
|
+
candidates: readonly ColorSwatchSelectionCandidate[];
|
|
30
|
+
rejectedCandidates: readonly ColorSwatchSelectionCandidate[];
|
|
31
|
+
target: ColorSelectionTarget;
|
|
32
|
+
tieBreak: {
|
|
33
|
+
policy: ColorSwatchTiePolicy;
|
|
34
|
+
applied: boolean;
|
|
35
|
+
candidateSteps: readonly ColorSwatchStep[];
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/***
|
|
39
|
+
Select the swatch step nearest a perceptual target that passes every required contrast context.
|
|
40
|
+
*/
|
|
41
|
+
export declare function selectColorSwatchStep(swatch: ColorSwatch, target: ColorSelectionTarget, contexts: readonly ColorContrastContext[], tiePolicy: ColorSwatchTiePolicy): ColorSwatchSelectionResult;
|
|
42
|
+
//# sourceMappingURL=selection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selection.d.ts","sourceRoot":"","sources":["../src/selection.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAGtC,OAAO,EAAsB,KAAK,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAExF,MAAM,MAAM,oBAAoB,GAAG,YAAY,GAAG,aAAa,CAAC;AAEhE,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,QAAQ,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,wBAAyB,SAAQ,oBAAoB;IACpE,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,MAAM,mCAAmC,GAAG,UAAU,GAAG,iBAAiB,GAAG,WAAW,CAAC;AAE/F,MAAM,WAAW,6BAA6B;IAC5C,IAAI,EAAE,eAAe,CAAC;IACtB,GAAG,EAAE,QAAQ,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,SAAS,wBAAwB,EAAE,CAAC;IAC/C,QAAQ,EAAE,OAAO,CAAC;IAClB,gBAAgB,EAAE,SAAS,mCAAmC,EAAE,CAAC;CAClE;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,EAAE,6BAA6B,GAAG,IAAI,CAAC;IAC/C,UAAU,EAAE,SAAS,6BAA6B,EAAE,CAAC;IACrD,kBAAkB,EAAE,SAAS,6BAA6B,EAAE,CAAC;IAC7D,MAAM,EAAE,oBAAoB,CAAC;IAC7B,QAAQ,EAAE;QACR,MAAM,EAAE,oBAAoB,CAAC;QAC7B,OAAO,EAAE,OAAO,CAAC;QACjB,cAAc,EAAE,SAAS,eAAe,EAAE,CAAC;KAC5C,CAAC;CACH;AAYD;;EAEE;AACF,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,oBAAoB,EAC5B,QAAQ,EAAE,SAAS,oBAAoB,EAAE,EACzC,SAAS,EAAE,oBAAoB,GAC9B,0BAA0B,CA6B5B"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { getContrastRatio } from './contrast';
|
|
2
|
+
import { parseHexColorOrThrow } from './hex';
|
|
3
|
+
import { deltaEoklch, normalizeHueDegrees, parseHexToOklch } from './internal-culori';
|
|
4
|
+
import { COLOR_SWATCH_STEPS } from './swatches';
|
|
5
|
+
const DISTANCE_TIE_TOLERANCE = Number.EPSILON * 16;
|
|
6
|
+
/***
|
|
7
|
+
Select the swatch step nearest a perceptual target that passes every required contrast context.
|
|
8
|
+
*/
|
|
9
|
+
export function selectColorSwatchStep(swatch, target, contexts, tiePolicy) {
|
|
10
|
+
const normalizedTarget = normalizeTarget(target);
|
|
11
|
+
const normalizedContexts = normalizeContexts(contexts);
|
|
12
|
+
const measured = COLOR_SWATCH_STEPS.map((step) => measureCandidate(swatch, step, normalizedTarget, normalizedContexts));
|
|
13
|
+
const eligible = measured.filter(({ passesContrast }) => passesContrast);
|
|
14
|
+
const nearestDistance = Math.min(...eligible.map(({ targetDistance }) => targetDistance));
|
|
15
|
+
const nearest = eligible.filter(({ targetDistance }) => Math.abs(targetDistance - nearestDistance) <= DISTANCE_TIE_TOLERANCE);
|
|
16
|
+
const selectedMeasured = tiePolicy === 'lower-step' ? nearest.at(0) : nearest.at(-1);
|
|
17
|
+
const tieCandidateSteps = nearest.map(({ step }) => step);
|
|
18
|
+
const candidates = measured.map((candidate) => createSelectionCandidate(candidate, selectedMeasured, nearestDistance, tieCandidateSteps));
|
|
19
|
+
const selected = candidates.find((candidate) => candidate.selected) ?? null;
|
|
20
|
+
return {
|
|
21
|
+
selected,
|
|
22
|
+
candidates,
|
|
23
|
+
rejectedCandidates: candidates.filter((candidate) => !candidate.selected),
|
|
24
|
+
target: normalizedTarget,
|
|
25
|
+
tieBreak: {
|
|
26
|
+
policy: tiePolicy,
|
|
27
|
+
applied: nearest.length > 1,
|
|
28
|
+
candidateSteps: tieCandidateSteps,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/***
|
|
33
|
+
Convert a measured candidate into its public selection diagnostic.
|
|
34
|
+
*/
|
|
35
|
+
function createSelectionCandidate(candidate, selected, nearestDistance, tieCandidateSteps) {
|
|
36
|
+
const isSelected = selected?.step === candidate.step;
|
|
37
|
+
return {
|
|
38
|
+
step: candidate.step,
|
|
39
|
+
hex: candidate.hex,
|
|
40
|
+
targetDistance: candidate.targetDistance,
|
|
41
|
+
contrasts: candidate.contrasts,
|
|
42
|
+
selected: isSelected,
|
|
43
|
+
rejectionReasons: getCandidateRejectionReasons(candidate, isSelected, nearestDistance, tieCandidateSteps),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/***
|
|
47
|
+
Explain every reason a candidate was not selected.
|
|
48
|
+
*/
|
|
49
|
+
function getCandidateRejectionReasons(candidate, selected, nearestDistance, tieCandidateSteps) {
|
|
50
|
+
if (selected)
|
|
51
|
+
return [];
|
|
52
|
+
if (!candidate.passesContrast)
|
|
53
|
+
return ['contrast'];
|
|
54
|
+
if (candidate.targetDistance > nearestDistance + DISTANCE_TIE_TOLERANCE) {
|
|
55
|
+
return ['target-distance'];
|
|
56
|
+
}
|
|
57
|
+
if (tieCandidateSteps.includes(candidate.step))
|
|
58
|
+
return ['tie-break'];
|
|
59
|
+
return ['target-distance'];
|
|
60
|
+
}
|
|
61
|
+
/***
|
|
62
|
+
Measure a swatch candidate against the target and required contrast contexts.
|
|
63
|
+
*/
|
|
64
|
+
function measureCandidate(swatch, step, target, contexts) {
|
|
65
|
+
const value = Reflect.get(swatch, step);
|
|
66
|
+
if (typeof value !== 'string') {
|
|
67
|
+
throw new Error(`[color-theory] Missing color swatch step: ${step}.`);
|
|
68
|
+
}
|
|
69
|
+
const hex = parseHexColorOrThrow(value);
|
|
70
|
+
const contrasts = contexts.map((context) => {
|
|
71
|
+
const contrast = getContrastRatio(hex, context.against);
|
|
72
|
+
return { ...context, contrast, passes: contrast >= context.minimumContrast };
|
|
73
|
+
});
|
|
74
|
+
return {
|
|
75
|
+
step,
|
|
76
|
+
hex,
|
|
77
|
+
targetDistance: measureTargetDistance(hex, target),
|
|
78
|
+
contrasts,
|
|
79
|
+
passesContrast: contrasts.every(({ passes }) => passes),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/***
|
|
83
|
+
Measure target distance with hue included only when the caller supplied it.
|
|
84
|
+
*/
|
|
85
|
+
function measureTargetDistance(hex, target) {
|
|
86
|
+
const candidate = parseHexToOklch(hex);
|
|
87
|
+
if (target.hueDegrees === undefined) {
|
|
88
|
+
return Math.hypot(candidate.l - target.lightness, candidate.c - target.chroma);
|
|
89
|
+
}
|
|
90
|
+
return deltaEoklch(candidate, {
|
|
91
|
+
mode: 'oklch',
|
|
92
|
+
l: target.lightness,
|
|
93
|
+
c: target.chroma,
|
|
94
|
+
h: target.hueDegrees,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/***
|
|
98
|
+
Validate and normalize contrast contexts for deterministic measurement.
|
|
99
|
+
*/
|
|
100
|
+
function normalizeContexts(contexts) {
|
|
101
|
+
if (contexts.length === 0) {
|
|
102
|
+
throw new Error('[color-theory] Color contrast contexts must not be empty.');
|
|
103
|
+
}
|
|
104
|
+
const ids = new Set();
|
|
105
|
+
return contexts.map((context) => {
|
|
106
|
+
const id = context.id.trim();
|
|
107
|
+
if (!id)
|
|
108
|
+
throw new Error('[color-theory] Color contrast context id must not be empty.');
|
|
109
|
+
if (ids.has(id)) {
|
|
110
|
+
throw new Error(`[color-theory] Duplicate color contrast context id: ${JSON.stringify(id)}.`);
|
|
111
|
+
}
|
|
112
|
+
ids.add(id);
|
|
113
|
+
if (!Number.isFinite(context.minimumContrast) ||
|
|
114
|
+
context.minimumContrast < 1 ||
|
|
115
|
+
context.minimumContrast > 21) {
|
|
116
|
+
throw new Error(`[color-theory] Minimum contrast must be a finite ratio from 1 through 21: ${context.minimumContrast}.`);
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
id,
|
|
120
|
+
against: parseHexColorOrThrow(context.against),
|
|
121
|
+
minimumContrast: context.minimumContrast,
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/***
|
|
126
|
+
Validate and normalize a perceptual color-selection target.
|
|
127
|
+
*/
|
|
128
|
+
function normalizeTarget(target) {
|
|
129
|
+
if (!Number.isFinite(target.lightness) || target.lightness < 0 || target.lightness > 1) {
|
|
130
|
+
throw new Error(`[color-theory] Target lightness must be finite and within 0 through 1: ${target.lightness}.`);
|
|
131
|
+
}
|
|
132
|
+
if (!Number.isFinite(target.chroma) || target.chroma < 0 || target.chroma > 0.4) {
|
|
133
|
+
throw new Error(`[color-theory] Target chroma must be finite and within 0 through 0.4: ${target.chroma}.`);
|
|
134
|
+
}
|
|
135
|
+
if (target.hueDegrees !== undefined && !Number.isFinite(target.hueDegrees)) {
|
|
136
|
+
throw new Error(`[color-theory] Target hue must be finite: ${target.hueDegrees}.`);
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
lightness: target.lightness,
|
|
140
|
+
chroma: target.chroma,
|
|
141
|
+
...(target.hueDegrees !== undefined
|
|
142
|
+
? { hueDegrees: normalizeHueDegrees(target.hueDegrees) }
|
|
143
|
+
: {}),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=selection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selection.js","sourceRoot":"","sources":["../src/selection.ts"],"names":[],"mappings":"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","sourcesContent":["import { getContrastRatio } from './contrast';\nimport type { HexColor } from './hex';\nimport { parseHexColorOrThrow } from './hex';\nimport { deltaEoklch, normalizeHueDegrees, parseHexToOklch } from './internal-culori';\nimport { COLOR_SWATCH_STEPS, type ColorSwatch, type ColorSwatchStep } from './swatches';\n\nexport type ColorSwatchTiePolicy = 'lower-step' | 'higher-step';\n\nexport interface ColorSelectionTarget {\n lightness: number;\n chroma: number;\n hueDegrees?: number;\n}\n\nexport interface ColorContrastContext {\n id: string;\n against: HexColor;\n minimumContrast: number;\n}\n\nexport interface ColorContrastMeasurement extends ColorContrastContext {\n contrast: number;\n passes: boolean;\n}\n\nexport type ColorSwatchCandidateRejectionReason = 'contrast' | 'target-distance' | 'tie-break';\n\nexport interface ColorSwatchSelectionCandidate {\n step: ColorSwatchStep;\n hex: HexColor;\n targetDistance: number;\n contrasts: readonly ColorContrastMeasurement[];\n selected: boolean;\n rejectionReasons: readonly ColorSwatchCandidateRejectionReason[];\n}\n\nexport interface ColorSwatchSelectionResult {\n selected: ColorSwatchSelectionCandidate | null;\n candidates: readonly ColorSwatchSelectionCandidate[];\n rejectedCandidates: readonly ColorSwatchSelectionCandidate[];\n target: ColorSelectionTarget;\n tieBreak: {\n policy: ColorSwatchTiePolicy;\n applied: boolean;\n candidateSteps: readonly ColorSwatchStep[];\n };\n}\n\ninterface MeasuredColorSwatchCandidate {\n readonly step: ColorSwatchStep;\n readonly hex: HexColor;\n readonly targetDistance: number;\n readonly contrasts: readonly ColorContrastMeasurement[];\n readonly passesContrast: boolean;\n}\n\nconst DISTANCE_TIE_TOLERANCE = Number.EPSILON * 16;\n\n/***\n Select the swatch step nearest a perceptual target that passes every required contrast context.\n*/\nexport function selectColorSwatchStep(\n swatch: ColorSwatch,\n target: ColorSelectionTarget,\n contexts: readonly ColorContrastContext[],\n tiePolicy: ColorSwatchTiePolicy,\n): ColorSwatchSelectionResult {\n const normalizedTarget = normalizeTarget(target);\n const normalizedContexts = normalizeContexts(contexts);\n const measured = COLOR_SWATCH_STEPS.map((step) =>\n measureCandidate(swatch, step, normalizedTarget, normalizedContexts),\n );\n const eligible = measured.filter(({ passesContrast }) => passesContrast);\n const nearestDistance = Math.min(...eligible.map(({ targetDistance }) => targetDistance));\n const nearest = eligible.filter(\n ({ targetDistance }) => Math.abs(targetDistance - nearestDistance) <= DISTANCE_TIE_TOLERANCE,\n );\n const selectedMeasured = tiePolicy === 'lower-step' ? nearest.at(0) : nearest.at(-1);\n const tieCandidateSteps = nearest.map(({ step }) => step);\n const candidates = measured.map((candidate) =>\n createSelectionCandidate(candidate, selectedMeasured, nearestDistance, tieCandidateSteps),\n );\n const selected = candidates.find((candidate) => candidate.selected) ?? null;\n\n return {\n selected,\n candidates,\n rejectedCandidates: candidates.filter((candidate) => !candidate.selected),\n target: normalizedTarget,\n tieBreak: {\n policy: tiePolicy,\n applied: nearest.length > 1,\n candidateSteps: tieCandidateSteps,\n },\n };\n}\n\n/***\n Convert a measured candidate into its public selection diagnostic.\n*/\nfunction createSelectionCandidate(\n candidate: MeasuredColorSwatchCandidate,\n selected: MeasuredColorSwatchCandidate | undefined,\n nearestDistance: number,\n tieCandidateSteps: readonly ColorSwatchStep[],\n): ColorSwatchSelectionCandidate {\n const isSelected = selected?.step === candidate.step;\n return {\n step: candidate.step,\n hex: candidate.hex,\n targetDistance: candidate.targetDistance,\n contrasts: candidate.contrasts,\n selected: isSelected,\n rejectionReasons: getCandidateRejectionReasons(\n candidate,\n isSelected,\n nearestDistance,\n tieCandidateSteps,\n ),\n };\n}\n\n/***\n Explain every reason a candidate was not selected.\n*/\nfunction getCandidateRejectionReasons(\n candidate: MeasuredColorSwatchCandidate,\n selected: boolean,\n nearestDistance: number,\n tieCandidateSteps: readonly ColorSwatchStep[],\n): readonly ColorSwatchCandidateRejectionReason[] {\n if (selected) return [];\n if (!candidate.passesContrast) return ['contrast'];\n if (candidate.targetDistance > nearestDistance + DISTANCE_TIE_TOLERANCE) {\n return ['target-distance'];\n }\n if (tieCandidateSteps.includes(candidate.step)) return ['tie-break'];\n return ['target-distance'];\n}\n\n/***\n Measure a swatch candidate against the target and required contrast contexts.\n*/\nfunction measureCandidate(\n swatch: ColorSwatch,\n step: ColorSwatchStep,\n target: ColorSelectionTarget,\n contexts: readonly ColorContrastContext[],\n): MeasuredColorSwatchCandidate {\n const value = Reflect.get(swatch, step);\n if (typeof value !== 'string') {\n throw new Error(`[color-theory] Missing color swatch step: ${step}.`);\n }\n const hex = parseHexColorOrThrow(value);\n const contrasts = contexts.map((context) => {\n const contrast = getContrastRatio(hex, context.against);\n return { ...context, contrast, passes: contrast >= context.minimumContrast };\n });\n return {\n step,\n hex,\n targetDistance: measureTargetDistance(hex, target),\n contrasts,\n passesContrast: contrasts.every(({ passes }) => passes),\n };\n}\n\n/***\n Measure target distance with hue included only when the caller supplied it.\n*/\nfunction measureTargetDistance(hex: HexColor, target: ColorSelectionTarget): number {\n const candidate = parseHexToOklch(hex);\n if (target.hueDegrees === undefined) {\n return Math.hypot(candidate.l - target.lightness, candidate.c - target.chroma);\n }\n return deltaEoklch(candidate, {\n mode: 'oklch',\n l: target.lightness,\n c: target.chroma,\n h: target.hueDegrees,\n });\n}\n\n/***\n Validate and normalize contrast contexts for deterministic measurement.\n*/\nfunction normalizeContexts(\n contexts: readonly ColorContrastContext[],\n): readonly ColorContrastContext[] {\n if (contexts.length === 0) {\n throw new Error('[color-theory] Color contrast contexts must not be empty.');\n }\n const ids = new Set<string>();\n return contexts.map((context) => {\n const id = context.id.trim();\n if (!id) throw new Error('[color-theory] Color contrast context id must not be empty.');\n if (ids.has(id)) {\n throw new Error(`[color-theory] Duplicate color contrast context id: ${JSON.stringify(id)}.`);\n }\n ids.add(id);\n if (\n !Number.isFinite(context.minimumContrast) ||\n context.minimumContrast < 1 ||\n context.minimumContrast > 21\n ) {\n throw new Error(\n `[color-theory] Minimum contrast must be a finite ratio from 1 through 21: ${context.minimumContrast}.`,\n );\n }\n return {\n id,\n against: parseHexColorOrThrow(context.against),\n minimumContrast: context.minimumContrast,\n };\n });\n}\n\n/***\n Validate and normalize a perceptual color-selection target.\n*/\nfunction normalizeTarget(target: ColorSelectionTarget): ColorSelectionTarget {\n if (!Number.isFinite(target.lightness) || target.lightness < 0 || target.lightness > 1) {\n throw new Error(\n `[color-theory] Target lightness must be finite and within 0 through 1: ${target.lightness}.`,\n );\n }\n if (!Number.isFinite(target.chroma) || target.chroma < 0 || target.chroma > 0.4) {\n throw new Error(\n `[color-theory] Target chroma must be finite and within 0 through 0.4: ${target.chroma}.`,\n );\n }\n if (target.hueDegrees !== undefined && !Number.isFinite(target.hueDegrees)) {\n throw new Error(`[color-theory] Target hue must be finite: ${target.hueDegrees}.`);\n }\n return {\n lightness: target.lightness,\n chroma: target.chroma,\n ...(target.hueDegrees !== undefined\n ? { hueDegrees: normalizeHueDegrees(target.hueDegrees) }\n : {}),\n };\n}\n"]}
|
|
@@ -12,6 +12,12 @@ export interface SemanticStatusSwatches<Role extends string> {
|
|
|
12
12
|
swatches: Record<Role, ColorSwatch>;
|
|
13
13
|
diagnostics: Record<Role, ColorSwatchDiagnostics>;
|
|
14
14
|
}
|
|
15
|
+
/***
|
|
16
|
+
Generate semantic status swatches from a role-to-seed record.
|
|
17
|
+
*/
|
|
15
18
|
export declare function createSemanticStatusSwatches<Role extends string>(seeds: Record<Role, SemanticStatusSeedInput>): SemanticStatusSwatches<Role>;
|
|
19
|
+
/***
|
|
20
|
+
Generate the canonical default semantic status swatches.
|
|
21
|
+
*/
|
|
16
22
|
export declare function createDefaultSemanticStatusSwatches(): SemanticStatusSwatches<DefaultSemanticStatusRole>;
|
|
17
23
|
//# sourceMappingURL=semantic-status.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"semantic-status.d.ts","sourceRoot":"","sources":["../src/semantic-status.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEtC,OAAO,KAAK,EAAE,WAAW,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAGtE,eAAO,MAAM,mCAAmC;;;;CAIL,CAAC;AAE5C,MAAM,MAAM,yBAAyB,GAAG,MAAM,OAAO,mCAAmC,CAAC;AAEzF,MAAM,MAAM,uBAAuB,GAAG,MAAM,GAAG,QAAQ,CAAC;AAExD,MAAM,WAAW,sBAAsB,CAAC,IAAI,SAAS,MAAM;IACzD,KAAK,EAAE,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IAC9B,QAAQ,EAAE,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IACpC,WAAW,EAAE,MAAM,CAAC,IAAI,EAAE,sBAAsB,CAAC,CAAC;CACnD;
|
|
1
|
+
{"version":3,"file":"semantic-status.d.ts","sourceRoot":"","sources":["../src/semantic-status.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEtC,OAAO,KAAK,EAAE,WAAW,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAGtE,eAAO,MAAM,mCAAmC;;;;CAIL,CAAC;AAE5C,MAAM,MAAM,yBAAyB,GAAG,MAAM,OAAO,mCAAmC,CAAC;AAEzF,MAAM,MAAM,uBAAuB,GAAG,MAAM,GAAG,QAAQ,CAAC;AAExD,MAAM,WAAW,sBAAsB,CAAC,IAAI,SAAS,MAAM;IACzD,KAAK,EAAE,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IAC9B,QAAQ,EAAE,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IACpC,WAAW,EAAE,MAAM,CAAC,IAAI,EAAE,sBAAsB,CAAC,CAAC;CACnD;AAmCD;;EAEE;AACF,wBAAgB,4BAA4B,CAAC,IAAI,SAAS,MAAM,EAC9D,KAAK,EAAE,MAAM,CAAC,IAAI,EAAE,uBAAuB,CAAC,GAC3C,sBAAsB,CAAC,IAAI,CAAC,CAS9B;AAED;;EAEE;AACF,wBAAgB,mCAAmC,IAAI,sBAAsB,CAAC,yBAAyB,CAAC,CAEvG"}
|
package/dist/semantic-status.js
CHANGED
|
@@ -5,21 +5,39 @@ export const DEFAULT_SEMANTIC_STATUS_COLOR_SEEDS = {
|
|
|
5
5
|
success: '#22c55e',
|
|
6
6
|
warning: '#f59e0b',
|
|
7
7
|
};
|
|
8
|
+
/***
|
|
9
|
+
Convert typed entries into a role-keyed immutable result record.
|
|
10
|
+
*/
|
|
11
|
+
function createRoleRecord(entries) {
|
|
12
|
+
return Object.fromEntries(entries);
|
|
13
|
+
}
|
|
14
|
+
/***
|
|
15
|
+
Normalize and generate one semantic status role.
|
|
16
|
+
*/
|
|
17
|
+
function generateSemanticStatus(role, seed) {
|
|
18
|
+
const seedHex = typeof seed === 'string' ? parseHexColorOrThrow(seed) : seed;
|
|
19
|
+
const generated = generateColorSwatch(seedHex);
|
|
20
|
+
return {
|
|
21
|
+
role,
|
|
22
|
+
seed: seedHex,
|
|
23
|
+
swatch: generated.swatch,
|
|
24
|
+
diagnostics: generated.diagnostics,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/***
|
|
28
|
+
Generate semantic status swatches from a role-to-seed record.
|
|
29
|
+
*/
|
|
8
30
|
export function createSemanticStatusSwatches(seeds) {
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const seedHex = typeof seed === 'string' ? parseHexColorOrThrow(seed) : seed;
|
|
16
|
-
normalizedSeeds[role] = seedHex;
|
|
17
|
-
const generated = generateColorSwatch(seedHex);
|
|
18
|
-
swatches[role] = generated.swatch;
|
|
19
|
-
diagnostics[role] = generated.diagnostics;
|
|
20
|
-
}
|
|
21
|
-
return { seeds: normalizedSeeds, swatches, diagnostics };
|
|
31
|
+
const generated = Object.entries(seeds).map(([role, seed]) => generateSemanticStatus(role, seed));
|
|
32
|
+
return {
|
|
33
|
+
seeds: createRoleRecord(generated.map(({ role, seed }) => [role, seed])),
|
|
34
|
+
swatches: createRoleRecord(generated.map(({ role, swatch }) => [role, swatch])),
|
|
35
|
+
diagnostics: createRoleRecord(generated.map(({ diagnostics, role }) => [role, diagnostics])),
|
|
36
|
+
};
|
|
22
37
|
}
|
|
38
|
+
/***
|
|
39
|
+
Generate the canonical default semantic status swatches.
|
|
40
|
+
*/
|
|
23
41
|
export function createDefaultSemanticStatusSwatches() {
|
|
24
42
|
return createSemanticStatusSwatches(DEFAULT_SEMANTIC_STATUS_COLOR_SEEDS);
|
|
25
43
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"semantic-status.js","sourceRoot":"","sources":["../src/semantic-status.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAE7C,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEjD,MAAM,CAAC,MAAM,mCAAmC,GAAG;IACjD,MAAM,EAAE,SAAS;IACjB,OAAO,EAAE,SAAS;IAClB,OAAO,EAAE,SAAS;CACuB,CAAC;
|
|
1
|
+
{"version":3,"file":"semantic-status.js","sourceRoot":"","sources":["../src/semantic-status.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAE7C,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEjD,MAAM,CAAC,MAAM,mCAAmC,GAAG;IACjD,MAAM,EAAE,SAAS;IACjB,OAAO,EAAE,SAAS;IAClB,OAAO,EAAE,SAAS;CACuB,CAAC;AAmB5C;;EAEE;AACF,SAAS,gBAAgB,CACvB,OAA4C;IAE5C,OAAO,MAAM,CAAC,WAAW,CAAC,OAAO,CAAwB,CAAC;AAC5D,CAAC;AAED;;EAEE;AACF,SAAS,sBAAsB,CAC7B,IAAU,EACV,IAA6B;IAE7B,MAAM,OAAO,GAAG,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC7E,MAAM,SAAS,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC;IAC/C,OAAO;QACL,IAAI;QACJ,IAAI,EAAE,OAAO;QACb,MAAM,EAAE,SAAS,CAAC,MAAM;QACxB,WAAW,EAAE,SAAS,CAAC,WAAW;KACnC,CAAC;AACJ,CAAC;AAED;;EAEE;AACF,MAAM,UAAU,4BAA4B,CAC1C,KAA4C;IAE5C,MAAM,SAAS,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,CAC3D,sBAAsB,CAAC,IAAY,EAAE,IAA+B,CAAC,CACtE,CAAC;IACF,OAAO;QACL,KAAK,EAAE,gBAAgB,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;QACxE,QAAQ,EAAE,gBAAgB,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;QAC/E,WAAW,EAAE,gBAAgB,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC;KAC7F,CAAC;AACJ,CAAC;AAED;;EAEE;AACF,MAAM,UAAU,mCAAmC;IACjD,OAAO,4BAA4B,CAAC,mCAAmC,CAAC,CAAC;AAC3E,CAAC","sourcesContent":["import type { HexColor } from './hex';\nimport { parseHexColorOrThrow } from './hex';\nimport type { ColorSwatch, ColorSwatchDiagnostics } from './swatches';\nimport { generateColorSwatch } from './swatches';\n\nexport const DEFAULT_SEMANTIC_STATUS_COLOR_SEEDS = {\n danger: '#ef4444',\n success: '#22c55e',\n warning: '#f59e0b',\n} as const satisfies Record<string, string>;\n\nexport type DefaultSemanticStatusRole = keyof typeof DEFAULT_SEMANTIC_STATUS_COLOR_SEEDS;\n\nexport type SemanticStatusSeedInput = string | HexColor;\n\nexport interface SemanticStatusSwatches<Role extends string> {\n seeds: Record<Role, HexColor>;\n swatches: Record<Role, ColorSwatch>;\n diagnostics: Record<Role, ColorSwatchDiagnostics>;\n}\n\ninterface GeneratedSemanticStatus<Role extends string> {\n readonly diagnostics: ColorSwatchDiagnostics;\n readonly role: Role;\n readonly seed: HexColor;\n readonly swatch: ColorSwatch;\n}\n\n/***\n Convert typed entries into a role-keyed immutable result record.\n*/\nfunction createRoleRecord<Role extends string, Value>(\n entries: readonly (readonly [Role, Value])[],\n): Record<Role, Value> {\n return Object.fromEntries(entries) as Record<Role, Value>;\n}\n\n/***\n Normalize and generate one semantic status role.\n*/\nfunction generateSemanticStatus<Role extends string>(\n role: Role,\n seed: SemanticStatusSeedInput,\n): GeneratedSemanticStatus<Role> {\n const seedHex = typeof seed === 'string' ? parseHexColorOrThrow(seed) : seed;\n const generated = generateColorSwatch(seedHex);\n return {\n role,\n seed: seedHex,\n swatch: generated.swatch,\n diagnostics: generated.diagnostics,\n };\n}\n\n/***\n Generate semantic status swatches from a role-to-seed record.\n*/\nexport function createSemanticStatusSwatches<Role extends string>(\n seeds: Record<Role, SemanticStatusSeedInput>,\n): SemanticStatusSwatches<Role> {\n const generated = Object.entries(seeds).map(([role, seed]) =>\n generateSemanticStatus(role as Role, seed as SemanticStatusSeedInput),\n );\n return {\n seeds: createRoleRecord(generated.map(({ role, seed }) => [role, seed])),\n swatches: createRoleRecord(generated.map(({ role, swatch }) => [role, swatch])),\n diagnostics: createRoleRecord(generated.map(({ diagnostics, role }) => [role, diagnostics])),\n };\n}\n\n/***\n Generate the canonical default semantic status swatches.\n*/\nexport function createDefaultSemanticStatusSwatches(): SemanticStatusSwatches<DefaultSemanticStatusRole> {\n return createSemanticStatusSwatches(DEFAULT_SEMANTIC_STATUS_COLOR_SEEDS);\n}\n"]}
|
package/dist/swatches.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"swatches.d.ts","sourceRoot":"","sources":["../src/swatches.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAGtC,eAAO,MAAM,kBAAkB,iEAAkE,CAAC;AAClG,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC;AAElE,eAAO,MAAM,sBAAsB,EAAG,GAAY,CAAC;AACnD,eAAO,MAAM,2BAA2B,QAAQ,CAAC;AAEjD,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,eAAe,EAAE,QAAQ,CAAC,CAAC;AAE5D,MAAM,MAAM,sBAAsB,
|
|
1
|
+
{"version":3,"file":"swatches.d.ts","sourceRoot":"","sources":["../src/swatches.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAGtC,eAAO,MAAM,kBAAkB,iEAAkE,CAAC;AAClG,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC;AAElE,eAAO,MAAM,sBAAsB,EAAG,GAAY,CAAC;AACnD,eAAO,MAAM,2BAA2B,QAAQ,CAAC;AAEjD,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,eAAe,EAAE,QAAQ,CAAC,CAAC;AAE5D,MAAM,MAAM,sBAAsB,GAChC,WAAW,GAAG,qBAAqB,GAAG,yBAAyB,CAAC;AAElE,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,sBAAsB,CAAC;IAC7B,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACxC,gBAAgB,EAAE,MAAM,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,cAAc,EAAE;QACd,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC;KACb,CAAC;CACH;AAgLD;;EAEE;AACF,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,QAAQ,GAAG;IACxD,MAAM,EAAE,WAAW,CAAC;IACpB,WAAW,EAAE,sBAAsB,CAAC;CACrC,CAaA"}
|
package/dist/swatches.js
CHANGED
|
@@ -2,19 +2,19 @@ import { deltaEoklch, oklchToHex, parseHexToOklch } from './internal-culori';
|
|
|
2
2
|
export const COLOR_SWATCH_STEPS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950];
|
|
3
3
|
export const COLOR_SWATCH_BASE_STEP = 500;
|
|
4
4
|
export const COLOR_SWATCH_BASE_LIGHTNESS = 0.551;
|
|
5
|
-
const BASELINE_LIGHTNESS_BY_STEP =
|
|
6
|
-
50
|
|
7
|
-
100
|
|
8
|
-
200
|
|
9
|
-
300
|
|
10
|
-
400
|
|
11
|
-
[COLOR_SWATCH_BASE_STEP
|
|
12
|
-
600
|
|
13
|
-
700
|
|
14
|
-
800
|
|
15
|
-
900
|
|
16
|
-
950
|
|
17
|
-
|
|
5
|
+
const BASELINE_LIGHTNESS_BY_STEP = new Map([
|
|
6
|
+
[50, 0.985],
|
|
7
|
+
[100, 0.967],
|
|
8
|
+
[200, 0.928],
|
|
9
|
+
[300, 0.872],
|
|
10
|
+
[400, 0.707],
|
|
11
|
+
[COLOR_SWATCH_BASE_STEP, COLOR_SWATCH_BASE_LIGHTNESS],
|
|
12
|
+
[600, 0.446],
|
|
13
|
+
[700, 0.373],
|
|
14
|
+
[800, 0.278],
|
|
15
|
+
[900, 0.21],
|
|
16
|
+
[950, 0.13],
|
|
17
|
+
]);
|
|
18
18
|
const MIN_USABLE_ADJACENT_DELTA = 0.012;
|
|
19
19
|
const MIN_USABLE_LIGHTNESS_RANGE = 0.35;
|
|
20
20
|
/***
|
|
@@ -40,80 +40,125 @@ function chromaMultiplierForStep(step) {
|
|
|
40
40
|
return 0.8;
|
|
41
41
|
}
|
|
42
42
|
/***
|
|
43
|
-
|
|
43
|
+
Resolve the canonical baseline lightness for a swatch step.
|
|
44
44
|
*/
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const swatch = {};
|
|
50
|
-
const lightnessEntries = [];
|
|
51
|
-
const adjacentDeltas = [];
|
|
52
|
-
for (const step of COLOR_SWATCH_STEPS) {
|
|
53
|
-
if (step === COLOR_SWATCH_BASE_STEP) {
|
|
54
|
-
swatch[step] = baseColor;
|
|
55
|
-
lightnessEntries.push(baseOklch.l);
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
const targetL = clamp01(BASELINE_LIGHTNESS_BY_STEP[step] + baseOffset);
|
|
59
|
-
const targetC = clamp01(baseOklch.c * chromaMultiplierForStep(step));
|
|
60
|
-
const target = { ...baseOklch, l: targetL, c: targetC };
|
|
61
|
-
const hex = oklchToHex(target);
|
|
62
|
-
swatch[step] = hex;
|
|
63
|
-
const candidateOklch = parseHexToOklch(hex);
|
|
64
|
-
lightnessEntries.push(candidateOklch.l);
|
|
65
|
-
const deltaEFromBase = deltaEoklch(baseOklch, candidateOklch);
|
|
66
|
-
if (deltaEFromBase < 0.02) {
|
|
67
|
-
warnings.push({
|
|
68
|
-
code: 'weak_step',
|
|
69
|
-
step,
|
|
70
|
-
deltaEFromBase,
|
|
71
|
-
message: `Swatch step ${step} is visually close to the base color.`,
|
|
72
|
-
});
|
|
73
|
-
}
|
|
45
|
+
function getBaselineLightness(step) {
|
|
46
|
+
const lightness = BASELINE_LIGHTNESS_BY_STEP.get(step);
|
|
47
|
+
if (lightness === undefined) {
|
|
48
|
+
throw new Error(`[color-theory] Missing baseline lightness for swatch step ${step}.`);
|
|
74
49
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (adjacentDelta < MIN_USABLE_ADJACENT_DELTA) {
|
|
85
|
-
warnings.push({
|
|
86
|
-
code: 'weak_adjacent_delta',
|
|
87
|
-
step: currentStep,
|
|
88
|
-
deltaEFromBase: adjacentDelta,
|
|
89
|
-
message: `Swatch step ${currentStep} is visually close to adjacent step ${previousStep}.`,
|
|
90
|
-
});
|
|
91
|
-
}
|
|
50
|
+
return lightness;
|
|
51
|
+
}
|
|
52
|
+
/***
|
|
53
|
+
Generate one immutable swatch sample from the base color.
|
|
54
|
+
*/
|
|
55
|
+
function createSwatchSample(baseColor, step, baseOffset) {
|
|
56
|
+
const base = parseHexToOklch(baseColor);
|
|
57
|
+
if (step === COLOR_SWATCH_BASE_STEP) {
|
|
58
|
+
return { hex: baseColor, lightness: base.l, step };
|
|
92
59
|
}
|
|
60
|
+
const target = {
|
|
61
|
+
...base,
|
|
62
|
+
l: clamp01(getBaselineLightness(step) + baseOffset),
|
|
63
|
+
c: clamp01(base.c * chromaMultiplierForStep(step)),
|
|
64
|
+
};
|
|
65
|
+
const hex = oklchToHex(target);
|
|
66
|
+
return { hex, lightness: parseHexToOklch(hex).l, step };
|
|
67
|
+
}
|
|
68
|
+
/***
|
|
69
|
+
Create any weak-step warning for a generated sample.
|
|
70
|
+
*/
|
|
71
|
+
function createWeakStepWarning(baseColor, sample) {
|
|
72
|
+
if (sample.step === COLOR_SWATCH_BASE_STEP)
|
|
73
|
+
return undefined;
|
|
74
|
+
const deltaEFromBase = deltaEoklch(parseHexToOklch(baseColor), parseHexToOklch(sample.hex));
|
|
75
|
+
if (deltaEFromBase >= 0.02)
|
|
76
|
+
return undefined;
|
|
77
|
+
return {
|
|
78
|
+
code: 'weak_step',
|
|
79
|
+
step: sample.step,
|
|
80
|
+
deltaEFromBase,
|
|
81
|
+
message: `Swatch step ${sample.step} is visually close to the base color.`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/***
|
|
85
|
+
Generate immutable swatch samples and their per-step warnings.
|
|
86
|
+
*/
|
|
87
|
+
function generateSwatchSamples(baseColor) {
|
|
88
|
+
const baseOffset = parseHexToOklch(baseColor).l - COLOR_SWATCH_BASE_LIGHTNESS;
|
|
89
|
+
const samples = COLOR_SWATCH_STEPS.map((step) => createSwatchSample(baseColor, step, baseOffset));
|
|
90
|
+
const warnings = samples
|
|
91
|
+
.map((sample) => createWeakStepWarning(baseColor, sample))
|
|
92
|
+
.filter((warning) => warning !== undefined);
|
|
93
|
+
return { samples, warnings };
|
|
94
|
+
}
|
|
95
|
+
/***
|
|
96
|
+
Measure adjacent swatch samples and report weak transitions.
|
|
97
|
+
*/
|
|
98
|
+
function measureAdjacentSamples(samples) {
|
|
99
|
+
const pairs = samples
|
|
100
|
+
.slice(1)
|
|
101
|
+
.map((current, index) => ({ current, previous: samples.at(index) }));
|
|
102
|
+
const measured = pairs.flatMap(({ current, previous }) => {
|
|
103
|
+
if (!previous)
|
|
104
|
+
return [];
|
|
105
|
+
const delta = deltaEoklch(parseHexToOklch(previous.hex), parseHexToOklch(current.hex));
|
|
106
|
+
const warning = delta < MIN_USABLE_ADJACENT_DELTA
|
|
107
|
+
? {
|
|
108
|
+
code: 'weak_adjacent_delta',
|
|
109
|
+
step: current.step,
|
|
110
|
+
deltaEFromBase: delta,
|
|
111
|
+
message: `Swatch step ${current.step} is visually close to adjacent step ${previous.step}.`,
|
|
112
|
+
}
|
|
113
|
+
: undefined;
|
|
114
|
+
return [{ delta, warning }];
|
|
115
|
+
});
|
|
116
|
+
return {
|
|
117
|
+
deltas: measured.map(({ delta }) => delta),
|
|
118
|
+
warnings: measured
|
|
119
|
+
.map(({ warning }) => warning)
|
|
120
|
+
.filter((warning) => warning !== undefined),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/***
|
|
124
|
+
Create diagnostics from generated samples and their adjacent deltas.
|
|
125
|
+
*/
|
|
126
|
+
function createSwatchDiagnostics(samples, adjacentDeltas, warnings) {
|
|
93
127
|
const minAdjacentDelta = Math.min(...adjacentDeltas);
|
|
94
128
|
const maxAdjacentDelta = Math.max(...adjacentDeltas);
|
|
129
|
+
const lightnessEntries = samples.map(({ lightness }) => lightness);
|
|
95
130
|
const minLightness = Math.min(...lightnessEntries);
|
|
96
131
|
const maxLightness = Math.max(...lightnessEntries);
|
|
97
132
|
const lightnessRange = maxLightness - minLightness;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
133
|
+
const rangeWarnings = lightnessRange < MIN_USABLE_LIGHTNESS_RANGE
|
|
134
|
+
? [
|
|
135
|
+
{
|
|
136
|
+
code: 'limited_lightness_range',
|
|
137
|
+
message: 'Generated swatch has a limited lightness range.',
|
|
138
|
+
},
|
|
139
|
+
]
|
|
140
|
+
: [];
|
|
141
|
+
return {
|
|
142
|
+
isUsable: minAdjacentDelta >= MIN_USABLE_ADJACENT_DELTA && lightnessRange >= MIN_USABLE_LIGHTNESS_RANGE,
|
|
143
|
+
warnings: [...warnings, ...rangeWarnings],
|
|
144
|
+
minAdjacentDelta,
|
|
145
|
+
maxAdjacentDelta,
|
|
146
|
+
lightnessRange: { min: minLightness, max: maxLightness },
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/***
|
|
150
|
+
Generate a full color swatch and diagnostics from a base color.
|
|
151
|
+
*/
|
|
152
|
+
export function generateColorSwatch(baseColor) {
|
|
153
|
+
const generated = generateSwatchSamples(baseColor);
|
|
154
|
+
const adjacent = measureAdjacentSamples(generated.samples);
|
|
155
|
+
const swatch = Object.fromEntries(generated.samples.map(({ hex, step }) => [step, hex]));
|
|
104
156
|
return {
|
|
105
157
|
swatch,
|
|
106
|
-
diagnostics:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
minAdjacentDelta,
|
|
111
|
-
maxAdjacentDelta,
|
|
112
|
-
lightnessRange: {
|
|
113
|
-
min: minLightness,
|
|
114
|
-
max: maxLightness,
|
|
115
|
-
},
|
|
116
|
-
},
|
|
158
|
+
diagnostics: createSwatchDiagnostics(generated.samples, adjacent.deltas, [
|
|
159
|
+
...generated.warnings,
|
|
160
|
+
...adjacent.warnings,
|
|
161
|
+
]),
|
|
117
162
|
};
|
|
118
163
|
}
|
|
119
164
|
//# sourceMappingURL=swatches.js.map
|
package/dist/swatches.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"swatches.js","sourceRoot":"","sources":["../src/swatches.ts"],"names":[],"mappings":"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","sourcesContent":["import type { HexColor } from './hex';\nimport { deltaEoklch, oklchToHex, parseHexToOklch } from './internal-culori';\n\nexport const COLOR_SWATCH_STEPS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950] as const;\nexport type ColorSwatchStep = (typeof COLOR_SWATCH_STEPS)[number];\n\nexport const COLOR_SWATCH_BASE_STEP = 500 as const;\nexport const COLOR_SWATCH_BASE_LIGHTNESS = 0.551;\n\nexport type ColorSwatch = Record<ColorSwatchStep, HexColor>;\n\nexport type ColorSwatchWarningCode =\n | 'weak_step'\n | 'weak_adjacent_delta'\n | 'limited_lightness_range';\n\nexport interface ColorSwatchWarning {\n code: ColorSwatchWarningCode;\n step?: ColorSwatchStep;\n message: string;\n deltaEFromBase?: number;\n}\n\nexport interface ColorSwatchDiagnostics {\n isUsable: boolean;\n warnings: readonly ColorSwatchWarning[];\n minAdjacentDelta: number;\n maxAdjacentDelta: number;\n lightnessRange: {\n min: number;\n max: number;\n };\n}\n\nconst BASELINE_LIGHTNESS_BY_STEP: Record<ColorSwatchStep, number> = {\n 50: 0.985,\n 100: 0.967,\n 200: 0.928,\n 300: 0.872,\n 400: 0.707,\n [COLOR_SWATCH_BASE_STEP]: COLOR_SWATCH_BASE_LIGHTNESS,\n 600: 0.446,\n 700: 0.373,\n 800: 0.278,\n 900: 0.21,\n 950: 0.13,\n};\n\nconst MIN_USABLE_ADJACENT_DELTA = 0.012;\nconst MIN_USABLE_LIGHTNESS_RANGE = 0.35;\n\n/***\n Clamp a number into the normalized zero-to-one range.\n*/\nfunction clamp01(value: number): number {\n if (!Number.isFinite(value)) return 0;\n return Math.min(1, Math.max(0, value));\n}\n\n/***\n Return the chroma multiplier used for a swatch step.\n*/\nfunction chromaMultiplierForStep(step: ColorSwatchStep): number {\n if (step <= 200) return 0.55;\n if (step <= 400) return 0.75;\n if (step === COLOR_SWATCH_BASE_STEP) return 1;\n if (step <= 700) return 0.9;\n return 0.8;\n}\n\n/***\n Generate a full color swatch and diagnostics from a base color.\n*/\nexport function generateColorSwatch(baseColor: HexColor): {\n swatch: ColorSwatch;\n diagnostics: ColorSwatchDiagnostics;\n} {\n const baseOklch = parseHexToOklch(baseColor);\n const baseOffset = baseOklch.l - COLOR_SWATCH_BASE_LIGHTNESS;\n\n const warnings: ColorSwatchWarning[] = [];\n const swatch = {} as ColorSwatch;\n const lightnessEntries: number[] = [];\n const adjacentDeltas: number[] = [];\n\n for (const step of COLOR_SWATCH_STEPS) {\n if (step === COLOR_SWATCH_BASE_STEP) {\n swatch[step] = baseColor;\n lightnessEntries.push(baseOklch.l);\n continue;\n }\n\n const targetL = clamp01(BASELINE_LIGHTNESS_BY_STEP[step] + baseOffset);\n const targetC = clamp01(baseOklch.c * chromaMultiplierForStep(step));\n const target = { ...baseOklch, l: targetL, c: targetC };\n const hex = oklchToHex(target);\n swatch[step] = hex;\n\n const candidateOklch = parseHexToOklch(hex);\n lightnessEntries.push(candidateOklch.l);\n\n const deltaEFromBase = deltaEoklch(baseOklch, candidateOklch);\n if (deltaEFromBase < 0.02) {\n warnings.push({\n code: 'weak_step',\n step,\n deltaEFromBase,\n message: `Swatch step ${step} is visually close to the base color.`,\n });\n }\n }\n\n for (let index = 1; index < COLOR_SWATCH_STEPS.length; index++) {\n const previousStep = COLOR_SWATCH_STEPS[index - 1];\n const currentStep = COLOR_SWATCH_STEPS[index];\n if (previousStep === undefined || currentStep === undefined) continue;\n\n const previous = parseHexToOklch(swatch[previousStep]);\n const current = parseHexToOklch(swatch[currentStep]);\n const adjacentDelta = deltaEoklch(previous, current);\n adjacentDeltas.push(adjacentDelta);\n\n if (adjacentDelta < MIN_USABLE_ADJACENT_DELTA) {\n warnings.push({\n code: 'weak_adjacent_delta',\n step: currentStep,\n deltaEFromBase: adjacentDelta,\n message: `Swatch step ${currentStep} is visually close to adjacent step ${previousStep}.`,\n });\n }\n }\n\n const minAdjacentDelta = Math.min(...adjacentDeltas);\n const maxAdjacentDelta = Math.max(...adjacentDeltas);\n const minLightness = Math.min(...lightnessEntries);\n const maxLightness = Math.max(...lightnessEntries);\n const lightnessRange = maxLightness - minLightness;\n\n if (lightnessRange < MIN_USABLE_LIGHTNESS_RANGE) {\n warnings.push({\n code: 'limited_lightness_range',\n message: 'Generated swatch has a limited lightness range.',\n });\n }\n\n return {\n swatch,\n diagnostics: {\n isUsable:\n minAdjacentDelta >= MIN_USABLE_ADJACENT_DELTA &&\n lightnessRange >= MIN_USABLE_LIGHTNESS_RANGE,\n warnings,\n minAdjacentDelta,\n maxAdjacentDelta,\n lightnessRange: {\n min: minLightness,\n max: maxLightness,\n },\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"swatches.js","sourceRoot":"","sources":["../src/swatches.ts"],"names":[],"mappings":"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","sourcesContent":["import type { HexColor } from './hex';\nimport { deltaEoklch, oklchToHex, parseHexToOklch } from './internal-culori';\n\nexport const COLOR_SWATCH_STEPS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950] as const;\nexport type ColorSwatchStep = (typeof COLOR_SWATCH_STEPS)[number];\n\nexport const COLOR_SWATCH_BASE_STEP = 500 as const;\nexport const COLOR_SWATCH_BASE_LIGHTNESS = 0.551;\n\nexport type ColorSwatch = Record<ColorSwatchStep, HexColor>;\n\nexport type ColorSwatchWarningCode =\n 'weak_step' | 'weak_adjacent_delta' | 'limited_lightness_range';\n\nexport interface ColorSwatchWarning {\n code: ColorSwatchWarningCode;\n step?: ColorSwatchStep;\n message: string;\n deltaEFromBase?: number;\n}\n\nexport interface ColorSwatchDiagnostics {\n isUsable: boolean;\n warnings: readonly ColorSwatchWarning[];\n minAdjacentDelta: number;\n maxAdjacentDelta: number;\n lightnessRange: {\n min: number;\n max: number;\n };\n}\n\nconst BASELINE_LIGHTNESS_BY_STEP = new Map<ColorSwatchStep, number>([\n [50, 0.985],\n [100, 0.967],\n [200, 0.928],\n [300, 0.872],\n [400, 0.707],\n [COLOR_SWATCH_BASE_STEP, COLOR_SWATCH_BASE_LIGHTNESS],\n [600, 0.446],\n [700, 0.373],\n [800, 0.278],\n [900, 0.21],\n [950, 0.13],\n]);\n\nconst MIN_USABLE_ADJACENT_DELTA = 0.012;\nconst MIN_USABLE_LIGHTNESS_RANGE = 0.35;\n\ninterface ColorSwatchSample {\n readonly hex: HexColor;\n readonly lightness: number;\n readonly step: ColorSwatchStep;\n}\n\ninterface GeneratedColorSwatchSamples {\n readonly samples: readonly ColorSwatchSample[];\n readonly warnings: readonly ColorSwatchWarning[];\n}\n\n/***\n Clamp a number into the normalized zero-to-one range.\n*/\nfunction clamp01(value: number): number {\n if (!Number.isFinite(value)) return 0;\n return Math.min(1, Math.max(0, value));\n}\n\n/***\n Return the chroma multiplier used for a swatch step.\n*/\nfunction chromaMultiplierForStep(step: ColorSwatchStep): number {\n if (step <= 200) return 0.55;\n if (step <= 400) return 0.75;\n if (step === COLOR_SWATCH_BASE_STEP) return 1;\n if (step <= 700) return 0.9;\n return 0.8;\n}\n\n/***\n Resolve the canonical baseline lightness for a swatch step.\n*/\nfunction getBaselineLightness(step: ColorSwatchStep): number {\n const lightness = BASELINE_LIGHTNESS_BY_STEP.get(step);\n if (lightness === undefined) {\n throw new Error(`[color-theory] Missing baseline lightness for swatch step ${step}.`);\n }\n return lightness;\n}\n\n/***\n Generate one immutable swatch sample from the base color.\n*/\nfunction createSwatchSample(\n baseColor: HexColor,\n step: ColorSwatchStep,\n baseOffset: number,\n): ColorSwatchSample {\n const base = parseHexToOklch(baseColor);\n if (step === COLOR_SWATCH_BASE_STEP) {\n return { hex: baseColor, lightness: base.l, step };\n }\n const target = {\n ...base,\n l: clamp01(getBaselineLightness(step) + baseOffset),\n c: clamp01(base.c * chromaMultiplierForStep(step)),\n };\n const hex = oklchToHex(target);\n return { hex, lightness: parseHexToOklch(hex).l, step };\n}\n\n/***\n Create any weak-step warning for a generated sample.\n*/\nfunction createWeakStepWarning(\n baseColor: HexColor,\n sample: ColorSwatchSample,\n): ColorSwatchWarning | undefined {\n if (sample.step === COLOR_SWATCH_BASE_STEP) return undefined;\n const deltaEFromBase = deltaEoklch(parseHexToOklch(baseColor), parseHexToOklch(sample.hex));\n if (deltaEFromBase >= 0.02) return undefined;\n return {\n code: 'weak_step',\n step: sample.step,\n deltaEFromBase,\n message: `Swatch step ${sample.step} is visually close to the base color.`,\n };\n}\n\n/***\n Generate immutable swatch samples and their per-step warnings.\n*/\nfunction generateSwatchSamples(baseColor: HexColor): GeneratedColorSwatchSamples {\n const baseOffset = parseHexToOklch(baseColor).l - COLOR_SWATCH_BASE_LIGHTNESS;\n const samples = COLOR_SWATCH_STEPS.map((step) => createSwatchSample(baseColor, step, baseOffset));\n const warnings = samples\n .map((sample) => createWeakStepWarning(baseColor, sample))\n .filter((warning): warning is ColorSwatchWarning => warning !== undefined);\n return { samples, warnings };\n}\n\n/***\n Measure adjacent swatch samples and report weak transitions.\n*/\nfunction measureAdjacentSamples(samples: readonly ColorSwatchSample[]): {\n readonly deltas: readonly number[];\n readonly warnings: readonly ColorSwatchWarning[];\n} {\n const pairs = samples\n .slice(1)\n .map((current, index) => ({ current, previous: samples.at(index) }));\n const measured = pairs.flatMap(({ current, previous }) => {\n if (!previous) return [];\n const delta = deltaEoklch(parseHexToOklch(previous.hex), parseHexToOklch(current.hex));\n const warning: ColorSwatchWarning | undefined =\n delta < MIN_USABLE_ADJACENT_DELTA\n ? {\n code: 'weak_adjacent_delta',\n step: current.step,\n deltaEFromBase: delta,\n message: `Swatch step ${current.step} is visually close to adjacent step ${previous.step}.`,\n }\n : undefined;\n return [{ delta, warning }];\n });\n return {\n deltas: measured.map(({ delta }) => delta),\n warnings: measured\n .map(({ warning }) => warning)\n .filter((warning): warning is ColorSwatchWarning => warning !== undefined),\n };\n}\n\n/***\n Create diagnostics from generated samples and their adjacent deltas.\n*/\nfunction createSwatchDiagnostics(\n samples: readonly ColorSwatchSample[],\n adjacentDeltas: readonly number[],\n warnings: readonly ColorSwatchWarning[],\n): ColorSwatchDiagnostics {\n const minAdjacentDelta = Math.min(...adjacentDeltas);\n const maxAdjacentDelta = Math.max(...adjacentDeltas);\n const lightnessEntries = samples.map(({ lightness }) => lightness);\n const minLightness = Math.min(...lightnessEntries);\n const maxLightness = Math.max(...lightnessEntries);\n const lightnessRange = maxLightness - minLightness;\n const rangeWarnings: readonly ColorSwatchWarning[] =\n lightnessRange < MIN_USABLE_LIGHTNESS_RANGE\n ? [\n {\n code: 'limited_lightness_range',\n message: 'Generated swatch has a limited lightness range.',\n },\n ]\n : [];\n return {\n isUsable:\n minAdjacentDelta >= MIN_USABLE_ADJACENT_DELTA && lightnessRange >= MIN_USABLE_LIGHTNESS_RANGE,\n warnings: [...warnings, ...rangeWarnings],\n minAdjacentDelta,\n maxAdjacentDelta,\n lightnessRange: { min: minLightness, max: maxLightness },\n };\n}\n\n/***\n Generate a full color swatch and diagnostics from a base color.\n*/\nexport function generateColorSwatch(baseColor: HexColor): {\n swatch: ColorSwatch;\n diagnostics: ColorSwatchDiagnostics;\n} {\n const generated = generateSwatchSamples(baseColor);\n const adjacent = measureAdjacentSamples(generated.samples);\n const swatch = Object.fromEntries(\n generated.samples.map(({ hex, step }) => [step, hex]),\n ) as ColorSwatch;\n return {\n swatch,\n diagnostics: createSwatchDiagnostics(generated.samples, adjacent.deltas, [\n ...generated.warnings,\n ...adjacent.warnings,\n ]),\n };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ankhorage/color-theory",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0
|
|
4
|
+
"version": "0.1.0",
|
|
5
5
|
"description": "Standalone color theory, harmony, swatch, contrast, and theme color generation utilities.",
|
|
6
6
|
"homepage": "https://github.com/ankhorage/color-theory#readme",
|
|
7
7
|
"bugs": {
|
|
@@ -59,9 +59,8 @@
|
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
61
|
"build": "rm -rf dist tsconfig.tsbuildinfo && bun x tsc -p tsconfig.json",
|
|
62
|
-
"changeset": "changeset",
|
|
63
|
-
"changeset:status": "changeset status --since=origin/main",
|
|
64
|
-
"knip": "ankhorage-knip",
|
|
62
|
+
"changeset": "ankhorage-changeset",
|
|
63
|
+
"changeset:status": "ankhorage-changeset status --since=origin/main",
|
|
65
64
|
"lint": "ankhorage-eslint . --max-warnings=0",
|
|
66
65
|
"lint:fix": "ankhorage-eslint . --fix --max-warnings=0",
|
|
67
66
|
"format": "ankhorage-prettier --write .",
|
|
@@ -70,16 +69,16 @@
|
|
|
70
69
|
"prepack": "bun run build",
|
|
71
70
|
"test": "bun test src",
|
|
72
71
|
"typecheck": "bun x tsc --noEmit -p tsconfig.json",
|
|
73
|
-
"version-packages": "changeset version"
|
|
72
|
+
"version-packages": "ankhorage-changeset version",
|
|
73
|
+
"knip:check": "ankhorage-knip"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@ankhorage/devtools": "^1.
|
|
77
|
-
"@ankhorage/paradox": "^0.1.
|
|
78
|
-
"@
|
|
79
|
-
"@types/bun": "^1.3.13",
|
|
76
|
+
"@ankhorage/devtools": "^1.9.4",
|
|
77
|
+
"@ankhorage/paradox": "^0.1.24",
|
|
78
|
+
"@types/bun": "^1.4.0",
|
|
80
79
|
"@types/culori": "^4.0.1",
|
|
81
80
|
"@types/node": "^25.6.0",
|
|
82
81
|
"typescript": "^5.9.3"
|
|
83
82
|
},
|
|
84
|
-
"packageManager": "bun@1.
|
|
83
|
+
"packageManager": "bun@1.4.0"
|
|
85
84
|
}
|