1<?php
2
3namespace LesserPHP\Utils;
4
5use LesserPHP\Constants;
6
7/**
8 * Color handling utilities
9 */
10class Color
11{
12    /**
13     * coerce a value for use in color operation
14     * returns null if the value can't be used in color operations
15     */
16    public static function coerceColor(array $value): ?array
17    {
18        switch ($value[0]) {
19            case 'color':
20                return $value;
21            case 'raw_color':
22                $c = ['color', 0, 0, 0];
23                $colorStr = substr($value[1], 1);
24                $num = hexdec($colorStr);
25                $width = strlen($colorStr) == 3 ? 16 : 256;
26
27                for ($i = 3; $i > 0; $i--) { // 3 2 1
28                    $t = (int) $num % $width;
29                    $num /= $width;
30
31                    $c[$i] = $t * (256 / $width) + $t * floor(16 / $width);
32                }
33
34                return $c;
35            case 'keyword':
36                $name = $value[1];
37                if (isset(Constants::CSS_COLORS[$name])) {
38                    $rgba = explode(',', Constants::CSS_COLORS[$name]);
39
40                    if (isset($rgba[3]))
41                        return ['color', $rgba[0], $rgba[1], $rgba[2], $rgba[3]];
42
43                    return ['color', $rgba[0], $rgba[1], $rgba[2]];
44                }
45                return null;
46        }
47        return null;
48    }
49
50    /**
51     * Calculate the perceptual brightness of a color object
52     */
53    public static function toLuma(array $color): float
54    {
55        [, $r, $g, $b] = Color::coerceColor($color);
56
57        $r = $r / 255;
58        $g = $g / 255;
59        $b = $b / 255;
60
61        $r = ($r <= 0.03928) ? $r / 12.92 : (($r + 0.055) / 1.055) ** 2.4;
62        $g = ($g <= 0.03928) ? $g / 12.92 : (($g + 0.055) / 1.055) ** 2.4;
63        $b = ($b <= 0.03928) ? $b / 12.92 : (($b + 0.055) / 1.055) ** 2.4;
64
65        return (0.2126 * $r) + (0.7152 * $g) + (0.0722 * $b);
66    }
67
68    /**
69     * Convert a color to HSL color space
70     */
71    public static function toHSL(array $color): array
72    {
73        if ($color[0] == 'hsl') return $color;
74
75        $r = $color[1] / 255;
76        $g = $color[2] / 255;
77        $b = $color[3] / 255;
78
79        $min = min($r, $g, $b);
80        $max = max($r, $g, $b);
81
82        $L = ($min + $max) / 2;
83        if ($min == $max) {
84            $S = $H = 0;
85        } else {
86            if ($L < 0.5) {
87                $S = ($max - $min) / ($max + $min);
88            } else {
89                $S = ($max - $min) / (2.0 - $max - $min);
90            }
91
92            if ($r == $max) {
93                $H = ($g - $b) / ($max - $min);
94            } elseif ($g == $max) {
95                $H = 2.0 + ($b - $r) / ($max - $min);
96            } elseif ($b == $max) {
97                $H = 4.0 + ($r - $g) / ($max - $min);
98            } else {
99                $H = 0;
100            }
101        }
102
103        $out = [
104            'hsl',
105            ($H < 0 ? $H + 6 : $H) * 60,
106            $S * 100,
107            $L * 100,
108        ];
109
110        if (count($color) > 4) $out[] = $color[4]; // copy alpha
111        return $out;
112    }
113
114
115    /**
116     * Converts a hsl array into a color value in rgb.
117     * Expects H to be in range of 0 to 360, S and L in 0 to 100
118     */
119    public static function toRGB(array $color): array
120    {
121        if ($color[0] == 'color') return $color;
122
123        $H = $color[1] / 360;
124        $S = $color[2] / 100;
125        $L = $color[3] / 100;
126
127        if ($S == 0) {
128            $r = $g = $b = $L;
129        } else {
130            $temp2 = $L < 0.5 ?
131                $L * (1.0 + $S) :
132                $L + $S - $L * $S;
133
134            $temp1 = 2.0 * $L - $temp2;
135
136            $r = self::calculateRGBComponent($H + 1 / 3, $temp1, $temp2);
137            $g = self::calculateRGBComponent($H, $temp1, $temp2);
138            $b = self::calculateRGBComponent($H - 1 / 3, $temp1, $temp2);
139        }
140
141        // $out = array('color', round($r*255), round($g*255), round($b*255));
142        $out = ['color', $r * 255, $g * 255, $b * 255];
143        if (count($color) > 4) $out[] = $color[4]; // copy alpha
144        return $out;
145    }
146
147
148    /**
149     * make sure a color's components don't go out of bounds
150     */
151    public static function fixColor(array $c): array
152    {
153        foreach (range(1, 3) as $i) {
154            if ($c[$i] < 0) $c[$i] = 0;
155            if ($c[$i] > 255) $c[$i] = 255;
156        }
157
158        return $c;
159    }
160
161    /**
162     * Helper function for the HSL to RGB conversion process.
163     *
164     * This function normalizes the input component of the HSL color and determines the RGB
165     * value based on the HSL values.
166     *
167     * @param float $comp The component of the HSL color to be normalized and converted.
168     * @param float $temp1 The first temporary variable used in the conversion process
169     * @param float $temp2 The second temporary variable used in the conversion process
170     *
171     * @return float The calculated RGB value as percentage of the maximum value (255)
172     */
173    protected static function calculateRGBComponent(float $comp, float $temp1, float $temp2): float
174    {
175        // Normalize the component value to be within the range [0, 1]
176        if ($comp < 0) $comp += 1.0;
177        elseif ($comp > 1) $comp -= 1.0;
178
179        // Determine the return value based on the value of the component
180        if (6 * $comp < 1) return $temp1 + ($temp2 - $temp1) * 6 * $comp;
181        if (2 * $comp < 1) return $temp2;
182        if (3 * $comp < 2) return $temp1 + ($temp2 - $temp1) * ((2 / 3) - $comp) * 6;
183
184        // Fallback return value, represents the case where the saturation of the color is zero
185        return $temp1;
186    }
187}
188