306 lines
9.5 KiB
PHP
306 lines
9.5 KiB
PHP
<?php
|
|
// Colour.php
|
|
// Created: 2023-01-02
|
|
// Updated: 2023-11-09
|
|
|
|
namespace Index\Colour;
|
|
|
|
use Stringable;
|
|
use Index\XNumber;
|
|
|
|
/**
|
|
* Abstract class for representing colours.
|
|
*/
|
|
abstract class Colour implements Stringable {
|
|
/**
|
|
* Retrieves the Red RGB byte.
|
|
*
|
|
* @return int A number ranging from 0 to 255.
|
|
*/
|
|
public abstract function getRed(): int;
|
|
|
|
/**
|
|
* Retrieves the Green RGB byte.
|
|
*
|
|
* @return int A number ranging from 0 to 255.
|
|
*/
|
|
public abstract function getGreen(): int;
|
|
|
|
/**
|
|
* Retrieves the Blue RGB byte.
|
|
*
|
|
* @return int A number ranging from 0 to 255.
|
|
*/
|
|
public abstract function getBlue(): int;
|
|
|
|
/**
|
|
* Retrieves the alpha component.
|
|
*
|
|
* @return float A number ranging from 0.0 to 1.0.
|
|
*/
|
|
public abstract function getAlpha(): float;
|
|
|
|
/**
|
|
* Whether this colour should be ignored and another should be used instead.
|
|
*
|
|
* @return bool true if this colour should be ignored.
|
|
*/
|
|
public abstract function shouldInherit(): bool;
|
|
|
|
/**
|
|
* Returns this colour in a format CSS understands.
|
|
*
|
|
* @return string CSS compatible colour.
|
|
*/
|
|
public abstract function __toString(): string;
|
|
|
|
private const READABILITY_THRESHOLD = 186.0;
|
|
private const LUMINANCE_WEIGHT_RED = .299;
|
|
private const LUMINANCE_WEIGHT_GREEN = .587;
|
|
private const LUMINANCE_WEIGHT_BLUE = .114;
|
|
|
|
// luminance shit might need further review
|
|
|
|
/**
|
|
* Calculates the luminance value of this colour.
|
|
*
|
|
* @return float Luminance value for this colour.
|
|
*/
|
|
public function getLuminance(): float {
|
|
return self::LUMINANCE_WEIGHT_RED * $this->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();
|