diff --git a/src/Colour/Colour.php b/src/Colour/Colour.php new file mode 100644 index 0000000..bb724c8 --- /dev/null +++ b/src/Colour/Colour.php @@ -0,0 +1,176 @@ +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(); diff --git a/src/Colour/ColourHSL.php b/src/Colour/ColourHSL.php new file mode 100644 index 0000000..8a0d0bb --- /dev/null +++ b/src/Colour/ColourHSL.php @@ -0,0 +1,127 @@ +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() + ); + } +} diff --git a/src/Colour/ColourNamed.php b/src/Colour/ColourNamed.php new file mode 100644 index 0000000..994c411 --- /dev/null +++ b/src/Colour/ColourNamed.php @@ -0,0 +1,217 @@ +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, + ]; +} diff --git a/src/Colour/ColourNull.php b/src/Colour/ColourNull.php new file mode 100644 index 0000000..aad75df --- /dev/null +++ b/src/Colour/ColourNull.php @@ -0,0 +1,28 @@ +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() + ); + } +}