Skip to content
This repository was archived by the owner on Mar 15, 2024. It is now read-only.

Commit dc00ef3

Browse files
committed
Make a P3 version of every color
1 parent 1454654 commit dc00ef3

File tree

2 files changed

+139
-78
lines changed

2 files changed

+139
-78
lines changed

custom/ColorTools.tsx

Lines changed: 119 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -815,54 +815,57 @@ function EditableScale({ name, lightThemeConfig, darkThemeConfig }: EditableScal
815815
// Normal steps
816816
steps.forEach((n) => {
817817
const index = newColors.findIndex((color) => color.name === name + n);
818-
let value = config['step' + n] ?? getCssVariable(`--colors-${name}${n}`);
818+
let sRgbValue: string = config['step' + n] ?? getCssVariable(`--colors-${name}${n}`);
819+
let p3Value: string = config.p3?.['step' + n] ?? sRgbValue;
819820

820821
// Generate step 10 if it's not coming from the config
821-
if (n === '10' && config.step10 === undefined) {
822+
if (n === '10') {
822823
const baseColor = config.step9 ?? getCssVariable(`--colors-${name}9`);
823824
const mixColor = config.step11 ?? getCssVariable(`--colors-${name}11`);
824825
const mixRatio = config.mixRatioStep10 ?? defaultMixRatioStep10;
825-
value = new Color(Color.mix(baseColor, mixColor, mixRatio, { space: 'lch' }));
826+
const step10 = new Color(Color.mix(baseColor, mixColor, mixRatio, { space: 'lch' }));
827+
828+
if (config.step10 === undefined) {
829+
sRgbValue = toHex(step10);
830+
}
831+
832+
if (config.p3?.step10 === undefined) {
833+
p3Value = toP3(step10);
834+
}
826835
}
827836

828837
if (newColors[index]) {
829-
newColors[index].value = toHex(value);
838+
newColors[index].value = toHex(sRgbValue);
839+
newColors[index].valueP3 = toP3(p3Value);
830840
} else {
831841
newColors.push({
832842
name: name + n,
833-
value: toHex(value),
843+
value: toHex(sRgbValue),
844+
valueP3: toP3(p3Value),
834845
});
835846
}
836847
});
837848

838-
// P3 steps
839-
// steps.forEach((n) => {
840-
// if (config.p3?.['step' + n]) {
841-
// newColors.push({
842-
// name: name + n + '-p3',
843-
// value: config.p3['step' + n],
844-
// });
845-
// }
846-
// });
847-
848849
// Set alpha scales
849-
newColors.forEach((targetColor) => {
850+
newColors.forEach((target) => {
850851
const darkThemeBackdrop = grayBackground[name];
851852

852-
const background = isDarkTheme
853+
const backgroundValue = isDarkTheme
853854
? newColors.find((color) => color.name === darkThemeBackdrop)?.value ??
854855
getCssVariable(`--colors-${darkThemeBackdrop}`)
855856
: '#ffffff';
856857

857858
newColors.push({
858-
name: targetColor.name.replace(/(\d)/, 'A$1'),
859-
value: getAlphaColor(targetColor.value, background),
859+
name: target.name.replace(/(\d)/, 'A$1'),
860+
value: getAlphaColorSrgb(target.value, backgroundValue),
861+
valueP3: getAlphaColorP3(target.value, backgroundValue),
860862
});
861863
});
862864

863865
// Set CSS variables
864866
newColors.forEach((color) => {
865867
document.body.style.setProperty(`--colors-${color.name}`, color.value);
868+
document.body.style.setProperty(`--colors-${color.name}-p3`, color.valueP3);
866869
});
867870

868871
setActive(true);
@@ -880,16 +883,18 @@ function EditableScale({ name, lightThemeConfig, darkThemeConfig }: EditableScal
880883
// Set new CSS variables when active or theme is changed
881884
React.useEffect(() => {
882885
// Deactivate and/or clear potentially stale stuff
883-
steps.forEach((step) => {
884-
document.body.style.removeProperty(`--colors-${name}${step}`);
885-
document.body.style.removeProperty(`--colors-${name}A${step}`);
886-
// document.body.style.removeProperty(`--colors-${name}${step}-p3`);
886+
steps.forEach((n) => {
887+
document.body.style.removeProperty(`--colors-${name}${n}`);
888+
document.body.style.removeProperty(`--colors-${name}A${n}`);
889+
document.body.style.removeProperty(`--colors-${name}${n}-p3`);
890+
document.body.style.removeProperty(`--colors-${name}A${n}-p3`);
887891
});
888892

889893
// Set relevant values if active
890894
if (active) {
891895
generatedColorsRef.current.forEach((color) => {
892896
document.body.style.setProperty(`--colors-${color.name}`, color.value);
897+
document.body.style.setProperty(`--colors-${color.name}-p3`, color.valueP3);
893898
});
894899
}
895900
}, [active, isDarkTheme]);
@@ -1060,12 +1065,12 @@ function EditableScale({ name, lightThemeConfig, darkThemeConfig }: EditableScal
10601065
? `export const ${color}Dark = {\n`
10611066
: `export const ${color} = {\n`;
10621067

1063-
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].forEach((step) => {
1064-
let value = computedStyle.getPropertyValue(`--colors-${color}${step}`);
1068+
steps.forEach((n) => {
1069+
let value = computedStyle.getPropertyValue(`--colors-${color}${n}`);
10651070

10661071
if (value) {
10671072
value = toHex(value);
1068-
clipboard += ` ${color}${step}: '${value}',\n`;
1073+
clipboard += ` ${color}${n}: '${value}',\n`;
10691074
}
10701075
});
10711076

@@ -1074,12 +1079,12 @@ function EditableScale({ name, lightThemeConfig, darkThemeConfig }: EditableScal
10741079
? `export const ${color}DarkA = {\n`
10751080
: `export const ${color}A = {\n`;
10761081

1077-
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].forEach((step) => {
1078-
let value = computedStyle.getPropertyValue(`--colors-${color}A${step}`);
1082+
steps.forEach((n) => {
1083+
let value = computedStyle.getPropertyValue(`--colors-${color}A${n}`);
10791084

10801085
if (value) {
10811086
value = toHex(value);
1082-
clipboard += ` ${color}A${step}: '${value}',\n`;
1087+
clipboard += ` ${color}A${n}: '${value}',\n`;
10831088
}
10841089
});
10851090

@@ -1088,11 +1093,11 @@ function EditableScale({ name, lightThemeConfig, darkThemeConfig }: EditableScal
10881093
? `export const ${color}DarkP3 = {\n`
10891094
: `export const ${color}P3 = {\n`;
10901095

1091-
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].forEach((step) => {
1092-
let value = computedStyle.getPropertyValue(`--colors-${color}${step}-p3`);
1096+
steps.forEach((n) => {
1097+
let value = computedStyle.getPropertyValue(`--colors-${color}${n}-p3`);
10931098

10941099
if (value) {
1095-
clipboard += ` ${color}${step}: '${value}',\n`;
1100+
clipboard += ` ${color}${n}: '${value}',\n`;
10961101
}
10971102
});
10981103

@@ -1174,16 +1179,16 @@ function EditableScale({ name, lightThemeConfig, darkThemeConfig }: EditableScal
11741179
pointerEvents: showCode ? 'auto' : 'none',
11751180
}}
11761181
>
1177-
{steps.map((step) => {
1182+
{steps.map((n) => {
11781183
const variableName = showAlphaValues
1179-
? `--colors-${name}A${step}`
1180-
: `--colors-${name}${step}`;
1184+
? `--colors-${name}A${n}`
1185+
: `--colors-${name}${n}`;
11811186
const valueToShow = getCssVariable(variableName);
11821187
const nameToShow = showAlphaValues ? `${name}A` : name;
11831188

11841189
return (
11851190
<Text
1186-
key={step}
1191+
key={n}
11871192
css={{
11881193
fontSize: '10px',
11891194
fontFamily: '$mono',
@@ -1193,8 +1198,7 @@ function EditableScale({ name, lightThemeConfig, darkThemeConfig }: EditableScal
11931198
lineHeight: '25px',
11941199
}}
11951200
>
1196-
{nameToShow}
1197-
{step}: '{toHex(valueToShow)}',
1201+
{nameToShow + n}: '{valueToShow}',
11981202
</Text>
11991203
);
12001204
})}
@@ -1429,7 +1433,7 @@ type ScaleSpec = {
14291433
type GeneratedColor = {
14301434
name: string;
14311435
value: string;
1432-
// valueP3: string;
1436+
valueP3: string;
14331437
};
14341438

14351439
function generateColors({
@@ -1506,6 +1510,7 @@ function generateColors({
15061510
colorMap.push({
15071511
name: `${name}${index + indexOffset}`,
15081512
value: toHex(color),
1513+
valueP3: toP3(color),
15091514
});
15101515
}
15111516

@@ -1514,19 +1519,22 @@ function generateColors({
15141519

15151520
// target = background * (1 - alpha) + foreground * alpha
15161521
// alpha = (target - background) / (foreground - background)
1517-
function getAlphaColor(targetColor: string, backgroundColor: string, debugColorName?: string) {
1518-
const [targetR, targetG, targetB] = new Color(targetColor).srgb.map((c) => Math.round(c * 255));
1519-
const [backgroundR, backgroundG, backgroundB] = new Color(backgroundColor).srgb.map((c) =>
1520-
Math.round(c * 255)
1521-
);
1522+
// Expects 0-1 numbers for the RGB channels
1523+
function getAlphaColor(
1524+
targetRgb: number[],
1525+
backgroundRgb: number[],
1526+
rgbPrecision: number,
1527+
alphaPrecision: number
1528+
) {
1529+
const [tr, tg, tb] = targetRgb.map((c) => Math.round(c * rgbPrecision));
1530+
const [br, bg, bb] = backgroundRgb.map((c) => Math.round(c * rgbPrecision));
15221531

15231532
// Is the background color lighter, RGB-wise, than target color?
15241533
// Decide whether we want to add as little color or as much color as possible,
15251534
// darkening or lightening the background respectively.
15261535
// If at least one of the bits of the target RGB value
15271536
// is lighter than the background, we want to lighten it.
1528-
let desiredRGB =
1529-
targetR > backgroundR ? 255 : targetG > backgroundG ? 255 : targetB > backgroundB ? 255 : 0;
1537+
let desiredRgb = tr > br ? rgbPrecision : tg > bg ? rgbPrecision : tb > bb ? rgbPrecision : 0;
15301538

15311539
// Light theme example:
15321540
// Consider a 200 120 150 target color with 255 255 255 background
@@ -1535,59 +1543,87 @@ function getAlphaColor(targetColor: string, backgroundColor: string, debugColorN
15351543
// Dark theme example:
15361544
// Consider a 200 120 150 target color with 12 24 28 background
15371545
// What is the alpha value that will nudge background's 12 red to 200?
1538-
const alphaR = (targetR - backgroundR) / (desiredRGB - backgroundR);
1539-
const alphaG = (targetG - backgroundG) / (desiredRGB - backgroundG);
1540-
const alphaB = (targetB - backgroundB) / (desiredRGB - backgroundB);
1546+
const alphaR = (tr - br) / (desiredRgb - br);
1547+
const alphaG = (tg - bg) / (desiredRgb - bg);
1548+
const alphaB = (tb - bb) / (desiredRgb - bb);
15411549

1542-
const clamp = (n: number) => (isNaN(n) ? 0 : Math.min(255, Math.max(0, n)));
1550+
const clampRgb = (n: number) => (isNaN(n) ? 0 : Math.min(rgbPrecision, Math.max(0, n)));
1551+
const clampA = (n: number) => (isNaN(n) ? 0 : Math.min(alphaPrecision, Math.max(0, n)));
15431552

1544-
// Round alpha in 1/255 increments as it’s going to be converted to hex after
1545-
const A = clamp(Math.ceil(Math.max(alphaR, alphaG, alphaB) * 255)) / 255;
1546-
1547-
let R = clamp(((backgroundR * (1 - A) - targetR) / A) * -1);
1548-
let G = clamp(((backgroundG * (1 - A) - targetG) / A) * -1);
1549-
let B = clamp(((backgroundB * (1 - A) - targetB) / A) * -1);
1553+
let A = clampA(Math.ceil(Math.max(alphaR, alphaG, alphaB) * alphaPrecision)) / alphaPrecision;
1554+
let R = clampRgb(((br * (1 - A) - tr) / A) * -1);
1555+
let G = clampRgb(((bg * (1 - A) - tg) / A) * -1);
1556+
let B = clampRgb(((bb * (1 - A) - tb) / A) * -1);
15501557

15511558
R = Math.ceil(R);
15521559
G = Math.ceil(G);
15531560
B = Math.ceil(B);
15541561

1555-
const overlayR = overlayRgbBits(R, A, backgroundR);
1556-
const overlayG = overlayRgbBits(G, A, backgroundG);
1557-
const overlayB = overlayRgbBits(B, A, backgroundB);
1562+
const overlayR = overlayAlphaInSingleChannel(R, A, br);
1563+
const overlayG = overlayAlphaInSingleChannel(G, A, bg);
1564+
const overlayB = overlayAlphaInSingleChannel(B, A, bb);
15581565

15591566
// Correct for rounding errors in light mode
1560-
if (desiredRGB === 0) {
1561-
if (targetR <= backgroundR && targetR !== overlayR) {
1562-
R = targetR > overlayR ? R + 1 : R - 1;
1567+
if (desiredRgb === 0) {
1568+
if (tr <= br && tr !== overlayR) {
1569+
R = tr > overlayR ? R + 1 : R - 1;
15631570
}
1564-
if (targetG <= backgroundG && targetG !== overlayG) {
1565-
G = targetG > overlayG ? G + 1 : G - 1;
1571+
if (tg <= bg && tg !== overlayG) {
1572+
G = tg > overlayG ? G + 1 : G - 1;
15661573
}
1567-
if (targetB <= backgroundB && targetB !== overlayB) {
1568-
B = targetB > overlayB ? B + 1 : B - 1;
1574+
if (tb <= bb && tb !== overlayB) {
1575+
B = tb > overlayB ? B + 1 : B - 1;
15691576
}
15701577
}
15711578

15721579
// Correct for rounding errors in dark mode
1573-
if (desiredRGB === 255) {
1574-
if (targetR >= backgroundR && targetR !== overlayR) {
1575-
R = targetR > overlayR ? R + 1 : R - 1;
1580+
if (desiredRgb === rgbPrecision) {
1581+
if (tr >= br && tr !== overlayR) {
1582+
R = tr > overlayR ? R + 1 : R - 1;
15761583
}
1577-
if (targetG >= backgroundG && targetG !== overlayG) {
1578-
G = targetG > overlayG ? G + 1 : G - 1;
1584+
if (tg >= bg && tg !== overlayG) {
1585+
G = tg > overlayG ? G + 1 : G - 1;
15791586
}
1580-
if (targetB >= backgroundB && targetB !== overlayB) {
1581-
B = targetB > overlayB ? B + 1 : B - 1;
1587+
if (tb >= bb && tb !== overlayB) {
1588+
B = tb > overlayB ? B + 1 : B - 1;
15821589
}
15831590
}
15841591

1585-
return toHex(`rgb(${R} ${G} ${B} / ${A})`);
1592+
// Convert back to 0-1 values
1593+
R = R / rgbPrecision;
1594+
G = G / rgbPrecision;
1595+
B = B / rgbPrecision;
1596+
1597+
return [R, G, B, A] as const;
1598+
}
1599+
1600+
function getAlphaColorSrgb(targetColor: string, backgroundColor: string) {
1601+
const [r, g, b, a] = getAlphaColor(
1602+
new Color(targetColor).to('srgb').coords,
1603+
new Color(backgroundColor).to('srgb').coords,
1604+
255,
1605+
255
1606+
);
1607+
1608+
return new Color('srgb', [r, g, b], a).toString({ format: 'hex' });
1609+
}
1610+
1611+
function getAlphaColorP3(targetColor: string, backgroundColor: string) {
1612+
const [r, g, b, a] = getAlphaColor(
1613+
new Color(targetColor).to('p3').coords,
1614+
new Color(backgroundColor).to('p3').coords,
1615+
// Not sure why, but the resulting P3 alpha colors are blended in the browser most precisely when
1616+
// rounded to 255 integers too. Is the browser using 0-255 rather than 0-1 under the hood for P3 too?
1617+
255,
1618+
1000
1619+
);
1620+
1621+
return new Color('p3', [r, g, b], a).toString();
15861622
}
15871623

15881624
// Important – I empirically discovered that this rounding is how the browser actually overlays
15891625
// transparent RGB bits over each other. It does NOT round the whole result altogether.
1590-
function overlayRgbBits(foreground: number, alpha: number, background: number) {
1626+
function overlayAlphaInSingleChannel(foreground: number, alpha: number, background: number) {
15911627
return Math.round(background * (1 - alpha)) + Math.round(foreground * alpha);
15921628
}
15931629

@@ -1609,4 +1645,12 @@ function toHex(color: Color | string) {
16091645
return new Color(color).to('srgb').toString({ format: 'hex' });
16101646
}
16111647

1648+
function toP3(color: Color | string) {
1649+
if (color instanceof Color) {
1650+
return color.to('p3').toString();
1651+
}
1652+
1653+
return new Color(color).to('p3').toString();
1654+
}
1655+
16121656
const getCssVariable = (name: string) => getComputedStyle(document.body).getPropertyValue(name);

pages/colors.tsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,9 +192,20 @@ export default function Colors() {
192192
colors.forEach((color) =>
193193
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].forEach((step) => {
194194
const valueP3 = computedStyle.getPropertyValue(`--colors-${color}${step}-p3`);
195+
const valueAlphaP3 = computedStyle.getPropertyValue(`--colors-${color}A${step}-p3`);
196+
195197
if (valueP3) {
196-
nextElement.style.setProperty(`--colors-${color}${step}`, valueP3);
197-
nextElement.style.setProperty(`--colors-${color}A${step}`, valueP3);
198+
nextElement.style.setProperty(
199+
`--colors-${color}${step}`,
200+
`var(--colors-${color}${step}-p3)`
201+
);
202+
}
203+
204+
if (valueAlphaP3) {
205+
nextElement.style.setProperty(
206+
`--colors-${color}A${step}`,
207+
`var(--colors-${color}A${step}-p3)`
208+
);
198209
}
199210
})
200211
);
@@ -1546,7 +1557,13 @@ function Palette({ showAlphaScales = false }: { showAlphaScales: boolean }) {
15461557
/>
15471558
))}
15481559

1549-
{showAlphaScales && <Box />}
1560+
{showAlphaScales && (
1561+
<Box css={{ alignSelf: 'center' }}>
1562+
<Text size="2" style={{ textTransform: 'capitalize' }}>
1563+
{color} A.
1564+
</Text>
1565+
</Box>
1566+
)}
15501567

15511568
{showAlphaScales &&
15521569
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map((i) => (

0 commit comments

Comments
 (0)