= 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 $product The current product. */ private static function screen_section( Registry $registry, array $product ): string { $slug = (string) $product['slug']; $section = $registry->current_section( $slug, $registry->visible( $slug ) ); if ( '' === $section || ! isset( $product['sections'][ $section ] ) ) { return ';--ra-screen-wash:' . self::wash( (string) $product['colour'] ); } $colour = (string) $product['sections'][ $section ]['colour']; $pair = self::inks( $colour ); return ';--ra-screen-sec:' . $colour . ';--ra-screen-sec-ink:' . $pair[0] . ';--ra-screen-sec-on:' . $pair[1] . ';--ra-screen-wash:' . self::wash( $colour ); } /** * The inline CSS of every registered product and its sections and audiences: the custom properties the * stylesheet reads (--ra-product, --ra-sec, --ra-sec-ink, --ra-sec-on, --ra-persona). * * @param Registry $registry The registry. */ public static function css( Registry $registry ): string { $css = ''; $current = $registry->current_product(); foreach ( $registry->products() as $slug => $product ) { $class = '.rooster-admin-product-' . $slug; $word = array() !== $product['word']['gradient'] ? ';--ra-word-from:' . $product['word']['gradient'][0] . ';--ra-word-to:' . $product['word']['gradient'][1] : ''; $pair = self::inks( $product['colour'] ); // the on-colour against the ink, which is what the fills use (0.22.0); both schemes (0.24.0) $css .= $class . '{--ra-product:' . $product['colour'] . ';--ra-product-ink:' . $pair[0] . ';--ra-product-on:' . $pair[1] . $word . '}'; if ( $slug === $current ) { $css .= '.rooster-admin-has-chrome{--ra-product:' . $product['colour'] . ';--ra-product-ink:' . $pair[0] . ';--ra-product-on:' . $pair[1] . $word . ( '' !== $product['hero_image'] ? ';--ra-hero:url(' . esc_url( $product['hero_image'] ) . ')' : '' ) . self::screen_section( $registry, $product ) . '}'; } foreach ( $product['sections'] as $key => $section ) { $sec = self::inks( $section['colour'] ); $css .= '.rooster-admin-sec-' . $slug . '--' . $key . '{--ra-sec:' . $section['colour'] . ';--ra-sec-ink:' . $sec[0] . ';--ra-sec-on:' . $sec[1] . ';--ra-sec-wash:' . self::wash( $section['colour'] ) . ';--ra-accent:' . $sec[0] . ';--ra-accent-on:' . $sec[1] . ';--ra-shadow-focus:0 0 0 1px ' . $sec[0] . '}'; // the accent of the controls inside (0.45.0); the head's wash, none on a warm section (0.58.0) } foreach ( $product['audiences'] as $key => $persona ) { $css .= '.rooster-admin-persona-' . $slug . '--' . $key . '{--ra-persona:' . $persona['colour'] . '}'; } } return $css; } }