From d99c1814ffb1b8809ce008d1d84e60ef736a0255 Mon Sep 17 00:00:00 2001 From: Kevin Zoschke Date: Tue, 16 Jul 2024 16:56:03 -0400 Subject: [PATCH] Add Jetpack_Color to classic-theme-helper package --- .../_inc/lib/class.color.php | 902 ++++++++++++++++++ .../_inc/lib/tonesque.php | 444 ++++----- ...move-jetpack-color-to-classic-theme-helper | 4 + .../classic-theme-helper/src/class-main.php | 3 +- .../src/jetpack-color.php | 8 + 5 files changed, 1139 insertions(+), 222 deletions(-) create mode 100644 projects/packages/classic-theme-helper/_inc/lib/class.color.php create mode 100644 projects/packages/classic-theme-helper/changelog/update-move-jetpack-color-to-classic-theme-helper create mode 100644 projects/packages/classic-theme-helper/src/jetpack-color.php diff --git a/projects/packages/classic-theme-helper/_inc/lib/class.color.php b/projects/packages/classic-theme-helper/_inc/lib/class.color.php new file mode 100644 index 0000000000000..4d1d0941db2b1 --- /dev/null +++ b/projects/packages/classic-theme-helper/_inc/lib/class.color.php @@ -0,0 +1,902 @@ + + * @author Matt Wiebe + * @license https://www.opensource.org/licenses/MIT + * + * @package automattic/jetpack + */ + +// phpcs:disable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid + +if ( ! class_exists( 'Jetpack_Color' ) ) { + /** + * Color utilities + */ + class Jetpack_Color { + /** + * Color code (later array or string, depending on type) + * + * @var int|array|string + */ + protected $color = 0; + + /** + * Initialize object + * + * @param string|array $color A color of the type $type. + * @param string $type The type of color we will construct from. + * One of hex (default), rgb, hsl, int. + */ + public function __construct( $color = null, $type = 'hex' ) { + if ( $color ) { + switch ( $type ) { + case 'hex': + $this->fromHex( $color ); + break; + case 'rgb': + if ( is_array( $color ) && count( $color ) === 3 ) { + list( $r, $g, $b ) = array_values( $color ); + $this->fromRgbInt( $r, $g, $b ); + } + break; + case 'hsl': + if ( is_array( $color ) && count( $color ) === 3 ) { + list( $h, $s, $l ) = array_values( $color ); + $this->fromHsl( $h, $s, $l ); + } + break; + case 'int': + $this->fromInt( $color ); + break; + default: + // there is no default. + break; + } + } + } + + /** + * Init color from hex value + * + * @param string $hex_value Color hex value. + * + * @return $this + * @throws RangeException Invalid color code range error. + */ + public function fromHex( $hex_value ) { + $hex_value = str_replace( '#', '', $hex_value ); + // handle short hex codes like #fff. + if ( 3 === strlen( $hex_value ) ) { + $hex_value = $hex_value[0] . $hex_value[0] . $hex_value[1] . $hex_value[1] . $hex_value[2] . $hex_value[2]; + } + return $this->fromInt( hexdec( $hex_value ) ); + } + + /** + * Init color from integer RGB values + * + * @param int $red Red color code. + * @param int $green Green color code. + * @param int $blue Blue color code. + * + * @return $this + * @throws RangeException Invalid color code range error. + */ + public function fromRgbInt( $red, $green, $blue ) { + if ( $red < 0 || $red > 255 ) { + throw new RangeException( 'Red value ' . $red . ' out of valid color code range' ); + } + + if ( $green < 0 || $green > 255 ) { + throw new RangeException( 'Green value ' . $green . ' out of valid color code range' ); + } + + if ( $blue < 0 || $blue > 255 ) { + throw new RangeException( 'Blue value ' . $blue . ' out of valid color code range' ); + } + + $this->color = ( intval( $red ) << 16 ) + ( intval( $green ) << 8 ) + intval( $blue ); + + return $this; + } + + /** + * Init color from hex RGB values + * + * @param string $red Red color code. + * @param string $green Green color code. + * @param string $blue Blue color code. + * + * @return $this + */ + public function fromRgbHex( $red, $green, $blue ) { + return $this->fromRgbInt( hexdec( $red ), hexdec( $green ), hexdec( $blue ) ); + } + + /** + * Converts an HSL color value to RGB. Conversion formula + * adapted from https://en.wikipedia.org/wiki/HSL_color_space. + * + * @param int $h Hue. [0-360]. + * @param int $s Saturation [0, 100]. + * @param int $l Lightness [0, 100]. + */ + public function fromHsl( $h, $s, $l ) { + $h /= 360; + $s /= 100; + $l /= 100; + + if ( 0 === $s ) { + // achromatic. + $r = $l; + $g = $l; + $b = $l; + } else { + $q = $l < 0.5 ? $l * ( 1 + $s ) : $l + $s - $l * $s; + $p = 2 * $l - $q; + $r = $this->hue2rgb( $p, $q, $h + 1 / 3 ); + $g = $this->hue2rgb( $p, $q, $h ); + $b = $this->hue2rgb( $p, $q, $h - 1 / 3 ); + } + + return $this->fromRgbInt( $r * 255, $g * 255, $b * 255 ); + } + + /** + * Helper function for Jetpack_Color::fromHsl() + * + * @param float $p Minimum of R/G/B [0, 1]. + * @param float $q Maximum of R/G/B [0, 1]. + * @param float $t Adjusted hue [0, 1]. + */ + private function hue2rgb( $p, $q, $t ) { + if ( $t < 0 ) { + ++$t; + } + if ( $t > 1 ) { + --$t; + } + if ( $t < 1 / 6 ) { + return $p + ( $q - $p ) * 6 * $t; + } + if ( $t < 1 / 2 ) { + return $q; + } + if ( $t < 2 / 3 ) { + return $p + ( $q - $p ) * ( 2 / 3 - $t ) * 6; + } + return $p; + } + + /** + * Init color from integer value + * + * @param int $int_value Color code. + * + * @return $this + * @throws RangeException Invalid color code range error. + */ + public function fromInt( $int_value ) { + if ( $int_value < 0 || $int_value > 16777215 ) { + throw new RangeException( $int_value . ' out of valid color code range' ); + } + + $this->color = $int_value; + + return $this; + } + + /** + * Convert color to hex + * + * @return string + */ + public function toHex() { + return sprintf( '%06x', $this->color ); + } + + /** + * Convert color to RGB array (integer values) + * + * @return array + */ + public function toRgbInt() { + return array( + 'red' => (int) ( 255 & ( $this->color >> 16 ) ), + 'green' => (int) ( 255 & ( $this->color >> 8 ) ), + 'blue' => (int) ( 255 & ( $this->color ) ), + ); + } + + /** + * Convert color to RGB array (hex values) + * + * @return array + */ + public function toRgbHex() { + $r = array(); + foreach ( $this->toRgbInt() as $item ) { + $r[] = dechex( $item ); + } + return $r; + } + + /** + * Get Hue/Saturation/Value for the current color + * (float values, slow but accurate) + * + * @return array + */ + public function toHsvFloat() { + $rgb = $this->toRgbInt(); + + $rgb_min = min( $rgb ); + $rgb_max = max( $rgb ); + + $hsv = array( + 'hue' => 0, + 'sat' => 0, + 'val' => $rgb_max, + ); + + // If v is 0, color is black. + if ( 0 === $hsv['val'] ) { + return $hsv; + } + + // Normalize RGB values to 1. + $rgb['red'] /= $hsv['val']; + $rgb['green'] /= $hsv['val']; + $rgb['blue'] /= $hsv['val']; + $rgb_min = min( $rgb ); + $rgb_max = max( $rgb ); + + // Calculate saturation. + $hsv['sat'] = $rgb_max - $rgb_min; + if ( 0 === $hsv['sat'] ) { + $hsv['hue'] = 0; + return $hsv; + } + + // Normalize saturation to 1. + $rgb['red'] = ( $rgb['red'] - $rgb_min ) / ( $rgb_max - $rgb_min ); + $rgb['green'] = ( $rgb['green'] - $rgb_min ) / ( $rgb_max - $rgb_min ); + $rgb['blue'] = ( $rgb['blue'] - $rgb_min ) / ( $rgb_max - $rgb_min ); + $rgb_min = min( $rgb ); + $rgb_max = max( $rgb ); + + // Calculate hue. + if ( $rgb_max === $rgb['red'] ) { + $hsv['hue'] = 0.0 + 60 * ( $rgb['green'] - $rgb['blue'] ); + if ( $hsv['hue'] < 0 ) { + $hsv['hue'] += 360; + } + } elseif ( $rgb_max === $rgb['green'] ) { + $hsv['hue'] = 120 + ( 60 * ( $rgb['blue'] - $rgb['red'] ) ); + } else { + $hsv['hue'] = 240 + ( 60 * ( $rgb['red'] - $rgb['green'] ) ); + } + + return $hsv; + } + + /** + * Get HSV values for color + * (integer values from 0-255, fast but less accurate) + * + * @return array + */ + public function toHsvInt() { + $rgb = $this->toRgbInt(); + + $rgb_min = min( $rgb ); + $rgb_max = max( $rgb ); + + $hsv = array( + 'hue' => 0, + 'sat' => 0, + 'val' => $rgb_max, + ); + + // If value is 0, color is black. + if ( 0 === $hsv['val'] ) { + return $hsv; + } + + // Calculate saturation. + $hsv['sat'] = round( 255 * ( $rgb_max - $rgb_min ) / $hsv['val'] ); + if ( 0 === $hsv['sat'] ) { + $hsv['hue'] = 0; + return $hsv; + } + + // Calculate hue. + if ( $rgb_max === $rgb['red'] ) { + $hsv['hue'] = round( 0 + 43 * ( $rgb['green'] - $rgb['blue'] ) / ( $rgb_max - $rgb_min ) ); + } elseif ( $rgb_max === $rgb['green'] ) { + $hsv['hue'] = round( 85 + 43 * ( $rgb['blue'] - $rgb['red'] ) / ( $rgb_max - $rgb_min ) ); + } else { + $hsv['hue'] = round( 171 + 43 * ( $rgb['red'] - $rgb['green'] ) / ( $rgb_max - $rgb_min ) ); + } + if ( $hsv['hue'] < 0 ) { + $hsv['hue'] += 255; + } + + return $hsv; + } + + /** + * Converts an RGB color value to HSL. Conversion formula + * adapted from https://en.wikipedia.org/wiki/HSL_color_space. + * Assumes r, g, and b are contained in the set [0, 255] and + * returns h in [0, 360], s in [0, 100], l in [0, 100] + * + * @return Array The HSL representation + */ + public function toHsl() { + list( $r, $g, $b ) = array_values( $this->toRgbInt() ); + $r /= 255; + $g /= 255; + $b /= 255; + $max = max( $r, $g, $b ); + $min = min( $r, $g, $b ); + $l = ( $max + $min ) / 2; + + if ( $max === $min ) { + // achromatic. + $s = 0; + $h = 0; + } else { + $d = $max - $min; + $s = $l > 0.5 ? $d / ( 2 - $max - $min ) : $d / ( $max + $min ); + switch ( $max ) { + case $r: + $h = ( $g - $b ) / $d + ( $g < $b ? 6 : 0 ); + break; + case $g: + $h = ( $b - $r ) / $d + 2; + break; + case $b: + $h = ( $r - $g ) / $d + 4; + break; + } + $h /= 6; + } + $h = (int) round( $h * 360 ); + $s = (int) round( $s * 100 ); + $l = (int) round( $l * 100 ); + return compact( 'h', 's', 'l' ); + } + + /** + * From a color code to a string to be used in CSS declaration. + * + * @param string $type Color code type. + * @param int $alpha Transparency. + * + * @return string + */ + public function toCSS( $type = 'hex', $alpha = 1 ) { + switch ( $type ) { + case 'hex': + return $this->toString(); + case 'rgb': + case 'rgba': + list( $r, $g, $b ) = array_values( $this->toRgbInt() ); + if ( is_numeric( $alpha ) && $alpha < 1 ) { + return "rgba( {$r}, {$g}, {$b}, $alpha )"; + } else { + return "rgb( {$r}, {$g}, {$b} )"; + } + case 'hsl': + case 'hsla': + list( $h, $s, $l ) = array_values( $this->toHsl() ); + if ( is_numeric( $alpha ) && $alpha < 1 ) { + return "hsla( {$h}, {$s}, {$l}, $alpha )"; + } else { + return "hsl( {$h}, {$s}, {$l} )"; + } + default: + return $this->toString(); + } + } + + /** + * Get current color in XYZ format + * + * @return array + */ + public function toXyz() { + $rgb = $this->toRgbInt(); + + // Normalize RGB values to 1. + $rgb_new = array(); + foreach ( $rgb as $item ) { + $rgb_new[] = $item / 255; + } + $rgb = $rgb_new; + + $rgb_new = array(); + foreach ( $rgb as $item ) { + if ( $item > 0.04045 ) { + $item = pow( ( ( $item + 0.055 ) / 1.055 ), 2.4 ); + } else { + $item = $item / 12.92; + } + $rgb_new[] = $item * 100; + } + $rgb = $rgb_new; + + // Observer. = 2°, Illuminant = D65. + $xyz = array( + 'x' => ( $rgb['red'] * 0.4124 ) + ( $rgb['green'] * 0.3576 ) + ( $rgb['blue'] * 0.1805 ), + 'y' => ( $rgb['red'] * 0.2126 ) + ( $rgb['green'] * 0.7152 ) + ( $rgb['blue'] * 0.0722 ), + 'z' => ( $rgb['red'] * 0.0193 ) + ( $rgb['green'] * 0.1192 ) + ( $rgb['blue'] * 0.9505 ), + ); + + return $xyz; + } + + /** + * Get color CIE-Lab values + * + * @return array + */ + public function toLabCie() { + $xyz = $this->toXyz(); + + // Ovserver = 2*, Iluminant=D65. + $xyz['x'] /= 95.047; + $xyz['y'] /= 100; + $xyz['z'] /= 108.883; + + $xyz_new = array(); + foreach ( $xyz as $item ) { + if ( $item > 0.008856 ) { + $xyz_new[] = pow( $item, 1 / 3 ); + } else { + $xyz_new[] = ( 7.787 * $item ) + ( 16 / 116 ); + } + } + $xyz = $xyz_new; + + $lab = array( + 'l' => ( 116 * $xyz['y'] ) - 16, + 'a' => 500 * ( $xyz['x'] - $xyz['y'] ), + 'b' => 200 * ( $xyz['y'] - $xyz['z'] ), + ); + + return $lab; + } + + /** + * Convert color to integer + * + * @return int + */ + public function toInt() { + return $this->color; + } + + /** + * Alias of toString() + * + * @return string + */ + public function __toString() { + return $this->toString(); + } + + /** + * Get color as string + * + * @return string + */ + public function toString() { + $str = $this->toHex(); + return strtoupper( "#{$str}" ); + } + + /** + * Get the distance between this color and the given color + * + * @param Jetpack_Color $color Color code. + * + * @return int + */ + public function getDistanceRgbFrom( Jetpack_Color $color ) { + $rgb1 = $this->toRgbInt(); + $rgb2 = $color->toRgbInt(); + + $r_diff = abs( $rgb1['red'] - $rgb2['red'] ); + $g_diff = abs( $rgb1['green'] - $rgb2['green'] ); + $b_diff = abs( $rgb1['blue'] - $rgb2['blue'] ); + + // Sum of RGB differences. + $diff = $r_diff + $g_diff + $b_diff; + return $diff; + } + + /** + * Get distance from the given color using the Delta E method + * + * @param Jetpack_Color $color Color code. + * + * @return float + */ + public function getDistanceLabFrom( Jetpack_Color $color ) { + $lab1 = $this->toLabCie(); + $lab2 = $color->toLabCie(); + + $l_diff = abs( $lab2['l'] - $lab1['l'] ); + $a_diff = abs( $lab2['a'] - $lab1['a'] ); + $b_diff = abs( $lab2['b'] - $lab1['b'] ); + + $delta = sqrt( $l_diff + $a_diff + $b_diff ); + + return $delta; + } + + /** + * Calculate luminosity. + * + * @return float + */ + public function toLuminosity() { + $lum = array(); + foreach ( $this->toRgbInt() as $slot => $value ) { + $chan = $value / 255; + $lum[ $slot ] = ( $chan <= 0.03928 ) ? $chan / 12.92 : pow( ( ( $chan + 0.055 ) / 1.055 ), 2.4 ); + } + return 0.2126 * $lum['red'] + 0.7152 * $lum['green'] + 0.0722 * $lum['blue']; + } + + /** + * Get distance between colors using luminance. + * Should be more than 5 for readable contrast + * + * @param Jetpack_Color $color Another color. + * @return float + */ + public function getDistanceLuminosityFrom( Jetpack_Color $color ) { + $l1 = $this->toLuminosity(); + $l2 = $color->toLuminosity(); + if ( $l1 > $l2 ) { + return ( $l1 + 0.05 ) / ( $l2 + 0.05 ); + } else { + return ( $l2 + 0.05 ) / ( $l1 + 0.05 ); + } + } + + /** + * Get maximum contrast color. + * + * @return $this + */ + public function getMaxContrastColor() { + $with_black = $this->getDistanceLuminosityFrom( new Jetpack_Color( '#000' ) ); + $with_white = $this->getDistanceLuminosityFrom( new Jetpack_Color( '#fff' ) ); + $color = new Jetpack_Color(); + $hex = ( $with_black >= $with_white ) ? '#000000' : '#ffffff'; + return $color->fromHex( $hex ); + } + + /** + * Get grayscale contrasting color. + * + * @param bool|int $contrast Contrast. + * + * @return $this + */ + public function getGrayscaleContrastingColor( $contrast = false ) { + if ( ! $contrast ) { + return $this->getMaxContrastColor(); + } + // don't allow less than 5. + $target_contrast = ( $contrast < 5 ) ? 5 : $contrast; + $color = $this->getMaxContrastColor(); + $contrast = $color->getDistanceLuminosityFrom( $this ); + + // if current max contrast is less than the target contrast, we had wishful thinking. + if ( $contrast <= $target_contrast ) { + return $color; + } + + $incr = ( '#000000' === $color->toString() ) ? 1 : -1; + while ( $contrast > $target_contrast ) { + $color = $color->incrementLightness( $incr ); + $contrast = $color->getDistanceLuminosityFrom( $this ); + } + + return $color; + } + + /** + * Gets a readable contrasting color. $this is assumed to be the text and $color the background color. + * + * @param object $bg_color A Color object that will be compared against $this. + * @param integer $min_contrast The minimum contrast to achieve, if possible. + * @return object A Color object, an increased contrast $this compared against $bg_color + */ + public function getReadableContrastingColor( $bg_color = false, $min_contrast = 5 ) { + if ( ! $bg_color || ! is_a( $bg_color, 'Jetpack_Color' ) ) { + return $this; + } + // you shouldn't use less than 5, but you might want to. + $target_contrast = $min_contrast; + // working things. + $contrast = $bg_color->getDistanceLuminosityFrom( $this ); + $max_contrast_color = $bg_color->getMaxContrastColor(); + $max_contrast = $max_contrast_color->getDistanceLuminosityFrom( $bg_color ); + + // if current max contrast is less than the target contrast, we had wishful thinking. + // still, go max. + if ( $max_contrast <= $target_contrast ) { + return $max_contrast_color; + } + // or, we might already have sufficient contrast. + if ( $contrast >= $target_contrast ) { + return $this; + } + + $incr = ( 0 === $max_contrast_color->toInt() ) ? -1 : 1; + while ( $contrast < $target_contrast ) { + $this->incrementLightness( $incr ); + $contrast = $bg_color->getDistanceLuminosityFrom( $this ); + // infininite loop prevention: you never know. + if ( 0 === $this->color || 16777215 === $this->color ) { + break; + } + } + + return $this; + } + + /** + * Detect if color is grayscale + * + * @param int $threshold Max difference between colors. + * + * @return bool + */ + public function isGrayscale( $threshold = 16 ) { + $rgb = $this->toRgbInt(); + + // Get min and max rgb values, then difference between them. + $rgb_min = min( $rgb ); + $rgb_max = max( $rgb ); + $diff = $rgb_max - $rgb_min; + + return $diff < $threshold; + } + + /** + * Get the closest matching color from the given array of colors + * + * @param array $colors array of integers or Jetpack_Color objects. + * + * @return mixed the array key of the matched color + */ + public function getClosestMatch( array $colors ) { + $match_dist = 10000; + $match_key = null; + foreach ( $colors as $key => $color ) { + if ( false === ( $color instanceof Jetpack_Color ) ) { + $c = new Jetpack_Color( $color ); + } + $dist = $this->getDistanceLabFrom( $c ); + if ( $dist < $match_dist ) { + $match_dist = $dist; + $match_key = $key; + } + } + + return $match_key; + } + + /* TRANSFORMS */ + + /** + * Transform -- Darken color. + * + * @param int $amount Amount. Default to 5. + * + * @return $this + */ + public function darken( $amount = 5 ) { + return $this->incrementLightness( - $amount ); + } + + /** + * Transform -- Lighten color. + * + * @param int $amount Amount. Default to 5. + * + * @return $this + */ + public function lighten( $amount = 5 ) { + return $this->incrementLightness( $amount ); + } + + /** + * Transform -- Increment lightness. + * + * @param int $amount Amount. + * + * @return $this + */ + public function incrementLightness( $amount ) { + $hsl = $this->toHsl(); + + $h = isset( $hsl['h'] ) ? $hsl['h'] : 0; + $s = isset( $hsl['s'] ) ? $hsl['s'] : 0; + $l = isset( $hsl['l'] ) ? $hsl['l'] : 0; + + $l += $amount; + if ( $l < 0 ) { + $l = 0; + } + if ( $l > 100 ) { + $l = 100; + } + return $this->fromHsl( $h, $s, $l ); + } + + /** + * Transform -- Saturate color. + * + * @param int $amount Amount. Default to 15. + * + * @return $this + */ + public function saturate( $amount = 15 ) { + return $this->incrementSaturation( $amount ); + } + + /** + * Transform -- Desaturate color. + * + * @param int $amount Amount. Default to 15. + * + * @return $this + */ + public function desaturate( $amount = 15 ) { + return $this->incrementSaturation( - $amount ); + } + + /** + * Transform -- Increment saturation. + * + * @param int $amount Amount. + * + * @return $this + */ + public function incrementSaturation( $amount ) { + $hsl = $this->toHsl(); + + $h = isset( $hsl['h'] ) ? $hsl['h'] : 0; + $s = isset( $hsl['s'] ) ? $hsl['s'] : 0; + $l = isset( $hsl['l'] ) ? $hsl['l'] : 0; + + $s += $amount; + if ( $s < 0 ) { + $s = 0; + } + if ( $s > 100 ) { + $s = 100; + } + return $this->fromHsl( $h, $s, $l ); + } + + /** + * Transform -- To grayscale. + * + * @return $this + */ + public function toGrayscale() { + $hsl = $this->toHsl(); + + $h = isset( $hsl['h'] ) ? $hsl['h'] : 0; + $s = 0; + $l = isset( $hsl['l'] ) ? $hsl['l'] : 0; + + return $this->fromHsl( $h, $s, $l ); + } + + /** + * Transform -- To the complementary color. + * + * The complement is the color on the opposite side of the color wheel, 180° away. + * + * @return $this + */ + public function getComplement() { + return $this->incrementHue( 180 ); + } + + /** + * Transform -- To an analogous color of the complement. + * + * @param int $step Pass `1` or `-1` to choose which direction around the color wheel. + * + * @return $this + */ + public function getSplitComplement( $step = 1 ) { + $incr = 180 + ( $step * 30 ); + return $this->incrementHue( $incr ); + } + + /** + * Transform -- To an analogous color. + * + * Analogous colors are those adjacent on the color wheel, separated by 30°. + * + * @param int $step Pass `1` or `-1` to choose which direction around the color wheel. + * + * @return $this + */ + public function getAnalog( $step = 1 ) { + $incr = $step * 30; + return $this->incrementHue( $incr ); + } + + /** + * Transform -- To a tetradic (rectangular) color. + * + * A rectangular color scheme uses a color, its complement, and the colors 60° from each. + * This transforms the color to its 60° "tetrad". + * + * @param int $step Pass `1` or `-1` to choose which direction around the color wheel. + * + * @return $this + */ + public function getTetrad( $step = 1 ) { + $incr = $step * 60; + return $this->incrementHue( $incr ); + } + + /** + * Transform -- To a triadic color. + * + * A triadic color scheme uses three colors evenly spaced (120°) around the color wheel. + * This transforms the color to one of its triadic colors. + * + * @param int $step Pass `1` or `-1` to choose which direction around the color wheel. + * + * @return $this + */ + public function getTriad( $step = 1 ) { + $incr = $step * 120; + return $this->incrementHue( $incr ); + } + + /** + * Transform -- Increment hue. + * + * @param int $amount Amount. + * + * @return $this + */ + public function incrementHue( $amount ) { + $hsl = $this->toHsl(); + + $h = isset( $hsl['h'] ) ? $hsl['h'] : 0; + $s = isset( $hsl['s'] ) ? $hsl['s'] : 0; + $l = isset( $hsl['l'] ) ? $hsl['l'] : 0; + + $h = ( $h + $amount ) % 360; + if ( $h < 0 ) { + $h += 360; + } + return $this->fromHsl( $h, $s, $l ); + } + } +} diff --git a/projects/packages/classic-theme-helper/_inc/lib/tonesque.php b/projects/packages/classic-theme-helper/_inc/lib/tonesque.php index b16480b914fae..e90d8a13305bb 100644 --- a/projects/packages/classic-theme-helper/_inc/lib/tonesque.php +++ b/projects/packages/classic-theme-helper/_inc/lib/tonesque.php @@ -8,271 +8,273 @@ * @package automattic/jetpack */ -/** - * Color representation class. - */ -class Tonesque { - /** - * Image URL. - * - * @var string - */ - private $image_url = ''; - /** - * Image identifier representing the image. - * - * @var null|object - */ - private $image_obj = null; +if ( ! class_exists( 'Tonesque' ) ) { /** - * Color code. - * - * @var string + * Color representation class. */ - private $color = ''; + class Tonesque { + /** + * Image URL. + * + * @var string + */ + private $image_url = ''; + /** + * Image identifier representing the image. + * + * @var null|object + */ + private $image_obj = null; + /** + * Color code. + * + * @var string + */ + private $color = ''; - /** - * Constructor. - * - * @param string $image_url Image URL. - */ - public function __construct( $image_url ) { - if ( ! class_exists( 'Jetpack_Color' ) && defined( 'JETPACK__PLUGIN_DIR' ) ) { - require_once JETPACK__PLUGIN_DIR . '/_inc/lib/class.color.php'; + /** + * Constructor. + * + * @param string $image_url Image URL. + */ + public function __construct( $image_url ) { + if ( ! class_exists( 'Jetpack_Color' ) ) { + require_once __DIR__ . '/class.color.php'; + } + + $this->image_url = esc_url_raw( $image_url ); + $this->image_url = trim( $this->image_url ); + /** + * Allows any image URL to be passed in for $this->image_url. + * + * @module theme-tools + * + * @since 2.5.0 + * + * @param string $image_url The URL to any image + */ + $this->image_url = apply_filters( 'tonesque_image_url', $this->image_url ); + + $this->image_obj = self::imagecreatefromurl( $this->image_url ); } - $this->image_url = esc_url_raw( $image_url ); - $this->image_url = trim( $this->image_url ); /** - * Allows any image URL to be passed in for $this->image_url. - * - * @module theme-tools + * Get an image object from a URL. * - * @since 2.5.0 + * @param string $image_url Image URL. * - * @param string $image_url The URL to any image + * @return object|bool Image object or false if the image could not be loaded. */ - $this->image_url = apply_filters( 'tonesque_image_url', $this->image_url ); + public static function imagecreatefromurl( $image_url ) { + $data = null; - $this->image_obj = self::imagecreatefromurl( $this->image_url ); - } + // If it's a URL. + if ( preg_match( '#^https?://#i', $image_url ) ) { + // If it's a url pointing to a local media library url. + $content_url = content_url(); + $_image_url = set_url_scheme( $image_url ); + if ( str_starts_with( $_image_url, $content_url ) ) { + $_image_path = str_replace( $content_url, WP_CONTENT_DIR, $_image_url ); + if ( file_exists( $_image_path ) ) { + $filetype = wp_check_filetype( $_image_path ); + $type = $filetype['type']; - /** - * Get an image object from a URL. - * - * @param string $image_url Image URL. - * - * @return object|bool Image object or false if the image could not be loaded. - */ - public static function imagecreatefromurl( $image_url ) { - $data = null; - - // If it's a URL. - if ( preg_match( '#^https?://#i', $image_url ) ) { - // If it's a url pointing to a local media library url. - $content_url = content_url(); - $_image_url = set_url_scheme( $image_url ); - if ( str_starts_with( $_image_url, $content_url ) ) { - $_image_path = str_replace( $content_url, WP_CONTENT_DIR, $_image_url ); - if ( file_exists( $_image_path ) ) { - $filetype = wp_check_filetype( $_image_path ); - $type = $filetype['type']; + if ( str_starts_with( $type, 'image/' ) ) { + $data = file_get_contents( $_image_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents + } + } + } - if ( str_starts_with( $type, 'image/' ) ) { - $data = file_get_contents( $_image_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents + if ( empty( $data ) ) { + $response = wp_safe_remote_get( $image_url ); + $response_code = wp_remote_retrieve_response_code( $response ); + if ( + is_wp_error( $response ) + || ! $response_code + || $response_code < 200 + || $response_code >= 300 + ) { + return false; } + $data = wp_remote_retrieve_body( $response ); } } - if ( empty( $data ) ) { - $response = wp_safe_remote_get( $image_url ); - $response_code = wp_remote_retrieve_response_code( $response ); - if ( - is_wp_error( $response ) - || ! $response_code - || $response_code < 200 - || $response_code >= 300 - ) { - return false; + // If it's a local path in our WordPress install. + if ( file_exists( $image_url ) ) { + $filetype = wp_check_filetype( $image_url ); + $type = $filetype['type']; + + if ( str_starts_with( $type, 'image/' ) ) { + $data = file_get_contents( $image_url ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents } - $data = wp_remote_retrieve_body( $response ); } - } - // If it's a local path in our WordPress install. - if ( file_exists( $image_url ) ) { - $filetype = wp_check_filetype( $image_url ); - $type = $filetype['type']; - - if ( str_starts_with( $type, 'image/' ) ) { - $data = file_get_contents( $image_url ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents + if ( null === $data ) { + return false; } - } - if ( null === $data ) { - return false; + // Now turn it into an image and return it. + return imagecreatefromstring( $data ); } - // Now turn it into an image and return it. - return imagecreatefromstring( $data ); - } + /** + * Construct object from image. + * + * @param string $type Type (hex, rgb, hsv) (optional). + * + * @return string|bool color as a string formatted as $type or false if the image could not be loaded. + */ + public function color( $type = 'hex' ) { + // Bail if there is no image to work with. + if ( ! $this->image_obj ) { + return false; + } - /** - * Construct object from image. - * - * @param string $type Type (hex, rgb, hsv) (optional). - * - * @return string|bool color as a string formatted as $type or false if the image could not be loaded. - */ - public function color( $type = 'hex' ) { - // Bail if there is no image to work with. - if ( ! $this->image_obj ) { - return false; + // Finds dominant color. + $color = self::grab_color(); + // Passes value to Color class. + return self::get_color( $color, $type ); } - // Finds dominant color. - $color = self::grab_color(); - // Passes value to Color class. - return self::get_color( $color, $type ); - } - - /** - * Grabs the color index for each of five sample points of the image - * - * @param string $type can be 'index' or 'hex'. - * - * @return array|false color indices or false if the image could not be loaded. - */ - public function grab_points( $type = 'index' ) { - $img = $this->image_obj; - if ( ! $img ) { - return false; - } + /** + * Grabs the color index for each of five sample points of the image + * + * @param string $type can be 'index' or 'hex'. + * + * @return array|false color indices or false if the image could not be loaded. + */ + public function grab_points( $type = 'index' ) { + $img = $this->image_obj; + if ( ! $img ) { + return false; + } - $height = imagesy( $img ); - $width = imagesx( $img ); + $height = imagesy( $img ); + $width = imagesx( $img ); - /* - * Sample five points in the image - * based on rule of thirds and center. - */ - $topy = round( $height / 3 ); - $bottomy = round( ( $height / 3 ) * 2 ); - $leftx = round( $width / 3 ); - $rightx = round( ( $width / 3 ) * 2 ); - $centery = round( $height / 2 ); - $centerx = round( $width / 2 ); + /* + * Sample five points in the image + * based on rule of thirds and center. + */ + $topy = round( $height / 3 ); + $bottomy = round( ( $height / 3 ) * 2 ); + $leftx = round( $width / 3 ); + $rightx = round( ( $width / 3 ) * 2 ); + $centery = round( $height / 2 ); + $centerx = round( $width / 2 ); - // Cast those colors into an array. - $points = array( - imagecolorat( $img, $leftx, $topy ), - imagecolorat( $img, $rightx, $topy ), - imagecolorat( $img, $leftx, $bottomy ), - imagecolorat( $img, $rightx, $bottomy ), - imagecolorat( $img, $centerx, $centery ), - ); + // Cast those colors into an array. + $points = array( + imagecolorat( $img, $leftx, $topy ), + imagecolorat( $img, $rightx, $topy ), + imagecolorat( $img, $leftx, $bottomy ), + imagecolorat( $img, $rightx, $bottomy ), + imagecolorat( $img, $centerx, $centery ), + ); - if ( 'hex' === $type ) { - foreach ( $points as $i => $p ) { - $c = imagecolorsforindex( $img, $p ); - $points[ $i ] = self::get_color( - array( - 'r' => $c['red'], - 'g' => $c['green'], - 'b' => $c['blue'], - ), - 'hex' - ); + if ( 'hex' === $type ) { + foreach ( $points as $i => $p ) { + $c = imagecolorsforindex( $img, $p ); + $points[ $i ] = self::get_color( + array( + 'r' => $c['red'], + 'g' => $c['green'], + 'b' => $c['blue'], + ), + 'hex' + ); + } } + + return $points; } - return $points; - } + /** + * Finds the average color of the image based on five sample points + * + * @return array|bool array with rgb color or false if the image could not be loaded. + */ + public function grab_color() { + $img = $this->image_obj; + if ( ! $img ) { + return false; + } - /** - * Finds the average color of the image based on five sample points - * - * @return array|bool array with rgb color or false if the image could not be loaded. - */ - public function grab_color() { - $img = $this->image_obj; - if ( ! $img ) { - return false; - } + $rgb = self::grab_points(); - $rgb = self::grab_points(); + $r = array(); + $g = array(); + $b = array(); - $r = array(); - $g = array(); - $b = array(); + /* + * Process the color points + * Find the average representation + */ + foreach ( $rgb as $color ) { + $index = imagecolorsforindex( $img, $color ); + $r[] = $index['red']; + $g[] = $index['green']; + $b[] = $index['blue']; + } + $red = round( array_sum( $r ) / 5 ); + $green = round( array_sum( $g ) / 5 ); + $blue = round( array_sum( $b ) / 5 ); - /* - * Process the color points - * Find the average representation - */ - foreach ( $rgb as $color ) { - $index = imagecolorsforindex( $img, $color ); - $r[] = $index['red']; - $g[] = $index['green']; - $b[] = $index['blue']; - } - $red = round( array_sum( $r ) / 5 ); - $green = round( array_sum( $g ) / 5 ); - $blue = round( array_sum( $b ) / 5 ); + // The average color of the image as rgb array. + $color = array( + 'r' => $red, + 'g' => $green, + 'b' => $blue, + ); - // The average color of the image as rgb array. - $color = array( - 'r' => $red, - 'g' => $green, - 'b' => $blue, - ); + return $color; + } - return $color; - } + /** + * Get a Color object using /lib class.color + * Convert to appropriate type + * + * @param string $color Color code. + * @param string $type Color type (rgb, hex, hsv). + * + * @return string + */ + public function get_color( $color, $type ) { + $c = new Jetpack_Color( $color, 'rgb' ); + $this->color = $c; - /** - * Get a Color object using /lib class.color - * Convert to appropriate type - * - * @param string $color Color code. - * @param string $type Color type (rgb, hex, hsv). - * - * @return string - */ - public function get_color( $color, $type ) { - $c = new Jetpack_Color( $color, 'rgb' ); - $this->color = $c; + switch ( $type ) { + case 'rgb': + $color = implode( ',', $c->toRgbInt() ); + break; + case 'hex': + $color = $c->toHex(); + break; + case 'hsv': + $color = implode( ',', $c->toHsvInt() ); + break; + default: + return $c->toHex(); + } - switch ( $type ) { - case 'rgb': - $color = implode( ',', $c->toRgbInt() ); - break; - case 'hex': - $color = $c->toHex(); - break; - case 'hsv': - $color = implode( ',', $c->toHsvInt() ); - break; - default: - return $c->toHex(); + return $color; } - return $color; - } + /** + * + * Checks contrast against main color + * Gives either black or white for using with opacity + * + * @return string|bool Returns black or white or false if the image could not be loaded. + */ + public function contrast() { + if ( ! $this->color ) { + return false; + } - /** - * - * Checks contrast against main color - * Gives either black or white for using with opacity - * - * @return string|bool Returns black or white or false if the image could not be loaded. - */ - public function contrast() { - if ( ! $this->color ) { - return false; + $c = $this->color->getMaxContrastColor(); + return implode( ',', $c->toRgbInt() ); } - - $c = $this->color->getMaxContrastColor(); - return implode( ',', $c->toRgbInt() ); } } diff --git a/projects/packages/classic-theme-helper/changelog/update-move-jetpack-color-to-classic-theme-helper b/projects/packages/classic-theme-helper/changelog/update-move-jetpack-color-to-classic-theme-helper new file mode 100644 index 0000000000000..14fd557a87b3b --- /dev/null +++ b/projects/packages/classic-theme-helper/changelog/update-move-jetpack-color-to-classic-theme-helper @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +Add Jetpack_Color class diff --git a/projects/packages/classic-theme-helper/src/class-main.php b/projects/packages/classic-theme-helper/src/class-main.php index 0820c0ae861e4..31e98c972f9f8 100644 --- a/projects/packages/classic-theme-helper/src/class-main.php +++ b/projects/packages/classic-theme-helper/src/class-main.php @@ -23,6 +23,7 @@ class Main { */ public $modules = array( 'responsive-videos.php', + 'jetpack-color.php', ); /** Holds the singleton instance of the Loader @@ -39,7 +40,7 @@ public static function init() { self::$instance = new Main(); self::$instance->load_modules(); // TODO Commenting below since we still load them from theme-tools module - // add_action( 'init', array( __CLASS__, 'jetpack_load_theme_tools' ), 30 ); + add_action( 'init', array( __CLASS__, 'jetpack_load_theme_tools' ), 30 ); // add_action( 'after_setup_theme', array( __CLASS__, 'jetpack_load_theme_compat' ), -1 ); } diff --git a/projects/packages/classic-theme-helper/src/jetpack-color.php b/projects/packages/classic-theme-helper/src/jetpack-color.php new file mode 100644 index 0000000000000..d3e1a9ea1c52d --- /dev/null +++ b/projects/packages/classic-theme-helper/src/jetpack-color.php @@ -0,0 +1,8 @@ +