@@ -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 = {
14291433type GeneratedColor = {
14301434 name : string ;
14311435 value : string ;
1432- // valueP3: string;
1436+ valueP3 : string ;
14331437} ;
14341438
14351439function 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+
16121656const getCssVariable = ( name : string ) => getComputedStyle ( document . body ) . getPropertyValue ( name ) ;
0 commit comments