@thejaredwilcurt/csslop 0.0.16 → 0.0.18

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.
@@ -1,12 +1,9 @@
1
1
  /**
2
- * @file Provides color parsing, color space conversion (sRGB, OKLab, OKLCH, HSL, HWB), color-mix evaluation, and hex formatting for CSS value minification.
2
+ * @file Provides color parsing, color space conversion (sRGB, OKLab, OKLCH, HSL, HWB), and hex formatting for CSS value minification.
3
3
  */
4
4
 
5
5
  import { namedColors } from './named-colors.js';
6
- import {
7
- parseAlphaString,
8
- roundCompactNumber
9
- } from './shared.js';
6
+ import { parseAlphaString } from './shared.js';
10
7
 
11
8
  /**
12
9
  * Reverse lookup from "r,g,b" to the shortest CSS named color keyword.
@@ -281,26 +278,6 @@ function oklchToOklab (L, C, H) {
281
278
  };
282
279
  }
283
280
 
284
- /**
285
- * Interpolates between two hue angles along the shorter arc, per the CSS Color specification.
286
- *
287
- * @param {number} h1 The first hue angle in degrees.
288
- * @param {number} h2 The second hue angle in degrees.
289
- * @param {number} t The interpolation factor from 0 to 1.
290
- * @return {number} The interpolated hue angle in degrees, normalized to 0–360.
291
- */
292
- function interpolateHueShorter (h1, h2, t) {
293
- let diff = h2 - h1;
294
- if (diff > 180) {
295
- diff -= 360;
296
- }
297
- if (diff < -180) {
298
- diff += 360;
299
- }
300
- let result = h1 + diff * t;
301
- return ((result % 360) + 360) % 360;
302
- }
303
-
304
281
  /**
305
282
  * Parse a hex color string to [r, g, b, a].
306
283
  *
@@ -430,469 +407,84 @@ function oklabToRgb (L, a, b) {
430
407
  }
431
408
 
432
409
  /**
433
- * Format an OKLCH result as a minified string.
410
+ * CIE Lab D50 reference white point.
434
411
  *
435
- * @param {number} L The lightness component.
436
- * @param {number} C The chroma component.
437
- * @param {number} H The hue angle in degrees.
438
- * @param {number|undefined} alpha The alpha value from 0 to 1, or undefined for fully opaque.
439
- * @return {string} A minified oklch() function string.
412
+ * @type {Array}
440
413
  */
441
- function formatOklch (L, C, H, alpha) {
442
- const fmtL = roundCompactNumber(L, 3);
443
- const fmtC = roundCompactNumber(C, 3);
444
- const fmtH = roundCompactNumber(H, 1);
445
- if (alpha !== undefined && alpha < 1) {
446
- return 'oklch(' + fmtL + ' ' + fmtC + ' ' + fmtH + '/' + roundCompactNumber(alpha, 3) + ')';
447
- }
448
- return 'oklch(' + fmtL + ' ' + fmtC + ' ' + fmtH + ')';
449
- }
414
+ const D50_WHITE_XYZ = [0.96422, 1, 0.82521];
450
415
 
451
416
  /**
452
- * Detect which channel indices have 'none' in a raw color function string.
417
+ * Converts a CIE Lab f-function value back to a linear-light ratio.
453
418
  *
454
- * @param {string} rawColorStr The raw CSS color function string (e.g. "rgb(none 0 0)").
455
- * @return {Array} An array of zero-based channel indices where 'none' was found.
419
+ * @param {number} f The f value.
420
+ * @return {number} The linear ratio.
456
421
  */
457
- function findNoneChannels (rawColorStr) {
458
- const indices = [];
459
- // Match rgb/rgba/hsl/hsla/hwb function calls and extract their arguments
460
- const functionMatch = rawColorStr.match(/\b(?:rgba?|hsla?|hwb)\(([^)]*)\)/i);
461
- if (functionMatch) {
462
- // Split arguments on whitespace, commas, or slash separators
463
- const parts = functionMatch[1].trim().split(/[\s,/]+/).map((part) => {
464
- return part.trim();
465
- }).filter((part) => {
466
- return part.length > 0;
467
- });
468
- parts.forEach((part, index) => {
469
- if (part.toLowerCase() === 'none') {
470
- indices.push(index);
471
- }
472
- });
422
+ function labFToLinearRatio (f) {
423
+ const delta = 6 / 29;
424
+ if (f > delta) {
425
+ return f * f * f;
473
426
  }
474
- return indices;
427
+ return 3 * delta * delta * (f - 16 / 116);
475
428
  }
476
429
 
477
430
  /**
478
- * Evaluate an N-color (3+) color-mix() expression. Returns a minified CSS color string or null.
431
+ * Converts CIE Lab (D50) components to linear-light sRGB using Bradford adaptation to D65.
479
432
  *
480
- * @param {string} colorSpace The interpolation color space ("srgb", "oklab", or "oklch").
481
- * @param {Array} args The raw argument strings for each color.
482
- * @return {string|null} A minified CSS color string, or null if the expression cannot be evaluated.
433
+ * @param {number} L The lightness component.
434
+ * @param {number} a The green-red axis component.
435
+ * @param {number} b The blue-yellow axis component.
436
+ * @return {object} An object with r, g, b linear sRGB components.
483
437
  */
484
- function evaluateNColorMix (colorSpace, args) {
485
- const parsedArgs = [];
486
- for (const arg of args) {
487
- const parsed = parseColorMixArg(arg.trim());
488
- if (!parsed) {
489
- return null;
490
- }
491
- parsedArgs.push(parsed);
492
- }
493
-
494
- // If any color is unresolvable (var(), currentcolor), whitespace-strip only
495
- if (parsedArgs.some((parsedArg) => {
496
- return !parsedArg.color;
497
- })) {
498
- return normalizeUnresolvableNColorMix(colorSpace, parsedArgs);
499
- }
500
-
501
- const percentages = normalizeNColorPercentages(parsedArgs);
502
- const percentageSum = percentages.reduce((sum, value) => {
503
- return sum + value;
504
- }, 0);
505
-
506
- // All-zero percentages → transparent black
507
- if (percentageSum === 0) {
508
- return rgbaToHex(0, 0, 0, 0);
509
- }
510
-
511
- let alphaMultiplier = 1;
512
- if (percentageSum < 100) {
513
- alphaMultiplier = percentageSum / 100;
514
- } else if (percentageSum > 100) {
515
- for (let i = 0; i < percentages.length; i++) {
516
- percentages[i] = percentages[i] / percentageSum * 100;
517
- }
518
- }
438
+ function labToLinearSrgb (L, a, b) {
439
+ const fy = (L + 16) / 116;
440
+ const fx = fy + a / 500;
441
+ const fz = fy - b / 200;
519
442
 
520
- // Compute weights
521
- const totalPercentage = percentages.reduce((sum, value) => {
522
- return sum + value;
523
- }, 0);
524
- const weights = percentages.map((value) => {
525
- return value / totalPercentage;
526
- });
527
- const colors = parsedArgs.map((parsedArg) => {
528
- return parsedArg.color;
529
- });
530
-
531
- if (colorSpace === 'srgb') {
532
- return mixNColorsSrgb(colors, weights, alphaMultiplier);
533
- }
443
+ const x = labFToLinearRatio(fx) * D50_WHITE_XYZ[0];
444
+ const y = labFToLinearRatio(fy) * D50_WHITE_XYZ[1];
445
+ const z = labFToLinearRatio(fz) * D50_WHITE_XYZ[2];
534
446
 
535
- if (colorSpace === 'oklab') {
536
- return mixNColorsOklab(colors, weights, alphaMultiplier);
537
- }
447
+ // Matrix from D50 XYZ (Bradford-adapted) to linear sRGB.
448
+ const red = 3.1338561 * x - 1.6168667 * y - 0.4906146 * z;
449
+ const green = -0.9787684 * x + 1.9161415 * y + 0.0334540 * z;
450
+ const blue = 0.0719453 * x - 0.2289914 * y + 1.4052427 * z;
538
451
 
539
- return null;
540
- }
541
-
542
- /**
543
- * Normalize percentages for an N-color color-mix() expression.
544
- * When no percentages are specified, each color gets an equal share of 100%.
545
- * When some are unspecified, the remaining percentage is split equally among them.
546
- *
547
- * @param {Array} parsedArgs The parsed color-mix arguments.
548
- * @return {Array} An array of normalized percentage values.
549
- */
550
- function normalizeNColorPercentages (parsedArgs) {
551
- const percentages = parsedArgs.map((parsedArg) => {
552
- return parsedArg.percentage;
553
- });
554
- if (percentages.every((value) => {
555
- return value === null;
556
- })) {
557
- const equalWeight = 100 / parsedArgs.length;
558
- return parsedArgs.map(() => {
559
- return equalWeight;
560
- });
561
- }
562
- const specifiedSum = percentages.reduce((sum, value) => {
563
- return sum + (value !== null ? value : 0);
564
- }, 0);
565
- const unspecifiedCount = percentages.filter((value) => {
566
- return value === null;
567
- }).length;
568
- if (unspecifiedCount > 0) {
569
- const remaining = Math.max(0, 100 - specifiedSum);
570
- const percentagePerUnspecified = remaining / unspecifiedCount;
571
- return percentages.map((value) => {
572
- return value !== null ? value : percentagePerUnspecified;
573
- });
574
- }
575
- return percentages;
576
- }
577
-
578
- /**
579
- * Build a whitespace-stripped color-mix() string for an unresolvable N-color expression.
580
- *
581
- * @param {string} colorSpace The interpolation color space.
582
- * @param {Array} parsedArgs The parsed color-mix arguments.
583
- * @return {string} A whitespace-stripped color-mix() expression.
584
- */
585
- function normalizeUnresolvableNColorMix (colorSpace, parsedArgs) {
586
- const parts = parsedArgs.map((parsedArg) => {
587
- const rawColor = parsedArg.raw.trim();
588
- const percentageString = parsedArg.percentage !== null ? ' ' + parsedArg.percentage + '%' : '';
589
- return rawColor + percentageString;
590
- });
591
- return 'color-mix(in ' + colorSpace + ',' + parts.join(',') + ')';
452
+ return { r: red, g: green, b: blue };
592
453
  }
593
454
 
594
455
  /**
595
- * Mix N colors in the sRGB color space using weighted averages.
456
+ * Converts CIE Lab (D50) components to clamped, gamma-encoded sRGB (0–1).
596
457
  *
597
- * @param {Array} colors Array of [r, g, b, a] color arrays.
598
- * @param {Array} weights Array of weight values for each color.
599
- * @param {number} alphaMultiplier Multiplier for the final alpha channel.
600
- * @return {string} A hex color string.
458
+ * @param {number} L The lightness component.
459
+ * @param {number} a The green-red axis component.
460
+ * @param {number} b The blue-yellow axis component.
461
+ * @return {object} An object with r, g, b sRGB components.
601
462
  */
602
- function mixNColorsSrgb (colors, weights, alphaMultiplier) {
603
- let r = 0;
604
- let g = 0;
605
- let b = 0;
606
- let alpha = 0;
607
- for (let i = 0; i < colors.length; i++) {
608
- r += colors[i][0] * weights[i];
609
- g += colors[i][1] * weights[i];
610
- b += colors[i][2] * weights[i];
611
- alpha += colors[i][3] * weights[i];
612
- }
613
- return rgbaToHex(Math.round(r), Math.round(g), Math.round(b), alpha * alphaMultiplier);
614
- }
615
-
616
- /**
617
- * Mix N colors in the OKLab color space using weighted averages.
618
- *
619
- * @param {Array} colors Array of [r, g, b, a] color arrays.
620
- * @param {Array} weights Array of weight values for each color.
621
- * @param {number} alphaMultiplier Multiplier for the final alpha channel.
622
- * @return {string} A hex color string.
623
- */
624
- function mixNColorsOklab (colors, weights, alphaMultiplier) {
625
- const oklabValues = colors.map((color) => {
626
- return rgbToOklab(color[0], color[1], color[2]);
627
- });
628
- let L = 0;
629
- let a = 0;
630
- let b = 0;
631
- let alpha = 0;
632
- for (let i = 0; i < oklabValues.length; i++) {
633
- L += oklabValues[i].L * weights[i];
634
- a += oklabValues[i].a * weights[i];
635
- b += oklabValues[i].b * weights[i];
636
- alpha += colors[i][3] * weights[i];
637
- }
638
- alpha *= alphaMultiplier;
639
- const rgb = oklabToRgb(L, a, b);
640
- return rgbaToHex(rgb[0], rgb[1], rgb[2], alpha >= 1 ? 1 : alpha);
641
- }
642
-
643
- /**
644
- * Evaluate a color-mix() expression. Returns a minified CSS color string or null.
645
- *
646
- * @param {string} expr The full color-mix() expression string.
647
- * @return {string|null} A minified CSS color string, or null if the expression cannot be evaluated.
648
- */
649
- function evaluateColorMix (expr) {
650
- // Parse: color-mix(in <space> [<hue-method>], <color> [<p>%], <color> [<p>%])
651
- // We need to handle nested parentheses for inner color functions
652
- const inner = extractBalancedArgs(expr, 'color-mix');
653
- if (!inner) {
654
- return null;
655
- }
656
-
657
- // Parse the interpolation method
658
- const inMatch = inner.match(/^in\s+(srgb|oklch|oklab)(?:\s+shorter\s+hue)?\s*,\s*/i);
659
- if (!inMatch) {
660
- return null;
661
- }
662
-
663
- const colorSpace = inMatch[1].toLowerCase();
664
- const rest = inner.slice(inMatch[0].length);
665
-
666
- // Split color arguments (handling nested parens)
667
- const args = splitColorMixArgs(rest);
668
- if (args.length < 2) {
669
- return null;
670
- }
671
-
672
- // N-color path (3+ colors)
673
- if (args.length > 2) {
674
- return evaluateNColorMix(colorSpace, args);
675
- }
676
-
677
- // Parse each argument: "<color> [<percentage>]"
678
- const parsed1 = parseColorMixArg(args[0].trim());
679
- const parsed2 = parseColorMixArg(args[1].trim());
680
- if (!parsed1 || !parsed2) {
681
- return null;
682
- }
683
-
684
- // Check for unresolvable colors (var(), currentcolor, etc.)
685
- if (!parsed1.color || !parsed2.color) {
686
- // Can still do normalization but not computation
687
- return normalizeColorMix(colorSpace, parsed1, parsed2);
688
- }
689
-
690
- // Normalize percentages per CSS spec
691
- let p1 = parsed1.percentage;
692
- let p2 = parsed2.percentage;
693
-
694
- if (p1 === null && p2 === null) {
695
- p1 = 50;
696
- p2 = 50;
697
- } else if (p1 === null) {
698
- p1 = 100 - p2;
699
- } else if (p2 === null) {
700
- p2 = 100 - p1;
701
- }
702
-
703
- let alphaMultiplier = 1;
704
- const pSum = p1 + p2;
705
- if (pSum === 0) {
706
- return null;
707
- }
708
-
709
- if (pSum < 100) {
710
- alphaMultiplier = pSum / 100;
711
- } else if (pSum > 100) {
712
- p1 = p1 / pSum * 100;
713
- p2 = p2 / pSum * 100;
714
- }
715
-
716
- // Trivial cases
717
- if (p1 === 0) {
718
- return rgbaToHex(parsed2.color[0], parsed2.color[1], parsed2.color[2], parsed2.color[3]);
719
- }
720
- if (p2 === 0) {
721
- return rgbaToHex(parsed1.color[0], parsed1.color[1], parsed1.color[2], parsed1.color[3]);
722
- }
723
-
724
- // CSS spec: 'none' channels are missing — fill from the other color before mixing
725
- const nones1 = findNoneChannels(parsed1.raw);
726
- const nones2 = findNoneChannels(parsed2.raw);
727
- for (const idx of nones1) {
728
- if (idx < parsed1.color.length) {
729
- parsed1.color[idx] = parsed2.color[idx];
730
- }
731
- }
732
- for (const idx of nones2) {
733
- if (idx < parsed2.color.length) {
734
- parsed2.color[idx] = parsed1.color[idx];
735
- }
736
- }
737
-
738
- const t1 = p1 / (p1 + p2);
739
- const t2 = p2 / (p1 + p2);
740
- const [r1, g1, b1, a1] = parsed1.color;
741
- const [r2, g2, b2, a2] = parsed2.color;
742
-
743
- if (colorSpace === 'srgb') {
744
- const r = Math.round(r1 * t1 + r2 * t2);
745
- const g = Math.round(g1 * t1 + g2 * t2);
746
- const b = Math.round(b1 * t1 + b2 * t2);
747
- const a = (a1 * t1 + a2 * t2) * alphaMultiplier;
748
- return rgbaToHex(r, g, b, a);
749
- }
750
-
751
- if (colorSpace === 'oklab') {
752
- const lab1 = rgbToOklab(r1, g1, b1);
753
- const lab2 = rgbToOklab(r2, g2, b2);
754
- const L = lab1.L * t1 + lab2.L * t2;
755
- const a = lab1.a * t1 + lab2.a * t2;
756
- const b = lab1.b * t1 + lab2.b * t2;
757
- const alpha = (a1 * t1 + a2 * t2) * alphaMultiplier;
758
- // Check if result fits in sRGB gamut
759
- const rgb = oklabToRgb(L, a, b);
760
- if (alpha >= 1) {
761
- return rgbaToHex(rgb[0], rgb[1], rgb[2], 1);
762
- }
763
- return rgbaToHex(rgb[0], rgb[1], rgb[2], alpha);
764
- }
765
-
766
- if (colorSpace === 'oklch') {
767
- const lch1 = rgbToOklch(r1, g1, b1);
768
- const lch2 = rgbToOklch(r2, g2, b2);
769
- const L = lch1.L * t1 + lch2.L * t2;
770
- const C = lch1.C * t1 + lch2.C * t2;
771
- const H = interpolateHueShorter(lch1.H, lch2.H, t2);
772
- const alpha = (a1 * t1 + a2 * t2) * alphaMultiplier;
773
- return formatOklch(L, C, H, alpha);
774
- }
775
-
776
- return null;
463
+ function labToSrgb (L, a, b) {
464
+ const linear = labToLinearSrgb(L, a, b);
465
+ return {
466
+ r: Math.max(0, Math.min(1, delinearize(linear.r))),
467
+ g: Math.max(0, Math.min(1, delinearize(linear.g))),
468
+ b: Math.max(0, Math.min(1, delinearize(linear.b)))
469
+ };
777
470
  }
778
471
 
779
472
  /**
780
- * Extract the balanced content inside a function call.
473
+ * Convert a standalone lab() value (CIE Lab, D50) to hex if it fits in sRGB gamut; returns null if out-of-gamut.
781
474
  *
782
- * @param {string} expr The expression string containing the function call.
783
- * @param {string} funcName The function name to locate (e.g. "color-mix").
784
- * @return {string|null} The content between the matching parentheses, or null if not found.
475
+ * @param {number} L The Lab lightness component.
476
+ * @param {number} a The Lab a-axis component.
477
+ * @param {number} b The Lab b-axis component.
478
+ * @param {number} alpha The alpha value from 0 to 1.
479
+ * @return {string|null} A hex color string, or null if out-of-gamut.
785
480
  */
786
- function extractBalancedArgs (expr, funcName) {
787
- const prefix = funcName + '(';
788
- const start = expr.indexOf(prefix);
789
- if (start === -1) {
481
+ function convertLabToHex (L, a, b, alpha) {
482
+ const linear = labToLinearSrgb(L, a, b);
483
+ if (linear.r < -0.002 || linear.r > 1.002 || linear.g < -0.002 || linear.g > 1.002 || linear.b < -0.002 || linear.b > 1.002) {
790
484
  return null;
791
485
  }
792
- let depth = 1;
793
- let position = start + prefix.length;
794
- while (position < expr.length && depth > 0) {
795
- if (expr[position] === '(') {
796
- depth++;
797
- } else if (expr[position] === ')') {
798
- depth--;
799
- }
800
- position++;
801
- }
802
- return expr.slice(start + prefix.length, position - 1);
803
- }
804
-
805
- /**
806
- * Split color-mix arguments at top-level commas (handling nested parens).
807
- *
808
- * @param {string} str The color arguments string, with arguments separated by commas.
809
- * @return {Array} An array of argument strings split at each top-level comma.
810
- */
811
- function splitColorMixArgs (str) {
812
- const args = [];
813
- let depth = 0;
814
- let start = 0;
815
- for (let position = 0; position < str.length; position++) {
816
- if (str[position] === '(') {
817
- depth++;
818
- } else if (str[position] === ')') {
819
- depth--;
820
- } else if (str[position] === ',' && depth === 0) {
821
- args.push(str.slice(start, position));
822
- start = position + 1;
823
- }
824
- }
825
- args.push(str.slice(start));
826
- return args;
827
- }
828
-
829
- /**
830
- * Parse a single color-mix argument: "<color> [<percentage>]" or "<percentage> <color>".
831
- *
832
- * @param {string} arg The color-mix argument string to parse.
833
- * @return {object|null} An object with color (Array or null), percentage (number or null), raw (string), and hasVar (boolean), or null if unparseable.
834
- */
835
- function parseColorMixArg (arg) {
836
- arg = arg.trim();
837
-
838
- // Try: percentage at end, e.g. "red 50%" or "rgb(0 0 0)50%"
839
- let match = arg.match(/^(.+?)\s*(\d+(?:\.\d+)?)%\s*$/);
840
- if (match) {
841
- const colorStr = match[1].trim();
842
- const percentage = parseFloat(match[2]);
843
- const color = parseColor(colorStr);
844
- // Check if color contains var() or currentcolor (cannot be evaluated statically)
845
- return { color, percentage, raw: colorStr, hasVar: /var\(|currentcolor/i.test(colorStr) };
846
- }
847
-
848
- // Try: percentage at start, e.g. "50% red"
849
- match = arg.match(/^(\d+(?:\.\d+)?)%\s+(.+)$/);
850
- if (match) {
851
- const colorStr = match[2].trim();
852
- const percentage = parseFloat(match[1]);
853
- const color = parseColor(colorStr);
854
- // Check if color contains var() or currentcolor (cannot be evaluated statically)
855
- return { color, percentage, raw: colorStr, hasVar: /var\(|currentcolor/i.test(colorStr) };
856
- }
857
-
858
- // No percentage
859
- const color = parseColor(arg);
860
- // Check if color contains var() or currentcolor (cannot be evaluated statically)
861
- return { color, percentage: null, raw: arg, hasVar: /var\(|currentcolor/i.test(arg) };
862
- }
863
-
864
- /**
865
- * Normalize a color-mix expression when we can't fully compute it.
866
- *
867
- * @param {string} colorSpace The interpolation color space ("srgb", "oklab", or "oklch").
868
- * @param {object} parsed1 The parsed first color argument with color, percentage, and raw fields.
869
- * @param {object} parsed2 The parsed second color argument with color, percentage, and raw fields.
870
- * @return {string} A normalized color-mix() expression with default percentages and color space elided.
871
- */
872
- function normalizeColorMix (colorSpace, parsed1, parsed2) {
873
- // Normalize percentages: strip explicit 50%/50% (the defaults)
874
- let p1Str = '';
875
- let p2Str = '';
876
- if (parsed1.percentage !== null && parsed1.percentage !== 50) {
877
- p1Str = ' ' + parsed1.percentage + '%';
878
- }
879
- if (parsed2.percentage !== null && parsed2.percentage !== 50) {
880
- p2Str = ' ' + parsed2.percentage + '%';
881
- }
882
-
883
- // Use the raw color strings (but try to minify known colors)
884
- let c1 = parsed1.raw;
885
- let c2 = parsed2.raw;
886
- if (parsed1.color) {
887
- c1 = rgbaToHex(parsed1.color[0], parsed1.color[1], parsed1.color[2], parsed1.color[3]);
888
- }
889
- if (parsed2.color) {
890
- c2 = rgbaToHex(parsed2.color[0], parsed2.color[1], parsed2.color[2], parsed2.color[3]);
891
- }
892
-
893
- // oklab is the default interpolation method per CSS Color 5 — elide it
894
- const spacePrefix = colorSpace === 'oklab' ? '' : 'in ' + colorSpace + ',';
895
- return 'color-mix(' + spacePrefix + c1 + p1Str + ',' + c2 + p2Str + ')';
486
+ const srgb = labToSrgb(L, a, b);
487
+ return rgbaToHex(Math.round(srgb.r * 255), Math.round(srgb.g * 255), Math.round(srgb.b * 255), alpha !== undefined ? alpha : 1);
896
488
  }
897
489
 
898
490
  /**
@@ -916,26 +508,6 @@ function convertOklabToHex (L, a, b, alpha) {
916
508
  return rgbaToHex(r, g, bl, alpha !== undefined ? alpha : 1);
917
509
  }
918
510
 
919
- /**
920
- * Handle color(from ...) relative color syntax for simple identity cases.
921
- *
922
- * @param {string} expr The color(from ...) expression string.
923
- * @return {string|null} A hex color string if the relative color is a simple identity transform, or null otherwise.
924
- */
925
- function evaluateRelativeColor (expr) {
926
- // Match: color(from <base-color> srgb r g b [/ <alpha>]) identity transform pattern
927
- const match = expr.match(/^color\(\s*from\s+(.+?)\s+srgb\s+r\s+g\s+b(?:\s*\/\s*([\d.]+%?))?\s*\)$/i);
928
- if (!match) {
929
- return null;
930
- }
931
- const baseColor = parseColor(match[1]);
932
- if (!baseColor) {
933
- return null;
934
- }
935
- const alpha = parseAlphaString(match[2], baseColor[3]);
936
- return rgbaToHex(baseColor[0], baseColor[1], baseColor[2], alpha);
937
- }
938
-
939
511
  export {
940
512
  hslToRgbChannels,
941
513
  rgbaToHex,
@@ -943,9 +515,8 @@ export {
943
515
  namedColors,
944
516
  parseColor,
945
517
  parseHex,
946
- evaluateColorMix,
518
+ convertLabToHex,
947
519
  convertOklabToHex,
948
- evaluateRelativeColor,
949
520
  shortestColor,
950
521
  srgbToOklab,
951
522
  oklabToSrgb,