Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
70 / 70
100.00% covered (success)
100.00%
16 / 16
CRAP
100.00% covered (success)
100.00%
1 / 1
Oklab
100.00% covered (success)
100.00%
70 / 70
100.00% covered (success)
100.00%
16 / 16
36
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 setL
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 setA
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 setB
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 setAlpha
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getL
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getA
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getB
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAlpha
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasAlpha
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 toRgb
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
 toOklch
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 toArray
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 render
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
8
 __toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 channels
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Color\Color;
16
17use OutOfRangeException;
18
19/**
20 * Pop Color Oklab color class
21 *
22 * @category   Pop
23 * @package    Pop\Color
24 * @author     Nick Sagona, III <nick@popphp.org>
25 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
26 * @license    https://www.popphp.org/license     New BSD License
27 * @version    2.0.0
28 */
29class Oklab extends AbstractColor
30{
31
32    /**
33     * L value
34     * @var float
35     */
36    protected float $l = 0;
37
38    /**
39     * A value
40     * @var float
41     */
42    protected float $a = 0;
43
44    /**
45     * B value
46     * @var float
47     */
48    protected float $b = 0;
49
50    /**
51     * Alpha value
52     * @var ?float
53     */
54    protected ?float $alpha = null;
55
56    /**
57     * Constructor
58     *
59     * Instantiate the Oklab color object
60     *
61     * @param float|string      $l
62     * @param float|string      $a
63     * @param float|string      $b
64     * @param float|string|null $alpha
65     */
66    public function __construct(float|string $l, float|string $a, float|string $b, float|string|null $alpha = null)
67    {
68        $this->setL($l);
69        $this->setA($a);
70        $this->setB($b);
71        if ($alpha !== null) {
72            $this->setAlpha($alpha);
73        }
74    }
75
76    /**
77     * Set L value
78     *
79     * @param  float|string $l
80     * @throws OutOfRangeException
81     * @return self
82     */
83    public function setL(float|string $l): self
84    {
85        $l = (float)$l;
86        if (($l > 1) || ($l < 0)) {
87            throw new OutOfRangeException('Error: The value of $l must be between 0 and 1.');
88        }
89        $this->l = $l;
90        return $this;
91    }
92
93    /**
94     * Set A value
95     *
96     * @param  float|string $a
97     * @throws OutOfRangeException
98     * @return self
99     */
100    public function setA(float|string $a): self
101    {
102        $a = (float)$a;
103        if (($a > 0.5) || ($a < -0.5)) {
104            throw new OutOfRangeException('Error: The value of $a must be between -0.5 and 0.5.');
105        }
106        $this->a = $a;
107        return $this;
108    }
109
110    /**
111     * Set B value
112     *
113     * @param  float|string $b
114     * @throws OutOfRangeException
115     * @return self
116     */
117    public function setB(float|string $b): self
118    {
119        $b = (float)$b;
120        if (($b > 0.5) || ($b < -0.5)) {
121            throw new OutOfRangeException('Error: The value of $b must be between -0.5 and 0.5.');
122        }
123        $this->b = $b;
124        return $this;
125    }
126
127    /**
128     * Set alpha value
129     *
130     * @param  float|string $alpha
131     * @throws OutOfRangeException
132     * @return self
133     */
134    public function setAlpha(float|string $alpha): self
135    {
136        $alpha = (float)$alpha;
137        if (($alpha > 1) || ($alpha < 0)) {
138            throw new OutOfRangeException('Error: The value of $alpha must be between 0 and 1.');
139        }
140        $this->alpha = $alpha;
141        return $this;
142    }
143
144    /**
145     * Get L value
146     *
147     * @return float
148     */
149    public function getL(): float
150    {
151        return $this->l;
152    }
153
154    /**
155     * Get A value
156     *
157     * @return float
158     */
159    public function getA(): float
160    {
161        return $this->a;
162    }
163
164    /**
165     * Get B value
166     *
167     * @return float
168     */
169    public function getB(): float
170    {
171        return $this->b;
172    }
173
174    /**
175     * Get alpha value
176     *
177     * @return float|null
178     */
179    public function getAlpha(): float|null
180    {
181        return $this->alpha;
182    }
183
184    /**
185     * Determine if the color object has an alpha value
186     *
187     * @return bool
188     */
189    public function hasAlpha(): bool
190    {
191        return ($this->alpha !== null);
192    }
193
194    /**
195     * Convert to RGB
196     *
197     * @return Rgb
198     */
199    public function toRgb(): Rgb
200    {
201        $l_ = $this->l + (0.3963377774 * $this->a) + (0.2158037573 * $this->b);
202        $m_ = $this->l - (0.1055613458 * $this->a) - (0.0638541728 * $this->b);
203        $s_ = $this->l - (0.0894841775 * $this->a) - (1.2914855480 * $this->b);
204
205        $l = $l_ ** 3;
206        $m = $m_ ** 3;
207        $s = $s_ ** 3;
208
209        $lr =  4.0767416621 * $l - 3.3077115913 * $m + 0.2309699292 * $s;
210        $lg = -1.2684380046 * $l + 2.6097574011 * $m - 0.3413193965 * $s;
211        $lb = -0.0041960863 * $l - 0.7034186147 * $m + 1.7076147010 * $s;
212
213        $r = (int)max(0, min(255, round(Rgb::delinearize($lr) * 255)));
214        $g = (int)max(0, min(255, round(Rgb::delinearize($lg) * 255)));
215        $b = (int)max(0, min(255, round(Rgb::delinearize($lb) * 255)));
216
217        return new Rgb($r, $g, $b, $this->alpha);
218    }
219
220    /**
221     * Convert to Oklch
222     *
223     * @return Oklch
224     */
225    public function toOklch(): Oklch
226    {
227        $c = sqrt(($this->a * $this->a) + ($this->b * $this->b));
228        $h = rad2deg(atan2($this->b, $this->a));
229        if ($h < 0) {
230            $h += 360;
231        }
232
233        return new Oklch(round($this->l, 4), round($c, 4), (int)round($h), $this->alpha);
234    }
235
236    /**
237     * Convert to array
238     *
239     * @param  bool $assoc
240     * @return array
241     */
242    public function toArray(bool $assoc = true): array
243    {
244        $oklab = [];
245
246        if ($assoc) {
247            $oklab['l'] = $this->l;
248            $oklab['a'] = $this->a;
249            $oklab['b'] = $this->b;
250            if ($this->alpha !== null) {
251                $oklab['alpha'] = $this->alpha;
252            }
253        } else {
254            $oklab[] = $this->l;
255            $oklab[] = $this->a;
256            $oklab[] = $this->b;
257            if ($this->alpha !== null) {
258                $oklab[] = $this->alpha;
259            }
260        }
261
262        return $oklab;
263    }
264
265    /**
266     * Convert to readable string
267     *
268     * @param  ?string $format
269     * @return string
270     */
271    public function render(?string $format = null): string
272    {
273        return match ($format) {
274            self::COMMA => $this->l . ', ' . $this->a . ', ' . $this->b .
275                (($this->alpha !== null) ? ', ' . $this->alpha : ''),
276            self::CSS => 'oklab(' . $this->l . ' ' . $this->a . ' ' . $this->b .
277                (($this->alpha !== null) ? ' / ' . $this->alpha : '') . ')',
278            self::PERCENT => $this->toRgb()->render(self::PERCENT),
279            default => $this->l . ' ' . $this->a . ' ' . $this->b .
280                (($this->alpha !== null) ? ' ' . $this->alpha : ''),
281        };
282    }
283
284    /**
285     * Return CSS-formatted string
286     *
287     * @return string
288     */
289    public function __toString(): string
290    {
291        return $this->render(self::CSS);
292    }
293
294    /**
295     * Get the valid channel names
296     *
297     * @return array
298     */
299    protected function channels(): array
300    {
301        return ['l', 'a', 'b', 'alpha'];
302    }
303
304}