Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 322
0.00% covered (danger)
0.00%
0 / 41
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_Color
0.00% covered (danger)
0.00%
0 / 321
0.00% covered (danger)
0.00%
0 / 41
15500
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
132
 fromHex
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 fromRgbInt
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
56
 fromRgbHex
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 fromHsl
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 hue2rgb
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
42
 fromInt
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 toHex
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 toRgbInt
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 toRgbHex
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 toHsvFloat
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
42
 toHsvInt
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
42
 toHsl
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
56
 toCSS
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
132
 toXyz
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
20
 toLabCie
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
 toInt
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 __toString
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 toString
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getDistanceRgbFrom
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 getDistanceLabFrom
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 toLuminosity
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 getDistanceLuminosityFrom
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getMaxContrastColor
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getGrayscaleContrastingColor
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 getReadableContrastingColor
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
90
 isGrayscale
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getClosestMatch
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 darken
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 lighten
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 incrementLightness
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 saturate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 desaturate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 incrementSaturation
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 toGrayscale
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getComplement
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSplitComplement
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getAnalog
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getTetrad
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getTriad
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 incrementHue
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
1<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2/**
3 * Color utility and conversion
4 *
5 * Represents a color value, and converts between RGB/HSV/XYZ/Lab/HSL
6 *
7 * Example:
8 * $color = new Jetpack_Color(0xFFFFFF);
9 *
10 * @author Harold Asbridge <hasbridge@gmail.com>
11 * @author Matt Wiebe <wiebe@automattic.com>
12 * @license https://www.opensource.org/licenses/MIT
13 *
14 * @package automattic/jetpack
15 */
16
17// phpcs:disable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
18
19if ( ! class_exists( 'Jetpack_Color' ) ) {
20    /**
21     * Color utilities
22     */
23    class Jetpack_Color {
24        /**
25         * Color code (later array or string, depending on type)
26         *
27         * @var int|array|string
28         */
29        protected $color = 0;
30
31        /**
32         * Initialize object
33         *
34         * @param string|array $color A color of the type $type.
35         * @param string       $type The type of color we will construct from.
36         *        One of hex (default), rgb, hsl, int.
37         */
38        public function __construct( $color = null, $type = 'hex' ) {
39            if ( $color ) {
40                switch ( $type ) {
41                    case 'hex':
42                        $this->fromHex( $color );
43                        break;
44                    case 'rgb':
45                        if ( is_array( $color ) && count( $color ) === 3 ) {
46                            list( $r, $g, $b ) = array_values( $color );
47                            $this->fromRgbInt( $r, $g, $b );
48                        }
49                        break;
50                    case 'hsl':
51                        if ( is_array( $color ) && count( $color ) === 3 ) {
52                            list( $h, $s, $l ) = array_values( $color );
53                            $this->fromHsl( $h, $s, $l );
54                        }
55                        break;
56                    case 'int':
57                        // @phan-suppress-next-line PhanTypeMismatchArgument
58                        $this->fromInt( $color );
59                        break;
60                    default:
61                        // there is no default.
62                        break;
63                }
64            }
65        }
66
67        /**
68         * Init color from hex value
69         *
70         * @param string $hex_value Color hex value.
71         *
72         * @return $this
73         * @throws RangeException Invalid color code range error.
74         */
75        public function fromHex( $hex_value ) {
76            $hex_value = str_replace( '#', '', $hex_value );
77            // handle short hex codes like #fff.
78            if ( 3 === strlen( $hex_value ) ) {
79                $hex_value = $hex_value[0] . $hex_value[0] . $hex_value[1] . $hex_value[1] . $hex_value[2] . $hex_value[2];
80            }
81            return $this->fromInt( hexdec( $hex_value ) );
82        }
83
84        /**
85         * Init color from integer RGB values
86         *
87         * @param int $red   Red color code.
88         * @param int $green Green color code.
89         * @param int $blue  Blue color code.
90         *
91         * @return $this
92         * @throws RangeException Invalid color code range error.
93         */
94        public function fromRgbInt( $red, $green, $blue ) {
95            if ( $red < 0 || $red > 255 ) {
96                throw new RangeException( 'Red value ' . $red . ' out of valid color code range' );
97            }
98
99            if ( $green < 0 || $green > 255 ) {
100                throw new RangeException( 'Green value ' . $green . ' out of valid color code range' );
101            }
102
103            if ( $blue < 0 || $blue > 255 ) {
104                throw new RangeException( 'Blue value ' . $blue . ' out of valid color code range' );
105            }
106
107            $this->color = ( intval( $red ) << 16 ) + ( intval( $green ) << 8 ) + intval( $blue );
108
109            return $this;
110        }
111
112        /**
113         * Init color from hex RGB values
114         *
115         * @param string $red   Red color code.
116         * @param string $green Green color code.
117         * @param string $blue  Blue color code.
118         *
119         * @return $this
120         */
121        public function fromRgbHex( $red, $green, $blue ) {
122            return $this->fromRgbInt( hexdec( $red ), hexdec( $green ), hexdec( $blue ) );
123        }
124
125        /**
126         * Converts an HSL color value to RGB. Conversion formula
127         * adapted from https://en.wikipedia.org/wiki/HSL_color_space.
128         *
129         * @param  int $h Hue. [0-360].
130         * @param  int $s Saturation [0, 100].
131         * @param  int $l Lightness [0, 100].
132         */
133        public function fromHsl( $h, $s, $l ) {
134            $h /= 360;
135            $s /= 100;
136            $l /= 100;
137
138            if ( 0 === $s ) {
139                // achromatic.
140                $r = $l;
141                $g = $l;
142                $b = $l;
143            } else {
144                $q = $l < 0.5 ? $l * ( 1 + $s ) : $l + $s - $l * $s;
145                $p = 2 * $l - $q;
146                $r = $this->hue2rgb( $p, $q, $h + 1 / 3 );
147                $g = $this->hue2rgb( $p, $q, $h );
148                $b = $this->hue2rgb( $p, $q, $h - 1 / 3 );
149            }
150
151            return $this->fromRgbInt( $r * 255, $g * 255, $b * 255 );
152        }
153
154        /**
155         * Helper function for Jetpack_Color::fromHsl()
156         *
157         * @param  float $p Minimum of R/G/B [0, 1].
158         * @param  float $q Maximum of R/G/B [0, 1].
159         * @param  float $t Adjusted hue [0, 1].
160         */
161        private function hue2rgb( $p, $q, $t ) {
162            if ( $t < 0 ) {
163                ++$t;
164            }
165            if ( $t > 1 ) {
166                --$t;
167            }
168            if ( $t < 1 / 6 ) {
169                return $p + ( $q - $p ) * 6 * $t;
170            }
171            if ( $t < 1 / 2 ) {
172                return $q;
173            }
174            if ( $t < 2 / 3 ) {
175                return $p + ( $q - $p ) * ( 2 / 3 - $t ) * 6;
176            }
177            return $p;
178        }
179
180        /**
181         * Init color from integer value
182         *
183         * @param int $int_value Color code.
184         *
185         * @return $this
186         * @throws RangeException Invalid color code range error.
187         */
188        public function fromInt( $int_value ) {
189            if ( $int_value < 0 || $int_value > 16777215 ) {
190                throw new RangeException( $int_value . ' out of valid color code range' );
191            }
192
193            $this->color = $int_value;
194
195            return $this;
196        }
197
198        /**
199         * Convert color to hex
200         *
201         * @return string
202         */
203        public function toHex() {
204            return sprintf( '%06x', $this->color );
205        }
206
207        /**
208         * Convert color to RGB array (integer values)
209         *
210         * @return array
211         */
212        public function toRgbInt() {
213            return array(
214                'red'   => (int) ( 255 & ( $this->color >> 16 ) ),
215                'green' => (int) ( 255 & ( $this->color >> 8 ) ),
216                'blue'  => (int) ( 255 & ( $this->color ) ),
217            );
218        }
219
220        /**
221         * Convert color to RGB array (hex values)
222         *
223         * @return array
224         */
225        public function toRgbHex() {
226            $r = array();
227            foreach ( $this->toRgbInt() as $item ) {
228                $r[] = dechex( $item );
229            }
230            return $r;
231        }
232
233        /**
234         * Get Hue/Saturation/Value for the current color
235         * (float values, slow but accurate)
236         *
237         * @return array
238         */
239        public function toHsvFloat() {
240            $rgb = $this->toRgbInt();
241
242            $rgb_min = min( $rgb );
243            $rgb_max = max( $rgb );
244
245            $hsv = array(
246                'hue' => 0,
247                'sat' => 0,
248                'val' => $rgb_max,
249            );
250
251            // If v is 0, color is black.
252            if ( 0 === $hsv['val'] ) {
253                return $hsv;
254            }
255
256            // Normalize RGB values to 1.
257            $rgb['red']   /= $hsv['val'];
258            $rgb['green'] /= $hsv['val'];
259            $rgb['blue']  /= $hsv['val'];
260            $rgb_min       = min( $rgb );
261            $rgb_max       = max( $rgb );
262
263            // Calculate saturation.
264            $hsv['sat'] = $rgb_max - $rgb_min;
265            if ( 0 === $hsv['sat'] ) {
266                $hsv['hue'] = 0;
267                return $hsv;
268            }
269
270            // Normalize saturation to 1.
271            $rgb['red']   = ( $rgb['red'] - $rgb_min ) / ( $rgb_max - $rgb_min );
272            $rgb['green'] = ( $rgb['green'] - $rgb_min ) / ( $rgb_max - $rgb_min );
273            $rgb['blue']  = ( $rgb['blue'] - $rgb_min ) / ( $rgb_max - $rgb_min );
274            $rgb_min      = min( $rgb );
275            $rgb_max      = max( $rgb );
276
277            // Calculate hue.
278            if ( $rgb_max === $rgb['red'] ) {
279                $hsv['hue'] = 0.0 + 60 * ( $rgb['green'] - $rgb['blue'] );
280                if ( $hsv['hue'] < 0 ) {
281                    $hsv['hue'] += 360;
282                }
283            } elseif ( $rgb_max === $rgb['green'] ) {
284                $hsv['hue'] = 120 + ( 60 * ( $rgb['blue'] - $rgb['red'] ) );
285            } else {
286                $hsv['hue'] = 240 + ( 60 * ( $rgb['red'] - $rgb['green'] ) );
287            }
288
289            return $hsv;
290        }
291
292        /**
293         * Get HSV values for color
294         * (integer values from 0-255, fast but less accurate)
295         *
296         * @return array
297         */
298        public function toHsvInt() {
299            $rgb = $this->toRgbInt();
300
301            $rgb_min = min( $rgb );
302            $rgb_max = max( $rgb );
303
304            $hsv = array(
305                'hue' => 0,
306                'sat' => 0,
307                'val' => $rgb_max,
308            );
309
310            // If value is 0, color is black.
311            if ( 0 === $hsv['val'] ) {
312                return $hsv;
313            }
314
315            // Calculate saturation.
316            $hsv['sat'] = round( 255 * ( $rgb_max - $rgb_min ) / $hsv['val'] );
317            // @phan-suppress-next-line PhanImpossibleTypeComparison
318            if ( 0 === $hsv['sat'] ) {
319                $hsv['hue'] = 0;
320                return $hsv;
321            }
322
323            // Calculate hue.
324            if ( $rgb_max === $rgb['red'] ) {
325                $hsv['hue'] = round( 0 + 43 * ( $rgb['green'] - $rgb['blue'] ) / ( $rgb_max - $rgb_min ) );
326            } elseif ( $rgb_max === $rgb['green'] ) {
327                $hsv['hue'] = round( 85 + 43 * ( $rgb['blue'] - $rgb['red'] ) / ( $rgb_max - $rgb_min ) );
328            } else {
329                $hsv['hue'] = round( 171 + 43 * ( $rgb['red'] - $rgb['green'] ) / ( $rgb_max - $rgb_min ) );
330            }
331            if ( $hsv['hue'] < 0 ) {
332                $hsv['hue'] += 255;
333            }
334
335            return $hsv;
336        }
337
338        /**
339         * Converts an RGB color value to HSL. Conversion formula
340         * adapted from https://en.wikipedia.org/wiki/HSL_color_space.
341         * Assumes r, g, and b are contained in the set [0, 255] and
342         * returns h in [0, 360], s in [0, 100], l in [0, 100]
343         *
344         * @return  Array          The HSL representation
345         */
346        public function toHsl() {
347            list( $r, $g, $b ) = array_values( $this->toRgbInt() );
348            $r                /= 255;
349            $g                /= 255;
350            $b                /= 255;
351            $max               = max( $r, $g, $b );
352            $min               = min( $r, $g, $b );
353            $l                 = ( $max + $min ) / 2;
354
355            if ( $max === $min ) {
356                // achromatic.
357                $s = 0;
358                $h = 0;
359            } else {
360                $d = $max - $min;
361                $s = $l > 0.5 ? $d / ( 2 - $max - $min ) : $d / ( $max + $min );
362                switch ( $max ) {
363                    case $r:
364                        $h = ( $g - $b ) / $d + ( $g < $b ? 6 : 0 );
365                        break;
366                    case $g:
367                        $h = ( $b - $r ) / $d + 2;
368                        break;
369                    case $b:
370                        $h = ( $r - $g ) / $d + 4;
371                        break;
372                }
373                $h /= 6;
374            }
375            $h = (int) round( $h * 360 );
376            $s = (int) round( $s * 100 );
377            $l = (int) round( $l * 100 );
378            return compact( 'h', 's', 'l' );
379        }
380
381        /**
382         * From a color code to a string to be used in CSS declaration.
383         *
384         * @param string $type  Color code type.
385         * @param int    $alpha Transparency.
386         *
387         * @return string
388         */
389        public function toCSS( $type = 'hex', $alpha = 1 ) {
390            switch ( $type ) {
391                case 'hex':
392                    return $this->toString();
393                case 'rgb':
394                case 'rgba':
395                    list( $r, $g, $b ) = array_values( $this->toRgbInt() );
396                    if ( is_numeric( $alpha ) && $alpha < 1 ) {
397                        return "rgba( {$r}{$g}{$b}$alpha )";
398                    } else {
399                        return "rgb( {$r}{$g}{$b} )";
400                    }
401                case 'hsl':
402                case 'hsla':
403                    list( $h, $s, $l ) = array_values( $this->toHsl() );
404                    if ( is_numeric( $alpha ) && $alpha < 1 ) {
405                        return "hsla( {$h}{$s}{$l}$alpha )";
406                    } else {
407                        return "hsl( {$h}{$s}{$l} )";
408                    }
409                default:
410                    return $this->toString();
411            }
412        }
413
414        /**
415         * Get current color in XYZ format
416         *
417         * @return array
418         */
419        public function toXyz() {
420            $rgb = $this->toRgbInt();
421
422            // Normalize RGB values to 1.
423            $rgb_new = array();
424            foreach ( $rgb as $item ) {
425                $rgb_new[] = $item / 255;
426            }
427            $rgb = $rgb_new;
428
429            $rgb_new = array();
430            foreach ( $rgb as $item ) {
431                if ( $item > 0.04045 ) {
432                    $item = pow( ( ( $item + 0.055 ) / 1.055 ), 2.4 );
433                } else {
434                    $item /= 12.92;
435                }
436                $rgb_new[] = $item * 100;
437            }
438            $rgb = $rgb_new;
439
440            // Observer. = 2°, Illuminant = D65.
441            $xyz = array(
442                // @phan-suppress-next-line PhanTypeInvalidDimOffset,PhanTypeInvalidLeftOperandOfNumericOp
443                'x' => ( $rgb['red'] * 0.4124 ) + ( $rgb['green'] * 0.3576 ) + ( $rgb['blue'] * 0.1805 ),
444                // @phan-suppress-next-line PhanTypeInvalidDimOffset,PhanTypeInvalidLeftOperandOfNumericOp
445                'y' => ( $rgb['red'] * 0.2126 ) + ( $rgb['green'] * 0.7152 ) + ( $rgb['blue'] * 0.0722 ),
446                // @phan-suppress-next-line PhanTypeInvalidDimOffset,PhanTypeInvalidLeftOperandOfNumericOp
447                'z' => ( $rgb['red'] * 0.0193 ) + ( $rgb['green'] * 0.1192 ) + ( $rgb['blue'] * 0.9505 ),
448            );
449
450            return $xyz;
451        }
452
453        /**
454         * Get color CIE-Lab values
455         *
456         * @return array
457         */
458        public function toLabCie() {
459            $xyz = $this->toXyz();
460
461            // Ovserver = 2*, Iluminant=D65.
462            $xyz['x'] /= 95.047;
463            $xyz['y'] /= 100;
464            $xyz['z'] /= 108.883;
465
466            $xyz_new = array();
467            foreach ( $xyz as $item ) {
468                if ( $item > 0.008856 ) {
469                    $xyz_new[] = pow( $item, 1 / 3 );
470                } else {
471                    $xyz_new[] = ( 7.787 * $item ) + ( 16 / 116 );
472                }
473            }
474            $xyz = $xyz_new;
475
476            $lab = array(
477                // @phan-suppress-next-line PhanTypeInvalidDimOffset,PhanTypeInvalidRightOperandOfNumericOp
478                'l' => ( 116 * $xyz['y'] ) - 16,
479                // @phan-suppress-next-line PhanTypeInvalidDimOffset,PhanTypeInvalidLeftOperandOfNumericOp,PhanTypeInvalidRightOperandOfNumericOp
480                'a' => 500 * ( $xyz['x'] - $xyz['y'] ),
481                // @phan-suppress-next-line PhanTypeInvalidDimOffset,PhanTypeInvalidLeftOperandOfNumericOp,PhanTypeInvalidRightOperandOfNumericOp
482                'b' => 200 * ( $xyz['y'] - $xyz['z'] ),
483            );
484
485            return $lab;
486        }
487
488        /**
489         * Convert color to integer
490         *
491         * @return int
492         */
493        public function toInt() {
494            return $this->color;
495        }
496
497        /**
498         * Alias of toString()
499         *
500         * @return string
501         */
502        public function __toString() {
503            return $this->toString();
504        }
505
506        /**
507         * Get color as string
508         *
509         * @return string
510         */
511        public function toString() {
512            $str = $this->toHex();
513            return strtoupper( "#{$str}" );
514        }
515
516        /**
517         * Get the distance between this color and the given color
518         *
519         * @param Jetpack_Color $color Color code.
520         *
521         * @return int
522         */
523        public function getDistanceRgbFrom( Jetpack_Color $color ) {
524            $rgb1 = $this->toRgbInt();
525            $rgb2 = $color->toRgbInt();
526
527            $r_diff = abs( $rgb1['red'] - $rgb2['red'] );
528            $g_diff = abs( $rgb1['green'] - $rgb2['green'] );
529            $b_diff = abs( $rgb1['blue'] - $rgb2['blue'] );
530
531            // Sum of RGB differences.
532            $diff = $r_diff + $g_diff + $b_diff;
533            return $diff;
534        }
535
536        /**
537         * Get distance from the given color using the Delta E method
538         *
539         * @param Jetpack_Color $color Color code.
540         *
541         * @return float
542         */
543        public function getDistanceLabFrom( Jetpack_Color $color ) {
544            $lab1 = $this->toLabCie();
545            $lab2 = $color->toLabCie();
546
547            $l_diff = abs( $lab2['l'] - $lab1['l'] );
548            $a_diff = abs( $lab2['a'] - $lab1['a'] );
549            $b_diff = abs( $lab2['b'] - $lab1['b'] );
550
551            $delta = sqrt( $l_diff + $a_diff + $b_diff );
552
553            return $delta;
554        }
555
556        /**
557         * Calculate luminosity.
558         *
559         * @return float
560         */
561        public function toLuminosity() {
562            $lum = array();
563            foreach ( $this->toRgbInt() as $slot => $value ) {
564                $chan         = $value / 255;
565                $lum[ $slot ] = ( $chan <= 0.03928 ) ? $chan / 12.92 : pow( ( ( $chan + 0.055 ) / 1.055 ), 2.4 );
566            }
567            return 0.2126 * $lum['red'] + 0.7152 * $lum['green'] + 0.0722 * $lum['blue'];
568        }
569
570        /**
571         * Get distance between colors using luminance.
572         * Should be more than 5 for readable contrast
573         *
574         * @param  Jetpack_Color $color Another color.
575         * @return float
576         */
577        public function getDistanceLuminosityFrom( Jetpack_Color $color ) {
578            $l1 = $this->toLuminosity();
579            $l2 = $color->toLuminosity();
580            if ( $l1 > $l2 ) {
581                return ( $l1 + 0.05 ) / ( $l2 + 0.05 );
582            } else {
583                return ( $l2 + 0.05 ) / ( $l1 + 0.05 );
584            }
585        }
586
587        /**
588         * Get maximum contrast color.
589         *
590         * @return $this
591         */
592        public function getMaxContrastColor() {
593            $with_black = $this->getDistanceLuminosityFrom( new Jetpack_Color( '#000' ) );
594            $with_white = $this->getDistanceLuminosityFrom( new Jetpack_Color( '#fff' ) );
595            $color      = new Jetpack_Color();
596            $hex        = ( $with_black >= $with_white ) ? '#000000' : '#ffffff';
597            return $color->fromHex( $hex );
598        }
599
600        /**
601         * Get grayscale contrasting color.
602         *
603         * @param bool|int $contrast Contrast.
604         *
605         * @return $this
606         */
607        public function getGrayscaleContrastingColor( $contrast = false ) {
608            if ( ! $contrast ) {
609                return $this->getMaxContrastColor();
610            }
611            // don't allow less than 5.
612            $target_contrast = ( $contrast < 5 ) ? 5 : $contrast;
613            $color           = $this->getMaxContrastColor();
614            $contrast        = $color->getDistanceLuminosityFrom( $this );
615
616            // if current max contrast is less than the target contrast, we had wishful thinking.
617            if ( $contrast <= $target_contrast ) {
618                return $color;
619            }
620
621            $incr = ( '#000000' === $color->toString() ) ? 1 : -1;
622            while ( $contrast > $target_contrast ) {
623                $color    = $color->incrementLightness( $incr );
624                $contrast = $color->getDistanceLuminosityFrom( $this );
625            }
626
627            return $color;
628        }
629
630        /**
631         * Gets a readable contrasting color. $this is assumed to be the text and $color the background color.
632         *
633         * @param  object  $bg_color      A Color object that will be compared against $this.
634         * @param  integer $min_contrast The minimum contrast to achieve, if possible.
635         * @return object                A Color object, an increased contrast $this compared against $bg_color
636         */
637        public function getReadableContrastingColor( $bg_color = null, $min_contrast = 5 ) {
638            if ( ! $bg_color || ! is_a( $bg_color, 'Jetpack_Color' ) ) {
639                return $this;
640            }
641            // you shouldn't use less than 5, but you might want to.
642            $target_contrast = $min_contrast;
643            // working things.
644            $contrast           = $bg_color->getDistanceLuminosityFrom( $this );
645            $max_contrast_color = $bg_color->getMaxContrastColor();
646            $max_contrast       = $max_contrast_color->getDistanceLuminosityFrom( $bg_color );
647
648            // if current max contrast is less than the target contrast, we had wishful thinking.
649            // still, go max.
650            if ( $max_contrast <= $target_contrast ) {
651                return $max_contrast_color;
652            }
653            // or, we might already have sufficient contrast.
654            if ( $contrast >= $target_contrast ) {
655                return $this;
656            }
657
658            $incr = ( 0 === $max_contrast_color->toInt() ) ? -1 : 1;
659            while ( $contrast < $target_contrast ) {
660                $this->incrementLightness( $incr );
661                $contrast = $bg_color->getDistanceLuminosityFrom( $this );
662                // infininite loop prevention: you never know.
663                if ( 0 === $this->color || 16777215 === $this->color ) {
664                    break;
665                }
666            }
667
668            return $this;
669        }
670
671        /**
672         * Detect if color is grayscale
673         *
674         * @param int $threshold Max difference between colors.
675         *
676         * @return bool
677         */
678        public function isGrayscale( $threshold = 16 ) {
679            $rgb = $this->toRgbInt();
680
681            // Get min and max rgb values, then difference between them.
682            $rgb_min = min( $rgb );
683            $rgb_max = max( $rgb );
684            $diff    = $rgb_max - $rgb_min;
685
686            return $diff < $threshold;
687        }
688
689        /**
690         * Get the closest matching color from the given array of colors
691         *
692         * @param array $colors array of integers or Jetpack_Color objects.
693         *
694         * @return mixed the array key of the matched color
695         */
696        public function getClosestMatch( array $colors ) {
697            $match_dist = 10000;
698            $match_key  = null;
699            foreach ( $colors as $key => $color ) {
700                if ( ! ( $color instanceof Jetpack_Color ) ) {
701                    $c = new Jetpack_Color( $color );
702                }
703                // @phan-suppress-next-line PhanPossiblyUndeclaredVariable,PhanTypeMismatchArgumentNullable
704                $dist = $this->getDistanceLabFrom( $c );
705                if ( $dist < $match_dist ) {
706                    $match_dist = $dist;
707                    $match_key  = $key;
708                }
709            }
710
711            return $match_key;
712        }
713
714        /* TRANSFORMS */
715
716        /**
717         * Transform -- Darken color.
718         *
719         * @param int $amount Amount. Default to 5.
720         *
721         * @return $this
722         */
723        public function darken( $amount = 5 ) {
724            return $this->incrementLightness( - $amount );
725        }
726
727        /**
728         * Transform -- Lighten color.
729         *
730         * @param int $amount Amount. Default to 5.
731         *
732         * @return $this
733         */
734        public function lighten( $amount = 5 ) {
735            return $this->incrementLightness( $amount );
736        }
737
738        /**
739         * Transform -- Increment lightness.
740         *
741         * @param int $amount Amount.
742         *
743         * @return $this
744         */
745        public function incrementLightness( $amount ) {
746            $hsl = $this->toHsl();
747
748            $h = $hsl['h'] ?? 0;
749            $s = $hsl['s'] ?? 0;
750            $l = $hsl['l'] ?? 0;
751
752            $l += $amount;
753            if ( $l < 0 ) {
754                $l = 0;
755            }
756            if ( $l > 100 ) {
757                $l = 100;
758            }
759            return $this->fromHsl( $h, $s, $l );
760        }
761
762        /**
763         * Transform -- Saturate color.
764         *
765         * @param int $amount Amount. Default to 15.
766         *
767         * @return $this
768         */
769        public function saturate( $amount = 15 ) {
770            return $this->incrementSaturation( $amount );
771        }
772
773        /**
774         * Transform -- Desaturate color.
775         *
776         * @param int $amount Amount. Default to 15.
777         *
778         * @return $this
779         */
780        public function desaturate( $amount = 15 ) {
781            return $this->incrementSaturation( - $amount );
782        }
783
784        /**
785         * Transform -- Increment saturation.
786         *
787         * @param int $amount Amount.
788         *
789         * @return $this
790         */
791        public function incrementSaturation( $amount ) {
792            $hsl = $this->toHsl();
793
794            $h = $hsl['h'] ?? 0;
795            $s = $hsl['s'] ?? 0;
796            $l = $hsl['l'] ?? 0;
797
798            $s += $amount;
799            if ( $s < 0 ) {
800                $s = 0;
801            }
802            if ( $s > 100 ) {
803                $s = 100;
804            }
805            return $this->fromHsl( $h, $s, $l );
806        }
807
808        /**
809         * Transform -- To grayscale.
810         *
811         * @return $this
812         */
813        public function toGrayscale() {
814            $hsl = $this->toHsl();
815
816            $h = $hsl['h'] ?? 0;
817            $s = 0;
818            $l = $hsl['l'] ?? 0;
819
820            return $this->fromHsl( $h, $s, $l );
821        }
822
823        /**
824         * Transform -- To the complementary color.
825         *
826         * The complement is the color on the opposite side of the color wheel, 180° away.
827         *
828         * @return $this
829         */
830        public function getComplement() {
831            return $this->incrementHue( 180 );
832        }
833
834        /**
835         * Transform -- To an analogous color of the complement.
836         *
837         * @param int $step Pass `1` or `-1` to choose which direction around the color wheel.
838         *
839         * @return $this
840         */
841        public function getSplitComplement( $step = 1 ) {
842            $incr = 180 + ( $step * 30 );
843            return $this->incrementHue( $incr );
844        }
845
846        /**
847         * Transform -- To an analogous color.
848         *
849         * Analogous colors are those adjacent on the color wheel, separated by 30°.
850         *
851         * @param int $step Pass `1` or `-1` to choose which direction around the color wheel.
852         *
853         * @return $this
854         */
855        public function getAnalog( $step = 1 ) {
856            $incr = $step * 30;
857            return $this->incrementHue( $incr );
858        }
859
860        /**
861         * Transform -- To a tetradic (rectangular) color.
862         *
863         * A rectangular color scheme uses a color, its complement, and the colors 60° from each.
864         * This transforms the color to its 60° "tetrad".
865         *
866         * @param int $step Pass `1` or `-1` to choose which direction around the color wheel.
867         *
868         * @return $this
869         */
870        public function getTetrad( $step = 1 ) {
871            $incr = $step * 60;
872            return $this->incrementHue( $incr );
873        }
874
875        /**
876         * Transform -- To a triadic color.
877         *
878         * A triadic color scheme uses three colors evenly spaced (120°) around the color wheel.
879         * This transforms the color to one of its triadic colors.
880         *
881         * @param int $step Pass `1` or `-1` to choose which direction around the color wheel.
882         *
883         * @return $this
884         */
885        public function getTriad( $step = 1 ) {
886            $incr = $step * 120;
887            return $this->incrementHue( $incr );
888        }
889
890        /**
891         * Transform -- Increment hue.
892         *
893         * @param int $amount Amount.
894         *
895         * @return $this
896         */
897        public function incrementHue( $amount ) {
898            $hsl = $this->toHsl();
899
900            $h = $hsl['h'] ?? 0;
901            $s = $hsl['s'] ?? 0;
902            $l = $hsl['l'] ?? 0;
903
904            $h = ( $h + $amount ) % 360;
905            if ( $h < 0 ) {
906                $h += 360;
907            }
908            return $this->fromHsl( $h, $s, $l );
909        }
910    }
911}