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