= 4.5 ) { return $hex; } [ $h, $s, $l ] = self::hsl( $hex ); $red = $h < 18.0 || $h >= 345.0; if ( $s >= 0.15 && ( $red || $h <= 70.0 ) ) { if ( ! $red ) { $h = 17.0 + ( $h - 17.0 ) * 0.2; } $s = max( $s, 0.85 ); for ( $i = 0; $i < 80 && self::contrast( self::from_hsl( $h, $s, $l ), '#ffffff' ) < 4.5; $i++ ) { $l -= 0.01; } return self::from_hsl( $h, $s, $l ); } $ink = $hex; for ( $i = 0; $i < 12 && self::contrast( $ink, '#ffffff' ) < 4.5; $i++ ) { $ink = self::darken( $ink, 0.12 ); } return $ink; } /** * A hex colour as hue (degrees), saturation and lightness (0..1). * * @param string $hex Colour. * @return array{0: float, 1: float, 2: float} */ public static function hsl( string $hex ): array { [ $r, $g, $b ] = array_map( static fn( int $c ): float => $c / 255, self::rgb( $hex ) ); $max = max( $r, $g, $b ); $min = min( $r, $g, $b ); $l = ( $max + $min ) / 2; $d = $max - $min; if ( $d <= 0.0 ) { return array( 0.0, 0.0, $l ); } $s = $d / ( 1 - abs( 2 * $l - 1 ) ); if ( $max === $r ) { $h = 60 * fmod( ( $g - $b ) / $d + 6, 6 ); } elseif ( $max === $g ) { $h = 60 * ( ( $b - $r ) / $d + 2 ); } else { $h = 60 * ( ( $r - $g ) / $d + 4 ); } return array( $h, $s, $l ); } /** * Hue (degrees), saturation and lightness (0..1) as a hex colour. * * @param float $h Hue. * @param float $s Saturation. * @param float $l Lightness. */ public static function from_hsl( float $h, float $s, float $l ): string { $l = max( 0.0, min( 1.0, $l ) ); $c = ( 1 - abs( 2 * $l - 1 ) ) * max( 0.0, min( 1.0, $s ) ); $x = $c * ( 1 - abs( fmod( $h / 60, 2 ) - 1 ) ); $m = $l - $c / 2; $k = (int) floor( fmod( $h, 360 ) / 60 ); $p = array( array( $c, $x, 0 ), array( $x, $c, 0 ), array( 0, $c, $x ), array( 0, $x, $c ), array( $x, 0, $c ), array( $c, 0, $x ) )[ max( 0, min( 5, $k ) ) ]; $out = '#'; foreach ( $p as $v ) { $out .= str_pad( dechex( (int) round( ( $v + $m ) * 255 ) ), 2, '0', STR_PAD_LEFT ); } return $out; } /** * The ink of a fill on the dark scheme (0.24.0): the fill lifted until it reads on the dark background (#09090b) * at 4.5:1; Colour::css() prints it as the dark half of each ink's light-dark() pair. * * @param string $hex Fill. */ public static function ink_dark( string $hex ): string { $ink = $hex; for ( $i = 0; $i < 12 && self::contrast( $ink, '#09090b' ) < 4.5; $i++ ) { $ink = self::tint( $ink, 0.12 ); } return $ink; } /** * A colour lifted until it reads on the dark admin menu (3.2:1 against #1d2327, the default scheme's panel): * the product's colour as the side menu shows it (the wordmark's last letters, 0.17.5). A colour that reads * already comes back as it is. * * @param string $hex Colour. */ public static function lift( string $hex ): string { $out = strtolower( $hex ); for ( $i = 0; $i < 12 && self::contrast( $out, '#1d2327' ) < 3.2; $i++ ) { $parts = array(); foreach ( self::rgb( $out ) as $c ) { $parts[] = str_pad( dechex( (int) round( $c + ( 255 - $c ) * 0.14 ) ), 2, '0', STR_PAD_LEFT ); } $out = '#' . implode( '', $parts ); } return $out; } /** * A colour moved towards another (amount 0..1 of the other): the start of a word's gradient, which carries the * red of the letter before it. * * @param string $hex Colour. * @param string $other The other colour. * @param float $amount Share of the other. */ public static function mix( string $hex, string $other, float $amount ): string { $out = '#'; $b = self::rgb( $other ); foreach ( self::rgb( $hex ) as $i => $c ) { $out .= str_pad( dechex( (int) round( $c + ( $b[ $i ] - $c ) * $amount ) ), 2, '0', STR_PAD_LEFT ); } return $out; } /** * A colour mixed with white (amount 0..1 of white): the lighter end of a word's gradient. * * @param string $hex Colour. * @param float $amount Share of white. */ public static function tint( string $hex, float $amount ): string { $out = '#'; foreach ( self::rgb( $hex ) as $c ) { $out .= str_pad( dechex( (int) round( $c + ( 255 - $c ) * $amount ) ), 2, '0', STR_PAD_LEFT ); } return $out; } /** * Black or white, whichever reads better on a fill. * * @param string $hex Fill. */ public static function on( string $hex ): string { return self::contrast( $hex, '#09090b' ) >= self::contrast( $hex, '#ffffff' ) ? '#09090b' : '#ffffff'; } /** * Whether a colour is warm (0.58.0, UI-Final-Review FR10): a red or an orange, hue under 45 or over 330 degrees, * saturated at least 15 %. On a warm section a warning or a destructive tone would no longer stand out, so its * colour stays on the 3 px accent, the icon and the focus ring and never washes a head (wash()). * * @param string $hex 6-digit hex colour. */ public static function is_warm( string $hex ): bool { [ $h, $s ] = self::hsl( $hex ); return $s >= 0.15 && ( $h < 45.0 || $h > 330.0 ); } /** * The wash of a card head in a colour (0.58.0): 7 % of the colour on the background, or the background itself for * a warm colour (FR10). A CSS value, right in both schemes (the background is a light-dark() token). * * @param string $hex 6-digit hex colour. */ public static function wash( string $hex ): string { return self::is_warm( $hex ) ? 'var(--ra-background)' : 'color-mix(in srgb,' . $hex . ' 7%,var(--ra-background))'; } /** * The ink of a fill and the text colour on it, each a light-dark() pair: the light ink reads on white, the dark * ink on the dark background (0.24.0). * * @param string $hex Fill. * @return array{0: string, 1: string} The ink pair and the on pair, as CSS values. */ public static function inks( string $hex ): array { $light = self::ink( $hex ); $dark = self::ink_dark( $hex ); return array( 'light-dark(' . $light . ',' . $dark . ')', 'light-dark(' . self::on( $light ) . ',' . self::on( $dark ) . ')' ); } /** * The screen's section on the body (0.25.0): `--ra-screen-sec`, `--ra-screen-sec-ink` and `--ra-screen-sec-on`, * for what sits outside a section's own element (a product's content panel, a dropdown a script appends to *
); '' on Home or on a screen without a section. The section's own variables (`--ra-sec*`) stay on the * elements that carry the section class, so a card or a pill of another section keeps its colour. Since 0.58.0 * `--ra-screen-wash`, the wash of a head outside a section's element: the screen's section's, else the product's. * * @param Registry $registry The registry. * @param array