UNPKG

postcss-color-hct

Version:

PostCSS plugin to transform hct() function to more compatible CSS (rgb() or rgba()).

629 lines (628 loc) 21.9 kB
"use strict"; Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } }); var postcss = require("postcss"); var reduceFunctionCall = require("reduce-function-call"); /** * @license * Copyright 2021 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ function signum(num) { if (num < 0) { return -1; } else if (num === 0) { return 0; } else { return 1; } } function lerp(start, stop, amount) { return (1 - amount) * start + amount * stop; } function clampInt(min, max, input) { if (input < min) { return min; } else if (input > max) { return max; } return input; } function clampDouble(min, max, input) { if (input < min) { return min; } else if (input > max) { return max; } return input; } function sanitizeDegreesDouble(degrees) { degrees = degrees % 360; if (degrees < 0) { degrees = degrees + 360; } return degrees; } function matrixMultiply(row, matrix) { const a = row[0] * matrix[0][0] + row[1] * matrix[0][1] + row[2] * matrix[0][2]; const b = row[0] * matrix[1][0] + row[1] * matrix[1][1] + row[2] * matrix[1][2]; const c = row[0] * matrix[2][0] + row[1] * matrix[2][1] + row[2] * matrix[2][2]; return [a, b, c]; } /** * @license * Copyright 2021 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const SRGB_TO_XYZ = [ [0.41233895, 0.35762064, 0.18051042], [0.2126, 0.7152, 0.0722], [0.01932141, 0.11916382, 0.95034478] ]; const XYZ_TO_SRGB = [ [ 3.2413774792388685, -1.5376652402851851, -0.49885366846268053 ], [ -0.9691452513005321, 1.8758853451067872, 0.04156585616912061 ], [ 0.05562093689691305, -0.20395524564742123, 1.0571799111220335 ] ]; const WHITE_POINT_D65 = [95.047, 100, 108.883]; function argbFromRgb(red, green, blue) { return (255 << 24 | (red & 255) << 16 | (green & 255) << 8 | blue & 255) >>> 0; } function redFromArgb(argb) { return argb >> 16 & 255; } function greenFromArgb(argb) { return argb >> 8 & 255; } function blueFromArgb(argb) { return argb & 255; } function argbFromXyz(x, y, z) { const matrix = XYZ_TO_SRGB; const linearR = matrix[0][0] * x + matrix[0][1] * y + matrix[0][2] * z; const linearG = matrix[1][0] * x + matrix[1][1] * y + matrix[1][2] * z; const linearB = matrix[2][0] * x + matrix[2][1] * y + matrix[2][2] * z; const r = delinearized(linearR); const g = delinearized(linearG); const b = delinearized(linearB); return argbFromRgb(r, g, b); } function xyzFromArgb(argb) { const r = linearized(redFromArgb(argb)); const g = linearized(greenFromArgb(argb)); const b = linearized(blueFromArgb(argb)); return matrixMultiply([r, g, b], SRGB_TO_XYZ); } function argbFromLstar(lstar) { const fy = (lstar + 16) / 116; const fz = fy; const fx = fy; const kappa = 24389 / 27; const epsilon = 216 / 24389; const lExceedsEpsilonKappa = lstar > 8; const y = lExceedsEpsilonKappa ? fy * fy * fy : lstar / kappa; const cubeExceedEpsilon = fy * fy * fy > epsilon; const x = cubeExceedEpsilon ? fx * fx * fx : lstar / kappa; const z = cubeExceedEpsilon ? fz * fz * fz : lstar / kappa; const whitePoint = WHITE_POINT_D65; return argbFromXyz(x * whitePoint[0], y * whitePoint[1], z * whitePoint[2]); } function lstarFromArgb(argb) { const y = xyzFromArgb(argb)[1] / 100; const e = 216 / 24389; if (y <= e) { return 24389 / 27 * y; } else { const yIntermediate = Math.pow(y, 1 / 3); return 116 * yIntermediate - 16; } } function yFromLstar(lstar) { const ke = 8; if (lstar > ke) { return Math.pow((lstar + 16) / 116, 3) * 100; } else { return lstar / (24389 / 27) * 100; } } function linearized(rgbComponent) { const normalized = rgbComponent / 255; if (normalized <= 0.040449936) { return normalized / 12.92 * 100; } else { return Math.pow((normalized + 0.055) / 1.055, 2.4) * 100; } } function delinearized(rgbComponent) { const normalized = rgbComponent / 100; let delinearized2 = 0; if (normalized <= 31308e-7) { delinearized2 = normalized * 12.92; } else { delinearized2 = 1.055 * Math.pow(normalized, 1 / 2.4) - 0.055; } return clampInt(0, 255, Math.round(delinearized2 * 255)); } function whitePointD65() { return WHITE_POINT_D65; } /** * @license * Copyright 2021 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ class ViewingConditions { constructor(n, aw, nbb, ncb, c, nc, rgbD, fl, fLRoot, z) { this.n = n; this.aw = aw; this.nbb = nbb; this.ncb = ncb; this.c = c; this.nc = nc; this.rgbD = rgbD; this.fl = fl; this.fLRoot = fLRoot; this.z = z; } static make(whitePoint = whitePointD65(), adaptingLuminance = 200 / Math.PI * yFromLstar(50) / 100, backgroundLstar = 50, surround = 2, discountingIlluminant = false) { const xyz = whitePoint; const rW = xyz[0] * 0.401288 + xyz[1] * 0.650173 + xyz[2] * -0.051461; const gW = xyz[0] * -0.250268 + xyz[1] * 1.204414 + xyz[2] * 0.045854; const bW = xyz[0] * -2079e-6 + xyz[1] * 0.048952 + xyz[2] * 0.953127; const f = 0.8 + surround / 10; const c = f >= 0.9 ? lerp(0.59, 0.69, (f - 0.9) * 10) : lerp(0.525, 0.59, (f - 0.8) * 10); let d = discountingIlluminant ? 1 : f * (1 - 1 / 3.6 * Math.exp((-adaptingLuminance - 42) / 92)); d = d > 1 ? 1 : d < 0 ? 0 : d; const nc = f; const rgbD = [ d * (100 / rW) + 1 - d, d * (100 / gW) + 1 - d, d * (100 / bW) + 1 - d ]; const k = 1 / (5 * adaptingLuminance + 1); const k4 = k * k * k * k; const k4F = 1 - k4; const fl = k4 * adaptingLuminance + 0.1 * k4F * k4F * Math.cbrt(5 * adaptingLuminance); const n = yFromLstar(backgroundLstar) / whitePoint[1]; const z = 1.48 + Math.sqrt(n); const nbb = 0.725 / Math.pow(n, 0.2); const ncb = nbb; const rgbAFactors = [ Math.pow(fl * rgbD[0] * rW / 100, 0.42), Math.pow(fl * rgbD[1] * gW / 100, 0.42), Math.pow(fl * rgbD[2] * bW / 100, 0.42) ]; const rgbA = [ 400 * rgbAFactors[0] / (rgbAFactors[0] + 27.13), 400 * rgbAFactors[1] / (rgbAFactors[1] + 27.13), 400 * rgbAFactors[2] / (rgbAFactors[2] + 27.13) ]; const aw = (2 * rgbA[0] + rgbA[1] + 0.05 * rgbA[2]) * nbb; return new ViewingConditions(n, aw, nbb, ncb, c, nc, rgbD, fl, Math.pow(fl, 0.25), z); } } ViewingConditions.DEFAULT = ViewingConditions.make(); /** * @license * Copyright 2021 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ class Cam16 { constructor(hue, chroma, j, q, m, s, jstar, astar, bstar) { this.hue = hue; this.chroma = chroma; this.j = j; this.q = q; this.m = m; this.s = s; this.jstar = jstar; this.astar = astar; this.bstar = bstar; } distance(other) { const dJ = this.jstar - other.jstar; const dA = this.astar - other.astar; const dB = this.bstar - other.bstar; const dEPrime = Math.sqrt(dJ * dJ + dA * dA + dB * dB); const dE = 1.41 * Math.pow(dEPrime, 0.63); return dE; } static fromInt(argb) { return Cam16.fromIntInViewingConditions(argb, ViewingConditions.DEFAULT); } static fromIntInViewingConditions(argb, viewingConditions) { const red = (argb & 16711680) >> 16; const green = (argb & 65280) >> 8; const blue = argb & 255; const redL = linearized(red); const greenL = linearized(green); const blueL = linearized(blue); const x = 0.41233895 * redL + 0.35762064 * greenL + 0.18051042 * blueL; const y = 0.2126 * redL + 0.7152 * greenL + 0.0722 * blueL; const z = 0.01932141 * redL + 0.11916382 * greenL + 0.95034478 * blueL; const rC = 0.401288 * x + 0.650173 * y - 0.051461 * z; const gC = -0.250268 * x + 1.204414 * y + 0.045854 * z; const bC = -2079e-6 * x + 0.048952 * y + 0.953127 * z; const rD = viewingConditions.rgbD[0] * rC; const gD = viewingConditions.rgbD[1] * gC; const bD = viewingConditions.rgbD[2] * bC; const rAF = Math.pow(viewingConditions.fl * Math.abs(rD) / 100, 0.42); const gAF = Math.pow(viewingConditions.fl * Math.abs(gD) / 100, 0.42); const bAF = Math.pow(viewingConditions.fl * Math.abs(bD) / 100, 0.42); const rA = signum(rD) * 400 * rAF / (rAF + 27.13); const gA = signum(gD) * 400 * gAF / (gAF + 27.13); const bA = signum(bD) * 400 * bAF / (bAF + 27.13); const a = (11 * rA + -12 * gA + bA) / 11; const b = (rA + gA - 2 * bA) / 9; const u = (20 * rA + 20 * gA + 21 * bA) / 20; const p2 = (40 * rA + 20 * gA + bA) / 20; const atan2 = Math.atan2(b, a); const atanDegrees = atan2 * 180 / Math.PI; const hue = atanDegrees < 0 ? atanDegrees + 360 : atanDegrees >= 360 ? atanDegrees - 360 : atanDegrees; const hueRadians = hue * Math.PI / 180; const ac = p2 * viewingConditions.nbb; const j = 100 * Math.pow(ac / viewingConditions.aw, viewingConditions.c * viewingConditions.z); const q = 4 / viewingConditions.c * Math.sqrt(j / 100) * (viewingConditions.aw + 4) * viewingConditions.fLRoot; const huePrime = hue < 20.14 ? hue + 360 : hue; const eHue = 0.25 * (Math.cos(huePrime * Math.PI / 180 + 2) + 3.8); const p1 = 5e4 / 13 * eHue * viewingConditions.nc * viewingConditions.ncb; const t = p1 * Math.sqrt(a * a + b * b) / (u + 0.305); const alpha = Math.pow(t, 0.9) * Math.pow(1.64 - Math.pow(0.29, viewingConditions.n), 0.73); const c = alpha * Math.sqrt(j / 100); const m = c * viewingConditions.fLRoot; const s = 50 * Math.sqrt(alpha * viewingConditions.c / (viewingConditions.aw + 4)); const jstar = (1 + 100 * 7e-3) * j / (1 + 7e-3 * j); const mstar = 1 / 0.0228 * Math.log(1 + 0.0228 * m); const astar = mstar * Math.cos(hueRadians); const bstar = mstar * Math.sin(hueRadians); return new Cam16(hue, c, j, q, m, s, jstar, astar, bstar); } static fromJch(j, c, h) { return Cam16.fromJchInViewingConditions(j, c, h, ViewingConditions.DEFAULT); } static fromJchInViewingConditions(j, c, h, viewingConditions) { const q = 4 / viewingConditions.c * Math.sqrt(j / 100) * (viewingConditions.aw + 4) * viewingConditions.fLRoot; const m = c * viewingConditions.fLRoot; const alpha = c / Math.sqrt(j / 100); const s = 50 * Math.sqrt(alpha * viewingConditions.c / (viewingConditions.aw + 4)); const hueRadians = h * Math.PI / 180; const jstar = (1 + 100 * 7e-3) * j / (1 + 7e-3 * j); const mstar = 1 / 0.0228 * Math.log(1 + 0.0228 * m); const astar = mstar * Math.cos(hueRadians); const bstar = mstar * Math.sin(hueRadians); return new Cam16(h, c, j, q, m, s, jstar, astar, bstar); } static fromUcs(jstar, astar, bstar) { return Cam16.fromUcsInViewingConditions(jstar, astar, bstar, ViewingConditions.DEFAULT); } static fromUcsInViewingConditions(jstar, astar, bstar, viewingConditions) { const a = astar; const b = bstar; const m = Math.sqrt(a * a + b * b); const M = (Math.exp(m * 0.0228) - 1) / 0.0228; const c = M / viewingConditions.fLRoot; let h = Math.atan2(b, a) * (180 / Math.PI); if (h < 0) { h += 360; } const j = jstar / (1 - (jstar - 100) * 7e-3); return Cam16.fromJchInViewingConditions(j, c, h, viewingConditions); } toInt() { return this.viewed(ViewingConditions.DEFAULT); } viewed(viewingConditions) { const alpha = this.chroma === 0 || this.j === 0 ? 0 : this.chroma / Math.sqrt(this.j / 100); const t = Math.pow(alpha / Math.pow(1.64 - Math.pow(0.29, viewingConditions.n), 0.73), 1 / 0.9); const hRad = this.hue * Math.PI / 180; const eHue = 0.25 * (Math.cos(hRad + 2) + 3.8); const ac = viewingConditions.aw * Math.pow(this.j / 100, 1 / viewingConditions.c / viewingConditions.z); const p1 = eHue * (5e4 / 13) * viewingConditions.nc * viewingConditions.ncb; const p2 = ac / viewingConditions.nbb; const hSin = Math.sin(hRad); const hCos = Math.cos(hRad); const gamma = 23 * (p2 + 0.305) * t / (23 * p1 + 11 * t * hCos + 108 * t * hSin); const a = gamma * hCos; const b = gamma * hSin; const rA = (460 * p2 + 451 * a + 288 * b) / 1403; const gA = (460 * p2 - 891 * a - 261 * b) / 1403; const bA = (460 * p2 - 220 * a - 6300 * b) / 1403; const rCBase = Math.max(0, 27.13 * Math.abs(rA) / (400 - Math.abs(rA))); const rC = signum(rA) * (100 / viewingConditions.fl) * Math.pow(rCBase, 1 / 0.42); const gCBase = Math.max(0, 27.13 * Math.abs(gA) / (400 - Math.abs(gA))); const gC = signum(gA) * (100 / viewingConditions.fl) * Math.pow(gCBase, 1 / 0.42); const bCBase = Math.max(0, 27.13 * Math.abs(bA) / (400 - Math.abs(bA))); const bC = signum(bA) * (100 / viewingConditions.fl) * Math.pow(bCBase, 1 / 0.42); const rF = rC / viewingConditions.rgbD[0]; const gF = gC / viewingConditions.rgbD[1]; const bF = bC / viewingConditions.rgbD[2]; const x = 1.86206786 * rF - 1.01125463 * gF + 0.14918677 * bF; const y = 0.38752654 * rF + 0.62144744 * gF - 897398e-8 * bF; const z = -0.0158415 * rF - 0.03412294 * gF + 1.04996444 * bF; const argb = argbFromXyz(x, y, z); return argb; } } /** * @license * Copyright 2021 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ class Hct { constructor(internalHue, internalChroma, internalTone) { this.internalHue = internalHue; this.internalChroma = internalChroma; this.internalTone = internalTone; this.setInternalState(this.toInt()); } static from(hue, chroma, tone) { return new Hct(hue, chroma, tone); } static fromInt(argb) { const cam = Cam16.fromInt(argb); const tone = lstarFromArgb(argb); return new Hct(cam.hue, cam.chroma, tone); } toInt() { return getInt(this.internalHue, this.internalChroma, this.internalTone); } get hue() { return this.internalHue; } set hue(newHue) { this.setInternalState(getInt(sanitizeDegreesDouble(newHue), this.internalChroma, this.internalTone)); } get chroma() { return this.internalChroma; } set chroma(newChroma) { this.setInternalState(getInt(this.internalHue, newChroma, this.internalTone)); } get tone() { return this.internalTone; } set tone(newTone) { this.setInternalState(getInt(this.internalHue, this.internalChroma, newTone)); } setInternalState(argb) { const cam = Cam16.fromInt(argb); const tone = lstarFromArgb(argb); this.internalHue = cam.hue; this.internalChroma = cam.chroma; this.internalTone = tone; } } const CHROMA_SEARCH_ENDPOINT = 0.4; const DE_MAX = 1; const DL_MAX = 0.2; const LIGHTNESS_SEARCH_ENDPOINT = 0.01; function getInt(hue, chroma, tone) { return getIntInViewingConditions(sanitizeDegreesDouble(hue), chroma, clampDouble(0, 100, tone), ViewingConditions.DEFAULT); } function getIntInViewingConditions(hue, chroma, tone, viewingConditions) { if (chroma < 1 || Math.round(tone) <= 0 || Math.round(tone) >= 100) { return argbFromLstar(tone); } hue = sanitizeDegreesDouble(hue); let high = chroma; let mid = chroma; let low = 0; let isFirstLoop = true; let answer = null; while (Math.abs(low - high) >= CHROMA_SEARCH_ENDPOINT) { const possibleAnswer = findCamByJ(hue, mid, tone); if (isFirstLoop) { if (possibleAnswer != null) { return possibleAnswer.viewed(viewingConditions); } else { isFirstLoop = false; mid = low + (high - low) / 2; continue; } } if (possibleAnswer === null) { high = mid; } else { answer = possibleAnswer; low = mid; } mid = low + (high - low) / 2; } if (answer === null) { return argbFromLstar(tone); } return answer.viewed(viewingConditions); } function findCamByJ(hue, chroma, tone) { let low = 0; let high = 100; let mid = 0; let bestdL = 1e3; let bestdE = 1e3; let bestCam = null; while (Math.abs(low - high) > LIGHTNESS_SEARCH_ENDPOINT) { mid = low + (high - low) / 2; const camBeforeClip = Cam16.fromJch(mid, chroma, hue); const clipped = camBeforeClip.toInt(); const clippedLstar = lstarFromArgb(clipped); const dL = Math.abs(tone - clippedLstar); if (dL < DL_MAX) { const camClipped = Cam16.fromInt(clipped); const dE = camClipped.distance(Cam16.fromJch(camClipped.j, camClipped.chroma, hue)); if (dE <= DE_MAX && dE <= bestdE) { bestdL = dL; bestdE = dE; bestCam = camClipped; } } if (bestdL === 0 && bestdE === 0) { break; } if (clippedLstar < tone) { low = mid; } else { high = mid; } } return bestCam; } /** * @license * Copyright 2021 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const hexFromArgb = (argb) => { const r = redFromArgb(argb); const g = greenFromArgb(argb); const b = blueFromArgb(argb); const outParts = [r.toString(16), g.toString(16), b.toString(16)]; for (const [i, part] of outParts.entries()) { if (part.length === 1) { outParts[i] = "0" + part; } } return "#" + outParts.join(""); }; function getColor(values) { if (values === void 0 || values.length === 0) { throw "HCT values are undefined"; } if (values.some(isNaN)) { throw "HCT values are not numbers"; } const [h, c, t] = values; if (isNaN(+h) || isNaN(+c) || isNaN(+t)) { throw 'Unable to parse HCT color: "' + values.join(",") + '"'; } const hex = hctToHex(h, c, t); const opacity = values.length > 3 ? values[3] : void 0; return rgbToString(hexToRgb(hex), opacity); } function hctToHex(h, c, t) { const color = Hct.from(Number(h), Number(c), Number(t)); const hex = hexFromArgb(color.toInt()); return hex; } function hexToRgb(hex) { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); const { r, g, b } = { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) }; return [r, g, b]; } function rgbToString(rgb, opacity) { const [r, g, b] = rgb; if (opacity) { return `rgb(${r} ${g} ${b} / ${opacity})`; } else { return `rgb(${r} ${g} ${b})`; } } function justFloat(n) { return parseFloat(n); } function colorValuesDefined(hct) { return !hct.some(isNaN); } function transformDecl(decl) { const value = decl.value; function reduceHcl(body) { const hctaValues = body.split(",").map(justFloat); if (!colorValuesDefined(hctaValues)) { throw decl.error('Unable to parse color: "' + value + '"'); } try { return getColor(hctaValues); } catch (e) { throw decl.error(e); } } decl.value = reduceFunctionCall(value, "hct", reduceHcl); } function colorHcl(css) { css.walkDecls(transformDecl); } function colorHclPlugin() { return colorHcl; } module.exports = postcss.plugin("postcss-color-hct", colorHclPlugin); exports.colorHclPlugin = colorHclPlugin;