1<?php
2
3namespace LesserPHP\Functions;
4
5use Exception;
6use LesserPHP\Utils\Asserts;
7use LesserPHP\Utils\Color;
8use LesserPHP\Utils\Util;
9
10/**
11 * Implements the Color Operation functions for LESS
12 *
13 * @todo inheritance from ColorChannels is only until we figure out how the alpha() method should work
14 * @link https://lesscss.org/functions/#color-operations
15 */
16class ColorOperation extends ColorChannels
17{
18    /** @inheritdoc */
19    public function getFunctions(): array
20    {
21        return [
22            'saturate' => [$this, 'saturate'],
23            'desaturate' => [$this, 'desaturate'],
24            'lighten' => [$this, 'lighten'],
25            'darken' => [$this, 'darken'],
26            'fadein' => [$this, 'fadein'],
27            'fadeout' => [$this, 'fadeout'],
28            'fade' => [$this, 'fade'],
29            'spin' => [$this, 'spin'],
30            'mix' => [$this, 'mix'],
31            'tint' => [$this, 'tint'],
32            'shade' => [$this, 'shade'],
33            //'greyscale' => [$this, 'greyscale'],
34            'contrast' => [$this, 'contrast'],
35        ];
36    }
37
38
39    /**
40     * Increase the saturation of a color in the HSL color space by an absolute amount
41     *
42     * @link https://lesscss.org/functions/#color-operations-saturate
43     * @throws Exception
44     */
45    public function saturate(array $args): array
46    {
47        [$color, $delta] = $this->colorArgs($args);
48
49        $hsl = Color::toHSL($color);
50        $hsl[2] = Util::clamp($hsl[2] + $delta, 100);
51        return Color::toRGB($hsl);
52    }
53
54    /**
55     * Decrease the saturation of a color in the HSL color space by an absolute amount
56     *
57     * @link https://lesscss.org/functions/#color-operations-desaturate
58     * @throws Exception
59     */
60    public function desaturate(array $args): array
61    {
62        [$color, $delta] = $this->colorArgs($args);
63
64        $hsl = Color::toHSL($color);
65        $hsl[2] = Util::clamp($hsl[2] - $delta, 100);
66        return Color::toRGB($hsl);
67    }
68
69    /**
70     * Increase the lightness of a color in the HSL color space by an absolute amount
71     *
72     * @link https://lesscss.org/functions/#color-operations-lighten
73     * @throws Exception
74     */
75    public function lighten(array $args): array
76    {
77        [$color, $delta] = $this->colorArgs($args);
78
79        $hsl = Color::toHSL($color);
80        $hsl[3] = Util::clamp($hsl[3] + $delta, 100);
81        return Color::toRGB($hsl);
82    }
83
84    /**
85     * Decrease the lightness of a color in the HSL color space by an absolute amount
86     *
87     * @link https://lesscss.org/functions/#color-operations-darken
88     * @throws Exception
89     */
90    public function darken(array $args): array
91    {
92        [$color, $delta] = $this->colorArgs($args);
93
94        $hsl = Color::toHSL($color);
95        $hsl[3] = Util::clamp($hsl[3] - $delta, 100);
96        return Color::toRGB($hsl);
97    }
98
99    /**
100     * Decrease the transparency (or increase the opacity) of a color, making it more opaque
101     *
102     * @link https://lesscss.org/functions/#color-operations-fadein
103     * @throws Exception
104     */
105    public function fadein(array $args): array
106    {
107        [$color, $delta] = $this->colorArgs($args);
108        $color[4] = Util::clamp(($color[4] ?? 1) + $delta / 100);
109        return $color;
110    }
111
112    /**
113     * Increase the transparency (or decrease the opacity) of a color, making it less opaque
114     *
115     * @link https://lesscss.org/functions/#color-operations-fadeout
116     * @throws Exception
117     */
118    public function fadeout(array $args): array
119    {
120        [$color, $delta] = $this->colorArgs($args);
121        $color[4] = Util::clamp(($color[4] ?? 1) - $delta / 100);
122        return $color;
123    }
124
125    /**
126     * Set the absolute opacity of a color.
127     * Can be applied to colors whether they already have an opacity value or not.
128     *
129     * @link https://lesscss.org/functions/#color-operations-fade
130     * @throws Exception
131     */
132    public function fade(array $args): array
133    {
134        [$color, $alpha] = $this->colorArgs($args);
135        $color[4] = Util::clamp($alpha / 100.0);
136        return $color;
137    }
138
139    /**
140     * Rotate the hue angle of a color in either direction
141     *
142     * @link https://lesscss.org/functions/#color-operations-spin
143     * @throws Exception
144     */
145    public function spin(array $args): array
146    {
147        [$color, $delta] = $this->colorArgs($args);
148
149        $hsl = Color::toHSL($color);
150
151        $hsl[1] = $hsl[1] + $delta % 360;
152        if ($hsl[1] < 0) $hsl[1] += 360;
153
154        return Color::toRGB($hsl);
155    }
156
157    /**
158     * mixes two colors by weight
159     * mix(@color1, @color2, [@weight: 50%]);
160     *
161     * @link https://lesscss.org/functions/#color-operations-mix
162     * @throws Exception
163     */
164    public function mix(array $args): array
165    {
166        if ($args[0] != 'list' || count($args[2]) < 2) {
167            throw new Exception('mix expects (color1, color2, weight)');
168        }
169
170        [$first, $second] = $args[2];
171        $first = Asserts::assertColor($first);
172        $second = Asserts::assertColor($second);
173
174        $first_a = $this->alpha($first);
175        $second_a = $this->alpha($second);
176
177        if (isset($args[2][2])) {
178            $weight = $args[2][2][1] / 100.0;
179        } else {
180            $weight = 0.5;
181        }
182
183        $w = $weight * 2 - 1;
184        $a = $first_a - $second_a;
185
186        $w1 = (($w * $a == -1 ? $w : ($w + $a) / (1 + $w * $a)) + 1) / 2.0;
187        $w2 = 1.0 - $w1;
188
189        $new = [
190            'color',
191            $w1 * $first[1] + $w2 * $second[1],
192            $w1 * $first[2] + $w2 * $second[2],
193            $w1 * $first[3] + $w2 * $second[3],
194        ];
195
196        if ($first_a != 1.0 || $second_a != 1.0) {
197            $new[] = $first_a * $weight + $second_a * ($weight - 1);
198        }
199
200        return Color::fixColor($new);
201    }
202
203    /**
204     * Mix color with white in variable proportion.
205     *
206     * It is the same as calling `mix(#ffffff, @color, @weight)`.
207     *
208     *     tint(@color, [@weight: 50%]);
209     *
210     * @link https://lesscss.org/functions/#color-operations-tint
211     * @throws Exception
212     * @return array Color
213     */
214    public function tint(array $args): array
215    {
216        $white = ['color', 255, 255, 255];
217        if ($args[0] == 'color') {
218            return $this->mix(['list', ',', [$white, $args]]);
219        } elseif ($args[0] == 'list' && count($args[2]) == 2) {
220            return $this->mix([$args[0], $args[1], [$white, $args[2][0], $args[2][1]]]);
221        } else {
222            throw new Exception('tint expects (color, weight)');
223        }
224    }
225
226    /**
227     * Mix color with black in variable proportion.
228     *
229     * It is the same as calling `mix(#000000, @color, @weight)`
230     *
231     *     shade(@color, [@weight: 50%]);
232     *
233     * @link http://lesscss.org/functions/#color-operations-shade
234     * @return array Color
235     * @throws Exception
236     */
237    public function shade(array $args): array
238    {
239        $black = ['color', 0, 0, 0];
240        if ($args[0] == 'color') {
241            return $this->mix(['list', ',', [$black, $args]]);
242        } elseif ($args[0] == 'list' && count($args[2]) == 2) {
243            return $this->mix([$args[0], $args[1], [$black, $args[2][0], $args[2][1]]]);
244        } else {
245            throw new Exception('shade expects (color, weight)');
246        }
247    }
248
249    // greyscale is missing
250
251    /**
252     * Choose which of two colors provides the greatest contrast with another
253     *
254     * @link https://lesscss.org/functions/#color-operations-contrast
255     * @throws Exception
256     */
257    public function contrast(array $args): array
258    {
259        $darkColor = ['color', 0, 0, 0];
260        $lightColor = ['color', 255, 255, 255];
261        $threshold = 0.43;
262
263        if ($args[0] == 'list') {
264            $inputColor = (isset($args[2][0])) ? Asserts::assertColor($args[2][0]) : $lightColor;
265            $darkColor = (isset($args[2][1])) ? Asserts::assertColor($args[2][1]) : $darkColor;
266            $lightColor = (isset($args[2][2])) ? Asserts::assertColor($args[2][2]) : $lightColor;
267            if (isset($args[2][3])) {
268                if (isset($args[2][3][2]) && $args[2][3][2] == '%') {
269                    $args[2][3][1] /= 100;
270                    unset($args[2][3][2]);
271                }
272                $threshold = Asserts::assertNumber($args[2][3]);
273            }
274        } else {
275            $inputColor = Asserts::assertColor($args);
276        }
277
278        $inputColor = Color::coerceColor($inputColor);
279        $darkColor = Color::coerceColor($darkColor);
280        $lightColor = Color::coerceColor($lightColor);
281
282        //Figure out which is actually light and dark!
283        if (Color::toLuma($darkColor) > Color::toLuma($lightColor)) {
284            $t = $lightColor;
285            $lightColor = $darkColor;
286            $darkColor = $t;
287        }
288
289        $inputColor_alpha = $this->alpha($inputColor);
290        if ((Color::toLuma($inputColor) * $inputColor_alpha) < $threshold) {
291            return $lightColor;
292        }
293        return $darkColor;
294    }
295
296
297    /**
298     * Helper function to get arguments for color manipulation functions.
299     * takes a list that contains a color like thing and a percentage
300     *
301     * @fixme explanation needs to be improved
302     * @throws Exception
303     */
304    protected function colorArgs(array $args): array
305    {
306        if ($args[0] != 'list' || count($args[2]) < 2) {
307            return [['color', 0, 0, 0], 0];
308        }
309        [$color, $delta] = $args[2];
310        $color = Asserts::assertColor($color);
311        $delta = floatval($delta[1]);
312
313        return [$color, $delta];
314    }
315}
316