Created
June 12, 2024 17:15
-
-
Save arthurgeron/103b4c255fdcf67d44c73034b209bc1c to your computer and use it in GitHub Desktop.
Uses Radix Themes V3 logic to generate alpha, display p3 and display p3 colors from a solid color and background color input
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Color from 'colorjs.io'; | |
function getAlphaColorSrgb(targetColor: string, backgroundColor: string, targetAlpha?: number) { | |
const [r, g, b, a] = getAlphaColor( | |
new Color(targetColor).to('srgb').coords, | |
new Color(backgroundColor).to('srgb').coords, | |
255, | |
255, | |
targetAlpha | |
); | |
return formatHex(new Color('srgb', [r, g, b], a).toString({ format: 'hex' })); | |
} | |
function getAlphaColorP3(targetColor: string, backgroundColor: string, targetAlpha?: number) { | |
const [r, g, b, a] = getAlphaColor( | |
new Color(targetColor).to('p3').coords, | |
new Color(backgroundColor).to('p3').coords, | |
255, | |
1000, | |
targetAlpha | |
); | |
return ( | |
new Color('p3', [r, g, b], a) | |
.toString({ precision: 4 }) | |
.replace('color(p3 ', 'color(display-p3 ') | |
); | |
} | |
function formatHex(str: string) { | |
if (!str.startsWith('#')) { | |
return str; | |
} | |
if (str.length === 4) { | |
const hash = str.charAt(0); | |
const r = str.charAt(1); | |
const g = str.charAt(2); | |
const b = str.charAt(3); | |
return hash + r + r + g + g + b + b; | |
} | |
if (str.length === 5) { | |
const hash = str.charAt(0); | |
const r = str.charAt(1); | |
const g = str.charAt(2); | |
const b = str.charAt(3); | |
const a = str.charAt(4); | |
return hash + r + r + g + g + b + b + a + a; | |
} | |
return str; | |
} | |
function getAlphaColor( | |
targetRgb: number[], | |
backgroundRgb: number[], | |
rgbPrecision: number, | |
alphaPrecision: number, | |
targetAlpha?: number | |
) { | |
const [tr, tg, tb] = targetRgb.map((c) => Math.round(c * rgbPrecision)); | |
const [br, bg, bb] = backgroundRgb.map((c) => Math.round(c * rgbPrecision)); | |
if ( | |
tr === undefined || | |
tg === undefined || | |
tb === undefined || | |
br === undefined || | |
bg === undefined || | |
bb === undefined | |
) { | |
throw Error('Color is undefined'); | |
} | |
let desiredRgb = 0; | |
if (tr > br) { | |
desiredRgb = rgbPrecision; | |
} else if (tg > bg) { | |
desiredRgb = rgbPrecision; | |
} else if (tb > bb) { | |
desiredRgb = rgbPrecision; | |
} | |
const alphaR = (tr - br) / (desiredRgb - br); | |
const alphaG = (tg - bg) / (desiredRgb - bg); | |
const alphaB = (tb - bb) / (desiredRgb - bb); | |
const isPureGray = [alphaR, alphaG, alphaB].every((alpha) => alpha === alphaR); | |
if (!targetAlpha && isPureGray) { | |
const V = desiredRgb / rgbPrecision; | |
return [V, V, V, alphaR] as const; | |
} | |
const clampRgb = (n: number) => (isNaN(n) ? 0 : Math.min(rgbPrecision, Math.max(0, n))); | |
const clampA = (n: number) => (isNaN(n) ? 0 : Math.min(alphaPrecision, Math.max(0, n))); | |
const maxAlpha = targetAlpha ?? Math.max(alphaR, alphaG, alphaB); | |
const A = clampA(Math.ceil(maxAlpha * alphaPrecision)) / alphaPrecision; | |
let R = clampRgb(((br * (1 - A) - tr) / A) * -1); | |
let G = clampRgb(((bg * (1 - A) - tg) / A) * -1); | |
let B = clampRgb(((bb * (1 - A) - tb) / A) * -1); | |
R = Math.ceil(R); | |
G = Math.ceil(G); | |
B = Math.ceil(B); | |
const blendedR = blendAlpha(R, A, br); | |
const blendedG = blendAlpha(G, A, bg); | |
const blendedB = blendAlpha(B, A, bb); | |
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; | |
} | |
} | |
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; | |
} | |
} | |
R = R / rgbPrecision; | |
G = G / rgbPrecision; | |
B = B / rgbPrecision; | |
return [R, G, B, A] as const; | |
} | |
function blendAlpha(foreground: number, alpha: number, background: number, round = true) { | |
if (round) { | |
return Math.round(background * (1 - alpha)) + Math.round(foreground * alpha); | |
} | |
return background * (1 - alpha) + foreground * alpha; | |
} | |
export function generateSingleColorStep({ | |
color, | |
background, | |
}: { | |
color: string; | |
background: string; | |
}) { | |
const colorObj = new Color(color).to('oklch'); | |
const backgroundColor = new Color(background).to('oklch'); | |
const colorHex = colorObj.to('srgb').toString({ format: 'hex' }); | |
const colorWideGamut = colorObj.toString({ format: 'oklch', precision: 4 }); | |
const alphaColorHex = getAlphaColorSrgb(color, background); | |
const alphaColorWideGamut = getAlphaColorP3(color, background); | |
return { | |
colorHex, | |
colorWideGamut, | |
alphaColorHex, | |
alphaColorWideGamut, | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is useful if you want to create your own custom palette for your Radix Theme, you can only worry about replacing the solid color tokens (e.g. --gray-1, --gray-2, etc) and use this utility to generate the other variations for that given step (Alpha, Display P3 and Display P3 Alpha).
Playground
Example:
