UNPKG

@tbela99/css-parser

Version:

CSS parser, minifier and validator for node and the browser

426 lines (423 loc) 14.9 kB
import { EnumToken, ColorType } from '../../ast/types.js'; import { minmax, getNumber, toPrecisionValue, toPrecisionAngle } from './color.js'; import { srgbvalues, srgb2lsrgbvalues } from './srgb.js'; import { srgb2lch, xyz2lchvalues } from './lch.js'; import { srgb2rgb } from './rgb.js'; import { srgb2hslvalues } from './hsl.js'; import { srgb2hwb } from './hwb.js'; import { srgb2labvalues } from './lab.js'; import { srgb2lp3values, srgb2p3values } from './p3.js'; import { getColorComponents } from './utils/components.js'; import { srgb2oklch } from './oklch.js'; import { srgb2oklab } from './oklab.js'; import { srgb2prophotorgbvalues } from './prophotorgb.js'; import { srgb2xyz_d65 } from './xyz.js'; import { XYZ_D65_to_D50, xyzd502lch } from './xyzd50.js'; import { srgb2rec2020values } from './rec2020.js'; import { isRectangularOrthogonalColorspace, isPolarColorspace } from '../syntax.js'; import { equalsIgnoreCase } from '../../parser/utils/text.js'; function interpolateHue(interpolationMethod, h1, h2) { switch (interpolationMethod) { case "longer": if (h2 - h1 < 180 && h2 - h1 > 0) { h1 += 360; } else if (h2 - h1 <= 0 && h2 - h1 > -180) { h2 += 360; } break; case "increasing": if (h2 < h1) { h2 += 360; } break; case "decreasing": if (h2 > h1) { h1 += 360; } break; case "shorter": default: // shorter if (h2 - h1 > 180) { h1 += 360; } else if (h2 - h1 < -180) { h2 += 360; } break; } return [h1, h2]; } /** * Compute color mix * @param args * @returns */ function colorMix(...args) { // invalid color or custom color profile if (args.length == 0 || args[0].typ === EnumToken.DashedIdenTokenType) { return null; } let i = 0; let missingPercentageCount = 0; let totalPercentage = 0; let leftOverPercentage = 0; let colorSpace = "oklab"; let hueInterpolationMethod = "shorter"; let values = null; const colors = []; const percentages = []; const srgbComponentValues = []; const colorComponents = []; if (args[i]?.typ === EnumToken.IdenTokenType) { if (!equalsIgnoreCase(args[i++].val, "in")) { return null; } if (args[i]?.typ !== EnumToken.IdenTokenType) { return null; } if (isRectangularOrthogonalColorspace(args[i])) { colorSpace = args[i++].val; } else if (isPolarColorspace(args[i])) { colorSpace = args[i++].val; if (args[i]?.typ !== EnumToken.IdenTokenType && args[i]?.typ !== EnumToken.ColorTokenType) { return null; } if (args[i].typ === EnumToken.IdenTokenType && !equalsIgnoreCase(args[i].val, "hue")) { if (args[i]?.typ !== EnumToken.IdenTokenType) { return null; } hueInterpolationMethod = args[i++].val; switch (hueInterpolationMethod) { case "increasing": case "decreasing": case "longer": case "shorter": break; default: return null; } if (!equalsIgnoreCase(args[i++].val, "hue")) { return null; } } } } while (i < args.length) { if (args[i].typ !== EnumToken.ColorTokenType || ColorType.SYS == args[i].kin || ColorType.DPSYS == args[i].kin || ColorType.NON_STD == args[i].kin || ColorType.CUSTOM_COLOR == args[i].kin || equalsIgnoreCase(args[i].val, "currentcolor")) { return null; } colorComponents.push(getColorComponents(args[i])); values = srgbvalues(args[i]); if (values == null) { return null; } switch (colorSpace) { case "srgb": break; case "display-p3": // @ts-ignore values = srgb2p3values(...values); break; case "display-p3-linear": // @ts-ignore values = srgb2lp3values(...values); break; case "a98-rgb": // @ts-ignore values = srgb2a98values(...values); break; case "prophoto-rgb": // @ts-ignore values = srgb2prophotorgbvalues(...values); break; case "srgb-linear": // @ts-ignore values = srgb2lsrgbvalues(...values); break; case "rec2020": // @ts-ignore values = srgb2rec2020values(...values); break; case "xyz": case "xyz-d65": // @ts-ignore values = srgb2xyz_d65(...values); break; case "xyz-d50": // @ts-ignore values = XYZ_D65_to_D50(...srgb2xyz_d65(...values)); break; case "rgb": // @ts-ignore values = srgb2rgb(...values); break; case "hsl": // @ts-ignore values = srgb2hslvalues(...values); break; case "hwb": // @ts-ignore values = srgb2hwb(...values); break; case "lab": // @ts-ignore values = srgb2labvalues(...values); break; case "lch": // @ts-ignore values = srgb2lch(...values); break; case "oklab": // @ts-ignore values = srgb2oklab(...values); break; case "oklch": // @ts-ignore values = srgb2oklch(...values); break; default: return null; } srgbComponentValues.push(values); colors.push(args[i++]); if (i >= args.length) { missingPercentageCount++; percentages.push(null); break; } if (args[i]?.typ === EnumToken.ColorTokenType) { percentages.push(null); missingPercentageCount++; continue; } if (args[i]?.typ === EnumToken.PercentageTokenType || (args[i]?.typ === EnumToken.NumberTokenType && 0 == args[i].val)) { if (args[i].val < 0) { return null; } percentages.push(minmax(getNumber(args[i++]), 0, 1)); totalPercentage += percentages.at(-1); } } // normalize percentages if (missingPercentageCount > 0) { let normalizedTotalPercentages = totalPercentage > 1 ? 0 : 1 - totalPercentage; for (i = 0; i < percentages.length; i++) { if (percentages[i] == null) { percentages[i] = normalizedTotalPercentages / missingPercentageCount; } } totalPercentage = 0; for (i = 0; i < percentages.length; i++) { totalPercentage += percentages[i]; } } if (totalPercentage != 1) { if (totalPercentage < 1) { leftOverPercentage = 1 - totalPercentage; } const perc = totalPercentage == 0 ? 1 : totalPercentage; // scale down percentages for (i = 0; i < percentages.length; i++) { percentages[i] = percentages[i] / perc; } totalPercentage = 1; } i = colors.length; let currentIndex = 0; let r1; let r2; let r; let mult1; let mult2; let mult; let premult1; let premult2; let mixedPremult; let colorSpace1; const stack = []; const lchSpaces = ["lch", "oklch"]; i = srgbComponentValues.length; while (i--) { stack.push({ color: srgbComponentValues[i], alpha: percentages[i], }); } // @ts-expect-error colorSpace1 = ColorType[colorComponents.at(-1).kin]?.toLowerCase?.(); if (colorComponents[0][3] != null && colorComponents[0][3].typ == EnumToken.IdenTokenType && colorComponents[0][3].val == "none" && colorComponents[1].length == 4) { // fix powerless alpha for last color if previous color has alpha stack[stack.length - 1].color[3] = stack[stack.length - 2].color[3]; } // powerless hue for lch spaces if (lchSpaces.includes(colorSpace1) || lchSpaces.includes(colorSpace)) { if ((stack.length > 1 && colorComponents[0][2].typ == EnumToken.IdenTokenType && colorComponents[0][2].val == "none") || stack[stack.length - 1].color[2] == 0) { stack[stack.length - 1].color[2] = stack[stack.length - 2].color[2]; } } while (stack.length > 1) { r1 = stack.pop(); r2 = stack.pop(); // r2 powerless alpha if (colorComponents[++currentIndex][3] != null && colorComponents[currentIndex][3].typ == EnumToken.IdenTokenType && colorComponents[currentIndex][3].val == "none") { // fix powerless alpha for last color if previous color has alpha r2.color[3] = r1.color[3]; } // @ts-expect-error colorSpace1 = ColorType[colorComponents[currentIndex].kin]?.toLowerCase?.(); // powerless hue for lch spaces if (lchSpaces.includes(colorSpace1) || lchSpaces.includes(colorSpace)) { if ((colorComponents[currentIndex][2].typ == EnumToken.IdenTokenType && colorComponents[currentIndex][2].val == "none") || r2.color[2] == 0) { r2.color[2] = r1.color[2]; } } if (hueInterpolationMethod != null) { let hueIndex = 2; let multiplier = 1; if (colorSpace == "hwb" || colorSpace == "hsl") { hueIndex = 0; multiplier = 360; } const [h1, h2] = interpolateHue(hueInterpolationMethod, r1.color[hueIndex] * multiplier, r2.color[hueIndex] * multiplier); r1.color[hueIndex] = h1 / multiplier; r2.color[hueIndex] = h2 / multiplier; } mult1 = r1.color[3] ?? 1; mult2 = r2.color[3] ?? 1; mult = mult1 * r1.alpha + mult2 * r2.alpha; premult1 = [r1.color[0] * mult1 * r1.alpha, r1.color[1] * mult1 * r1.alpha, r1.color[2] * mult1 * r1.alpha]; premult2 = [r2.color[0] * mult2 * r2.alpha, r2.color[1] * mult2 * r2.alpha, r2.color[2] * mult2 * r2.alpha]; mixedPremult = [premult1[0] + premult2[0], premult1[1] + premult2[1], premult1[2] + premult2[2]]; if (mult == 0) { r = { color: [0, 0, 0, 0], alpha: 0 }; } else { r = { color: [mixedPremult[0] / mult, mixedPremult[1] / mult, mixedPremult[2] / mult], alpha: mult, }; } stack.push(r); } const result = stack.pop(); values = result.color; values.length = 3; const alpha = result.alpha * (1 - leftOverPercentage); if (alpha != 1) { values.push(alpha); } switch (colorSpace) { case "xyz": case "xyz-d65": case "xyz-d50": if (colorSpace == "xyz-d50") { // @ts-ignore values = xyzd502lch(...values); } else { // @ts-ignore values = xyz2lchvalues(...values); } // @ts-ignore return { typ: EnumToken.ColorTokenType, val: "lch", chi: values.map((v) => { return { typ: EnumToken.NumberTokenType, val: toPrecisionValue(v), }; }), kin: ColorType.LCH, }; case "srgb": case "srgb-linear": case "a98-rgb": case "rec2020": case "display-p3": case "display-p3-linear": case "prophoto-rgb": // @ts-ignore return { typ: EnumToken.ColorTokenType, val: "color", chi: [{ typ: EnumToken.IdenTokenType, val: colorSpace }].concat( // @ts-expect-error values.map((v) => { return { typ: EnumToken.NumberTokenType, val: toPrecisionValue(v), }; })), kin: ColorType.COLOR, cal: "col", }; case "rgb": case "hsl": case "hwb": case "lab": case "lch": case "oklab": case "oklch": if (colorSpace == "hsl" || colorSpace == "hwb") { // @ts-ignore if (values[2] < 0) { // @ts-ignore values[2] += 1; } } else if (colorSpace == "lch" || colorSpace == "oklch") { // @ts-ignore if (values[2] < 0) { // @ts-ignore values[2] += 360; } else if (values[2] > 360) { // @ts-ignore values[2] %= 360; } } // @ts-ignore const result = { typ: EnumToken.ColorTokenType, val: colorSpace, chi: values.map((v) => { return { typ: EnumToken.NumberTokenType, val: toPrecisionValue(v), }; }), kin: ColorType[colorSpace.toUpperCase().replaceAll("-", "_")], }; if (colorSpace == "hsl" || colorSpace == "hwb") { // @ts-ignore result.chi[0] = { typ: EnumToken.AngleTokenType, val: toPrecisionAngle(result.chi[0].val * 360) }; // @ts-ignore result.chi[1] = { typ: EnumToken.PercentageTokenType, val: toPrecisionValue(result.chi[1].val) * 100 }; // @ts-ignore result.chi[2] = { typ: EnumToken.PercentageTokenType, val: toPrecisionValue(result.chi[2].val) * 100 }; } return result; } return null; } export { colorMix };