@ankhorage/color-theory 0.0.8 → 0.2.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 +12 -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 +65 -2
- package/dist/harmony.d.ts.map +1 -1
- package/dist/harmony.js +99 -34
- 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 +1 -2
- package/dist/neutral.d.ts.map +1 -1
- package/dist/neutral.js +7 -13
- 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,17 @@
|
|
|
1
1
|
# @ankhorage/color-theory
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 78cd2ca: Publish the canonical seven-harmony catalog, distinguish tetradic and square geometry, and report unreliable hue generation for achromatic or low-chroma primary colors.
|
|
8
|
+
|
|
9
|
+
## 0.1.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- be8317b: Add public contrast measurement, caller-defined foreground selection, and deterministic swatch-step selection with perceptual and contrast diagnostics.
|
|
14
|
+
|
|
3
15
|
## 0.0.8
|
|
4
16
|
|
|
5
17
|
### 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
CHANGED
|
@@ -1,7 +1,69 @@
|
|
|
1
1
|
import type { HexColor } from './hex';
|
|
2
|
-
export declare const COLOR_HARMONIES: readonly ["monochromatic", "analogous", "complementary", "triadic", "tetradic", "splitComplementary"];
|
|
3
|
-
export type ColorHarmony = (typeof COLOR_HARMONIES)[number];
|
|
4
2
|
export type GeneratedColorRole = 'primary' | 'secondary' | 'tertiary' | 'quaternary';
|
|
3
|
+
export declare const COLOR_HARMONY_CATALOG: readonly [{
|
|
4
|
+
readonly id: "monochromatic";
|
|
5
|
+
readonly label: "Monochromatic";
|
|
6
|
+
readonly description: "Uses one hue with ramp variation.";
|
|
7
|
+
readonly offsets: readonly [0];
|
|
8
|
+
readonly roles: readonly ["primary"];
|
|
9
|
+
readonly roleCount: 1;
|
|
10
|
+
}, {
|
|
11
|
+
readonly id: "analogous";
|
|
12
|
+
readonly label: "Analogous";
|
|
13
|
+
readonly description: "Uses the primary hue and its two neighboring hues at minus and plus 30 degrees.";
|
|
14
|
+
readonly offsets: readonly [0, -30, 30];
|
|
15
|
+
readonly roles: readonly ["primary", "secondary", "tertiary"];
|
|
16
|
+
readonly roleCount: 3;
|
|
17
|
+
}, {
|
|
18
|
+
readonly id: "complementary";
|
|
19
|
+
readonly label: "Complementary";
|
|
20
|
+
readonly description: "Pairs the primary hue with its opposite hue at 180 degrees.";
|
|
21
|
+
readonly offsets: readonly [0, 180];
|
|
22
|
+
readonly roles: readonly ["primary", "secondary"];
|
|
23
|
+
readonly roleCount: 2;
|
|
24
|
+
}, {
|
|
25
|
+
readonly id: "splitComplementary";
|
|
26
|
+
readonly label: "Split complementary";
|
|
27
|
+
readonly description: "Uses the primary hue with hues at 150 and 210 degrees.";
|
|
28
|
+
readonly offsets: readonly [0, 150, 210];
|
|
29
|
+
readonly roles: readonly ["primary", "secondary", "tertiary"];
|
|
30
|
+
readonly roleCount: 3;
|
|
31
|
+
}, {
|
|
32
|
+
readonly id: "triadic";
|
|
33
|
+
readonly label: "Triadic";
|
|
34
|
+
readonly description: "Uses three evenly spaced hues at 120-degree intervals.";
|
|
35
|
+
readonly offsets: readonly [0, 120, 240];
|
|
36
|
+
readonly roles: readonly ["primary", "secondary", "tertiary"];
|
|
37
|
+
readonly roleCount: 3;
|
|
38
|
+
}, {
|
|
39
|
+
readonly id: "tetradic";
|
|
40
|
+
readonly label: "Tetradic";
|
|
41
|
+
readonly description: "Uses a rectangular tetrad at 0, 60, 180, and 240 degrees.";
|
|
42
|
+
readonly offsets: readonly [0, 60, 180, 240];
|
|
43
|
+
readonly roles: readonly ["primary", "secondary", "tertiary", "quaternary"];
|
|
44
|
+
readonly roleCount: 4;
|
|
45
|
+
}, {
|
|
46
|
+
readonly id: "square";
|
|
47
|
+
readonly label: "Square";
|
|
48
|
+
readonly description: "Uses four evenly spaced hues at 90-degree intervals.";
|
|
49
|
+
readonly offsets: readonly [0, 90, 180, 270];
|
|
50
|
+
readonly roles: readonly ["primary", "secondary", "tertiary", "quaternary"];
|
|
51
|
+
readonly roleCount: 4;
|
|
52
|
+
}];
|
|
53
|
+
export type ColorHarmonyDefinition = (typeof COLOR_HARMONY_CATALOG)[number];
|
|
54
|
+
export type ColorHarmony = ColorHarmonyDefinition['id'];
|
|
55
|
+
export declare const COLOR_HARMONIES: readonly ColorHarmony[];
|
|
56
|
+
export declare const MIN_HUEFUL_CHROMA = 0.015;
|
|
57
|
+
export type HarmonyGenerationWarningCode = 'achromatic_primary' | 'low_chroma_primary';
|
|
58
|
+
export interface HarmonyGenerationWarning {
|
|
59
|
+
readonly code: HarmonyGenerationWarningCode;
|
|
60
|
+
readonly message: string;
|
|
61
|
+
}
|
|
62
|
+
export interface HarmonyGenerationDiagnostics {
|
|
63
|
+
readonly isHueReliable: boolean;
|
|
64
|
+
readonly primaryChroma: number;
|
|
65
|
+
readonly warnings: readonly HarmonyGenerationWarning[];
|
|
66
|
+
}
|
|
5
67
|
export interface GeneratedHarmonyRoleColor {
|
|
6
68
|
role: GeneratedColorRole;
|
|
7
69
|
hex: HexColor;
|
|
@@ -15,6 +77,7 @@ export interface GeneratedHarmonyRoleColors {
|
|
|
15
77
|
secondary?: GeneratedHarmonyRoleColor;
|
|
16
78
|
tertiary?: GeneratedHarmonyRoleColor;
|
|
17
79
|
quaternary?: GeneratedHarmonyRoleColor;
|
|
80
|
+
diagnostics: HarmonyGenerationDiagnostics;
|
|
18
81
|
}
|
|
19
82
|
/***
|
|
20
83
|
Generate role-based harmony colors from a primary color and harmony strategy.
|
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,
|
|
1
|
+
{"version":3,"file":"harmony.d.ts","sourceRoot":"","sources":["../src/harmony.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAGtC,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,WAAW,GAAG,UAAU,GAAG,YAAY,CAAC;AAWrF,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyDsB,CAAC;AAEzD,MAAM,MAAM,sBAAsB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAC;AAC5E,MAAM,MAAM,YAAY,GAAG,sBAAsB,CAAC,IAAI,CAAC,CAAC;AAExD,eAAO,MAAM,eAAe,EAAE,SAAS,YAAY,EAA8C,CAAC;AAElG,eAAO,MAAM,iBAAiB,QAAQ,CAAC;AAEvC,MAAM,MAAM,4BAA4B,GAAG,oBAAoB,GAAG,oBAAoB,CAAC;AAEvF,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,IAAI,EAAE,4BAA4B,CAAC;IAC5C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,SAAS,wBAAwB,EAAE,CAAC;CACxD;AAED,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;IACvC,WAAW,EAAE,4BAA4B,CAAC;CAC3C;AAqCD;;EAEE;AACF,wBAAgB,yBAAyB,CACvC,YAAY,EAAE,QAAQ,EACtB,OAAO,EAAE,YAAY,GACpB,0BAA0B,CAiC5B"}
|
package/dist/harmony.js
CHANGED
|
@@ -1,49 +1,113 @@
|
|
|
1
1
|
import { normalizeHueDegrees, oklchToHex, parseHexToOklch } from './internal-culori';
|
|
2
|
-
export const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
export const COLOR_HARMONY_CATALOG = [
|
|
3
|
+
{
|
|
4
|
+
id: 'monochromatic',
|
|
5
|
+
label: 'Monochromatic',
|
|
6
|
+
description: 'Uses one hue with ramp variation.',
|
|
7
|
+
offsets: [0],
|
|
8
|
+
roles: ['primary'],
|
|
9
|
+
roleCount: 1,
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
id: 'analogous',
|
|
13
|
+
label: 'Analogous',
|
|
14
|
+
description: 'Uses the primary hue and its two neighboring hues at minus and plus 30 degrees.',
|
|
15
|
+
offsets: [0, -30, 30],
|
|
16
|
+
roles: ['primary', 'secondary', 'tertiary'],
|
|
17
|
+
roleCount: 3,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'complementary',
|
|
21
|
+
label: 'Complementary',
|
|
22
|
+
description: 'Pairs the primary hue with its opposite hue at 180 degrees.',
|
|
23
|
+
offsets: [0, 180],
|
|
24
|
+
roles: ['primary', 'secondary'],
|
|
25
|
+
roleCount: 2,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'splitComplementary',
|
|
29
|
+
label: 'Split complementary',
|
|
30
|
+
description: 'Uses the primary hue with hues at 150 and 210 degrees.',
|
|
31
|
+
offsets: [0, 150, 210],
|
|
32
|
+
roles: ['primary', 'secondary', 'tertiary'],
|
|
33
|
+
roleCount: 3,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'triadic',
|
|
37
|
+
label: 'Triadic',
|
|
38
|
+
description: 'Uses three evenly spaced hues at 120-degree intervals.',
|
|
39
|
+
offsets: [0, 120, 240],
|
|
40
|
+
roles: ['primary', 'secondary', 'tertiary'],
|
|
41
|
+
roleCount: 3,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'tetradic',
|
|
45
|
+
label: 'Tetradic',
|
|
46
|
+
description: 'Uses a rectangular tetrad at 0, 60, 180, and 240 degrees.',
|
|
47
|
+
offsets: [0, 60, 180, 240],
|
|
48
|
+
roles: ['primary', 'secondary', 'tertiary', 'quaternary'],
|
|
49
|
+
roleCount: 4,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: 'square',
|
|
53
|
+
label: 'Square',
|
|
54
|
+
description: 'Uses four evenly spaced hues at 90-degree intervals.',
|
|
55
|
+
offsets: [0, 90, 180, 270],
|
|
56
|
+
roles: ['primary', 'secondary', 'tertiary', 'quaternary'],
|
|
57
|
+
roleCount: 4,
|
|
58
|
+
},
|
|
9
59
|
];
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
60
|
+
export const COLOR_HARMONIES = COLOR_HARMONY_CATALOG.map(({ id }) => id);
|
|
61
|
+
export const MIN_HUEFUL_CHROMA = 0.015;
|
|
62
|
+
/***
|
|
63
|
+
Create diagnostics for the reliability of generated hue relationships.
|
|
64
|
+
*/
|
|
65
|
+
function createHarmonyGenerationDiagnostics(primaryChroma) {
|
|
66
|
+
const warnings = [];
|
|
67
|
+
if (primaryChroma === 0) {
|
|
68
|
+
warnings.push({
|
|
69
|
+
code: 'achromatic_primary',
|
|
70
|
+
message: 'The selected primary is achromatic, so generated hue relationships are not visible.',
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
else if (primaryChroma < MIN_HUEFUL_CHROMA) {
|
|
74
|
+
warnings.push({
|
|
75
|
+
code: 'low_chroma_primary',
|
|
76
|
+
message: 'The selected primary has low chroma, so generated hue relationships may be weak.',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
isHueReliable: primaryChroma >= MIN_HUEFUL_CHROMA,
|
|
81
|
+
primaryChroma,
|
|
82
|
+
warnings,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/***
|
|
86
|
+
Resolve a complete harmony definition from the canonical public catalog.
|
|
87
|
+
*/
|
|
88
|
+
function getHarmonyDefinition(harmony) {
|
|
89
|
+
const definition = COLOR_HARMONY_CATALOG.find(({ id }) => id === harmony);
|
|
90
|
+
if (!definition) {
|
|
91
|
+
throw new Error(`[color-theory] Missing definition for harmony ${harmony}.`);
|
|
92
|
+
}
|
|
93
|
+
return definition;
|
|
94
|
+
}
|
|
26
95
|
/***
|
|
27
96
|
Generate role-based harmony colors from a primary color and harmony strategy.
|
|
28
97
|
*/
|
|
29
98
|
export function generateHarmonyRoleColors(primaryColor, harmony) {
|
|
30
99
|
const base = parseHexToOklch(primaryColor);
|
|
31
|
-
const
|
|
32
|
-
const offsets = OFFSETS_BY_HARMONY[harmony];
|
|
100
|
+
const definition = getHarmonyDefinition(harmony);
|
|
33
101
|
const baseHue = normalizeHueDegrees(base.h);
|
|
34
|
-
const colors =
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (!role)
|
|
38
|
-
continue;
|
|
39
|
-
const hueDegrees = normalizeHueDegrees(baseHue + (offsets[index] ?? 0));
|
|
40
|
-
colors.push({
|
|
102
|
+
const colors = definition.roles.map((role, index) => {
|
|
103
|
+
const hueDegrees = normalizeHueDegrees(baseHue + (definition.offsets.at(index) ?? 0));
|
|
104
|
+
return {
|
|
41
105
|
role,
|
|
42
106
|
hex: role === 'primary' ? primaryColor : oklchToHex({ ...base, h: hueDegrees }),
|
|
43
107
|
hueDegrees,
|
|
44
108
|
source: role === 'primary' ? 'selected' : 'generated',
|
|
45
|
-
}
|
|
46
|
-
}
|
|
109
|
+
};
|
|
110
|
+
});
|
|
47
111
|
const primary = colors.find((color) => color.role === 'primary');
|
|
48
112
|
if (!primary) {
|
|
49
113
|
throw new Error('[color-theory] Expected generated harmony role colors to include primary.');
|
|
@@ -58,6 +122,7 @@ export function generateHarmonyRoleColors(primaryColor, harmony) {
|
|
|
58
122
|
...(secondary ? { secondary } : {}),
|
|
59
123
|
...(tertiary ? { tertiary } : {}),
|
|
60
124
|
...(quaternary ? { quaternary } : {}),
|
|
125
|
+
diagnostics: createHarmonyGenerationDiagnostics(base.c),
|
|
61
126
|
};
|
|
62
127
|
}
|
|
63
128
|
//# sourceMappingURL=harmony.js.map
|
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;
|
|
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 type GeneratedColorRole = 'primary' | 'secondary' | 'tertiary' | 'quaternary';\n\ninterface ColorHarmonyCatalogEntry {\n readonly id: string;\n readonly label: string;\n readonly description: string;\n readonly offsets: readonly number[];\n readonly roles: readonly GeneratedColorRole[];\n readonly roleCount: 1 | 2 | 3 | 4;\n}\n\nexport const COLOR_HARMONY_CATALOG = [\n {\n id: 'monochromatic',\n label: 'Monochromatic',\n description: 'Uses one hue with ramp variation.',\n offsets: [0],\n roles: ['primary'],\n roleCount: 1,\n },\n {\n id: 'analogous',\n label: 'Analogous',\n description: 'Uses the primary hue and its two neighboring hues at minus and plus 30 degrees.',\n offsets: [0, -30, 30],\n roles: ['primary', 'secondary', 'tertiary'],\n roleCount: 3,\n },\n {\n id: 'complementary',\n label: 'Complementary',\n description: 'Pairs the primary hue with its opposite hue at 180 degrees.',\n offsets: [0, 180],\n roles: ['primary', 'secondary'],\n roleCount: 2,\n },\n {\n id: 'splitComplementary',\n label: 'Split complementary',\n description: 'Uses the primary hue with hues at 150 and 210 degrees.',\n offsets: [0, 150, 210],\n roles: ['primary', 'secondary', 'tertiary'],\n roleCount: 3,\n },\n {\n id: 'triadic',\n label: 'Triadic',\n description: 'Uses three evenly spaced hues at 120-degree intervals.',\n offsets: [0, 120, 240],\n roles: ['primary', 'secondary', 'tertiary'],\n roleCount: 3,\n },\n {\n id: 'tetradic',\n label: 'Tetradic',\n description: 'Uses a rectangular tetrad at 0, 60, 180, and 240 degrees.',\n offsets: [0, 60, 180, 240],\n roles: ['primary', 'secondary', 'tertiary', 'quaternary'],\n roleCount: 4,\n },\n {\n id: 'square',\n label: 'Square',\n description: 'Uses four evenly spaced hues at 90-degree intervals.',\n offsets: [0, 90, 180, 270],\n roles: ['primary', 'secondary', 'tertiary', 'quaternary'],\n roleCount: 4,\n },\n] as const satisfies readonly ColorHarmonyCatalogEntry[];\n\nexport type ColorHarmonyDefinition = (typeof COLOR_HARMONY_CATALOG)[number];\nexport type ColorHarmony = ColorHarmonyDefinition['id'];\n\nexport const COLOR_HARMONIES: readonly ColorHarmony[] = COLOR_HARMONY_CATALOG.map(({ id }) => id);\n\nexport const MIN_HUEFUL_CHROMA = 0.015;\n\nexport type HarmonyGenerationWarningCode = 'achromatic_primary' | 'low_chroma_primary';\n\nexport interface HarmonyGenerationWarning {\n readonly code: HarmonyGenerationWarningCode;\n readonly message: string;\n}\n\nexport interface HarmonyGenerationDiagnostics {\n readonly isHueReliable: boolean;\n readonly primaryChroma: number;\n readonly warnings: readonly HarmonyGenerationWarning[];\n}\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 diagnostics: HarmonyGenerationDiagnostics;\n}\n\n/***\n Create diagnostics for the reliability of generated hue relationships.\n*/\nfunction createHarmonyGenerationDiagnostics(primaryChroma: number): HarmonyGenerationDiagnostics {\n const warnings: HarmonyGenerationWarning[] = [];\n if (primaryChroma === 0) {\n warnings.push({\n code: 'achromatic_primary',\n message:\n 'The selected primary is achromatic, so generated hue relationships are not visible.',\n });\n } else if (primaryChroma < MIN_HUEFUL_CHROMA) {\n warnings.push({\n code: 'low_chroma_primary',\n message: 'The selected primary has low chroma, so generated hue relationships may be weak.',\n });\n }\n return {\n isHueReliable: primaryChroma >= MIN_HUEFUL_CHROMA,\n primaryChroma,\n warnings,\n };\n}\n\n/***\n Resolve a complete harmony definition from the canonical public catalog.\n*/\nfunction getHarmonyDefinition(harmony: ColorHarmony): ColorHarmonyDefinition {\n const definition = COLOR_HARMONY_CATALOG.find(({ id }) => id === harmony);\n if (!definition) {\n throw new Error(`[color-theory] Missing definition for harmony ${harmony}.`);\n }\n return definition;\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 definition = getHarmonyDefinition(harmony);\n const baseHue = normalizeHueDegrees(base.h);\n\n const colors = definition.roles.map((role, index): GeneratedHarmonyRoleColor => {\n const hueDegrees = normalizeHueDegrees(baseHue + (definition.offsets.at(index) ?? 0));\n return {\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 diagnostics: createHarmonyGenerationDiagnostics(base.c),\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
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type GeneratedHarmonyRoleColors } from './harmony';
|
|
2
2
|
import type { HexColor } from './hex';
|
|
3
3
|
import { type ColorSwatch, type ColorSwatchDiagnostics } from './swatches';
|
|
4
|
-
export declare const MIN_HUEFUL_CHROMA = 0.015;
|
|
5
4
|
export interface NeutralSwatchResult {
|
|
6
5
|
neutralKeyColor: HexColor;
|
|
7
6
|
neutral: ColorSwatch;
|
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,
|
|
1
|
+
{"version":3,"file":"neutral.d.ts","sourceRoot":"","sources":["../src/neutral.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,0BAA0B,EAAqB,MAAM,WAAW,CAAC;AAClG,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEtC,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,sBAAsB,EAE5B,MAAM,YAAY,CAAC;AAEpB,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
|
@@ -1,22 +1,16 @@
|
|
|
1
|
+
import { MIN_HUEFUL_CHROMA } from './harmony';
|
|
1
2
|
import { normalizeHueDegrees, oklchToHex, parseHexToOklch } from './internal-culori';
|
|
2
3
|
import { COLOR_SWATCH_BASE_LIGHTNESS, generateColorSwatch, } from './swatches';
|
|
3
|
-
export const MIN_HUEFUL_CHROMA = 0.015;
|
|
4
4
|
/***
|
|
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.
|