Added colour handling (tests soon).

This commit is contained in:
flash 2023-01-02 03:23:48 +00:00
parent a0b3a08d7f
commit 3f0504b1fd
5 changed files with 628 additions and 0 deletions

176
src/Colour/Colour.php Normal file
View file

@ -0,0 +1,176 @@
<?php
namespace Index\Colour;
use Stringable;
abstract class Colour implements Stringable {
public abstract function getRed(): int;
public abstract function getGreen(): int;
public abstract function getBlue(): int;
public abstract function getAlpha(): float;
public abstract function shouldInherit(): bool;
public abstract function __toString(): string;
private const READABILITY_THRESHOLD = 186;
private const LUMINANCE_WEIGHT_RED = .299;
private const LUMINANCE_WEIGHT_GREEN = .587;
private const LUMINANCE_WEIGHT_BLUE = .114;
public function getLuminance(): float {
return self::LUMINANCE_WEIGHT_RED * $this->getRed()
+ self::LUMINANCE_WEIGHT_GREEN * $this->getGreen()
+ self::LUMINANCE_WEIGHT_BLUE * $this->getBlue();
}
public function isBright(): bool {
return $this->getLuminance() > self::READABILITY_THRESHOLD;
}
public function isDark(): bool {
return $this->getLuminance() <= self::READABILITY_THRESHOLD;
}
private static ColourNull $none;
public static function none(): Colour {
return self::$none;
}
private const MSZ_INHERIT = 0x40000000;
public static function fromMisuzu(int $raw): Colour {
if($raw & self::MSZ_INHERIT)
return self::$none;
return ColourRGB::fromRawRGB($raw);
}
public static function parse(?string $value): Colour {
if($value === null)
return self::$none;
$value = strtolower(trim($value));
if($value === '' || $value === 'inherit')
return self::$none;
if(ctype_alpha($value))
return new ColourNamed($value);
if($value[0] === '#') {
$value = substr($value, 1);
if(ctype_xdigit($value)) {
$length = strlen($value);
if($length === 3) {
$value = $value[0] . $value[0] . $value[1] . $value[1] . $value[2] . $value[2];
$length *= 2;
} elseif($length === 4) {
$value = $value[0] . $value[0] . $value[1] . $value[1] . $value[2] . $value[2] . $value[3] . $value[3];
$length *= 2;
}
if($length === 6)
return ColourRGB::fromRawRGB(hexdec($value));
if($length === 8)
return ColourRGB::fromRawRGBA(hexdec($value));
}
return self::$none;
}
if(str_starts_with($value, 'rgb(') || str_starts_with($value, 'rgba(')) {
$open = strpos($value, '(');
if($open === false)
return self::$none;
$close = strpos($value, ')', $open);
if($close === false)
return self::$none;
$open += 1;
$value = substr($value, $open, $close - $open);
if(strpos($value, ',') === false) {
// todo: support comma-less syntax
return self::$none;
} else {
$value = explode(',', $value, 4);
$parts = count($value);
if($parts !== 3 && $parts !== 4)
return self::$none;
$value[0] = (int)trim($value[0]);
$value[1] = (int)trim($value[1]);
$value[2] = (int)trim($value[2]);
$value[3] = (float)trim($value[3] ?? '1');
}
return new ColourRGB(...$value);
}
if(str_starts_with($value, 'hsl(') || str_starts_with($value, 'hsla(')) {
$open = strpos($value, '(');
if($open === false)
return self::$none;
$close = strpos($value, ')', $open);
if($close === false)
return self::$none;
$open += 1;
$value = substr($value, $open, $close - $open);
if(strpos($value, ',') === false) {
// todo: support comma-less syntax
return self::$none;
} else {
$value = explode(',', $value, 4);
$parts = count($value);
if($parts !== 3 && $parts !== 4)
return self::$none;
if(!str_ends_with($value[1], '%') || !str_ends_with($value[2], '%'))
return self::$none;
$value[1] = (float)substr($value[1], 0, -1);
$value[2] = (float)substr($value[2], 0, -1);
if($value[1] < 0 || $value[1] > 100 || $value[2] < 0 || $value[2] > 100)
return self::$none;
$value[1] /= 100.0;
$value[2] /= 100.0;
if(ctype_digit($value[0])) {
$value[0] = (float)$value[0];
} else {
if(str_ends_with($value[0], 'deg')) {
$value[0] = (float)substr($value[0], 0, -3);
} elseif(str_ends_with($value[0], 'grad')) {
$value[0] = 0.9 * (float)substr($value[0], 0, -4);
} elseif(str_ends_with($value[0], 'rad')) {
$value[0] = rad2deg((float)substr($value[0], 0, -3));
} elseif(str_ends_with($value[0], 'turn')) {
$value[0] = 360.0 * ((float)substr($value[0], 0, -4));
} else {
return self::$none;
}
}
$value[3] = (float)trim($value[3] ?? '1');
}
return new ColourHSL(...$value);
}
return self::$none;
}
public static function init(): void {
self::$none = new ColourNull;
}
}
Colour::init();

127
src/Colour/ColourHSL.php Normal file
View file

@ -0,0 +1,127 @@
<?php
namespace Index\Colour;
class ColourHSL extends Colour {
private float $hue;
private float $saturation;
private float $lightness;
private float $alpha;
private int $red;
private int $green;
private int $blue;
public function __construct(float $hue, float $saturation, float $lightness, float $alpha) {
$hue %= 360;
$this->hue = $hue;
$this->saturation = max(0.0, min(1.0, $saturation));
$this->lightness = max(0.0, min(1.0, $lightness));
$this->alpha = max(0.0, min(1.0, $alpha));
$c = (1 - abs(2 * $lightness - 1)) * $saturation;
$x = $c * (1 - abs(fmod($hue / 60, 2) - 1));
$m = $lightness - ($c / 2);
$r = $g = $b = 0;
if($hue < 60) {
$r = $c;
$g = $x;
} elseif($hue < 120) {
$r = $x;
$g = $c;
} elseif($hue < 180) {
$g = $c;
$b = $x;
} elseif($hue < 240) {
$g = $x;
$b = $c;
} elseif($hue < 300) {
$r = $x;
$b = $c;
} else {
$r = $c;
$b = $x;
}
$this->red = (int)round(($r + $m) * 255);
$this->green = (int)round(($g + $m) * 255);
$this->blue = (int)round(($b + $m) * 255);
}
public function getRed(): int {
return $this->red;
}
public function getGreen(): int {
return $this->green;
}
public function getBlue(): int {
return $this->blue;
}
public function getHue(): float {
return $this->hue;
}
public function getSaturation(): float {
return $this->saturation;
}
public function getLightness(): float {
return $this->lightness;
}
public function getAlpha(): float {
return $this->alpha;
}
public function shouldInherit(): bool {
return false;
}
public function __toString(): string {
if($this->alpha < 1.0)
return sprintf('hsla(%.2Fdeg, %d%%, %d%%, %.3F)', $this->hue, $this->saturation * 100, $this->lightness * 100, $this->alpha);
return sprintf('hsl(%.2Fdeg, %d%%, %d%%)', $this->hue, $this->saturation * 100, $this->lightness * 100);
}
public static function convert(Colour $colour): ColourHSL {
if($colour instanceof ColourHSL)
return $colour;
$r = $colour->getRed() / 255.0;
$g = $colour->getGreen() / 255.0;
$b = $colour->getBlue() / 255.0;
$max = max($r, $g, $b);
$min = min($r, $g, $b);
$h = $s = 0;
$l = ($max + $min) / 2;
$d = $max - $min;
if($d <> 0) {
$s = $d / (1 - abs(2 * $l - 1));
if($max == $r) {
$h = 60 * fmod((($g - $b) / $d), 6);
if($b > $g)
$h += 360;
} elseif($max == $g) {
$h = 60 * (($b - $r) / $d + 2);
} else {
$h = 60 * (($r - $g) / $d + 4);
}
}
return new ColourHSL(
round($h, 3),
round($s, 3),
round($l, 3),
$colour->getAlpha()
);
}
}

217
src/Colour/ColourNamed.php Normal file
View file

@ -0,0 +1,217 @@
<?php
namespace Index\Colour;
class ColourNamed extends Colour {
private string $name;
private int $red = 0;
private int $green = 0;
private int $blue = 0;
private bool $transparent;
public function __construct(string $name) {
$this->name = strtolower($name);
$raw = self::COLOURS[$name] ?? -1;
$this->transparent = $raw === -1;
if($raw === -1) {
$this->transparent = true;
} else {
$this->transparent = false;
$this->red = ($raw >> 16) & 0xFF;
$this->green = ($raw >> 8) & 0xFF;
$this->blue = ($raw) & 0xFF;
}
}
public function getName(): string {
return $this->name;
}
public function isTransparent(): bool {
return $this->transparent;
}
public function getRed(): int {
return $this->red;
}
public function getGreen(): int {
return $this->green;
}
public function getBlue(): int {
return $this->blue;
}
public function getAlpha(): float {
return $this->transparent ? 0.0 : 1.0;;
}
public function shouldInherit(): bool {
return false;
}
public function __toString(): string {
return $this->name;
}
private const COLOURS = [
// CSS Level 1
'black' => 0x000000,
'silver' => 0xC0C0C0,
'gray' => 0x808080,
'white' => 0xFFFFFF,
'maroon' => 0x800000,
'red' => 0xFF0000,
'purple' => 0x800080,
'fuchsia' => 0xFF00FF,
'green' => 0x008000,
'lime' => 0x00FF00,
'olive' => 0x808000,
'yellow' => 0xFFFF00,
'navy' => 0x000080,
'blue' => 0x0000FF,
'teal' => 0x008080,
'aqua' => 0x00FFFF,
// CSS Level 2
'orange' => 0xFFA500,
// CSS Level 3
'aliceblue' => 0xF0F8FF,
'antiquewhite' => 0xFAEBD7,
'aquamarine' => 0x7FFFD4,
'azure' => 0xF0FFFF,
'beige' => 0xF5F5DC,
'bisque' => 0xFFE4C4,
'blanchedalmond' => 0xFFEBCD,
'blueviolet' => 0x8A2BE2,
'brown' => 0xA52A2A,
'burlywood' => 0xDEB887,
'cadetblue' => 0x5F9EA0,
'chartreuse' => 0x7FFF00,
'chocolate' => 0xD2691E,
'coral' => 0xFF7F50,
'cornflowerblue' => 0x6495ED,
'cornsilk' => 0xFFF8DC,
'crimson' => 0xDC143C,
'cyan' => 0x00FFFF,
'darkblue' => 0x00008B,
'darkcyan' => 0x008B8B,
'darkgoldenrod' => 0xB8860B,
'darkgray' => 0xA9A9A9,
'darkgreen' => 0x006400,
'darkgrey' => 0xA9A9A9,
'darkkhaki' => 0xBDB76B,
'darkmagenta' => 0x8B008B,
'darkolivegreen' => 0x556B2F,
'darkorange' => 0xFF8C00,
'darkorchid' => 0x9932CC,
'darkred' => 0x8B0000,
'darksalmon' => 0xE9967A,
'darkseagreen' => 0x8FBC8F,
'darkslateblue' => 0x483D8B,
'darkslategray' => 0x2F4F4F,
'darkslategrey' => 0x2F4F4F,
'darkturquoise' => 0x00CED1,
'darkviolet' => 0x9400D3,
'deeppink' => 0xFF1493,
'deepskyblue' => 0x00BFFF,
'dimgray' => 0x696969,
'dimgrey' => 0x696969,
'dodgerblue' => 0x1E90FF,
'firebrick' => 0xB22222,
'floralwhite' => 0xFFFAF0,
'forestgreen' => 0x228B22,
'gainsboro' => 0xDCDCDC,
'ghostwhite' => 0xF8F8FF,
'gold' => 0xFFD700,
'goldenrod' => 0xDAA520,
'greenyellow' => 0xADFF2F,
'grey' => 0x808080,
'honeydew' => 0xF0FFF0,
'hotpink' => 0xFF69B4,
'indianred' => 0xCD5C5C,
'indigo' => 0x4B0082,
'ivory' => 0xFFFFF0,
'khaki' => 0xF0E68C,
'lavender' => 0xE6E6FA,
'lavenderblush' => 0xFFF0F5,
'lawngreen' => 0x7CFC00,
'lemonchiffon' => 0xFFFACD,
'lightblue' => 0xADD8E6,
'lightcoral' => 0xF08080,
'lightcyan' => 0xE0FFFF,
'lightgoldenrodyellow' => 0xFAFAD2,
'lightgray' => 0xD3D3D3,
'lightgreen' => 0x90EE90,
'lightgrey' => 0xD3D3D3,
'lightpink' => 0xFFB6C1,
'lightsalmon' => 0xFFA07A,
'lightseagreen' => 0x20B2AA,
'lightskyblue' => 0x87CEFA,
'lightslategray' => 0x778899,
'lightslategrey' => 0x778899,
'lightsteelblue' => 0xB0C4DE,
'lightyellow' => 0xFFFFE0,
'limegreen' => 0x32CD32,
'linen' => 0xFAF0E6,
'magenta' => 0xFF00FF,
'mediumaquamarine' => 0x66CDAA,
'mediumblue' => 0x0000CD,
'mediumorchid' => 0xBA55D3,
'mediumpurple' => 0x9370DB,
'mediumseagreen' => 0x3CB371,
'mediumslateblue' => 0x7B68EE,
'mediumspringgreen' => 0x00FA9A,
'mediumturquoise' => 0x48D1CC,
'mediumvioletred' => 0xC71585,
'midnightblue' => 0x191970,
'mintcream' => 0xF5FFFA,
'mistyrose' => 0xFFE4E1,
'moccasin' => 0xFFE4B5,
'navajowhite' => 0xFFDEAD,
'oldlace' => 0xFDF5E6,
'olivedrab' => 0x6B8E23,
'orangered' => 0xFF4500,
'orchid' => 0xDA70D6,
'palegoldenrod' => 0xEEE8AA,
'palegreen' => 0x98FB98,
'paleturquoise' => 0xAFEEEE,
'palevioletred' => 0xDB7093,
'papayawhip' => 0xFFEFD5,
'peachpuff' => 0xFFDAB9,
'peru' => 0xCD853F,
'pink' => 0xFFC0CB,
'plum' => 0xDDA0DD,
'powderblue' => 0xB0E0E6,
'rosybrown' => 0xBC8F8F,
'royalblue' => 0x4169E1,
'saddlebrown' => 0x8B4513,
'salmon' => 0xFA8072,
'sandybrown' => 0xF4A460,
'seagreen' => 0x2E8B57,
'seashell' => 0xFFF5EE,
'sienna' => 0xA0522D,
'skyblue' => 0x87CEEB,
'slateblue' => 0x6A5ACD,
'slategray' => 0x708090,
'slategrey' => 0x708090,
'snow' => 0xFFFAFA,
'springgreen' => 0x00FF7F,
'steelblue' => 0x4682B4,
'tan' => 0xD2B48C,
'thistle' => 0xD8BFD8,
'tomato' => 0xFF6347,
'transparent' => -1,
'turquoise' => 0x40E0D0,
'violet' => 0xEE82EE,
'wheat' => 0xF5DEB3,
'whitesmoke' => 0xF5F5F5,
'yellowgreen' => 0x9ACD32,
// CSS Level 4
'rebeccapurple' => 0x663399,
];
}

28
src/Colour/ColourNull.php Normal file
View file

@ -0,0 +1,28 @@
<?php
namespace Index\Colour;
class ColourNull extends Colour {
public function getRed(): int {
return 0;
}
public function getGreen(): int {
return 0;
}
public function getBlue(): int {
return 0;
}
public function getAlpha(): float {
return 1;
}
public function shouldInherit(): bool {
return true;
}
public function __toString(): string {
return 'inherit';
}
}

80
src/Colour/ColourRGB.php Normal file
View file

@ -0,0 +1,80 @@
<?php
namespace Index\Colour;
class ColourRGB extends Colour {
private int $red;
private int $green;
private int $blue;
private float $alpha;
public function __construct(int $red, int $green, int $blue, float $alpha) {
$this->red = max(0, min(255, $red));
$this->green = max(0, min(255, $green));
$this->blue = max(0, min(255, $blue));
$this->alpha = max(0.0, min(1.0, $alpha));
}
public function getRed(): int {
return $this->red;
}
public function getGreen(): int {
return $this->green;
}
public function getBlue(): int {
return $this->blue;
}
public function getAlpha(): float {
return $this->alpha;
}
public function shouldInherit(): bool {
return false;
}
public function __toString(): string {
if($this->alpha < 1.0)
return sprintf('rgba(%d, %d, %d, %.3F)', $this->red, $this->green, $this->blue, $this->alpha);
return sprintf('#%02x%02x%02x', $this->red, $this->green, $this->blue);
}
public static function fromRawRGB(int $raw): ColourRGB {
return new ColourRGB(
(($raw >> 16) & 0xFF),
(($raw >> 8) & 0xFF),
($raw & 0xFF),
1.0
);
}
public static function fromRawARGB(int $raw): ColourRGB {
return new ColourRGB(
(($raw >> 16) & 0xFF),
(($raw >> 8) & 0xFF),
($raw & 0xFF),
(($raw >> 24) & 0xFF) / 255.0,
);
}
public static function fromRawRGBA(int $raw): ColourRGB {
return new ColourRGB(
(($raw >> 24) & 0xFF),
(($raw >> 16) & 0xFF),
(($raw >> 8) & 0xFF),
($raw & 0xFF) / 255.0,
);
}
public static function convert(Colour $colour): ColourRGB {
if($colour instanceof ColourRGB)
return $colour;
return new ColourRGB(
$colour->getRed(),
$colour->getGreen(),
$colour->getBlue(),
$colour->getAlpha()
);
}
}