getRed() + self::LUMINANCE_WEIGHT_GREEN * $this->getGreen() + self::LUMINANCE_WEIGHT_BLUE * $this->getBlue(); } /** * Checks whether this colour is on the brighter side of things. * * @return bool true if this colour would not be very legible on a light background. */ public function isLight(): bool { return $this->getLuminance() > self::READABILITY_THRESHOLD; } /** * Checks whether this colour is on the darker side of things. * * @return bool true if this colour would not be very legible on a dark background. */ public function isDark(): bool { return $this->getLuminance() <= self::READABILITY_THRESHOLD; } private static ColourNull $none; /** * Gets an empty colour. * * @return Colour A global instance of ColourNull. */ public static function none(): Colour { return self::$none; } /** * Mixes two colours. * 0.0 -> Entirely $colour1 * 0.5 -> Midpoint between $colour1 and $colour2 * 1.0 -> Entirely $colour2 * * @param Colour $colour1 Starting colour. * @param Colour $colour2 Ending colour. * @param float $weight Weight of $colour2. * @return Colour Mixed colour. */ public static function mix(Colour $colour1, Colour $colour2, float $weight): Colour { $weight = min(1, max(0, $weight)); if(XNumber::almostEquals($weight, 0.0)) return $colour1; if(XNumber::almostEquals($weight, 1.0)) return $colour2; if($colour1->shouldInherit() || $colour2->shouldInherit()) return self::none(); return new ColourRGB( (int)round(XNumber::weighted($colour2->getRed(), $colour1->getRed(), $weight)), (int)round(XNumber::weighted($colour2->getGreen(), $colour1->getGreen(), $weight)), (int)round(XNumber::weighted($colour2->getBlue(), $colour1->getBlue(), $weight)), XNumber::weighted($colour2->getAlpha(), $colour1->getAlpha(), $weight), ); } private const MSZ_INHERIT = 0x40000000; /** * Creates a Colour object from raw Misuzu format. * * If bit 31 is set the global instance of ColourNull will always be returned, * otherwise an instance of ColourRGB will be created using the fromRawRGB method (top 8 bits ignored). * * @param int $raw Raw RGB colour in Misuzu format. * @return Colour Colour instance representing the Misuzu colour. */ public static function fromMisuzu(int $raw): Colour { if($raw & self::MSZ_INHERIT) return self::$none; return ColourRGB::fromRawRGB($raw); } /** * Converts a Colour object to raw Misuzu format. * * If shouldInherit is true, an integer with only bit 31 will be returned, * otherwise a raw RGB value will be returned. * * @param Colour $colour Colour to be converted to raw Misuzu format. * @return int Raw Misuzu format colour. */ public static function toMisuzu(Colour $colour): int { if($colour->shouldInherit()) return self::MSZ_INHERIT; return ($colour->getRed() << 16) | ($colour->getGreen() << 8) | $colour->getBlue(); } /** * Attempts to parse a CSS format colour. * * @param ?string $value CSS format colour. * @return Colour Parsed CSS colour. */ 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) && ColourNamed::isValidName($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; for($i = 0; $i < $parts; ++$i) $value[$i] = trim($value[$i]); if(str_ends_with($value[1], '%')) $value[1] = substr($value[1], 0, -1); if(str_ends_with($value[2], '%')) $value[2] = substr($value[2], 0, -1); $value[1] = (float)$value[1]; $value[2] = (float)$value[2]; 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] = round(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; } /** @internal */ public static function init(): void { self::$none = new ColourNull; } } Colour::init();