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