@tenphi/glaze 0.5.7 → 0.6.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/dist/index.d.cts CHANGED
@@ -6,6 +6,7 @@
6
6
  * against a base color. Used by glaze when resolving dependent colors
7
7
  * with `contrast`.
8
8
  */
9
+ type LinearRgb = [number, number, number];
9
10
  type ContrastPreset = 'AA' | 'AAA' | 'AA-large' | 'AAA-large';
10
11
  type MinContrast$1 = number | ContrastPreset;
11
12
  interface FindLightnessForContrastOptions {
@@ -42,6 +43,39 @@ declare function resolveMinContrast(value: MinContrast$1): number;
42
43
  * against a base color, staying as close to `preferredLightness` as possible.
43
44
  */
44
45
  declare function findLightnessForContrast(options: FindLightnessForContrastOptions): FindLightnessForContrastResult;
46
+ interface FindValueForMixContrastOptions {
47
+ /** Preferred mix parameter (0–1). */
48
+ preferredValue: number;
49
+ /** Base color as linear sRGB. */
50
+ baseLinearRgb: LinearRgb;
51
+ /** Target color as linear sRGB. */
52
+ targetLinearRgb: LinearRgb;
53
+ /** WCAG contrast target. */
54
+ contrast: MinContrast$1;
55
+ /**
56
+ * Compute the luminance of the mixed color at parameter t.
57
+ * For opaque: luminance of OKHSL-interpolated color.
58
+ * For transparent: luminance of alpha-composited color over base.
59
+ */
60
+ luminanceAtValue: (t: number) => number;
61
+ /** Convergence threshold. Default: 1e-4. */
62
+ epsilon?: number;
63
+ /** Maximum binary-search iterations per branch. Default: 20. */
64
+ maxIterations?: number;
65
+ }
66
+ interface FindValueForMixContrastResult {
67
+ /** Chosen mix parameter (0–1). */
68
+ value: number;
69
+ /** Achieved WCAG contrast ratio. */
70
+ contrast: number;
71
+ /** Whether the target was reached. */
72
+ met: boolean;
73
+ }
74
+ /**
75
+ * Find the mix parameter (ratio or opacity) that satisfies a WCAG 2 contrast
76
+ * target against a base color, staying as close to `preferredValue` as possible.
77
+ */
78
+ declare function findValueForMixContrast(options: FindValueForMixContrastOptions): FindValueForMixContrastResult;
45
79
  //#endregion
46
80
  //#region src/types.d.ts
47
81
  /** A value or [normal, high-contrast] pair. */
@@ -144,7 +178,41 @@ interface ShadowColorDef {
144
178
  /** Override default tuning. Merged field-by-field with global `shadowTuning`. */
145
179
  tuning?: ShadowTuning;
146
180
  }
147
- type ColorDef = RegularColorDef | ShadowColorDef;
181
+ interface MixColorDef {
182
+ type: 'mix';
183
+ /** Background/base color name — the "from" color. */
184
+ base: string;
185
+ /** Target color name — the "to" color to mix toward. */
186
+ target: string;
187
+ /**
188
+ * Mix ratio 0–100 (0 = pure base, 100 = pure target).
189
+ * In 'transparent' blend mode, this controls the opacity of the target.
190
+ * Supports [normal, highContrast] pair.
191
+ */
192
+ value: HCPair<number>;
193
+ /**
194
+ * Blending mode. Default: 'opaque'.
195
+ * - 'opaque': produces a solid color by interpolating base and target.
196
+ * - 'transparent': produces the target color with alpha = value/100.
197
+ */
198
+ blend?: 'opaque' | 'transparent';
199
+ /**
200
+ * Interpolation color space for opaque blending. Default: 'okhsl'.
201
+ * - 'okhsl': perceptually uniform, consistent with Glaze's internal model.
202
+ * - 'srgb': linear sRGB interpolation, matches browser compositing.
203
+ *
204
+ * Ignored for 'transparent' blend (always composites in linear sRGB).
205
+ */
206
+ space?: 'okhsl' | 'srgb';
207
+ /**
208
+ * Minimum WCAG contrast between the base and the resulting color.
209
+ * In 'opaque' mode, adjusts the mix ratio to meet contrast.
210
+ * In 'transparent' mode, adjusts opacity to meet contrast against the composite.
211
+ * Supports [normal, highContrast] pair.
212
+ */
213
+ contrast?: HCPair<MinContrast>;
214
+ }
215
+ type ColorDef = RegularColorDef | ShadowColorDef | MixColorDef;
148
216
  type ColorMap = Record<string, ColorDef>;
149
217
  /** Resolved color for a single scheme variant. */
150
218
  interface ResolvedColorVariant {
@@ -394,6 +462,10 @@ declare namespace glaze {
394
462
  * computation for WCAG 2 contrast calculations, and multi-format output
395
463
  * (okhsl, rgb, hsl, oklch).
396
464
  */
465
+ /**
466
+ * Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to OKLab [L, a, b].
467
+ */
468
+ declare function okhslToOklab(h: number, s: number, l: number): [number, number, number];
397
469
  /**
398
470
  * Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to linear sRGB.
399
471
  * Channels may exceed [0, 1] near gamut boundaries — caller must clamp if needed.
@@ -413,9 +485,11 @@ declare function contrastRatioFromLuminance(yA: number, yB: number): number;
413
485
  */
414
486
  declare function okhslToSrgb(h: number, s: number, l: number): [number, number, number];
415
487
  /**
416
- * Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to OKLab [L, a, b].
488
+ * Compute WCAG 2 relative luminance from linear sRGB, matching the browser
489
+ * rendering pipeline: gamma-encode, clamp to sRGB gamut [0,1], then linearize.
490
+ * This avoids over/under-estimating luminance for out-of-gamut OKHSL colors.
417
491
  */
418
- declare function okhslToOklab(h: number, s: number, l: number): [number, number, number];
492
+ declare function gamutClampedLuminance(linearRgb: [number, number, number]): number;
419
493
  /**
420
494
  * Convert gamma-encoded sRGB (0–1 per channel) to OKHSL.
421
495
  * Returns [h, s, l] where h: 0–360, s: 0–1, l: 0–1.
@@ -447,5 +521,5 @@ declare function formatHsl(h: number, s: number, l: number): string;
447
521
  */
448
522
  declare function formatOklch(h: number, s: number, l: number): string;
449
523
  //#endregion
450
- export { type AdaptationMode, type ColorDef, type ColorMap, type ContrastPreset, type FindLightnessForContrastOptions, type FindLightnessForContrastResult, type GlazeColorFormat, type GlazeColorInput, type GlazeColorToken, type GlazeConfig, type GlazeCssOptions, type GlazeCssResult, type GlazeExtendOptions, type GlazeJsonOptions, type GlazeOutputModes, type GlazePalette, type GlazeShadowInput, type GlazeTheme, type GlazeThemeExport, type GlazeTokenOptions, type HCPair, type HexColor, type MinContrast, type OkhslColor, type RegularColorDef, type RelativeValue, type ResolvedColor, type ResolvedColorVariant, type ShadowColorDef, type ShadowTuning, contrastRatioFromLuminance, findLightnessForContrast, formatHsl, formatOkhsl, formatOklch, formatRgb, glaze, okhslToLinearSrgb, okhslToOklab, okhslToSrgb, parseHex, relativeLuminanceFromLinearRgb, resolveMinContrast, srgbToOkhsl };
524
+ export { type AdaptationMode, type ColorDef, type ColorMap, type ContrastPreset, type FindLightnessForContrastOptions, type FindLightnessForContrastResult, type FindValueForMixContrastOptions, type FindValueForMixContrastResult, type GlazeColorFormat, type GlazeColorInput, type GlazeColorToken, type GlazeConfig, type GlazeCssOptions, type GlazeCssResult, type GlazeExtendOptions, type GlazeJsonOptions, type GlazeOutputModes, type GlazePalette, type GlazeShadowInput, type GlazeTheme, type GlazeThemeExport, type GlazeTokenOptions, type HCPair, type HexColor, type MinContrast, type MixColorDef, type OkhslColor, type RegularColorDef, type RelativeValue, type ResolvedColor, type ResolvedColorVariant, type ShadowColorDef, type ShadowTuning, contrastRatioFromLuminance, findLightnessForContrast, findValueForMixContrast, formatHsl, formatOkhsl, formatOklch, formatRgb, gamutClampedLuminance, glaze, okhslToLinearSrgb, okhslToOklab, okhslToSrgb, parseHex, relativeLuminanceFromLinearRgb, resolveMinContrast, srgbToOkhsl };
451
525
  //# sourceMappingURL=index.d.cts.map
package/dist/index.d.mts CHANGED
@@ -6,6 +6,7 @@
6
6
  * against a base color. Used by glaze when resolving dependent colors
7
7
  * with `contrast`.
8
8
  */
9
+ type LinearRgb = [number, number, number];
9
10
  type ContrastPreset = 'AA' | 'AAA' | 'AA-large' | 'AAA-large';
10
11
  type MinContrast$1 = number | ContrastPreset;
11
12
  interface FindLightnessForContrastOptions {
@@ -42,6 +43,39 @@ declare function resolveMinContrast(value: MinContrast$1): number;
42
43
  * against a base color, staying as close to `preferredLightness` as possible.
43
44
  */
44
45
  declare function findLightnessForContrast(options: FindLightnessForContrastOptions): FindLightnessForContrastResult;
46
+ interface FindValueForMixContrastOptions {
47
+ /** Preferred mix parameter (0–1). */
48
+ preferredValue: number;
49
+ /** Base color as linear sRGB. */
50
+ baseLinearRgb: LinearRgb;
51
+ /** Target color as linear sRGB. */
52
+ targetLinearRgb: LinearRgb;
53
+ /** WCAG contrast target. */
54
+ contrast: MinContrast$1;
55
+ /**
56
+ * Compute the luminance of the mixed color at parameter t.
57
+ * For opaque: luminance of OKHSL-interpolated color.
58
+ * For transparent: luminance of alpha-composited color over base.
59
+ */
60
+ luminanceAtValue: (t: number) => number;
61
+ /** Convergence threshold. Default: 1e-4. */
62
+ epsilon?: number;
63
+ /** Maximum binary-search iterations per branch. Default: 20. */
64
+ maxIterations?: number;
65
+ }
66
+ interface FindValueForMixContrastResult {
67
+ /** Chosen mix parameter (0–1). */
68
+ value: number;
69
+ /** Achieved WCAG contrast ratio. */
70
+ contrast: number;
71
+ /** Whether the target was reached. */
72
+ met: boolean;
73
+ }
74
+ /**
75
+ * Find the mix parameter (ratio or opacity) that satisfies a WCAG 2 contrast
76
+ * target against a base color, staying as close to `preferredValue` as possible.
77
+ */
78
+ declare function findValueForMixContrast(options: FindValueForMixContrastOptions): FindValueForMixContrastResult;
45
79
  //#endregion
46
80
  //#region src/types.d.ts
47
81
  /** A value or [normal, high-contrast] pair. */
@@ -144,7 +178,41 @@ interface ShadowColorDef {
144
178
  /** Override default tuning. Merged field-by-field with global `shadowTuning`. */
145
179
  tuning?: ShadowTuning;
146
180
  }
147
- type ColorDef = RegularColorDef | ShadowColorDef;
181
+ interface MixColorDef {
182
+ type: 'mix';
183
+ /** Background/base color name — the "from" color. */
184
+ base: string;
185
+ /** Target color name — the "to" color to mix toward. */
186
+ target: string;
187
+ /**
188
+ * Mix ratio 0–100 (0 = pure base, 100 = pure target).
189
+ * In 'transparent' blend mode, this controls the opacity of the target.
190
+ * Supports [normal, highContrast] pair.
191
+ */
192
+ value: HCPair<number>;
193
+ /**
194
+ * Blending mode. Default: 'opaque'.
195
+ * - 'opaque': produces a solid color by interpolating base and target.
196
+ * - 'transparent': produces the target color with alpha = value/100.
197
+ */
198
+ blend?: 'opaque' | 'transparent';
199
+ /**
200
+ * Interpolation color space for opaque blending. Default: 'okhsl'.
201
+ * - 'okhsl': perceptually uniform, consistent with Glaze's internal model.
202
+ * - 'srgb': linear sRGB interpolation, matches browser compositing.
203
+ *
204
+ * Ignored for 'transparent' blend (always composites in linear sRGB).
205
+ */
206
+ space?: 'okhsl' | 'srgb';
207
+ /**
208
+ * Minimum WCAG contrast between the base and the resulting color.
209
+ * In 'opaque' mode, adjusts the mix ratio to meet contrast.
210
+ * In 'transparent' mode, adjusts opacity to meet contrast against the composite.
211
+ * Supports [normal, highContrast] pair.
212
+ */
213
+ contrast?: HCPair<MinContrast>;
214
+ }
215
+ type ColorDef = RegularColorDef | ShadowColorDef | MixColorDef;
148
216
  type ColorMap = Record<string, ColorDef>;
149
217
  /** Resolved color for a single scheme variant. */
150
218
  interface ResolvedColorVariant {
@@ -394,6 +462,10 @@ declare namespace glaze {
394
462
  * computation for WCAG 2 contrast calculations, and multi-format output
395
463
  * (okhsl, rgb, hsl, oklch).
396
464
  */
465
+ /**
466
+ * Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to OKLab [L, a, b].
467
+ */
468
+ declare function okhslToOklab(h: number, s: number, l: number): [number, number, number];
397
469
  /**
398
470
  * Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to linear sRGB.
399
471
  * Channels may exceed [0, 1] near gamut boundaries — caller must clamp if needed.
@@ -413,9 +485,11 @@ declare function contrastRatioFromLuminance(yA: number, yB: number): number;
413
485
  */
414
486
  declare function okhslToSrgb(h: number, s: number, l: number): [number, number, number];
415
487
  /**
416
- * Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to OKLab [L, a, b].
488
+ * Compute WCAG 2 relative luminance from linear sRGB, matching the browser
489
+ * rendering pipeline: gamma-encode, clamp to sRGB gamut [0,1], then linearize.
490
+ * This avoids over/under-estimating luminance for out-of-gamut OKHSL colors.
417
491
  */
418
- declare function okhslToOklab(h: number, s: number, l: number): [number, number, number];
492
+ declare function gamutClampedLuminance(linearRgb: [number, number, number]): number;
419
493
  /**
420
494
  * Convert gamma-encoded sRGB (0–1 per channel) to OKHSL.
421
495
  * Returns [h, s, l] where h: 0–360, s: 0–1, l: 0–1.
@@ -447,5 +521,5 @@ declare function formatHsl(h: number, s: number, l: number): string;
447
521
  */
448
522
  declare function formatOklch(h: number, s: number, l: number): string;
449
523
  //#endregion
450
- export { type AdaptationMode, type ColorDef, type ColorMap, type ContrastPreset, type FindLightnessForContrastOptions, type FindLightnessForContrastResult, type GlazeColorFormat, type GlazeColorInput, type GlazeColorToken, type GlazeConfig, type GlazeCssOptions, type GlazeCssResult, type GlazeExtendOptions, type GlazeJsonOptions, type GlazeOutputModes, type GlazePalette, type GlazeShadowInput, type GlazeTheme, type GlazeThemeExport, type GlazeTokenOptions, type HCPair, type HexColor, type MinContrast, type OkhslColor, type RegularColorDef, type RelativeValue, type ResolvedColor, type ResolvedColorVariant, type ShadowColorDef, type ShadowTuning, contrastRatioFromLuminance, findLightnessForContrast, formatHsl, formatOkhsl, formatOklch, formatRgb, glaze, okhslToLinearSrgb, okhslToOklab, okhslToSrgb, parseHex, relativeLuminanceFromLinearRgb, resolveMinContrast, srgbToOkhsl };
524
+ export { type AdaptationMode, type ColorDef, type ColorMap, type ContrastPreset, type FindLightnessForContrastOptions, type FindLightnessForContrastResult, type FindValueForMixContrastOptions, type FindValueForMixContrastResult, type GlazeColorFormat, type GlazeColorInput, type GlazeColorToken, type GlazeConfig, type GlazeCssOptions, type GlazeCssResult, type GlazeExtendOptions, type GlazeJsonOptions, type GlazeOutputModes, type GlazePalette, type GlazeShadowInput, type GlazeTheme, type GlazeThemeExport, type GlazeTokenOptions, type HCPair, type HexColor, type MinContrast, type MixColorDef, type OkhslColor, type RegularColorDef, type RelativeValue, type ResolvedColor, type ResolvedColorVariant, type ShadowColorDef, type ShadowTuning, contrastRatioFromLuminance, findLightnessForContrast, findValueForMixContrast, formatHsl, formatOkhsl, formatOklch, formatRgb, gamutClampedLuminance, glaze, okhslToLinearSrgb, okhslToOklab, okhslToSrgb, parseHex, relativeLuminanceFromLinearRgb, resolveMinContrast, srgbToOkhsl };
451
525
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -79,8 +79,8 @@ const OKLab_to_linear_sRGB_coefficients = [
79
79
  .73956515,
80
80
  -.45954404,
81
81
  .08285427,
82
- .12541073,
83
- -.14503204
82
+ .1254107,
83
+ .14503204
84
84
  ]],
85
85
  [[.13110757611180954, 1.813339709266608], [
86
86
  1.35733652,
@@ -252,10 +252,9 @@ const getCs = (L, a, b, cusp) => {
252
252
  ];
253
253
  };
254
254
  /**
255
- * Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to linear sRGB.
256
- * Channels may exceed [0, 1] near gamut boundaries — caller must clamp if needed.
255
+ * Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to OKLab [L, a, b].
257
256
  */
258
- function okhslToLinearSrgb(h, s, l) {
257
+ function okhslToOklab(h, s, l) {
259
258
  const L = toeInv(l);
260
259
  let a = 0;
261
260
  let b = 0;
@@ -282,11 +281,18 @@ function okhslToLinearSrgb(h, s, l) {
282
281
  a = c * a_;
283
282
  b = c * b_;
284
283
  }
285
- return OKLabToLinearSRGB([
284
+ return [
286
285
  L,
287
286
  a,
288
287
  b
289
- ]);
288
+ ];
289
+ }
290
+ /**
291
+ * Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to linear sRGB.
292
+ * Channels may exceed [0, 1] near gamut boundaries — caller must clamp if needed.
293
+ */
294
+ function okhslToLinearSrgb(h, s, l) {
295
+ return OKLabToLinearSRGB(okhslToOklab(h, s, l));
290
296
  }
291
297
  /**
292
298
  * Compute relative luminance Y from linear sRGB channels.
@@ -325,40 +331,15 @@ function okhslToSrgb(h, s, l) {
325
331
  ];
326
332
  }
327
333
  /**
328
- * Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to OKLab [L, a, b].
334
+ * Compute WCAG 2 relative luminance from linear sRGB, matching the browser
335
+ * rendering pipeline: gamma-encode, clamp to sRGB gamut [0,1], then linearize.
336
+ * This avoids over/under-estimating luminance for out-of-gamut OKHSL colors.
329
337
  */
330
- function okhslToOklab(h, s, l) {
331
- const L = toeInv(l);
332
- let a = 0;
333
- let b = 0;
334
- const hNorm = constrainAngle(h) / 360;
335
- if (L !== 0 && L !== 1 && s !== 0) {
336
- const a_ = Math.cos(TAU * hNorm);
337
- const b_ = Math.sin(TAU * hNorm);
338
- const [c0, cMid, cMax] = getCs(L, a_, b_, findCuspOKLCH(a_, b_));
339
- const mid = .8;
340
- const midInv = 1.25;
341
- let t, k0, k1, k2;
342
- if (s < mid) {
343
- t = midInv * s;
344
- k0 = 0;
345
- k1 = mid * c0;
346
- k2 = 1 - k1 / cMid;
347
- } else {
348
- t = 5 * (s - .8);
349
- k0 = cMid;
350
- k1 = .2 * cMid ** 2 * 1.25 ** 2 / c0;
351
- k2 = 1 - k1 / (cMax - cMid);
352
- }
353
- const c = k0 + t * k1 / (1 - k2 * t);
354
- a = c * a_;
355
- b = c * b_;
356
- }
357
- return [
358
- L,
359
- a,
360
- b
361
- ];
338
+ function gamutClampedLuminance(linearRgb) {
339
+ const r = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[0]))));
340
+ const g = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[1]))));
341
+ const b = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[2]))));
342
+ return .2126 * r + .7152 * g + .0722 * b;
362
343
  }
363
344
  const linearSrgbToOklab = (rgb) => {
364
345
  return transform(cbrt3(transform(rgb, linear_sRGB_to_LMS_M)), LMS_to_OKLab_M);
@@ -447,7 +428,7 @@ function fmt$1(value, decimals) {
447
428
  * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
448
429
  */
449
430
  function formatOkhsl(h, s, l) {
450
- return `okhsl(${fmt$1(h, 1)} ${fmt$1(s, 1)}% ${fmt$1(l, 1)}%)`;
431
+ return `okhsl(${fmt$1(h, 2)} ${fmt$1(s, 2)}% ${fmt$1(l, 2)}%)`;
451
432
  }
452
433
  /**
453
434
  * Format OKHSL values as a CSS `rgb(R G B)` string with rounded integer values.
@@ -475,7 +456,7 @@ function formatHsl(h, s, l) {
475
456
  else if (max === g) hh = ((b - r) / delta + 2) * 60;
476
457
  else hh = ((r - g) / delta + 4) * 60;
477
458
  }
478
- return `hsl(${fmt$1(hh, 1)} ${fmt$1(ss * 100, 1)}% ${fmt$1(ll * 100, 1)}%)`;
459
+ return `hsl(${fmt$1(hh, 2)} ${fmt$1(ss * 100, 2)}% ${fmt$1(ll * 100, 2)}%)`;
479
460
  }
480
461
  /**
481
462
  * Format OKHSL values as a CSS `oklch(L C H)` string.
@@ -511,17 +492,6 @@ function resolveMinContrast(value) {
511
492
  const CACHE_SIZE = 512;
512
493
  const luminanceCache = /* @__PURE__ */ new Map();
513
494
  const cacheOrder = [];
514
- /**
515
- * Compute WCAG 2 relative luminance from linear sRGB, matching the browser
516
- * rendering pipeline: gamma-encode, clamp to sRGB gamut [0,1], then linearize.
517
- * This avoids over/under-estimating luminance for out-of-gamut OKHSL colors.
518
- */
519
- function gamutClampedLuminance(linearRgb) {
520
- const r = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[0]))));
521
- const g = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[1]))));
522
- const b = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[2]))));
523
- return .2126 * r + .7152 * g + .0722 * b;
524
- }
525
495
  function cachedLuminance(h, s, l) {
526
496
  const lRounded = Math.round(l * 1e4) / 1e4;
527
497
  const key = `${h}|${s}|${lRounded}`;
@@ -650,7 +620,7 @@ function findLightnessForContrast(options) {
650
620
  const searchTarget = target + .01;
651
621
  const yBase = gamutClampedLuminance(baseLinearRgb);
652
622
  const crPref = contrastRatioFromLuminance(cachedLuminance(hue, saturation, preferredLightness), yBase);
653
- if (crPref >= target) return {
623
+ if (crPref >= searchTarget) return {
654
624
  lightness: preferredLightness,
655
625
  contrast: crPref,
656
626
  met: true,
@@ -699,6 +669,135 @@ function findLightnessForContrast(options) {
699
669
  candidates.sort((a, b) => b.contrast - a.contrast);
700
670
  return candidates[0];
701
671
  }
672
+ /**
673
+ * Binary-search one branch [lo, hi] for the nearest passing mix value
674
+ * to `preferred`.
675
+ */
676
+ function searchMixBranch(lo, hi, yBase, target, epsilon, maxIter, preferred, luminanceAt) {
677
+ const crLo = contrastRatioFromLuminance(luminanceAt(lo), yBase);
678
+ const crHi = contrastRatioFromLuminance(luminanceAt(hi), yBase);
679
+ if (crLo < target && crHi < target) {
680
+ if (crLo >= crHi) return {
681
+ lightness: lo,
682
+ contrast: crLo,
683
+ met: false
684
+ };
685
+ return {
686
+ lightness: hi,
687
+ contrast: crHi,
688
+ met: false
689
+ };
690
+ }
691
+ let low = lo;
692
+ let high = hi;
693
+ for (let i = 0; i < maxIter; i++) {
694
+ if (high - low < epsilon) break;
695
+ const mid = (low + high) / 2;
696
+ if (contrastRatioFromLuminance(luminanceAt(mid), yBase) >= target) if (mid < preferred) low = mid;
697
+ else high = mid;
698
+ else if (mid < preferred) high = mid;
699
+ else low = mid;
700
+ }
701
+ const crLow = contrastRatioFromLuminance(luminanceAt(low), yBase);
702
+ const crHigh = contrastRatioFromLuminance(luminanceAt(high), yBase);
703
+ const lowPasses = crLow >= target;
704
+ const highPasses = crHigh >= target;
705
+ if (lowPasses && highPasses) {
706
+ if (Math.abs(low - preferred) <= Math.abs(high - preferred)) return {
707
+ lightness: low,
708
+ contrast: crLow,
709
+ met: true
710
+ };
711
+ return {
712
+ lightness: high,
713
+ contrast: crHigh,
714
+ met: true
715
+ };
716
+ }
717
+ if (lowPasses) return {
718
+ lightness: low,
719
+ contrast: crLow,
720
+ met: true
721
+ };
722
+ if (highPasses) return {
723
+ lightness: high,
724
+ contrast: crHigh,
725
+ met: true
726
+ };
727
+ return crLow >= crHigh ? {
728
+ lightness: low,
729
+ contrast: crLow,
730
+ met: false
731
+ } : {
732
+ lightness: high,
733
+ contrast: crHigh,
734
+ met: false
735
+ };
736
+ }
737
+ /**
738
+ * Find the mix parameter (ratio or opacity) that satisfies a WCAG 2 contrast
739
+ * target against a base color, staying as close to `preferredValue` as possible.
740
+ */
741
+ function findValueForMixContrast(options) {
742
+ const { preferredValue, baseLinearRgb, contrast: contrastInput, luminanceAtValue, epsilon = 1e-4, maxIterations = 20 } = options;
743
+ const target = resolveMinContrast(contrastInput);
744
+ const searchTarget = target + .01;
745
+ const yBase = gamutClampedLuminance(baseLinearRgb);
746
+ const crPref = contrastRatioFromLuminance(luminanceAtValue(preferredValue), yBase);
747
+ if (crPref >= searchTarget) return {
748
+ value: preferredValue,
749
+ contrast: crPref,
750
+ met: true
751
+ };
752
+ const darkerResult = preferredValue > 0 ? searchMixBranch(0, preferredValue, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue) : null;
753
+ const lighterResult = preferredValue < 1 ? searchMixBranch(preferredValue, 1, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue) : null;
754
+ if (darkerResult) darkerResult.met = darkerResult.contrast >= target;
755
+ if (lighterResult) lighterResult.met = lighterResult.contrast >= target;
756
+ const darkerPasses = darkerResult?.met ?? false;
757
+ const lighterPasses = lighterResult?.met ?? false;
758
+ if (darkerPasses && lighterPasses) {
759
+ if (Math.abs(darkerResult.lightness - preferredValue) <= Math.abs(lighterResult.lightness - preferredValue)) return {
760
+ value: darkerResult.lightness,
761
+ contrast: darkerResult.contrast,
762
+ met: true
763
+ };
764
+ return {
765
+ value: lighterResult.lightness,
766
+ contrast: lighterResult.contrast,
767
+ met: true
768
+ };
769
+ }
770
+ if (darkerPasses) return {
771
+ value: darkerResult.lightness,
772
+ contrast: darkerResult.contrast,
773
+ met: true
774
+ };
775
+ if (lighterPasses) return {
776
+ value: lighterResult.lightness,
777
+ contrast: lighterResult.contrast,
778
+ met: true
779
+ };
780
+ const candidates = [];
781
+ if (darkerResult) candidates.push({
782
+ ...darkerResult,
783
+ branch: "lower"
784
+ });
785
+ if (lighterResult) candidates.push({
786
+ ...lighterResult,
787
+ branch: "upper"
788
+ });
789
+ if (candidates.length === 0) return {
790
+ value: preferredValue,
791
+ contrast: crPref,
792
+ met: false
793
+ };
794
+ candidates.sort((a, b) => b.contrast - a.contrast);
795
+ return {
796
+ value: candidates[0].lightness,
797
+ contrast: candidates[0].contrast,
798
+ met: candidates[0].met
799
+ };
800
+ }
702
801
 
703
802
  //#endregion
704
803
  //#region src/glaze.ts
@@ -730,6 +829,9 @@ function pairHC(p) {
730
829
  function isShadowDef(def) {
731
830
  return def.type === "shadow";
732
831
  }
832
+ function isMixDef(def) {
833
+ return def.type === "mix";
834
+ }
733
835
  const DEFAULT_SHADOW_TUNING = {
734
836
  saturationFactor: .18,
735
837
  maxSaturation: .25,
@@ -797,6 +899,13 @@ function validateColorDefs(defs) {
797
899
  }
798
900
  continue;
799
901
  }
902
+ if (isMixDef(def)) {
903
+ if (!names.has(def.base)) throw new Error(`glaze: mix "${name}" references non-existent base "${def.base}".`);
904
+ if (!names.has(def.target)) throw new Error(`glaze: mix "${name}" references non-existent target "${def.target}".`);
905
+ if (isShadowDef(defs[def.base])) throw new Error(`glaze: mix "${name}" base "${def.base}" references a shadow color.`);
906
+ if (isShadowDef(defs[def.target])) throw new Error(`glaze: mix "${name}" target "${def.target}" references a shadow color.`);
907
+ continue;
908
+ }
800
909
  const regDef = def;
801
910
  if (regDef.contrast !== void 0 && !regDef.base) throw new Error(`glaze: color "${name}" has "contrast" without "base".`);
802
911
  if (regDef.lightness !== void 0 && !isAbsoluteLightness(regDef.lightness) && !regDef.base) throw new Error(`glaze: color "${name}" has relative "lightness" without "base".`);
@@ -815,6 +924,9 @@ function validateColorDefs(defs) {
815
924
  if (isShadowDef(def)) {
816
925
  dfs(def.bg);
817
926
  if (def.fg) dfs(def.fg);
927
+ } else if (isMixDef(def)) {
928
+ dfs(def.base);
929
+ dfs(def.target);
818
930
  } else {
819
931
  const regDef = def;
820
932
  if (regDef.base) dfs(regDef.base);
@@ -834,6 +946,9 @@ function topoSort(defs) {
834
946
  if (isShadowDef(def)) {
835
947
  visit(def.bg);
836
948
  if (def.fg) visit(def.fg);
949
+ } else if (isMixDef(def)) {
950
+ visit(def.base);
951
+ visit(def.target);
837
952
  } else {
838
953
  const regDef = def;
839
954
  if (regDef.base) visit(regDef.base);
@@ -949,6 +1064,7 @@ function getSchemeVariant(color, isDark, isHighContrast) {
949
1064
  }
950
1065
  function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
951
1066
  if (isShadowDef(def)) return resolveShadowForScheme(def, ctx, isDark, isHighContrast);
1067
+ if (isMixDef(def)) return resolveMixForScheme(def, ctx, isDark, isHighContrast);
952
1068
  const regDef = def;
953
1069
  const mode = regDef.mode ?? "auto";
954
1070
  const isRoot = isAbsoluteLightness(regDef.lightness) && !regDef.base;
@@ -994,6 +1110,83 @@ function resolveShadowForScheme(def, ctx, isDark, isHighContrast) {
994
1110
  const tuning = resolveShadowTuning(def.tuning);
995
1111
  return computeShadow(bgVariant, fgVariant, intensity, tuning);
996
1112
  }
1113
+ function variantToLinearRgb(v) {
1114
+ return okhslToLinearSrgb(v.h, v.s, v.l);
1115
+ }
1116
+ /**
1117
+ * Resolve hue for OKHSL mixing, handling achromatic colors.
1118
+ * When one color has no saturation, its hue is meaningless —
1119
+ * use the hue from the color that has saturation (matches CSS
1120
+ * color-mix "missing component" behavior).
1121
+ */
1122
+ function mixHue(base, target, t) {
1123
+ const SAT_EPSILON = 1e-6;
1124
+ const baseHasSat = base.s > SAT_EPSILON;
1125
+ const targetHasSat = target.s > SAT_EPSILON;
1126
+ if (baseHasSat && targetHasSat) return circularLerp(base.h, target.h, t);
1127
+ if (targetHasSat) return target.h;
1128
+ return base.h;
1129
+ }
1130
+ function linearSrgbLerp(base, target, t) {
1131
+ return [
1132
+ base[0] + (target[0] - base[0]) * t,
1133
+ base[1] + (target[1] - base[1]) * t,
1134
+ base[2] + (target[2] - base[2]) * t
1135
+ ];
1136
+ }
1137
+ function linearRgbToVariant(rgb) {
1138
+ const [h, s, l] = srgbToOkhsl([
1139
+ Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[0]))),
1140
+ Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[1]))),
1141
+ Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[2])))
1142
+ ]);
1143
+ return {
1144
+ h,
1145
+ s,
1146
+ l,
1147
+ alpha: 1
1148
+ };
1149
+ }
1150
+ function resolveMixForScheme(def, ctx, isDark, isHighContrast) {
1151
+ const baseResolved = ctx.resolved.get(def.base);
1152
+ const targetResolved = ctx.resolved.get(def.target);
1153
+ const baseVariant = getSchemeVariant(baseResolved, isDark, isHighContrast);
1154
+ const targetVariant = getSchemeVariant(targetResolved, isDark, isHighContrast);
1155
+ let t = clamp(isHighContrast ? pairHC(def.value) : pairNormal(def.value), 0, 100) / 100;
1156
+ const blend = def.blend ?? "opaque";
1157
+ const space = def.space ?? "okhsl";
1158
+ const baseLinear = variantToLinearRgb(baseVariant);
1159
+ const targetLinear = variantToLinearRgb(targetVariant);
1160
+ if (def.contrast !== void 0) {
1161
+ const minCr = isHighContrast ? pairHC(def.contrast) : pairNormal(def.contrast);
1162
+ let luminanceAt;
1163
+ if (blend === "transparent") luminanceAt = (v) => gamutClampedLuminance(linearSrgbLerp(baseLinear, targetLinear, v));
1164
+ else if (space === "srgb") luminanceAt = (v) => gamutClampedLuminance(linearSrgbLerp(baseLinear, targetLinear, v));
1165
+ else luminanceAt = (v) => {
1166
+ return gamutClampedLuminance(okhslToLinearSrgb(mixHue(baseVariant, targetVariant, v), baseVariant.s + (targetVariant.s - baseVariant.s) * v, baseVariant.l + (targetVariant.l - baseVariant.l) * v));
1167
+ };
1168
+ t = findValueForMixContrast({
1169
+ preferredValue: t,
1170
+ baseLinearRgb: baseLinear,
1171
+ targetLinearRgb: targetLinear,
1172
+ contrast: minCr,
1173
+ luminanceAtValue: luminanceAt
1174
+ }).value;
1175
+ }
1176
+ if (blend === "transparent") return {
1177
+ h: targetVariant.h,
1178
+ s: targetVariant.s,
1179
+ l: targetVariant.l,
1180
+ alpha: clamp(t, 0, 1)
1181
+ };
1182
+ if (space === "srgb") return linearRgbToVariant(linearSrgbLerp(baseLinear, targetLinear, t));
1183
+ return {
1184
+ h: mixHue(baseVariant, targetVariant, t),
1185
+ s: clamp(baseVariant.s + (targetVariant.s - baseVariant.s) * t, 0, 1),
1186
+ l: clamp(baseVariant.l + (targetVariant.l - baseVariant.l) * t, 0, 1),
1187
+ alpha: 1
1188
+ };
1189
+ }
997
1190
  function resolveAllColors(hue, saturation, defs) {
998
1191
  validateColorDefs(defs);
999
1192
  const order = topoSort(defs);
@@ -1004,7 +1197,8 @@ function resolveAllColors(hue, saturation, defs) {
1004
1197
  resolved: /* @__PURE__ */ new Map()
1005
1198
  };
1006
1199
  function defMode(def) {
1007
- return isShadowDef(def) ? void 0 : def.mode ?? "auto";
1200
+ if (isShadowDef(def) || isMixDef(def)) return void 0;
1201
+ return def.mode ?? "auto";
1008
1202
  }
1009
1203
  const lightMap = /* @__PURE__ */ new Map();
1010
1204
  for (const name of order) {
@@ -1442,5 +1636,5 @@ glaze.resetConfig = function resetConfig() {
1442
1636
  };
1443
1637
 
1444
1638
  //#endregion
1445
- export { contrastRatioFromLuminance, findLightnessForContrast, formatHsl, formatOkhsl, formatOklch, formatRgb, glaze, okhslToLinearSrgb, okhslToOklab, okhslToSrgb, parseHex, relativeLuminanceFromLinearRgb, resolveMinContrast, srgbToOkhsl };
1639
+ export { contrastRatioFromLuminance, findLightnessForContrast, findValueForMixContrast, formatHsl, formatOkhsl, formatOklch, formatRgb, gamutClampedLuminance, glaze, okhslToLinearSrgb, okhslToOklab, okhslToSrgb, parseHex, relativeLuminanceFromLinearRgb, resolveMinContrast, srgbToOkhsl };
1446
1640
  //# sourceMappingURL=index.mjs.map