@tokens-studio/tokenscript-schemas 0.0.11 → 0.0.13
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/dist/cli/index.cjs +31 -8
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +30 -8
- package/dist/cli/index.js.map +1 -1
- package/package.json +2 -1
- package/src/bundler/bundle-schema.ts +146 -0
- package/src/bundler/index.ts +151 -0
- package/src/bundler/schema-dependency-resolver.ts +299 -0
- package/src/bundler/selective-bundler.test.ts +94 -0
- package/src/bundler/selective-bundler.ts +159 -0
- package/src/bundler/types.ts +93 -0
- package/src/bundler/utils.ts +74 -0
- package/src/cli/commands/bundle.integration.test.ts +153 -0
- package/src/cli/commands/bundle.test.ts +57 -0
- package/src/cli/commands/bundle.ts +237 -0
- package/src/cli/commands/list.ts +109 -0
- package/src/cli/config-schema.ts +36 -0
- package/src/cli/index.ts +50 -0
- package/src/cli/output-generator.ts +63 -0
- package/src/downloader/index.ts +248 -0
- package/src/downloader/types.ts +48 -0
- package/src/index.ts +8 -0
- package/src/schemas/functions/adjust_chroma/adjust-chroma.tokenscript +27 -0
- package/src/schemas/functions/adjust_chroma/schema.json +48 -0
- package/src/schemas/functions/adjust_chroma/unit.test.ts +76 -0
- package/src/schemas/functions/adjust_hue/adjust-hue.tokenscript +32 -0
- package/src/schemas/functions/adjust_hue/schema.json +48 -0
- package/src/schemas/functions/adjust_hue/unit.test.ts +68 -0
- package/src/schemas/functions/adjust_lightness/adjust-lightness.tokenscript +31 -0
- package/src/schemas/functions/adjust_lightness/schema.json +48 -0
- package/src/schemas/functions/adjust_lightness/unit.test.ts +88 -0
- package/src/schemas/functions/adjust_to_contrast/adjust-to-contrast.tokenscript +131 -0
- package/src/schemas/functions/adjust_to_contrast/schema.json +56 -0
- package/src/schemas/functions/adjust_to_contrast/unit.test.ts +81 -0
- package/src/schemas/functions/alpha_blend/alpha-blend.tokenscript +46 -0
- package/src/schemas/functions/alpha_blend/schema.json +28 -0
- package/src/schemas/functions/alpha_blend/unit.test.ts +135 -0
- package/src/schemas/functions/alpha_scale/alpha-scale.tokenscript +38 -0
- package/src/schemas/functions/alpha_scale/schema.json +24 -0
- package/src/schemas/functions/alpha_scale/unit.test.ts +50 -0
- package/src/schemas/functions/analogous/analogous.tokenscript +47 -0
- package/src/schemas/functions/analogous/schema.json +28 -0
- package/src/schemas/functions/analogous/unit.test.ts +64 -0
- package/src/schemas/functions/apca_contrast/apca-contrast.tokenscript +129 -0
- package/src/schemas/functions/apca_contrast/schema.json +24 -0
- package/src/schemas/functions/apca_contrast/unit.test.ts +259 -0
- package/src/schemas/functions/are_similar/are-similar.tokenscript +24 -0
- package/src/schemas/functions/are_similar/schema.json +57 -0
- package/src/schemas/functions/are_similar/unit.test.ts +57 -0
- package/src/schemas/functions/auto_text_color/auto-text-color.tokenscript +41 -0
- package/src/schemas/functions/auto_text_color/schema.json +53 -0
- package/src/schemas/functions/auto_text_color/unit.test.ts +122 -0
- package/src/schemas/functions/best_contrast/best-contrast.tokenscript +66 -0
- package/src/schemas/functions/best_contrast/schema.json +24 -0
- package/src/schemas/functions/best_contrast/unit.test.ts +64 -0
- package/src/schemas/functions/chroma/chroma.tokenscript +12 -0
- package/src/schemas/functions/chroma/schema.json +44 -0
- package/src/schemas/functions/chroma/unit.test.ts +86 -0
- package/src/schemas/functions/clamp_chroma/clamp-chroma.tokenscript +21 -0
- package/src/schemas/functions/clamp_chroma/schema.json +52 -0
- package/src/schemas/functions/clamp_chroma/unit.test.ts +60 -0
- package/src/schemas/functions/clamp_lightness/clamp-lightness.tokenscript +21 -0
- package/src/schemas/functions/clamp_lightness/schema.json +52 -0
- package/src/schemas/functions/clamp_lightness/unit.test.ts +76 -0
- package/src/schemas/functions/clamp_to_gamut/clamp-to-gamut.tokenscript +41 -0
- package/src/schemas/functions/clamp_to_gamut/schema.json +20 -0
- package/src/schemas/functions/clamp_to_gamut/unit.test.ts +167 -0
- package/src/schemas/functions/complement/complement.tokenscript +21 -0
- package/src/schemas/functions/complement/schema.json +20 -0
- package/src/schemas/functions/complement/unit.test.ts +81 -0
- package/src/schemas/functions/contrast_ratio/contrast-ratio.tokenscript +36 -0
- package/src/schemas/functions/contrast_ratio/schema.json +24 -0
- package/src/schemas/functions/contrast_ratio/unit.test.ts +91 -0
- package/src/schemas/functions/cooler/cooler.tokenscript +45 -0
- package/src/schemas/functions/cooler/schema.json +43 -0
- package/src/schemas/functions/cooler/unit.test.ts +69 -0
- package/src/schemas/functions/darken/darken.tokenscript +37 -0
- package/src/schemas/functions/darken/schema.json +24 -0
- package/src/schemas/functions/darken/unit.test.ts +105 -0
- package/src/schemas/functions/delta_e_2000/delta-e-2000.tokenscript +184 -0
- package/src/schemas/functions/delta_e_2000/schema.json +36 -0
- package/src/schemas/functions/delta_e_2000/unit.test.ts +243 -0
- package/src/schemas/functions/delta_e_76/delta-e-76.tokenscript +45 -0
- package/src/schemas/functions/delta_e_76/schema.json +24 -0
- package/src/schemas/functions/delta_e_76/unit.test.ts +123 -0
- package/src/schemas/functions/delta_e_ok/delta-e-ok.tokenscript +43 -0
- package/src/schemas/functions/delta_e_ok/schema.json +24 -0
- package/src/schemas/functions/delta_e_ok/unit.test.ts +235 -0
- package/src/schemas/functions/desaturate/desaturate.tokenscript +32 -0
- package/src/schemas/functions/desaturate/schema.json +24 -0
- package/src/schemas/functions/desaturate/unit.test.ts +54 -0
- package/src/schemas/functions/distributed/distributed.tokenscript +54 -0
- package/src/schemas/functions/distributed/schema.json +32 -0
- package/src/schemas/functions/distributed/unit.test.ts +58 -0
- package/src/schemas/functions/diverging/diverging.tokenscript +58 -0
- package/src/schemas/functions/diverging/schema.json +32 -0
- package/src/schemas/functions/diverging/unit.test.ts +70 -0
- package/src/schemas/functions/grayscale/grayscale.tokenscript +17 -0
- package/src/schemas/functions/grayscale/schema.json +20 -0
- package/src/schemas/functions/grayscale/unit.test.ts +79 -0
- package/src/schemas/functions/harmonize/harmonize.tokenscript +61 -0
- package/src/schemas/functions/harmonize/schema.json +52 -0
- package/src/schemas/functions/harmonize/unit.test.ts +56 -0
- package/src/schemas/functions/hue/hue.tokenscript +12 -0
- package/src/schemas/functions/hue/schema.json +44 -0
- package/src/schemas/functions/hue/unit.test.ts +75 -0
- package/src/schemas/functions/hue_difference/hue-difference.tokenscript +42 -0
- package/src/schemas/functions/hue_difference/schema.json +24 -0
- package/src/schemas/functions/hue_difference/unit.test.ts +125 -0
- package/src/schemas/functions/in_gamut/in-gamut.tokenscript +51 -0
- package/src/schemas/functions/in_gamut/schema.json +24 -0
- package/src/schemas/functions/in_gamut/unit.test.ts +178 -0
- package/src/schemas/functions/interpolate/interpolate.tokenscript +61 -0
- package/src/schemas/functions/interpolate/schema.json +52 -0
- package/src/schemas/functions/interpolate/unit.test.ts +96 -0
- package/src/schemas/functions/invert/invert-initializer.tokenscript +29 -0
- package/src/schemas/functions/invert/schema.json +20 -0
- package/src/schemas/functions/invert/unit.test.ts +216 -0
- package/src/schemas/functions/is_cool/is-cool.tokenscript +41 -0
- package/src/schemas/functions/is_cool/schema.json +20 -0
- package/src/schemas/functions/is_cool/unit.test.ts +189 -0
- package/src/schemas/functions/is_dark/is-dark.tokenscript +16 -0
- package/src/schemas/functions/is_dark/schema.json +24 -0
- package/src/schemas/functions/is_dark/unit.test.ts +87 -0
- package/src/schemas/functions/is_light/is-light.tokenscript +16 -0
- package/src/schemas/functions/is_light/schema.json +24 -0
- package/src/schemas/functions/is_light/unit.test.ts +86 -0
- package/src/schemas/functions/is_neutral/is-neutral.tokenscript +16 -0
- package/src/schemas/functions/is_neutral/schema.json +53 -0
- package/src/schemas/functions/is_neutral/unit.test.ts +85 -0
- package/src/schemas/functions/is_warm/is-warm.tokenscript +62 -0
- package/src/schemas/functions/is_warm/schema.json +20 -0
- package/src/schemas/functions/is_warm/unit.test.ts +161 -0
- package/src/schemas/functions/lighten/lighten.tokenscript +37 -0
- package/src/schemas/functions/lighten/schema.json +24 -0
- package/src/schemas/functions/lighten/unit.test.ts +109 -0
- package/src/schemas/functions/lightness/lightness.tokenscript +12 -0
- package/src/schemas/functions/lightness/schema.json +49 -0
- package/src/schemas/functions/lightness/unit.test.ts +99 -0
- package/src/schemas/functions/luminance/luminance.tokenscript +16 -0
- package/src/schemas/functions/luminance/schema.json +20 -0
- package/src/schemas/functions/luminance/unit.test.ts +105 -0
- package/src/schemas/functions/meets_contrast/meets-contrast.tokenscript +49 -0
- package/src/schemas/functions/meets_contrast/schema.json +28 -0
- package/src/schemas/functions/meets_contrast/unit.test.ts +170 -0
- package/src/schemas/functions/mix/mix.tokenscript +47 -0
- package/src/schemas/functions/mix/schema.json +28 -0
- package/src/schemas/functions/mix/unit.test.ts +95 -0
- package/src/schemas/functions/monochromatic/monochromatic.tokenscript +72 -0
- package/src/schemas/functions/monochromatic/schema.json +24 -0
- package/src/schemas/functions/monochromatic/unit.test.ts +91 -0
- package/src/schemas/functions/muted/muted.tokenscript +25 -0
- package/src/schemas/functions/muted/schema.json +48 -0
- package/src/schemas/functions/muted/unit.test.ts +100 -0
- package/src/schemas/functions/neutral_variant/neutral-variant.tokenscript +23 -0
- package/src/schemas/functions/neutral_variant/schema.json +48 -0
- package/src/schemas/functions/neutral_variant/unit.test.ts +102 -0
- package/src/schemas/functions/relative_luminance/relative-luminance.tokenscript +15 -0
- package/src/schemas/functions/relative_luminance/schema.json +49 -0
- package/src/schemas/functions/relative_luminance/unit.test.ts +104 -0
- package/src/schemas/functions/rotate_hue/rotate-hue.tokenscript +20 -0
- package/src/schemas/functions/rotate_hue/schema.json +24 -0
- package/src/schemas/functions/rotate_hue/unit.test.ts +86 -0
- package/src/schemas/functions/saturate/saturate.tokenscript +33 -0
- package/src/schemas/functions/saturate/schema.json +24 -0
- package/src/schemas/functions/saturate/unit.test.ts +59 -0
- package/src/schemas/functions/scale_chroma/scale-chroma.tokenscript +22 -0
- package/src/schemas/functions/scale_chroma/schema.json +48 -0
- package/src/schemas/functions/scale_chroma/unit.test.ts +79 -0
- package/src/schemas/functions/scale_lightness/scale-lightness.tokenscript +23 -0
- package/src/schemas/functions/scale_lightness/schema.json +48 -0
- package/src/schemas/functions/scale_lightness/unit.test.ts +73 -0
- package/src/schemas/functions/sepia/schema.json +48 -0
- package/src/schemas/functions/sepia/sepia.tokenscript +54 -0
- package/src/schemas/functions/sepia/unit.test.ts +88 -0
- package/src/schemas/functions/set_chroma/schema.json +24 -0
- package/src/schemas/functions/set_chroma/set-chroma.tokenscript +18 -0
- package/src/schemas/functions/set_chroma/unit.test.ts +79 -0
- package/src/schemas/functions/set_hue/schema.json +24 -0
- package/src/schemas/functions/set_hue/set-hue.tokenscript +18 -0
- package/src/schemas/functions/set_hue/unit.test.ts +90 -0
- package/src/schemas/functions/set_lightness/schema.json +24 -0
- package/src/schemas/functions/set_lightness/set-lightness.tokenscript +18 -0
- package/src/schemas/functions/set_lightness/unit.test.ts +80 -0
- package/src/schemas/functions/shade_scale/schema.json +24 -0
- package/src/schemas/functions/shade_scale/shade-scale.tokenscript +61 -0
- package/src/schemas/functions/shade_scale/unit.test.ts +64 -0
- package/src/schemas/functions/split_complement/schema.json +24 -0
- package/src/schemas/functions/split_complement/split-complement.tokenscript +38 -0
- package/src/schemas/functions/split_complement/unit.test.ts +53 -0
- package/src/schemas/functions/steps/schema.json +28 -0
- package/src/schemas/functions/steps/steps.tokenscript +54 -0
- package/src/schemas/functions/steps/unit.test.ts +71 -0
- package/src/schemas/functions/tetradic/schema.json +20 -0
- package/src/schemas/functions/tetradic/tetradic.tokenscript +40 -0
- package/src/schemas/functions/tetradic/unit.test.ts +50 -0
- package/src/schemas/functions/tint_scale/schema.json +32 -0
- package/src/schemas/functions/tint_scale/tint-scale.tokenscript +71 -0
- package/src/schemas/functions/tint_scale/unit.test.ts +64 -0
- package/src/schemas/functions/to_gamut/schema.json +48 -0
- package/src/schemas/functions/to_gamut/to-gamut.tokenscript +96 -0
- package/src/schemas/functions/to_gamut/unit.test.ts +97 -0
- package/src/schemas/functions/triadic/schema.json +20 -0
- package/src/schemas/functions/triadic/triadic.tokenscript +33 -0
- package/src/schemas/functions/triadic/unit.test.ts +64 -0
- package/src/schemas/functions/vibrant/schema.json +48 -0
- package/src/schemas/functions/vibrant/unit.test.ts +55 -0
- package/src/schemas/functions/vibrant/vibrant.tokenscript +29 -0
- package/src/schemas/functions/warmer/schema.json +43 -0
- package/src/schemas/functions/warmer/unit.test.ts +69 -0
- package/src/schemas/functions/warmer/warmer.tokenscript +45 -0
- package/src/schemas/functions/wcag_level/schema.json +48 -0
- package/src/schemas/functions/wcag_level/unit.test.ts +75 -0
- package/src/schemas/functions/wcag_level/wcag-level.tokenscript +50 -0
- package/src/schemas/types/css-color/from-hsl-color.tokenscript +16 -0
- package/src/schemas/types/css-color/from-hwb-color.tokenscript +16 -0
- package/src/schemas/types/css-color/from-lab-color.tokenscript +16 -0
- package/src/schemas/types/css-color/from-lch-color.tokenscript +16 -0
- package/src/schemas/types/css-color/from-oklab-color.tokenscript +16 -0
- package/src/schemas/types/css-color/from-oklch-color.tokenscript +16 -0
- package/src/schemas/types/css-color/from-p3-color.tokenscript +16 -0
- package/src/schemas/types/css-color/from-rgb-color.tokenscript +15 -0
- package/src/schemas/types/css-color/from-srgb-color.tokenscript +16 -0
- package/src/schemas/types/css-color/from-srgb-linear-color.tokenscript +16 -0
- package/src/schemas/types/css-color/from-xyz-d50-color.tokenscript +16 -0
- package/src/schemas/types/css-color/from-xyz-d65-color.tokenscript +16 -0
- package/src/schemas/types/css-color/initializer.tokenscript +13 -0
- package/src/schemas/types/css-color/schema.json +148 -0
- package/src/schemas/types/css-color/unit.test.ts +402 -0
- package/src/schemas/types/hex-color/initializer.tokenscript +3 -0
- package/src/schemas/types/hex-color/schema.json +24 -0
- package/src/schemas/types/hex-color/unit.test.ts +123 -0
- package/src/schemas/types/hsl-color/from-srgb.tokenscript +87 -0
- package/src/schemas/types/hsl-color/initializer.tokenscript +16 -0
- package/src/schemas/types/hsl-color/schema.json +48 -0
- package/src/schemas/types/hsl-color/unit.test.ts +201 -0
- package/src/schemas/types/hsv-color/from-srgb.tokenscript +80 -0
- package/src/schemas/types/hsv-color/initializer.tokenscript +16 -0
- package/src/schemas/types/hsv-color/schema.json +48 -0
- package/src/schemas/types/hsv-color/unit.test.ts +162 -0
- package/src/schemas/types/hwb-color/from-hsv.tokenscript +31 -0
- package/src/schemas/types/hwb-color/initializer.tokenscript +16 -0
- package/src/schemas/types/hwb-color/schema.json +48 -0
- package/src/schemas/types/hwb-color/unit.test.ts +150 -0
- package/src/schemas/types/lab-color/from-xyz-d50.tokenscript +78 -0
- package/src/schemas/types/lab-color/initializer.tokenscript +16 -0
- package/src/schemas/types/lab-color/schema.json +48 -0
- package/src/schemas/types/lab-color/unit.test.ts +263 -0
- package/src/schemas/types/lch-color/from-lab.tokenscript +44 -0
- package/src/schemas/types/lch-color/initializer.tokenscript +16 -0
- package/src/schemas/types/lch-color/schema.json +48 -0
- package/src/schemas/types/lch-color/unit.test.ts +173 -0
- package/src/schemas/types/okhsl-color/from-oklab.tokenscript +410 -0
- package/src/schemas/types/okhsl-color/initializer.tokenscript +24 -0
- package/src/schemas/types/okhsl-color/schema.json +48 -0
- package/src/schemas/types/okhsl-color/unit.test.ts +514 -0
- package/src/schemas/types/okhsv-color/from-oklab.tokenscript +286 -0
- package/src/schemas/types/okhsv-color/initializer.tokenscript +24 -0
- package/src/schemas/types/okhsv-color/schema.json +48 -0
- package/src/schemas/types/okhsv-color/unit.test.ts +499 -0
- package/src/schemas/types/oklab-color/from-okhsl.tokenscript +195 -0
- package/src/schemas/types/oklab-color/from-okhsv.tokenscript +197 -0
- package/src/schemas/types/oklab-color/from-oklch.tokenscript +39 -0
- package/src/schemas/types/oklab-color/from-xyz-d65.tokenscript +43 -0
- package/src/schemas/types/oklab-color/initializer.tokenscript +16 -0
- package/src/schemas/types/oklab-color/schema.json +78 -0
- package/src/schemas/types/oklab-color/unit.test.ts +345 -0
- package/src/schemas/types/oklch-color/from-oklab.tokenscript +45 -0
- package/src/schemas/types/oklch-color/initializer.tokenscript +16 -0
- package/src/schemas/types/oklch-color/schema.json +48 -0
- package/src/schemas/types/oklch-color/unit.test.ts +267 -0
- package/src/schemas/types/p3-color/from-p3-linear.tokenscript +59 -0
- package/src/schemas/types/p3-color/initializer.tokenscript +16 -0
- package/src/schemas/types/p3-color/schema.json +48 -0
- package/src/schemas/types/p3-color/unit.test.ts +119 -0
- package/src/schemas/types/p3-linear-color/from-xyz-d65.tokenscript +47 -0
- package/src/schemas/types/p3-linear-color/initializer.tokenscript +16 -0
- package/src/schemas/types/p3-linear-color/schema.json +48 -0
- package/src/schemas/types/p3-linear-color/unit.test.ts +82 -0
- package/src/schemas/types/rgb-color/from-hex.tokenscript +43 -0
- package/src/schemas/types/rgb-color/initializer.tokenscript +16 -0
- package/src/schemas/types/rgb-color/schema.json +55 -0
- package/src/schemas/types/rgb-color/to-hex.tokenscript +42 -0
- package/src/schemas/types/rgb-color/unit.test.ts +302 -0
- package/src/schemas/types/srgb-color/from-hsl.tokenscript +106 -0
- package/src/schemas/types/srgb-color/from-linear.tokenscript +58 -0
- package/src/schemas/types/srgb-color/from-rgb.tokenscript +20 -0
- package/src/schemas/types/srgb-color/initializer.tokenscript +16 -0
- package/src/schemas/types/srgb-color/schema.json +68 -0
- package/src/schemas/types/srgb-color/unit.test.ts +303 -0
- package/src/schemas/types/srgb-linear-color/from-srgb.tokenscript +55 -0
- package/src/schemas/types/srgb-linear-color/from-xyz-d65.tokenscript +34 -0
- package/src/schemas/types/srgb-linear-color/initializer.tokenscript +13 -0
- package/src/schemas/types/srgb-linear-color/schema.json +58 -0
- package/src/schemas/types/srgb-linear-color/unit.test.ts +291 -0
- package/src/schemas/types/xyz-d50-color/from-xyz-d65.tokenscript +36 -0
- package/src/schemas/types/xyz-d50-color/initializer.tokenscript +16 -0
- package/src/schemas/types/xyz-d50-color/schema.json +48 -0
- package/src/schemas/types/xyz-d50-color/unit.test.ts +240 -0
- package/src/schemas/types/xyz-d65-color/from-linear-p3.tokenscript +47 -0
- package/src/schemas/types/xyz-d65-color/from-linear-srgb.tokenscript +38 -0
- package/src/schemas/types/xyz-d65-color/from-oklab.tokenscript +44 -0
- package/src/schemas/types/xyz-d65-color/initializer.tokenscript +16 -0
- package/src/schemas/types/xyz-d65-color/schema.json +68 -0
- package/src/schemas/types/xyz-d65-color/unit.test.ts +319 -0
- package/src/utils/schema-uri.ts +192 -0
- package/src/utils/type.ts +194 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CIE LCH Color Schema Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for the CIE LCH color space (polar form of Lab)
|
|
5
|
+
* Validates against ColorJS for parity
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { log } from "@tests/helpers/logger";
|
|
9
|
+
import { executeWithSchema, getBundledSchema } from "@tests/helpers/schema-test-utils";
|
|
10
|
+
import Color from "colorjs.io";
|
|
11
|
+
import { describe, expect, it } from "vitest";
|
|
12
|
+
import type { ColorSpecification } from "@/bundler/types";
|
|
13
|
+
|
|
14
|
+
// ColorJS reference tolerance
|
|
15
|
+
const TOLERANCE = 1e-6;
|
|
16
|
+
const HUE_TOLERANCE = 1e-3;
|
|
17
|
+
|
|
18
|
+
describe("CIE LCH Color Schema", () => {
|
|
19
|
+
describe("Schema Definition", () => {
|
|
20
|
+
it("should have correct schema structure", async () => {
|
|
21
|
+
const schema = (await getBundledSchema("lch-color")) as ColorSpecification;
|
|
22
|
+
|
|
23
|
+
expect(schema.name).toBe("LCH");
|
|
24
|
+
expect(schema.type).toBe("color");
|
|
25
|
+
expect(schema.schema).toBeDefined();
|
|
26
|
+
expect(schema.schema?.properties).toHaveProperty("l");
|
|
27
|
+
expect(schema.schema?.properties).toHaveProperty("c");
|
|
28
|
+
expect(schema.schema?.properties).toHaveProperty("h");
|
|
29
|
+
expect(schema.schema?.required).toEqual(["l", "c", "h"]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should have lch initializer", async () => {
|
|
33
|
+
const schema = (await getBundledSchema("lch-color")) as ColorSpecification;
|
|
34
|
+
|
|
35
|
+
expect(schema.initializers).toHaveLength(1);
|
|
36
|
+
expect(schema.initializers[0].keyword).toBe("lch");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should have conversion from Lab", async () => {
|
|
40
|
+
const schema = (await getBundledSchema("lch-color")) as ColorSpecification;
|
|
41
|
+
|
|
42
|
+
expect(schema.conversions).toHaveLength(1);
|
|
43
|
+
|
|
44
|
+
const labToLch = schema.conversions.find((c: { source: string }) =>
|
|
45
|
+
c.source.includes("lab-color"),
|
|
46
|
+
);
|
|
47
|
+
expect(labToLch).toBeDefined();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("Full Conversion Chain: sRGB → ... → Lab → LCH", () => {
|
|
52
|
+
it("should convert sRGB red through to LCH", async () => {
|
|
53
|
+
const result = await executeWithSchema(
|
|
54
|
+
"lch-color",
|
|
55
|
+
"type",
|
|
56
|
+
`
|
|
57
|
+
variable srgb: Color.SRGB;
|
|
58
|
+
srgb.r = 1;
|
|
59
|
+
srgb.g = 0;
|
|
60
|
+
srgb.b = 0;
|
|
61
|
+
|
|
62
|
+
variable linear: Color.LinearSRGB = srgb.to.linearsrgb();
|
|
63
|
+
variable xyz65: Color.XYZD65 = linear.to.xyzd65();
|
|
64
|
+
variable xyz50: Color.XYZD50 = xyz65.to.xyzd50();
|
|
65
|
+
variable lab: Color.Lab = xyz50.to.lab();
|
|
66
|
+
lab.to.lch()
|
|
67
|
+
`,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// ColorJS reference
|
|
71
|
+
const colorJS = new Color("srgb", [1, 0, 0]).to("lch");
|
|
72
|
+
|
|
73
|
+
log.info(`\n=== sRGB RED → LCH (full chain) ===`);
|
|
74
|
+
log.info(
|
|
75
|
+
`TokenScript: { l: ${(result as any).value.l.value}, c: ${(result as any).value.c.value}, h: ${(result as any).value.h.value} }`,
|
|
76
|
+
);
|
|
77
|
+
log.info(
|
|
78
|
+
`ColorJS: { l: ${colorJS.coords[0]}, c: ${colorJS.coords[1]}, h: ${colorJS.coords[2]} }`,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
expect((result as any).value.l.value).toBeCloseTo(colorJS.coords[0], 5);
|
|
82
|
+
expect((result as any).value.c.value).toBeCloseTo(colorJS.coords[1], 5);
|
|
83
|
+
expect((result as any).value.h.value).toBeCloseTo(colorJS.coords[2], 1);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("ColorJS Parity", () => {
|
|
88
|
+
const testCases = [
|
|
89
|
+
{ name: "red", srgb: [1, 0, 0] },
|
|
90
|
+
{ name: "green", srgb: [0, 1, 0] },
|
|
91
|
+
{ name: "blue", srgb: [0, 0, 1] },
|
|
92
|
+
{ name: "cyan", srgb: [0, 1, 1] },
|
|
93
|
+
{ name: "magenta", srgb: [1, 0, 1] },
|
|
94
|
+
{ name: "yellow", srgb: [1, 1, 0] },
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
for (const { name, srgb } of testCases) {
|
|
98
|
+
it(`should match ColorJS for ${name}`, async () => {
|
|
99
|
+
const result = await executeWithSchema(
|
|
100
|
+
"lch-color",
|
|
101
|
+
"type",
|
|
102
|
+
`
|
|
103
|
+
variable srgb: Color.SRGB;
|
|
104
|
+
srgb.r = ${srgb[0]};
|
|
105
|
+
srgb.g = ${srgb[1]};
|
|
106
|
+
srgb.b = ${srgb[2]};
|
|
107
|
+
|
|
108
|
+
variable linear: Color.LinearSRGB = srgb.to.linearsrgb();
|
|
109
|
+
variable xyz65: Color.XYZD65 = linear.to.xyzd65();
|
|
110
|
+
variable xyz50: Color.XYZD50 = xyz65.to.xyzd50();
|
|
111
|
+
variable lab: Color.Lab = xyz50.to.lab();
|
|
112
|
+
lab.to.lch()
|
|
113
|
+
`,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// ColorJS reference
|
|
117
|
+
const colorJS = new Color("srgb", srgb as [number, number, number]).to("lch");
|
|
118
|
+
|
|
119
|
+
const tsL = (result as any).value.l.value;
|
|
120
|
+
const tsC = (result as any).value.c.value;
|
|
121
|
+
const tsH = (result as any).value.h.value;
|
|
122
|
+
|
|
123
|
+
const diffL = Math.abs(tsL - colorJS.coords[0]);
|
|
124
|
+
const diffC = Math.abs(tsC - colorJS.coords[1]);
|
|
125
|
+
let diffH = Math.abs(tsH - colorJS.coords[2]);
|
|
126
|
+
if (diffH > 180) diffH = 360 - diffH;
|
|
127
|
+
|
|
128
|
+
const maxDiff = Math.max(diffL, diffC);
|
|
129
|
+
|
|
130
|
+
log.info(`\n=== ${name.toUpperCase()} ColorJS Parity ===`);
|
|
131
|
+
log.info(`Input sRGB: { r: ${srgb[0]}, g: ${srgb[1]}, b: ${srgb[2]} }`);
|
|
132
|
+
log.info(
|
|
133
|
+
`TokenScript: { l: ${tsL.toFixed(4)}, c: ${tsC.toFixed(4)}, h: ${tsH.toFixed(3)} }`,
|
|
134
|
+
);
|
|
135
|
+
log.info(
|
|
136
|
+
`ColorJS: { l: ${colorJS.coords[0].toFixed(4)}, c: ${colorJS.coords[1].toFixed(4)}, h: ${colorJS.coords[2].toFixed(3)} }`,
|
|
137
|
+
);
|
|
138
|
+
log.info(`Max Diff (L,C): ${maxDiff.toExponential(2)}`);
|
|
139
|
+
log.info(`Hue Diff: ${diffH.toExponential(2)}`);
|
|
140
|
+
log.info(
|
|
141
|
+
`Status: ${maxDiff < TOLERANCE && diffH < HUE_TOLERANCE ? "✅ PASS" : "❌ FAIL"}`,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
expect(maxDiff).toBeLessThan(TOLERANCE);
|
|
145
|
+
expect(diffH).toBeLessThan(HUE_TOLERANCE);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("Edge Cases", () => {
|
|
151
|
+
it("should handle neutral gray (chroma ≈ 0)", async () => {
|
|
152
|
+
const result = await executeWithSchema(
|
|
153
|
+
"lch-color",
|
|
154
|
+
"type",
|
|
155
|
+
`
|
|
156
|
+
variable srgb: Color.SRGB;
|
|
157
|
+
srgb.r = 0.5;
|
|
158
|
+
srgb.g = 0.5;
|
|
159
|
+
srgb.b = 0.5;
|
|
160
|
+
|
|
161
|
+
variable linear: Color.LinearSRGB = srgb.to.linearsrgb();
|
|
162
|
+
variable xyz65: Color.XYZD65 = linear.to.xyzd65();
|
|
163
|
+
variable xyz50: Color.XYZD50 = xyz65.to.xyzd50();
|
|
164
|
+
variable lab: Color.Lab = xyz50.to.lab();
|
|
165
|
+
lab.to.lch()
|
|
166
|
+
`,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// Gray should have chroma ≈ 0
|
|
170
|
+
expect((result as any).value.c.value).toBeLessThan(0.01);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
// OKLab to OKHSL Conversion
|
|
2
|
+
// Reference: Björn Ottosson - "A perceptual color picker: OKHSL and OKHSV"
|
|
3
|
+
// URL: https://bottosson.github.io/posts/colorpicker/
|
|
4
|
+
// Reference: https://github.com/color-js/color.js/blob/main/src/spaces/okhsl.js
|
|
5
|
+
// Reference: https://bottosson.github.io/posts/oklab/ (original OKLab paper)
|
|
6
|
+
//
|
|
7
|
+
// OKHSL provides a perceptually uniform HSL with proper saturation scaling.
|
|
8
|
+
// The algorithm uses three key chroma values (C_0, C_mid, C_max) and
|
|
9
|
+
// performs piecewise interpolation to map chroma to saturation.
|
|
10
|
+
//
|
|
11
|
+
// This implementation includes the FULL findGamutIntersection algorithm
|
|
12
|
+
// with Halley's method refinement for accurate gamut boundary detection.
|
|
13
|
+
//
|
|
14
|
+
// Input: Color.OKLab with l (0-1), a, b coordinates
|
|
15
|
+
// Output: Color.OKHSL with h (0-360), s (0-1), l (0-1)
|
|
16
|
+
|
|
17
|
+
variable lab_l: Number = input.l;
|
|
18
|
+
variable lab_a: Number = input.a;
|
|
19
|
+
variable lab_b: Number = input.b;
|
|
20
|
+
|
|
21
|
+
// Native constants
|
|
22
|
+
variable pi_val: Number = pi();
|
|
23
|
+
variable float_max: Number = 999999999;
|
|
24
|
+
|
|
25
|
+
// Toe function constants
|
|
26
|
+
// K3 = (1 + K1) / (1 + K2)
|
|
27
|
+
variable toe_k1: Number = 0.206;
|
|
28
|
+
variable toe_k2: Number = 0.03;
|
|
29
|
+
variable toe_k3: Number = 1.17009708737864;
|
|
30
|
+
|
|
31
|
+
// LMS to OKLab matrix coefficients (LabtoLMS_M columns 1,2)
|
|
32
|
+
variable lab_lms_kl_a: Number = 0.3963377774;
|
|
33
|
+
variable lab_lms_kl_b: Number = 0.2158037573;
|
|
34
|
+
variable lab_lms_km_a: Number = -0.1055613458;
|
|
35
|
+
variable lab_lms_km_b: Number = -0.0638541728;
|
|
36
|
+
variable lab_lms_ks_a: Number = -0.0894841775;
|
|
37
|
+
variable lab_lms_ks_b: Number = -1.2914855480;
|
|
38
|
+
|
|
39
|
+
// LMS to sRGB-linear matrix
|
|
40
|
+
variable lms_r0: Number = 4.0767416360759583;
|
|
41
|
+
variable lms_r1: Number = -3.3077115392580629;
|
|
42
|
+
variable lms_r2: Number = 0.2309699031821043;
|
|
43
|
+
variable lms_g0: Number = -1.2684379732850315;
|
|
44
|
+
variable lms_g1: Number = 2.6097573492876882;
|
|
45
|
+
variable lms_g2: Number = -0.3413193760026570;
|
|
46
|
+
variable lms_b0: Number = -0.0041960761386756;
|
|
47
|
+
variable lms_b1: Number = -0.7034186179359362;
|
|
48
|
+
variable lms_b2: Number = 1.7076146940746117;
|
|
49
|
+
|
|
50
|
+
// RGB limit coefficients for determining which channel clips first
|
|
51
|
+
variable red_limit_a: Number = -1.8817031;
|
|
52
|
+
variable red_limit_b: Number = -0.80936501;
|
|
53
|
+
variable green_limit_a: Number = 1.8144408;
|
|
54
|
+
variable green_limit_b: Number = -1.19445267;
|
|
55
|
+
|
|
56
|
+
// Red Kn coefficients
|
|
57
|
+
variable red_k0: Number = 1.19086277;
|
|
58
|
+
variable red_k1: Number = 1.76576728;
|
|
59
|
+
variable red_k2: Number = 0.59662641;
|
|
60
|
+
variable red_k3: Number = 0.75515197;
|
|
61
|
+
variable red_k4: Number = 0.56771245;
|
|
62
|
+
|
|
63
|
+
// Green Kn coefficients
|
|
64
|
+
variable green_k0: Number = 0.73956515;
|
|
65
|
+
variable green_k1: Number = -0.45954404;
|
|
66
|
+
variable green_k2: Number = 0.08285427;
|
|
67
|
+
variable green_k3: Number = 0.12541073;
|
|
68
|
+
variable green_k4: Number = -0.14503204;
|
|
69
|
+
|
|
70
|
+
// Blue Kn coefficients
|
|
71
|
+
variable blue_k0: Number = 1.35733652;
|
|
72
|
+
variable blue_k1: Number = -0.00915799;
|
|
73
|
+
variable blue_k2: Number = -1.1513021;
|
|
74
|
+
variable blue_k3: Number = -0.50559606;
|
|
75
|
+
variable blue_k4: Number = 0.00692167;
|
|
76
|
+
|
|
77
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
78
|
+
// Step 1: Convert to polar coordinates and apply toe function
|
|
79
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
80
|
+
variable c: Number = sqrt(lab_a * lab_a + lab_b * lab_b);
|
|
81
|
+
|
|
82
|
+
// Apply toe function: toe(x) = 0.5 * (k3*x - k1 + sqrt((k3*x - k1)^2 + 4*k2*k3*x))
|
|
83
|
+
variable l: Number = lab_l;
|
|
84
|
+
if (lab_l > 0.0001 && lab_l < 0.9999) [
|
|
85
|
+
variable term: Number = toe_k3 * lab_l - toe_k1;
|
|
86
|
+
l = 0.5 * (term + sqrt(term * term + 4 * toe_k2 * toe_k3 * lab_l));
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
// Compute hue using atan2(-b, -a) + 0.5 (ColorJS convention)
|
|
90
|
+
variable h: Number = 0;
|
|
91
|
+
if (c > 0.00001) [
|
|
92
|
+
variable h_normalized: Number = 0.5 + atan2(-lab_b, -lab_a) / (2 * pi_val);
|
|
93
|
+
h = h_normalized * 360;
|
|
94
|
+
if (h < 0) [ h = h + 360; ];
|
|
95
|
+
if (h >= 360) [ h = h - 360; ];
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
// Initialize saturation
|
|
99
|
+
variable s: Number = 0;
|
|
100
|
+
|
|
101
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
102
|
+
// Step 2: Check if chromatic (non-achromatic, non-edge)
|
|
103
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
104
|
+
variable is_chromatic: Number = 1;
|
|
105
|
+
if (c < 0.00001) [ is_chromatic = 0; ];
|
|
106
|
+
if (l < 0.0001) [ is_chromatic = 0; ];
|
|
107
|
+
if (l > 0.9999) [ is_chromatic = 0; ];
|
|
108
|
+
|
|
109
|
+
if (is_chromatic > 0.5) [
|
|
110
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
111
|
+
// Step 3: Normalize hue direction (a_, b_)
|
|
112
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
113
|
+
variable a_: Number = lab_a / c;
|
|
114
|
+
variable b_: Number = lab_b / c;
|
|
115
|
+
|
|
116
|
+
// Pre-compute LMS coefficients
|
|
117
|
+
variable kl: Number = lab_lms_kl_a * a_ + lab_lms_kl_b * b_;
|
|
118
|
+
variable km: Number = lab_lms_km_a * a_ + lab_lms_km_b * b_;
|
|
119
|
+
variable ks: Number = lab_lms_ks_a * a_ + lab_lms_ks_b * b_;
|
|
120
|
+
|
|
121
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
122
|
+
// Step 4: computeMaxSaturation - Find S_max for this hue
|
|
123
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
124
|
+
variable test_red: Number = red_limit_a * a_ + red_limit_b * b_;
|
|
125
|
+
variable test_green: Number = green_limit_a * a_ + green_limit_b * b_;
|
|
126
|
+
|
|
127
|
+
variable k0: Number = blue_k0;
|
|
128
|
+
variable k1_coef: Number = blue_k1;
|
|
129
|
+
variable k2_coef: Number = blue_k2;
|
|
130
|
+
variable k3_coef: Number = blue_k3;
|
|
131
|
+
variable k4_coef: Number = blue_k4;
|
|
132
|
+
variable wl: Number = lms_b0;
|
|
133
|
+
variable wm: Number = lms_b1;
|
|
134
|
+
variable ws: Number = lms_b2;
|
|
135
|
+
|
|
136
|
+
if (test_red > 1) [
|
|
137
|
+
k0 = red_k0; k1_coef = red_k1; k2_coef = red_k2; k3_coef = red_k3; k4_coef = red_k4;
|
|
138
|
+
wl = lms_r0; wm = lms_r1; ws = lms_r2;
|
|
139
|
+
] else [
|
|
140
|
+
if (test_green > 1) [
|
|
141
|
+
k0 = green_k0; k1_coef = green_k1; k2_coef = green_k2; k3_coef = green_k3; k4_coef = green_k4;
|
|
142
|
+
wl = lms_g0; wm = lms_g1; ws = lms_g2;
|
|
143
|
+
];
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
// Polynomial approximation
|
|
147
|
+
variable s_max: Number = k0 + k1_coef * a_ + k2_coef * b_ + k3_coef * a_ * a_ + k4_coef * a_ * b_;
|
|
148
|
+
|
|
149
|
+
// Halley's method refinement for s_max
|
|
150
|
+
variable l_temp: Number = 1 + s_max * kl;
|
|
151
|
+
variable m_temp: Number = 1 + s_max * km;
|
|
152
|
+
variable s_temp: Number = 1 + s_max * ks;
|
|
153
|
+
|
|
154
|
+
variable l_cubed: Number = l_temp * l_temp * l_temp;
|
|
155
|
+
variable m_cubed: Number = m_temp * m_temp * m_temp;
|
|
156
|
+
variable s_cubed: Number = s_temp * s_temp * s_temp;
|
|
157
|
+
|
|
158
|
+
variable l_ds: Number = 3 * kl * l_temp * l_temp;
|
|
159
|
+
variable m_ds: Number = 3 * km * m_temp * m_temp;
|
|
160
|
+
variable s_ds: Number = 3 * ks * s_temp * s_temp;
|
|
161
|
+
|
|
162
|
+
variable l_ds2: Number = 6 * kl * kl * l_temp;
|
|
163
|
+
variable m_ds2: Number = 6 * km * km * m_temp;
|
|
164
|
+
variable s_ds2: Number = 6 * ks * ks * s_temp;
|
|
165
|
+
|
|
166
|
+
variable f: Number = wl * l_cubed + wm * m_cubed + ws * s_cubed;
|
|
167
|
+
variable f1: Number = wl * l_ds + wm * m_ds + ws * s_ds;
|
|
168
|
+
variable f2: Number = wl * l_ds2 + wm * m_ds2 + ws * s_ds2;
|
|
169
|
+
|
|
170
|
+
variable halley_denom: Number = f1 * f1 - 0.5 * f * f2;
|
|
171
|
+
if (abs(halley_denom) > 0.000001) [
|
|
172
|
+
s_max = s_max - f * f1 / halley_denom;
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
176
|
+
// Step 5: findCusp - Get L_cusp and C_cusp
|
|
177
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
178
|
+
variable l_cusp: Number = 1;
|
|
179
|
+
variable c_cusp: Number = 0;
|
|
180
|
+
|
|
181
|
+
if (s_max > 0) [
|
|
182
|
+
variable cusp_l_: Number = 1 + s_max * kl;
|
|
183
|
+
variable cusp_m_: Number = 1 + s_max * km;
|
|
184
|
+
variable cusp_s_: Number = 1 + s_max * ks;
|
|
185
|
+
|
|
186
|
+
variable cusp_l: Number = cusp_l_ * cusp_l_ * cusp_l_;
|
|
187
|
+
variable cusp_m: Number = cusp_m_ * cusp_m_ * cusp_m_;
|
|
188
|
+
variable cusp_s: Number = cusp_s_ * cusp_s_ * cusp_s_;
|
|
189
|
+
|
|
190
|
+
variable cusp_r: Number = lms_r0 * cusp_l + lms_r1 * cusp_m + lms_r2 * cusp_s;
|
|
191
|
+
variable cusp_g: Number = lms_g0 * cusp_l + lms_g1 * cusp_m + lms_g2 * cusp_s;
|
|
192
|
+
variable cusp_b: Number = lms_b0 * cusp_l + lms_b1 * cusp_m + lms_b2 * cusp_s;
|
|
193
|
+
|
|
194
|
+
variable max_rgb: Number = cusp_r;
|
|
195
|
+
if (cusp_g > max_rgb) [ max_rgb = cusp_g; ];
|
|
196
|
+
if (cusp_b > max_rgb) [ max_rgb = cusp_b; ];
|
|
197
|
+
if (max_rgb < 0.0001) [ max_rgb = 0.0001; ];
|
|
198
|
+
|
|
199
|
+
l_cusp = pow(1 / max_rgb, 0.3333333333333333);
|
|
200
|
+
c_cusp = l_cusp * s_max;
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
204
|
+
// Step 6: findGamutIntersection - Find C_max at current L
|
|
205
|
+
// This is the FULL algorithm with Halley's method for upper half
|
|
206
|
+
//
|
|
207
|
+
// Finds intersection of line: L = L0*(1-t) + t*L1, C = t*C1
|
|
208
|
+
// where L0=L1=lab_l and C1=1 (seeking max chroma at this L)
|
|
209
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
210
|
+
variable c_max: Number = 0;
|
|
211
|
+
|
|
212
|
+
if (l_cusp > 0.0001 && c_cusp > 0.0001) [
|
|
213
|
+
// Parameters for gamut intersection: l1=lab_l, c1=1, l0=lab_l
|
|
214
|
+
variable l1: Number = lab_l;
|
|
215
|
+
variable c1: Number = 1;
|
|
216
|
+
variable l0: Number = lab_l;
|
|
217
|
+
|
|
218
|
+
// Check which half: (l1 - l0) * c_cusp - (l_cusp - l0) * c1
|
|
219
|
+
// Since l1 = l0 = lab_l, this simplifies to: -(l_cusp - lab_l) * 1 = lab_l - l_cusp
|
|
220
|
+
variable half_test: Number = lab_l - l_cusp;
|
|
221
|
+
variable t_intersect: Number = 0;
|
|
222
|
+
|
|
223
|
+
if (half_test <= 0) [
|
|
224
|
+
// Lower half (below cusp) - simple triangle intersection
|
|
225
|
+
// t = (c_cusp * l0) / (c1 * l_cusp + c_cusp * (l0 - l1))
|
|
226
|
+
// Since l0 = l1, denominator = c1 * l_cusp = l_cusp
|
|
227
|
+
variable lower_denom: Number = l_cusp;
|
|
228
|
+
if (abs(lower_denom) > 0.00001) [
|
|
229
|
+
t_intersect = (c_cusp * lab_l) / lower_denom;
|
|
230
|
+
];
|
|
231
|
+
] else [
|
|
232
|
+
// Upper half (above cusp) - triangle + Halley's method
|
|
233
|
+
// First: triangle intersection
|
|
234
|
+
// t = (c_cusp * (l0 - 1)) / (c1 * (l_cusp - 1) + c_cusp * (l0 - l1))
|
|
235
|
+
// Since l0 = l1 = lab_l: t = (c_cusp * (lab_l - 1)) / (l_cusp - 1)
|
|
236
|
+
variable upper_denom: Number = l_cusp - 1;
|
|
237
|
+
if (abs(upper_denom) > 0.00001) [
|
|
238
|
+
t_intersect = c_cusp * (lab_l - 1) / upper_denom;
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
// Halley's method refinement for upper half
|
|
242
|
+
// dl = l1 - l0 = 0, dc = c1 = 1
|
|
243
|
+
variable dl: Number = 0;
|
|
244
|
+
variable dc: Number = 1;
|
|
245
|
+
|
|
246
|
+
variable ldt_: Number = dl + dc * kl;
|
|
247
|
+
variable mdt_: Number = dl + dc * km;
|
|
248
|
+
variable sdt_: Number = dl + dc * ks;
|
|
249
|
+
|
|
250
|
+
// Compute L and C at current t
|
|
251
|
+
variable L_at_t: Number = l0 * (1 - t_intersect) + t_intersect * l1;
|
|
252
|
+
variable C_at_t: Number = t_intersect * c1;
|
|
253
|
+
|
|
254
|
+
// LMS values at (L, C)
|
|
255
|
+
variable l_at: Number = L_at_t + C_at_t * kl;
|
|
256
|
+
variable m_at: Number = L_at_t + C_at_t * km;
|
|
257
|
+
variable s_at: Number = L_at_t + C_at_t * ks;
|
|
258
|
+
|
|
259
|
+
variable l_lms: Number = l_at * l_at * l_at;
|
|
260
|
+
variable m_lms: Number = m_at * m_at * m_at;
|
|
261
|
+
variable s_lms: Number = s_at * s_at * s_at;
|
|
262
|
+
|
|
263
|
+
// First derivatives
|
|
264
|
+
variable ldt: Number = 3 * ldt_ * l_at * l_at;
|
|
265
|
+
variable mdt: Number = 3 * mdt_ * m_at * m_at;
|
|
266
|
+
variable sdt: Number = 3 * sdt_ * s_at * s_at;
|
|
267
|
+
|
|
268
|
+
// Second derivatives
|
|
269
|
+
variable ldt2: Number = 6 * ldt_ * ldt_ * l_at;
|
|
270
|
+
variable mdt2: Number = 6 * mdt_ * mdt_ * m_at;
|
|
271
|
+
variable sdt2: Number = 6 * sdt_ * sdt_ * s_at;
|
|
272
|
+
|
|
273
|
+
// Red channel Halley step
|
|
274
|
+
variable r_val: Number = lms_r0 * l_lms + lms_r1 * m_lms + lms_r2 * s_lms - 1;
|
|
275
|
+
variable r1: Number = lms_r0 * ldt + lms_r1 * mdt + lms_r2 * sdt;
|
|
276
|
+
variable r2: Number = lms_r0 * ldt2 + lms_r1 * mdt2 + lms_r2 * sdt2;
|
|
277
|
+
variable r_denom: Number = r1 * r1 - 0.5 * r_val * r2;
|
|
278
|
+
variable ur: Number = 0;
|
|
279
|
+
variable tr: Number = float_max;
|
|
280
|
+
if (abs(r_denom) > 0.000001) [
|
|
281
|
+
ur = r1 / r_denom;
|
|
282
|
+
if (ur >= 0) [ tr = -r_val * ur; ];
|
|
283
|
+
];
|
|
284
|
+
|
|
285
|
+
// Green channel Halley step
|
|
286
|
+
variable g_val: Number = lms_g0 * l_lms + lms_g1 * m_lms + lms_g2 * s_lms - 1;
|
|
287
|
+
variable g1: Number = lms_g0 * ldt + lms_g1 * mdt + lms_g2 * sdt;
|
|
288
|
+
variable g2: Number = lms_g0 * ldt2 + lms_g1 * mdt2 + lms_g2 * sdt2;
|
|
289
|
+
variable g_denom: Number = g1 * g1 - 0.5 * g_val * g2;
|
|
290
|
+
variable ug: Number = 0;
|
|
291
|
+
variable tg: Number = float_max;
|
|
292
|
+
if (abs(g_denom) > 0.000001) [
|
|
293
|
+
ug = g1 / g_denom;
|
|
294
|
+
if (ug >= 0) [ tg = -g_val * ug; ];
|
|
295
|
+
];
|
|
296
|
+
|
|
297
|
+
// Blue channel Halley step
|
|
298
|
+
variable b_val: Number = lms_b0 * l_lms + lms_b1 * m_lms + lms_b2 * s_lms - 1;
|
|
299
|
+
variable b1: Number = lms_b0 * ldt + lms_b1 * mdt + lms_b2 * sdt;
|
|
300
|
+
variable b2: Number = lms_b0 * ldt2 + lms_b1 * mdt2 + lms_b2 * sdt2;
|
|
301
|
+
variable b_denom: Number = b1 * b1 - 0.5 * b_val * b2;
|
|
302
|
+
variable ub: Number = 0;
|
|
303
|
+
variable tb: Number = float_max;
|
|
304
|
+
if (abs(b_denom) > 0.000001) [
|
|
305
|
+
ub = b1 / b_denom;
|
|
306
|
+
if (ub >= 0) [ tb = -b_val * ub; ];
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
// Take minimum of the three corrections
|
|
310
|
+
variable t_correction: Number = tr;
|
|
311
|
+
if (tg < t_correction) [ t_correction = tg; ];
|
|
312
|
+
if (tb < t_correction) [ t_correction = tb; ];
|
|
313
|
+
if (t_correction < float_max) [
|
|
314
|
+
t_intersect = t_intersect + t_correction;
|
|
315
|
+
];
|
|
316
|
+
];
|
|
317
|
+
|
|
318
|
+
// C_max = t * c1 = t (since c1 = 1)
|
|
319
|
+
c_max = t_intersect;
|
|
320
|
+
if (c_max < 0) [ c_max = 0; ];
|
|
321
|
+
];
|
|
322
|
+
|
|
323
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
324
|
+
// Step 7: getStMid - Polynomial approximation for mid-saturation ST
|
|
325
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
326
|
+
variable st_mid_s: Number = 0.11516993 + 1 / (
|
|
327
|
+
7.44778970 + 4.15901240 * b_ +
|
|
328
|
+
a_ * (-2.19557347 + 1.75198401 * b_ +
|
|
329
|
+
a_ * (-2.13704948 - 10.02301043 * b_ +
|
|
330
|
+
a_ * (-4.24894561 + 5.38770819 * b_ + 4.69891013 * a_))));
|
|
331
|
+
|
|
332
|
+
variable st_mid_t: Number = 0.11239642 + 1 / (
|
|
333
|
+
1.61320320 - 0.68124379 * b_ +
|
|
334
|
+
a_ * (0.40370612 + 0.90148123 * b_ +
|
|
335
|
+
a_ * (-0.27087943 + 0.61223990 * b_ +
|
|
336
|
+
a_ * (0.00299215 - 0.45399568 * b_ - 0.14661872 * a_))));
|
|
337
|
+
|
|
338
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
339
|
+
// Step 8: getCs - Compute C_0, C_mid, C_max with scale factor k
|
|
340
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
341
|
+
variable st_max_s: Number = c_cusp / (l_cusp + 0.0001);
|
|
342
|
+
variable st_max_t: Number = c_cusp / (1 - l_cusp + 0.0001);
|
|
343
|
+
|
|
344
|
+
// Scale factor k = c_max / min(L * st_max_s, (1-L) * st_max_t)
|
|
345
|
+
variable min_st: Number = lab_l * st_max_s;
|
|
346
|
+
variable min_st_t: Number = (1 - lab_l) * st_max_t;
|
|
347
|
+
if (min_st_t < min_st) [ min_st = min_st_t; ];
|
|
348
|
+
|
|
349
|
+
variable k_factor: Number = 1;
|
|
350
|
+
if (min_st > 0.0001 && c_max > 0.0001) [
|
|
351
|
+
k_factor = c_max / min_st;
|
|
352
|
+
];
|
|
353
|
+
|
|
354
|
+
// C_mid = 0.9 * k * sqrt(sqrt(1 / (1/ca^4 + 1/cb^4)))
|
|
355
|
+
variable ca: Number = lab_l * st_mid_s;
|
|
356
|
+
variable cb: Number = (1 - lab_l) * st_mid_t;
|
|
357
|
+
variable ca4: Number = ca * ca * ca * ca;
|
|
358
|
+
variable cb4: Number = cb * cb * cb * cb;
|
|
359
|
+
variable c_mid: Number = 0;
|
|
360
|
+
if (ca4 > 0.0000001 && cb4 > 0.0000001) [
|
|
361
|
+
c_mid = 0.9 * k_factor * sqrt(sqrt(1 / (1 / ca4 + 1 / cb4)));
|
|
362
|
+
];
|
|
363
|
+
|
|
364
|
+
// C_0 using average ST values (0.4, 0.8)
|
|
365
|
+
variable ca0: Number = lab_l * 0.4;
|
|
366
|
+
variable cb0: Number = (1 - lab_l) * 0.8;
|
|
367
|
+
variable ca0_sq: Number = ca0 * ca0;
|
|
368
|
+
variable cb0_sq: Number = cb0 * cb0;
|
|
369
|
+
variable c_0: Number = 0;
|
|
370
|
+
if (ca0_sq > 0.0000001 && cb0_sq > 0.0000001) [
|
|
371
|
+
c_0 = sqrt(1 / (1 / ca0_sq + 1 / cb0_sq));
|
|
372
|
+
];
|
|
373
|
+
|
|
374
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
375
|
+
// Step 9: Compute saturation using piecewise interpolation
|
|
376
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
377
|
+
variable mid: Number = 0.8;
|
|
378
|
+
|
|
379
|
+
if (c < c_mid && c_mid > 0.0001 && c_0 > 0.0001) [
|
|
380
|
+
// Below mid-point: s = t * 0.8 where t = c / (k1 + k2*c)
|
|
381
|
+
variable k1_low: Number = mid * c_0;
|
|
382
|
+
variable k2_low: Number = 1 - k1_low / c_mid;
|
|
383
|
+
variable t_low: Number = c / (k1_low + k2_low * c + 0.0001);
|
|
384
|
+
s = t_low * mid;
|
|
385
|
+
] else [
|
|
386
|
+
if (c_mid > 0.0001 && c_0 > 0.0001 && c_max > c_mid) [
|
|
387
|
+
// Above mid-point: s = 0.8 + 0.2 * t where t = (c - c_mid) / (k1 + k2*(c - c_mid))
|
|
388
|
+
variable mid_inv: Number = 1.25;
|
|
389
|
+
variable k0_high: Number = c_mid;
|
|
390
|
+
variable k1_high: Number = 0.2 * c_mid * c_mid * mid_inv * mid_inv / c_0;
|
|
391
|
+
variable k2_high: Number = 1 - k1_high / (c_max - c_mid + 0.0001);
|
|
392
|
+
variable c_diff: Number = c - k0_high;
|
|
393
|
+
variable t_high: Number = c_diff / (k1_high + k2_high * c_diff + 0.0001);
|
|
394
|
+
s = mid + 0.2 * t_high;
|
|
395
|
+
];
|
|
396
|
+
];
|
|
397
|
+
|
|
398
|
+
// Clamp saturation
|
|
399
|
+
if (s > 1) [ s = 1; ];
|
|
400
|
+
if (s < 0) [ s = 0; ];
|
|
401
|
+
];
|
|
402
|
+
|
|
403
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
404
|
+
// Output
|
|
405
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
406
|
+
variable output: Color.OKHSL;
|
|
407
|
+
output.h = h;
|
|
408
|
+
output.s = s;
|
|
409
|
+
output.l = l;
|
|
410
|
+
output
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// OKHSL Color Initializer
|
|
2
|
+
// Reference: Björn Ottosson - "A perceptual color picker: OKHSL and OKHSV"
|
|
3
|
+
// URL: https://bottosson.github.io/posts/colorpicker/
|
|
4
|
+
//
|
|
5
|
+
// Creates an OKHSL color from hue, saturation, and lightness values.
|
|
6
|
+
// OKHSL is a perceptually uniform version of HSL built on OKLab.
|
|
7
|
+
//
|
|
8
|
+
// Parameters:
|
|
9
|
+
// - h: Hue angle (0-360 degrees), same as OKLCH hue
|
|
10
|
+
// - s: Saturation (0-1), normalized to sRGB gamut boundary
|
|
11
|
+
// - l: Lightness (0-1), perceptually uniform with toe function
|
|
12
|
+
//
|
|
13
|
+
// Input: Object with h, s, l properties
|
|
14
|
+
// Output: Color.OKHSL
|
|
15
|
+
|
|
16
|
+
variable h: Number = input.h;
|
|
17
|
+
variable s: Number = input.s;
|
|
18
|
+
variable l: Number = input.l;
|
|
19
|
+
|
|
20
|
+
variable output: Color.OKHSL;
|
|
21
|
+
output.h = h;
|
|
22
|
+
output.s = s;
|
|
23
|
+
output.l = l;
|
|
24
|
+
output
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "OKHSL",
|
|
3
|
+
"type": "color",
|
|
4
|
+
"description": "OKHSL color space by Björn Ottosson - a perceptually uniform HSL based on OKLab. H is hue (0-360), S is saturation (0-1, normalized to sRGB gamut), L is lightness (0-1). Reference: https://bottosson.github.io/posts/colorpicker/",
|
|
5
|
+
"schema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"h": {
|
|
9
|
+
"type": "number",
|
|
10
|
+
"description": "Hue angle (0-360 degrees), same as OKLCH hue"
|
|
11
|
+
},
|
|
12
|
+
"s": {
|
|
13
|
+
"type": "number",
|
|
14
|
+
"description": "Saturation (0-1), normalized relative to sRGB gamut boundary"
|
|
15
|
+
},
|
|
16
|
+
"l": {
|
|
17
|
+
"type": "number",
|
|
18
|
+
"description": "Lightness (0-1), perceptually uniform with toe function applied"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"required": ["h", "s", "l"],
|
|
22
|
+
"order": ["h", "s", "l"],
|
|
23
|
+
"additionalProperties": false
|
|
24
|
+
},
|
|
25
|
+
"initializers": [
|
|
26
|
+
{
|
|
27
|
+
"title": "OKHSL Color Initializer",
|
|
28
|
+
"keyword": "okhsl",
|
|
29
|
+
"description": "Creates an OKHSL color from H, S, L values",
|
|
30
|
+
"script": {
|
|
31
|
+
"type": "/api/v1/core/tokenscript/0/",
|
|
32
|
+
"script": "./initializer.tokenscript"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"conversions": [
|
|
37
|
+
{
|
|
38
|
+
"source": "/api/v1/core/oklab-color/0/",
|
|
39
|
+
"target": "$self",
|
|
40
|
+
"description": "Converts OKLab to OKHSL using Ottosson's algorithm with Halley's method refinement for gamut boundary",
|
|
41
|
+
"lossless": true,
|
|
42
|
+
"script": {
|
|
43
|
+
"type": "/api/v1/core/tokenscript/0/",
|
|
44
|
+
"script": "./from-oklab.tokenscript"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|