radix-theme-generator
Version:
A utility for generating themes for the Radix Themes UI library
496 lines • 23.3 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
if (ar || !(i in from)) {
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
ar[i] = from[i];
}
}
return to.concat(ar || Array.prototype.slice.call(from));
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateRadixColors = exports.lightColors = void 0;
var RadixColors = __importStar(require("@radix-ui/colors"));
var bezier_easing_1 = __importDefault(require("bezier-easing"));
var colorjs_io_1 = __importDefault(require("colorjs.io"));
var types_1 = require("../types");
var arrayOf12 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
exports.lightColors = Object.fromEntries(types_1.scaleNames.map(function (scaleName) { return [
scaleName,
Object.values(RadixColors["".concat(scaleName, "P3")]).map(function (str) {
return new colorjs_io_1.default(str).to("oklch");
}),
]; }));
var darkColors = Object.fromEntries(types_1.scaleNames.map(function (scaleName) { return [
scaleName,
Object.values(RadixColors["".concat(scaleName, "DarkP3")]).map(function (str) {
return new colorjs_io_1.default(str).to("oklch");
}),
]; }));
var lightGrayColors = Object.fromEntries(types_1.grayScaleNames.map(function (scaleName) { return [
scaleName,
Object.values(RadixColors["".concat(scaleName, "P3")]).map(function (str) {
return new colorjs_io_1.default(str).to("oklch");
}),
]; }));
var darkGrayColors = Object.fromEntries(types_1.grayScaleNames.map(function (scaleName) { return [
scaleName,
Object.values(RadixColors["".concat(scaleName, "DarkP3")]).map(function (str) {
return new colorjs_io_1.default(str).to("oklch");
}),
]; }));
var generateRadixColors = function (_a) {
var appearance = _a.appearance, args = __rest(_a, ["appearance"]);
var allScales = appearance === "light" ? exports.lightColors : darkColors;
var grayScales = appearance === "light" ? lightGrayColors : darkGrayColors;
var backgroundColor = new colorjs_io_1.default(args.background).to("oklch");
var grayBaseColor = new colorjs_io_1.default(args.gray).to("oklch");
var grayScaleColors = getScaleFromColor(grayBaseColor, grayScales, backgroundColor);
var accentBaseColor = new colorjs_io_1.default(args.accent).to("oklch");
var accentScaleColors = getScaleFromColor(accentBaseColor, allScales, backgroundColor);
// Enforce srgb for the background color
var backgroundHex = backgroundColor.to("srgb").toString({ format: "hex" });
// Make sure we use the tint from the gray scale for when base is pure white or black
var accentBaseHex = accentBaseColor.to("srgb").toString({ format: "hex" });
if (accentBaseHex === "#000" || accentBaseHex === "#fff") {
accentScaleColors = grayScaleColors.map(function (color) {
return color.clone();
});
}
var _b = getStep9Colors(accentScaleColors, accentBaseColor), accent9Color = _b[0], accentContrastColor = _b[1];
accentScaleColors[8] = accent9Color;
accentScaleColors[9] = getButtonHoverColor(accent9Color, [accentScaleColors]);
// Limit saturation of the text colors
accentScaleColors[10].coords[1] = Math.min(Math.max(accentScaleColors[8].coords[1], accentScaleColors[7].coords[1]), accentScaleColors[10].coords[1]);
accentScaleColors[11].coords[1] = Math.min(Math.max(accentScaleColors[8].coords[1], accentScaleColors[7].coords[1]), accentScaleColors[11].coords[1]);
var accentScaleHex = accentScaleColors.map(function (color) {
return color.to("srgb").toString({ format: "hex" });
});
var accentScaleWideGamut = accentScaleColors.map(toOklchString);
var accentScaleAlphaHex = accentScaleHex.map(function (color) {
return getAlphaColorSrgb(color, backgroundHex);
});
var accentScaleAlphaWideGamutString = accentScaleHex.map(function (color) {
return getAlphaColorP3(color, backgroundHex);
});
var accentContrastColorHex = accentContrastColor
.to("srgb")
.toString({ format: "hex" });
var grayScaleHex = grayScaleColors.map(function (color) {
return color.to("srgb").toString({ format: "hex" });
});
var grayScaleWideGamut = grayScaleColors.map(toOklchString);
var grayScaleAlphaHex = grayScaleHex.map(function (color) {
return getAlphaColorSrgb(color, backgroundHex);
});
var grayScaleAlphaWideGamutString = grayScaleHex.map(function (color) {
return getAlphaColorP3(color, backgroundHex);
});
var accentSurfaceHex = appearance === "light"
? getAlphaColorSrgb(accentScaleHex[1], backgroundHex, 0.8)
: getAlphaColorSrgb(accentScaleHex[1], backgroundHex, 0.5);
var accentSurfaceWideGamutString = appearance === "light"
? getAlphaColorP3(accentScaleWideGamut[1], backgroundHex, 0.8)
: getAlphaColorP3(accentScaleWideGamut[1], backgroundHex, 0.5);
return {
accentScale: accentScaleHex,
accentScaleAlpha: accentScaleAlphaHex,
accentScaleWideGamut: accentScaleWideGamut,
accentScaleAlphaWideGamut: accentScaleAlphaWideGamutString,
accentContrast: accentContrastColorHex,
grayScale: grayScaleHex,
grayScaleAlpha: grayScaleAlphaHex,
grayScaleWideGamut: grayScaleWideGamut,
grayScaleAlphaWideGamut: grayScaleAlphaWideGamutString,
graySurface: appearance === "light" ? "#ffffffcc" : "rgba(0, 0, 0, 0.05)",
graySurfaceWideGamut: appearance === "light"
? "color(display-p3 1 1 1 / 80%)"
: "color(display-p3 0 0 0 / 5%)",
accentSurface: accentSurfaceHex,
accentSurfaceWideGamut: accentSurfaceWideGamutString,
background: backgroundHex,
};
};
exports.generateRadixColors = generateRadixColors;
function getStep9Colors(scale, accentBaseColor) {
var referenceBackgroundColor = scale[0];
var distance = accentBaseColor.deltaEOK(referenceBackgroundColor) * 100;
// If the accent base color is close to the page background color, it's likely
// white on white or black on black, so we want to return something that makes sense instead
if (distance < 25) {
return [scale[8], getTextColor(scale[8])];
}
return [accentBaseColor, getTextColor(accentBaseColor)];
}
function getButtonHoverColor(source, scales) {
var _a = source.coords, L = _a[0], C = _a[1], H = _a[2];
var newL = L > 0.4 ? L - 0.03 / (L + 0.1) : L + 0.03 / (L + 0.1);
var newC = L > 0.4 && !isNaN(H) ? C * 0.93 + 0 : C;
var buttonHoverColor = new colorjs_io_1.default("oklch", [newL, newC, H]);
// Find closest in-scale color to donate the chroma and hue.
// Especially useful when the source color is pure white or black,
// but the gray scale is tinted.
var closestColor = buttonHoverColor;
var minDistance = Infinity;
scales.forEach(function (scale) {
for (var _i = 0, scale_1 = scale; _i < scale_1.length; _i++) {
var color = scale_1[_i];
var distance = buttonHoverColor.deltaEOK(color);
if (distance < minDistance) {
minDistance = distance;
closestColor = color;
}
}
});
buttonHoverColor.coords[1] = closestColor.coords[1];
buttonHoverColor.coords[2] = closestColor.coords[2];
return buttonHoverColor;
}
function getScaleFromColor(source, scales, backgroundColor) {
var allColors = [];
Object.entries(scales).forEach(function (_a) {
var name = _a[0], scale = _a[1];
for (var _i = 0, scale_2 = scale; _i < scale_2.length; _i++) {
var color = scale_2[_i];
var distance = source.deltaEOK(color);
allColors.push({ scale: name, distance: distance, color: color });
}
});
allColors.sort(function (a, b) { return a.distance - b.distance; });
// Remove non-unique scales
var closestColors = allColors.filter(function (color, i, arr) {
return i === arr.findIndex(function (value) { return value.scale === color.scale; });
});
// If the next two closest colors are both grays, remove the second one until it’s not a gray anymore.
// This is because up next we will be comparing how close the two closest colors are to the source color,
// and since the grays are all extremely close to each other, we won’t get any useful data from the second
// closest color if it’s also a gray.
var grayScaleNamesStr = types_1.grayScaleNames;
var allAreGrays = closestColors.every(function (color) {
return grayScaleNamesStr.includes(color.scale);
});
if (!allAreGrays && grayScaleNamesStr.includes(closestColors[0].scale)) {
while (grayScaleNamesStr.includes(closestColors[1].scale)) {
closestColors.splice(1, 1);
}
}
var colorA = closestColors[0];
var colorB = closestColors[1];
// Light trigonometry ahead.
//
// We want to determine the color that is the closest to the source color. Sometimes it makes sense
// to proportionally mix the two closest colors together, but sometimes it is not useful at all.
// Color coords are spatial in 3D, however we can treat the data we have as a 2D projection that is good enough.
//
// Case 1:
// If the distances between the source color, the 1st closest color (A) and the 2nd closest color (B) form
// a triangle where NEITHER angle A nor B are larger than 90 degrees, then we want to mix the 1st and the 2nd
// closest colors in the same proportion as distances AD and BD are to each other. Mixing the two would result
// in a color that would be closer to the source color than either of the two original closest colors.
// Example: source color is a desaturated blue, which is between "indigo" and "slate" scales.
//
// C ← Source color
// /|⟍
// / | ⟍
// b / | ⟍ a
// / | ⟍
// / | ⟍
// A --- D -------- B
// ↑
// The color we want to use as the base, which is a mix of A and B.
//
// Case 2:
// If the distances between the source color, the 1st closest color (A) and the 2nd closest color (B) form
// a triangle where EITHER angle A or B are larger than 90 degrees, then we don’t care about point B because it’s
// directionally the same as A, as mixing A and B can’t provide us with a color that is any closer to the source.
// Example: source color is a saturated blue, with "blue" being the closest scale, and "indigo" just being further.
//
// C ← Source color
// \⟍
// \ ⟍
// \ ⟍ a
// b \ ⟍
// \ ⟍
// A ------- B
// ↑
// The color we want to use as the base, which is not influenced by B.
// We’ll need all the lengths of the triangle sides, named after the angles they look at:
var a = colorB.distance;
var b = colorA.distance;
var c = colorA.color.deltaEOK(colorB.color);
// We can get the ratios of AD to BD lengths with trigonometry using tangents,
// as the ratio of the tangents of the opposite angles will match.
var cosA = (Math.pow(b, 2) + Math.pow(c, 2) - Math.pow(a, 2)) / (2 * b * c);
var radA = Math.acos(cosA);
var sinA = Math.sin(radA);
var cosB = (Math.pow(a, 2) + Math.pow(c, 2) - Math.pow(b, 2)) / (2 * a * c);
var radB = Math.acos(cosB);
var sinB = Math.sin(radB);
// Tangent of angle C in the ACD triangle
var tanC1 = cosA / sinA;
// Tangent of angle C in the BCD triangle
var tanC2 = cosB / sinB;
// The ratio of the tangents corresponds to the ratio of the distances AD to BD
// In the end, it means how much of scale B we want to mix into scale A.
// If it’s "0" or less, this is an obtuse triangle from case 2, and we use just scale A.
var ratio = Math.max(0, tanC1 / tanC2) * 0.5;
// The base scale is going to be a mix of the two closest scales, with the mix ratio we determined before
var scaleA = scales[colorA.scale];
var scaleB = scales[colorB.scale];
var scale = arrayOf12.map(function (i) {
return new colorjs_io_1.default(colorjs_io_1.default.mix(scaleA[i], scaleB[i], ratio)).to("oklch");
});
// Get the closest color from the pre-mixed scale we created
var baseColor = scale
.slice()
.sort(function (a, b) { return source.deltaEOK(a) - source.deltaEOK(b); })[0];
// Note the chroma difference between the source color and the base color
var ratioC = source.coords[1] / baseColor.coords[1];
// Modify hue and chroma of the scale to match the source color
scale.forEach(function (color) {
color.coords[1] = Math.min(source.coords[1] * 1.5, color.coords[1] * ratioC);
color.coords[2] = source.coords[2];
});
// Light mode
if (scale[0].coords[0] > 0.5) {
var lightnessScale_1 = scale.map(function (_a) {
var coords = _a.coords;
return coords[0];
});
var backgroundL_1 = Math.max(0, Math.min(1, backgroundColor.coords[0]));
var newLightnessScale_1 = transposeProgressionStart(backgroundL_1, __spreadArray([1], lightnessScale_1, true), lightModeEasing);
// Remove the step we added
newLightnessScale_1.shift();
newLightnessScale_1.forEach(function (lightness, i) {
scale[i].coords[0] = lightness;
});
return scale;
}
// Dark mode
var ease = __spreadArray([], darkModeEasing, true);
var referenceBackgroundColorL = scale[0].coords[0];
var backgroundColorL = Math.max(0, Math.min(1, backgroundColor.coords[0]));
// If background is lighter than step 0, we want to gradually change the easing to linear
var ratioL = backgroundColorL / referenceBackgroundColorL;
if (ratioL > 1) {
var maxRatio = 1.5;
for (var i = 0; i < ease.length; i++) {
var metaRatio = (ratioL - 1) * (maxRatio / (maxRatio - 1));
ease[i] = ratioL > maxRatio ? 0 : Math.max(0, ease[i] * (1 - metaRatio));
}
}
var lightnessScale = scale.map(function (_a) {
var coords = _a.coords;
return coords[0];
});
var backgroundL = backgroundColor.coords[0];
var newLightnessScale = transposeProgressionStart(backgroundL, lightnessScale, ease);
newLightnessScale.forEach(function (lightness, i) {
scale[i].coords[0] = lightness;
});
return scale;
}
function getTextColor(background) {
var white = new colorjs_io_1.default("oklch", [1, 0, 0]);
if (Math.abs(white.contrastAPCA(background)) < 40) {
var _a = background.coords, L = _a[0], C = _a[1], H = _a[2];
return new colorjs_io_1.default("oklch", [0.25, Math.max(0.08 * C, 0.04), H]);
}
return white;
}
// target = background * (1 - alpha) + foreground * alpha
// alpha = (target - background) / (foreground - background)
// Expects 0-1 numbers for the RGB channels
function getAlphaColor(targetRgb, backgroundRgb, rgbPrecision, alphaPrecision, targetAlpha) {
var _a = targetRgb.map(function (c) { return Math.round(c * rgbPrecision); }), tr = _a[0], tg = _a[1], tb = _a[2];
var _b = backgroundRgb.map(function (c) { return Math.round(c * rgbPrecision); }), br = _b[0], bg = _b[1], bb = _b[2];
if (tr === undefined ||
tg === undefined ||
tb === undefined ||
br === undefined ||
bg === undefined ||
bb === undefined) {
throw Error("Color is undefined");
}
// Is the background color lighter, RGB-wise, than target color?
// Decide whether we want to add as little color or as much color as possible,
// darkening or lightening the background respectively.
// If at least one of the bits of the target RGB value
// is lighter than the background, we want to lighten it.
var desiredRgb = 0;
if (tr > br) {
desiredRgb = rgbPrecision;
}
else if (tg > bg) {
desiredRgb = rgbPrecision;
}
else if (tb > bb) {
desiredRgb = rgbPrecision;
}
var alphaR = (tr - br) / (desiredRgb - br);
var alphaG = (tg - bg) / (desiredRgb - bg);
var alphaB = (tb - bb) / (desiredRgb - bb);
var isPureGray = [alphaR, alphaG, alphaB].every(function (alpha) { return alpha === alphaR; });
// No need for precision gymnastics with pure grays, and we can get cleaner output
if (!targetAlpha && isPureGray) {
// Convert back to 0-1 values
var V = desiredRgb / rgbPrecision;
return [V, V, V, alphaR];
}
var clampRgb = function (n) {
return isNaN(n) ? 0 : Math.min(rgbPrecision, Math.max(0, n));
};
var clampA = function (n) {
return isNaN(n) ? 0 : Math.min(alphaPrecision, Math.max(0, n));
};
var maxAlpha = targetAlpha !== null && targetAlpha !== void 0 ? targetAlpha : Math.max(alphaR, alphaG, alphaB);
var A = clampA(Math.ceil(maxAlpha * alphaPrecision)) / alphaPrecision;
var R = clampRgb(((br * (1 - A) - tr) / A) * -1);
var G = clampRgb(((bg * (1 - A) - tg) / A) * -1);
var B = clampRgb(((bb * (1 - A) - tb) / A) * -1);
R = Math.ceil(R);
G = Math.ceil(G);
B = Math.ceil(B);
var blendedR = blendAlpha(R, A, br);
var blendedG = blendAlpha(G, A, bg);
var blendedB = blendAlpha(B, A, bb);
// Correct for rounding errors in light mode
if (desiredRgb === 0) {
if (tr <= br && tr !== blendedR) {
R = tr > blendedR ? R + 1 : R - 1;
}
if (tg <= bg && tg !== blendedG) {
G = tg > blendedG ? G + 1 : G - 1;
}
if (tb <= bb && tb !== blendedB) {
B = tb > blendedB ? B + 1 : B - 1;
}
}
// Correct for rounding errors in dark mode
if (desiredRgb === rgbPrecision) {
if (tr >= br && tr !== blendedR) {
R = tr > blendedR ? R + 1 : R - 1;
}
if (tg >= bg && tg !== blendedG) {
G = tg > blendedG ? G + 1 : G - 1;
}
if (tb >= bb && tb !== blendedB) {
B = tb > blendedB ? B + 1 : B - 1;
}
}
// Convert back to 0-1 values
R = R / rgbPrecision;
G = G / rgbPrecision;
B = B / rgbPrecision;
return [R, G, B, A];
}
// Important – I empirically discovered that this rounding is how the browser actually overlays
// transparent RGB bits over each other. It does NOT round the whole result altogether.
function blendAlpha(foreground, alpha, background, round) {
if (round === void 0) { round = true; }
if (round) {
return (Math.round(background * (1 - alpha)) + Math.round(foreground * alpha));
}
return background * (1 - alpha) + foreground * alpha;
}
function getAlphaColorSrgb(targetColor, backgroundColor, targetAlpha) {
var _a = getAlphaColor(new colorjs_io_1.default(targetColor).to("srgb").coords, new colorjs_io_1.default(backgroundColor).to("srgb").coords, 255, 255, targetAlpha), r = _a[0], g = _a[1], b = _a[2], a = _a[3];
return formatHex(new colorjs_io_1.default("srgb", [r, g, b], a).toString({ format: "hex" }));
}
function getAlphaColorP3(targetColor, backgroundColor, targetAlpha) {
var _a = getAlphaColor(new colorjs_io_1.default(targetColor).to("p3").coords, new colorjs_io_1.default(backgroundColor).to("p3").coords,
// Not sure why, but the resulting P3 alpha colors are blended in the browser most precisely when
// rounded to 255 integers too. Is the browser using 0-255 rather than 0-1 under the hood for P3 too?
255, 1000, targetAlpha), r = _a[0], g = _a[1], b = _a[2], a = _a[3];
return (new colorjs_io_1.default("p3", [r, g, b], a)
.toString({ precision: 4 })
// Important: in non-browser environments colorjs.io outputs a different format for some reason
.replace("color(p3 ", "color(display-p3 "));
}
// Format shortform hex to longform
function formatHex(str) {
if (!str.startsWith("#")) {
return str;
}
if (str.length === 4) {
var hash = str.charAt(0);
var r = str.charAt(1);
var g = str.charAt(2);
var b = str.charAt(3);
return hash + r + r + g + g + b + b;
}
if (str.length === 5) {
var hash = str.charAt(0);
var r = str.charAt(1);
var g = str.charAt(2);
var b = str.charAt(3);
var a = str.charAt(4);
return hash + r + r + g + g + b + b + a + a;
}
return str;
}
var darkModeEasing = [1, 0, 1, 0];
var lightModeEasing = [0, 2, 0, 2];
function transposeProgressionStart(to, arr, curve) {
return arr.map(function (n, i, arr) {
var lastIndex = arr.length - 1;
var diff = arr[0] - to;
var fn = bezier_easing_1.default.apply(void 0, curve);
return n - diff * fn(1 - i / lastIndex);
});
}
function transposeProgressionEnd(to, arr, curve) {
return arr.map(function (n, i, arr) {
var lastIndex = arr.length - 1;
var diff = arr[lastIndex] - to;
var fn = bezier_easing_1.default.apply(void 0, curve);
return n - diff * fn(i / lastIndex);
});
}
// Convert to OKLCH string with percentage for the lightness channel
// https://github.com/radix-ui/themes/issues/420
function toOklchString(color) {
var L = +(color.coords[0] * 100).toFixed(1);
return color
.to("oklch")
.toString({ precision: 4 })
.replace(/(\S+)(.+)/, "oklch(".concat(L, "%$2"));
}
//# sourceMappingURL=generateRadixColors.js.map