Color   D
last analyzed

Complexity

Total Complexity 59

Size/Duplication

Total Lines 518
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 155
c 5
b 0
f 0
dl 0
loc 518
ccs 169
cts 169
cp 1
rs 4.08
wmc 59

26 Methods

Rating   Name   Duplication   Size   Complexity  
A mix() 0 13 1
A isDark() 0 3 1
A fromCssColor() 0 3 1
A __toString() 0 3 1
A getLuminosity() 0 11 4
A fromHex() 0 7 2
A colorDifference() 0 7 1
A darken() 0 8 1
A getHex() 0 7 1
A hslToRgb() 0 28 3
A brightnessDifference() 0 9 2
A adjustHue() 0 20 4
A fromRgb() 0 7 2
A lighten() 0 8 1
A __construct() 0 6 1
A luminosityContrast() 0 6 1
A equals() 0 3 1
A hueToRgb() 0 20 6
A getRgb() 0 3 1
A fromHsl() 0 7 2
B getHsl() 0 46 7
A isReadable() 0 10 4
B getMatchingTextColor() 0 26 7
A jsonSerialize() 0 3 1
A hexToRgb() 0 13 2
A getBrightness() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Color often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Color, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace MikeAlmond\Color;
6
7
use MikeAlmond\Color\Exceptions\ColorException;
8
use MikeAlmond\Color\Exceptions\InvalidColorException;
9
10
/**
11
 * Class Color
12
 * @package MikeAlmond\Color
13
 */
14
class Color implements \JsonSerializable
15
{
16
17
    /**
18
     * @var int[]
19
     */
20
    private $colors = [
21
        'r' => 0,
22
        'g' => 0,
23
        'b' => 0,
24
    ];
25
26
    /**
27
     * Create a new Skeleton Instance
28
     *
29
     * @param int $red
30
     * @param int $green
31
     * @param int $blue
32
     */
33 31
    private function __construct(int $red, int $green, int $blue)
34
    {
35 31
        $this->colors = [
36 31
            'r' => $red,
37 31
            'g' => $green,
38 31
            'b' => $blue,
39
        ];
40 31
    }
41
42
    /**
43
     * @param string $color
44
     *
45
     * @return Color
46
     */
47 39
    public static function fromHex(string $color): Color
48
    {
49 39
        if (!Validator::isValidHex($color)) {
50 11
            throw new InvalidColorException('Invalid hex value');
51
        }
52
53 28
        return new self(...array_values(self::hexToRgb($color)));
54
    }
55
56
    /**
57
     * @param int $red
58
     * @param int $green
59
     * @param int $blue
60
     *
61
     * @return Color
62
     */
63 14
    public static function fromRgb(int $red, int $green, int $blue): Color
64
    {
65 14
        if (!Validator::isValidRgb($red, $green, $blue)) {
66 2
            throw new InvalidColorException('Invalid RGB values');
67
        }
68
69 12
        return new self($red, $green, $blue);
70
    }
71
72
    /**
73
     * @param float $hue
74
     * @param float $saturation
75
     * @param float $lightness
76
     *
77
     * @return Color
78
     */
79 3
    public static function fromHsl(float $hue, float $saturation, float $lightness): Color
80
    {
81 3
        if (!Validator::isValidHsl($hue, $saturation, $lightness)) {
82 1
            throw new InvalidColorException('Invalid HSL value');
83
        }
84
85 2
        return new self(...array_values(self::hslToRgb($hue, $saturation, $lightness)));
86
    }
87
88
    /**
89
     * @param string $color
90
     *
91
     * @return Color
92
     */
93 2
    public static function fromCssColor(string $color): Color
94
    {
95 2
        return self::fromHex(X11Colors::search($color));
96
    }
97
98
    /**
99
     * @param string $color
100
     *
101
     * @return array
102
     */
103 28
    private static function hexToRgb(string $color): array
104
    {
105 28
        $color = ltrim($color, '#');
106
107
        // Convert the shorthand hex to the full hex (09F => 0099FF)
108 28
        if (strlen($color) == 3) {
109 1
            $color = $color[0] . $color[0] . $color[1] . $color[1] . $color[2] . $color[2];
110
        }
111
112
        return [
113 28
            'r' => (int)hexdec(substr($color, 0, 2)),
114 28
            'g' => (int)hexdec(substr($color, 2, 2)),
115 28
            'b' => (int)hexdec(substr($color, 4, 2)),
116
        ];
117
    }
118
119
    /**
120
     * Converts an HSL hue to it's RGB value.
121
     *
122
     * Thanks to Easy RGB for this function (Hue_2_RGB).
123
     * http://www.easyrgb.com/index.php?X=MATH&$h=19#text19
124
     * http://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion
125
     *
126
     * @param float $p   Temporary based on whether the luminance is less than 0.5, and
127
     *                   calculated using the saturation and luminance values.
128
     * @param float $q
129
     * @param float $hue The hue (to be converted to an RGB value)  For red, add 1/3 to the hue, green
130
     *                   leave it alone, and blue you subtract 1/3 from the hue.
131
     *
132
     * @return mixed
133
     */
134 8
    private static function hueToRgb($p, $q, $hue): float
135
    {
136 8
        if ($hue < 0) {
137 5
            $hue += 1;
138
        }
139 8
        if ($hue > 1) {
140 4
            $hue -= 1;
141
        }
142
143 8
        if ($hue < 1 / 6) {
144 6
            return $p + ($q - $p) * 6 * $hue;
145
        }
146 8
        if ($hue < 1 / 2) {
147 8
            return $q;
148
        }
149 8
        if ($hue < 2 / 3) {
150 6
            return $p + ($q - $p) * (2 / 3 - $hue) * 6;
151
        }
152
153 8
        return $p;
154
    }
155
156
    /**
157
     * Converts an HSL color value to RGB.
158
     *
159
     * Conversion formula adapted from http://www.niwa.nu/2013/05/math-behind-colorspace-conversions-rgb-hsl/
160
     * and http://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion
161
     *
162
     * Assumes h, s, and l are in the set [0-1]
163
     *
164
     * @param float $hue
165
     * @param float $saturation
166
     * @param float $lightness
167
     *
168
     * @return array
169
     */
170 10
    private static function hslToRgb(float $hue, float $saturation, float $lightness): array
171
    {
172
        // If saturation is 0, the given color is grey and only
173
        // lightness is relevant.
174 10
        if ($saturation == 0) {
175 4
            $lightness = (int)ceil($lightness * 255);
176
177 4
            return ['r' => $lightness, 'g' => $lightness, 'b' => $lightness];
178
        }
179
180
        // Calculate some temporary variables to make the calculation easier
181 8
        $q = $lightness < 0.5
182 4
            ? $lightness * (1 + $saturation)
183 8
            : $lightness + $saturation - $lightness * $saturation;
184
185 8
        $p = 2 * $lightness - $q;
186
187
        // Run the calculated vars through hueToRgb to calculate the RGB value. Note that for the Red
188
        // value, we add a third (120 degrees), to adjust the hue to the correct section of the circle for red.
189
        // Similarity, for blue, we subtract 1/3.
190 8
        $red   = self::hueToRgb($p, $q, $hue + 1 / 3);
191 8
        $green = self::hueToRgb($p, $q, $hue);
192 8
        $blue  = self::hueToRgb($p, $q, $hue - 1 / 3);
193
194
        return [
195 8
            'r' => (int)round($red * 255, 2),
196 8
            'g' => (int)round(round($green, 2) * 255),
197 8
            'b' => (int)round($blue * 255, 2),
198
        ];
199
    }
200
201
    /**
202
     * Checks to see if body text on a background color is readable based on WCAG2 standards
203
     *
204
     * WCAG 2 level AA requires a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text.
205
     * Level AAA requires a contrast ratio of at least 7:1 for normal text and 4.5:1 for large text.
206
     *
207
     * Large text is defined as 14 point (typically 18.66px) and bold or larger, or 18 point (typically 24px) or larger.
208
     *
209
     * @param Color  $backgroundColor
210
     * @param string $level
211
     * @param int    $fontSize
212
     *
213
     * @return bool
214
     */
215 3
    public function isReadable(Color $backgroundColor, $level = 'AA', int $fontSize = 12): bool
216
    {
217 3
        $contrast = round($this->luminosityContrast($backgroundColor), 2);
218
219
        // Normal text
220 3
        if ($fontSize < 19) {
221 3
            return $contrast >= ($level === 'AA' ? 4.5 : 7.1);
222
        }
223
224 1
        return $contrast >= ($level === 'AA' ? 3.0 : 4.5);
225
    }
226
227
    /**
228
     * @return bool
229
     */
230 2
    public function isDark(): bool
231
    {
232 2
        return $this->getBrightness() < 136;
233
    }
234
235
    /**
236
     * @param Color $color
237
     *
238
     * @return bool
239
     */
240 2
    public function equals(Color $color): bool
241
    {
242 2
        return $this->getHex() == $color->getHex();
243
    }
244
245
    /**
246
     * Get's the luminosity contrast based on the sRGB color space
247
     * https://www.w3.org/TR/WCAG20/#relativeluminancedef
248
     *
249
     * @return float
250
     */
251 3
    public function getLuminosity(): float
252
    {
253 3
        $rSrgb = $this->colors['r'] / 255;
254 3
        $gSrgb = $this->colors['g'] / 255;
255 3
        $bSrgb = $this->colors['b'] / 255;
256
257 3
        $r = $rSrgb <= 0.03928 ? $rSrgb / 12.92 : pow(($rSrgb + 0.055) / 1.055, 2.4);
258 3
        $g = $gSrgb <= 0.03928 ? $gSrgb / 12.92 : pow(($gSrgb + 0.055) / 1.055, 2.4);
259 3
        $b = $bSrgb <= 0.03928 ? $bSrgb / 12.92 : pow(($bSrgb + 0.055) / 1.055, 2.4);
260
261 3
        return (0.2126 * $r) + (0.7152 * $g) + (0.0722 * $b);
262
    }
263
264
    /**
265
     * Based on the W3C working draft on accessibility's brightness formula
266
     * https://www.w3.org/TR/AERT#color-contrast
267
     *
268
     * @return float
269
     */
270 3
    public function getBrightness(): float
271
    {
272 3
        return (($this->colors['r'] * 299) + ($this->colors['g'] * 587) + ($this->colors['b'] * 114)) / 1000;
273
    }
274
275
    /**
276
     * @deprecated use `luminosityContrast` instead
277
     *
278
     * This works by summing up the differences between the three color components red, green and blue.
279
     * A value higher than 500 is recommended for good readability.
280
     *
281
     * @param Color $color
282
     *
283
     * @return int
284
     */
285 1
    public function colorDifference(Color $color): int
286
    {
287 1
        $color2 = $color->getRgb();
288
289 1
        return (int)abs($this->colors['r'] - $color2['r'])
290 1
            + (int)abs($this->colors['g'] - $color2['g'])
291 1
            + (int)abs($this->colors['b'] - $color2['b']);
292
    }
293
294
    /**
295
     * @deprecated use `luminosityContrast` instead
296
     *
297
     * This function tries to compare the brightness of the colors. A return value of
298
     * more than 125 is recommended. Combining it with the color difference above might make sense.
299
     *
300
     * @param Color $color
301
     *
302
     * @return float
303
     */
304 2
    public function brightnessDifference(Color $color): float
305
    {
306 2
        $difference = $this->getBrightness() - $color->getBrightness();
307
308 2
        if ($difference < 0) {
309 1
            $difference *= -1.0;
310
        }
311
312 2
        return $difference;
313
    }
314
315
    /**
316
     * Uses the luminosity to calculate the difference between the given colors.
317
     * The returned value should be larger than 4.5 for best readability.
318
     *
319
     * @param Color $color
320
     *
321
     * @return float
322
     */
323 3
    public function luminosityContrast(Color $color): float
324
    {
325 3
        $colorLuminosity1 = $this->getLuminosity() + 0.05;
326 3
        $colorLuminosity2 = $color->getLuminosity() + 0.05;
327
328 3
        return max($colorLuminosity1, $colorLuminosity2) / min($colorLuminosity1, $colorLuminosity2);
329
    }
330
331
    /**
332
     * @param Color $color
333
     * @param float $percentage A number between 0 and 1
334
     *
335
     * @return Color
336
     */
337 1
    public function mix(Color $color, float $percentage = 50)
338
    {
339 1
        $steps     = 2;
340 1
        $weight    = ($percentage * $steps) / 100;
341 1
        $mixWeight = $steps - $weight;
342
343
        $mixedColor = [
344 1
            'r' => (int)ceil((($this->getRgb()['r'] * $weight) + ($color->getRgb()['r'] * $mixWeight)) / $steps),
345 1
            'g' => (int)ceil((($this->getRgb()['g'] * $weight) + ($color->getRgb()['g'] * $mixWeight)) / $steps),
346 1
            'b' => (int)ceil((($this->getRgb()['b'] * $weight) + ($color->getRgb()['b'] * $mixWeight)) / $steps),
347
        ];
348
349 1
        return new self(...array_values($mixedColor));
350
    }
351
352
    /**
353
     * @return string
354
     */
355 16
    public function getHex(): string
356
    {
357 16
        return sprintf(
358 16
            '%02X%02X%02X',
359 16
            $this->colors['r'],
360 16
            $this->colors['g'],
361 16
            $this->colors['b']
362
        );
363
    }
364
365
    /**
366
     * @return int[]
367
     */
368 7
    public function getRgb(): array
369
    {
370 7
        return $this->colors;
371
    }
372
373
    /**
374
     * Converts RGB color to HSL color
375
     * @see http://en.wikipedia.org/wiki/HSL_and_HSV#Hue_and_chroma
376
     *
377
     * @return array
378
     */
379 12
    public function getHsl(): array
380
    {
381 12
        $red   = $this->colors['r'] / 255;
382 12
        $green = $this->colors['g'] / 255;
383 12
        $blue  = $this->colors['b'] / 255;
384
385
        // Determine lowest & highest value and chroma
386 12
        $max    = max($red, $green, $blue);
387 12
        $min    = min($red, $green, $blue);
388 12
        $chroma = $max - $min;
389
390
        // Calculate Luminosity
391 12
        $lightness = ($max + $min) / 2;
392
393
        // If chroma is 0, the given color is grey
394
        // therefore hue and saturation are set to 0
395 12
        if ($chroma == 0) {
396 5
            return ['h' => 0, 's' => 0, 'l' => $lightness];
397
        }
398
399 10
        $saturation = $lightness > 0.5
400 2
            ? $chroma / (2 - $max - $min)
401 10
            : $chroma / ($max + $min);
402
403
        switch ($max) {
404 10
            case $red:
405 3
                $hue = ($green - $blue) / $chroma + ($green < $blue ? 6 : 0);
406 3
                break;
407
408 10
            case $green:
409 2
                $hue = ($blue - $red) / $chroma + 2;
410 2
                break;
411
412 8
            case $blue:
413
            default:
414 8
                $hue = ($red - $green) / $chroma + 4;
415 8
                break;
416
        }
417
418 10
        $hue /= 6;
419
420
        // Return HSL Color as array
421
        return [
422 10
            'h' => $hue,
423 10
            's' => $saturation,
424 10
            'l' => $lightness,
425
        ];
426
    }
427
428
    /**
429
     * @param string $default
430
     *
431
     * @return Color
432
     */
433 1
    public function getMatchingTextColor($default = 'CCCCCC')
434
    {
435
        // Always set black's matching text color to the default
436 1
        if ($this->getHsl()['l'] == 0) {
437 1
            return self::fromHex($default);
438
        }
439
440 1
        if ($this->isDark()) {
441 1
            $color = $this->lighten(125);
442 1
            $count = 1;
443
            do {
444 1
                $color = $color->lighten(20);
445 1
                $count++;
446 1
            } while (!$this->isReadable($color) && $count < 100);
447
448 1
            return $color;
449
        }
450
451 1
        $color = $this->darken(100);
452 1
        $count = 1;
453
        do {
454 1
            $color = $color->darken(10);
455 1
            $count++;
456 1
        } while (!$this->isReadable($color) && $count < 100);
457
458 1
        return $color;
459
    }
460
461
    /**
462
     * @param float $percentage
463
     *
464
     * @return Color
465
     */
466 5
    public function darken(float $percentage): Color
467
    {
468 5
        $colors      = $this->getHsl();
469 5
        $colors['l'] -= $colors['l'] * ($percentage / 100);
470
471 5
        $darkerColor = self::hslToRgb($colors['h'], $colors['s'], max(round($colors['l'], 5), 0));
472
473 5
        return new self(...array_values($darkerColor));
474
    }
475
476
    /**
477
     * @param float $percentage
478
     *
479
     * @return Color
480
     */
481 3
    public function lighten(float $percentage): Color
482
    {
483 3
        $colors      = $this->getHsl();
484 3
        $colors['l'] += $colors['l'] * ($percentage / 100);
485
486 3
        $lighterColor = self::hslToRgb($colors['h'], $colors['s'], min(round($colors['l'], 5), 1));
487
488 3
        return new self(...array_values($lighterColor));
489
    }
490
491
    /**
492
     * @param float $degrees
493
     *
494
     * @return Color
495
     */
496 5
    public function adjustHue(float $degrees = 30): Color
497
    {
498 5
        if (!Validator::isValidAdjustment($degrees)) {
499 1
            throw new ColorException('You must specify a proper value between 360 and -360');
500
        }
501
502 4
        $colors = $this->getHsl();
503
504
        // Convert the hue to degrees and add the adjustment
505 4
        $hue = ($colors['h'] * 360) + $degrees;
506
507 4
        if ($hue >= 360) {
508 3
            $hue -= 360;
509 4
        } elseif ($hue < 0) {
510 1
            $hue += 360;
511
        }
512
513 4
        $adjustedColor = self::hslToRgb($hue / 360, $colors['s'], $colors['l']);
514
515 4
        return new self(...array_values($adjustedColor));
516
    }
517
518
    /**
519
     * @return string
520
     */
521 4
    public function __toString(): string
522
    {
523 4
        return $this->getHex();
524
    }
525
526
    /**
527
     * @return array
528
     */
529 1
    public function jsonSerialize(): array
530
    {
531 1
        return $this->colors;
532
    }
533
}
534