From 565014e1e243c3851f914b8103c93cdab6bc8a1e Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Mon, 11 Nov 2024 18:48:56 +0900 Subject: [PATCH 01/13] diff --git c/4dev/tests/Convert/CoreLibsConvertMathTest.php i/4dev/tests/Convert/CoreLibsConvertMathTest.php index 9a97e37e..c98b4b2a 100644 --- c/4dev/tests/Convert/CoreLibsConvertMathTest.php +++ i/4dev/tests/Convert/CoreLibsConvertMathTest.php @@ -113,6 +113,8 @@ final class CoreLibsConvertMathTest extends TestCase \CoreLibs\Convert\Math::initNumeric($input) ); } + + // TODO: cbrt tests } // __END__ diff --git c/www/admin/class_test.convert.colors.php i/www/admin/class_test.convert.colors.php index 6f809691..a37cb2df 100644 --- c/www/admin/class_test.convert.colors.php +++ i/www/admin/class_test.convert.colors.php @@ -19,6 +19,8 @@ $LOG_FILE_ID = 'classTest-convert-colors'; ob_end_flush(); use CoreLibs\Convert\Colors; +use CoreLibs\Convert\Color\Color; +use CoreLibs\Convert\Color\Coordinates; use CoreLibs\Debug\Support as DgS; use CoreLibs\Convert\SetVarType; @@ -52,16 +54,16 @@ try { print "**Exception: " . $e->getMessage() . "
" . print_r($e, true) . "

"; } // B(valid) -$rgb = [10, 20, 30]; +$rgb = [50, 20, 30]; $hex = '#0a141e'; $hsb = [210, 67, 12]; $hsb_f = [210.5, 67.5, 12.5]; -$hsl = [210, 50, 7.8]; +$hsb = [210, 50, 7.8]; print "S::COLOR rgb->hex: $rgb[0], $rgb[1], $rgb[2]: " . Colors::rgb2hex($rgb[0], $rgb[1], $rgb[2]) . "
"; print "S::COLOR hex->rgb: $hex: " . DgS::printAr(SetVarType::setArray( Colors::hex2rgb($hex) )) . "
"; -print "C::S/COLOR rgb->hext: $hex: " . DgS::printAr(SetVarType::setArray( +print "C::S/COLOR rgb->hex: $hex: " . DgS::printAr(SetVarType::setArray( CoreLibs\Convert\Colors::hex2rgb($hex) )) . "
"; // C(to hsb/hsl) @@ -82,9 +84,9 @@ print "S::COLOR hsb_f->rgb: $hsb_f[0], $hsb_f[1], $hsb_f[2]: " . DgS::printAr(SetVarType::setArray( Colors::hsb2rgb($hsb_f[0], $hsb_f[1], $hsb_f[2]) )) . "
"; -print "S::COLOR hsl->rgb: $hsl[0], $hsl[1], $hsl[2]: " +print "S::COLOR hsl->rgb: $hsb[0], $hsb[1], $hsb[2]: " . DgS::printAr(SetVarType::setArray( - Colors::hsl2rgb($hsl[0], $hsl[1], $hsl[2]) + Colors::hsl2rgb($hsb[0], $hsb[1], $hsb[2]) )) . "
"; $hsb = [0, 0, 5]; @@ -102,8 +104,44 @@ print "RANDOM IN: H: " . $h . ", S: " . $s . ", B/L: " . $b . "/" . $l . "
"; print "RANDOM hsb->rgb:
" . DgS::printAr(SetVarType::setArray(Colors::hsb2rgb($h, $s, $b))) . "

"; print "RANDOM hsl->rgb:
" . DgS::printAr(SetVarType::setArray(Colors::hsl2rgb($h, $s, $l))) . "

"; +$rgb = [0, 0, 0]; +print "rgb 0,0,0: " . Dgs::printAr($rgb) . " => " . Dgs::printAr(Colors::rgb2hsb($rgb[0], $rgb[1], $rgb[2])) . "
"; + // TODO: run compare check input must match output +$hwb = Color::hsbToHwb(Coordinates\HSB::__constructFromArray([ + 160, + 0, + 50, +])); +print "HWB: " . DgS::printAr($hwb) . "
"; +$hsb = Color::hwbToHsb($hwb); +print "HSB: " . DgS::printAr($hsb) . "
"; + +$oklch = Color::rgbToOkLch(Coordinates\RGB::__constructFromArray([ + 250, + 0, + 0 +])); +print "OkLch: " . DgS::printAr($oklch) . "
"; +$rgb = Color::okLchToRgb($oklch); +print "OkLch -> RGB: " . DgS::printAr($rgb) . "
"; + +$oklab = Color::rgbToOkLab(Coordinates\RGB::__constructFromArray([ + 250, + 0, + 0 +])); +print "OkLab: " . DgS::printAr($oklab) . "
"; +$rgb = Color::okLabToRgb($oklab); +print "OkLab -> RGB: " . DgS::printAr($rgb) . "
"; + +$rgb = Coordinates\RGB::__constructFromArray([250, 100, 10])->toLinear(); +print "RGBlinear: " . DgS::printAr($rgb) . "
"; +$rgb = Coordinates\RGB::__constructFromArray([0, 0, 0])->toLinear(); +print "RGBlinear: " . DgS::printAr($rgb) . "
"; + + print ""; // __END__ diff --git c/www/lib/CoreLibs/Convert/Color/Color.php i/www/lib/CoreLibs/Convert/Color/Color.php new file mode 100644 index 00000000..c56f8c11 --- /dev/null +++ i/www/lib/CoreLibs/Convert/Color/Color.php @@ -0,0 +1,803 @@ + HSL + + /** + * converts a RGB (0-255) to HSL + * return: + * class with hue (0-360), saturation (0-100%) and luminance (0-100%) + * + * @param RGB $rgb Class for rgb + * @return HSL Class hue/sat/luminance + */ + public static function rgbToHsl(RGB $rgb): HSL + { + $red = $rgb->R / 255; + $green = $rgb->G / 255; + $blue = $rgb->B / 255; + + $min = min($red, $green, $blue); + $max = max($red, $green, $blue); + $chroma = $max - $min; + $sat = 0; + $hue = 0; + // luminance + $lum = ($max + $min) / 2; + + // achromatic + if ($chroma == 0) { + // H, S, L + return HSL::__constructFromArray([ + 0.0, + 0.0, + $lum * 100, + ]); + } else { + $sat = $chroma / (1 - abs(2 * $lum - 1)); + if ($max == $red) { + $hue = fmod((($green - $blue) / $chroma), 6); + if ($hue < 0) { + $hue = (6 - fmod(abs($hue), 6)); + } + } elseif ($max == $green) { + $hue = ($blue - $red) / $chroma + 2; + } elseif ($max == $blue) { + $hue = ($red - $green) / $chroma + 4; + } + $hue = $hue * 60; + // $sat = 1 - abs(2 * $lum - 1); + return HSL::__constructFromArray([ + $hue, + $sat * 100, + $lum * 100, + ]); + } + } + + /** + * converts an HSL to RGB + * if HSL value is invalid, set this value to 0 + * + * @param HSL $hsl Class with hue: 0-360 (degrees), + * saturation: 0-100, + * luminance: 0-100 + * @return RGB Class for rgb + */ + public static function hslToRgb(HSL $hsl): RGB + { + $hue = $hsl->H; + $sat = $hsl->S; + $lum = $hsl->L; + // calc to internal convert value for hue + $hue = (1 / 360) * $hue; + // convert to internal 0-1 format + $sat /= 100; + $lum /= 100; + // if saturation is 0 + if ($sat == 0) { + $lum = round($lum * 255); + return RGB::__constructFromArray([$lum, $lum, $lum]); + } else { + $m2 = $lum < 0.5 ? $lum * ($sat + 1) : ($lum + $sat) - ($lum * $sat); + $m1 = $lum * 2 - $m2; + $hueue = function ($base) use ($m1, $m2) { + // base = hue, hue > 360 (1) - 360 (1), else < 0 + 360 (1) + $base = $base < 0 ? $base + 1 : ($base > 1 ? $base - 1 : $base); + // 6: 60, 2: 180, 3: 240 + // 2/3 = 240 + // 1/3 = 120 (all from 360) + if ($base * 6 < 1) { + return $m1 + ($m2 - $m1) * $base * 6; + } + if ($base * 2 < 1) { + return $m2; + } + if ($base * 3 < 2) { + return $m1 + ($m2 - $m1) * ((2 / 3) - $base) * 6; + } + return $m1; + }; + + return RGB::__constructFromArray([ + 255 * $hueue($hue + (1 / 3)), + 255 * $hueue($hue), + 255 * $hueue($hue - (1 / 3)), + ]); + } + } + + // MARK: RGB <-> HSB + + /** + * rgb2hsb does not clean convert back to rgb in a round trip + * converts RGB to HSB/V values + * returns: + * Class with hue (0-360), sat (0-100%), brightness/value (0-100%) + * + * @param RGB $rgb Class for rgb + * @return HSB Class Hue, Sat, Brightness/Value + */ + public static function rgbToHsb(RGB $rgb): HSB + { + $red = $rgb->R / 255; + $green = $rgb->G / 255; + $blue = $rgb->B / 255; + + $MAX = max($red, $green, $blue); + $MIN = min($red, $green, $blue); + $HUE = 0; + $DELTA = $MAX - $MIN; + + // achromatic + if ($MAX == $MIN) { + return HSB::__constructFromArray([0, 0, $MAX * 100]); + } + if ($red == $MAX) { + $HUE = fmod(($green - $blue) / $DELTA, 6); + } elseif ($green == $MAX) { + $HUE = (($blue - $red) / $DELTA) + 2; + } elseif ($blue == $MAX) { + $HUE = (($red - $green) / $DELTA) + 4; + } + $HUE *= 60; + // avoid negative + if ($HUE < 0) { + $HUE += 360; + } + + return HSB::__constructFromArray([ + $HUE, // Hue + ($DELTA / $MAX) * 100, // Saturation + $MAX * 100, // Brightness + ]); + } + + /** + * hsb2rgb does not clean convert back to hsb in a round trip + * converts HSB/V to RGB values RGB is full INT + * if HSB/V value is invalid, sets this value to 0 + * + * @param HSB $hsb hue 0-360 (int), + * saturation 0-100 (int), + * brightness/value 0-100 (int) + * @return RGB Class for RGB + */ + public static function hsbToRgb(HSB $hsb): RGB + { + $H = $hsb->H; + $S = $hsb->S; + $V = $hsb->B; + // convert to internal 0-1 format + $S /= 100; + $V /= 100; + + if ($S == 0) { + $V = $V * 255; + return RGB::__constructFromArray([$V, $V, $V]); + } + + $Hi = floor($H / 60); + $f = ($H / 60) - $Hi; + $p = $V * (1 - $S); + $q = $V * (1 - ($S * $f)); + $t = $V * (1 - ($S * (1 - $f))); + + switch ($Hi) { + case 0: + $red = $V; + $green = $t; + $blue = $p; + break; + case 1: + $red = $q; + $green = $V; + $blue = $p; + break; + case 2: + $red = $p; + $green = $V; + $blue = $t; + break; + case 3: + $red = $p; + $green = $q; + $blue = $V; + break; + case 4: + $red = $t; + $green = $p; + $blue = $V; + break; + case 5: + $red = $V; + $green = $p; + $blue = $q; + break; + default: + $red = 0; + $green = 0; + $blue = 0; + } + + return RGB::__constructFromArray([ + $red * 255, + $green * 255, + $blue * 255, + ]); + } + + // MARK: HSL <-> HSB + + /** + * Convert HSL to HSB + * + * @param HSL $hsl + * @return HSB + */ + public static function hslToHsb(HSL $hsl): HSB + { + $saturation = $hsl->S / 100; + $lightness = $hsl->L / 100; + $value = $lightness + $saturation * min($lightness, 1 - $lightness); + // check for black and white + $saturation = ($value === 0) ? + 0 : + 200 * (1 - $lightness / $value); + return HSB::__constructFromArray([ + $hsl->H, + $saturation, + $value * 100, + ]); + } + + /** + * Convert HSB to HSL + * + * @param HSB $hsb + * @return HSL + */ + public static function hsbToHsl(HSB $hsb): HSL + { + // hsv/toHsl + $hue = $hsb->H; + $saturation = $hsb->S / 100; + $value = $hsb->V / 100; + + $lightness = $value * (1 - $saturation / 2); + // check for B/W + $saturation = in_array($lightness, [0, 1], true) ? + 0 : + 100 * ($value - $lightness) / min($lightness, 1 - $lightness) + ; + + return HSL::__constructFromArray([ + $hue, + $saturation, + $lightness * 100, + ]); + } + + // MARK: HSB <-> HWB + + /** + * convert HSB to HWB + * + * @param HSB $hsb + * @return HWB + */ + public static function hsbToHwb(HSB $hsb): HWB + { + // hsv\Hwb + return HWB::__constructFromArray([ + $hsb->H, // hue, + $hsb->B * (100 - $hsb->S) / 100, // 2: brightness, 1: saturation + 100 - $hsb->B, + ]); + } + + /** + * convert HWB to HSB + * + * @param HWB $hwb + * @return HSB + */ + public static function hwbToHsb(HWB $hwb): HSB + { + $hue = $hwb->H; + $whiteness = $hwb->W / 100; + $blackness = $hwb->B / 100; + + $sum = $whiteness + $blackness; + // for black and white + if ($sum >= 1) { + $saturation = 0; + $value = $whiteness / $sum * 100; + } else { + $value = 1 - $blackness; + $saturation = $value === 0 ? 0 : (1 - $whiteness / $value) * 100; + $value *= 100; + } + + return HSB::__constructFromArray([ + $hue, + $saturation, + $value, + ]); + } + + // MARK: RGB <-> HWB + + /** + * Convert RGB to HWB + * via rgb -> hsl -> hsb -> hwb + * + * @param RGB $rgb + * @return HWB + */ + public static function rgbToHwb(RGB $rgb): HWB + { + return self::hsbToHwb( + self::hslToHsb( + self::rgbToHsl($rgb) + ) + ); + } + + /** + * Convert HWB to RGB + * via hwb -> hsb -> hsl -> rgb + * + * @param HWB $hwb + * @return RGB + */ + public static function hwbToRgb(HWB $hwb): RGB + { + return self::hslToRgb( + self::hsbToHsl( + self::hwbToHsb($hwb) + ) + ); + } + + // MARK: HSL <-> HWB + + /** + * Convert HSL to HWB + * via hsl -> hsb -> hwb + * + * @param HSL $hsl + * @return HWB + */ + public static function hslToHwb(HSL $hsl): HWB + { + return self::hsbToHwb( + self::hslToHsb( + $hsl + ) + ); + } + + /** + * Convert HWB to HSL + * via hwb -> hsb -> hsl + * + * @param HWB $hwb + * @return HSL + */ + public static function hwbToHsl(HWB $hwb): HSL + { + return self::hsbToHsl( + self::hwbToHsb($hwb) + ); + } + + // MARK: OkLch <-> OkLab + + /** + * okLAab to okLCH + * + * @param Lab $lab + * @return LCH + */ + public static function okLabToOkLch(Lab $lab): LCH + { + // okLab\toOkLch + $a = $lab->a; + $b = $lab->b; + + $hue = atan2($b, $a) * 180 / pi(); + + return LCH::__constructFromArray([ + $lab->L, + sqrt($a ** 2 + $b ** 2), + $hue >= 0 ? $hue : $hue + 360, + ]); + } + + /** + * okLCH to okLab + * + * @param LCH $lch + * @return Lab + */ + public static function okLchToOkLab(LCH $lch): Lab + { + // oklch/toOkLab + // oklch to oklab + return Lab::__constructFromArray([ + $lch->L, + $lch->C * cos($lch->H * pi() / 180), // a + $lch->C * sin($lch->H * pi() / 180), // b + ], 'Oklab'); + } + + // MARK: xyzD65 <-> linearRGB + + /** + * convert linear RGB to xyz D65 + * if rgb is not flagged linear, it will be auto converted + * + * @param RGB $rgb + * @return XYZD65 + */ + public static function linRgbToXyzD65(RGB $rgb): XYZD65 + { + // if not linear, convert to linear + if (!$rgb->linear) { + $rgb->toLinear(); + } + return XYZD65::__constructFromArray(Math::multiplyMatrices( + [ + [0.41239079926595934, 0.357584339383878, 0.1804807884018343], + [0.21263900587151027, 0.715168678767756, 0.07219231536073371], + [0.01933081871559182, 0.11919477979462598, 0.9505321522496607], + ], + $rgb->returnAsArray() + )); + } + + /** + * Convert xyz D65 to linear RGB + * + * @param XYZD65 $xyzD65 + * @return RGB + */ + public static function xyzD65ToLinRgb(XYZD65 $xyzD65): RGB + { + // xyz D65 to linrgb + return RGB::__constructFromArray(Math::multiplyMatrices( + a : [ + [ 3.2409699419045226, -1.537383177570094, -0.4986107602930034 ], + [ -0.9692436362808796, 1.8759675015077202, 0.04155505740717559 ], + [ 0.05563007969699366, -0.20397695888897652, 1.0569715142428786 ], + ], + b : $xyzD65->returnAsArray() + ), linear: true); + } + + // MARK: xyzD65 <-> OkLab + + /** + * xyz D65 to OkLab + * + * @param XYZD65 $xyzD65 + * @return Lab + */ + public static function xyzD65ToOkLab(XYZD65 $xyzD65): Lab + { + return Lab::__constructFromArray(Math::multiplyMatrices( + [ + [0.2104542553, 0.7936177850, -0.0040720468], + [1.9779984951, -2.4285922050, 0.4505937099], + [0.0259040371, 0.7827717662, -0.8086757660], + ], + array_map( + callback: fn ($v) => pow($v, 1 / 3), + array: Math::multiplyMatrices( + a: [ + [0.8190224432164319, 0.3619062562801221, -0.12887378261216414], + [0.0329836671980271, 0.9292868468965546, 0.03614466816999844], + [0.048177199566046255, 0.26423952494422764, 0.6335478258136937], + ], + b: $xyzD65->returnAsArray(), + ), + ) + ), 'Oklab'); + } + + /** + * xyz D65 to OkLab + * + * @param Lab $lab + * @return XYZD65 + */ + public static function okLabToXyzD65(Lab $lab): XYZD65 + { + return XYZD65::__constructFromArray(Math::multiplyMatrices( + a: [ + [1.2268798733741557, -0.5578149965554813, 0.28139105017721583], + [-0.04057576262431372, 1.1122868293970594, -0.07171106666151701], + [-0.07637294974672142, -0.4214933239627914, 1.5869240244272418], + ], + b: array_map( + callback: fn ($v) => is_numeric($v) ? $v ** 3 : 0, + array: Math::multiplyMatrices( + a: [ + [0.99999999845051981432, 0.39633779217376785678, 0.21580375806075880339], + [1.0000000088817607767, -0.1055613423236563494, -0.063854174771705903402], + [1.0000000546724109177, -0.089484182094965759684, -1.2914855378640917399], + ], + // Divide $lightness by 100 to convert from CSS OkLab + b: $lab->returnAsArray(), + ), + ), + )); + } + + // MARK: rgb <-> oklab + + /** + * Undocumented function + * + * @param RGB $rgb + * @return Lab + */ + public static function rgbToOkLab(RGB $rgb): Lab + { + return self::xyzD65ToOkLab( + self::linRgbToXyzD65($rgb) + ); + } + + /** + * Undocumented function + * + * @param Lab $lab + * @return RGB + */ + public static function okLabToRgb(Lab $lab): RGB + { + return self::xyzD65ToLinRgb( + self::okLabToXyzD65($lab) + )->fromLinear(); + } + + // MARK: rgb <-> oklch + + /** + * convert rgb to OkLch + * via rgb -> linear rgb -> xyz D65 -> OkLab -> OkLch + * + * @param RGB $rbh + * @return LCH + */ + public static function rgbToOkLch(RGB $rgb): LCH + { + return self::okLabToOkLch( + self::rgbToOkLab($rgb) + ); + } + + /** + * Convert OkLch to rgb + * via OkLab -> OkLch -> xyz D65 -> linear rgb -> rgb + * + * @param LCH $lch + * @return RGB + */ + public static function okLchToRgb(LCH $lch): RGB + { + return self::okLabToRgb( + self::okLchToOkLab($lch) + ); + } + + // MARK: HSL <-> OKLab + + /** + * Undocumented function + * + * @param HSL $hsl + * @return Lab + */ + public static function hslToOkLab(HSL $hsl): Lab + { + return self::rgbToOkLab( + self::hslToRgb($hsl) + ); + } + + /** + * Undocumented function + * + * @param Lab $lab + * @return HSL + */ + public static function okLabToHsl(Lab $lab): HSL + { + return self::rgbToHsl( + self::okLabToRgb($lab) + ); + } + + // MARK: HSL <-> OKLCH + + /** + * Undocumented function + * + * @param HSL $hsl + * @return LCH + */ + public static function hslToOkLch(HSL $hsl): LCH + { + return self::rgbToOkLch( + self::hslToRgb($hsl) + ); + } + + /** + * Undocumented function + * + * @param LCH $lch + * @return HSL + */ + public static function okLchToHsl(LCH $lch): HSL + { + return self::rgbToHsl( + self::okLchToRgb($lch) + ); + } + + // MARK: HSB <-> OKLab + + /** + * Undocumented function + * + * @param HSB $hsb + * @return Lab + */ + public static function hsbToOkLab(HSB $hsb): Lab + { + return self::rgbToOkLab( + self::hsbToRgb($hsb) + ); + } + + /** + * Undocumented function + * + * @param Lab $lab + * @return HSB + */ + public static function okLabToHsb(Lab $lab): HSB + { + return self::rgbToHsb( + self::okLabToRgb($lab) + ); + } + + // MARK: HSB <-> OKLCH + + /** + * Undocumented function + * + * @param HSB $hsb + * @return LCH + */ + public static function hsbToOkLch(HSB $hsb): LCH + { + return self::rgbToOkLch( + self::hsbToRgb($hsb) + ); + } + + /** + * Undocumented function + * + * @param LCH $lch + * @return HSB + */ + public static function okLchToHsb(LCH $lch): HSB + { + return self::rgbToHsb( + self::okLchToRgb($lch) + ); + } + + // MARK: HWB <-> OKLab + + /** + * Undocumented function + * + * @param HWB $hwb + * @return Lab + */ + public function hwbToOkLab(HWB $hwb): Lab + { + return self::rgbToOkLab( + self::hwbToRgb($hwb) + ); + } + + /** + * Undocumented function + * + * @param Lab $lab + * @return HWB + */ + public function okLabToHwb(Lab $lab): HWB + { + return self::rgbToHwb( + self::okLabToRgb($lab) + ); + } + + // MARK: HWB <-> OKLCH + + /** + * Undocumented function + * + * @param HWB $hwb + * @return LCH + */ + public function hwbToOkLch(HWB $hwb): LCH + { + return self::rgbToOkLch( + self::hwbToRgb($hwb) + ); + } + + /** + * Undocumented function + * + * @param LCH $lch + * @return HWB + */ + public function okLchToHwb(LCH $lch): HWB + { + return self::rgbToHwb( + self::okLchToRgb($lch) + ); + } +} diff --git c/www/lib/CoreLibs/Convert/Color/Coordinates/HSB.php i/www/lib/CoreLibs/Convert/Color/Coordinates/HSB.php new file mode 100644 index 00000000..b435a9ef --- /dev/null +++ i/www/lib/CoreLibs/Convert/Color/Coordinates/HSB.php @@ -0,0 +1,155 @@ +setAsArray([$H, $S, $B]); + } + + /** + * set from array + * where 0: Hue, 1: Saturation, 2: Brightness + * + * @param array{0:float,1:float,2:float} $hsb + * @return self + */ + public static function __constructFromArray(array $hsb): self + { + return (new HSB())->setAsArray($hsb); + } + + /** + * set color + * + * @param string $name + * @param float $value + * @return void + */ + public function __set(string $name, float $value): void + { + $name = strtoupper($name); + if (!property_exists($this, $name)) { + throw new \ErrorException('Creation of dynamic property is not allowed', 0); + } + switch ($name) { + case 'H': + if ($value == 360) { + $value = 0; + } + if ($value < 0 || $value > 359) { + throw new \LengthException( + 'Argument value ' . $value . ' for hue is not in the range of 0 to 359', + 1 + ); + } + break; + case 'S': + if ($value < 0 || $value > 100) { + throw new \LengthException( + 'Argument value ' . $value . ' for saturation is not in the range of 0 to 100', + 2 + ); + } + break; + case 'B': + if ($value < 0 || $value > 100) { + throw new \LengthException( + 'Argument value ' . $value . ' for brightness is not in the range of 0 to 100', + 3 + ); + } + break; + } + $this->$name = $value; + } + + /** + * get color + * + * @param string $name + * @return float + */ + public function __get(string $name): float + { + $name = strtoupper($name); + if (!property_exists($this, $name)) { + throw new \ErrorException('Creation of dynamic property is not allowed', 0); + } + return $this->$name; + } + + /** + * Returns the color as array + * where 0: Hue, 1: Saturation, 2: Brightness + * + * @return array{0:float,1:float,2:float} + */ + public function returnAsArray(): array + { + return [$this->H, $this->S, $this->B]; + } + + /** + * set color as array + * where 0: Hue, 1: Saturation, 2: Brightness + * + * @param array{0:float,1:float,2:float} $hsb + * @return self + */ + public function setAsArray(array $hsb): self + { + $this->__set('H', $hsb[0]); + $this->__set('S', $hsb[1]); + $this->__set('B', $hsb[2]); + return $this; + } + + /** + * no hsb in css + * + * @param float|string|null $opacity + * @return string + * @throws \ErrorException + */ + public function toCssString(null|float|string $opacity = null): string + { + throw new \ErrorException('HSB is not available as CSS color string', 0); + } +} + +// __END__ diff --git c/www/lib/CoreLibs/Convert/Color/Coordinates/HSL.php i/www/lib/CoreLibs/Convert/Color/Coordinates/HSL.php new file mode 100644 index 00000000..21be7fe5 --- /dev/null +++ i/www/lib/CoreLibs/Convert/Color/Coordinates/HSL.php @@ -0,0 +1,140 @@ +setAsArray([$H, $S, $L]); + } + + /** + * set from array + * where 0: Hue, 1: Saturation, 2: Lightness + * + * @param array{0:float,1:float,2:float} $hsl + * @return self + */ + public static function __constructFromArray(array $hsl): self + { + return (new HSL())->setAsArray($hsl); + } + + /** + * set color + * + * @param string $name + * @param float $value + * @return void + */ + public function __set(string $name, float $value): void + { + if (!property_exists($this, $name)) { + throw new \ErrorException('Creation of dynamic property is not allowed', 0); + } + switch ($name) { + case 'H': + if ($value == 360) { + $value = 0; + } + if ($value < 0 || $value > 359) { + throw new \LengthException( + 'Argument value ' . $value . ' for hue is not in the range of 0 to 359', + 1 + ); + } + break; + case 'S': + if ($value < 0 || $value > 100) { + throw new \LengthException( + 'Argument value ' . $value . ' for saturation is not in the range of 0 to 100', + 2 + ); + } + break; + case 'L': + if ($value < 0 || $value > 100) { + throw new \LengthException( + 'Argument value ' . $value . ' for luminance is not in the range of 0 to 100', + 3 + ); + } + break; + } + $this->$name = $value; + } + + /** + * get color + * + * @param string $name + * @return float + */ + public function __get(string $name): float + { + if (!property_exists($this, $name)) { + throw new \ErrorException('Creation of dynamic property is not allowed', 0); + } + return $this->$name; + } + + /** + * Returns the color as array + * where 0: Hue, 1: Saturation, 2: Lightness + * + * @return array{0:float,1:float,2:float} + */ + public function returnAsArray(): array + { + return [$this->H, $this->S, $this->L]; + } + + /** + * set color as array + * where 0: Hue, 1: Saturation, 2: Lightness + * + * @param array{0:float,1:float,2:float} $hsl + * @return self + */ + public function setAsArray(array $hsl): self + { + $this->__set('H', $hsl[0]); + $this->__set('S', $hsl[1]); + $this->__set('L', $hsl[2]); + return $this; + } +} + +// __END__ diff --git c/www/lib/CoreLibs/Convert/Color/Coordinates/HWB.php i/www/lib/CoreLibs/Convert/Color/Coordinates/HWB.php new file mode 100644 index 00000000..ee6b7f63 --- /dev/null +++ i/www/lib/CoreLibs/Convert/Color/Coordinates/HWB.php @@ -0,0 +1,140 @@ +setAsArray([$H, $W, $B]); + } + + /** + * set from array + * where 0: Hue, 1: Whiteness, 2: Blackness + * + * @param array{0:float,1:float,2:float} $hwb + * @return self + */ + public static function __constructFromArray(array $hwb): self + { + return (new HWB())->setAsArray($hwb); + } + + /** + * set color + * + * @param string $name + * @param float $value + * @return void + */ + public function __set(string $name, float $value): void + { + if (!property_exists($this, $name)) { + throw new \ErrorException('Creation of dynamic property is not allowed', 0); + } + switch ($name) { + case 'H': + if ($value == 360) { + $value = 0; + } + if ($value < 0 || $value > 360) { + throw new \LengthException( + 'Argument value ' . $value . ' for hue is not in the range of 0 to 360', + 1 + ); + } + break; + case 'W': + if ($value < 0 || $value > 100) { + throw new \LengthException( + 'Argument value ' . $value . ' for saturation is not in the range of 0 to 100', + 2 + ); + } + break; + case 'B': + if ($value < 0 || $value > 100) { + throw new \LengthException( + 'Argument value ' . $value . ' for luminance is not in the range of 0 to 100', + 3 + ); + } + break; + } + $this->$name = $value; + } + + /** + * get color + * + * @param string $name + * @return float + */ + public function __get(string $name): float + { + if (!property_exists($this, $name)) { + throw new \ErrorException('Creation of dynamic property is not allowed', 0); + } + return $this->$name; + } + + /** + * Returns the color as array + * where 0: Hue, 1: Whiteness, 2: Blackness + * + * @return array{0:float,1:float,2:float} + */ + public function returnAsArray(): array + { + return [$this->H, $this->W, $this->B]; + } + + /** + * set color as array + * where 0: Hue, 1: Whiteness, 2: Blackness + * + * @param array{0:float,1:float,2:float} $hwb + * @return self + */ + public function setAsArray(array $hwb): self + { + $this->__set('H', $hwb[0]); + $this->__set('W', $hwb[1]); + $this->__set('B', $hwb[2]); + return $this; + } +} + +// __END__ diff --git c/www/lib/CoreLibs/Convert/Color/Coordinates/LCH.php i/www/lib/CoreLibs/Convert/Color/Coordinates/LCH.php new file mode 100644 index 00000000..648fb466 --- /dev/null +++ i/www/lib/CoreLibs/Convert/Color/Coordinates/LCH.php @@ -0,0 +1,169 @@ +setAsArray([$L, $c, $h]); + } + + /** + * set from array + * where 0: Lightness, 1: Chroma, 2: Hue + * + * @param array{0:float,1:float,2:float} $lch + * @return self + */ + public static function __constructFromArray(array $lch): self + { + return (new LCH())->setAsArray($lch); + } + + /** + * set color + * + * @param string $name + * @param float $value + * @return void + */ + public function __set(string $name, float $value): void + { + if (!property_exists($this, $name)) { + throw new \ErrorException('Creation of dynamic property is not allowed', 0); + } + switch ($name) { + // case 'L': + // if ($this->colorspace == 'cie' && ($value < 0 || $value > 100)) { + // throw new \LengthException( + // 'Argument value ' . $value . ' for lightness is not in the range of ' + // . '0 to 100', + // 3 + // ); + // } elseif ($this->colorspace == 'ok' && ($value < 0 || $value > 1)) { + // throw new \LengthException( + // 'Argument value ' . $value . ' for lightness is not in the range of ' + // . '0 to 1', + // 3 + // ); + // } + // break; + // case 'c': + // if ($this->colorspace == 'cie' && ($value < 0 || $value > 230)) { + // throw new \LengthException( + // 'Argument value ' . $value . ' for chroma is not in the range of ' + // . '0 to 230 with normal upper limit of 150', + // 3 + // ); + // } elseif ($this->colorspace == 'ok' && ($value < 0 || $value > 0.5)) { + // throw new \LengthException( + // 'Argument value ' . $value . ' for chroma is not in the range of ' + // . '0 to 0.5 with normal upper limit of 0.5', + // 3 + // ); + // } + // break; + case 'h': + if ($value == 360) { + $value = 0; + } + if ($value < 0 || $value > 360) { + throw new \LengthException( + 'Argument value ' . $value . ' for lightness is not in the range of 0 to 360', + 1 + ); + } + break; + } + $this->$name = $value; + } + + /** + * get color + * + * @param string $name + * @return float + */ + public function __get(string $name): float + { + if (!property_exists($this, $name)) { + throw new \ErrorException('Creation of dynamic property is not allowed', 0); + } + return $this->$name; + } + + /** + * Returns the color as array + * where 0: Lightness, 1: Chroma, 2: Hue + * + * @return array{0:float,1:float,2:float} + */ + public function returnAsArray(): array + { + return [$this->L, $this->C, $this->H]; + } + + /** + * set color as array + * where 0: Lightness, 1: Chroma, 2: Hue + * + * @param array{0:float,1:float,2:float} $lch + * @return self + */ + public function setAsArray(array $lch): self + { + $this->__set('L', $lch[0]); + $this->__set('C', $lch[1]); + $this->__set('H', $lch[2]); + return $this; + } +} + +// __END__ diff --git c/www/lib/CoreLibs/Convert/Color/Coordinates/Lab.php i/www/lib/CoreLibs/Convert/Color/Coordinates/Lab.php new file mode 100644 index 00000000..e2eb11a4 --- /dev/null +++ i/www/lib/CoreLibs/Convert/Color/Coordinates/Lab.php @@ -0,0 +1,177 @@ + allowed colorspaces */ + private const COLORSPACES = ['Oklab', 'cie']; + + /** @var float lightness/luminance + * CIE: 0f to 100f + * OKlab: 0.0 to 1.0 + * BOTH: 0% to 100% + */ + private float $L = 0.0; + /** @var float a axis distance + * CIE: -125 to 125, cannot be more than +/- 160 + * OKlab: -0.4 to 0.4, cannot exceed +/- 0.5 + * BOTH: -100% to 100% (+/-125 or 0.4) + */ + private float $a = 0.0; + /** @var float b axis distance + * CIE: -125 to 125, cannot be more than +/- 160 + * OKlab: -0.4 to 0.4, cannot exceed +/- 0.5 + * BOTH: -100% to 100% (+/-125 or 0.4) + */ + private float $b = 0.0; + + /** @var string color space: either ok or cie */ + private string $colorspace = ''; + + /** + * Color Coordinate: Lab + * for oklab or cie + */ + public function __construct() + { + } + + /** + * set with each value as parameters + * + * @param float $L + * @param float $a + * @param float $b + * @param string $colorspace + * @return self + */ + public static function __constructFromSet(float $L, float $a, float $b, string $colorspace): self + { + return (new Lab())->setColorspace($colorspace)->setAsArray([$L, $a, $b]); + } + + /** + * set from array + * where 0: Lightness, 1: a, 2: b + * + * @param array{0:float,1:float,2:float} $rgb + * @param string $colorspace + * @return self + */ + public static function __constructFromArray(array $lab, string $colorspace): self + { + return (new Lab())->setColorspace($colorspace)->setAsArray($lab); + } + + /** + * set color + * + * @param string $name + * @param float $value + * @return void + */ + public function __set(string $name, float $value): void + { + if (!property_exists($this, $name)) { + throw new \ErrorException('Creation of dynamic property is not allowed', 0); + } + // switch ($name) { + // case 'L': + // if ($value == 360) { + // $value = 0; + // } + // if ($value < 0 || $value > 360) { + // throw new \LengthException( + // 'Argument value ' . $value . ' for lightness is not in the range of 0 to 360', + // 1 + // ); + // } + // break; + // case 'a': + // if ($value < 0 || $value > 100) { + // throw new \LengthException( + // 'Argument value ' . $value . ' for a is not in the range of 0 to 100', + // 2 + // ); + // } + // break; + // case 'b': + // if ($value < 0 || $value > 100) { + // throw new \LengthException( + // 'Argument value ' . $value . ' for b is not in the range of 0 to 100', + // 3 + // ); + // } + // break; + // } + $this->$name = $value; + } + + /** + * get color + * + * @param string $name + * @return float + */ + public function __get(string $name): float + { + if (!property_exists($this, $name)) { + throw new \ErrorException('Creation of dynamic property is not allowed', 0); + } + return $this->$name; + } + + /** + * set the colorspace + * + * @param string $colorspace + * @return self + */ + private function setColorspace(string $colorspace): self + { + if (!in_array($colorspace, $this::COLORSPACES)) { + throw new \InvalidArgumentException('Not allowed colorspace', 0); + } + $this->colorspace = $colorspace; + return $this; + } + + /** + * Returns the color as array + * where 0: Lightness, 1: a, 2: b + * + * @return array{0:float,1:float,2:float} + */ + public function returnAsArray(): array + { + return [$this->L, $this->a, $this->b]; + } + + /** + * set color as array + * where 0: Lightness, 1: a, 2: b + * + * @param array{0:float,1:float,2:float} $lab + * @return self + */ + public function setAsArray(array $lab): self + { + $this->__set('L', $lab[0]); + $this->__set('a', $lab[1]); + $this->__set('b', $lab[2]); + return $this; + } +} + +// __END__ diff --git c/www/lib/CoreLibs/Convert/Color/Coordinates/RGB.php i/www/lib/CoreLibs/Convert/Color/Coordinates/RGB.php new file mode 100644 index 00000000..acc81952 --- /dev/null +++ i/www/lib/CoreLibs/Convert/Color/Coordinates/RGB.php @@ -0,0 +1,226 @@ +flagLinear($linear)->setAsArray([$R, $G, $B]); + } + + /** + * set from array + * where 0: Red, 1: Green, 2: Blue + * + * @param array{0:float,1:float,2:float} $rgb + * @param bool $linear [default=false] + * @return self + */ + public static function __constructFromArray(array $rgb, bool $linear = false): self + { + return (new RGB())->flagLinear($linear)->setAsArray($rgb); + } + + /** + * set color + * + * @param string $name + * @param float $value + * @return void + */ + public function __set(string $name, float $value): void + { + // do not allow setting linear from outside + if ($name == 'linear') { + return; + } + if (!property_exists($this, $name)) { + throw new \ErrorException('Creation of dynamic property is not allowed', 0); + } + // if not linear + if (!$this->linear && ($value < 0 || $value > 255)) { + throw new \LengthException('Argument value ' . $value . ' for color ' . $name + . ' is not in the range of 0 to 255', 1); + } elseif ($this->linear && ($value < -10E10 || $value > 1)) { + // not allow very very small negative numbers + throw new \LengthException('Argument value ' . $value . ' for color ' . $name + . ' is not in the range of 0 to 1 for linear rgb', 1); + } + $this->$name = $value; + } + + /** + * get color + * + * @param string $name + * @return float|bool + */ + public function __get(string $name): float|bool + { + if (!property_exists($this, $name)) { + throw new \ErrorException('Creation of dynamic property is not allowed', 0); + } + return $this->$name; + } + + /** + * Returns the color as array + * where 0: Red, 1: Green, 2: Blue + * + * @return array{0:float,1:float,2:float} + */ + public function returnAsArray(): array + { + return [$this->R, $this->G, $this->B]; + } + + /** + * set color as array + * where 0: Red, 1: Green, 2: Blue + * + * @param array{0:float,1:float,2:float} $rgb + * @return self + */ + public function setAsArray(array $rgb): self + { + $this->__set('R', $rgb[0]); + $this->__set('G', $rgb[1]); + $this->__set('B', $rgb[2]); + return $this; + } + + /** + * set as linear + * can be used as chain call on create if input is linear RGB + * RGB::__construct**(...)->flagLinear(); + * as it returns self + * + * @return self + */ + private function flagLinear(bool $linear): self + { + $this->linear = $linear; + return $this; + } + + /** + * Both function source: + * https://bottosson.github.io/posts/colorwrong/#what-can-we-do%3F + * but reverse f: fromLinear and f_inv for toLinear + * Code copied from here: + * https://stackoverflow.com/a/12894053 + * + * converts RGB to linear + * We come from 0-255 so we need to divide by 255 + * + * @return self + */ + public function toLinear(): self + { + $this->flagLinear(true)->setAsArray(array_map( + callback: function (int|float $v) { + $v = (float)($v / 255); + $abs = abs($v); + $sign = ($v < 0) ? -1 : 1; + return (float)( + $abs <= 0.04045 ? + $v / 12.92 : + $sign * pow(($abs + 0.055) / 1.055, 2.4) + ); + }, + array: $this->returnAsArray(), + )); + return $this; + } + + /** + * convert back to normal sRGB from linear RGB + * we go to 0-255 rgb so we multiply by 255 + * + * @return self + */ + public function fromLinear(): self + { + $this->flagLinear(false)->setAsArray(array_map( + callback: function (int|float $v) { + $abs = abs($v); + $sign = ($v < 0) ? -1 : 1; + // during reverse in some situations the values can become negative in very small ways + // like -...E16 and ...E17 + return ($ret = (float)(255 * ( + $abs <= 0.0031308 ? + $v * 12.92 : + $sign * (1.055 * pow($abs, 1.0 / 2.4) - 0.055) + ))) < 0 ? 0 : $ret; + }, + array: $this->returnAsArray(), + )); + // $this->linear = false; + return $this; + } + + /** + * convert to css string with optional opacity + * Note: if this is a linea RGB, this data will not be correct + * + * @param float|string|null $opacity + * @return string + */ + public function toCssString(null|float|string $opacity = null): string + { + // set opacity, either a string or float + if (is_string($opacity)) { + $opacity = ' / ' . $opacity; + } elseif ($opacity !== null) { + $opacity = ' / ' . $opacity; + } else { + $opacity = ''; + } + return 'rgb(' + . (int)round($this->R, 0) + . ' ' + . (int)round($this->G, 0) + . ' ' + . (int)round($this->B, 0) + . $opacity + . ')'; + } +} + +// __END__ diff --git c/www/lib/CoreLibs/Convert/Color/Coordinates/XYZD65.php i/www/lib/CoreLibs/Convert/Color/Coordinates/XYZD65.php new file mode 100644 index 00000000..ebdf633d --- /dev/null +++ i/www/lib/CoreLibs/Convert/Color/Coordinates/XYZD65.php @@ -0,0 +1,116 @@ +setAsArray([$X, $Y, $Z]); + } + + /** + * set from array + * where 0: X, 1: Y, 2: Z + * + * @param array{0:float,1:float,2:float} $xyzD65 + * @return self + */ + public static function __constructFromArray(array $xyzD65): self + { + return (new XYZD65())->setAsArray($xyzD65); + } + + /** + * set color + * + * @param string $name + * @param float $value + * @return void + */ + public function __set(string $name, float $value): void + { + if (!property_exists($this, $name)) { + throw new \ErrorException('Creation of dynamic property is not allowed', 0); + } + // if ($value < 0 || $value > 255) { + // throw new \LengthException('Argument value ' . $value . ' for color ' . $name + // . ' is not in the range of 0 to 255', 1); + // } + $this->$name = $value; + } + + /** + * get color + * + * @param string $name + * @return float + */ + public function __get(string $name): float + { + if (!property_exists($this, $name)) { + throw new \ErrorException('Creation of dynamic property is not allowed', 0); + } + return $this->$name; + } + + /** + * Returns the color as array + * where 0: X, 1: Y, 2: Z + * + * @return array{0:float,1:float,2:float} + */ + public function returnAsArray(): array + { + return [$this->X, $this->Y, $this->Z]; + } + + /** + * set color as array + * where 0: X, 1: Y, 2: Z + * + * @param array{0:float,1:float,2:float} $xyzD65 + * @return self + */ + public function setAsArray(array $xyzD65): self + { + $this->__set('X', $xyzD65[0]); + $this->__set('Y', $xyzD65[1]); + $this->__set('Z', $xyzD65[2]); + return $this; + } +} + +// __END__ diff --git c/www/lib/CoreLibs/Convert/Color/OkLab.php i/www/lib/CoreLibs/Convert/Color/OkLab.php new file mode 100644 index 00000000..2bbdbbc2 --- /dev/null +++ i/www/lib/CoreLibs/Convert/Color/OkLab.php @@ -0,0 +1,80 @@ + oklab + * oklab -> rgb + * rgb -> okhsl + * okshl -> rgb + * rgb -> okhsv + * okhsv -> rgb +*/ + +declare(strict_types=1); + +namespace CoreLibs\Convert\Color; + +class OkLab +{ + /** + * lines sRGB to oklab + * + * @param int $red + * @param int $green + * @param int $blue + * @return array + */ + public static function srgb2okLab(int $red, int $green, int $blue): array + { + $l = (float)0.4122214708 * (float)$red + + (float)0.5363325363 * (float)$green + + (float)0.0514459929 * (float)$blue; + $m = (float)0.2119034982 * (float)$red + + (float)0.6806995451 * (float)$green + + (float)0.1073969566 * (float)$blue; + $s = (float)0.0883024619 * (float)$red + + (float)0.2817188376 * (float)$green + + (float)0.6299787005 * (float)$blue; + + // cbrtf = 3 root (val) + $l_ = pow($l, 1.0 / 3); + $m_ = pow($m, 1.0 / 3); + $s_ = pow($s, 1.0 / 3); + + return [ + (float)0.2104542553 * $l_ + (float)0.7936177850 * $m_ - (float)0.0040720468 * $s_, + (float)1.9779984951 * $l_ - (float)2.4285922050 * $m_ + (float)0.4505937099 * $s_, + (float)0.0259040371 * $l_ + (float)0.7827717662 * $m_ - (float)0.8086757660 * $s_, + ]; + } + + /** + * convert okLab to linear sRGB + * + * @param float $L + * @param float $a + * @param float $b + * @return array + */ + public static function okLab2srgb(float $L, float $a, float $b): array + { + $l_ = $L + (float)0.3963377774 * $a + (float)0.2158037573 * $b; + $m_ = $L - (float)0.1055613458 * $a - (float)0.0638541728 * $b; + $s_ = $L - (float)0.0894841775 * $a - (float)1.2914855480 * $b; + + $l = $l_ * $l_ * $l_; + $m = $m_ * $m_ * $m_; + $s = $s_ * $s_ * $s_; + + return [ + (int)round(+(float)4.0767416621 * $l - (float)3.3077115913 * $m + (float)0.2309699292 * $s), + (int)round(-(float)1.2684380046 * $l + (float)2.6097574011 * $m - (float)0.3413193965 * $s), + (int)round(-(float)0.0041960863 * $l - (float)0.7034186147 * $m + (float)1.7076147010 * $s), + ]; + } +} + +// __END__ diff --git c/www/lib/CoreLibs/Convert/Color/Stringify.php i/www/lib/CoreLibs/Convert/Color/Stringify.php new file mode 100644 index 00000000..6ca68431 --- /dev/null +++ i/www/lib/CoreLibs/Convert/Color/Stringify.php @@ -0,0 +1,35 @@ +toCssString($opacity); + } +} + +// __END__ diff --git c/www/lib/CoreLibs/Convert/Colors.php i/www/lib/CoreLibs/Convert/Colors.php index f9f56171..8ff32608 100644 --- c/www/lib/CoreLibs/Convert/Colors.php +++ i/www/lib/CoreLibs/Convert/Colors.php @@ -120,26 +120,29 @@ class Colors $MAX = max($red, $green, $blue); $MIN = min($red, $green, $blue); $HUE = 0; + $DELTA = $MAX - $MIN; + // achromatic if ($MAX == $MIN) { return [0, 0, round($MAX * 100)]; } if ($red == $MAX) { - $HUE = ($green - $blue) / ($MAX - $MIN); + $HUE = fmod(($green - $blue) / $DELTA, 6); } elseif ($green == $MAX) { - $HUE = 2 + (($blue - $red) / ($MAX - $MIN)); + $HUE = (($blue - $red) / $DELTA) + 2; } elseif ($blue == $MAX) { - $HUE = 4 + (($red - $green) / ($MAX - $MIN)); + $HUE = (($red - $green) / $DELTA) + 4; } $HUE *= 60; + // avoid negative if ($HUE < 0) { $HUE += 360; } return [ - (int)round($HUE), - (int)round((($MAX - $MIN) / $MAX) * 100), - (int)round($MAX * 100) + (int)round($HUE), // Hue + (int)round(($DELTA / $MAX) * 100), // Saturation + (int)round($MAX * 100) // Value/Brightness ]; } diff --git c/www/lib/CoreLibs/Convert/Math.php i/www/lib/CoreLibs/Convert/Math.php index 205abbf1..26739c5f 100644 --- c/www/lib/CoreLibs/Convert/Math.php +++ i/www/lib/CoreLibs/Convert/Math.php @@ -56,6 +56,95 @@ class Math return (float)$number; } } + + /** + * calc cube root + * + * @param float $number Number to cubic root + * @return float Calculated value + */ + public static function cbrt(float $number): float + { + return pow($number, 1.0 / 3); + } + + /** + * This function is directly inspired by the multiplyMatrices() function in color.js + * form Lea Verou and Chris Lilley. + * (see https://github.com/LeaVerou/color.js/blob/main/src/multiply-matrices.js) + * From: + * https://github.com/matthieumastadenis/couleur/blob/3842cf51c9517e77afaa0a36ec78643a0c258e0b/src/utils/utils.php#L507 + * + * It returns an array which is the product of the two number matrices passed as parameters. + * + * @param array> $a m x n matrice + * @param array> $b n x p matrice + * + * @return array> m x p product + */ + public static function multiplyMatrices(array $a, array $b): array + { + $m = count($a); + + if (!is_array($a[0] ?? null)) { + // $a is vector, convert to [[a, b, c, ...]] + $a = [ $a ]; + } + + if (!is_array($b[0])) { + // $b is vector, convert to [[a], [b], [c], ...]] + $b = array_map( + callback: fn ($v) => [ $v ], + array: $b, + ); + } + + $p = count($b[0]); + + // transpose $b: + $bCols = array_map( + callback: fn ($k) => \array_map( + (fn ($i) => $i[$k]), + $b, + ), + array: array_keys($b[0]), + ); + + $product = array_map( + callback: fn ($row) => array_map( + callback: fn ($col) => is_array($row) ? + array_reduce( + array: $row, + callback: fn ($a, $v, $i = null) => $a + $v * ( + $col[$i ?? array_search($v, $row)] ?? 0 + ), + initial: 0, + ) : + array_reduce( + array: $col, + callback: fn ($a, $v) => $a + $v * $row, + initial: 0, + ), + array: $bCols, + ), + array: $a, + ); + + if ($m === 1) { + // Avoid [[a, b, c, ...]]: + $product = $product[0]; + } + + if ($p === 1) { + // Avoid [[a], [b], [c], ...]]: + return array_map( + callback: fn ($v) => $v[0], + array: $product, + ); + } + + return $product; + } } // __END__ --- .../tests/Convert/CoreLibsConvertMathTest.php | 2 + www/admin/class_test.convert.colors.php | 48 +- www/lib/CoreLibs/Convert/Color/Color.php | 803 ++++++++++++++++++ .../Convert/Color/Coordinates/HSB.php | 155 ++++ .../Convert/Color/Coordinates/HSL.php | 140 +++ .../Convert/Color/Coordinates/HWB.php | 140 +++ .../Convert/Color/Coordinates/LCH.php | 169 ++++ .../Convert/Color/Coordinates/Lab.php | 177 ++++ .../Convert/Color/Coordinates/RGB.php | 226 +++++ .../Convert/Color/Coordinates/XYZD65.php | 116 +++ www/lib/CoreLibs/Convert/Color/OkLab.php | 80 ++ www/lib/CoreLibs/Convert/Color/Stringify.php | 35 + www/lib/CoreLibs/Convert/Colors.php | 15 +- www/lib/CoreLibs/Convert/Math.php | 89 ++ 14 files changed, 2184 insertions(+), 11 deletions(-) create mode 100644 www/lib/CoreLibs/Convert/Color/Color.php create mode 100644 www/lib/CoreLibs/Convert/Color/Coordinates/HSB.php create mode 100644 www/lib/CoreLibs/Convert/Color/Coordinates/HSL.php create mode 100644 www/lib/CoreLibs/Convert/Color/Coordinates/HWB.php create mode 100644 www/lib/CoreLibs/Convert/Color/Coordinates/LCH.php create mode 100644 www/lib/CoreLibs/Convert/Color/Coordinates/Lab.php create mode 100644 www/lib/CoreLibs/Convert/Color/Coordinates/RGB.php create mode 100644 www/lib/CoreLibs/Convert/Color/Coordinates/XYZD65.php create mode 100644 www/lib/CoreLibs/Convert/Color/OkLab.php create mode 100644 www/lib/CoreLibs/Convert/Color/Stringify.php diff --git a/4dev/tests/Convert/CoreLibsConvertMathTest.php b/4dev/tests/Convert/CoreLibsConvertMathTest.php index 9a97e37e..c98b4b2a 100644 --- a/4dev/tests/Convert/CoreLibsConvertMathTest.php +++ b/4dev/tests/Convert/CoreLibsConvertMathTest.php @@ -113,6 +113,8 @@ final class CoreLibsConvertMathTest extends TestCase \CoreLibs\Convert\Math::initNumeric($input) ); } + + // TODO: cbrt tests } // __END__ diff --git a/www/admin/class_test.convert.colors.php b/www/admin/class_test.convert.colors.php index 6f809691..a37cb2df 100644 --- a/www/admin/class_test.convert.colors.php +++ b/www/admin/class_test.convert.colors.php @@ -19,6 +19,8 @@ $LOG_FILE_ID = 'classTest-convert-colors'; ob_end_flush(); use CoreLibs\Convert\Colors; +use CoreLibs\Convert\Color\Color; +use CoreLibs\Convert\Color\Coordinates; use CoreLibs\Debug\Support as DgS; use CoreLibs\Convert\SetVarType; @@ -52,16 +54,16 @@ try { print "**Exception: " . $e->getMessage() . "
" . print_r($e, true) . "

"; } // B(valid) -$rgb = [10, 20, 30]; +$rgb = [50, 20, 30]; $hex = '#0a141e'; $hsb = [210, 67, 12]; $hsb_f = [210.5, 67.5, 12.5]; -$hsl = [210, 50, 7.8]; +$hsb = [210, 50, 7.8]; print "S::COLOR rgb->hex: $rgb[0], $rgb[1], $rgb[2]: " . Colors::rgb2hex($rgb[0], $rgb[1], $rgb[2]) . "
"; print "S::COLOR hex->rgb: $hex: " . DgS::printAr(SetVarType::setArray( Colors::hex2rgb($hex) )) . "
"; -print "C::S/COLOR rgb->hext: $hex: " . DgS::printAr(SetVarType::setArray( +print "C::S/COLOR rgb->hex: $hex: " . DgS::printAr(SetVarType::setArray( CoreLibs\Convert\Colors::hex2rgb($hex) )) . "
"; // C(to hsb/hsl) @@ -82,9 +84,9 @@ print "S::COLOR hsb_f->rgb: $hsb_f[0], $hsb_f[1], $hsb_f[2]: " . DgS::printAr(SetVarType::setArray( Colors::hsb2rgb($hsb_f[0], $hsb_f[1], $hsb_f[2]) )) . "
"; -print "S::COLOR hsl->rgb: $hsl[0], $hsl[1], $hsl[2]: " +print "S::COLOR hsl->rgb: $hsb[0], $hsb[1], $hsb[2]: " . DgS::printAr(SetVarType::setArray( - Colors::hsl2rgb($hsl[0], $hsl[1], $hsl[2]) + Colors::hsl2rgb($hsb[0], $hsb[1], $hsb[2]) )) . "
"; $hsb = [0, 0, 5]; @@ -102,8 +104,44 @@ print "RANDOM IN: H: " . $h . ", S: " . $s . ", B/L: " . $b . "/" . $l . "
"; print "RANDOM hsb->rgb:
" . DgS::printAr(SetVarType::setArray(Colors::hsb2rgb($h, $s, $b))) . "

"; print "RANDOM hsl->rgb:
" . DgS::printAr(SetVarType::setArray(Colors::hsl2rgb($h, $s, $l))) . "

"; +$rgb = [0, 0, 0]; +print "rgb 0,0,0: " . Dgs::printAr($rgb) . " => " . Dgs::printAr(Colors::rgb2hsb($rgb[0], $rgb[1], $rgb[2])) . "
"; + // TODO: run compare check input must match output +$hwb = Color::hsbToHwb(Coordinates\HSB::__constructFromArray([ + 160, + 0, + 50, +])); +print "HWB: " . DgS::printAr($hwb) . "
"; +$hsb = Color::hwbToHsb($hwb); +print "HSB: " . DgS::printAr($hsb) . "
"; + +$oklch = Color::rgbToOkLch(Coordinates\RGB::__constructFromArray([ + 250, + 0, + 0 +])); +print "OkLch: " . DgS::printAr($oklch) . "
"; +$rgb = Color::okLchToRgb($oklch); +print "OkLch -> RGB: " . DgS::printAr($rgb) . "
"; + +$oklab = Color::rgbToOkLab(Coordinates\RGB::__constructFromArray([ + 250, + 0, + 0 +])); +print "OkLab: " . DgS::printAr($oklab) . "
"; +$rgb = Color::okLabToRgb($oklab); +print "OkLab -> RGB: " . DgS::printAr($rgb) . "
"; + +$rgb = Coordinates\RGB::__constructFromArray([250, 100, 10])->toLinear(); +print "RGBlinear: " . DgS::printAr($rgb) . "
"; +$rgb = Coordinates\RGB::__constructFromArray([0, 0, 0])->toLinear(); +print "RGBlinear: " . DgS::printAr($rgb) . "
"; + + print ""; // __END__ diff --git a/www/lib/CoreLibs/Convert/Color/Color.php b/www/lib/CoreLibs/Convert/Color/Color.php new file mode 100644 index 00000000..c56f8c11 --- /dev/null +++ b/www/lib/CoreLibs/Convert/Color/Color.php @@ -0,0 +1,803 @@ + HSL + + /** + * converts a RGB (0-255) to HSL + * return: + * class with hue (0-360), saturation (0-100%) and luminance (0-100%) + * + * @param RGB $rgb Class for rgb + * @return HSL Class hue/sat/luminance + */ + public static function rgbToHsl(RGB $rgb): HSL + { + $red = $rgb->R / 255; + $green = $rgb->G / 255; + $blue = $rgb->B / 255; + + $min = min($red, $green, $blue); + $max = max($red, $green, $blue); + $chroma = $max - $min; + $sat = 0; + $hue = 0; + // luminance + $lum = ($max + $min) / 2; + + // achromatic + if ($chroma == 0) { + // H, S, L + return HSL::__constructFromArray([ + 0.0, + 0.0, + $lum * 100, + ]); + } else { + $sat = $chroma / (1 - abs(2 * $lum - 1)); + if ($max == $red) { + $hue = fmod((($green - $blue) / $chroma), 6); + if ($hue < 0) { + $hue = (6 - fmod(abs($hue), 6)); + } + } elseif ($max == $green) { + $hue = ($blue - $red) / $chroma + 2; + } elseif ($max == $blue) { + $hue = ($red - $green) / $chroma + 4; + } + $hue = $hue * 60; + // $sat = 1 - abs(2 * $lum - 1); + return HSL::__constructFromArray([ + $hue, + $sat * 100, + $lum * 100, + ]); + } + } + + /** + * converts an HSL to RGB + * if HSL value is invalid, set this value to 0 + * + * @param HSL $hsl Class with hue: 0-360 (degrees), + * saturation: 0-100, + * luminance: 0-100 + * @return RGB Class for rgb + */ + public static function hslToRgb(HSL $hsl): RGB + { + $hue = $hsl->H; + $sat = $hsl->S; + $lum = $hsl->L; + // calc to internal convert value for hue + $hue = (1 / 360) * $hue; + // convert to internal 0-1 format + $sat /= 100; + $lum /= 100; + // if saturation is 0 + if ($sat == 0) { + $lum = round($lum * 255); + return RGB::__constructFromArray([$lum, $lum, $lum]); + } else { + $m2 = $lum < 0.5 ? $lum * ($sat + 1) : ($lum + $sat) - ($lum * $sat); + $m1 = $lum * 2 - $m2; + $hueue = function ($base) use ($m1, $m2) { + // base = hue, hue > 360 (1) - 360 (1), else < 0 + 360 (1) + $base = $base < 0 ? $base + 1 : ($base > 1 ? $base - 1 : $base); + // 6: 60, 2: 180, 3: 240 + // 2/3 = 240 + // 1/3 = 120 (all from 360) + if ($base * 6 < 1) { + return $m1 + ($m2 - $m1) * $base * 6; + } + if ($base * 2 < 1) { + return $m2; + } + if ($base * 3 < 2) { + return $m1 + ($m2 - $m1) * ((2 / 3) - $base) * 6; + } + return $m1; + }; + + return RGB::__constructFromArray([ + 255 * $hueue($hue + (1 / 3)), + 255 * $hueue($hue), + 255 * $hueue($hue - (1 / 3)), + ]); + } + } + + // MARK: RGB <-> HSB + + /** + * rgb2hsb does not clean convert back to rgb in a round trip + * converts RGB to HSB/V values + * returns: + * Class with hue (0-360), sat (0-100%), brightness/value (0-100%) + * + * @param RGB $rgb Class for rgb + * @return HSB Class Hue, Sat, Brightness/Value + */ + public static function rgbToHsb(RGB $rgb): HSB + { + $red = $rgb->R / 255; + $green = $rgb->G / 255; + $blue = $rgb->B / 255; + + $MAX = max($red, $green, $blue); + $MIN = min($red, $green, $blue); + $HUE = 0; + $DELTA = $MAX - $MIN; + + // achromatic + if ($MAX == $MIN) { + return HSB::__constructFromArray([0, 0, $MAX * 100]); + } + if ($red == $MAX) { + $HUE = fmod(($green - $blue) / $DELTA, 6); + } elseif ($green == $MAX) { + $HUE = (($blue - $red) / $DELTA) + 2; + } elseif ($blue == $MAX) { + $HUE = (($red - $green) / $DELTA) + 4; + } + $HUE *= 60; + // avoid negative + if ($HUE < 0) { + $HUE += 360; + } + + return HSB::__constructFromArray([ + $HUE, // Hue + ($DELTA / $MAX) * 100, // Saturation + $MAX * 100, // Brightness + ]); + } + + /** + * hsb2rgb does not clean convert back to hsb in a round trip + * converts HSB/V to RGB values RGB is full INT + * if HSB/V value is invalid, sets this value to 0 + * + * @param HSB $hsb hue 0-360 (int), + * saturation 0-100 (int), + * brightness/value 0-100 (int) + * @return RGB Class for RGB + */ + public static function hsbToRgb(HSB $hsb): RGB + { + $H = $hsb->H; + $S = $hsb->S; + $V = $hsb->B; + // convert to internal 0-1 format + $S /= 100; + $V /= 100; + + if ($S == 0) { + $V = $V * 255; + return RGB::__constructFromArray([$V, $V, $V]); + } + + $Hi = floor($H / 60); + $f = ($H / 60) - $Hi; + $p = $V * (1 - $S); + $q = $V * (1 - ($S * $f)); + $t = $V * (1 - ($S * (1 - $f))); + + switch ($Hi) { + case 0: + $red = $V; + $green = $t; + $blue = $p; + break; + case 1: + $red = $q; + $green = $V; + $blue = $p; + break; + case 2: + $red = $p; + $green = $V; + $blue = $t; + break; + case 3: + $red = $p; + $green = $q; + $blue = $V; + break; + case 4: + $red = $t; + $green = $p; + $blue = $V; + break; + case 5: + $red = $V; + $green = $p; + $blue = $q; + break; + default: + $red = 0; + $green = 0; + $blue = 0; + } + + return RGB::__constructFromArray([ + $red * 255, + $green * 255, + $blue * 255, + ]); + } + + // MARK: HSL <-> HSB + + /** + * Convert HSL to HSB + * + * @param HSL $hsl + * @return HSB + */ + public static function hslToHsb(HSL $hsl): HSB + { + $saturation = $hsl->S / 100; + $lightness = $hsl->L / 100; + $value = $lightness + $saturation * min($lightness, 1 - $lightness); + // check for black and white + $saturation = ($value === 0) ? + 0 : + 200 * (1 - $lightness / $value); + return HSB::__constructFromArray([ + $hsl->H, + $saturation, + $value * 100, + ]); + } + + /** + * Convert HSB to HSL + * + * @param HSB $hsb + * @return HSL + */ + public static function hsbToHsl(HSB $hsb): HSL + { + // hsv/toHsl + $hue = $hsb->H; + $saturation = $hsb->S / 100; + $value = $hsb->V / 100; + + $lightness = $value * (1 - $saturation / 2); + // check for B/W + $saturation = in_array($lightness, [0, 1], true) ? + 0 : + 100 * ($value - $lightness) / min($lightness, 1 - $lightness) + ; + + return HSL::__constructFromArray([ + $hue, + $saturation, + $lightness * 100, + ]); + } + + // MARK: HSB <-> HWB + + /** + * convert HSB to HWB + * + * @param HSB $hsb + * @return HWB + */ + public static function hsbToHwb(HSB $hsb): HWB + { + // hsv\Hwb + return HWB::__constructFromArray([ + $hsb->H, // hue, + $hsb->B * (100 - $hsb->S) / 100, // 2: brightness, 1: saturation + 100 - $hsb->B, + ]); + } + + /** + * convert HWB to HSB + * + * @param HWB $hwb + * @return HSB + */ + public static function hwbToHsb(HWB $hwb): HSB + { + $hue = $hwb->H; + $whiteness = $hwb->W / 100; + $blackness = $hwb->B / 100; + + $sum = $whiteness + $blackness; + // for black and white + if ($sum >= 1) { + $saturation = 0; + $value = $whiteness / $sum * 100; + } else { + $value = 1 - $blackness; + $saturation = $value === 0 ? 0 : (1 - $whiteness / $value) * 100; + $value *= 100; + } + + return HSB::__constructFromArray([ + $hue, + $saturation, + $value, + ]); + } + + // MARK: RGB <-> HWB + + /** + * Convert RGB to HWB + * via rgb -> hsl -> hsb -> hwb + * + * @param RGB $rgb + * @return HWB + */ + public static function rgbToHwb(RGB $rgb): HWB + { + return self::hsbToHwb( + self::hslToHsb( + self::rgbToHsl($rgb) + ) + ); + } + + /** + * Convert HWB to RGB + * via hwb -> hsb -> hsl -> rgb + * + * @param HWB $hwb + * @return RGB + */ + public static function hwbToRgb(HWB $hwb): RGB + { + return self::hslToRgb( + self::hsbToHsl( + self::hwbToHsb($hwb) + ) + ); + } + + // MARK: HSL <-> HWB + + /** + * Convert HSL to HWB + * via hsl -> hsb -> hwb + * + * @param HSL $hsl + * @return HWB + */ + public static function hslToHwb(HSL $hsl): HWB + { + return self::hsbToHwb( + self::hslToHsb( + $hsl + ) + ); + } + + /** + * Convert HWB to HSL + * via hwb -> hsb -> hsl + * + * @param HWB $hwb + * @return HSL + */ + public static function hwbToHsl(HWB $hwb): HSL + { + return self::hsbToHsl( + self::hwbToHsb($hwb) + ); + } + + // MARK: OkLch <-> OkLab + + /** + * okLAab to okLCH + * + * @param Lab $lab + * @return LCH + */ + public static function okLabToOkLch(Lab $lab): LCH + { + // okLab\toOkLch + $a = $lab->a; + $b = $lab->b; + + $hue = atan2($b, $a) * 180 / pi(); + + return LCH::__constructFromArray([ + $lab->L, + sqrt($a ** 2 + $b ** 2), + $hue >= 0 ? $hue : $hue + 360, + ]); + } + + /** + * okLCH to okLab + * + * @param LCH $lch + * @return Lab + */ + public static function okLchToOkLab(LCH $lch): Lab + { + // oklch/toOkLab + // oklch to oklab + return Lab::__constructFromArray([ + $lch->L, + $lch->C * cos($lch->H * pi() / 180), // a + $lch->C * sin($lch->H * pi() / 180), // b + ], 'Oklab'); + } + + // MARK: xyzD65 <-> linearRGB + + /** + * convert linear RGB to xyz D65 + * if rgb is not flagged linear, it will be auto converted + * + * @param RGB $rgb + * @return XYZD65 + */ + public static function linRgbToXyzD65(RGB $rgb): XYZD65 + { + // if not linear, convert to linear + if (!$rgb->linear) { + $rgb->toLinear(); + } + return XYZD65::__constructFromArray(Math::multiplyMatrices( + [ + [0.41239079926595934, 0.357584339383878, 0.1804807884018343], + [0.21263900587151027, 0.715168678767756, 0.07219231536073371], + [0.01933081871559182, 0.11919477979462598, 0.9505321522496607], + ], + $rgb->returnAsArray() + )); + } + + /** + * Convert xyz D65 to linear RGB + * + * @param XYZD65 $xyzD65 + * @return RGB + */ + public static function xyzD65ToLinRgb(XYZD65 $xyzD65): RGB + { + // xyz D65 to linrgb + return RGB::__constructFromArray(Math::multiplyMatrices( + a : [ + [ 3.2409699419045226, -1.537383177570094, -0.4986107602930034 ], + [ -0.9692436362808796, 1.8759675015077202, 0.04155505740717559 ], + [ 0.05563007969699366, -0.20397695888897652, 1.0569715142428786 ], + ], + b : $xyzD65->returnAsArray() + ), linear: true); + } + + // MARK: xyzD65 <-> OkLab + + /** + * xyz D65 to OkLab + * + * @param XYZD65 $xyzD65 + * @return Lab + */ + public static function xyzD65ToOkLab(XYZD65 $xyzD65): Lab + { + return Lab::__constructFromArray(Math::multiplyMatrices( + [ + [0.2104542553, 0.7936177850, -0.0040720468], + [1.9779984951, -2.4285922050, 0.4505937099], + [0.0259040371, 0.7827717662, -0.8086757660], + ], + array_map( + callback: fn ($v) => pow($v, 1 / 3), + array: Math::multiplyMatrices( + a: [ + [0.8190224432164319, 0.3619062562801221, -0.12887378261216414], + [0.0329836671980271, 0.9292868468965546, 0.03614466816999844], + [0.048177199566046255, 0.26423952494422764, 0.6335478258136937], + ], + b: $xyzD65->returnAsArray(), + ), + ) + ), 'Oklab'); + } + + /** + * xyz D65 to OkLab + * + * @param Lab $lab + * @return XYZD65 + */ + public static function okLabToXyzD65(Lab $lab): XYZD65 + { + return XYZD65::__constructFromArray(Math::multiplyMatrices( + a: [ + [1.2268798733741557, -0.5578149965554813, 0.28139105017721583], + [-0.04057576262431372, 1.1122868293970594, -0.07171106666151701], + [-0.07637294974672142, -0.4214933239627914, 1.5869240244272418], + ], + b: array_map( + callback: fn ($v) => is_numeric($v) ? $v ** 3 : 0, + array: Math::multiplyMatrices( + a: [ + [0.99999999845051981432, 0.39633779217376785678, 0.21580375806075880339], + [1.0000000088817607767, -0.1055613423236563494, -0.063854174771705903402], + [1.0000000546724109177, -0.089484182094965759684, -1.2914855378640917399], + ], + // Divide $lightness by 100 to convert from CSS OkLab + b: $lab->returnAsArray(), + ), + ), + )); + } + + // MARK: rgb <-> oklab + + /** + * Undocumented function + * + * @param RGB $rgb + * @return Lab + */ + public static function rgbToOkLab(RGB $rgb): Lab + { + return self::xyzD65ToOkLab( + self::linRgbToXyzD65($rgb) + ); + } + + /** + * Undocumented function + * + * @param Lab $lab + * @return RGB + */ + public static function okLabToRgb(Lab $lab): RGB + { + return self::xyzD65ToLinRgb( + self::okLabToXyzD65($lab) + )->fromLinear(); + } + + // MARK: rgb <-> oklch + + /** + * convert rgb to OkLch + * via rgb -> linear rgb -> xyz D65 -> OkLab -> OkLch + * + * @param RGB $rbh + * @return LCH + */ + public static function rgbToOkLch(RGB $rgb): LCH + { + return self::okLabToOkLch( + self::rgbToOkLab($rgb) + ); + } + + /** + * Convert OkLch to rgb + * via OkLab -> OkLch -> xyz D65 -> linear rgb -> rgb + * + * @param LCH $lch + * @return RGB + */ + public static function okLchToRgb(LCH $lch): RGB + { + return self::okLabToRgb( + self::okLchToOkLab($lch) + ); + } + + // MARK: HSL <-> OKLab + + /** + * Undocumented function + * + * @param HSL $hsl + * @return Lab + */ + public static function hslToOkLab(HSL $hsl): Lab + { + return self::rgbToOkLab( + self::hslToRgb($hsl) + ); + } + + /** + * Undocumented function + * + * @param Lab $lab + * @return HSL + */ + public static function okLabToHsl(Lab $lab): HSL + { + return self::rgbToHsl( + self::okLabToRgb($lab) + ); + } + + // MARK: HSL <-> OKLCH + + /** + * Undocumented function + * + * @param HSL $hsl + * @return LCH + */ + public static function hslToOkLch(HSL $hsl): LCH + { + return self::rgbToOkLch( + self::hslToRgb($hsl) + ); + } + + /** + * Undocumented function + * + * @param LCH $lch + * @return HSL + */ + public static function okLchToHsl(LCH $lch): HSL + { + return self::rgbToHsl( + self::okLchToRgb($lch) + ); + } + + // MARK: HSB <-> OKLab + + /** + * Undocumented function + * + * @param HSB $hsb + * @return Lab + */ + public static function hsbToOkLab(HSB $hsb): Lab + { + return self::rgbToOkLab( + self::hsbToRgb($hsb) + ); + } + + /** + * Undocumented function + * + * @param Lab $lab + * @return HSB + */ + public static function okLabToHsb(Lab $lab): HSB + { + return self::rgbToHsb( + self::okLabToRgb($lab) + ); + } + + // MARK: HSB <-> OKLCH + + /** + * Undocumented function + * + * @param HSB $hsb + * @return LCH + */ + public static function hsbToOkLch(HSB $hsb): LCH + { + return self::rgbToOkLch( + self::hsbToRgb($hsb) + ); + } + + /** + * Undocumented function + * + * @param LCH $lch + * @return HSB + */ + public static function okLchToHsb(LCH $lch): HSB + { + return self::rgbToHsb( + self::okLchToRgb($lch) + ); + } + + // MARK: HWB <-> OKLab + + /** + * Undocumented function + * + * @param HWB $hwb + * @return Lab + */ + public function hwbToOkLab(HWB $hwb): Lab + { + return self::rgbToOkLab( + self::hwbToRgb($hwb) + ); + } + + /** + * Undocumented function + * + * @param Lab $lab + * @return HWB + */ + public function okLabToHwb(Lab $lab): HWB + { + return self::rgbToHwb( + self::okLabToRgb($lab) + ); + } + + // MARK: HWB <-> OKLCH + + /** + * Undocumented function + * + * @param HWB $hwb + * @return LCH + */ + public function hwbToOkLch(HWB $hwb): LCH + { + return self::rgbToOkLch( + self::hwbToRgb($hwb) + ); + } + + /** + * Undocumented function + * + * @param LCH $lch + * @return HWB + */ + public function okLchToHwb(LCH $lch): HWB + { + return self::rgbToHwb( + self::okLchToRgb($lch) + ); + } +} diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/HSB.php b/www/lib/CoreLibs/Convert/Color/Coordinates/HSB.php new file mode 100644 index 00000000..b435a9ef --- /dev/null +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/HSB.php @@ -0,0 +1,155 @@ +setAsArray([$H, $S, $B]); + } + + /** + * set from array + * where 0: Hue, 1: Saturation, 2: Brightness + * + * @param array{0:float,1:float,2:float} $hsb + * @return self + */ + public static function __constructFromArray(array $hsb): self + { + return (new HSB())->setAsArray($hsb); + } + + /** + * set color + * + * @param string $name + * @param float $value + * @return void + */ + public function __set(string $name, float $value): void + { + $name = strtoupper($name); + if (!property_exists($this, $name)) { + throw new \ErrorException('Creation of dynamic property is not allowed', 0); + } + switch ($name) { + case 'H': + if ($value == 360) { + $value = 0; + } + if ($value < 0 || $value > 359) { + throw new \LengthException( + 'Argument value ' . $value . ' for hue is not in the range of 0 to 359', + 1 + ); + } + break; + case 'S': + if ($value < 0 || $value > 100) { + throw new \LengthException( + 'Argument value ' . $value . ' for saturation is not in the range of 0 to 100', + 2 + ); + } + break; + case 'B': + if ($value < 0 || $value > 100) { + throw new \LengthException( + 'Argument value ' . $value . ' for brightness is not in the range of 0 to 100', + 3 + ); + } + break; + } + $this->$name = $value; + } + + /** + * get color + * + * @param string $name + * @return float + */ + public function __get(string $name): float + { + $name = strtoupper($name); + if (!property_exists($this, $name)) { + throw new \ErrorException('Creation of dynamic property is not allowed', 0); + } + return $this->$name; + } + + /** + * Returns the color as array + * where 0: Hue, 1: Saturation, 2: Brightness + * + * @return array{0:float,1:float,2:float} + */ + public function returnAsArray(): array + { + return [$this->H, $this->S, $this->B]; + } + + /** + * set color as array + * where 0: Hue, 1: Saturation, 2: Brightness + * + * @param array{0:float,1:float,2:float} $hsb + * @return self + */ + public function setAsArray(array $hsb): self + { + $this->__set('H', $hsb[0]); + $this->__set('S', $hsb[1]); + $this->__set('B', $hsb[2]); + return $this; + } + + /** + * no hsb in css + * + * @param float|string|null $opacity + * @return string + * @throws \ErrorException + */ + public function toCssString(null|float|string $opacity = null): string + { + throw new \ErrorException('HSB is not available as CSS color string', 0); + } +} + +// __END__ diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/HSL.php b/www/lib/CoreLibs/Convert/Color/Coordinates/HSL.php new file mode 100644 index 00000000..21be7fe5 --- /dev/null +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/HSL.php @@ -0,0 +1,140 @@ +setAsArray([$H, $S, $L]); + } + + /** + * set from array + * where 0: Hue, 1: Saturation, 2: Lightness + * + * @param array{0:float,1:float,2:float} $hsl + * @return self + */ + public static function __constructFromArray(array $hsl): self + { + return (new HSL())->setAsArray($hsl); + } + + /** + * set color + * + * @param string $name + * @param float $value + * @return void + */ + public function __set(string $name, float $value): void + { + if (!property_exists($this, $name)) { + throw new \ErrorException('Creation of dynamic property is not allowed', 0); + } + switch ($name) { + case 'H': + if ($value == 360) { + $value = 0; + } + if ($value < 0 || $value > 359) { + throw new \LengthException( + 'Argument value ' . $value . ' for hue is not in the range of 0 to 359', + 1 + ); + } + break; + case 'S': + if ($value < 0 || $value > 100) { + throw new \LengthException( + 'Argument value ' . $value . ' for saturation is not in the range of 0 to 100', + 2 + ); + } + break; + case 'L': + if ($value < 0 || $value > 100) { + throw new \LengthException( + 'Argument value ' . $value . ' for luminance is not in the range of 0 to 100', + 3 + ); + } + break; + } + $this->$name = $value; + } + + /** + * get color + * + * @param string $name + * @return float + */ + public function __get(string $name): float + { + if (!property_exists($this, $name)) { + throw new \ErrorException('Creation of dynamic property is not allowed', 0); + } + return $this->$name; + } + + /** + * Returns the color as array + * where 0: Hue, 1: Saturation, 2: Lightness + * + * @return array{0:float,1:float,2:float} + */ + public function returnAsArray(): array + { + return [$this->H, $this->S, $this->L]; + } + + /** + * set color as array + * where 0: Hue, 1: Saturation, 2: Lightness + * + * @param array{0:float,1:float,2:float} $hsl + * @return self + */ + public function setAsArray(array $hsl): self + { + $this->__set('H', $hsl[0]); + $this->__set('S', $hsl[1]); + $this->__set('L', $hsl[2]); + return $this; + } +} + +// __END__ diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/HWB.php b/www/lib/CoreLibs/Convert/Color/Coordinates/HWB.php new file mode 100644 index 00000000..ee6b7f63 --- /dev/null +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/HWB.php @@ -0,0 +1,140 @@ +setAsArray([$H, $W, $B]); + } + + /** + * set from array + * where 0: Hue, 1: Whiteness, 2: Blackness + * + * @param array{0:float,1:float,2:float} $hwb + * @return self + */ + public static function __constructFromArray(array $hwb): self + { + return (new HWB())->setAsArray($hwb); + } + + /** + * set color + * + * @param string $name + * @param float $value + * @return void + */ + public function __set(string $name, float $value): void + { + if (!property_exists($this, $name)) { + throw new \ErrorException('Creation of dynamic property is not allowed', 0); + } + switch ($name) { + case 'H': + if ($value == 360) { + $value = 0; + } + if ($value < 0 || $value > 360) { + throw new \LengthException( + 'Argument value ' . $value . ' for hue is not in the range of 0 to 360', + 1 + ); + } + break; + case 'W': + if ($value < 0 || $value > 100) { + throw new \LengthException( + 'Argument value ' . $value . ' for saturation is not in the range of 0 to 100', + 2 + ); + } + break; + case 'B': + if ($value < 0 || $value > 100) { + throw new \LengthException( + 'Argument value ' . $value . ' for luminance is not in the range of 0 to 100', + 3 + ); + } + break; + } + $this->$name = $value; + } + + /** + * get color + * + * @param string $name + * @return float + */ + public function __get(string $name): float + { + if (!property_exists($this, $name)) { + throw new \ErrorException('Creation of dynamic property is not allowed', 0); + } + return $this->$name; + } + + /** + * Returns the color as array + * where 0: Hue, 1: Whiteness, 2: Blackness + * + * @return array{0:float,1:float,2:float} + */ + public function returnAsArray(): array + { + return [$this->H, $this->W, $this->B]; + } + + /** + * set color as array + * where 0: Hue, 1: Whiteness, 2: Blackness + * + * @param array{0:float,1:float,2:float} $hwb + * @return self + */ + public function setAsArray(array $hwb): self + { + $this->__set('H', $hwb[0]); + $this->__set('W', $hwb[1]); + $this->__set('B', $hwb[2]); + return $this; + } +} + +// __END__ diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/LCH.php b/www/lib/CoreLibs/Convert/Color/Coordinates/LCH.php new file mode 100644 index 00000000..648fb466 --- /dev/null +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/LCH.php @@ -0,0 +1,169 @@ +setAsArray([$L, $c, $h]); + } + + /** + * set from array + * where 0: Lightness, 1: Chroma, 2: Hue + * + * @param array{0:float,1:float,2:float} $lch + * @return self + */ + public static function __constructFromArray(array $lch): self + { + return (new LCH())->setAsArray($lch); + } + + /** + * set color + * + * @param string $name + * @param float $value + * @return void + */ + public function __set(string $name, float $value): void + { + if (!property_exists($this, $name)) { + throw new \ErrorException('Creation of dynamic property is not allowed', 0); + } + switch ($name) { + // case 'L': + // if ($this->colorspace == 'cie' && ($value < 0 || $value > 100)) { + // throw new \LengthException( + // 'Argument value ' . $value . ' for lightness is not in the range of ' + // . '0 to 100', + // 3 + // ); + // } elseif ($this->colorspace == 'ok' && ($value < 0 || $value > 1)) { + // throw new \LengthException( + // 'Argument value ' . $value . ' for lightness is not in the range of ' + // . '0 to 1', + // 3 + // ); + // } + // break; + // case 'c': + // if ($this->colorspace == 'cie' && ($value < 0 || $value > 230)) { + // throw new \LengthException( + // 'Argument value ' . $value . ' for chroma is not in the range of ' + // . '0 to 230 with normal upper limit of 150', + // 3 + // ); + // } elseif ($this->colorspace == 'ok' && ($value < 0 || $value > 0.5)) { + // throw new \LengthException( + // 'Argument value ' . $value . ' for chroma is not in the range of ' + // . '0 to 0.5 with normal upper limit of 0.5', + // 3 + // ); + // } + // break; + case 'h': + if ($value == 360) { + $value = 0; + } + if ($value < 0 || $value > 360) { + throw new \LengthException( + 'Argument value ' . $value . ' for lightness is not in the range of 0 to 360', + 1 + ); + } + break; + } + $this->$name = $value; + } + + /** + * get color + * + * @param string $name + * @return float + */ + public function __get(string $name): float + { + if (!property_exists($this, $name)) { + throw new \ErrorException('Creation of dynamic property is not allowed', 0); + } + return $this->$name; + } + + /** + * Returns the color as array + * where 0: Lightness, 1: Chroma, 2: Hue + * + * @return array{0:float,1:float,2:float} + */ + public function returnAsArray(): array + { + return [$this->L, $this->C, $this->H]; + } + + /** + * set color as array + * where 0: Lightness, 1: Chroma, 2: Hue + * + * @param array{0:float,1:float,2:float} $lch + * @return self + */ + public function setAsArray(array $lch): self + { + $this->__set('L', $lch[0]); + $this->__set('C', $lch[1]); + $this->__set('H', $lch[2]); + return $this; + } +} + +// __END__ diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/Lab.php b/www/lib/CoreLibs/Convert/Color/Coordinates/Lab.php new file mode 100644 index 00000000..e2eb11a4 --- /dev/null +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/Lab.php @@ -0,0 +1,177 @@ + allowed colorspaces */ + private const COLORSPACES = ['Oklab', 'cie']; + + /** @var float lightness/luminance + * CIE: 0f to 100f + * OKlab: 0.0 to 1.0 + * BOTH: 0% to 100% + */ + private float $L = 0.0; + /** @var float a axis distance + * CIE: -125 to 125, cannot be more than +/- 160 + * OKlab: -0.4 to 0.4, cannot exceed +/- 0.5 + * BOTH: -100% to 100% (+/-125 or 0.4) + */ + private float $a = 0.0; + /** @var float b axis distance + * CIE: -125 to 125, cannot be more than +/- 160 + * OKlab: -0.4 to 0.4, cannot exceed +/- 0.5 + * BOTH: -100% to 100% (+/-125 or 0.4) + */ + private float $b = 0.0; + + /** @var string color space: either ok or cie */ + private string $colorspace = ''; + + /** + * Color Coordinate: Lab + * for oklab or cie + */ + public function __construct() + { + } + + /** + * set with each value as parameters + * + * @param float $L + * @param float $a + * @param float $b + * @param string $colorspace + * @return self + */ + public static function __constructFromSet(float $L, float $a, float $b, string $colorspace): self + { + return (new Lab())->setColorspace($colorspace)->setAsArray([$L, $a, $b]); + } + + /** + * set from array + * where 0: Lightness, 1: a, 2: b + * + * @param array{0:float,1:float,2:float} $rgb + * @param string $colorspace + * @return self + */ + public static function __constructFromArray(array $lab, string $colorspace): self + { + return (new Lab())->setColorspace($colorspace)->setAsArray($lab); + } + + /** + * set color + * + * @param string $name + * @param float $value + * @return void + */ + public function __set(string $name, float $value): void + { + if (!property_exists($this, $name)) { + throw new \ErrorException('Creation of dynamic property is not allowed', 0); + } + // switch ($name) { + // case 'L': + // if ($value == 360) { + // $value = 0; + // } + // if ($value < 0 || $value > 360) { + // throw new \LengthException( + // 'Argument value ' . $value . ' for lightness is not in the range of 0 to 360', + // 1 + // ); + // } + // break; + // case 'a': + // if ($value < 0 || $value > 100) { + // throw new \LengthException( + // 'Argument value ' . $value . ' for a is not in the range of 0 to 100', + // 2 + // ); + // } + // break; + // case 'b': + // if ($value < 0 || $value > 100) { + // throw new \LengthException( + // 'Argument value ' . $value . ' for b is not in the range of 0 to 100', + // 3 + // ); + // } + // break; + // } + $this->$name = $value; + } + + /** + * get color + * + * @param string $name + * @return float + */ + public function __get(string $name): float + { + if (!property_exists($this, $name)) { + throw new \ErrorException('Creation of dynamic property is not allowed', 0); + } + return $this->$name; + } + + /** + * set the colorspace + * + * @param string $colorspace + * @return self + */ + private function setColorspace(string $colorspace): self + { + if (!in_array($colorspace, $this::COLORSPACES)) { + throw new \InvalidArgumentException('Not allowed colorspace', 0); + } + $this->colorspace = $colorspace; + return $this; + } + + /** + * Returns the color as array + * where 0: Lightness, 1: a, 2: b + * + * @return array{0:float,1:float,2:float} + */ + public function returnAsArray(): array + { + return [$this->L, $this->a, $this->b]; + } + + /** + * set color as array + * where 0: Lightness, 1: a, 2: b + * + * @param array{0:float,1:float,2:float} $lab + * @return self + */ + public function setAsArray(array $lab): self + { + $this->__set('L', $lab[0]); + $this->__set('a', $lab[1]); + $this->__set('b', $lab[2]); + return $this; + } +} + +// __END__ diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/RGB.php b/www/lib/CoreLibs/Convert/Color/Coordinates/RGB.php new file mode 100644 index 00000000..acc81952 --- /dev/null +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/RGB.php @@ -0,0 +1,226 @@ +flagLinear($linear)->setAsArray([$R, $G, $B]); + } + + /** + * set from array + * where 0: Red, 1: Green, 2: Blue + * + * @param array{0:float,1:float,2:float} $rgb + * @param bool $linear [default=false] + * @return self + */ + public static function __constructFromArray(array $rgb, bool $linear = false): self + { + return (new RGB())->flagLinear($linear)->setAsArray($rgb); + } + + /** + * set color + * + * @param string $name + * @param float $value + * @return void + */ + public function __set(string $name, float $value): void + { + // do not allow setting linear from outside + if ($name == 'linear') { + return; + } + if (!property_exists($this, $name)) { + throw new \ErrorException('Creation of dynamic property is not allowed', 0); + } + // if not linear + if (!$this->linear && ($value < 0 || $value > 255)) { + throw new \LengthException('Argument value ' . $value . ' for color ' . $name + . ' is not in the range of 0 to 255', 1); + } elseif ($this->linear && ($value < -10E10 || $value > 1)) { + // not allow very very small negative numbers + throw new \LengthException('Argument value ' . $value . ' for color ' . $name + . ' is not in the range of 0 to 1 for linear rgb', 1); + } + $this->$name = $value; + } + + /** + * get color + * + * @param string $name + * @return float|bool + */ + public function __get(string $name): float|bool + { + if (!property_exists($this, $name)) { + throw new \ErrorException('Creation of dynamic property is not allowed', 0); + } + return $this->$name; + } + + /** + * Returns the color as array + * where 0: Red, 1: Green, 2: Blue + * + * @return array{0:float,1:float,2:float} + */ + public function returnAsArray(): array + { + return [$this->R, $this->G, $this->B]; + } + + /** + * set color as array + * where 0: Red, 1: Green, 2: Blue + * + * @param array{0:float,1:float,2:float} $rgb + * @return self + */ + public function setAsArray(array $rgb): self + { + $this->__set('R', $rgb[0]); + $this->__set('G', $rgb[1]); + $this->__set('B', $rgb[2]); + return $this; + } + + /** + * set as linear + * can be used as chain call on create if input is linear RGB + * RGB::__construct**(...)->flagLinear(); + * as it returns self + * + * @return self + */ + private function flagLinear(bool $linear): self + { + $this->linear = $linear; + return $this; + } + + /** + * Both function source: + * https://bottosson.github.io/posts/colorwrong/#what-can-we-do%3F + * but reverse f: fromLinear and f_inv for toLinear + * Code copied from here: + * https://stackoverflow.com/a/12894053 + * + * converts RGB to linear + * We come from 0-255 so we need to divide by 255 + * + * @return self + */ + public function toLinear(): self + { + $this->flagLinear(true)->setAsArray(array_map( + callback: function (int|float $v) { + $v = (float)($v / 255); + $abs = abs($v); + $sign = ($v < 0) ? -1 : 1; + return (float)( + $abs <= 0.04045 ? + $v / 12.92 : + $sign * pow(($abs + 0.055) / 1.055, 2.4) + ); + }, + array: $this->returnAsArray(), + )); + return $this; + } + + /** + * convert back to normal sRGB from linear RGB + * we go to 0-255 rgb so we multiply by 255 + * + * @return self + */ + public function fromLinear(): self + { + $this->flagLinear(false)->setAsArray(array_map( + callback: function (int|float $v) { + $abs = abs($v); + $sign = ($v < 0) ? -1 : 1; + // during reverse in some situations the values can become negative in very small ways + // like -...E16 and ...E17 + return ($ret = (float)(255 * ( + $abs <= 0.0031308 ? + $v * 12.92 : + $sign * (1.055 * pow($abs, 1.0 / 2.4) - 0.055) + ))) < 0 ? 0 : $ret; + }, + array: $this->returnAsArray(), + )); + // $this->linear = false; + return $this; + } + + /** + * convert to css string with optional opacity + * Note: if this is a linea RGB, this data will not be correct + * + * @param float|string|null $opacity + * @return string + */ + public function toCssString(null|float|string $opacity = null): string + { + // set opacity, either a string or float + if (is_string($opacity)) { + $opacity = ' / ' . $opacity; + } elseif ($opacity !== null) { + $opacity = ' / ' . $opacity; + } else { + $opacity = ''; + } + return 'rgb(' + . (int)round($this->R, 0) + . ' ' + . (int)round($this->G, 0) + . ' ' + . (int)round($this->B, 0) + . $opacity + . ')'; + } +} + +// __END__ diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/XYZD65.php b/www/lib/CoreLibs/Convert/Color/Coordinates/XYZD65.php new file mode 100644 index 00000000..ebdf633d --- /dev/null +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/XYZD65.php @@ -0,0 +1,116 @@ +setAsArray([$X, $Y, $Z]); + } + + /** + * set from array + * where 0: X, 1: Y, 2: Z + * + * @param array{0:float,1:float,2:float} $xyzD65 + * @return self + */ + public static function __constructFromArray(array $xyzD65): self + { + return (new XYZD65())->setAsArray($xyzD65); + } + + /** + * set color + * + * @param string $name + * @param float $value + * @return void + */ + public function __set(string $name, float $value): void + { + if (!property_exists($this, $name)) { + throw new \ErrorException('Creation of dynamic property is not allowed', 0); + } + // if ($value < 0 || $value > 255) { + // throw new \LengthException('Argument value ' . $value . ' for color ' . $name + // . ' is not in the range of 0 to 255', 1); + // } + $this->$name = $value; + } + + /** + * get color + * + * @param string $name + * @return float + */ + public function __get(string $name): float + { + if (!property_exists($this, $name)) { + throw new \ErrorException('Creation of dynamic property is not allowed', 0); + } + return $this->$name; + } + + /** + * Returns the color as array + * where 0: X, 1: Y, 2: Z + * + * @return array{0:float,1:float,2:float} + */ + public function returnAsArray(): array + { + return [$this->X, $this->Y, $this->Z]; + } + + /** + * set color as array + * where 0: X, 1: Y, 2: Z + * + * @param array{0:float,1:float,2:float} $xyzD65 + * @return self + */ + public function setAsArray(array $xyzD65): self + { + $this->__set('X', $xyzD65[0]); + $this->__set('Y', $xyzD65[1]); + $this->__set('Z', $xyzD65[2]); + return $this; + } +} + +// __END__ diff --git a/www/lib/CoreLibs/Convert/Color/OkLab.php b/www/lib/CoreLibs/Convert/Color/OkLab.php new file mode 100644 index 00000000..2bbdbbc2 --- /dev/null +++ b/www/lib/CoreLibs/Convert/Color/OkLab.php @@ -0,0 +1,80 @@ + oklab + * oklab -> rgb + * rgb -> okhsl + * okshl -> rgb + * rgb -> okhsv + * okhsv -> rgb +*/ + +declare(strict_types=1); + +namespace CoreLibs\Convert\Color; + +class OkLab +{ + /** + * lines sRGB to oklab + * + * @param int $red + * @param int $green + * @param int $blue + * @return array + */ + public static function srgb2okLab(int $red, int $green, int $blue): array + { + $l = (float)0.4122214708 * (float)$red + + (float)0.5363325363 * (float)$green + + (float)0.0514459929 * (float)$blue; + $m = (float)0.2119034982 * (float)$red + + (float)0.6806995451 * (float)$green + + (float)0.1073969566 * (float)$blue; + $s = (float)0.0883024619 * (float)$red + + (float)0.2817188376 * (float)$green + + (float)0.6299787005 * (float)$blue; + + // cbrtf = 3 root (val) + $l_ = pow($l, 1.0 / 3); + $m_ = pow($m, 1.0 / 3); + $s_ = pow($s, 1.0 / 3); + + return [ + (float)0.2104542553 * $l_ + (float)0.7936177850 * $m_ - (float)0.0040720468 * $s_, + (float)1.9779984951 * $l_ - (float)2.4285922050 * $m_ + (float)0.4505937099 * $s_, + (float)0.0259040371 * $l_ + (float)0.7827717662 * $m_ - (float)0.8086757660 * $s_, + ]; + } + + /** + * convert okLab to linear sRGB + * + * @param float $L + * @param float $a + * @param float $b + * @return array + */ + public static function okLab2srgb(float $L, float $a, float $b): array + { + $l_ = $L + (float)0.3963377774 * $a + (float)0.2158037573 * $b; + $m_ = $L - (float)0.1055613458 * $a - (float)0.0638541728 * $b; + $s_ = $L - (float)0.0894841775 * $a - (float)1.2914855480 * $b; + + $l = $l_ * $l_ * $l_; + $m = $m_ * $m_ * $m_; + $s = $s_ * $s_ * $s_; + + return [ + (int)round(+(float)4.0767416621 * $l - (float)3.3077115913 * $m + (float)0.2309699292 * $s), + (int)round(-(float)1.2684380046 * $l + (float)2.6097574011 * $m - (float)0.3413193965 * $s), + (int)round(-(float)0.0041960863 * $l - (float)0.7034186147 * $m + (float)1.7076147010 * $s), + ]; + } +} + +// __END__ diff --git a/www/lib/CoreLibs/Convert/Color/Stringify.php b/www/lib/CoreLibs/Convert/Color/Stringify.php new file mode 100644 index 00000000..6ca68431 --- /dev/null +++ b/www/lib/CoreLibs/Convert/Color/Stringify.php @@ -0,0 +1,35 @@ +toCssString($opacity); + } +} + +// __END__ diff --git a/www/lib/CoreLibs/Convert/Colors.php b/www/lib/CoreLibs/Convert/Colors.php index f9f56171..8ff32608 100644 --- a/www/lib/CoreLibs/Convert/Colors.php +++ b/www/lib/CoreLibs/Convert/Colors.php @@ -120,26 +120,29 @@ class Colors $MAX = max($red, $green, $blue); $MIN = min($red, $green, $blue); $HUE = 0; + $DELTA = $MAX - $MIN; + // achromatic if ($MAX == $MIN) { return [0, 0, round($MAX * 100)]; } if ($red == $MAX) { - $HUE = ($green - $blue) / ($MAX - $MIN); + $HUE = fmod(($green - $blue) / $DELTA, 6); } elseif ($green == $MAX) { - $HUE = 2 + (($blue - $red) / ($MAX - $MIN)); + $HUE = (($blue - $red) / $DELTA) + 2; } elseif ($blue == $MAX) { - $HUE = 4 + (($red - $green) / ($MAX - $MIN)); + $HUE = (($red - $green) / $DELTA) + 4; } $HUE *= 60; + // avoid negative if ($HUE < 0) { $HUE += 360; } return [ - (int)round($HUE), - (int)round((($MAX - $MIN) / $MAX) * 100), - (int)round($MAX * 100) + (int)round($HUE), // Hue + (int)round(($DELTA / $MAX) * 100), // Saturation + (int)round($MAX * 100) // Value/Brightness ]; } diff --git a/www/lib/CoreLibs/Convert/Math.php b/www/lib/CoreLibs/Convert/Math.php index 205abbf1..26739c5f 100644 --- a/www/lib/CoreLibs/Convert/Math.php +++ b/www/lib/CoreLibs/Convert/Math.php @@ -56,6 +56,95 @@ class Math return (float)$number; } } + + /** + * calc cube root + * + * @param float $number Number to cubic root + * @return float Calculated value + */ + public static function cbrt(float $number): float + { + return pow($number, 1.0 / 3); + } + + /** + * This function is directly inspired by the multiplyMatrices() function in color.js + * form Lea Verou and Chris Lilley. + * (see https://github.com/LeaVerou/color.js/blob/main/src/multiply-matrices.js) + * From: + * https://github.com/matthieumastadenis/couleur/blob/3842cf51c9517e77afaa0a36ec78643a0c258e0b/src/utils/utils.php#L507 + * + * It returns an array which is the product of the two number matrices passed as parameters. + * + * @param array> $a m x n matrice + * @param array> $b n x p matrice + * + * @return array> m x p product + */ + public static function multiplyMatrices(array $a, array $b): array + { + $m = count($a); + + if (!is_array($a[0] ?? null)) { + // $a is vector, convert to [[a, b, c, ...]] + $a = [ $a ]; + } + + if (!is_array($b[0])) { + // $b is vector, convert to [[a], [b], [c], ...]] + $b = array_map( + callback: fn ($v) => [ $v ], + array: $b, + ); + } + + $p = count($b[0]); + + // transpose $b: + $bCols = array_map( + callback: fn ($k) => \array_map( + (fn ($i) => $i[$k]), + $b, + ), + array: array_keys($b[0]), + ); + + $product = array_map( + callback: fn ($row) => array_map( + callback: fn ($col) => is_array($row) ? + array_reduce( + array: $row, + callback: fn ($a, $v, $i = null) => $a + $v * ( + $col[$i ?? array_search($v, $row)] ?? 0 + ), + initial: 0, + ) : + array_reduce( + array: $col, + callback: fn ($a, $v) => $a + $v * $row, + initial: 0, + ), + array: $bCols, + ), + array: $a, + ); + + if ($m === 1) { + // Avoid [[a, b, c, ...]]: + $product = $product[0]; + } + + if ($p === 1) { + // Avoid [[a], [b], [c], ...]]: + return array_map( + callback: fn ($v) => $v[0], + array: $product, + ); + } + + return $product; + } } // __END__ From 720b78b687dedc5c6bc34ef3d76a6249c5c00f13 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Tue, 12 Nov 2024 18:52:24 +0900 Subject: [PATCH 02/13] Add CIE XYZ classes for D50/D65 whitespace before clean up --- .../Convert/Color/Coordinates/XYZD50.php | 128 ++++++++++++++++++ .../Convert/Color/Coordinates/XYZD65.php | 54 +++++--- 2 files changed, 161 insertions(+), 21 deletions(-) create mode 100644 www/lib/CoreLibs/Convert/Color/Coordinates/XYZD50.php diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/XYZD50.php b/www/lib/CoreLibs/Convert/Color/Coordinates/XYZD50.php new file mode 100644 index 00000000..349bb5cd --- /dev/null +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/XYZD50.php @@ -0,0 +1,128 @@ + allowed colorspaces */ + private const COLORSPACES = ['CIEXYZ']; + + /** @var float X coordinate */ + private float $X = 0.0; + /** @var float Y coordinate (Luminance) */ + private float $Y = 0.0; + /** @var float Z coordinate (blue) */ + private float $Z = 0.0; + + /** @var string color space: either ok or cie */ + private string $colorspace = ''; + + /** + * Color Coordinate Lch + * for oklch + */ + public function __construct() + { + } + + /** + * set from array + * where 0: X, 1: Y, 2: Z + * + * @param array{0:float,1:float,2:float} $colors + * @param string $colorspace + * @return self + */ + public static function __constructFromArray(array $colors, string $colorspace = 'CieXYZ'): self + { + return (new XYZD50())->setColorspace($colorspace)->setFromArray($colors); + } + + /** + * set color + * + * @param string $name + * @param float $value + * @return void + */ + public function __set(string $name, float $value): void + { + if (!property_exists($this, $name)) { + throw new \ErrorException('Creation of dynamic property is not allowed', 0); + } + // if ($value < 0 || $value > 255) { + // throw new \LengthException('Argument value ' . $value . ' for color ' . $name + // . ' is not in the range of 0 to 255', 1); + // } + $this->$name = $value; + } + + /** + * get color + * + * @param string $name + * @return float + */ + public function __get(string $name): float + { + if (!property_exists($this, $name)) { + throw new \ErrorException('Creation of dynamic property is not allowed', 0); + } + return $this->$name; + } + + /** + * set the colorspace + * + * @param string $colorspace + * @return self + */ + private function setColorspace(string $colorspace): self + { + if (!in_array($colorspace, $this::COLORSPACES)) { + throw new \InvalidArgumentException('Not allowed colorspace', 0); + } + $this->colorspace = $colorspace; + return $this; + } + + /** + * Returns the color as array + * where 0: X, 1: Y, 2: Z + * + * @return array{0:float,1:float,2:float} + */ + public function returnAsArray(): array + { + return [$this->X, $this->Y, $this->Z]; + } + + /** + * set color as array + * where 0: X, 1: Y, 2: Z + * + * @param array{0:float,1:float,2:float} $colors + * @return self + */ + public function setFromArray(array $colors): self + { + $this->__set('X', $colors[0]); + $this->__set('Y', $colors[1]); + $this->__set('Z', $colors[2]); + return $this; + } +} + +// __END__ diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/XYZD65.php b/www/lib/CoreLibs/Convert/Color/Coordinates/XYZD65.php index ebdf633d..76c96e00 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/XYZD65.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/XYZD65.php @@ -16,10 +16,19 @@ namespace CoreLibs\Convert\Color\Coordinates; class XYZD65 { + /** @var array allowed colorspaces */ + private const COLORSPACES = ['XYZ']; + + /** @var float X coordinate */ private float $X = 0.0; + /** @var float Y coordinate */ private float $Y = 0.0; + /** @var float Y coordinate */ private float $Z = 0.0; + /** @var string color space: either ok or cie */ + private string $colorspace = ''; + /** * Color Coordinate Lch * for oklch @@ -28,29 +37,17 @@ class XYZD65 { } - /** - * set with each value as parameters - * - * @param float $X - * @param float $Y - * @param float $Z - * @return self - */ - public static function __constructFromSet(float $X, float $Y, float $Z): self - { - return (new XYZD65())->setAsArray([$X, $Y, $Z]); - } - /** * set from array * where 0: X, 1: Y, 2: Z * - * @param array{0:float,1:float,2:float} $xyzD65 + * @param array{0:float,1:float,2:float} $colors + * @param string $colorspace * @return self */ - public static function __constructFromArray(array $xyzD65): self + public static function __constructFromArray(array $colors, string $colorspace = 'XYZ'): self { - return (new XYZD65())->setAsArray($xyzD65); + return (new XYZD65())->setColorspace($colorspace)->setFromArray($colors); } /** @@ -86,6 +83,21 @@ class XYZD65 return $this->$name; } + /** + * set the colorspace + * + * @param string $colorspace + * @return self + */ + private function setColorspace(string $colorspace): self + { + if (!in_array($colorspace, $this::COLORSPACES)) { + throw new \InvalidArgumentException('Not allowed colorspace', 0); + } + $this->colorspace = $colorspace; + return $this; + } + /** * Returns the color as array * where 0: X, 1: Y, 2: Z @@ -101,14 +113,14 @@ class XYZD65 * set color as array * where 0: X, 1: Y, 2: Z * - * @param array{0:float,1:float,2:float} $xyzD65 + * @param array{0:float,1:float,2:float} $colors * @return self */ - public function setAsArray(array $xyzD65): self + public function setFromArray(array $colors): self { - $this->__set('X', $xyzD65[0]); - $this->__set('Y', $xyzD65[1]); - $this->__set('Z', $xyzD65[2]); + $this->__set('X', $colors[0]); + $this->__set('Y', $colors[1]); + $this->__set('Z', $colors[2]); return $this; } } From 921b9cb3d915e577c7af308c684e77cf735401e3 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Tue, 12 Nov 2024 18:53:02 +0900 Subject: [PATCH 03/13] Remove not used Color Coordinate classes and old oklab convert class --- .../Convert/Color/Coordinates/XYZD50.php | 128 ------------------ .../Convert/Color/Coordinates/XYZD65.php | 128 ------------------ www/lib/CoreLibs/Convert/Color/OkLab.php | 80 ----------- 3 files changed, 336 deletions(-) delete mode 100644 www/lib/CoreLibs/Convert/Color/Coordinates/XYZD50.php delete mode 100644 www/lib/CoreLibs/Convert/Color/Coordinates/XYZD65.php delete mode 100644 www/lib/CoreLibs/Convert/Color/OkLab.php diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/XYZD50.php b/www/lib/CoreLibs/Convert/Color/Coordinates/XYZD50.php deleted file mode 100644 index 349bb5cd..00000000 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/XYZD50.php +++ /dev/null @@ -1,128 +0,0 @@ - allowed colorspaces */ - private const COLORSPACES = ['CIEXYZ']; - - /** @var float X coordinate */ - private float $X = 0.0; - /** @var float Y coordinate (Luminance) */ - private float $Y = 0.0; - /** @var float Z coordinate (blue) */ - private float $Z = 0.0; - - /** @var string color space: either ok or cie */ - private string $colorspace = ''; - - /** - * Color Coordinate Lch - * for oklch - */ - public function __construct() - { - } - - /** - * set from array - * where 0: X, 1: Y, 2: Z - * - * @param array{0:float,1:float,2:float} $colors - * @param string $colorspace - * @return self - */ - public static function __constructFromArray(array $colors, string $colorspace = 'CieXYZ'): self - { - return (new XYZD50())->setColorspace($colorspace)->setFromArray($colors); - } - - /** - * set color - * - * @param string $name - * @param float $value - * @return void - */ - public function __set(string $name, float $value): void - { - if (!property_exists($this, $name)) { - throw new \ErrorException('Creation of dynamic property is not allowed', 0); - } - // if ($value < 0 || $value > 255) { - // throw new \LengthException('Argument value ' . $value . ' for color ' . $name - // . ' is not in the range of 0 to 255', 1); - // } - $this->$name = $value; - } - - /** - * get color - * - * @param string $name - * @return float - */ - public function __get(string $name): float - { - if (!property_exists($this, $name)) { - throw new \ErrorException('Creation of dynamic property is not allowed', 0); - } - return $this->$name; - } - - /** - * set the colorspace - * - * @param string $colorspace - * @return self - */ - private function setColorspace(string $colorspace): self - { - if (!in_array($colorspace, $this::COLORSPACES)) { - throw new \InvalidArgumentException('Not allowed colorspace', 0); - } - $this->colorspace = $colorspace; - return $this; - } - - /** - * Returns the color as array - * where 0: X, 1: Y, 2: Z - * - * @return array{0:float,1:float,2:float} - */ - public function returnAsArray(): array - { - return [$this->X, $this->Y, $this->Z]; - } - - /** - * set color as array - * where 0: X, 1: Y, 2: Z - * - * @param array{0:float,1:float,2:float} $colors - * @return self - */ - public function setFromArray(array $colors): self - { - $this->__set('X', $colors[0]); - $this->__set('Y', $colors[1]); - $this->__set('Z', $colors[2]); - return $this; - } -} - -// __END__ diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/XYZD65.php b/www/lib/CoreLibs/Convert/Color/Coordinates/XYZD65.php deleted file mode 100644 index 76c96e00..00000000 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/XYZD65.php +++ /dev/null @@ -1,128 +0,0 @@ - allowed colorspaces */ - private const COLORSPACES = ['XYZ']; - - /** @var float X coordinate */ - private float $X = 0.0; - /** @var float Y coordinate */ - private float $Y = 0.0; - /** @var float Y coordinate */ - private float $Z = 0.0; - - /** @var string color space: either ok or cie */ - private string $colorspace = ''; - - /** - * Color Coordinate Lch - * for oklch - */ - public function __construct() - { - } - - /** - * set from array - * where 0: X, 1: Y, 2: Z - * - * @param array{0:float,1:float,2:float} $colors - * @param string $colorspace - * @return self - */ - public static function __constructFromArray(array $colors, string $colorspace = 'XYZ'): self - { - return (new XYZD65())->setColorspace($colorspace)->setFromArray($colors); - } - - /** - * set color - * - * @param string $name - * @param float $value - * @return void - */ - public function __set(string $name, float $value): void - { - if (!property_exists($this, $name)) { - throw new \ErrorException('Creation of dynamic property is not allowed', 0); - } - // if ($value < 0 || $value > 255) { - // throw new \LengthException('Argument value ' . $value . ' for color ' . $name - // . ' is not in the range of 0 to 255', 1); - // } - $this->$name = $value; - } - - /** - * get color - * - * @param string $name - * @return float - */ - public function __get(string $name): float - { - if (!property_exists($this, $name)) { - throw new \ErrorException('Creation of dynamic property is not allowed', 0); - } - return $this->$name; - } - - /** - * set the colorspace - * - * @param string $colorspace - * @return self - */ - private function setColorspace(string $colorspace): self - { - if (!in_array($colorspace, $this::COLORSPACES)) { - throw new \InvalidArgumentException('Not allowed colorspace', 0); - } - $this->colorspace = $colorspace; - return $this; - } - - /** - * Returns the color as array - * where 0: X, 1: Y, 2: Z - * - * @return array{0:float,1:float,2:float} - */ - public function returnAsArray(): array - { - return [$this->X, $this->Y, $this->Z]; - } - - /** - * set color as array - * where 0: X, 1: Y, 2: Z - * - * @param array{0:float,1:float,2:float} $colors - * @return self - */ - public function setFromArray(array $colors): self - { - $this->__set('X', $colors[0]); - $this->__set('Y', $colors[1]); - $this->__set('Z', $colors[2]); - return $this; - } -} - -// __END__ diff --git a/www/lib/CoreLibs/Convert/Color/OkLab.php b/www/lib/CoreLibs/Convert/Color/OkLab.php deleted file mode 100644 index 2bbdbbc2..00000000 --- a/www/lib/CoreLibs/Convert/Color/OkLab.php +++ /dev/null @@ -1,80 +0,0 @@ - oklab - * oklab -> rgb - * rgb -> okhsl - * okshl -> rgb - * rgb -> okhsv - * okhsv -> rgb -*/ - -declare(strict_types=1); - -namespace CoreLibs\Convert\Color; - -class OkLab -{ - /** - * lines sRGB to oklab - * - * @param int $red - * @param int $green - * @param int $blue - * @return array - */ - public static function srgb2okLab(int $red, int $green, int $blue): array - { - $l = (float)0.4122214708 * (float)$red + - (float)0.5363325363 * (float)$green + - (float)0.0514459929 * (float)$blue; - $m = (float)0.2119034982 * (float)$red + - (float)0.6806995451 * (float)$green + - (float)0.1073969566 * (float)$blue; - $s = (float)0.0883024619 * (float)$red + - (float)0.2817188376 * (float)$green + - (float)0.6299787005 * (float)$blue; - - // cbrtf = 3 root (val) - $l_ = pow($l, 1.0 / 3); - $m_ = pow($m, 1.0 / 3); - $s_ = pow($s, 1.0 / 3); - - return [ - (float)0.2104542553 * $l_ + (float)0.7936177850 * $m_ - (float)0.0040720468 * $s_, - (float)1.9779984951 * $l_ - (float)2.4285922050 * $m_ + (float)0.4505937099 * $s_, - (float)0.0259040371 * $l_ + (float)0.7827717662 * $m_ - (float)0.8086757660 * $s_, - ]; - } - - /** - * convert okLab to linear sRGB - * - * @param float $L - * @param float $a - * @param float $b - * @return array - */ - public static function okLab2srgb(float $L, float $a, float $b): array - { - $l_ = $L + (float)0.3963377774 * $a + (float)0.2158037573 * $b; - $m_ = $L - (float)0.1055613458 * $a - (float)0.0638541728 * $b; - $s_ = $L - (float)0.0894841775 * $a - (float)1.2914855480 * $b; - - $l = $l_ * $l_ * $l_; - $m = $m_ * $m_ * $m_; - $s = $s_ * $s_ * $s_; - - return [ - (int)round(+(float)4.0767416621 * $l - (float)3.3077115913 * $m + (float)0.2309699292 * $s), - (int)round(-(float)1.2684380046 * $l + (float)2.6097574011 * $m - (float)0.3413193965 * $s), - (int)round(-(float)0.0041960863 * $l - (float)0.7034186147 * $m + (float)1.7076147010 * $s), - ]; - } -} - -// __END__ From 00821bd5ea06d8d21a61c8b0614348f83de42bbf Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Tue, 12 Nov 2024 18:53:18 +0900 Subject: [PATCH 04/13] Move all Cie XYZ to dedicated class as this is not used in direct frontend convert Clean up old Colors class with calling new class calls Test all and set phpstan deprecated messages Add all missing convert functions for oklab/cielab/oklch/cielch calls Prepare for test run creation --- www/admin/class_test.convert.colors.php | 36 + www/lib/CoreLibs/Convert/Color/CieXyz.php | 344 +++++++++ www/lib/CoreLibs/Convert/Color/Color.php | 695 +++++++++++++----- .../Convert/Color/Coordinates/HSB.php | 51 +- .../Convert/Color/Coordinates/HSL.php | 73 +- .../Convert/Color/Coordinates/HWB.php | 73 +- .../Convert/Color/Coordinates/LCH.php | 79 +- .../Convert/Color/Coordinates/Lab.php | 61 +- .../Convert/Color/Coordinates/RGB.php | 155 +++- .../Convert/Color/Coordinates/XYZ.php | 162 ++++ www/lib/CoreLibs/Convert/Color/Stringify.php | 21 +- www/lib/CoreLibs/Convert/Colors.php | 44 +- 12 files changed, 1446 insertions(+), 348 deletions(-) create mode 100644 www/lib/CoreLibs/Convert/Color/CieXyz.php create mode 100644 www/lib/CoreLibs/Convert/Color/Coordinates/XYZ.php diff --git a/www/admin/class_test.convert.colors.php b/www/admin/class_test.convert.colors.php index a37cb2df..84d70ce5 100644 --- a/www/admin/class_test.convert.colors.php +++ b/www/admin/class_test.convert.colors.php @@ -31,6 +31,36 @@ $log = new CoreLibs\Logging\Logging([ ]); $color_class = 'CoreLibs\Convert\Colors'; +/** + * print out a color block with info + * + * @param string $color + * @param string $text + * @param string $text_add + * @return string + */ +function display(string $color, string $text, string $text_add): string +{ + $css = 'margin:5px;padding:50px;' + . 'width:10%;' + . 'text-align:center;' + . 'color:white;text-shadow: 0 0 5px black;font-weight:bold;'; + $template = << + {TEXT} + + HTML; + return str_replace( + ["{COLOR}", "{TEXT}", "{CSS}"], + [ + $color, + $text . ($text_add ? '
' . $text_add : ''), + $css + ], + $template + ); +} + $PAGE_NAME = 'TEST CLASS: CONVERT COLORS'; print ""; print "" . $PAGE_NAME . ""; @@ -133,14 +163,20 @@ $oklab = Color::rgbToOkLab(Coordinates\RGB::__constructFromArray([ 0 ])); print "OkLab: " . DgS::printAr($oklab) . "
"; +print display($oklab->toCssString(), $oklab->toCssString(), 'Oklab'); $rgb = Color::okLabToRgb($oklab); print "OkLab -> RGB: " . DgS::printAr($rgb) . "
"; +print display($rgb->toCssString(), $rgb->toCssString(), 'OkLab to RGB'); $rgb = Coordinates\RGB::__constructFromArray([250, 100, 10])->toLinear(); print "RGBlinear: " . DgS::printAr($rgb) . "
"; $rgb = Coordinates\RGB::__constructFromArray([0, 0, 0])->toLinear(); print "RGBlinear: " . DgS::printAr($rgb) . "
"; +$cie_lab = Color::okLabToLab($oklab); +print "CieLab: " . DgS::printAr($cie_lab) . "
"; +print display($cie_lab->toCssString(), $cie_lab->toCssString(), 'OkLab to Cie Lab'); + print ""; diff --git a/www/lib/CoreLibs/Convert/Color/CieXyz.php b/www/lib/CoreLibs/Convert/Color/CieXyz.php new file mode 100644 index 00000000..2dbd1253 --- /dev/null +++ b/www/lib/CoreLibs/Convert/Color/CieXyz.php @@ -0,0 +1,344 @@ +fromLinear(); + } + + /** + * Convert RGB to CIE Lab + * via xyz D65 to xyz D50 + * + * @param RGB $rgb + * @return Lab + */ + public static function rgbViaXyzD65ViaXyzD50ToLab(RGB $rgb): Lab + { + return self::xyzD50ToLab( + self::xyzD65ToXyzD50( + self::linRgbToXyzD65($rgb) + ) + ); + } + + /** + * Convert CIE Lab to RGB + * via xyz D50 to xyz D65 + * + * @param Lab $lab + * @return RGB + */ + public static function labViaXyzD50ViaXyzD65ToRgb(Lab $lab): RGB + { + return self::xyzD65ToLinRgb( + self::xyzD50ToXyxD65( + self::labToXyzD50($lab) + ) + )->fromLinear(); + } + + /** + * Undocumented function + * + * @param Lab $lab + * @return Lab + */ + public static function okLabViaXyzD65ViaXyzD50ToLab(Lab $lab): Lab + { + return self::xyzD50ToLab( + self::xyzD65ToXyzD50( + self::okLabToXyzD65($lab) + ) + ); + } + + /** + * Undocumented function + * + * @param Lab $lab + * @return Lab + */ + public static function labViaXyzD50ViaXyzD65ToOkLab(Lab $lab): Lab + { + return self::xyzD65ToOkLab( + self::xyzD50ToXyxD65( + self::labToXyzD50($lab) + ) + ); + } + + // MARK: xyzD65 <-> xyzD50 + + /** + * xyzD65 to xyzD50 whitepoint + * + * @param XYZ $xyz + * @return XYZ + */ + private static function xyzD65ToXyzD50(XYZ $xyz): XYZ + { + return XYZ::__constructFromArray(Math::multiplyMatrices( + a: [ + [1.0479298208405488, 0.022946793341019088, -0.05019222954313557], + [0.029627815688159344, 0.990434484573249, -0.01707382502938514], + [-0.009243058152591178, 0.015055144896577895, 0.7518742899580008], + ], + b: $xyz->returnAsArray(), + ), whitepoint: 'D50'); + } + + /** + * xyzD50 to xyzD65 whitepoint + * + * @param XYZ $xyz + * @return XYZ + */ + private static function xyzD50ToXyxD65(XYZ $xyz): XYZ + { + return XYZ::__constructFromArray(Math::multiplyMatrices( + a: [ + [0.9554734527042182, -0.023098536874261423, 0.0632593086610217], + [-0.028369706963208136, 1.0099954580058226, 0.021041398966943008], + [0.012314001688319899, -0.020507696433477912, 1.3303659366080753], + ], + b: $xyz->returnAsArray() + ), whitepoint: 'D65'); + } + + // MARK: xyzD50 <-> Lab + + /** + * Convert xyzD50 to Lab (Cie) + * + * @param XYZ $xyz + * @return Lab + */ + private static function xyzD50ToLab(XYZ $xyz): Lab + { + $_xyz = $xyz->returnAsArray(); + $d50 = [ + 0.3457 / 0.3585, + 1.00000, + (1.0 - 0.3457 - 0.3585) / 0.3585, + ]; + + $a = 216 / 24389; + $b = 24389 / 27; + + $_xyz = array_map( + fn ($k, $v) => $v / $d50[$k], + array_keys($_xyz), + array_values($_xyz), + ); + + $f = array_map( + fn ($v) => (($v > $a) ? + pow($v, 1 / 3) : + (($b * $v + 16) / 116) + ), + $_xyz, + ); + + return Lab::__constructFromArray([ + (116 * $f[1]) - 16, + 500 * ($f[0] - $f[1]), + 200 * ($f[1] - $f[2]), + ], colorspace: 'CIELab'); + } + + /** + * Convert Lab (Cie) to xyz D50 + * + * @param Lab $lab + * @return XYZ + */ + private static function labToXyzD50(Lab $lab): XYZ + { + $_lab = $lab->returnAsArray(); + $a = 24389 / 27; + $b = 216 / 24389; + $f = []; + $f[1] = ($_lab[0] + 16) / 116; + $f[0] = $_lab[1] / 500 + $f[1]; + $f[2] = $f[1] - $_lab[2] / 200; + $xyz = [ + // x + pow($f[0], 3) > $b ? + pow($f[0], 3) : + (116 * $f[0] - 16) / $a, + // y + $_lab[0] > $a * $b ? + pow(($_lab[0] + 16) / 116, 3) : + $_lab[0] / $a, + // z + pow($f[2], 3) > $b ? + pow($f[2], 3) : + (116 * $f[2] - 16) / $a, + ]; + + $d50 = [ + 0.3457 / 0.3585, + 1.00000, + (1.0 - 0.3457 - 0.3585) / 0.3585, + ]; + + return XYZ::__constructFromArray( + array_map( + fn ($k, $v) => $v * $d50[$k], + array_keys($xyz), + array_values($xyz), + ), + whitepoint: 'D50' + ); + } + + // MARK: xyzD65 <-> (linear)RGB + + /** + * convert linear RGB to xyz D65 + * if rgb is not flagged linear, it will be auto converted + * + * @param RGB $rgb + * @return XYZ + */ + private static function linRgbToXyzD65(RGB $rgb): XYZ + { + // if not linear, convert to linear + if (!$rgb->linear) { + $rgb->toLinear(); + } + return XYZ::__constructFromArray(Math::multiplyMatrices( + [ + [0.41239079926595934, 0.357584339383878, 0.1804807884018343], + [0.21263900587151027, 0.715168678767756, 0.07219231536073371], + [0.01933081871559182, 0.11919477979462598, 0.9505321522496607], + ], + $rgb->returnAsArray() + ), whitepoint: 'D65'); + } + + /** + * Convert xyz D65 to linear RGB + * + * @param XYZ $xyz + * @return RGB + */ + private static function xyzD65ToLinRgb(XYZ $xyz): RGB + { + // xyz D65 to linrgb + return RGB::__constructFromArray(Math::multiplyMatrices( + a : [ + [ 3.2409699419045226, -1.537383177570094, -0.4986107602930034 ], + [ -0.9692436362808796, 1.8759675015077202, 0.04155505740717559 ], + [ 0.05563007969699366, -0.20397695888897652, 1.0569715142428786 ], + ], + b : $xyz->returnAsArray() + ), linear: true); + } + + // MARK: xyzD65 <-> OkLab + + /** + * xyz D65 to OkLab + * + * @param XYZ $xyz + * @return Lab + */ + private static function xyzD65ToOkLab(XYZ $xyz): Lab + { + return Lab::__constructFromArray(Math::multiplyMatrices( + [ + [0.2104542553, 0.7936177850, -0.0040720468], + [1.9779984951, -2.4285922050, 0.4505937099], + [0.0259040371, 0.7827717662, -0.8086757660], + ], + array_map( + callback: fn ($v) => pow($v, 1 / 3), + array: Math::multiplyMatrices( + a: [ + [0.8190224432164319, 0.3619062562801221, -0.12887378261216414], + [0.0329836671980271, 0.9292868468965546, 0.03614466816999844], + [0.048177199566046255, 0.26423952494422764, 0.6335478258136937], + ], + b: $xyz->returnAsArray(), + ), + ) + ), colorspace: 'OkLab'); + } + + /** + * xyz D65 to OkLab + * + * @param Lab $lab + * @return XYZ + */ + private static function okLabToXyzD65(Lab $lab): XYZ + { + return XYZ::__constructFromArray(Math::multiplyMatrices( + a: [ + [1.2268798733741557, -0.5578149965554813, 0.28139105017721583], + [-0.04057576262431372, 1.1122868293970594, -0.07171106666151701], + [-0.07637294974672142, -0.4214933239627914, 1.5869240244272418], + ], + b: array_map( + callback: fn ($v) => is_numeric($v) ? $v ** 3 : 0, + array: Math::multiplyMatrices( + a: [ + [0.99999999845051981432, 0.39633779217376785678, 0.21580375806075880339], + [1.0000000088817607767, -0.1055613423236563494, -0.063854174771705903402], + [1.0000000546724109177, -0.089484182094965759684, -1.2914855378640917399], + ], + // Divide $lightness by 100 to convert from CSS OkLab + b: $lab->returnAsArray(), + ), + ), + ), whitepoint: 'D65'); + } +} + +// __END__ diff --git a/www/lib/CoreLibs/Convert/Color/Color.php b/www/lib/CoreLibs/Convert/Color/Color.php index c56f8c11..3b490e9b 100644 --- a/www/lib/CoreLibs/Convert/Color/Color.php +++ b/www/lib/CoreLibs/Convert/Color/Color.php @@ -9,38 +9,79 @@ * We convert between color cooradinates and color spaces * as seen in the list below * - * | | RGB | Oklab | Cie - * | | | HSB | | | | | | | - * | | RGB | HSV | HSL | HWB | OkLab | OkLch | CieLab | CieLch | - * -------+-----+-----+-----+-----+-------+-------+--------+--------+ - * RGB | - | o | o | o | o | o | | | - * HSB/HB | o | - | o | o | | | | | - * HSL | o | o | - | o | o | o | | | - * HWB | o | o | o | - | | | | | - * OkLab | o | | o | | - | | | | - * OkLch | o | | o | | | - | | | - * CieLab | | | | | | | - | | - * CieLch | | | | | | | | - | + * | | RGB | Oklab | CieLab | + * | | | HSB | | | | | | | + * | | RGB | HSV | HSL | HWB | OkLab | OkLch | CieLab | CieLch | + * --------+-----+-----+-----+-----+-------+-------+--------+--------+ + * RGB | - | o | o | o | o | o | o | o | + * HSB/HSV | o | - | o | o | o | o | o | o | + * HSL | o | o | - | o | o | o | o | o | + * HWB | o | o | o | - | o | o | o | o | + * OkLab | o | o | o | o | - | o | o | o | + * OkLch | o | o | o | o | o | - | o | o | + * CieLab | o | o | o | o | o | o | - | o | + * CieLch | o | o | o | o | o | o | o | - | * * All color coordinates are classes * The data can then be converted to a CSS string + * + * CieXyz Class + * Not theat xyz (CIEXYZ) does not have its own conversion as it is not used in web + * applications + * Also XYZ has two different coordinate systems for the D50 an D65 whitepoint */ declare(strict_types=1); namespace CoreLibs\Convert\Color; -use CoreLibs\Convert\Math; use CoreLibs\Convert\Color\Coordinates\RGB; use CoreLibs\Convert\Color\Coordinates\HSL; use CoreLibs\Convert\Color\Coordinates\HSB; use CoreLibs\Convert\Color\Coordinates\HWB; use CoreLibs\Convert\Color\Coordinates\LCH; use CoreLibs\Convert\Color\Coordinates\Lab; -use CoreLibs\Convert\Color\Coordinates\XYZD65; class Color { + // MARK: general lab/lch + + /** + * general Lab to LCH convert + * + * @param Lab $lab + * @return array{0:float,1:float,2:float} LCH values as array + */ + private static function __labToLch(Lab $lab): array + { + // cieLab to cieLch + $a = $lab->a; + $b = $lab->b; + + $hue = atan2($b, $a) * 180 / pi(); + + return [ + $lab->L, + sqrt($a ** 2 + $b ** 2), + $hue >= 0 ? $hue : $hue + 360, + ]; + } + + /** + * general LCH to Lab convert + * + * @param LCH $lch + * @return array{0:float,1:float,2:float} Lab values as array + */ + private static function __lchToLab(LCH $lch): array + { + return [ + $lch->L, + $lch->C * cos($lch->H * pi() / 180), // a + $lch->C * sin($lch->H * pi() / 180), // b + ]; + } + // MARK: RGB <-> HSL /** @@ -267,6 +308,40 @@ class Color ]); } + // MARK: RGB <-> HWB + + /** + * Convert RGB to HWB + * via rgb -> hsl -> hsb -> hwb + * + * @param RGB $rgb + * @return HWB + */ + public static function rgbToHwb(RGB $rgb): HWB + { + return self::hsbToHwb( + self::hslToHsb( + self::rgbToHsl($rgb) + ) + ); + } + + /** + * Convert HWB to RGB + * via hwb -> hsb -> hsl -> rgb + * + * @param HWB $hwb + * @return RGB + */ + public static function hwbToRgb(HWB $hwb): RGB + { + return self::hslToRgb( + self::hsbToHsl( + self::hwbToHsb($hwb) + ) + ); + } + // MARK: HSL <-> HSB /** @@ -318,6 +393,38 @@ class Color ]); } + // MARK: HSL <-> HWB + + /** + * Convert HSL to HWB + * via hsl -> hsb -> hwb + * + * @param HSL $hsl + * @return HWB + */ + public static function hslToHwb(HSL $hsl): HWB + { + return self::hsbToHwb( + self::hslToHsb( + $hsl + ) + ); + } + + /** + * Convert HWB to HSL + * via hwb -> hsb -> hsl + * + * @param HWB $hwb + * @return HSL + */ + public static function hwbToHsl(HWB $hwb): HSL + { + return self::hsbToHsl( + self::hwbToHsb($hwb) + ); + } + // MARK: HSB <-> HWB /** @@ -366,70 +473,31 @@ class Color ]); } - // MARK: RGB <-> HWB + // MARK: LAB <-> LCH + + // toLch /** - * Convert RGB to HWB - * via rgb -> hsl -> hsb -> hwb + * CIE Lab to LCH * - * @param RGB $rgb - * @return HWB + * @param Lab $lab + * @return LCH */ - public static function rgbToHwb(RGB $rgb): HWB + public static function labToLch(Lab $lab): LCH { - return self::hsbToHwb( - self::hslToHsb( - self::rgbToHsl($rgb) - ) - ); + // cieLab to cieLch + return LCH::__constructFromArray(self::__labToLch($lab), colorspace: 'CIELab'); } /** - * Convert HWB to RGB - * via hwb -> hsb -> hsl -> rgb + * Convert CIE LCH to Lab * - * @param HWB $hwb - * @return RGB + * @param LCH $lch + * @return Lab */ - public static function hwbToRgb(HWB $hwb): RGB + public static function lchToLab(LCH $lch): Lab { - return self::hslToRgb( - self::hsbToHsl( - self::hwbToHsb($hwb) - ) - ); - } - - // MARK: HSL <-> HWB - - /** - * Convert HSL to HWB - * via hsl -> hsb -> hwb - * - * @param HSL $hsl - * @return HWB - */ - public static function hslToHwb(HSL $hsl): HWB - { - return self::hsbToHwb( - self::hslToHsb( - $hsl - ) - ); - } - - /** - * Convert HWB to HSL - * via hwb -> hsb -> hsl - * - * @param HWB $hwb - * @return HSL - */ - public static function hwbToHsl(HWB $hwb): HSL - { - return self::hsbToHsl( - self::hwbToHsb($hwb) - ); + return Lab::__constructFromArray(self::__lchToLab($lch), colorspace: 'CIELab'); } // MARK: OkLch <-> OkLab @@ -443,16 +511,7 @@ class Color public static function okLabToOkLch(Lab $lab): LCH { // okLab\toOkLch - $a = $lab->a; - $b = $lab->b; - - $hue = atan2($b, $a) * 180 / pi(); - - return LCH::__constructFromArray([ - $lab->L, - sqrt($a ** 2 + $b ** 2), - $hue >= 0 ? $hue : $hue + 360, - ]); + return LCH::__constructFromArray(self::__labToLch($lab), colorspace: 'OkLab'); } /** @@ -465,114 +524,7 @@ class Color { // oklch/toOkLab // oklch to oklab - return Lab::__constructFromArray([ - $lch->L, - $lch->C * cos($lch->H * pi() / 180), // a - $lch->C * sin($lch->H * pi() / 180), // b - ], 'Oklab'); - } - - // MARK: xyzD65 <-> linearRGB - - /** - * convert linear RGB to xyz D65 - * if rgb is not flagged linear, it will be auto converted - * - * @param RGB $rgb - * @return XYZD65 - */ - public static function linRgbToXyzD65(RGB $rgb): XYZD65 - { - // if not linear, convert to linear - if (!$rgb->linear) { - $rgb->toLinear(); - } - return XYZD65::__constructFromArray(Math::multiplyMatrices( - [ - [0.41239079926595934, 0.357584339383878, 0.1804807884018343], - [0.21263900587151027, 0.715168678767756, 0.07219231536073371], - [0.01933081871559182, 0.11919477979462598, 0.9505321522496607], - ], - $rgb->returnAsArray() - )); - } - - /** - * Convert xyz D65 to linear RGB - * - * @param XYZD65 $xyzD65 - * @return RGB - */ - public static function xyzD65ToLinRgb(XYZD65 $xyzD65): RGB - { - // xyz D65 to linrgb - return RGB::__constructFromArray(Math::multiplyMatrices( - a : [ - [ 3.2409699419045226, -1.537383177570094, -0.4986107602930034 ], - [ -0.9692436362808796, 1.8759675015077202, 0.04155505740717559 ], - [ 0.05563007969699366, -0.20397695888897652, 1.0569715142428786 ], - ], - b : $xyzD65->returnAsArray() - ), linear: true); - } - - // MARK: xyzD65 <-> OkLab - - /** - * xyz D65 to OkLab - * - * @param XYZD65 $xyzD65 - * @return Lab - */ - public static function xyzD65ToOkLab(XYZD65 $xyzD65): Lab - { - return Lab::__constructFromArray(Math::multiplyMatrices( - [ - [0.2104542553, 0.7936177850, -0.0040720468], - [1.9779984951, -2.4285922050, 0.4505937099], - [0.0259040371, 0.7827717662, -0.8086757660], - ], - array_map( - callback: fn ($v) => pow($v, 1 / 3), - array: Math::multiplyMatrices( - a: [ - [0.8190224432164319, 0.3619062562801221, -0.12887378261216414], - [0.0329836671980271, 0.9292868468965546, 0.03614466816999844], - [0.048177199566046255, 0.26423952494422764, 0.6335478258136937], - ], - b: $xyzD65->returnAsArray(), - ), - ) - ), 'Oklab'); - } - - /** - * xyz D65 to OkLab - * - * @param Lab $lab - * @return XYZD65 - */ - public static function okLabToXyzD65(Lab $lab): XYZD65 - { - return XYZD65::__constructFromArray(Math::multiplyMatrices( - a: [ - [1.2268798733741557, -0.5578149965554813, 0.28139105017721583], - [-0.04057576262431372, 1.1122868293970594, -0.07171106666151701], - [-0.07637294974672142, -0.4214933239627914, 1.5869240244272418], - ], - b: array_map( - callback: fn ($v) => is_numeric($v) ? $v ** 3 : 0, - array: Math::multiplyMatrices( - a: [ - [0.99999999845051981432, 0.39633779217376785678, 0.21580375806075880339], - [1.0000000088817607767, -0.1055613423236563494, -0.063854174771705903402], - [1.0000000546724109177, -0.089484182094965759684, -1.2914855378640917399], - ], - // Divide $lightness by 100 to convert from CSS OkLab - b: $lab->returnAsArray(), - ), - ), - )); + return Lab::__constructFromArray(self::__lchToLab($lch), colorspace: 'OkLab'); } // MARK: rgb <-> oklab @@ -585,9 +537,7 @@ class Color */ public static function rgbToOkLab(RGB $rgb): Lab { - return self::xyzD65ToOkLab( - self::linRgbToXyzD65($rgb) - ); + return CieXyz::rgbViaXyzD65ToOkLab($rgb); } /** @@ -598,9 +548,7 @@ class Color */ public static function okLabToRgb(Lab $lab): RGB { - return self::xyzD65ToLinRgb( - self::okLabToXyzD65($lab) - )->fromLinear(); + return CieXyz::okLabViaXyzD65ToRgb($lab); } // MARK: rgb <-> oklch @@ -753,7 +701,7 @@ class Color * @param HWB $hwb * @return Lab */ - public function hwbToOkLab(HWB $hwb): Lab + public static function hwbToOkLab(HWB $hwb): Lab { return self::rgbToOkLab( self::hwbToRgb($hwb) @@ -766,7 +714,7 @@ class Color * @param Lab $lab * @return HWB */ - public function okLabToHwb(Lab $lab): HWB + public static function okLabToHwb(Lab $lab): HWB { return self::rgbToHwb( self::okLabToRgb($lab) @@ -781,7 +729,7 @@ class Color * @param HWB $hwb * @return LCH */ - public function hwbToOkLch(HWB $hwb): LCH + public static function hwbToOkLch(HWB $hwb): LCH { return self::rgbToOkLch( self::hwbToRgb($hwb) @@ -794,10 +742,367 @@ class Color * @param LCH $lch * @return HWB */ - public function okLchToHwb(LCH $lch): HWB + public static function okLchToHwb(LCH $lch): HWB { return self::rgbToHwb( self::okLchToRgb($lch) ); } + + // MARK: RGB <-> Lab (Cie) + + /** + * RGB to Lab + * via RGB -> linRgb -> xyz D65 -> xyz D50 -> Lab + * + * @param RGB $rgb + * @return Lab + */ + public static function rgbToLab(RGB $rgb): Lab + { + return CieXyz::rgbViaXyzD65ViaXyzD50ToLab($rgb); + /* return CieXyz::xyzD50ToLab( + CieXyz::xyzD65ToXyzD50( + CieXyz::linRgbToXyzD65($rgb) + ) + ); */ + } + + /** + * Lab to RGB + * via Lab -> xyz D50 -> xyz D65 -> lin RGB -> RGB + * + * @param Lab $lab + * @return RGB + */ + public static function labToRgb(Lab $lab): RGB + { + return CieXyz::labViaXyzD50ViaXyzD65ToRgb($lab); + /* return CieXyz::xyzD65ToLinRgb( + CieXyz::xyzD50ToXyxD65( + CieXyz::labToXyzD50($lab) + ) + )->fromLinear(); */ + } + + // MARK: RGB <-> Lch (Cie) + + /** + * Convert RGB to LCH (Cie) + * via RGB to Lab + * + * @param RGB $rgb + * @return LCH + */ + public static function rgbToLch(RGB $rgb): LCH + { + // return self::rgbToL + return self::labToLch( + self::rgbToLab($rgb) + ); + } + + /** + * Convert LCH (Cie) to RGB + * via Lab to RGB + * + * @param LCH $lch + * @return RGB + */ + public static function lchToRgb(LCH $lch): RGB + { + return self::labToRgb( + self::lchToLab($lch) + ); + } + + // MARK: HSL <-> Lab (CIE) + + /** + * HSL to Lab (CIE) + * + * @param HSL $hsl + * @return Lab + */ + public static function hslToLab(HSL $hsl): Lab + { + return self::rgbToLab( + self::hslToRgb($hsl) + ); + } + + /** + * Lab (CIE) to HSL + * + * @param Lab $lab + * @return HSL + */ + public static function labToHsl(Lab $lab): HSL + { + return self::rgbToHsl( + self::labToRgb($lab) + ); + } + + // MARK: HSL <-> Lch (CIE) + + /** + * Undocumented function + * + * @param HSL $hsl + * @return LCH + */ + public static function hslToLch(HSL $hsl): LCH + { + return self::rgbToLch( + self::hslToRgb($hsl) + ); + } + + /** + * Undocumented function + * + * @param LCH $lch + * @return HSL + */ + public static function lchToHsl(LCH $lch): HSL + { + return self::rgbToHsl( + self::lchToRgb($lch) + ); + } + + // MARK: HSB <-> Lab (CIE) + + /** + * Undocumented function + * + * @param HSB $hsb + * @return Lab + */ + public static function hsbToLab(HSB $hsb): Lab + { + return self::rgbToLab( + self::hsbToRgb($hsb) + ); + } + + /** + * Undocumented function + * + * @param Lab $lab + * @return HSB + */ + public static function labToHsb(Lab $lab): HSB + { + return self::rgbToHsb( + self::labToRgb($lab) + ); + } + + // MARK: HSB <-> Lch (CIE) + + /** + * Undocumented function + * + * @param HSB $hsb + * @return LCH + */ + public static function hsbToLch(HSB $hsb): LCH + { + return self::rgbToLch( + self::hsbToRgb($hsb) + ); + } + + /** + * Undocumented function + * + * @param LCH $lch + * @return HSB + */ + public static function lchToHsb(LCH $lch): HSB + { + return self::rgbToHsb( + self::lchToRgb($lch) + ); + } + + // MARK: HWB <-> Lab (CIE) + + /** + * Undocumented function + * + * @param HWB $hwb + * @return Lab + */ + public static function hwbToLab(HWB $hwb): Lab + { + return self::rgbToLab( + self::hwbToRgb($hwb) + ); + } + + /** + * Undocumented function + * + * @param Lab $lab + * @return HWB + */ + public static function labToHwb(Lab $lab): HWB + { + return self::rgbToHwb( + self::labToRgb($lab) + ); + } + + // MARK: HWB <-> Lch (CIE) + + /** + * Undocumented function + * + * @param HWB $hwb + * @return Lch + */ + public static function hwbToLch(HWB $hwb): Lch + { + return self::rgbToLch( + self::hwbToRgb($hwb) + ); + } + + /** + * Undocumented function + * + * @param LCH $lch + * @return HWB + */ + public static function lchToHweb(LCH $lch): HWB + { + return self::rgbToHwb( + self::lchToRgb($lch) + ); + } + + // MARK: Lab (Cie) <-> OkLab + + /** + * okLab to Lab (Cie) + * + * @param Lab $lab + * @return Lab + */ + public static function okLabToLab(Lab $lab): Lab + { + return CieXyz::okLabViaXyzD65ViaXyzD50ToLab($lab); + /* return CieXyz::xyzD50ToLab( + CieXyz::xyzD65ToXyzD50( + CieXyz::okLabToXyzD65($lab) + ) + ); */ + } + + /** + * Lab (Cie) to okLab + * + * @param Lab $lab + * @return Lab + */ + public static function labToOkLab(Lab $lab): Lab + { + return CieXyz::labViaXyzD50ViaXyzD65ToOkLab($lab); + /* return CieXyz::xyzD65ToOkLab( + CieXyz::xyzD50ToXyxD65( + CieXyz::labToXyzD50($lab) + ) + ); */ + } + + // MARK: Lab (Cie) <-> Oklch + + /** + * OkLch to Lab (CIE) + * + * @param LCH $lch + * @return Lab + */ + public static function okLchToLab(LCH $lch): Lab + { + return self::okLabToLab( + self::okLchToOkLab($lch) + ); + } + + /** + * Lab (CIE) to OkLch + * + * @param Lab $lab + * @return LCH + */ + public static function labToOkLch(Lab $lab): LCH + { + return self::okLabToOkLch( + self::labToOkLab($lab) + ); + } + + // MARK: Lch (Cie) <-> OkLch + + /** + * okLch to Lch (Cie) + * via okLabToLab + * + * @param LCH $lch + * @return LCH + */ + public static function okLchToLch(LCH $lch): LCH + { + return self::labToLch( + self::okLabToLab( + self::okLchToOkLab($lch) + ) + ); + } + + /** + * Lch (Cie) to OkLch + * via labToOkLab + * + * @param LCH $lch + * @return LCH + */ + public static function lchToOkLch(LCH $lch): LCH + { + return self::labToOkLch( + self::lchToLab($lch) + ); + } + + // MARK: Lch (Cie) to OkLab + + /** + * OkLab to Lch (CIE) + * + * @param LAB $lab + * @return LCH + */ + public static function okLabToLch(LAB $lab): LCH + { + return self::labToLch( + self::okLabToLab($lab) + ); + } + + /** + * Lch (CIE) to OkLab + * + * @param LCH $lch + * @return LAB + */ + public static function lchToOkLab(LCH $lch): LAB + { + return self::labToOkLab( + self::lchToLab($lch) + ); + } } diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/HSB.php b/www/lib/CoreLibs/Convert/Color/Coordinates/HSB.php index b435a9ef..7ebb2dc6 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/HSB.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/HSB.php @@ -13,6 +13,9 @@ namespace CoreLibs\Convert\Color\Coordinates; class HSB { + /** @var array allowed colorspaces */ + private const COLORSPACES = ['sRGB']; + /** @var float hue */ private float $H = 0.0; /** @var float saturation */ @@ -20,6 +23,9 @@ class HSB /** @var float brightness / value */ private float $B = 0.0; + /** @var string color space: either ok or cie */ + private string $colorspace = ''; + /** * HSB (HSV) color coordinates * Hue/Saturation/Brightness or Value @@ -28,29 +34,17 @@ class HSB { } - /** - * set with each value as parameters - * - * @param float $H Hue - * @param float $S Saturation - * @param float $B Brightness - * @return self - */ - public static function __constructFromSet(float $H, float $S, float $B): self - { - return (new HSB())->setAsArray([$H, $S, $B]); - } - /** * set from array * where 0: Hue, 1: Saturation, 2: Brightness * - * @param array{0:float,1:float,2:float} $hsb + * @param array{0:float,1:float,2:float} $colors + * @param string $colorspace [default=sRGB] * @return self */ - public static function __constructFromArray(array $hsb): self + public static function __constructFromArray(array $colors, string $colorspace = 'sRGB'): self { - return (new HSB())->setAsArray($hsb); + return (new HSB())->setColorspace($colorspace)->setFromArray($colors); } /** @@ -113,6 +107,21 @@ class HSB return $this->$name; } + /** + * set the colorspace + * + * @param string $colorspace + * @return self + */ + private function setColorspace(string $colorspace): self + { + if (!in_array($colorspace, $this::COLORSPACES)) { + throw new \InvalidArgumentException('Not allowed colorspace', 0); + } + $this->colorspace = $colorspace; + return $this; + } + /** * Returns the color as array * where 0: Hue, 1: Saturation, 2: Brightness @@ -128,14 +137,14 @@ class HSB * set color as array * where 0: Hue, 1: Saturation, 2: Brightness * - * @param array{0:float,1:float,2:float} $hsb + * @param array{0:float,1:float,2:float} $colors * @return self */ - public function setAsArray(array $hsb): self + public function setFromArray(array $colors): self { - $this->__set('H', $hsb[0]); - $this->__set('S', $hsb[1]); - $this->__set('B', $hsb[2]); + $this->__set('H', $colors[0]); + $this->__set('S', $colors[1]); + $this->__set('B', $colors[2]); return $this; } diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/HSL.php b/www/lib/CoreLibs/Convert/Color/Coordinates/HSL.php index 21be7fe5..15005471 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/HSL.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/HSL.php @@ -11,14 +11,23 @@ declare(strict_types=1); namespace CoreLibs\Convert\Color\Coordinates; +use CoreLibs\Convert\Color\Stringify; + class HSL { + /** @var array allowed colorspaces */ + private const COLORSPACES = ['sRGB']; + /** @var float hue */ private float $H = 0.0; /** @var float saturation */ private float $S = 0.0; /** @var float lightness (luminance) */ private float $L = 0.0; + + /** @var string color space: either ok or cie */ + private string $colorspace = ''; + /** * Color Coordinate HSL * Hue/Saturation/Lightness @@ -27,29 +36,17 @@ class HSL { } - /** - * set with each value as parameters - * - * @param float $H Hue - * @param float $S Saturation - * @param float $L Lightness - * @return self - */ - public static function __constructFromSet(float $H, float $S, float $L): self - { - return (new HSL())->setAsArray([$H, $S, $L]); - } - /** * set from array * where 0: Hue, 1: Saturation, 2: Lightness * - * @param array{0:float,1:float,2:float} $hsl + * @param array{0:float,1:float,2:float} $colors + * @param string $colorspace [default=sRGB] * @return self */ - public static function __constructFromArray(array $hsl): self + public static function __constructFromArray(array $colors, string $colorspace = 'sRGB'): self { - return (new HSL())->setAsArray($hsl); + return (new HSL())->setColorspace($colorspace)->setFromArray($colors); } /** @@ -110,6 +107,21 @@ class HSL return $this->$name; } + /** + * set the colorspace + * + * @param string $colorspace + * @return self + */ + private function setColorspace(string $colorspace): self + { + if (!in_array($colorspace, $this::COLORSPACES)) { + throw new \InvalidArgumentException('Not allowed colorspace', 0); + } + $this->colorspace = $colorspace; + return $this; + } + /** * Returns the color as array * where 0: Hue, 1: Saturation, 2: Lightness @@ -125,16 +137,35 @@ class HSL * set color as array * where 0: Hue, 1: Saturation, 2: Lightness * - * @param array{0:float,1:float,2:float} $hsl + * @param array{0:float,1:float,2:float} $colors * @return self */ - public function setAsArray(array $hsl): self + public function setFromArray(array $colors): self { - $this->__set('H', $hsl[0]); - $this->__set('S', $hsl[1]); - $this->__set('L', $hsl[2]); + $this->__set('H', $colors[0]); + $this->__set('S', $colors[1]); + $this->__set('L', $colors[2]); return $this; } + + /** + * convert to css string with optional opacityt + * + * @param float|string|null $opacity + * @return string + */ + public function toCssString(null|float|string $opacity = null): string + { + $string = 'hsl(' + . $this->H + . ' ' + . $this->S + . ' ' + . $this->L + . Stringify::setOpacity($opacity) + . ')'; + return $string; + } } // __END__ diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/HWB.php b/www/lib/CoreLibs/Convert/Color/Coordinates/HWB.php index ee6b7f63..1dd6eb23 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/HWB.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/HWB.php @@ -11,14 +11,23 @@ declare(strict_types=1); namespace CoreLibs\Convert\Color\Coordinates; +use CoreLibs\Convert\Color\Stringify; + class HWB { + /** @var array allowed colorspaces */ + private const COLORSPACES = ['sRGB']; + /** @var float Hue */ private float $H = 0.0; /** @var float Whiteness */ private float $W = 0.0; /** @var float Blackness */ private float $B = 0.0; + + /** @var string color space: either ok or cie */ + private string $colorspace = ''; + /** * Color Coordinate: HWB * Hue/Whiteness/Blackness @@ -27,29 +36,17 @@ class HWB { } - /** - * set with each value as parameters - * - * @param float $H Hue - * @param float $W Whiteness - * @param float $B Blackness - * @return self - */ - public static function __constructFromSet(float $H, float $W, float $B): self - { - return (new HWB())->setAsArray([$H, $W, $B]); - } - /** * set from array * where 0: Hue, 1: Whiteness, 2: Blackness * - * @param array{0:float,1:float,2:float} $hwb + * @param array{0:float,1:float,2:float} $colors + * @param string $colorspace [default=sRGB] * @return self */ - public static function __constructFromArray(array $hwb): self + public static function __constructFromArray(array $colors, string $colorspace = 'sRGB'): self { - return (new HWB())->setAsArray($hwb); + return (new HWB())->setColorspace($colorspace)->setFromArray($colors); } /** @@ -110,6 +107,21 @@ class HWB return $this->$name; } + /** + * set the colorspace + * + * @param string $colorspace + * @return self + */ + private function setColorspace(string $colorspace): self + { + if (!in_array($colorspace, $this::COLORSPACES)) { + throw new \InvalidArgumentException('Not allowed colorspace', 0); + } + $this->colorspace = $colorspace; + return $this; + } + /** * Returns the color as array * where 0: Hue, 1: Whiteness, 2: Blackness @@ -125,16 +137,35 @@ class HWB * set color as array * where 0: Hue, 1: Whiteness, 2: Blackness * - * @param array{0:float,1:float,2:float} $hwb + * @param array{0:float,1:float,2:float} $colors * @return self */ - public function setAsArray(array $hwb): self + public function setFromArray(array $colors): self { - $this->__set('H', $hwb[0]); - $this->__set('W', $hwb[1]); - $this->__set('B', $hwb[2]); + $this->__set('H', $colors[0]); + $this->__set('W', $colors[1]); + $this->__set('B', $colors[2]); return $this; } + + /** + * convert to css string with optional opacityt + * + * @param float|string|null $opacity + * @return string + */ + public function toCssString(null|float|string $opacity = null): string + { + $string = 'hwb(' + . $this->H + . ' ' + . $this->W + . ' ' + . $this->B + . Stringify::setOpacity($opacity) + . ')'; + return $string; + } } // __END__ diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/LCH.php b/www/lib/CoreLibs/Convert/Color/Coordinates/LCH.php index 648fb466..872c597b 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/LCH.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/LCH.php @@ -12,8 +12,13 @@ declare(strict_types=1); namespace CoreLibs\Convert\Color\Coordinates; +use CoreLibs\Convert\Color\Stringify; + class LCH { + /** @var array allowed colorspaces */ + private const COLORSPACES = ['OkLab', 'CIELab']; + /** @var float Lightness/Luminance * CIE: 0 to 100 * OKlch: 0.0 to 1.0 @@ -42,29 +47,17 @@ class LCH { } - /** - * set with each value as parameters - * - * @param float $L - * @param float $c - * @param float $h - * @return self - */ - public static function __constructFromSet(float $L, float $c, float $h): self - { - return (new LCH())->setAsArray([$L, $c, $h]); - } - /** * set from array * where 0: Lightness, 1: Chroma, 2: Hue * - * @param array{0:float,1:float,2:float} $lch + * @param array{0:float,1:float,2:float} $colors + * @param string $colorspace * @return self */ - public static function __constructFromArray(array $lch): self + public static function __constructFromArray(array $colors, string $colorspace): self { - return (new LCH())->setAsArray($lch); + return (new LCH())->setColorspace($colorspace)->setFromArray($colors); } /** @@ -139,6 +132,21 @@ class LCH return $this->$name; } + /** + * set the colorspace + * + * @param string $colorspace + * @return self + */ + private function setColorspace(string $colorspace): self + { + if (!in_array($colorspace, $this::COLORSPACES)) { + throw new \InvalidArgumentException('Not allowed colorspace', 0); + } + $this->colorspace = $colorspace; + return $this; + } + /** * Returns the color as array * where 0: Lightness, 1: Chroma, 2: Hue @@ -154,16 +162,45 @@ class LCH * set color as array * where 0: Lightness, 1: Chroma, 2: Hue * - * @param array{0:float,1:float,2:float} $lch + * @param array{0:float,1:float,2:float} $colors * @return self */ - public function setAsArray(array $lch): self + public function setFromArray(array $colors): self { - $this->__set('L', $lch[0]); - $this->__set('C', $lch[1]); - $this->__set('H', $lch[2]); + $this->__set('L', $colors[0]); + $this->__set('C', $colors[1]); + $this->__set('H', $colors[2]); return $this; } + + /** + * Convert into css string with optional opacity + * + * @param null|float|string|null $opacity + * @return string + */ + public function toCssString(null|float|string $opacity = null): string + { + $string = ''; + switch ($this->colorspace) { + case 'CIELab': + $string = 'lch'; + break; + case 'OkLab': + $string = 'oklch'; + break; + } + $string .= '(' + . $this->L + . ' ' + . $this->c + . ' ' + . $this->h + . Stringify::setOpacity($opacity) + . ');'; + + return $string; + } } // __END__ diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/Lab.php b/www/lib/CoreLibs/Convert/Color/Coordinates/Lab.php index e2eb11a4..8e9f573a 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/Lab.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/Lab.php @@ -12,10 +12,12 @@ declare(strict_types=1); namespace CoreLibs\Convert\Color\Coordinates; +use CoreLibs\Convert\Color\Stringify; + class Lab { /** @var array allowed colorspaces */ - private const COLORSPACES = ['Oklab', 'cie']; + private const COLORSPACES = ['OkLab', 'CIELab']; /** @var float lightness/luminance * CIE: 0f to 100f @@ -47,20 +49,6 @@ class Lab { } - /** - * set with each value as parameters - * - * @param float $L - * @param float $a - * @param float $b - * @param string $colorspace - * @return self - */ - public static function __constructFromSet(float $L, float $a, float $b, string $colorspace): self - { - return (new Lab())->setColorspace($colorspace)->setAsArray([$L, $a, $b]); - } - /** * set from array * where 0: Lightness, 1: a, 2: b @@ -69,9 +57,9 @@ class Lab * @param string $colorspace * @return self */ - public static function __constructFromArray(array $lab, string $colorspace): self + public static function __constructFromArray(array $colors, string $colorspace): self { - return (new Lab())->setColorspace($colorspace)->setAsArray($lab); + return (new Lab())->setColorspace($colorspace)->setFromArray($colors); } /** @@ -162,16 +150,45 @@ class Lab * set color as array * where 0: Lightness, 1: a, 2: b * - * @param array{0:float,1:float,2:float} $lab + * @param array{0:float,1:float,2:float} $colors * @return self */ - public function setAsArray(array $lab): self + public function setFromArray(array $colors): self { - $this->__set('L', $lab[0]); - $this->__set('a', $lab[1]); - $this->__set('b', $lab[2]); + $this->__set('L', $colors[0]); + $this->__set('a', $colors[1]); + $this->__set('b', $colors[2]); return $this; } + + /** + * Convert into css string with optional opacity + * + * @param null|float|string|null $opacity + * @return string + */ + public function toCssString(null|float|string $opacity = null): string + { + $string = ''; + switch ($this->colorspace) { + case 'CIELab': + $string = 'lab'; + break; + case 'OkLab': + $string = 'oklab'; + break; + } + $string .= '(' + . $this->L + . ' ' + . $this->a + . ' ' + . $this->b + . Stringify::setOpacity($opacity) + . ');'; + + return $string; + } } // __END__ diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/RGB.php b/www/lib/CoreLibs/Convert/Color/Coordinates/RGB.php index acc81952..7fc3eda0 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/RGB.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/RGB.php @@ -11,8 +11,13 @@ declare(strict_types=1); namespace CoreLibs\Convert\Color\Coordinates; +use CoreLibs\Convert\Color\Stringify; + class RGB { + /** @var array allowed colorspaces */ + private const COLORSPACES = ['sRGB']; + /** @var float red 0 to 255 or 0.0f to 1.0f for linear RGB */ private float $R = 0.0; /** @var float green 0 to 255 or 0.0f to 1.0f for linear RGB */ @@ -20,6 +25,9 @@ class RGB /** @var float blue 0 to 255 or 0.0f to 1.0f for linear RGB */ private float $B = 0.0; + /** @var string color space: either ok or cie */ + private string $colorspace = ''; + /** @var bool set if this is linear */ private bool $linear = false; @@ -30,31 +38,34 @@ class RGB { } - /** - * set with each value as parameters - * - * @param float $R Red - * @param float $G Green - * @param float $B Blue - * @param bool $linear [default=false] - * @return self - */ - public static function __constructFromSet(float $R, float $G, float $B, bool $linear = false): self - { - return (new RGB())->flagLinear($linear)->setAsArray([$R, $G, $B]); - } - /** * set from array * where 0: Red, 1: Green, 2: Blue * - * @param array{0:float,1:float,2:float} $rgb + * @param array{0:float,1:float,2:float} $colors + * @param string $colorspace [default=sRGB] * @param bool $linear [default=false] * @return self */ - public static function __constructFromArray(array $rgb, bool $linear = false): self + public static function __constructFromArray(array $colors, string $colorspace = 'sRGB', bool $linear = false): self { - return (new RGB())->flagLinear($linear)->setAsArray($rgb); + return (new RGB())->setColorspace($colorspace)->flagLinear($linear)->setFromArray($colors); + } + + /** + * Undocumented function + * + * @param string $hex_string + * @param string $colorspace + * @param bool $linear + * @return self + */ + public static function __constructFromHexString( + string $hex_string, + string $colorspace = 'sRGB', + bool $linear = false + ): self { + return (new RGB())->setColorspace($colorspace)->flagLinear($linear)->setFromHex($hex_string); } /** @@ -99,6 +110,21 @@ class RGB return $this->$name; } + /** + * set the colorspace + * + * @param string $colorspace + * @return self + */ + private function setColorspace(string $colorspace): self + { + if (!in_array($colorspace, $this::COLORSPACES)) { + throw new \InvalidArgumentException('Not allowed colorspace', 0); + } + $this->colorspace = $colorspace; + return $this; + } + /** * Returns the color as array * where 0: Red, 1: Green, 2: Blue @@ -114,17 +140,76 @@ class RGB * set color as array * where 0: Red, 1: Green, 2: Blue * - * @param array{0:float,1:float,2:float} $rgb + * @param array{0:float,1:float,2:float} $colors * @return self */ - public function setAsArray(array $rgb): self + public function setFromArray(array $colors): self { - $this->__set('R', $rgb[0]); - $this->__set('G', $rgb[1]); - $this->__set('B', $rgb[2]); + $this->__set('R', $colors[0]); + $this->__set('G', $colors[1]); + $this->__set('B', $colors[2]); return $this; } + /** + * Return current set RGB as hex string. with or without # prefix + * + * @param bool $hex_prefix + * @return string + */ + public function returnAsHex(bool $hex_prefix = true): string + { + // prefix + $hex_color = ''; + if ($hex_prefix === true) { + $hex_color = '#'; + } + // convert if in linear + if ($this->linear) { + $this->fromLinear(); + } + foreach ($this->returnAsArray() as $color) { + $hex_color .= str_pad(dechex((int)$color), 2, '0', STR_PAD_LEFT); + } + return $hex_color; + } + + /** + * set colors RGB from hex string + * + * @param string $hex_string + * @return self + */ + public function setFromHex(string $hex_string): self + { + $hex_string = preg_replace("/[^0-9A-Fa-f]/", '', $hex_string); // Gets a proper hex string + if (!is_string($hex_string)) { + throw new \InvalidArgumentException('hex_string argument cannot be empty', 1); + } + $rgbArray = []; + if (strlen($hex_string) == 6) { + // If a proper hex code, convert using bitwise operation. + // No overhead... faster + $colorVal = hexdec($hex_string); + $rgbArray = [ + 0xFF & ($colorVal >> 0x10), + 0xFF & ($colorVal >> 0x8), + 0xFF & $colorVal + ]; + } elseif (strlen($hex_string) == 3) { + // If shorthand notation, need some string manipulations + $rgbArray = [ + hexdec(str_repeat(substr($hex_string, 0, 1), 2)), + hexdec(str_repeat(substr($hex_string, 1, 1), 2)), + hexdec(str_repeat(substr($hex_string, 2, 1), 2)) + ]; + } else { + // Invalid hex color code + throw new \UnexpectedValueException('Invalid hex_string: ' . $hex_string, 2); + } + return $this->setFromArray($rgbArray); + } + /** * set as linear * can be used as chain call on create if input is linear RGB @@ -153,7 +238,7 @@ class RGB */ public function toLinear(): self { - $this->flagLinear(true)->setAsArray(array_map( + $this->flagLinear(true)->setFromArray(array_map( callback: function (int|float $v) { $v = (float)($v / 255); $abs = abs($v); @@ -177,7 +262,7 @@ class RGB */ public function fromLinear(): self { - $this->flagLinear(false)->setAsArray(array_map( + $this->flagLinear(false)->setFromArray(array_map( callback: function (int|float $v) { $abs = abs($v); $sign = ($v < 0) ? -1 : 1; @@ -197,29 +282,31 @@ class RGB /** * convert to css string with optional opacity - * Note: if this is a linea RGB, this data will not be correct + * Note: if this is a linear RGB, the data will converted during this operation and the converted back * * @param float|string|null $opacity * @return string */ public function toCssString(null|float|string $opacity = null): string { - // set opacity, either a string or float - if (is_string($opacity)) { - $opacity = ' / ' . $opacity; - } elseif ($opacity !== null) { - $opacity = ' / ' . $opacity; - } else { - $opacity = ''; + // if we are in linear mode, convert to normal mode temporary + $was_linear = false; + if ($this->linear) { + $this->fromLinear(); + $was_linear = true; } - return 'rgb(' + $string = 'rgb(' . (int)round($this->R, 0) . ' ' . (int)round($this->G, 0) . ' ' . (int)round($this->B, 0) - . $opacity + . Stringify::setOpacity($opacity) . ')'; + if ($was_linear) { + $this->toLinear(); + } + return $string; } } diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/XYZ.php b/www/lib/CoreLibs/Convert/Color/Coordinates/XYZ.php new file mode 100644 index 00000000..7c773ed9 --- /dev/null +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/XYZ.php @@ -0,0 +1,162 @@ + allowed colorspaces */ + private const COLORSPACES = ['CIEXYZ']; + /** @var array allowed whitepoints + * D50: ICC profile PCS (horizon light) <-> CieLab + * D65: RGB color space (noon) <-> linear RGB + */ + private const ILLUMINANT = ['D50', 'D65']; + + /** @var float X coordinate */ + private float $X = 0.0; + /** @var float Y coordinate (Luminance) */ + private float $Y = 0.0; + /** @var float Z coordinate (blue) */ + private float $Z = 0.0; + + /** @var string color space: either ok or cie */ + private string $colorspace = ''; + + private string $whitepoint = ''; + + /** + * Color Coordinate Lch + * for oklch + */ + public function __construct() + { + } + + /** + * set from array + * where 0: X, 1: Y, 2: Z + * + * @param array{0:float,1:float,2:float} $colors + * @param string $colorspace [default=CIEXYZ] + * @param string $whitepoint [default=''] only D65 or D50 allowed + * @return self + */ + public static function __constructFromArray( + array $colors, + string $colorspace = 'CIEXYZ', + string $whitepoint = '' + ): self { + return (new XYZ()) + ->setColorspace($colorspace) + ->setWhitepoint($whitepoint) + ->setFromArray($colors); + } + + /** + * set color + * + * @param string $name + * @param float $value + * @return void + */ + public function __set(string $name, float $value): void + { + if (!property_exists($this, $name)) { + throw new \ErrorException('Creation of dynamic property is not allowed', 0); + } + // if ($value < 0 || $value > 255) { + // throw new \LengthException('Argument value ' . $value . ' for color ' . $name + // . ' is not in the range of 0 to 255', 1); + // } + $this->$name = $value; + } + + /** + * get color + * + * @param string $name + * @return float + */ + public function __get(string $name): float + { + if (!property_exists($this, $name)) { + throw new \ErrorException('Creation of dynamic property is not allowed', 0); + } + return $this->$name; + } + + /** + * set the colorspace + * + * @param string $colorspace + * @return self + */ + private function setColorspace(string $colorspace): self + { + if (!in_array($colorspace, $this::COLORSPACES)) { + throw new \InvalidArgumentException('Not allowed colorspace', 0); + } + $this->colorspace = $colorspace; + return $this; + } + + /** + * set the whitepoint flag + * + * @param string $whitepoint + * @return self + */ + private function setWhitepoint(string $whitepoint): self + { + if (empty($whitepoint)) { + $this->whitepoint = ''; + return $this; + } + if (!in_array($whitepoint, $this::ILLUMINANT)) { + throw new \InvalidArgumentException('Not allowed whitepoint', 0); + } + $this->whitepoint = $whitepoint; + return $this; + } + + /** + * Returns the color as array + * where 0: X, 1: Y, 2: Z + * + * @return array{0:float,1:float,2:float} + */ + public function returnAsArray(): array + { + return [$this->X, $this->Y, $this->Z]; + } + + /** + * set color as array + * where 0: X, 1: Y, 2: Z + * + * @param array{0:float,1:float,2:float} $colors + * @return self + */ + public function setFromArray(array $colors): self + { + $this->__set('X', $colors[0]); + $this->__set('Y', $colors[1]); + $this->__set('Z', $colors[2]); + return $this; + } +} + +// __END__ diff --git a/www/lib/CoreLibs/Convert/Color/Stringify.php b/www/lib/CoreLibs/Convert/Color/Stringify.php index 6ca68431..72dfa020 100644 --- a/www/lib/CoreLibs/Convert/Color/Stringify.php +++ b/www/lib/CoreLibs/Convert/Color/Stringify.php @@ -20,7 +20,26 @@ use CoreLibs\Convert\Color\Coordinates\LCH; class Stringify { /** - * Undocumented function + * Build the opactiy sub string part and return it + * + * @param null|float|string|null $opacity + * @return string + */ + public static function setOpacity(null|float|string $opacity = null): string + { + // set opacity, either a string or float + if (is_string($opacity)) { + $opacity = ' / ' . $opacity; + } elseif ($opacity !== null) { + $opacity = ' / ' . $opacity; + } else { + $opacity = ''; + } + return $opacity; + } + + /** + * return the CSS string including optional opacity * * @param RGB|Lab|LCH|HSL|HWB $data * @param null|float|string $opacity diff --git a/www/lib/CoreLibs/Convert/Colors.php b/www/lib/CoreLibs/Convert/Colors.php index 8ff32608..61fe9384 100644 --- a/www/lib/CoreLibs/Convert/Colors.php +++ b/www/lib/CoreLibs/Convert/Colors.php @@ -17,6 +17,9 @@ declare(strict_types=1); namespace CoreLibs\Convert; +use CoreLibs\Convert\Color\Color; +use CoreLibs\Convert\Color\Coordinates; + class Colors { /** @@ -37,7 +40,8 @@ class Colors int $blue, bool $hex_prefix = true ): string { - $hex_color = ''; + return Coordinates\RGB::__constructFromArray([$red, $green, $blue])->returnAsHex($hex_prefix); + /* $hex_color = ''; if ($hex_prefix === true) { $hex_color = '#'; } @@ -50,7 +54,7 @@ class Colors // pad left with 0 $hex_color .= str_pad(dechex($$color), 2, '0', STR_PAD_LEFT); } - return $hex_color; + return $hex_color; */ } /** @@ -69,7 +73,7 @@ class Colors bool $return_as_string = false, string $seperator = ',' ): string|array { - $hex_string = preg_replace("/[^0-9A-Fa-f]/", '', $hex_string); // Gets a proper hex string + /* $hex_string = preg_replace("/[^0-9A-Fa-f]/", '', $hex_string); // Gets a proper hex string if (!is_string($hex_string)) { throw new \InvalidArgumentException('hex_string argument cannot be empty', 1); } @@ -89,7 +93,8 @@ class Colors } else { // Invalid hex color code throw new \UnexpectedValueException('Invalid hex_string: ' . $hex_string, 2); - } + } */ + $rgbArray = Coordinates\RGB::__constructFromHexString($hex_string)->returnAsArray(); // returns the rgb string or the associative array return $return_as_string ? implode($seperator, $rgbArray) : $rgbArray; } @@ -108,7 +113,10 @@ class Colors */ public static function rgb2hsb(int $red, int $green, int $blue): array { - // check that rgb is from 0 to 255 + return Color::rgbToHsb( + Coordinates\RGB::__constructFromArray([$red, $green, $blue]) + )->returnAsArray(); + /* // check that rgb is from 0 to 255 foreach (['red', 'green', 'blue'] as $color) { if ($$color < 0 || $$color > 255) { throw new \LengthException('Argument value ' . $$color . ' for color ' . $color @@ -143,7 +151,7 @@ class Colors (int)round($HUE), // Hue (int)round(($DELTA / $MAX) * 100), // Saturation (int)round($MAX * 100) // Value/Brightness - ]; + ]; */ } /** @@ -159,7 +167,11 @@ class Colors */ public static function hsb2rgb(float $H, float $S, float $V): array { - // check that H is 0 to 359, 360 = 0 + return Color::hsbToRgb( + Coordinates\HSB::__constructFromArray([$H, $S, $V]) + )->returnAsArray(); + + /* // check that H is 0 to 359, 360 = 0 // and S and V are 0 to 1 if ($H == 360) { $H = 0; @@ -229,7 +241,7 @@ class Colors (int)round($red * 255), (int)round($green * 255), (int)round($blue * 255) - ]; + ]; */ } /** @@ -245,7 +257,11 @@ class Colors */ public static function rgb2hsl(int $red, int $green, int $blue): array { - // check that rgb is from 0 to 255 + return Color::rgbToHsl( + Coordinates\RGB::__constructFromArray([$red, $green, $blue]) + )->returnAsArray(); + + /* // check that rgb is from 0 to 255 foreach (['red', 'green', 'blue'] as $color) { if ($$color < 0 || $$color > 255) { throw new \LengthException('Argument value ' . $$color . ' for color ' . $color @@ -285,7 +301,7 @@ class Colors round($sat * 100, 1), round($lum * 100, 1) ]; - } + } */ } /** @@ -300,7 +316,11 @@ class Colors */ public static function hsl2rgb(float $hue, float $sat, float $lum): array { - if ($hue == 360) { + return Color::hslToRgb( + Coordinates\HSL::__constructFromArray([$hue, $sat, $lum]) + )->returnAsArray(); + + /* if ($hue == 360) { $hue = 0; } if ($hue < 0 || $hue > 359) { @@ -347,7 +367,7 @@ class Colors (int)round(255 * $hueue($hue)), (int)round(255 * $hueue($hue - (1 / 3))) ]; - } + } */ } } From 625272198d7630f7daa20db8bb27e3430884ffb9 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Wed, 13 Nov 2024 11:42:24 +0900 Subject: [PATCH 05/13] Math matrix phpunit checks added --- .../tests/Convert/CoreLibsConvertMathTest.php | 163 +++++++++++++++++- www/lib/CoreLibs/Convert/Math.php | 4 +- 2 files changed, 158 insertions(+), 9 deletions(-) diff --git a/4dev/tests/Convert/CoreLibsConvertMathTest.php b/4dev/tests/Convert/CoreLibsConvertMathTest.php index c98b4b2a..476c5c1f 100644 --- a/4dev/tests/Convert/CoreLibsConvertMathTest.php +++ b/4dev/tests/Convert/CoreLibsConvertMathTest.php @@ -18,7 +18,7 @@ final class CoreLibsConvertMathTest extends TestCase * * @return array */ - public function fceilProvider(): array + public function providerFceil(): array { return [ '5.5 must be 6' => [5.5, 6], @@ -31,7 +31,7 @@ final class CoreLibsConvertMathTest extends TestCase * Undocumented function * * @covers ::fceil - * @dataProvider fceilProvider + * @dataProvider providerFceil * @testdox fceil: Input $input must be $expected * * @param float $input @@ -51,7 +51,7 @@ final class CoreLibsConvertMathTest extends TestCase * * @return array */ - public function floorProvider(): array + public function providerFloor(): array { return [ '5123456 with -3 must be 5123000' => [5123456, -3, 5123000], @@ -63,7 +63,7 @@ final class CoreLibsConvertMathTest extends TestCase * Undocumented function * * @covers ::floorp - * @dataProvider floorProvider + * @dataProvider providerFloor * @testdox floor: Input $input with cutoff $cutoff must be $expected * * @param int $input @@ -84,7 +84,7 @@ final class CoreLibsConvertMathTest extends TestCase * * @return array */ - public function initNumericProvider(): array + public function providerInitNumeric(): array { return [ '5 must be 5' => [5, 5, 'int'], @@ -98,7 +98,7 @@ final class CoreLibsConvertMathTest extends TestCase * Undocumented function * * @covers ::initNumeric - * @dataProvider initNumericProvider + * @dataProvider providerInitNumeric * @testdox initNumeric: Input $info $input must match $expected [$_dataName] * * @param int|float|string $input @@ -114,7 +114,156 @@ final class CoreLibsConvertMathTest extends TestCase ); } - // TODO: cbrt tests + /** + * Undocumented function + * + * @return array + */ + public function providerCbrt(): array + { + return [ + 'cube root of 2' => [2, 1.25992, 5], + 'cube root of 3' => [3, 1.44225, 5], + 'cube root of -1' => [-1, 'NAN', 0], + ]; + } + + /** + * Undocumented function + * + * @covers ::cbrt + * @dataProvider providerCbrt + * @testdox initNumeric: Input $input must match $expected [$_dataName] + * + * @param float|int $number + * @param float $expected + * @param int $round_to + * @return void + */ + public function testCbrt(float|int $number, float|string $expected, int $round_to): void + { + print "OUT: " . \CoreLibs\Convert\Math::cbrt($number) . "\n"; + $this->assertEquals( + $expected, + round(\CoreLibs\Convert\Math::cbrt($number), $round_to) + ); + } + + /** + * Undocumented function + * + * @return array + */ + public function providerMultiplyMatrices(): array + { + return [ + 'single' => [ + [1, 2, 3], + [1, 2, 3], + [14] + ], + 'double first' => [ + [ + [1, 2, 3], + [1, 2, 3] + ], + [1, 2, 3], + [ + 14, + 14 + ] + ], + 'double both' => [ + [ + [1, 2, 3], + [1, 2, 3], + ], + [ + [1, 2, 3], + [1, 2, 3], + ], + [ + [3, 6, 9], + [3, 6, 9] + ] + ], + 'tripple first, single second' => [ + [ + [1, 2, 3], + [1, 2, 3], + [1, 2, 3], + ], + [1, 2, 3], + [ + 14, + 14, + 14 + ] + ], + 'tripple first, double second' => [ + [ + [1, 2, 3], + [1, 2, 3], + [1, 2, 3], + ], + [ + [1, 2, 3], + [1, 2, 3], + ], + [ + [3, 6, 9], + [3, 6, 9], + [3, 6, 9], + ] + ], + 'single first, tripple second' => [ + [1, 2, 3], + [ + [1, 2, 3], + [1, 2, 3], + [1, 2, 3], + ], + [ + [6, 12, 18], + ] + ], + 'double first, tripple second' => [ + [ + [1, 2, 3], + [1, 2, 3], + ], + [ + [1, 2, 3], + [1, 2, 3], + [1, 2, 3], + ], + [ + [6, 12, 18], + [6, 12, 18], + ] + ], + ]; + } + + /** + * Undocumented function + * + * @covers ::multiplyMatrices + * @dataProvider providerMultiplyMatrices + * @testdox initNumeric: Input $input_a x $input_b must match $expected [$_dataName] + * + * @param array $input_a + * @param array $input_b + * @param array $expected + * @return void + */ + public function testMultiplyMatrices(array $input_a, array $input_b, array $expected): void + { + $this->assertEquals( + $expected, + \CoreLibs\Convert\Math::multiplyMatrices($input_a, $input_b) + ); + } } // __END__ diff --git a/www/lib/CoreLibs/Convert/Math.php b/www/lib/CoreLibs/Convert/Math.php index 26739c5f..8af796b1 100644 --- a/www/lib/CoreLibs/Convert/Math.php +++ b/www/lib/CoreLibs/Convert/Math.php @@ -63,9 +63,9 @@ class Math * @param float $number Number to cubic root * @return float Calculated value */ - public static function cbrt(float $number): float + public static function cbrt(float|int $number): float { - return pow($number, 1.0 / 3); + return pow((float)$number, 1.0 / 3); } /** From f5964fed02934911aed3aa21635d40a588bb0b25 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Wed, 13 Nov 2024 12:45:21 +0900 Subject: [PATCH 06/13] Legacy colors test update --- .../Convert/CoreLibsConvertColorsTest.php | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/4dev/tests/Convert/CoreLibsConvertColorsTest.php b/4dev/tests/Convert/CoreLibsConvertColorsTest.php index aecd3f09..5d730055 100644 --- a/4dev/tests/Convert/CoreLibsConvertColorsTest.php +++ b/4dev/tests/Convert/CoreLibsConvertColorsTest.php @@ -9,7 +9,7 @@ use PHPUnit\Framework\TestCase; /** * Test class for Convert\Colors * @coversDefaultClass \CoreLibs\Convert\Colors - * @testdox \CoreLibs\Convert\Colors method tests + * @testdox \CoreLibs\Convert\Colors legacy method tests */ final class CoreLibsConvertColorsTest extends TestCase { @@ -21,7 +21,7 @@ final class CoreLibsConvertColorsTest extends TestCase * * @return array */ - public function rgb2hexColorProvider(): array + public function providerRgb2hexColor(): array { return [ 'color' => [ @@ -88,7 +88,7 @@ final class CoreLibsConvertColorsTest extends TestCase * * @return array */ - public function hex2rgbColorProvider(): array + public function providerHex2rgbColor(): array { return [ 'color' => [ @@ -215,7 +215,7 @@ final class CoreLibsConvertColorsTest extends TestCase * * @return array */ - public function rgb2hsbColorProvider(): array + public function providerRgb2hsbColor(): array { $list = []; foreach ($this->rgb2hslAndhsbList() as $name => $values) { @@ -234,7 +234,7 @@ final class CoreLibsConvertColorsTest extends TestCase * * @return array */ - public function hsb2rgbColorProvider(): array + public function providerHsb2rgbColor(): array { $list = []; foreach ($this->rgb2hslAndhsbList() as $name => $values) { @@ -253,7 +253,7 @@ final class CoreLibsConvertColorsTest extends TestCase * * @return array */ - public function rgb2hslColorProvider(): array + public function providerRgb2hslColor(): array { $list = []; foreach ($this->rgb2hslAndhsbList() as $name => $values) { @@ -272,7 +272,7 @@ final class CoreLibsConvertColorsTest extends TestCase * * @return array */ - public function hsl2rgbColorProvider(): array + public function providerHsl2rgbColor(): array { $list = []; foreach ($this->rgb2hslAndhsbList() as $name => $values) { @@ -291,7 +291,7 @@ final class CoreLibsConvertColorsTest extends TestCase * TODO: add cross convert check * * @covers ::rgb2hex - * @dataProvider rgb2hexColorProvider + * @dataProvider providerRgb2hexColor * @testdox rgb2hex $input_r,$input_g,$input_b will be $expected [$_dataName] * * @param int $input_r @@ -342,7 +342,7 @@ final class CoreLibsConvertColorsTest extends TestCase * Undocumented function * * @covers ::hex2rgb - * @dataProvider hex2rgbColorProvider + * @dataProvider providerHex2rgbColor * @testdox hex2rgb $input will be $expected, $expected_str str[,], $expected_str_sep str[$separator] [$_dataName] * * @param string $input @@ -385,7 +385,7 @@ final class CoreLibsConvertColorsTest extends TestCase * Undocumented function * * @covers ::rgb2hsb - * @dataProvider rgb2hsbColorProvider + * @dataProvider providerRgb2hsbColor * @testdox rgb2hsb $input_r,$input_g,$input_b will be $expected [$_dataName] * * @param integer $input_r @@ -409,7 +409,7 @@ final class CoreLibsConvertColorsTest extends TestCase * Undocumented function * * @covers ::hsb2rgb - * @dataProvider hsb2rgbColorProvider + * @dataProvider providerHsb2rgbColor * @testdox hsb2rgb $input_h,$input_s,$input_b will be $expected [$_dataName] * * @param float $input_h @@ -434,7 +434,7 @@ final class CoreLibsConvertColorsTest extends TestCase * Undocumented function * * @covers ::rgb2hsl - * @dataProvider rgb2hslColorProvider + * @dataProvider providerRgb2hslColor * @testdox rgb2hsl $input_r,$input_g,$input_b will be $expected [$_dataName] * * @param integer $input_r @@ -458,7 +458,7 @@ final class CoreLibsConvertColorsTest extends TestCase * Undocumented function * * @covers ::hsl2rgb - * @dataProvider hsl2rgbColorProvider + * @dataProvider providerHsl2rgbColor * @testdox hsl2rgb $input_h,$input_s,$input_l will be $expected [$_dataName] * * @param integer|float $input_h From 2bd68f32ac79fb295c42bb7162e702aec6bc1210 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Wed, 13 Nov 2024 13:16:02 +0900 Subject: [PATCH 07/13] Legacy color convert update to use new methods --- www/lib/CoreLibs/Convert/Colors.php | 285 ++++------------------------ 1 file changed, 39 insertions(+), 246 deletions(-) diff --git a/www/lib/CoreLibs/Convert/Colors.php b/www/lib/CoreLibs/Convert/Colors.php index 61fe9384..96aaee12 100644 --- a/www/lib/CoreLibs/Convert/Colors.php +++ b/www/lib/CoreLibs/Convert/Colors.php @@ -41,20 +41,6 @@ class Colors bool $hex_prefix = true ): string { return Coordinates\RGB::__constructFromArray([$red, $green, $blue])->returnAsHex($hex_prefix); - /* $hex_color = ''; - if ($hex_prefix === true) { - $hex_color = '#'; - } - foreach (['red', 'green', 'blue'] as $color) { - // if not valid, abort - if ($$color < 0 || $$color > 255) { - throw new \LengthException('Argument value ' . $$color . ' for color ' . $color - . ' is not in the range of 0 to 255', 1); - } - // pad left with 0 - $hex_color .= str_pad(dechex($$color), 2, '0', STR_PAD_LEFT); - } - return $hex_color; */ } /** @@ -73,28 +59,22 @@ class Colors bool $return_as_string = false, string $seperator = ',' ): string|array { - /* $hex_string = preg_replace("/[^0-9A-Fa-f]/", '', $hex_string); // Gets a proper hex string - if (!is_string($hex_string)) { - throw new \InvalidArgumentException('hex_string argument cannot be empty', 1); - } $rgbArray = []; - if (strlen($hex_string) == 6) { - // If a proper hex code, convert using bitwise operation. - // No overhead... faster - $colorVal = hexdec($hex_string); - $rgbArray['r'] = 0xFF & ($colorVal >> 0x10); - $rgbArray['g'] = 0xFF & ($colorVal >> 0x8); - $rgbArray['b'] = 0xFF & $colorVal; - } elseif (strlen($hex_string) == 3) { - // If shorthand notation, need some string manipulations - $rgbArray['r'] = hexdec(str_repeat(substr($hex_string, 0, 1), 2)); - $rgbArray['g'] = hexdec(str_repeat(substr($hex_string, 1, 1), 2)); - $rgbArray['b'] = hexdec(str_repeat(substr($hex_string, 2, 1), 2)); - } else { - // Invalid hex color code - throw new \UnexpectedValueException('Invalid hex_string: ' . $hex_string, 2); - } */ - $rgbArray = Coordinates\RGB::__constructFromHexString($hex_string)->returnAsArray(); + // rewrite to previous r/g/b key output + foreach (Coordinates\RGB::__constructFromHexString($hex_string)->returnAsArray() as $p => $el) { + switch ($p) { + case 0: + $k = 'r'; + break; + case 1: + $k = 'g'; + break; + case 2: + $k = 'b'; + break; + } + $rgbArray[$k] = (int)round($el); + } // returns the rgb string or the associative array return $return_as_string ? implode($seperator, $rgbArray) : $rgbArray; } @@ -113,45 +93,12 @@ class Colors */ public static function rgb2hsb(int $red, int $green, int $blue): array { - return Color::rgbToHsb( - Coordinates\RGB::__constructFromArray([$red, $green, $blue]) - )->returnAsArray(); - /* // check that rgb is from 0 to 255 - foreach (['red', 'green', 'blue'] as $color) { - if ($$color < 0 || $$color > 255) { - throw new \LengthException('Argument value ' . $$color . ' for color ' . $color - . ' is not in the range of 0 to 255', 1); - } - $$color = $$color / 255; - } - - $MAX = max($red, $green, $blue); - $MIN = min($red, $green, $blue); - $HUE = 0; - $DELTA = $MAX - $MIN; - - // achromatic - if ($MAX == $MIN) { - return [0, 0, round($MAX * 100)]; - } - if ($red == $MAX) { - $HUE = fmod(($green - $blue) / $DELTA, 6); - } elseif ($green == $MAX) { - $HUE = (($blue - $red) / $DELTA) + 2; - } elseif ($blue == $MAX) { - $HUE = (($red - $green) / $DELTA) + 4; - } - $HUE *= 60; - // avoid negative - if ($HUE < 0) { - $HUE += 360; - } - - return [ - (int)round($HUE), // Hue - (int)round(($DELTA / $MAX) * 100), // Saturation - (int)round($MAX * 100) // Value/Brightness - ]; */ + return array_map( + fn ($v) => (int)round($v), + Color::rgbToHsb( + Coordinates\RGB::__constructFromArray([$red, $green, $blue]) + )->returnAsArray() + ); } /** @@ -167,81 +114,12 @@ class Colors */ public static function hsb2rgb(float $H, float $S, float $V): array { - return Color::hsbToRgb( - Coordinates\HSB::__constructFromArray([$H, $S, $V]) - )->returnAsArray(); - - /* // check that H is 0 to 359, 360 = 0 - // and S and V are 0 to 1 - if ($H == 360) { - $H = 0; - } - if ($H < 0 || $H > 359) { - throw new \LengthException('Argument value ' . $H . ' for hue is not in the range of 0 to 359', 1); - } - if ($S < 0 || $S > 100) { - throw new \LengthException('Argument value ' . $S . ' for saturation is not in the range of 0 to 100', 2); - } - if ($V < 0 || $V > 100) { - throw new \LengthException('Argument value ' . $V . ' for brightness is not in the range of 0 to 100', 3); - } - // convert to internal 0-1 format - $S /= 100; - $V /= 100; - - if ($S == 0) { - $V = (int)round($V * 255); - return [$V, $V, $V]; - } - - $Hi = floor($H / 60); - $f = ($H / 60) - $Hi; - $p = $V * (1 - $S); - $q = $V * (1 - ($S * $f)); - $t = $V * (1 - ($S * (1 - $f))); - - switch ($Hi) { - case 0: - $red = $V; - $green = $t; - $blue = $p; - break; - case 1: - $red = $q; - $green = $V; - $blue = $p; - break; - case 2: - $red = $p; - $green = $V; - $blue = $t; - break; - case 3: - $red = $p; - $green = $q; - $blue = $V; - break; - case 4: - $red = $t; - $green = $p; - $blue = $V; - break; - case 5: - $red = $V; - $green = $p; - $blue = $q; - break; - default: - $red = 0; - $green = 0; - $blue = 0; - } - - return [ - (int)round($red * 255), - (int)round($green * 255), - (int)round($blue * 255) - ]; */ + return array_map( + fn ($v) => (int)round($v), + Color::hsbToRgb( + Coordinates\HSB::__constructFromArray([$H, $S, $V]) + )->returnAsArray() + ); } /** @@ -257,51 +135,12 @@ class Colors */ public static function rgb2hsl(int $red, int $green, int $blue): array { - return Color::rgbToHsl( - Coordinates\RGB::__constructFromArray([$red, $green, $blue]) - )->returnAsArray(); - - /* // check that rgb is from 0 to 255 - foreach (['red', 'green', 'blue'] as $color) { - if ($$color < 0 || $$color > 255) { - throw new \LengthException('Argument value ' . $$color . ' for color ' . $color - . ' is not in the range of 0 to 255', 1); - } - $$color = $$color / 255; - } - - $min = min($red, $green, $blue); - $max = max($red, $green, $blue); - $chroma = $max - $min; - $sat = 0; - $hue = 0; - // luminance - $lum = ($max + $min) / 2; - - // achromatic - if ($chroma == 0) { - // H, S, L - return [0.0, 0.0, round($lum * 100, 1)]; - } else { - $sat = $chroma / (1 - abs(2 * $lum - 1)); - if ($max == $red) { - $hue = fmod((($green - $blue) / $chroma), 6); - if ($hue < 0) { - $hue = (6 - fmod(abs($hue), 6)); - } - } elseif ($max == $green) { - $hue = ($blue - $red) / $chroma + 2; - } elseif ($max == $blue) { - $hue = ($red - $green) / $chroma + 4; - } - $hue = $hue * 60; - // $sat = 1 - abs(2 * $lum - 1); - return [ - round($hue, 1), - round($sat * 100, 1), - round($lum * 100, 1) - ]; - } */ + return array_map( + fn ($v) => round($v, 1), + Color::rgbToHsl( + Coordinates\RGB::__constructFromArray([$red, $green, $blue]) + )->returnAsArray() + ); } /** @@ -316,58 +155,12 @@ class Colors */ public static function hsl2rgb(float $hue, float $sat, float $lum): array { - return Color::hslToRgb( - Coordinates\HSL::__constructFromArray([$hue, $sat, $lum]) - )->returnAsArray(); - - /* if ($hue == 360) { - $hue = 0; - } - if ($hue < 0 || $hue > 359) { - throw new \LengthException('Argument value ' . $hue . ' for hue is not in the range of 0 to 359', 1); - } - if ($sat < 0 || $sat > 100) { - throw new \LengthException('Argument value ' . $sat . ' for saturation is not in the range of 0 to 100', 2); - } - if ($lum < 0 || $lum > 100) { - throw new \LengthException('Argument value ' . $lum . ' for luminance is not in the range of 0 to 100', 3); - } - // calc to internal convert value for hue - $hue = (1 / 360) * $hue; - // convert to internal 0-1 format - $sat /= 100; - $lum /= 100; - // if saturation is 0 - if ($sat == 0) { - $lum = (int)round($lum * 255); - return [$lum, $lum, $lum]; - } else { - $m2 = $lum < 0.5 ? $lum * ($sat + 1) : ($lum + $sat) - ($lum * $sat); - $m1 = $lum * 2 - $m2; - $hueue = function ($base) use ($m1, $m2) { - // base = hue, hue > 360 (1) - 360 (1), else < 0 + 360 (1) - $base = $base < 0 ? $base + 1 : ($base > 1 ? $base - 1 : $base); - // 6: 60, 2: 180, 3: 240 - // 2/3 = 240 - // 1/3 = 120 (all from 360) - if ($base * 6 < 1) { - return $m1 + ($m2 - $m1) * $base * 6; - } - if ($base * 2 < 1) { - return $m2; - } - if ($base * 3 < 2) { - return $m1 + ($m2 - $m1) * ((2 / 3) - $base) * 6; - } - return $m1; - }; - - return [ - (int)round(255 * $hueue($hue + (1 / 3))), - (int)round(255 * $hueue($hue)), - (int)round(255 * $hueue($hue - (1 / 3))) - ]; - } */ + return array_map( + fn ($v) => round($v), + Color::hslToRgb( + Coordinates\HSL::__constructFromArray([$hue, $sat, $lum]) + )->returnAsArray() + ); } } From 32c192a362b336f5bea6bc980e71fd68d905f4db Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Wed, 13 Nov 2024 19:19:35 +0900 Subject: [PATCH 08/13] Basic colors test add started Also fixes for various things that come up during test writing Test phpunit not yet finished (exceptions, etc) Note: a lot of checks for extreme values are (int) so we do not fail for small float values --- .../Convert/CoreLibsConvertColorTest.php | 345 ++++++++++++++++++ www/admin/class_test.convert.colors.php | 101 +++-- www/lib/CoreLibs/Convert/Color/Color.php | 19 +- .../Convert/Color/Coordinates/HSB.php | 8 +- .../Convert/Color/Coordinates/HSL.php | 8 +- .../Convert/Color/Coordinates/HWB.php | 8 +- .../Convert/Color/Coordinates/RGB.php | 5 +- 7 files changed, 432 insertions(+), 62 deletions(-) create mode 100644 4dev/tests/Convert/CoreLibsConvertColorTest.php diff --git a/4dev/tests/Convert/CoreLibsConvertColorTest.php b/4dev/tests/Convert/CoreLibsConvertColorTest.php new file mode 100644 index 00000000..3b5d950f --- /dev/null +++ b/4dev/tests/Convert/CoreLibsConvertColorTest.php @@ -0,0 +1,345 @@ + round($v), + $values + ); + } + + public function testSingle() + { + $this->assertTrue(true, 'Single test'); + // $rgb = Color\Coordinates\RGB::__constructFromArray([0, 0, 60]); + // print "IN: " . print_r($rgb, true) . "\n"; + // $hsl = Color\Color::rgbToHsl($rgb); + // print "to HSL: " . print_r($hsl, true) . "\n"; + // $hsb = Color\Color::hslToHsb($hsl); + // print "to HSB: " . print_r($hsb, true) . "\n"; + // $hwb = Color\Color::hsbToHwb($hsb); + // print "to HWB: " . print_r($hwb, true) . "\n"; + // // and reverse + // $hsb_r = Color\Color::hwbToHsb($hwb); + // print "R to HSB: " . print_r($hsb_r, true) . "\n"; + // $hsl_r = Color\Color::hsbToHsl($hsb_r); + // print "R to HSB: " . print_r($hsl_r, true) . "\n"; + // $rgb_r = Color\Color::hslToRgb($hsl_r); + // print "R to RGB: " . print_r($rgb_r, true) . "\n"; + + // $hsl = Color\Coordinates\HSL::__constructFromArray([0, 0, 0]); + // print "IN HSL: " . print_r($hsl, true) . "\n"; + // $hsb = Color\Color::hslToHsb($hsl); + // print "to HSB: " . print_r($hsb, true) . "\n"; + // $hwb = Color\Color::hsbToHwb($hsb); + // print "to HWB: " . print_r($hwb, true) . "\n"; + // // and reverse + // $hsb_r = Color\Color::hwbToHsb($hwb); + // print "R to HSB: " . print_r($hsb_r, true) . "\n"; + // $hsl_r = Color\Color::hsbToHsl($hsb_r); + // print "R to HSL: " . print_r($hsl_r, true) . "\n"; + // print "--------\n"; + // $hsb = Color\Coordinates\HSB::__constructFromArray([0, 20, 0]); + // print "IN HSB: " . print_r($hsb, true) . "\n"; + // $hsl = Color\Color::hsbToHsl($hsb); + // print "to HSL: " . print_r($hsl, true) . "\n"; + // $hwb = Color\Color::hslToHwb($hsl); + // print "to HWB: " . print_r($hwb, true) . "\n"; + // // and reverse + // $hsl_r = Color\Color::hwbToHsl($hwb); + // print "R to HSB: " . print_r($hsb_r, true) . "\n"; + // $hsb_r = Color\Color::hslToHsb($hsl_r); + // print "R to HSL: " . print_r($hsb_r, true) . "\n"; + // print "--------\n"; + // $hwb = Color\Coordinates\HWB::__constructFromArray([0, 20, 100]); + // print "IN: " . print_r($hwb, true) . "\n"; + // $hsl = Color\Color::hwbToHsl($hwb); + // print "to HSL: " . print_r($hsl, true) . "\n"; + // $hwb_r = Color\Color::hslToHwb($hsl); + // print "HSL to HWB: " . print_r($hwb_r, true) . "\n"; + // $hsb = Color\Color::hwbToHsb($hwb); + // print "to HSB: " . print_r($hsb, true) . "\n"; + // $hwb_r = Color\Color::hsbToHwb($hsb); + // print "HSL to HWB: " . print_r($hwb_r, true) . "\n"; + } + + /** + * From/To RGB <-> ... conversion tests + * + * @covers ::rgbToHsb + * @covers ::rgbToHsl + * @covers ::rgbToHwb + * @covers ::hsbToRgb + * @covers ::hslToRgb + * @covers ::hwebToRgb + * @testdox Convert from and to RGB via HSL, HWB, HSB/V + * + * @return void + */ + public function testRgbColorCoordinateConvertToAndBack(): void + { + for ($r = 0; $r <= 300; $r += 60) { + for ($g = 0; $g <= 300; $g += 60) { + for ($b = 0; $b <= 300; $b += 60) { + // for this test we stay in the correct lane + if ($r > 255) { + $r = 255; + } + if ($g > 255) { + $g = 255; + } + if ($b > 255) { + $b = 255; + } + // base is always the same + $color = Color\Coordinates\RGB::__constructFromArray([$r, $g, $b]); + $base = 'rgb'; + foreach (['hsb', 'hsl', 'hwb'] as $coord) { + // print "COORD: " . $coord . ", RGB: " . print_r($color->returnAsArray(), true) . "\n"; + // rgb to X and back must be same + $target = $base . 'To' . ucfirst($coord); + $source = $coord . 'To' . ucfirst($base); + $converted_color = Color\Color::$target($color); + $color_b = Color\Color::$source($converted_color); + // $converted_color = Color\Color::rgbToHsb($color); + // $rgb_b = Color\Color::hsbToRgb($converted_color); + $this->assertEqualsWithDelta( + $color->returnAsArray(), + $color_b->returnAsArray(), + self::DELTA, + 'Convert ' . $base . ' to ' . $coord . ': ' . print_r($color->returnAsArray(), true) . '/' + . print_r($color_b->returnAsArray(), true) + ); + } + } + } + } + } + + // HSL / HSB / HWB conversion are not reversable if + // HSL: lightness 0 or 100 + // HSB: saturation or brightness 0 + // HWB: blackness >= 80 and whitness >= 20 or B>=20 & W>=20 or B>=50 & W>=50 + + /** + * Undocumented function + * + * @covers ::hslToHsb + * @covers ::hsbToHsl + * @covers ::hslToHwb + * @covers ::hwbToHsl + * @testdox Convert from and to HSL via RGB, HWB, HSB/V + * + * @return void + */ + public function testHslColorCoordinateConvertToAndBack(): void + { + for ($H = 0; $H <= 360; $H += 60) { + for ($S = 0; $S <= 100; $S += 20) { + for ($L = 0; $L <= 100; $L += 20) { + // if lightness 0 or 100 then we cannot reverse (B/W) + if (($L == 0 or $L == 100)) { + continue; + } + $color = Color\Coordinates\HSL::__constructFromArray([$H, $S, $L]); + $base = 'hsl'; + foreach (['hsb', 'hwb', 'rgb'] as $coord) { + // for rgb hue on S = 0 is irrelevant (B/W) + if ($H > 0 && $coord == 'rgb') { + continue; + } + $target = $base . 'To' . ucfirst($coord); + $source = $coord . 'To' . ucfirst($base); + $converted_color = Color\Color::$target($color); + $color_b = Color\Color::$source($converted_color); + // print "COORD: " . $coord . ", HSL: " . print_r($color->returnAsArray(), true) . "\n"; + $this->assertEqualsWithDelta( + $color->returnAsArray(), + $color_b->returnAsArray(), + self::DELTA, + 'Convert HSL to ' . $coord . ': ' . print_r($color->returnAsArray(), true) . '/' + . print_r($color_b->returnAsArray(), true) + ); + } + } + } + } + } + + /** + * Undocumented function + * + * @covers ::hsbToHsl + * @covers ::hslToHsb + * @covers ::hsbToHwb + * @covers ::hwbToHsb + * @testdox Convert from and to HSB via RGB, HWB, HSL + * + * @return void + */ + public function testHsbColorCoordinateConvertToAndBack(): void + { + for ($H = 0; $H <= 360; $H += 60) { + for ($S = 0; $S <= 100; $S += 20) { + for ($B = 0; $B <= 100; $B += 20) { + // if sat or brightness is 0 then we cannot reverse correctly (B/W) + if ($S == 0 or $B == 0) { + continue; + } + $color = Color\Coordinates\HSB::__constructFromArray([$H, $S, $B]); + $base = 'hsb'; + foreach (['hwb', 'hsl', 'rgb'] as $coord) { + $target = $base . 'To' . ucfirst($coord); + $source = $coord . 'To' . ucfirst($base); + $converted_color = Color\Color::$target($color); + $color_b = Color\Color::$source($converted_color); + // print "COORD: " . $coord . ", HSL: " . print_r($color->returnAsArray(), true) . "\n"; + $this->assertEqualsWithDelta( + $color->returnAsArray(), + $color_b->returnAsArray(), + self::DELTA, + 'Convert ' . $base . ' to ' . $coord . ': ' . print_r($color->returnAsArray(), true) . '/' + . print_r($color_b->returnAsArray(), true) + ); + } + } + } + } + } + + + /** + * Undocumented function + * + * @covers ::hwbToHsl + * @covers ::hslToHwb + * @covers ::hwbToHsb + * @covers ::hsbToHwb + * @testdox Convert from and to HWB via RGB, HSL, HSB/V + * + * @return void + */ + public function testHwbColorCoordinateConvertToAndBack(): void + { + for ($H = 0; $H <= 360; $H += 60) { + for ($W = 0; $W <= 100; $W += 20) { + for ($B = 0; $B <= 100; $B += 20) { + // if W>=20 and B>=80 or B>=20 and W>=20 or both >=50 + // we cannot reverse correctl (B/W) + if ( + ($W >= 20 && $B >= 80) || + ($W >= 80 && $B >= 20) || + ($W >= 50 && $B >= 50) + ) { + continue; + } + $base = 'hwb'; + $color = Color\Coordinates\HWB::__constructFromArray([$H, $W, $B]); + foreach (['hsl', 'hsb', 'rgb'] as $coord) { + // for rgb hue on S = 0 is irrelevant (B/W) + if ($H > 0 && $coord == 'rgb') { + continue; + } + $target = $base . 'To' . ucfirst($coord); + $source = $coord . 'To' . ucfirst($base); + $converted_color = Color\Color::$target($color); + $color_b = Color\Color::$source($converted_color); + // print "COORD: " . $coord . ", HSL: " . print_r($color->returnAsArray(), true) . "\n"; + $this->assertEqualsWithDelta( + $color->returnAsArray(), + $color_b->returnAsArray(), + self::DELTA, + 'Convert ' . $base . ' to ' . $coord . ': ' . print_r($color->returnAsArray(), true) . '/' + . print_r($color_b->returnAsArray(), true) + ); + } + } + } + } + } + + /** + * Undocumented function + * + * covers ::returnAsHex() + * covers ::__constructFromHexString() + * @testdox Convert from RGB to hex and back + * + * @return void + */ + public function testRgbToFromHex(): void + { + for ($r = 0; $r <= 300; $r += 60) { + for ($g = 0; $g <= 300; $g += 60) { + for ($b = 0; $b <= 300; $b += 60) { + // for this test we stay in the correct lane + if ($r > 255) { + $r = 255; + } + if ($g > 255) { + $g = 255; + } + if ($b > 255) { + $b = 255; + } + // with or without prefix + foreach ([true, false] as $hex_prefix) { + $hex_color = Color\Coordinates\RGB::__constructFromArray([$r, $g, $b]) + ->returnAsHex($hex_prefix); + // parse into hex to rgb and see if we get the same r/g/b + $color = Color\Coordinates\RGB::__constructFromHexString($hex_color)->returnAsArray(); + // + $this->assertEquals( + [$r, $g, $b], + $color, + 'Convert rgb to hex and back: ' . print_r([$r, $g, $b], true) . '/' + . print_r($color, true) + ); + } + } + } + } + } + + // oklab + + // cie lab + + // create exceptions for all color spaces + + public function testExceptionHSB(): void + { + // for H/S/B exception the same + $this->expectException(\LengthException::class); + Color\Coordinates\HSB::__constructFromArray([900, 10, 10]); + + // invalid access to class + // $this->expectException(\ErrorException::class); + } +} + +// __END__ diff --git a/www/admin/class_test.convert.colors.php b/www/admin/class_test.convert.colors.php index 84d70ce5..131bc642 100644 --- a/www/admin/class_test.convert.colors.php +++ b/www/admin/class_test.convert.colors.php @@ -68,8 +68,63 @@ print ""; print ''; print '

' . $PAGE_NAME . '

'; +// out of bounds test + // define a list of from to color sets for conversion test +$hwb = Color::hsbToHwb(Coordinates\HSB::__constructFromArray([ + 160, + 0, + 50, +])); +print "HWB: " . DgS::printAr($hwb) . "
"; +$hsb = Color::hwbToHsb($hwb); +print "HSB: " . DgS::printAr($hsb) . "
"; + +$oklch = Color::rgbToOkLch(Coordinates\RGB::__constructFromArray([ + 250, + 0, + 0 +])); +print "OkLch: " . DgS::printAr($oklch) . "
"; +$rgb = Color::okLchToRgb($oklch); +print "OkLch -> RGB: " . DgS::printAr($rgb) . "
"; + +$oklab = Color::rgbToOkLab(Coordinates\RGB::__constructFromArray([ + 250, + 0, + 0 +])); +print "OkLab: " . DgS::printAr($oklab) . "
"; +print display($oklab->toCssString(), $oklab->toCssString(), 'Oklab'); +$rgb = Color::okLabToRgb($oklab); +print "OkLab -> RGB: " . DgS::printAr($rgb) . "
"; +print display($rgb->toCssString(), $rgb->toCssString(), 'OkLab to RGB'); + +$rgb = Coordinates\RGB::__constructFromArray([250, 100, 10])->toLinear(); +print "RGBlinear: " . DgS::printAr($rgb) . "
"; +$rgb = Coordinates\RGB::__constructFromArray([0, 0, 0])->toLinear(); +print "RGBlinear: " . DgS::printAr($rgb) . "
"; + +$cie_lab = Color::okLabToLab($oklab); +print "CieLab: " . DgS::printAr($cie_lab) . "
"; +print display($cie_lab->toCssString(), $cie_lab->toCssString(), 'OkLab to Cie Lab'); + +$rgb = Coordinates\RGB::__constructFromArray([0, 0, 60]); +$hsb = Color::rgbToHsb($rgb); +$rgb_b = Color::hsbToRgb($hsb); +print "RGB: " . DgS::printAr($rgb) . "
"; +print "RGB->HSB: " . DgS::printAr($hsb) . "
"; +print "HSB->RGB: " . DgS::printAr($rgb_b) . "
"; + +$hsl = Coordinates\HSL::__constructFromArray([0, 20, 0]); +$hsb = Coordinates\HSB::__constructFromArray([0, 20, 0]); +$hsl_from_hsb = Color::hsbToHsl($hsb); +print "HSL from HSB: " . DgS::printAr($hsl_from_hsb) . "
"; + +print "
"; +print "

LEGACY

"; + // A(out of bounds) try { print "C::S/COLOR invalid rgb->hex (gray 125): -1, -1, -1: " @@ -83,6 +138,7 @@ try { } catch (\LengthException $e) { print "**Exception: " . $e->getMessage() . "
" . print_r($e, true) . "

"; } +print "
"; // B(valid) $rgb = [50, 20, 30]; $hex = '#0a141e'; @@ -125,6 +181,8 @@ print "S::COLOR hsb->rgb: $hsb[0], $hsb[1], $hsb[2]: " Colors::hsb2rgb($hsb[0], $hsb[1], $hsb[2]) )) . "
"; +print "
"; + // Random text $h = rand(0, 359); $s = rand(15, 70); @@ -134,49 +192,12 @@ print "RANDOM IN: H: " . $h . ", S: " . $s . ", B/L: " . $b . "/" . $l . "
"; print "RANDOM hsb->rgb:
" . DgS::printAr(SetVarType::setArray(Colors::hsb2rgb($h, $s, $b))) . "

"; print "RANDOM hsl->rgb:
" . DgS::printAr(SetVarType::setArray(Colors::hsl2rgb($h, $s, $l))) . "

"; +print "
"; + $rgb = [0, 0, 0]; print "rgb 0,0,0: " . Dgs::printAr($rgb) . " => " . Dgs::printAr(Colors::rgb2hsb($rgb[0], $rgb[1], $rgb[2])) . "
"; -// TODO: run compare check input must match output - -$hwb = Color::hsbToHwb(Coordinates\HSB::__constructFromArray([ - 160, - 0, - 50, -])); -print "HWB: " . DgS::printAr($hwb) . "
"; -$hsb = Color::hwbToHsb($hwb); -print "HSB: " . DgS::printAr($hsb) . "
"; - -$oklch = Color::rgbToOkLch(Coordinates\RGB::__constructFromArray([ - 250, - 0, - 0 -])); -print "OkLch: " . DgS::printAr($oklch) . "
"; -$rgb = Color::okLchToRgb($oklch); -print "OkLch -> RGB: " . DgS::printAr($rgb) . "
"; - -$oklab = Color::rgbToOkLab(Coordinates\RGB::__constructFromArray([ - 250, - 0, - 0 -])); -print "OkLab: " . DgS::printAr($oklab) . "
"; -print display($oklab->toCssString(), $oklab->toCssString(), 'Oklab'); -$rgb = Color::okLabToRgb($oklab); -print "OkLab -> RGB: " . DgS::printAr($rgb) . "
"; -print display($rgb->toCssString(), $rgb->toCssString(), 'OkLab to RGB'); - -$rgb = Coordinates\RGB::__constructFromArray([250, 100, 10])->toLinear(); -print "RGBlinear: " . DgS::printAr($rgb) . "
"; -$rgb = Coordinates\RGB::__constructFromArray([0, 0, 0])->toLinear(); -print "RGBlinear: " . DgS::printAr($rgb) . "
"; - -$cie_lab = Color::okLabToLab($oklab); -print "CieLab: " . DgS::printAr($cie_lab) . "
"; -print display($cie_lab->toCssString(), $cie_lab->toCssString(), 'OkLab to Cie Lab'); - +print "
"; print ""; diff --git a/www/lib/CoreLibs/Convert/Color/Color.php b/www/lib/CoreLibs/Convert/Color/Color.php index 3b490e9b..49e6bd96 100644 --- a/www/lib/CoreLibs/Convert/Color/Color.php +++ b/www/lib/CoreLibs/Convert/Color/Color.php @@ -354,15 +354,21 @@ class Color { $saturation = $hsl->S / 100; $lightness = $hsl->L / 100; - $value = $lightness + $saturation * min($lightness, 1 - $lightness); + // if lightness is 0, then we cannot return convert to hsb + $value = $lightness + $saturation * min($lightness, 1 - $lightness); + // print "Orig: " . print_r($hsl, true) . "\n"; + // print "SAT: " . $saturation . ", Lightness: " . $lightness . ", Value: " . $value . "\n"; + // var_dump($value); + // check for black and white - $saturation = ($value === 0) ? + $saturation = $value == 0 ? 0 : 200 * (1 - $lightness / $value); + $value *= 100; return HSB::__constructFromArray([ $hsl->H, $saturation, - $value * 100, + $value, ]); } @@ -377,11 +383,11 @@ class Color // hsv/toHsl $hue = $hsb->H; $saturation = $hsb->S / 100; - $value = $hsb->V / 100; + $value = $hsb->B / 100; $lightness = $value * (1 - $saturation / 2); // check for B/W - $saturation = in_array($lightness, [0, 1], true) ? + $saturation = in_array($lightness, [0, 1]) ? 0 : 100 * ($value - $lightness) / min($lightness, 1 - $lightness) ; @@ -456,6 +462,7 @@ class Color $blackness = $hwb->B / 100; $sum = $whiteness + $blackness; + // print "S: B/W: " . $sum . " /W: " . $whiteness . " /B: " . $blackness . "\n"; // for black and white if ($sum >= 1) { $saturation = 0; @@ -475,8 +482,6 @@ class Color // MARK: LAB <-> LCH - // toLch - /** * CIE Lab to LCH * diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/HSB.php b/www/lib/CoreLibs/Convert/Color/Coordinates/HSB.php index 7ebb2dc6..40a74dab 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/HSB.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/HSB.php @@ -62,10 +62,10 @@ class HSB } switch ($name) { case 'H': - if ($value == 360) { + if ((int)$value == 360) { $value = 0; } - if ($value < 0 || $value > 359) { + if ((int)$value < 0 || (int)$value > 359) { throw new \LengthException( 'Argument value ' . $value . ' for hue is not in the range of 0 to 359', 1 @@ -73,7 +73,7 @@ class HSB } break; case 'S': - if ($value < 0 || $value > 100) { + if ((int)$value < 0 || (int)$value > 100) { throw new \LengthException( 'Argument value ' . $value . ' for saturation is not in the range of 0 to 100', 2 @@ -81,7 +81,7 @@ class HSB } break; case 'B': - if ($value < 0 || $value > 100) { + if ((int)$value < 0 || (int)$value > 100) { throw new \LengthException( 'Argument value ' . $value . ' for brightness is not in the range of 0 to 100', 3 diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/HSL.php b/www/lib/CoreLibs/Convert/Color/Coordinates/HSL.php index 15005471..1b5b4ecc 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/HSL.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/HSL.php @@ -63,10 +63,10 @@ class HSL } switch ($name) { case 'H': - if ($value == 360) { + if ((int)$value == 360) { $value = 0; } - if ($value < 0 || $value > 359) { + if ((int)$value < 0 || (int)$value > 359) { throw new \LengthException( 'Argument value ' . $value . ' for hue is not in the range of 0 to 359', 1 @@ -74,7 +74,7 @@ class HSL } break; case 'S': - if ($value < 0 || $value > 100) { + if ((int)$value < 0 || (int)$value > 100) { throw new \LengthException( 'Argument value ' . $value . ' for saturation is not in the range of 0 to 100', 2 @@ -82,7 +82,7 @@ class HSL } break; case 'L': - if ($value < 0 || $value > 100) { + if ((int)$value < 0 || (int)$value > 100) { throw new \LengthException( 'Argument value ' . $value . ' for luminance is not in the range of 0 to 100', 3 diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/HWB.php b/www/lib/CoreLibs/Convert/Color/Coordinates/HWB.php index 1dd6eb23..39999f41 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/HWB.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/HWB.php @@ -63,10 +63,10 @@ class HWB } switch ($name) { case 'H': - if ($value == 360) { + if ((int)$value == 360) { $value = 0; } - if ($value < 0 || $value > 360) { + if ((int)$value < 0 || (int)$value > 360) { throw new \LengthException( 'Argument value ' . $value . ' for hue is not in the range of 0 to 360', 1 @@ -74,7 +74,7 @@ class HWB } break; case 'W': - if ($value < 0 || $value > 100) { + if ((int)$value < 0 || (int)$value > 100) { throw new \LengthException( 'Argument value ' . $value . ' for saturation is not in the range of 0 to 100', 2 @@ -82,7 +82,7 @@ class HWB } break; case 'B': - if ($value < 0 || $value > 100) { + if ((int)$value < 0 || (int)$value > 100) { throw new \LengthException( 'Argument value ' . $value . ' for luminance is not in the range of 0 to 100', 3 diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/RGB.php b/www/lib/CoreLibs/Convert/Color/Coordinates/RGB.php index 7fc3eda0..17dbeb6c 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/RGB.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/RGB.php @@ -85,11 +85,10 @@ class RGB throw new \ErrorException('Creation of dynamic property is not allowed', 0); } // if not linear - if (!$this->linear && ($value < 0 || $value > 255)) { + if (!$this->linear && ((int)$value < 0 || (int)$value > 255)) { throw new \LengthException('Argument value ' . $value . ' for color ' . $name . ' is not in the range of 0 to 255', 1); - } elseif ($this->linear && ($value < -10E10 || $value > 1)) { - // not allow very very small negative numbers + } elseif ($this->linear && ((int)$value < 0 || (int)$value > 1)) { throw new \LengthException('Argument value ' . $value . ' for color ' . $name . ' is not in the range of 0 to 1 for linear rgb', 1); } From 3845bc7ff5bc371fcc4c91662dddef76379c8397 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Thu, 14 Nov 2024 14:51:31 +0900 Subject: [PATCH 09/13] Color Coordinates class udpates move creation into the main constructor and do not rely on "::create" or any other pass through creation. Make all constructors equal with options array so we can create an Interface Remove all outsite setters. Once a color is set this color stays --- www/lib/CoreLibs/Convert/Color/CieXyz.php | 32 ++++---- www/lib/CoreLibs/Convert/Color/Color.php | 32 ++++---- .../Convert/Color/Coordinates/HSB.php | 47 ++++++++---- .../Convert/Color/Coordinates/HSL.php | 49 ++++++++---- .../Convert/Color/Coordinates/HWB.php | 47 ++++++++---- .../Interface/CoordinatesInterface.php | 53 +++++++++++++ .../Convert/Color/Coordinates/LCH.php | 45 ++++++++--- .../Convert/Color/Coordinates/Lab.php | 45 ++++++++--- .../Convert/Color/Coordinates/RGB.php | 63 ++++++++------- .../Convert/Color/Coordinates/XYZ.php | 76 ++++++++++++++----- www/lib/CoreLibs/Convert/Colors.php | 12 +-- 11 files changed, 351 insertions(+), 150 deletions(-) create mode 100644 www/lib/CoreLibs/Convert/Color/Coordinates/Interface/CoordinatesInterface.php diff --git a/www/lib/CoreLibs/Convert/Color/CieXyz.php b/www/lib/CoreLibs/Convert/Color/CieXyz.php index 2dbd1253..8651a7dd 100644 --- a/www/lib/CoreLibs/Convert/Color/CieXyz.php +++ b/www/lib/CoreLibs/Convert/Color/CieXyz.php @@ -83,7 +83,7 @@ class CieXyz } /** - * Undocumented function + * Convert from oklab to cie lab * * @param Lab $lab * @return Lab @@ -98,7 +98,7 @@ class CieXyz } /** - * Undocumented function + * Convert from cie lab to oklab * * @param Lab $lab * @return Lab @@ -122,14 +122,14 @@ class CieXyz */ private static function xyzD65ToXyzD50(XYZ $xyz): XYZ { - return XYZ::__constructFromArray(Math::multiplyMatrices( + return new XYZ(Math::multiplyMatrices( a: [ [1.0479298208405488, 0.022946793341019088, -0.05019222954313557], [0.029627815688159344, 0.990434484573249, -0.01707382502938514], [-0.009243058152591178, 0.015055144896577895, 0.7518742899580008], ], b: $xyz->returnAsArray(), - ), whitepoint: 'D50'); + ), options: ["whitepoint" => 'D50']); } /** @@ -140,14 +140,14 @@ class CieXyz */ private static function xyzD50ToXyxD65(XYZ $xyz): XYZ { - return XYZ::__constructFromArray(Math::multiplyMatrices( + return new XYZ(Math::multiplyMatrices( a: [ [0.9554734527042182, -0.023098536874261423, 0.0632593086610217], [-0.028369706963208136, 1.0099954580058226, 0.021041398966943008], [0.012314001688319899, -0.020507696433477912, 1.3303659366080753], ], b: $xyz->returnAsArray() - ), whitepoint: 'D65'); + ), options: ["whitepoint" => 'D65']); } // MARK: xyzD50 <-> Lab @@ -184,7 +184,7 @@ class CieXyz $_xyz, ); - return Lab::__constructFromArray([ + return new Lab([ (116 * $f[1]) - 16, 500 * ($f[0] - $f[1]), 200 * ($f[1] - $f[2]), @@ -227,13 +227,13 @@ class CieXyz (1.0 - 0.3457 - 0.3585) / 0.3585, ]; - return XYZ::__constructFromArray( + return new XYZ( array_map( fn ($k, $v) => $v * $d50[$k], array_keys($xyz), array_values($xyz), ), - whitepoint: 'D50' + options: ["whitepoint" => 'D50'] ); } @@ -252,14 +252,14 @@ class CieXyz if (!$rgb->linear) { $rgb->toLinear(); } - return XYZ::__constructFromArray(Math::multiplyMatrices( + return new XYZ(Math::multiplyMatrices( [ [0.41239079926595934, 0.357584339383878, 0.1804807884018343], [0.21263900587151027, 0.715168678767756, 0.07219231536073371], [0.01933081871559182, 0.11919477979462598, 0.9505321522496607], ], $rgb->returnAsArray() - ), whitepoint: 'D65'); + ), options: ["whitepoint" => 'D65']); } /** @@ -271,14 +271,14 @@ class CieXyz private static function xyzD65ToLinRgb(XYZ $xyz): RGB { // xyz D65 to linrgb - return RGB::__constructFromArray(Math::multiplyMatrices( + return new RGB(Math::multiplyMatrices( a : [ [ 3.2409699419045226, -1.537383177570094, -0.4986107602930034 ], [ -0.9692436362808796, 1.8759675015077202, 0.04155505740717559 ], [ 0.05563007969699366, -0.20397695888897652, 1.0569715142428786 ], ], b : $xyz->returnAsArray() - ), linear: true); + ), options: ["linear" => true]); } // MARK: xyzD65 <-> OkLab @@ -291,7 +291,7 @@ class CieXyz */ private static function xyzD65ToOkLab(XYZ $xyz): Lab { - return Lab::__constructFromArray(Math::multiplyMatrices( + return new Lab(Math::multiplyMatrices( [ [0.2104542553, 0.7936177850, -0.0040720468], [1.9779984951, -2.4285922050, 0.4505937099], @@ -319,7 +319,7 @@ class CieXyz */ private static function okLabToXyzD65(Lab $lab): XYZ { - return XYZ::__constructFromArray(Math::multiplyMatrices( + return new XYZ(Math::multiplyMatrices( a: [ [1.2268798733741557, -0.5578149965554813, 0.28139105017721583], [-0.04057576262431372, 1.1122868293970594, -0.07171106666151701], @@ -337,7 +337,7 @@ class CieXyz b: $lab->returnAsArray(), ), ), - ), whitepoint: 'D65'); + ), options: ["whitepoint" => 'D65']); } } diff --git a/www/lib/CoreLibs/Convert/Color/Color.php b/www/lib/CoreLibs/Convert/Color/Color.php index 49e6bd96..9dd2f063 100644 --- a/www/lib/CoreLibs/Convert/Color/Color.php +++ b/www/lib/CoreLibs/Convert/Color/Color.php @@ -109,7 +109,7 @@ class Color // achromatic if ($chroma == 0) { // H, S, L - return HSL::__constructFromArray([ + return new HSL([ 0.0, 0.0, $lum * 100, @@ -128,7 +128,7 @@ class Color } $hue = $hue * 60; // $sat = 1 - abs(2 * $lum - 1); - return HSL::__constructFromArray([ + return new HSL([ $hue, $sat * 100, $lum * 100, @@ -158,7 +158,7 @@ class Color // if saturation is 0 if ($sat == 0) { $lum = round($lum * 255); - return RGB::__constructFromArray([$lum, $lum, $lum]); + return new RGB([$lum, $lum, $lum]); } else { $m2 = $lum < 0.5 ? $lum * ($sat + 1) : ($lum + $sat) - ($lum * $sat); $m1 = $lum * 2 - $m2; @@ -180,7 +180,7 @@ class Color return $m1; }; - return RGB::__constructFromArray([ + return new RGB([ 255 * $hueue($hue + (1 / 3)), 255 * $hueue($hue), 255 * $hueue($hue - (1 / 3)), @@ -212,7 +212,7 @@ class Color // achromatic if ($MAX == $MIN) { - return HSB::__constructFromArray([0, 0, $MAX * 100]); + return new HSB([0, 0, $MAX * 100]); } if ($red == $MAX) { $HUE = fmod(($green - $blue) / $DELTA, 6); @@ -227,7 +227,7 @@ class Color $HUE += 360; } - return HSB::__constructFromArray([ + return new HSB([ $HUE, // Hue ($DELTA / $MAX) * 100, // Saturation $MAX * 100, // Brightness @@ -255,7 +255,7 @@ class Color if ($S == 0) { $V = $V * 255; - return RGB::__constructFromArray([$V, $V, $V]); + return new RGB([$V, $V, $V]); } $Hi = floor($H / 60); @@ -301,7 +301,7 @@ class Color $blue = 0; } - return RGB::__constructFromArray([ + return new RGB([ $red * 255, $green * 255, $blue * 255, @@ -365,7 +365,7 @@ class Color 0 : 200 * (1 - $lightness / $value); $value *= 100; - return HSB::__constructFromArray([ + return new HSB([ $hsl->H, $saturation, $value, @@ -392,7 +392,7 @@ class Color 100 * ($value - $lightness) / min($lightness, 1 - $lightness) ; - return HSL::__constructFromArray([ + return new HSL([ $hue, $saturation, $lightness * 100, @@ -442,7 +442,7 @@ class Color public static function hsbToHwb(HSB $hsb): HWB { // hsv\Hwb - return HWB::__constructFromArray([ + return new HWB([ $hsb->H, // hue, $hsb->B * (100 - $hsb->S) / 100, // 2: brightness, 1: saturation 100 - $hsb->B, @@ -473,7 +473,7 @@ class Color $value *= 100; } - return HSB::__constructFromArray([ + return new HSB([ $hue, $saturation, $value, @@ -491,7 +491,7 @@ class Color public static function labToLch(Lab $lab): LCH { // cieLab to cieLch - return LCH::__constructFromArray(self::__labToLch($lab), colorspace: 'CIELab'); + return new LCH(self::__labToLch($lab), colorspace: 'CIELab'); } /** @@ -502,7 +502,7 @@ class Color */ public static function lchToLab(LCH $lch): Lab { - return Lab::__constructFromArray(self::__lchToLab($lch), colorspace: 'CIELab'); + return new Lab(self::__lchToLab($lch), colorspace: 'CIELab'); } // MARK: OkLch <-> OkLab @@ -516,7 +516,7 @@ class Color public static function okLabToOkLch(Lab $lab): LCH { // okLab\toOkLch - return LCH::__constructFromArray(self::__labToLch($lab), colorspace: 'OkLab'); + return new LCH(self::__labToLch($lab), colorspace: 'OkLab'); } /** @@ -529,7 +529,7 @@ class Color { // oklch/toOkLab // oklch to oklab - return Lab::__constructFromArray(self::__lchToLab($lch), colorspace: 'OkLab'); + return new Lab(self::__lchToLab($lch), colorspace: 'OkLab'); } // MARK: rgb <-> oklab diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/HSB.php b/www/lib/CoreLibs/Convert/Color/Coordinates/HSB.php index 40a74dab..821fe010 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/HSB.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/HSB.php @@ -11,7 +11,7 @@ declare(strict_types=1); namespace CoreLibs\Convert\Color\Coordinates; -class HSB +class HSB implements Interface\CoordinatesInterface { /** @var array allowed colorspaces */ private const COLORSPACES = ['sRGB']; @@ -29,22 +29,43 @@ class HSB /** * HSB (HSV) color coordinates * Hue/Saturation/Brightness or Value + * + * @param string|array{0:float,1:float,2:float} $colors + * @param string $colorspace [default=sRGB] + * @param array $options [default=[]] + * @throws \InvalidArgumentException only array colors allowed */ - public function __construct() + public function __construct(string|array $colors, string $colorspace = 'sRGB', array $options = []) { + if (!is_array($colors)) { + throw new \InvalidArgumentException('Only array colors allowed', 0); + } + $this->setColorspace($colorspace)->parseOptions($options)->setFromArray($colors); } /** * set from array * where 0: Hue, 1: Saturation, 2: Brightness * - * @param array{0:float,1:float,2:float} $colors + * @param string|array{0:float,1:float,2:float} $colors * @param string $colorspace [default=sRGB] + * @param array $options [default=[]] * @return self */ - public static function __constructFromArray(array $colors, string $colorspace = 'sRGB'): self + public static function create(string|array $colors, string $colorspace = 'sRGB', array $options = []): self { - return (new HSB())->setColorspace($colorspace)->setFromArray($colors); + return new HSB($colors, $colorspace, $options); + } + + /** + * parse options + * + * @param array $options + * @return self + */ + private function parseOptions(array $options): self + { + return $this; } /** @@ -54,7 +75,7 @@ class HSB * @param float $value * @return void */ - public function __set(string $name, float $value): void + private function set(string $name, float $value): void { $name = strtoupper($name); if (!property_exists($this, $name)) { @@ -65,9 +86,9 @@ class HSB if ((int)$value == 360) { $value = 0; } - if ((int)$value < 0 || (int)$value > 359) { + if ((int)$value < 0 || (int)$value > 360) { throw new \LengthException( - 'Argument value ' . $value . ' for hue is not in the range of 0 to 359', + 'Argument value ' . $value . ' for hue is not in the range of 0 to 360', 1 ); } @@ -98,7 +119,7 @@ class HSB * @param string $name * @return float */ - public function __get(string $name): float + public function __get(string $name): float|string|bool { $name = strtoupper($name); if (!property_exists($this, $name)) { @@ -140,11 +161,11 @@ class HSB * @param array{0:float,1:float,2:float} $colors * @return self */ - public function setFromArray(array $colors): self + private function setFromArray(array $colors): self { - $this->__set('H', $colors[0]); - $this->__set('S', $colors[1]); - $this->__set('B', $colors[2]); + $this->set('H', $colors[0]); + $this->set('S', $colors[1]); + $this->set('B', $colors[2]); return $this; } diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/HSL.php b/www/lib/CoreLibs/Convert/Color/Coordinates/HSL.php index 1b5b4ecc..64081e6e 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/HSL.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/HSL.php @@ -13,7 +13,7 @@ namespace CoreLibs\Convert\Color\Coordinates; use CoreLibs\Convert\Color\Stringify; -class HSL +class HSL implements Interface\CoordinatesInterface { /** @var array allowed colorspaces */ private const COLORSPACES = ['sRGB']; @@ -31,22 +31,43 @@ class HSL /** * Color Coordinate HSL * Hue/Saturation/Lightness + * + * @param string|array{0:float,1:float,2:float} $colors + * @param string $colorspace [default=sRGB] + * @param array $options [default=[]] + * @throws \InvalidArgumentException only array colors allowed */ - public function __construct() + public function __construct(string|array $colors, string $colorspace = 'sRGB', array $options = []) { + if (!is_array($colors)) { + throw new \InvalidArgumentException('Only array colors allowed', 0); + } + $this->setColorspace($colorspace)->parseOptions($options)->setFromArray($colors); } /** * set from array * where 0: Hue, 1: Saturation, 2: Lightness * - * @param array{0:float,1:float,2:float} $colors + * @param string|array{0:float,1:float,2:float} $colors * @param string $colorspace [default=sRGB] + * @param array $options [default=[]] * @return self */ - public static function __constructFromArray(array $colors, string $colorspace = 'sRGB'): self + public static function create(string|array $colors, string $colorspace = 'sRGB', array $options = []): self { - return (new HSL())->setColorspace($colorspace)->setFromArray($colors); + return new HSL($colors, $colorspace, $options); + } + + /** + * parse options + * + * @param array $options + * @return self + */ + private function parseOptions(array $options): self + { + return $this; } /** @@ -56,7 +77,7 @@ class HSL * @param float $value * @return void */ - public function __set(string $name, float $value): void + private function set(string $name, float $value): void { if (!property_exists($this, $name)) { throw new \ErrorException('Creation of dynamic property is not allowed', 0); @@ -66,9 +87,9 @@ class HSL if ((int)$value == 360) { $value = 0; } - if ((int)$value < 0 || (int)$value > 359) { + if ((int)$value < 0 || (int)$value > 360) { throw new \LengthException( - 'Argument value ' . $value . ' for hue is not in the range of 0 to 359', + 'Argument value ' . $value . ' for hue is not in the range of 0 to 360', 1 ); } @@ -84,7 +105,7 @@ class HSL case 'L': if ((int)$value < 0 || (int)$value > 100) { throw new \LengthException( - 'Argument value ' . $value . ' for luminance is not in the range of 0 to 100', + 'Argument value ' . $value . ' for lightness is not in the range of 0 to 100', 3 ); } @@ -99,7 +120,7 @@ class HSL * @param string $name * @return float */ - public function __get(string $name): float + public function __get(string $name): float|string|bool { if (!property_exists($this, $name)) { throw new \ErrorException('Creation of dynamic property is not allowed', 0); @@ -140,11 +161,11 @@ class HSL * @param array{0:float,1:float,2:float} $colors * @return self */ - public function setFromArray(array $colors): self + private function setFromArray(array $colors): self { - $this->__set('H', $colors[0]); - $this->__set('S', $colors[1]); - $this->__set('L', $colors[2]); + $this->set('H', $colors[0]); + $this->set('S', $colors[1]); + $this->set('L', $colors[2]); return $this; } diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/HWB.php b/www/lib/CoreLibs/Convert/Color/Coordinates/HWB.php index 39999f41..0f1ce58f 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/HWB.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/HWB.php @@ -13,7 +13,7 @@ namespace CoreLibs\Convert\Color\Coordinates; use CoreLibs\Convert\Color\Stringify; -class HWB +class HWB implements Interface\CoordinatesInterface { /** @var array allowed colorspaces */ private const COLORSPACES = ['sRGB']; @@ -31,22 +31,43 @@ class HWB /** * Color Coordinate: HWB * Hue/Whiteness/Blackness + * + * @param string|array{0:float,1:float,2:float} $colors + * @param string $colorspace [default=sRGB] + * @param array $options [default=[]] + * @throws \InvalidArgumentException only array colors allowed */ - public function __construct() + public function __construct(string|array $colors, string $colorspace = 'sRGB', array $options = []) { + if (!is_array($colors)) { + throw new \InvalidArgumentException('Only array colors allowed', 0); + } + $this->setColorspace($colorspace)->parseOptions($options)->setFromArray($colors); } /** * set from array * where 0: Hue, 1: Whiteness, 2: Blackness * - * @param array{0:float,1:float,2:float} $colors + * @param string|array{0:float,1:float,2:float} $colors * @param string $colorspace [default=sRGB] + * @param array $options [default=[]] * @return self */ - public static function __constructFromArray(array $colors, string $colorspace = 'sRGB'): self + public static function create(string|array $colors, string $colorspace = 'sRGB', array $options = []): self { - return (new HWB())->setColorspace($colorspace)->setFromArray($colors); + return new HWB($colors, $colorspace, $options); + } + + /** + * parse options + * + * @param array $options + * @return self + */ + private function parseOptions(array $options): self + { + return $this; } /** @@ -56,7 +77,7 @@ class HWB * @param float $value * @return void */ - public function __set(string $name, float $value): void + private function set(string $name, float $value): void { if (!property_exists($this, $name)) { throw new \ErrorException('Creation of dynamic property is not allowed', 0); @@ -76,7 +97,7 @@ class HWB case 'W': if ((int)$value < 0 || (int)$value > 100) { throw new \LengthException( - 'Argument value ' . $value . ' for saturation is not in the range of 0 to 100', + 'Argument value ' . $value . ' for whiteness is not in the range of 0 to 100', 2 ); } @@ -84,7 +105,7 @@ class HWB case 'B': if ((int)$value < 0 || (int)$value > 100) { throw new \LengthException( - 'Argument value ' . $value . ' for luminance is not in the range of 0 to 100', + 'Argument value ' . $value . ' for blackness is not in the range of 0 to 100', 3 ); } @@ -99,7 +120,7 @@ class HWB * @param string $name * @return float */ - public function __get(string $name): float + public function __get(string $name): float|string|bool { if (!property_exists($this, $name)) { throw new \ErrorException('Creation of dynamic property is not allowed', 0); @@ -140,11 +161,11 @@ class HWB * @param array{0:float,1:float,2:float} $colors * @return self */ - public function setFromArray(array $colors): self + private function setFromArray(array $colors): self { - $this->__set('H', $colors[0]); - $this->__set('W', $colors[1]); - $this->__set('B', $colors[2]); + $this->set('H', $colors[0]); + $this->set('W', $colors[1]); + $this->set('B', $colors[2]); return $this; } diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/Interface/CoordinatesInterface.php b/www/lib/CoreLibs/Convert/Color/Coordinates/Interface/CoordinatesInterface.php new file mode 100644 index 00000000..72c056de --- /dev/null +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/Interface/CoordinatesInterface.php @@ -0,0 +1,53 @@ + $options [default=[]] + * @return self + */ + public static function create(string|array $colors, string $colorspace = '', array $options = []): self; + + /** + * get color + * + * @param string $name + * @return float + */ + public function __get(string $name): float|string|bool; + + /** + * Returns the color as array + * where 0: Lightness, 1: a, 2: b + * + * @return array{0:float,1:float,2:float} + */ + public function returnAsArray(): array; + + /** + * Convert into css string with optional opacity + * + * @param null|float|string|null $opacity + * @return string + */ + public function toCssString(null|float|string $opacity = null): string; +} + +// __END__ diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/LCH.php b/www/lib/CoreLibs/Convert/Color/Coordinates/LCH.php index 872c597b..76b3ad69 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/LCH.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/LCH.php @@ -14,7 +14,7 @@ namespace CoreLibs\Convert\Color\Coordinates; use CoreLibs\Convert\Color\Stringify; -class LCH +class LCH implements Interface\CoordinatesInterface { /** @var array allowed colorspaces */ private const COLORSPACES = ['OkLab', 'CIELab']; @@ -42,22 +42,43 @@ class LCH /** * Color Coordinate Lch * for oklch + * + * @param string|array{0:float,1:float,2:float} $colors + * @param string $colorspace [default=''] + * @param array $options [default=[]] + * @throws \InvalidArgumentException only array colors allowed */ - public function __construct() + public function __construct(string|array $colors, string $colorspace = '', array $options = []) { + if (!is_array($colors)) { + throw new \InvalidArgumentException('Only array colors allowed', 0); + } + $this->setColorspace($colorspace)->parseOptions($options)->setFromArray($colors); } /** * set from array * where 0: Lightness, 1: Chroma, 2: Hue * - * @param array{0:float,1:float,2:float} $colors - * @param string $colorspace + * @param string|{0:float,1:float,2:float} $colors + * @param string $colorspace [default=''] + * @param array $options [default=[]] * @return self */ - public static function __constructFromArray(array $colors, string $colorspace): self + public static function create(string|array $colors, string $colorspace = '', array $options = []): self { - return (new LCH())->setColorspace($colorspace)->setFromArray($colors); + return new LCH($colors, $colorspace, $options); + } + + /** + * parse options + * + * @param array $options + * @return self + */ + private function parseOptions(array $options): self + { + return $this; } /** @@ -67,7 +88,7 @@ class LCH * @param float $value * @return void */ - public function __set(string $name, float $value): void + private function set(string $name, float $value): void { if (!property_exists($this, $name)) { throw new \ErrorException('Creation of dynamic property is not allowed', 0); @@ -124,7 +145,7 @@ class LCH * @param string $name * @return float */ - public function __get(string $name): float + public function __get(string $name): float|string|bool { if (!property_exists($this, $name)) { throw new \ErrorException('Creation of dynamic property is not allowed', 0); @@ -165,11 +186,11 @@ class LCH * @param array{0:float,1:float,2:float} $colors * @return self */ - public function setFromArray(array $colors): self + private function setFromArray(array $colors): self { - $this->__set('L', $colors[0]); - $this->__set('C', $colors[1]); - $this->__set('H', $colors[2]); + $this->set('L', $colors[0]); + $this->set('C', $colors[1]); + $this->set('H', $colors[2]); return $this; } diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/Lab.php b/www/lib/CoreLibs/Convert/Color/Coordinates/Lab.php index 8e9f573a..1b475d5c 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/Lab.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/Lab.php @@ -14,7 +14,7 @@ namespace CoreLibs\Convert\Color\Coordinates; use CoreLibs\Convert\Color\Stringify; -class Lab +class Lab implements Interface\CoordinatesInterface { /** @var array allowed colorspaces */ private const COLORSPACES = ['OkLab', 'CIELab']; @@ -44,22 +44,43 @@ class Lab /** * Color Coordinate: Lab * for oklab or cie + * + * @param string|array{0:float,1:float,2:float} $rgb + * @param string $colorspace [default=''] + * @param array $options [default=[]] + * @throws \InvalidArgumentException only array colors allowed */ - public function __construct() + public function __construct(string|array $colors, string $colorspace = '', array $options = []) { + if (!is_array($colors)) { + throw new \InvalidArgumentException('Only array colors allowed', 0); + } + $this->setColorspace($colorspace)->parseOptions($options)->setFromArray($colors); } /** * set from array * where 0: Lightness, 1: a, 2: b * - * @param array{0:float,1:float,2:float} $rgb - * @param string $colorspace + * @param array{0:float,1:float,2:float} $rgb + * @param string $colorspace [default=''] + * @param array $options [default=[]] * @return self */ - public static function __constructFromArray(array $colors, string $colorspace): self + public static function create(string|array $colors, string $colorspace = '', array $options = []): self { - return (new Lab())->setColorspace($colorspace)->setFromArray($colors); + return new Lab($colors, $colorspace, $options); + } + + /** + * parse options + * + * @param array $options + * @return self + */ + private function parseOptions(array $options): self + { + return $this; } /** @@ -69,7 +90,7 @@ class Lab * @param float $value * @return void */ - public function __set(string $name, float $value): void + private function set(string $name, float $value): void { if (!property_exists($this, $name)) { throw new \ErrorException('Creation of dynamic property is not allowed', 0); @@ -112,7 +133,7 @@ class Lab * @param string $name * @return float */ - public function __get(string $name): float + public function __get(string $name): float|string|bool { if (!property_exists($this, $name)) { throw new \ErrorException('Creation of dynamic property is not allowed', 0); @@ -153,11 +174,11 @@ class Lab * @param array{0:float,1:float,2:float} $colors * @return self */ - public function setFromArray(array $colors): self + private function setFromArray(array $colors): self { - $this->__set('L', $colors[0]); - $this->__set('a', $colors[1]); - $this->__set('b', $colors[2]); + $this->set('L', $colors[0]); + $this->set('a', $colors[1]); + $this->set('b', $colors[2]); return $this; } diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/RGB.php b/www/lib/CoreLibs/Convert/Color/Coordinates/RGB.php index 17dbeb6c..474c37f1 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/RGB.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/RGB.php @@ -13,7 +13,7 @@ namespace CoreLibs\Convert\Color\Coordinates; use CoreLibs\Convert\Color\Stringify; -class RGB +class RGB implements Interface\CoordinatesInterface { /** @var array allowed colorspaces */ private const COLORSPACES = ['sRGB']; @@ -33,41 +33,48 @@ class RGB /** * Color Coordinate RGB + * @param array{0:float,1:float,2:float}|string $colors RGB color array or hex string + * @param string $colorspace [default=sRGB] + * @param array $options [default=[]] only "linear" allowed at the moment */ - public function __construct() + public function __construct(string|array $colors, string $colorspace = 'sRGB', array $options = []) { + $this->setColorspace($colorspace)->parseOptions($options); + if (is_array($colors)) { + $this->setFromArray($colors); + } else { + $this->setFromHex($colors); + } } /** - * set from array + * set from array or string * where 0: Red, 1: Green, 2: Blue + * OR #ffffff or ffffff * - * @param array{0:float,1:float,2:float} $colors + * @param array{0:float,1:float,2:float}|string $colors RGB color array or hex string * @param string $colorspace [default=sRGB] - * @param bool $linear [default=false] + * @param array $options [default=[]] only "linear" allowed at the moment * @return self */ - public static function __constructFromArray(array $colors, string $colorspace = 'sRGB', bool $linear = false): self + public static function create(string|array $colors, string $colorspace = 'sRGB', array $options = []): self { - return (new RGB())->setColorspace($colorspace)->flagLinear($linear)->setFromArray($colors); + return new RGB($colors, $colorspace, $options); } /** - * Undocumented function + * parse options * - * @param string $hex_string - * @param string $colorspace - * @param bool $linear + * @param array $options * @return self */ - public static function __constructFromHexString( - string $hex_string, - string $colorspace = 'sRGB', - bool $linear = false - ): self { - return (new RGB())->setColorspace($colorspace)->flagLinear($linear)->setFromHex($hex_string); + private function parseOptions(array $options): self + { + $this->flagLinear($options['linear'] ?? false); + return $this; } + /** * set color * @@ -75,7 +82,7 @@ class RGB * @param float $value * @return void */ - public function __set(string $name, float $value): void + private function set(string $name, float $value): void { // do not allow setting linear from outside if ($name == 'linear') { @@ -90,7 +97,7 @@ class RGB . ' is not in the range of 0 to 255', 1); } elseif ($this->linear && ((int)$value < 0 || (int)$value > 1)) { throw new \LengthException('Argument value ' . $value . ' for color ' . $name - . ' is not in the range of 0 to 1 for linear rgb', 1); + . ' is not in the range of 0 to 1 for linear rgb', 2); } $this->$name = $value; } @@ -101,7 +108,7 @@ class RGB * @param string $name * @return float|bool */ - public function __get(string $name): float|bool + public function __get(string $name): float|string|bool { if (!property_exists($this, $name)) { throw new \ErrorException('Creation of dynamic property is not allowed', 0); @@ -142,11 +149,11 @@ class RGB * @param array{0:float,1:float,2:float} $colors * @return self */ - public function setFromArray(array $colors): self + private function setFromArray(array $colors): self { - $this->__set('R', $colors[0]); - $this->__set('G', $colors[1]); - $this->__set('B', $colors[2]); + $this->set('R', $colors[0]); + $this->set('G', $colors[1]); + $this->set('B', $colors[2]); return $this; } @@ -179,11 +186,11 @@ class RGB * @param string $hex_string * @return self */ - public function setFromHex(string $hex_string): self + private function setFromHex(string $hex_string): self { $hex_string = preg_replace("/[^0-9A-Fa-f]/", '', $hex_string); // Gets a proper hex string - if (!is_string($hex_string)) { - throw new \InvalidArgumentException('hex_string argument cannot be empty', 1); + if (empty($hex_string) || !is_string($hex_string)) { + throw new \InvalidArgumentException('hex_string argument cannot be empty', 3); } $rgbArray = []; if (strlen($hex_string) == 6) { @@ -204,7 +211,7 @@ class RGB ]; } else { // Invalid hex color code - throw new \UnexpectedValueException('Invalid hex_string: ' . $hex_string, 2); + throw new \UnexpectedValueException('Invalid hex_string: ' . $hex_string, 4); } return $this->setFromArray($rgbArray); } diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/XYZ.php b/www/lib/CoreLibs/Convert/Color/Coordinates/XYZ.php index 7c773ed9..01fb926d 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/XYZ.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/XYZ.php @@ -15,7 +15,7 @@ declare(strict_types=1); namespace CoreLibs\Convert\Color\Coordinates; -class XYZ +class XYZ implements Interface\CoordinatesInterface { /** @var array allowed colorspaces */ private const COLORSPACES = ['CIEXYZ']; @@ -35,34 +35,58 @@ class XYZ /** @var string color space: either ok or cie */ private string $colorspace = ''; + /** @var string illuminat white point: only D50 and D65 are allowed */ private string $whitepoint = ''; /** * Color Coordinate Lch - * for oklch + * for oklch conversion + * + * @param string|array{0:float,1:float,2:float} $colors + * @param string $colorspace [default=CIEXYZ] + * @param array $options [default=[]] Only "whitepoint" option allowed + * @throws \InvalidArgumentException only array colors allowed */ - public function __construct() - { + public function __construct( + string|array $colors, + string $colorspace = 'CIEXYZ', + array $options = [], + ) { + if (!is_array($colors)) { + throw new \InvalidArgumentException('Only array colors allowed', 0); + } + $this->setColorspace($colorspace) + ->parseOptions($options) + ->setFromArray($colors); } /** * set from array * where 0: X, 1: Y, 2: Z * - * @param array{0:float,1:float,2:float} $colors - * @param string $colorspace [default=CIEXYZ] - * @param string $whitepoint [default=''] only D65 or D50 allowed + * @param array{0:float,1:float,2:float} $colors + * @param string $colorspace [default=CIEXYZ] + * @param array $options [default=[]] Only "whitepoint" option allowed * @return self */ - public static function __constructFromArray( - array $colors, + public static function create( + string|array $colors, string $colorspace = 'CIEXYZ', - string $whitepoint = '' + array $options = [], ): self { - return (new XYZ()) - ->setColorspace($colorspace) - ->setWhitepoint($whitepoint) - ->setFromArray($colors); + return new XYZ($colors, $colorspace, $options); + } + + /** + * parse options + * + * @param array $options + * @return self + */ + private function parseOptions(array $options): self + { + $this->setWhitepoint($options['whitepoint'] ?? ''); + return $this; } /** @@ -72,7 +96,7 @@ class XYZ * @param float $value * @return void */ - public function __set(string $name, float $value): void + private function set(string $name, float $value): void { if (!property_exists($this, $name)) { throw new \ErrorException('Creation of dynamic property is not allowed', 0); @@ -90,7 +114,7 @@ class XYZ * @param string $name * @return float */ - public function __get(string $name): float + public function __get(string $name): float|string|bool { if (!property_exists($this, $name)) { throw new \ErrorException('Creation of dynamic property is not allowed', 0); @@ -150,13 +174,25 @@ class XYZ * @param array{0:float,1:float,2:float} $colors * @return self */ - public function setFromArray(array $colors): self + private function setFromArray(array $colors): self { - $this->__set('X', $colors[0]); - $this->__set('Y', $colors[1]); - $this->__set('Z', $colors[2]); + $this->set('X', $colors[0]); + $this->set('Y', $colors[1]); + $this->set('Z', $colors[2]); return $this; } + + /** + * no hsb in css + * + * @param float|string|null $opacity + * @return string + * @throws \ErrorException + */ + public function toCssString(null|float|string $opacity = null): string + { + throw new \ErrorException('XYZ is not available as CSS color string', 0); + } } // __END__ diff --git a/www/lib/CoreLibs/Convert/Colors.php b/www/lib/CoreLibs/Convert/Colors.php index 96aaee12..f4b6f2eb 100644 --- a/www/lib/CoreLibs/Convert/Colors.php +++ b/www/lib/CoreLibs/Convert/Colors.php @@ -40,7 +40,7 @@ class Colors int $blue, bool $hex_prefix = true ): string { - return Coordinates\RGB::__constructFromArray([$red, $green, $blue])->returnAsHex($hex_prefix); + return (new Coordinates\RGB([$red, $green, $blue]))->returnAsHex($hex_prefix); } /** @@ -61,7 +61,7 @@ class Colors ): string|array { $rgbArray = []; // rewrite to previous r/g/b key output - foreach (Coordinates\RGB::__constructFromHexString($hex_string)->returnAsArray() as $p => $el) { + foreach ((new Coordinates\RGB($hex_string))->returnAsArray() as $p => $el) { switch ($p) { case 0: $k = 'r'; @@ -96,7 +96,7 @@ class Colors return array_map( fn ($v) => (int)round($v), Color::rgbToHsb( - Coordinates\RGB::__constructFromArray([$red, $green, $blue]) + new Coordinates\RGB([$red, $green, $blue]) )->returnAsArray() ); } @@ -117,7 +117,7 @@ class Colors return array_map( fn ($v) => (int)round($v), Color::hsbToRgb( - Coordinates\HSB::__constructFromArray([$H, $S, $V]) + new Coordinates\HSB([$H, $S, $V]) )->returnAsArray() ); } @@ -138,7 +138,7 @@ class Colors return array_map( fn ($v) => round($v, 1), Color::rgbToHsl( - Coordinates\RGB::__constructFromArray([$red, $green, $blue]) + new Coordinates\RGB([$red, $green, $blue]) )->returnAsArray() ); } @@ -158,7 +158,7 @@ class Colors return array_map( fn ($v) => round($v), Color::hslToRgb( - Coordinates\HSL::__constructFromArray([$hue, $sat, $lum]) + new Coordinates\HSL([$hue, $sat, $lum]) )->returnAsArray() ); } From a9f1d878f7e3826bcc195f6757c21c41d9499c35 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Fri, 15 Nov 2024 18:13:16 +0900 Subject: [PATCH 10/13] Math: add epsilon compare for float, update Color Coordinate calls Math has a compare with epsilon for float numbers. Use this for fixing sligth color conversion issues. NOTE: this might need some adjustment over time All phpunint tests written and checked --- .../Convert/CoreLibsConvertColorTest.php | 883 +++++++++++++++++- .../tests/Convert/CoreLibsConvertMathTest.php | 125 ++- www/admin/class_test.convert.colors.php | 16 +- www/lib/CoreLibs/Convert/Color/CieXyz.php | 2 +- www/lib/CoreLibs/Convert/Color/Color.php | 10 - .../Convert/Color/Coordinates/HSB.php | 13 +- .../Convert/Color/Coordinates/HSL.php | 15 +- .../Convert/Color/Coordinates/HWB.php | 15 +- .../Convert/Color/Coordinates/LCH.php | 76 +- .../Convert/Color/Coordinates/Lab.php | 80 +- .../Convert/Color/Coordinates/RGB.php | 20 +- .../Convert/Color/Coordinates/XYZ.php | 8 +- www/lib/CoreLibs/Convert/Color/Stringify.php | 19 - www/lib/CoreLibs/Convert/Color/Utils.php | 56 ++ www/lib/CoreLibs/Convert/Math.php | 60 ++ 15 files changed, 1246 insertions(+), 152 deletions(-) create mode 100644 www/lib/CoreLibs/Convert/Color/Utils.php diff --git a/4dev/tests/Convert/CoreLibsConvertColorTest.php b/4dev/tests/Convert/CoreLibsConvertColorTest.php index 3b5d950f..543ff537 100644 --- a/4dev/tests/Convert/CoreLibsConvertColorTest.php +++ b/4dev/tests/Convert/CoreLibsConvertColorTest.php @@ -16,6 +16,8 @@ final class CoreLibsConvertColorTest extends TestCase { // 12 precision allowed, RGB and back has a lot of float imprecisions here private const DELTA = 0.000000000001; + // rgb to oklab and back will have slight shift + private const DELTA_OKLAB = 1.05; // sRGB base convert test, should round around and come out the same // use for RGB 0, 0, 0 in 60 steps and then max 255 @@ -32,10 +34,12 @@ final class CoreLibsConvertColorTest extends TestCase ); } + // MARK: single test + public function testSingle() { $this->assertTrue(true, 'Single test'); - // $rgb = Color\Coordinates\RGB::__constructFromArray([0, 0, 60]); + // $rgb = new Color\Coordinates\RGB([0, 0, 60]); // print "IN: " . print_r($rgb, true) . "\n"; // $hsl = Color\Color::rgbToHsl($rgb); // print "to HSL: " . print_r($hsl, true) . "\n"; @@ -51,7 +55,7 @@ final class CoreLibsConvertColorTest extends TestCase // $rgb_r = Color\Color::hslToRgb($hsl_r); // print "R to RGB: " . print_r($rgb_r, true) . "\n"; - // $hsl = Color\Coordinates\HSL::__constructFromArray([0, 0, 0]); + // $hsl = new Color\Coordinates\HSL([0, 0, 0]); // print "IN HSL: " . print_r($hsl, true) . "\n"; // $hsb = Color\Color::hslToHsb($hsl); // print "to HSB: " . print_r($hsb, true) . "\n"; @@ -63,7 +67,7 @@ final class CoreLibsConvertColorTest extends TestCase // $hsl_r = Color\Color::hsbToHsl($hsb_r); // print "R to HSL: " . print_r($hsl_r, true) . "\n"; // print "--------\n"; - // $hsb = Color\Coordinates\HSB::__constructFromArray([0, 20, 0]); + // $hsb = new Color\Coordinates\HSB([0, 20, 0]); // print "IN HSB: " . print_r($hsb, true) . "\n"; // $hsl = Color\Color::hsbToHsl($hsb); // print "to HSL: " . print_r($hsl, true) . "\n"; @@ -75,7 +79,7 @@ final class CoreLibsConvertColorTest extends TestCase // $hsb_r = Color\Color::hslToHsb($hsl_r); // print "R to HSL: " . print_r($hsb_r, true) . "\n"; // print "--------\n"; - // $hwb = Color\Coordinates\HWB::__constructFromArray([0, 20, 100]); + // $hwb = new Color\Coordinates\HWB([0, 20, 100]); // print "IN: " . print_r($hwb, true) . "\n"; // $hsl = Color\Color::hwbToHsl($hwb); // print "to HSL: " . print_r($hsl, true) . "\n"; @@ -87,6 +91,8 @@ final class CoreLibsConvertColorTest extends TestCase // print "HSL to HWB: " . print_r($hwb_r, true) . "\n"; } + // MARK: RGB base + /** * From/To RGB <-> ... conversion tests * @@ -116,7 +122,7 @@ final class CoreLibsConvertColorTest extends TestCase $b = 255; } // base is always the same - $color = Color\Coordinates\RGB::__constructFromArray([$r, $g, $b]); + $color = new Color\Coordinates\RGB([$r, $g, $b]); $base = 'rgb'; foreach (['hsb', 'hsl', 'hwb'] as $coord) { // print "COORD: " . $coord . ", RGB: " . print_r($color->returnAsArray(), true) . "\n"; @@ -145,6 +151,8 @@ final class CoreLibsConvertColorTest extends TestCase // HSB: saturation or brightness 0 // HWB: blackness >= 80 and whitness >= 20 or B>=20 & W>=20 or B>=50 & W>=50 + // MARK: HSL base + /** * Undocumented function * @@ -165,7 +173,7 @@ final class CoreLibsConvertColorTest extends TestCase if (($L == 0 or $L == 100)) { continue; } - $color = Color\Coordinates\HSL::__constructFromArray([$H, $S, $L]); + $color = new Color\Coordinates\HSL([$H, $S, $L]); $base = 'hsl'; foreach (['hsb', 'hwb', 'rgb'] as $coord) { // for rgb hue on S = 0 is irrelevant (B/W) @@ -190,6 +198,8 @@ final class CoreLibsConvertColorTest extends TestCase } } + // MARK: HSB + /** * Undocumented function * @@ -210,7 +220,7 @@ final class CoreLibsConvertColorTest extends TestCase if ($S == 0 or $B == 0) { continue; } - $color = Color\Coordinates\HSB::__constructFromArray([$H, $S, $B]); + $color = new Color\Coordinates\HSB([$H, $S, $B]); $base = 'hsb'; foreach (['hwb', 'hsl', 'rgb'] as $coord) { $target = $base . 'To' . ucfirst($coord); @@ -231,6 +241,7 @@ final class CoreLibsConvertColorTest extends TestCase } } + // MARK: HWB /** * Undocumented function @@ -258,7 +269,7 @@ final class CoreLibsConvertColorTest extends TestCase continue; } $base = 'hwb'; - $color = Color\Coordinates\HWB::__constructFromArray([$H, $W, $B]); + $color = new Color\Coordinates\HWB([$H, $W, $B]); foreach (['hsl', 'hsb', 'rgb'] as $coord) { // for rgb hue on S = 0 is irrelevant (B/W) if ($H > 0 && $coord == 'rgb') { @@ -282,12 +293,13 @@ final class CoreLibsConvertColorTest extends TestCase } } + // MARK: RGB to hex + /** * Undocumented function * - * covers ::returnAsHex() - * covers ::__constructFromHexString() - * @testdox Convert from RGB to hex and back + * @covers ::returnAsHex() + * @testdox Convert from and to RGB via hex * * @return void */ @@ -308,10 +320,10 @@ final class CoreLibsConvertColorTest extends TestCase } // with or without prefix foreach ([true, false] as $hex_prefix) { - $hex_color = Color\Coordinates\RGB::__constructFromArray([$r, $g, $b]) + $hex_color = (new Color\Coordinates\RGB([$r, $g, $b])) ->returnAsHex($hex_prefix); // parse into hex to rgb and see if we get the same r/g/b - $color = Color\Coordinates\RGB::__constructFromHexString($hex_color)->returnAsArray(); + $color = (new Color\Coordinates\RGB($hex_color))->returnAsArray(); // $this->assertEquals( [$r, $g, $b], @@ -325,20 +337,849 @@ final class CoreLibsConvertColorTest extends TestCase } } - // oklab + // MARK: RGB Linear - // cie lab - - // create exceptions for all color spaces - - public function testExceptionHSB(): void + /** + * linear RGB conversion tests + * + * @covers ::fromLinear + * @covers ::toLinear + * @testdox Convert from and to RGB linear conversion check + * + * @return void + */ + public function testRgbFromToLinear() { + $rgb = (new Color\Coordinates\RGB([10, 20, 30]))->toLinear(); + $this->assertEquals( + true, + $rgb->linear, + 'On create flagged linear missing' + ); + $rgb_color = $rgb->returnAsArray(); + $rgb->toLinear(); + $this->assertEquals( + $rgb_color, + $rgb->returnAsArray(), + 'Double linear call does double linear encoding' + ); + $rgb->fromLinear(); + $this->assertEquals( + false, + $rgb->linear, + 'On reverse linear, flag is missing' + ); + $rgb_color = $rgb->returnAsArray(); + $this->assertEquals( + $rgb_color, + $rgb->returnAsArray(), + 'Double linear inverse call does double linear decoding' + ); + $rgb = new Color\Coordinates\RGB([20, 30, 40]); + $rgb_color = $rgb->returnAsArray(); + $this->assertEquals( + false, + $rgb->linear, + 'On create without linear flag is linear' + ); + $rgb->toLinear(); + $this->assertEquals( + true, + $rgb->linear, + 'On linear call flag is not linear' + ); + $rgb->fromLinear(); + $this->assertEquals( + $rgb_color, + $rgb->returnAsArray(), + 'conversion to and from linear not matching' + ); + } + + // MARK: okLab + + /** + * From/To RGB <-> OkLab / OkLch + * + * @covers ::rgbToOkLab + * @covers ::rgbToOkLch + * @covers ::okLabToRgb + * @covers ::okLchToRgb + * @testdox Convert from and to RGB to OkLab / OkLch + * + * @return void + */ + public function testRgbColorCoordinateConvertToAndBackBackOkLab() + { + for ($r = 0; $r <= 300; $r += 60) { + for ($g = 0; $g <= 300; $g += 60) { + for ($b = 0; $b <= 300; $b += 60) { + // for this test we stay in the correct lane + if ($r > 255) { + $r = 255; + } + if ($g > 255) { + $g = 255; + } + if ($b > 255) { + $b = 255; + } + // base is always the same + $color = new Color\Coordinates\RGB([$r, $g, $b]); + $base = 'rgb'; + foreach (['okLab', 'okLch'] as $coord) { + // print "COORD: " . $coord . ", RGB: " . print_r($color->returnAsArray(), true) . "\n"; + // rgb to X and back must be same + $target = $base . 'To' . ucfirst($coord); + $source = $coord . 'To' . ucfirst($base); + $converted_color = Color\Color::$target($color); + $color_b = Color\Color::$source($converted_color); + // $converted_color = Color\Color::rgbToHsb($color); + // $rgb_b = Color\Color::hsbToRgb($converted_color); + $this->assertEqualsWithDelta( + $color->returnAsArray(), + $color_b->returnAsArray(), + self::DELTA_OKLAB, + 'Convert ' . $base . ' to ' . $coord . ': ' . print_r($color->returnAsArray(), true) . '/' + . print_r($color_b->returnAsArray(), true) + ); + } + } + } + } + } + + /** + * internal oklab/oklch conversion + * + * @covers ::okLchToOkLab + * @covers ::okLabToOkLch + * @testdox Convert from and to OkLab / OkLch + * + * @return void + */ + public function testOkLabOkLchColorCoordinateConvertToFrom() + { + for ($L = 0.0; $L <= 1.0; $L += 0.2) { + for ($C = 0.0; $C <= 0.5; $C += 0.1) { + for ($H = 0.0; $H <= 360.0; $H += 60.0) { + // chroma 0.0 is B/W skip it + if ($C == 0.0) { + continue; + } + $color = new Color\Coordinates\LCH([$L, $C, $H], 'OkLab'); + $base = 'okLch'; + foreach (['okLab'] as $coord) { + // rgb to X and back must be same + $target = $base . 'To' . ucfirst($coord); + $source = $coord . 'To' . ucfirst($base); + $converted_color = Color\Color::$target($color); + $color_b = Color\Color::$source($converted_color); + // $converted_color = Color\Color::rgbToHsb($color); + // $rgb_b = Color\Color::hsbToRgb($converted_color); + $this->assertEqualsWithDelta( + $color->returnAsArray(), + $color_b->returnAsArray(), + self::DELTA, + 'Convert ' . $base . ' to ' . $coord . ': ' . print_r($color->returnAsArray(), true) . '/' + . print_r($color_b->returnAsArray(), true) + ); + } + } + } + } + } + + // MARK: CIELab + + /** + * From/To RGB <-> Cie lab / Cie lch + * + * @covers ::rgbToLab + * @covers ::rgbToLch + * @covers ::labToRgb + * @covers ::lchToRgb + * @testdox Convert from and to RGB to Cie Lab / Cie Lch + * + * @return void + */ + public function testRgbColorCoordinateConvertToAndBackBackCieLab() + { + for ($r = 0; $r <= 300; $r += 60) { + for ($g = 0; $g <= 300; $g += 60) { + for ($b = 0; $b <= 300; $b += 60) { + // for this test we stay in the correct lane + if ($r > 255) { + $r = 255; + } + if ($g > 255) { + $g = 255; + } + if ($b > 255) { + $b = 255; + } + // base is always the same + $color = new Color\Coordinates\RGB([$r, $g, $b]); + $base = 'rgb'; + foreach (['lab', 'lch'] as $coord) { + // print "COORD: " . $coord . ", RGB: " . print_r($color->returnAsArray(), true) . "\n"; + // rgb to X and back must be same + $target = $base . 'To' . ucfirst($coord); + $source = $coord . 'To' . ucfirst($base); + $converted_color = Color\Color::$target($color); + $color_b = Color\Color::$source($converted_color); + // $converted_color = Color\Color::rgbToHsb($color); + // $rgb_b = Color\Color::hsbToRgb($converted_color); + $this->assertEqualsWithDelta( + $color->returnAsArray(), + $color_b->returnAsArray(), + self::DELTA_OKLAB, + 'Convert ' . $base . ' to ' . $coord . ': ' . print_r($color->returnAsArray(), true) . '/' + . print_r($color_b->returnAsArray(), true) + ); + } + } + } + } + } + + /** + * internal cie lab/cie lch conversion + * + * @covers ::lchToLab + * @covers ::labToLch + * @testdox Convert from and to Cie Lab / Cie Lch + * + * @return void + */ + public function testLabLchColorCoordinateConvertToFrom() + { + for ($L = 0.0; $L <= 1.0; $L += 0.2) { + for ($C = 0.0; $C <= 0.5; $C += 0.1) { + for ($H = 0.0; $H <= 360.0; $H += 60.0) { + // chroma 0.0 is B/W skip it + if ($C == 0.0) { + continue; + } + $color = new Color\Coordinates\LCH([$L, $C, $H], 'OkLab'); + $base = 'lch'; + foreach (['lab'] as $coord) { + // rgb to X and back must be same + $target = $base . 'To' . ucfirst($coord); + $source = $coord . 'To' . ucfirst($base); + $converted_color = Color\Color::$target($color); + $color_b = Color\Color::$source($converted_color); + // $converted_color = Color\Color::rgbToHsb($color); + // $rgb_b = Color\Color::hsbToRgb($converted_color); + $this->assertEqualsWithDelta( + $color->returnAsArray(), + $color_b->returnAsArray(), + self::DELTA, + 'Convert ' . $base . ' to ' . $coord . ': ' . print_r($color->returnAsArray(), true) . '/' + . print_r($color_b->returnAsArray(), true) + ); + } + } + } + } + } + + // MARK: Exceptions + + /** + * Undocumented function + * + * @return array + */ + public function providerHueBased(): array + { + // all HSB/V HSL HWB have the same value range, create test data for all of them + return [ + 'H' => [ + 'color' => [900, 10, 10], + 'error_code' => 1, + 'error_string' => '/ for hue is not in the range of 0 to 360$/' + ], + 'H' => [ + 'color' => [-1, 10, 10], 'error_code' => 1, + 'error_string' => '/ for hue is not in the range of 0 to 360$/', + ], + 'H close' => [ + 'color' => [360.1, 10, 10], + 'error_code' => 1, + 'error_string' => '/ for hue is not in the range of 0 to 360$/' + ], + 'H close' => [ + 'color' => [-0.1, 10, 10], 'error_code' => 1, + 'error_string' => '/ for hue is not in the range of 0 to 360$/', + ], + 'S/W' => [ + 'color' => [90, 900, 10], 'error_code' => 2, + 'error_string' => 'is not in the range of 0 to 100', + ], + 'S/W' => [ + 'color' => [90, -1, 10], 'error_code' => 2, + 'error_string' => 'is not in the range of 0 to 100', + ], + 'S/W close' => [ + 'color' => [90, 100.1, 10], 'error_code' => 2, + 'error_string' => 'is not in the range of 0 to 100', + ], + 'S/W close' => [ + 'color' => [90, -0.1, 10], 'error_code' => 2, + 'error_string' => 'is not in the range of 0 to 100', + ], + 'L/B' => [ + 'color' => [90, 10, 900], 'error_code' => 3, + 'error_string' => 'is not in the range of 0 to 100', + ], + 'L/B' => [ + 'color' => [90, 10, -1], 'error_code' => 3, + 'error_string' => 'is not in the range of 0 to 100', + ], + 'L/B close' => [ + 'color' => [90, 10, 100.1], 'error_code' => 3, + 'error_string' => 'is not in the range of 0 to 100', + ], + 'L/B close' => [ + 'color' => [90, 10, -0.1], 'error_code' => 3, + 'error_string' => 'is not in the range of 0 to 100', + ], + ]; + } + + // MARK: HSB Exceptions + + /** + * Undocumented function + * + * @dataProvider providerHueBased + * @testdox Exception handling for HSB for error $error_code [$_dataName] + * + * @param array $color + * @param int $error_code + * @param string $error_string + * @return void + */ + public function testExceptionHSB(array $color, int $error_code, string $error_string): void + { + // error string based on code + switch ($error_code) { + case 2: + $error_string = "/ for saturation $error_string$/"; + break; + case 3: + $error_string = "/ for brightness $error_string$/"; + break; + } // for H/S/B exception the same $this->expectException(\LengthException::class); - Color\Coordinates\HSB::__constructFromArray([900, 10, 10]); + $this->expectExceptionCode($error_code); + $this->expectExceptionMessageMatches($error_string); + new Color\Coordinates\HSB($color); + } + /** + * Undocumented function + * + * @testdox Exception handling for HSB general calls + * + * @return void + */ + public function testExceptionHSBGeneral() + { + // allow + $b = new Color\Coordinates\HSB([0, 0, 0], 'sRGB'); // invalid access to class - // $this->expectException(\ErrorException::class); + $b = new Color\Coordinates\HSB([0, 0, 0]); + $this->expectException(\ErrorException::class); + $this->expectExceptionCode(0); + $this->expectExceptionMessage("Creation of dynamic property is not allowed"); + $b->g; + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionCode(0); + $this->expectExceptionMessage("Only array colors allowed"); + new Color\Coordinates\HSB('string'); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionCode(0); + $this->expectExceptionMessage("Not allowed colorspace"); + new Color\Coordinates\HSB([0, 0, 0], 'FOO_BAR'); + } + + // MARK: HSL Exceptions + + /** + * Undocumented function + * + * @dataProvider providerHueBased + * @testdox Exception handling for HSL for error $error_code [$_dataName] + * + * @param array $color + * @param int $error_code + * @param string $error_string + * @return void + */ + public function testExceptionHSL(array $color, int $error_code, string $error_string): void + { + // error string based on code + switch ($error_code) { + case 2: + $error_string = "/ for saturation $error_string$/"; + break; + case 3: + $error_string = "/ for lightness $error_string$/"; + break; + } + // for H/S/B exception the same + $this->expectException(\LengthException::class); + $this->expectExceptionCode($error_code); + $this->expectExceptionMessageMatches($error_string); + new Color\Coordinates\HSL($color); + } + + /** + * Undocumented function + * + * @testdox Exception handling for HSL general calls + * + * @return void + */ + public function testExceptionHSLGeneral() + { + // allow + $b = new Color\Coordinates\HSL([0, 0, 0], 'sRGB'); + // invalid access to class + $b = new Color\Coordinates\HSL([0, 0, 0]); + $this->expectException(\ErrorException::class); + $this->expectExceptionCode(0); + $this->expectExceptionMessage("Creation of dynamic property is not allowed"); + $b->g; + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionCode(0); + $this->expectExceptionMessage("Only array colors allowed"); + new Color\Coordinates\HSL('string'); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionCode(0); + $this->expectExceptionMessage("Not allowed colorspace"); + new Color\Coordinates\HSL([0, 0, 0], 'FOO_BAR'); + } + + // MARK: HWB Exceptions + + /** + * Undocumented function + * + * @dataProvider providerHueBased + * @testdox Exception handling for HWB for error $error_code [$_dataName] + * + * @param array $color + * @param int $error_code + * @param string $error_string + * @return void + */ + public function testExceptionHWB(array $color, int $error_code, string $error_string): void + { + // error string based on code + switch ($error_code) { + case 2: + $error_string = "/ for whiteness $error_string$/"; + break; + case 3: + $error_string = "/ for blackness $error_string$/"; + break; + } + // for H/S/B exception the same + $this->expectException(\LengthException::class); + $this->expectExceptionCode($error_code); + $this->expectExceptionMessageMatches($error_string); + new Color\Coordinates\HWB($color); + } + + /** + * Undocumented function + * + * @testdox Exception handling for HWB general calls + * + * @return void + */ + public function testExceptionHWBGeneral() + { + // allow + $b = new Color\Coordinates\HWB([0, 0, 0], 'sRGB'); + // invalid access to class + $b = new Color\Coordinates\HWB([0, 0, 0]); + $this->expectException(\ErrorException::class); + $this->expectExceptionCode(0); + $this->expectExceptionMessage("Creation of dynamic property is not allowed"); + $b->g; + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionCode(0); + $this->expectExceptionMessage("Only array colors allowed"); + new Color\Coordinates\HWB('string'); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionCode(0); + $this->expectExceptionMessage("Not allowed colorspace"); + new Color\Coordinates\HWB([0, 0, 0], 'FOO_BAR'); + } + + // MARK: RGB Exceptions + + /** + * Undocumented function + * + * @return array + */ + public function providerRgbBased(): array + { + // all HSB/V HSL HWB have the same value range, create test data for all of them + return [ + 'R' => [ + 'color' => [900, 10, 10], + 'error_code' => 1, + 'error_string' => '/ is not in the range of 0 to 255$/', + 'linear' => false, + ], + 'R' => [ + 'color' => [-1, 10, 10], 'error_code' => 1, + 'error_string' => '/ is not in the range of 0 to 255$/', + 'linear' => false, + ], + 'G' => [ + 'color' => [90, 900, 10], 'error_code' => 1, + 'error_string' => '/ is not in the range of 0 to 255$/', + 'linear' => false, + ], + 'G' => [ + 'color' => [90, -1, 10], 'error_code' => 1, + 'error_string' => '/ is not in the range of 0 to 255$/', + 'linear' => false, + ], + 'B' => [ + 'color' => [90, 10, 900], 'error_code' => 1, + 'error_string' => '/ is not in the range of 0 to 255$/', + 'linear' => false, + ], + 'B' => [ + 'color' => [90, 10, -1], 'error_code' => 1, + 'error_string' => '/ is not in the range of 0 to 255$/', + 'linear' => false, + ], + 'R linear' => [ + 'color' => [2, 0.5, 0.5], + 'error_code' => 2, + 'error_string' => '/ is not in the range of 0 to 1 for linear rgb$/', + 'linear' => true, + ], + 'R linear' => [ + 'color' => [-1, 0.5, 0.5], + 'error_code' => 2, + 'error_string' => '/ is not in the range of 0 to 1 for linear rgb$/', + 'linear' => true, + ], + 'G linear' => [ + 'color' => [0.5, 2, 0.5], + 'error_code' => 2, + 'error_string' => '/ is not in the range of 0 to 1 for linear rgb$/', + 'linear' => true, + ], + 'G linear' => [ + 'color' => [0.5, -1, 0.5], + 'error_code' => 2, + 'error_string' => '/ is not in the range of 0 to 1 for linear rgb$/', + 'linear' => true, + ], + 'B linear' => [ + 'color' => [0.5, 0.5, 2], + 'error_code' => 2, + 'error_string' => '/ is not in the range of 0 to 1 for linear rgb$/', + 'linear' => true, + ], + 'B linear' => [ + 'color' => [0.5, 0.5, -1], + 'error_code' => 2, + 'error_string' => '/ is not in the range of 0 to 1 for linear rgb$/', + 'linear' => true, + ], + ]; + } + + /** + * Undocumented function + * + * @dataProvider providerRgbBased + * @testdox Exception handling for RGB for error $error_code [$_dataName] + * + * @param string|array $color + * @param int $error_code + * @param string $error_string + * @param bool $linear + * @return void + */ + public function testExceptionRGB(string|array $color, int $error_code, string $error_string, bool $linear): void + { + // for RGB exception the same + $this->expectException(\LengthException::class); + $this->expectExceptionCode($error_code); + $this->expectExceptionMessageMatches($error_string); + new Color\Coordinates\RGB($color, options: ["linear" => $linear]); + } + + /** + * Undocumented function + * + * @covers ::__get + * @testdox Exception handling for RGB general calls + * + * @return void + */ + public function testExceptionRGBGeneral() + { + // allow + $b = new Color\Coordinates\RGB([0, 0, 0], 'sRGB'); + // invalid access to class + $b = new Color\Coordinates\RGB([0, 0, 0]); + $this->expectException(\ErrorException::class); + $this->expectExceptionCode(0); + $this->expectExceptionMessage("Creation of dynamic property is not allowed"); + $b->h; + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionCode(0); + $this->expectExceptionMessage("Not allowed colorspace"); + new Color\Coordinates\RGB([0, 0, 0], 'FOO_BAR'); + } + + /** + * Undocumented function + * + * @covers ::setFromHex + * @testdox Exception handling for RGB setFromHex failues + * + * @return void + */ + public function testExceptionRGBFromHex() + { + $color = ''; + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionCode(3); + $this->expectExceptionMessage('hex_string argument cannot be empty'); + new Color\Coordinates\RGB($color); + + $color = 'zshj'; + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionCode(3); + $this->expectExceptionMessage('hex_string argument cannot be empty'); + new Color\Coordinates\RGB($color); + + $color = 'aabff'; + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionCode(4); + $this->expectExceptionMessageMatches('/^Invalid hex_string: /'); + new Color\Coordinates\RGB($color); + } + + // MARK: Lab Exceptions + + /** + * Undocumented function + * + * @return array + */ + public function providerLabBased(): array + { + // all HSB/V HSL HWB have the same value range, create test data for all of them + return [ + 'L CieLab' => [ + 'color' => [900, 10, 10], 'error_code' => 1, + 'error_string' => '/ for lightness is not in the range of 0 to 100 for CIE Lab$/', + 'colorspace' => 'CIELab', + ], + 'L CieLab' => [ + 'color' => [-1, 10, 10], 'error_code' => 1, + 'error_string' => '/ for lightness is not in the range of 0 to 100 for CIE Lab$/', + 'colorspace' => 'CIELab', + ], + 'L OkLab' => [ + 'color' => [900, 0.2, 0.2], 'error_code' => 1, + 'error_string' => '/ for lightness is not in the range of 0.0 to 1.0 for OkLab$/', + 'colorspace' => 'OkLab', + ], + 'L OkLab' => [ + 'color' => [-1, 0.2, 0.2], 'error_code' => 1, + 'error_string' => '/ for lightness is not in the range of 0.0 to 1.0 for OkLab$/', + 'colorspace' => 'OkLab', + ], + 'a CieLab' => [ + 'color' => [90, 900, 10], 'error_code' => 2, + 'error_string' => '/ for a is not in the range of -125 to 125 for CIE Lab$/', + 'colorspace' => 'CIELab', + ], + 'a CieLab' => [ + 'color' => [90, -900, 10], 'error_code' => 2, + 'error_string' => '/ for a is not in the range of -125 to 125 for CIE Lab$/', + 'colorspace' => 'CIELab', + ], + 'a OkLab' => [ + 'color' => [0.5, 900, 0.2], 'error_code' => 2, + 'error_string' => '/ for a is not in the range of -0.5 to 0.5 for OkLab$/', + 'colorspace' => 'OkLab', + ], + 'a OkLab' => [ + 'color' => [0.6, -900, 0.2], 'error_code' => 2, + 'error_string' => '/ for a is not in the range of -0.5 to 0.5 for OkLab$/', + 'colorspace' => 'OkLab', + ], + 'b CieLab' => [ + 'color' => [90, 10, 900], 'error_code' => 3, + 'error_string' => '/ for b is not in the range of -125 to 125 for CIE Lab$/', + 'colorspace' => 'CIELab', + ], + 'b CieLab' => [ + 'color' => [90, 10, -999], 'error_code' => 3, + 'error_string' => '/ for b is not in the range of -125 to 125 for CIE Lab$/', + 'colorspace' => 'CIELab', + ], + 'b OkLab' => [ + 'color' => [0.6, 0.2, 900], 'error_code' => 3, + 'error_string' => '/ for b is not in the range of -0.5 to 0.5 for OkLab$/', + 'colorspace' => 'OkLab', + ], + 'b OkLab' => [ + 'color' => [0.6, 0.2, -999], 'error_code' => 3, + 'error_string' => '/ for b is not in the range of -0.5 to 0.5 for OkLab$/', + 'colorspace' => 'OkLab', + ], + ]; + } + + /** + * Undocumented function + * + * @dataProvider providerLabBased + * @testdox Exception handling for Lab for error $error_code [$_dataName] + * + * @param string|array $color + * @param int $error_code + * @param string $error_string + * @param string $colorspace + * @return void + */ + public function testExceptionLab( + string|array $color, + int $error_code, + string $error_string, + string $colorspace + ): void { + // for RGB exception the same + $this->expectException(\LengthException::class); + $this->expectExceptionCode($error_code); + $this->expectExceptionMessageMatches($error_string); + new Color\Coordinates\Lab($color, colorspace: $colorspace); + } + + /** + * Undocumented function + * + * @covers ::__get + * @testdox Exception handling for Lab general calls + * + * @return void + */ + public function testExceptionLabGeneral() + { + // allow + $b = new Color\Coordinates\Lab([0, 0, 0], 'OkLab'); + // invalid access to class + $b = new Color\Coordinates\Lab([0, 0, 0], 'CIELab'); + $this->expectException(\ErrorException::class); + $this->expectExceptionCode(0); + $this->expectExceptionMessage("Creation of dynamic property is not allowed"); + $b->x; + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionCode(0); + $this->expectExceptionMessage("Only array colors allowed"); + new Color\Coordinates\Lab('string', 'CIELab'); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionCode(0); + $this->expectExceptionMessage("Not allowed colorspace"); + new Color\Coordinates\Lab([0, 0, 0], 'FOO_BAR'); + } + + // MARK: LCH Exceptions + + // public function testExceptionLch(string|array $color, int $error_code, string $error_string): void + + /** + * Undocumented function + * + * @covers ::__get + * @testdox Exception handling for LCH general calls + * + * @return void + */ + public function testExceptionLchGeneral() + { + // allow + $b = new Color\Coordinates\LCH([0, 0, 0], 'OkLab'); + // invalid access to class + $b = new Color\Coordinates\LCH([0, 0, 0], 'CIELab'); + $this->expectException(\ErrorException::class); + $this->expectExceptionCode(0); + $this->expectExceptionMessage("Creation of dynamic property is not allowed"); + $b->x; + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionCode(0); + $this->expectExceptionMessage("Only array colors allowed"); + new Color\Coordinates\LCH('string', 'CIELab'); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionCode(0); + $this->expectExceptionMessage("Not allowed colorspace"); + new Color\Coordinates\LCH([0, 0, 0], 'FOO_BAR'); + } + + // MARK: XYZ Exceptions + + // Note, we do not check for value exceptions here + // public function testExceptionXyz(string|array $color, int $error_code, string $error_string): void + + /** + * Undocumented function + * + * @covers ::__get + * @testdox Exception handling for XYZ general calls + * + * @return void + */ + public function testExceptionXyzGeneral() + { + // allow + $b = new Color\Coordinates\XYZ([0, 0, 0], 'CIEXYZ'); + // invalid access to class + $b = new Color\Coordinates\XYZ([0, 0, 0]); + $this->expectException(\ErrorException::class); + $this->expectExceptionCode(0); + $this->expectExceptionMessage("Creation of dynamic property is not allowed"); + $b->x; + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionCode(0); + $this->expectExceptionMessage("Only array colors allowed"); + new Color\Coordinates\XYZ('string'); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionCode(0); + $this->expectExceptionMessage("Not allowed colorspace"); + new Color\Coordinates\XYZ([0, 0, 0], 'FOO_BAR'); } } diff --git a/4dev/tests/Convert/CoreLibsConvertMathTest.php b/4dev/tests/Convert/CoreLibsConvertMathTest.php index 476c5c1f..95002850 100644 --- a/4dev/tests/Convert/CoreLibsConvertMathTest.php +++ b/4dev/tests/Convert/CoreLibsConvertMathTest.php @@ -142,7 +142,6 @@ final class CoreLibsConvertMathTest extends TestCase */ public function testCbrt(float|int $number, float|string $expected, int $round_to): void { - print "OUT: " . \CoreLibs\Convert\Math::cbrt($number) . "\n"; $this->assertEquals( $expected, round(\CoreLibs\Convert\Math::cbrt($number), $round_to) @@ -264,6 +263,130 @@ final class CoreLibsConvertMathTest extends TestCase \CoreLibs\Convert\Math::multiplyMatrices($input_a, $input_b) ); } + + /** + * Undocumented function + * + * @return array + */ + public function providerEqualWithEpsilon(): array + { + return [ + 'equal' => [ + 'a' => 0.000000000000000222, + 'b' => 0.000000000000000222, + 'epsilon' => PHP_FLOAT_EPSILON, + 'equal' => true, + ], + 'almost equal' => [ + 'a' => 0.000000000000000222, + 'b' => 0.000000000000000232, + 'epsilon' => PHP_FLOAT_EPSILON, + 'equal' => true, + ], + 'not equal' => [ + 'a' => 0.000000000000000222, + 'b' => 0.000000000000004222, + 'epsilon' => PHP_FLOAT_EPSILON, + 'equal' => false, + ], + 'equal, different epsilon' => [ + 'a' => 0.000000000000000222, + 'b' => 0.000000000000004222, + 'epsilon' => 0.0001, + 'equal' => true, + ], + 'not equal, different epsilon' => [ + 'a' => 0.0001, + 'b' => 0.0002, + 'epsilon' => 0.0001, + 'equal' => false, + ] + ]; + } + + /** + * Undocumented function + * + * @covers ::equalWithEpsilon + * @dataProvider providerEqualWithEpsilon + * @testdox equalWithEpsilon with $a and $b and Epsilon: $epsilon must be equal: $equal [$_dataName] + * + * @return void + */ + public function testEqualWithEpsilon(float $a, float $b, float $epsilon, bool $equal): void + { + $this->assertEquals( + $equal, + \CoreLibs\Convert\Math::equalWithEpsilon($a, $b, $epsilon) + ); + } + + /** + * Undocumented function + * + * @return array + */ + public function providerCompareWithEpsilon(): array + { + return [ + 'smaller, true' => [ + 'value' => 0.0001, + 'compare' => '<', + 'limit' => 0.0002, + 'epsilon' => 0.00001, + 'match' => true, + ], + 'smaller, false' => [ + 'value' => 0.0001, + 'compare' => '<', + 'limit' => 0.0001, + 'epsilon' => 0.00001, + 'match' => false, + ], + 'bigger, true' => [ + 'value' => 0.0002, + 'compare' => '>', + 'limit' => 0.0001, + 'epsilon' => 0.00001, + 'match' => true, + ], + 'bigger, false' => [ + 'value' => 0.0001, + 'compare' => '>', + 'limit' => 0.0001, + 'epsilon' => 0.00001, + 'match' => false, + ], + ]; + } + + /** + * Undocumented function + * + * @covers ::compareWithEpsilon + * @dataProvider providerCompareWithEpsilon + * @testdox compareWithEpsilon $value $compare $limit with $epsilon must match: $match [$_dataName] + * + * @param float $value + * @param string $compare + * @param float $limit + * @param float $epslion + * @param bool $match + * @return void + */ + public function testCompareWithEpsilon( + float $value, + string $compare, + float $limit, + float $epsilon, + bool $match + ): void { + $this->assertEquals( + $match, + \CoreLibs\Convert\Math::compareWithEpsilon($value, $compare, $limit, $epsilon) + ); + } } // __END__ diff --git a/www/admin/class_test.convert.colors.php b/www/admin/class_test.convert.colors.php index 131bc642..0254ce67 100644 --- a/www/admin/class_test.convert.colors.php +++ b/www/admin/class_test.convert.colors.php @@ -72,7 +72,7 @@ print '

' . $PAGE_NAME . '

'; // define a list of from to color sets for conversion test -$hwb = Color::hsbToHwb(Coordinates\HSB::__constructFromArray([ +$hwb = Color::hsbToHwb(new Coordinates\HSB([ 160, 0, 50, @@ -81,7 +81,7 @@ print "HWB: " . DgS::printAr($hwb) . "
"; $hsb = Color::hwbToHsb($hwb); print "HSB: " . DgS::printAr($hsb) . "
"; -$oklch = Color::rgbToOkLch(Coordinates\RGB::__constructFromArray([ +$oklch = Color::rgbToOkLch(Coordinates\RGB::create([ 250, 0, 0 @@ -90,7 +90,7 @@ print "OkLch: " . DgS::printAr($oklch) . "
"; $rgb = Color::okLchToRgb($oklch); print "OkLch -> RGB: " . DgS::printAr($rgb) . "
"; -$oklab = Color::rgbToOkLab(Coordinates\RGB::__constructFromArray([ +$oklab = Color::rgbToOkLab(Coordinates\RGB::create([ 250, 0, 0 @@ -101,24 +101,24 @@ $rgb = Color::okLabToRgb($oklab); print "OkLab -> RGB: " . DgS::printAr($rgb) . "
"; print display($rgb->toCssString(), $rgb->toCssString(), 'OkLab to RGB'); -$rgb = Coordinates\RGB::__constructFromArray([250, 100, 10])->toLinear(); +$rgb = Coordinates\RGB::create([250, 100, 10])->toLinear(); print "RGBlinear: " . DgS::printAr($rgb) . "
"; -$rgb = Coordinates\RGB::__constructFromArray([0, 0, 0])->toLinear(); +$rgb = Coordinates\RGB::create([0, 0, 0])->toLinear(); print "RGBlinear: " . DgS::printAr($rgb) . "
"; $cie_lab = Color::okLabToLab($oklab); print "CieLab: " . DgS::printAr($cie_lab) . "
"; print display($cie_lab->toCssString(), $cie_lab->toCssString(), 'OkLab to Cie Lab'); -$rgb = Coordinates\RGB::__constructFromArray([0, 0, 60]); +$rgb = Coordinates\RGB::create([0, 0, 60]); $hsb = Color::rgbToHsb($rgb); $rgb_b = Color::hsbToRgb($hsb); print "RGB: " . DgS::printAr($rgb) . "
"; print "RGB->HSB: " . DgS::printAr($hsb) . "
"; print "HSB->RGB: " . DgS::printAr($rgb_b) . "
"; -$hsl = Coordinates\HSL::__constructFromArray([0, 20, 0]); -$hsb = Coordinates\HSB::__constructFromArray([0, 20, 0]); +$hsl = Coordinates\HSL::create([0, 20, 0]); +$hsb = Coordinates\HSB::create([0, 20, 0]); $hsl_from_hsb = Color::hsbToHsl($hsb); print "HSL from HSB: " . DgS::printAr($hsl_from_hsb) . "
"; diff --git a/www/lib/CoreLibs/Convert/Color/CieXyz.php b/www/lib/CoreLibs/Convert/Color/CieXyz.php index 8651a7dd..65157597 100644 --- a/www/lib/CoreLibs/Convert/Color/CieXyz.php +++ b/www/lib/CoreLibs/Convert/Color/CieXyz.php @@ -250,7 +250,7 @@ class CieXyz { // if not linear, convert to linear if (!$rgb->linear) { - $rgb->toLinear(); + $rgb = (new RGB($rgb->returnAsArray()))->toLinear(); } return new XYZ(Math::multiplyMatrices( [ diff --git a/www/lib/CoreLibs/Convert/Color/Color.php b/www/lib/CoreLibs/Convert/Color/Color.php index 9dd2f063..17f3accc 100644 --- a/www/lib/CoreLibs/Convert/Color/Color.php +++ b/www/lib/CoreLibs/Convert/Color/Color.php @@ -766,11 +766,6 @@ class Color public static function rgbToLab(RGB $rgb): Lab { return CieXyz::rgbViaXyzD65ViaXyzD50ToLab($rgb); - /* return CieXyz::xyzD50ToLab( - CieXyz::xyzD65ToXyzD50( - CieXyz::linRgbToXyzD65($rgb) - ) - ); */ } /** @@ -783,11 +778,6 @@ class Color public static function labToRgb(Lab $lab): RGB { return CieXyz::labViaXyzD50ViaXyzD65ToRgb($lab); - /* return CieXyz::xyzD65ToLinRgb( - CieXyz::xyzD50ToXyxD65( - CieXyz::labToXyzD50($lab) - ) - )->fromLinear(); */ } // MARK: RGB <-> Lch (Cie) diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/HSB.php b/www/lib/CoreLibs/Convert/Color/Coordinates/HSB.php index 821fe010..62d83a5b 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/HSB.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/HSB.php @@ -11,6 +11,8 @@ declare(strict_types=1); namespace CoreLibs\Convert\Color\Coordinates; +use CoreLibs\Convert\Color\Utils; + class HSB implements Interface\CoordinatesInterface { /** @var array allowed colorspaces */ @@ -83,10 +85,11 @@ class HSB implements Interface\CoordinatesInterface } switch ($name) { case 'H': - if ((int)$value == 360) { + if ($value == 360.0) { $value = 0; } - if ((int)$value < 0 || (int)$value > 360) { + // if ($value < 0 || $value > 360) { + if (Utils::compare(0.0, $value, 360.0, Utils::EPSILON_SMALL)) { throw new \LengthException( 'Argument value ' . $value . ' for hue is not in the range of 0 to 360', 1 @@ -94,7 +97,8 @@ class HSB implements Interface\CoordinatesInterface } break; case 'S': - if ((int)$value < 0 || (int)$value > 100) { + // if ($value < 0 || $value > 100) { + if (Utils::compare(0.0, $value, 100.0, Utils::EPSILON_SMALL)) { throw new \LengthException( 'Argument value ' . $value . ' for saturation is not in the range of 0 to 100', 2 @@ -102,7 +106,8 @@ class HSB implements Interface\CoordinatesInterface } break; case 'B': - if ((int)$value < 0 || (int)$value > 100) { + // if ($value < 0 || $value > 100) { + if (Utils::compare(0.0, $value, 100.0, Utils::EPSILON_SMALL)) { throw new \LengthException( 'Argument value ' . $value . ' for brightness is not in the range of 0 to 100', 3 diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/HSL.php b/www/lib/CoreLibs/Convert/Color/Coordinates/HSL.php index 64081e6e..c5198c23 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/HSL.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/HSL.php @@ -11,7 +11,7 @@ declare(strict_types=1); namespace CoreLibs\Convert\Color\Coordinates; -use CoreLibs\Convert\Color\Stringify; +use CoreLibs\Convert\Color\Utils; class HSL implements Interface\CoordinatesInterface { @@ -84,10 +84,11 @@ class HSL implements Interface\CoordinatesInterface } switch ($name) { case 'H': - if ((int)$value == 360) { + if ($value == 360.0) { $value = 0; } - if ((int)$value < 0 || (int)$value > 360) { + // if ($value < 0 || $value > 360) { + if (Utils::compare(0.0, $value, 360.0, Utils::EPSILON_SMALL)) { throw new \LengthException( 'Argument value ' . $value . ' for hue is not in the range of 0 to 360', 1 @@ -95,7 +96,8 @@ class HSL implements Interface\CoordinatesInterface } break; case 'S': - if ((int)$value < 0 || (int)$value > 100) { + // if ($value < 0 || $value > 100) { + if (Utils::compare(0.0, $value, 100.0, Utils::EPSILON_SMALL)) { throw new \LengthException( 'Argument value ' . $value . ' for saturation is not in the range of 0 to 100', 2 @@ -103,7 +105,8 @@ class HSL implements Interface\CoordinatesInterface } break; case 'L': - if ((int)$value < 0 || (int)$value > 100) { + // if ($value < 0 || $value > 100) { + if (Utils::compare(0.0, $value, 100.0, Utils::EPSILON_SMALL)) { throw new \LengthException( 'Argument value ' . $value . ' for lightness is not in the range of 0 to 100', 3 @@ -183,7 +186,7 @@ class HSL implements Interface\CoordinatesInterface . $this->S . ' ' . $this->L - . Stringify::setOpacity($opacity) + . Utils::setOpacity($opacity) . ')'; return $string; } diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/HWB.php b/www/lib/CoreLibs/Convert/Color/Coordinates/HWB.php index 0f1ce58f..dbad9861 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/HWB.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/HWB.php @@ -11,7 +11,7 @@ declare(strict_types=1); namespace CoreLibs\Convert\Color\Coordinates; -use CoreLibs\Convert\Color\Stringify; +use CoreLibs\Convert\Color\Utils; class HWB implements Interface\CoordinatesInterface { @@ -84,10 +84,11 @@ class HWB implements Interface\CoordinatesInterface } switch ($name) { case 'H': - if ((int)$value == 360) { + if ($value == 360.0) { $value = 0; } - if ((int)$value < 0 || (int)$value > 360) { + // if ($value < 0 || $value > 360) { + if (Utils::compare(0.0, $value, 360.0, Utils::EPSILON_SMALL)) { throw new \LengthException( 'Argument value ' . $value . ' for hue is not in the range of 0 to 360', 1 @@ -95,7 +96,8 @@ class HWB implements Interface\CoordinatesInterface } break; case 'W': - if ((int)$value < 0 || (int)$value > 100) { + // if ($value < 0 || $value > 100) { + if (Utils::compare(0.0, $value, 100.0, Utils::EPSILON_SMALL)) { throw new \LengthException( 'Argument value ' . $value . ' for whiteness is not in the range of 0 to 100', 2 @@ -103,7 +105,8 @@ class HWB implements Interface\CoordinatesInterface } break; case 'B': - if ((int)$value < 0 || (int)$value > 100) { + // if ($value < 0 || $value > 100) { + if (Utils::compare(0.0, $value, 100.0, Utils::EPSILON_SMALL)) { throw new \LengthException( 'Argument value ' . $value . ' for blackness is not in the range of 0 to 100', 3 @@ -183,7 +186,7 @@ class HWB implements Interface\CoordinatesInterface . $this->W . ' ' . $this->B - . Stringify::setOpacity($opacity) + . Utils::setOpacity($opacity) . ')'; return $string; } diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/LCH.php b/www/lib/CoreLibs/Convert/Color/Coordinates/LCH.php index 76b3ad69..8f886292 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/LCH.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/LCH.php @@ -12,7 +12,7 @@ declare(strict_types=1); namespace CoreLibs\Convert\Color\Coordinates; -use CoreLibs\Convert\Color\Stringify; +use CoreLibs\Convert\Color\Utils; class LCH implements Interface\CoordinatesInterface { @@ -94,43 +94,43 @@ class LCH implements Interface\CoordinatesInterface throw new \ErrorException('Creation of dynamic property is not allowed', 0); } switch ($name) { - // case 'L': - // if ($this->colorspace == 'cie' && ($value < 0 || $value > 100)) { - // throw new \LengthException( - // 'Argument value ' . $value . ' for lightness is not in the range of ' - // . '0 to 100', - // 3 - // ); - // } elseif ($this->colorspace == 'ok' && ($value < 0 || $value > 1)) { - // throw new \LengthException( - // 'Argument value ' . $value . ' for lightness is not in the range of ' - // . '0 to 1', - // 3 - // ); - // } - // break; - // case 'c': - // if ($this->colorspace == 'cie' && ($value < 0 || $value > 230)) { - // throw new \LengthException( - // 'Argument value ' . $value . ' for chroma is not in the range of ' - // . '0 to 230 with normal upper limit of 150', - // 3 - // ); - // } elseif ($this->colorspace == 'ok' && ($value < 0 || $value > 0.5)) { - // throw new \LengthException( - // 'Argument value ' . $value . ' for chroma is not in the range of ' - // . '0 to 0.5 with normal upper limit of 0.5', - // 3 - // ); - // } - // break; - case 'h': - if ($value == 360) { - $value = 0; - } - if ($value < 0 || $value > 360) { + case 'L': + // if ($this->colorspace == 'CIELab' && ($value < 0 || $value > 100)) { + if ($this->colorspace == 'CIELab' && Utils::compare(0.0, $value, 100.0, Utils::ESPILON_BIG)) { throw new \LengthException( - 'Argument value ' . $value . ' for lightness is not in the range of 0 to 360', + 'Argument value ' . $value . ' for lightness is not in the range of 0 to 100 for CIE Lab', + 1 + ); + // } elseif ($this->colorspace == 'OkLab' && ($value < 0 || $value > 1)) { + } elseif ($this->colorspace == 'OkLab' && Utils::compare(0.0, $value, 1.0, Utils::EPSILON_SMALL)) { + throw new \LengthException( + 'Argument value ' . $value . ' for lightness is not in the range of 0.0 to 1.0 for OkLab', + 1 + ); + } + break; + case 'C': + // if ($this->colorspace == 'CIELab' && ($value < 0 || $value > 230)) { + if ($this->colorspace == 'CIELab' && Utils::compare(0.0, $value, 230.0, Utils::EPSILON_SMALL)) { + throw new \LengthException( + 'Argument value ' . $value . ' for chroma is not in the range of ' + . '0 to 150 and a maximum of 230 for CIE Lab', + 1 + ); + // } elseif ($this->colorspace == 'OkLab' && ($value < 0 || $value > 0.55)) { + } elseif ($this->colorspace == 'OkLab' && Utils::compare(0.0, $value, 0.55, Utils::EPSILON_SMALL)) { + throw new \LengthException( + 'Argument value ' . $value . ' for lightness is not in the range of ' + . '0.0 to 0.4 and a maximum of 0.5 for OkLab', + 1 + ); + } + break; + case 'H': + // if ($value < 0 || $value > 360) { + if (Utils::compare(0.0, $value, 360.0, Utils::EPSILON_SMALL)) { + throw new \LengthException( + 'Argument value ' . $value . ' for hue is not in the range of 0.0 to 360.0', 1 ); } @@ -217,7 +217,7 @@ class LCH implements Interface\CoordinatesInterface . $this->c . ' ' . $this->h - . Stringify::setOpacity($opacity) + . Utils::setOpacity($opacity) . ');'; return $string; diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/Lab.php b/www/lib/CoreLibs/Convert/Color/Coordinates/Lab.php index 1b475d5c..60635c0c 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/Lab.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/Lab.php @@ -12,7 +12,7 @@ declare(strict_types=1); namespace CoreLibs\Convert\Color\Coordinates; -use CoreLibs\Convert\Color\Stringify; +use CoreLibs\Convert\Color\Utils; class Lab implements Interface\CoordinatesInterface { @@ -95,35 +95,53 @@ class Lab implements Interface\CoordinatesInterface if (!property_exists($this, $name)) { throw new \ErrorException('Creation of dynamic property is not allowed', 0); } - // switch ($name) { - // case 'L': - // if ($value == 360) { - // $value = 0; - // } - // if ($value < 0 || $value > 360) { - // throw new \LengthException( - // 'Argument value ' . $value . ' for lightness is not in the range of 0 to 360', - // 1 - // ); - // } - // break; - // case 'a': - // if ($value < 0 || $value > 100) { - // throw new \LengthException( - // 'Argument value ' . $value . ' for a is not in the range of 0 to 100', - // 2 - // ); - // } - // break; - // case 'b': - // if ($value < 0 || $value > 100) { - // throw new \LengthException( - // 'Argument value ' . $value . ' for b is not in the range of 0 to 100', - // 3 - // ); - // } - // break; - // } + switch ($name) { + case 'L': + // if ($this->colorspace == 'CIELab' && ($value < 0 || $value > 100)) { + if ($this->colorspace == 'CIELab' && Utils::compare(0.0, $value, 100.0, Utils::ESPILON_BIG)) { + throw new \LengthException( + 'Argument value ' . $value . ' for lightness is not in the range of 0 to 100 for CIE Lab', + 1 + ); + // } elseif ($this->colorspace == 'OkLab' && ($value < 0 || $value > 1)) { + } elseif ($this->colorspace == 'OkLab' && Utils::compare(0.0, $value, 1.0, Utils::EPSILON_SMALL)) { + throw new \LengthException( + 'Argument value ' . $value . ' for lightness is not in the range of 0.0 to 1.0 for OkLab', + 1 + ); + } + break; + case 'a': + // if ($this->colorspace == 'CIELab' && ($value < -125 || $value > 125)) { + if ($this->colorspace == 'CIELab' && Utils::compare(-125.0, $value, 125.0, Utils::EPSILON_SMALL)) { + throw new \LengthException( + 'Argument value ' . $value . ' for a is not in the range of -125 to 125 for CIE Lab', + 2 + ); + // } elseif ($this->colorspace == 'OkLab' && ($value < -0.55 || $value > 0.55)) { + } elseif ($this->colorspace == 'OkLab' && Utils::compare(-0.55, $value, 0.55, Utils::EPSILON_SMALL)) { + throw new \LengthException( + 'Argument value ' . $value . ' for a is not in the range of -0.5 to 0.5 for OkLab', + 2 + ); + } + break; + case 'b': + // if ($this->colorspace == 'CIELab' && ($value < -125 || $value > 125)) { + if ($this->colorspace == 'CIELab' && Utils::compare(-125.0, $value, 125.0, Utils::EPSILON_SMALL)) { + throw new \LengthException( + 'Argument value ' . $value . ' for b is not in the range of -125 to 125 for CIE Lab', + 3 + ); + // } elseif ($this->colorspace == 'OkLab' && ($value < -0.55 || $value > 0.55)) { + } elseif ($this->colorspace == 'OkLab' && Utils::compare(-0.55, $value, 0.55, Utils::EPSILON_SMALL)) { + throw new \LengthException( + 'Argument value ' . $value . ' for b is not in the range of -0.5 to 0.5 for OkLab', + 3 + ); + } + break; + } $this->$name = $value; } @@ -205,7 +223,7 @@ class Lab implements Interface\CoordinatesInterface . $this->a . ' ' . $this->b - . Stringify::setOpacity($opacity) + . Utils::setOpacity($opacity) . ');'; return $string; diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/RGB.php b/www/lib/CoreLibs/Convert/Color/Coordinates/RGB.php index 474c37f1..ce69e855 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/RGB.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/RGB.php @@ -11,7 +11,7 @@ declare(strict_types=1); namespace CoreLibs\Convert\Color\Coordinates; -use CoreLibs\Convert\Color\Stringify; +use CoreLibs\Convert\Color\Utils; class RGB implements Interface\CoordinatesInterface { @@ -94,8 +94,11 @@ class RGB implements Interface\CoordinatesInterface // if not linear if (!$this->linear && ((int)$value < 0 || (int)$value > 255)) { throw new \LengthException('Argument value ' . $value . ' for color ' . $name - . ' is not in the range of 0 to 255', 1); - } elseif ($this->linear && ((int)$value < 0 || (int)$value > 1)) { + . ' is not in the range of 0 to 255', 1); + } elseif ( + // $this->linear && ($value < 0.0 || $value > 1.0) + $this->linear && Utils::compare(0.0, $value, 1.0, 0.000001) + ) { throw new \LengthException('Argument value ' . $value . ' for color ' . $name . ' is not in the range of 0 to 1 for linear rgb', 2); } @@ -244,6 +247,10 @@ class RGB implements Interface\CoordinatesInterface */ public function toLinear(): self { + // if linear, as is + if ($this->linear) { + return $this; + } $this->flagLinear(true)->setFromArray(array_map( callback: function (int|float $v) { $v = (float)($v / 255); @@ -268,6 +275,10 @@ class RGB implements Interface\CoordinatesInterface */ public function fromLinear(): self { + // if not linear, as is + if (!$this->linear) { + return $this; + } $this->flagLinear(false)->setFromArray(array_map( callback: function (int|float $v) { $abs = abs($v); @@ -282,7 +293,6 @@ class RGB implements Interface\CoordinatesInterface }, array: $this->returnAsArray(), )); - // $this->linear = false; return $this; } @@ -307,7 +317,7 @@ class RGB implements Interface\CoordinatesInterface . (int)round($this->G, 0) . ' ' . (int)round($this->B, 0) - . Stringify::setOpacity($opacity) + . Utils::setOpacity($opacity) . ')'; if ($was_linear) { $this->toLinear(); diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/XYZ.php b/www/lib/CoreLibs/Convert/Color/Coordinates/XYZ.php index 01fb926d..2e30b98d 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/XYZ.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/XYZ.php @@ -15,6 +15,8 @@ declare(strict_types=1); namespace CoreLibs\Convert\Color\Coordinates; +// use CoreLibs\Convert\Color\Utils; + class XYZ implements Interface\CoordinatesInterface { /** @var array allowed colorspaces */ @@ -101,9 +103,11 @@ class XYZ implements Interface\CoordinatesInterface if (!property_exists($this, $name)) { throw new \ErrorException('Creation of dynamic property is not allowed', 0); } - // if ($value < 0 || $value > 255) { + // TODO: setup XYZ value limits + // X: 0 to 95.047, Y: 0 to 100, Z: 0 to 108.88 + // if (Utils::compare(0.0, $value, 100.0, Utils::EPSILON_SMALL))) { // throw new \LengthException('Argument value ' . $value . ' for color ' . $name - // . ' is not in the range of 0 to 255', 1); + // . ' is not in the range of 0 to 100.0', 1); // } $this->$name = $value; } diff --git a/www/lib/CoreLibs/Convert/Color/Stringify.php b/www/lib/CoreLibs/Convert/Color/Stringify.php index 72dfa020..b2de79b9 100644 --- a/www/lib/CoreLibs/Convert/Color/Stringify.php +++ b/www/lib/CoreLibs/Convert/Color/Stringify.php @@ -19,25 +19,6 @@ use CoreLibs\Convert\Color\Coordinates\LCH; class Stringify { - /** - * Build the opactiy sub string part and return it - * - * @param null|float|string|null $opacity - * @return string - */ - public static function setOpacity(null|float|string $opacity = null): string - { - // set opacity, either a string or float - if (is_string($opacity)) { - $opacity = ' / ' . $opacity; - } elseif ($opacity !== null) { - $opacity = ' / ' . $opacity; - } else { - $opacity = ''; - } - return $opacity; - } - /** * return the CSS string including optional opacity * diff --git a/www/lib/CoreLibs/Convert/Color/Utils.php b/www/lib/CoreLibs/Convert/Color/Utils.php new file mode 100644 index 00000000..b273f001 --- /dev/null +++ b/www/lib/CoreLibs/Convert/Color/Utils.php @@ -0,0 +1,56 @@ +', $upper, $epslion) + ) { + return true; + } + return false; + } + + /** + * Build the opactiy sub string part and return it + * + * @param null|float|string|null $opacity + * @return string + */ + public static function setOpacity(null|float|string $opacity = null): string + { + // set opacity, either a string or float + if (is_string($opacity)) { + $opacity = ' / ' . $opacity; + } elseif ($opacity !== null) { + $opacity = ' / ' . $opacity; + } else { + $opacity = ''; + } + return $opacity; + } +} + +// __END__ diff --git a/www/lib/CoreLibs/Convert/Math.php b/www/lib/CoreLibs/Convert/Math.php index 8af796b1..fedc60ed 100644 --- a/www/lib/CoreLibs/Convert/Math.php +++ b/www/lib/CoreLibs/Convert/Math.php @@ -68,6 +68,66 @@ class Math return pow((float)$number, 1.0 / 3); } + /** + * use PHP_FLOAT_EPSILON to compare if two float numbers are matching + * + * @param float $x + * @param float $y + * @param float $epsilon [default=PHP_FLOAT_EPSILON] + * @return bool True equal + */ + public static function equalWithEpsilon(float $x, float $y, float $epsilon = PHP_FLOAT_EPSILON): bool + { + if (abs($x - $y) < $epsilon) { + return true; + } + return false; + } + + /** + * Compare two value base on direction given + * The default delta is PHP_FLOAT_EPSILON + * + * @param float $value + * @param string $compare + * @param float $limit + * @param float $epsilon [default=PHP_FLOAT_EPSILON] + * @return bool True on smaller/large or equal + */ + public static function compareWithEpsilon( + float $value, + string $compare, + float $limit, + float $epsilon = PHP_FLOAT_EPSILON + ): bool { + switch ($compare) { + case '<': + if ($value < ($limit - $epsilon)) { + return true; + } + break; + case '<=': + if ($value <= ($limit - $epsilon)) { + return true; + } + break; + case '==': + return self::equalWithEpsilon($value, $limit, $epsilon); + break; + case '>': + if ($value > ($limit + $epsilon)) { + return true; + } + break; + case '>=': + if ($value >= ($limit + $epsilon)) { + return true; + } + break; + } + return false; + } + /** * This function is directly inspired by the multiplyMatrices() function in color.js * form Lea Verou and Chris Lilley. From 5213805a58592956e8dc873e7af53f109bf3823d Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Fri, 15 Nov 2024 18:18:45 +0900 Subject: [PATCH 11/13] phan updates --- www/lib/CoreLibs/Convert/Color/Color.php | 2 +- www/lib/CoreLibs/Convert/Color/Coordinates/Lab.php | 4 ++-- www/lib/CoreLibs/Convert/Color/Coordinates/XYZ.php | 2 +- www/lib/CoreLibs/Convert/Colors.php | 1 + www/lib/CoreLibs/Convert/Math.php | 6 +++--- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/www/lib/CoreLibs/Convert/Color/Color.php b/www/lib/CoreLibs/Convert/Color/Color.php index 17f3accc..744491fc 100644 --- a/www/lib/CoreLibs/Convert/Color/Color.php +++ b/www/lib/CoreLibs/Convert/Color/Color.php @@ -562,7 +562,7 @@ class Color * convert rgb to OkLch * via rgb -> linear rgb -> xyz D65 -> OkLab -> OkLch * - * @param RGB $rbh + * @param RGB $rgb * @return LCH */ public static function rgbToOkLch(RGB $rgb): LCH diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/Lab.php b/www/lib/CoreLibs/Convert/Color/Coordinates/Lab.php index 60635c0c..d4e0b47e 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/Lab.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/Lab.php @@ -45,7 +45,7 @@ class Lab implements Interface\CoordinatesInterface * Color Coordinate: Lab * for oklab or cie * - * @param string|array{0:float,1:float,2:float} $rgb + * @param string|array{0:float,1:float,2:float} $colors * @param string $colorspace [default=''] * @param array $options [default=[]] * @throws \InvalidArgumentException only array colors allowed @@ -62,7 +62,7 @@ class Lab implements Interface\CoordinatesInterface * set from array * where 0: Lightness, 1: a, 2: b * - * @param array{0:float,1:float,2:float} $rgb + * @param array{0:float,1:float,2:float} $colors * @param string $colorspace [default=''] * @param array $options [default=[]] * @return self diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/XYZ.php b/www/lib/CoreLibs/Convert/Color/Coordinates/XYZ.php index 2e30b98d..0909badb 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/XYZ.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/XYZ.php @@ -82,7 +82,7 @@ class XYZ implements Interface\CoordinatesInterface /** * parse options * - * @param array $options + * @param array $options * @return self */ private function parseOptions(array $options): self diff --git a/www/lib/CoreLibs/Convert/Colors.php b/www/lib/CoreLibs/Convert/Colors.php index f4b6f2eb..52f3f9d8 100644 --- a/www/lib/CoreLibs/Convert/Colors.php +++ b/www/lib/CoreLibs/Convert/Colors.php @@ -62,6 +62,7 @@ class Colors $rgbArray = []; // rewrite to previous r/g/b key output foreach ((new Coordinates\RGB($hex_string))->returnAsArray() as $p => $el) { + $k = ''; switch ($p) { case 0: $k = 'r'; diff --git a/www/lib/CoreLibs/Convert/Math.php b/www/lib/CoreLibs/Convert/Math.php index fedc60ed..8b8043db 100644 --- a/www/lib/CoreLibs/Convert/Math.php +++ b/www/lib/CoreLibs/Convert/Math.php @@ -137,10 +137,10 @@ class Math * * It returns an array which is the product of the two number matrices passed as parameters. * - * @param array> $a m x n matrice - * @param array> $b n x p matrice + * @param array> $a m x n matrice + * @param array> $b n x p matrice * - * @return array> m x p product + * @return array> m x p product */ public static function multiplyMatrices(array $a, array $b): array { From e5a9b149b1770cf696eb1bb47fdb6d03c2935c96 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Fri, 15 Nov 2024 19:43:30 +0900 Subject: [PATCH 12/13] phpstan fixes with move away from __get to dedicated get --- www/lib/CoreLibs/Convert/Color/CieXyz.php | 47 +++++++++----- www/lib/CoreLibs/Convert/Color/Color.php | 62 +++++++++---------- .../Convert/Color/Coordinates/HSB.php | 2 +- .../Convert/Color/Coordinates/HSL.php | 2 +- .../Convert/Color/Coordinates/HWB.php | 2 +- .../Interface/CoordinatesInterface.php | 2 +- .../Convert/Color/Coordinates/LCH.php | 8 +-- .../Convert/Color/Coordinates/Lab.php | 2 +- .../Convert/Color/Coordinates/RGB.php | 2 +- .../Convert/Color/Coordinates/XYZ.php | 2 +- www/lib/CoreLibs/Convert/Color/Utils.php | 6 +- www/lib/CoreLibs/Convert/Math.php | 9 ++- 12 files changed, 80 insertions(+), 66 deletions(-) diff --git a/www/lib/CoreLibs/Convert/Color/CieXyz.php b/www/lib/CoreLibs/Convert/Color/CieXyz.php index 65157597..d8fd927c 100644 --- a/www/lib/CoreLibs/Convert/Color/CieXyz.php +++ b/www/lib/CoreLibs/Convert/Color/CieXyz.php @@ -112,6 +112,21 @@ class CieXyz ); } + // MARK: helper convert any array to array{float, float, float} + + /** + * This is a hack for phpstan until we write a proper matrix to class + * conversion wrapper function + * + * @param array|float|int> $_array + * @return array{0:float,1:float,2:float} + */ + private static function convertArray(array $_array): array + { + /** @var array{0:float,1:float,2:float} */ + return [$_array[0], $_array[1], $_array[2]]; + } + // MARK: xyzD65 <-> xyzD50 /** @@ -122,14 +137,14 @@ class CieXyz */ private static function xyzD65ToXyzD50(XYZ $xyz): XYZ { - return new XYZ(Math::multiplyMatrices( + return new XYZ(self::convertArray(Math::multiplyMatrices( a: [ [1.0479298208405488, 0.022946793341019088, -0.05019222954313557], [0.029627815688159344, 0.990434484573249, -0.01707382502938514], [-0.009243058152591178, 0.015055144896577895, 0.7518742899580008], ], b: $xyz->returnAsArray(), - ), options: ["whitepoint" => 'D50']); + )), options: ["whitepoint" => 'D50']); } /** @@ -140,14 +155,14 @@ class CieXyz */ private static function xyzD50ToXyxD65(XYZ $xyz): XYZ { - return new XYZ(Math::multiplyMatrices( + return new XYZ(self::convertArray(Math::multiplyMatrices( a: [ [0.9554734527042182, -0.023098536874261423, 0.0632593086610217], [-0.028369706963208136, 1.0099954580058226, 0.021041398966943008], [0.012314001688319899, -0.020507696433477912, 1.3303659366080753], ], b: $xyz->returnAsArray() - ), options: ["whitepoint" => 'D65']); + )), options: ["whitepoint" => 'D65']); } // MARK: xyzD50 <-> Lab @@ -228,11 +243,11 @@ class CieXyz ]; return new XYZ( - array_map( + self::convertArray(array_map( fn ($k, $v) => $v * $d50[$k], array_keys($xyz), array_values($xyz), - ), + )), options: ["whitepoint" => 'D50'] ); } @@ -249,17 +264,17 @@ class CieXyz private static function linRgbToXyzD65(RGB $rgb): XYZ { // if not linear, convert to linear - if (!$rgb->linear) { + if (!$rgb->get('linear')) { $rgb = (new RGB($rgb->returnAsArray()))->toLinear(); } - return new XYZ(Math::multiplyMatrices( + return new XYZ(self::convertArray(Math::multiplyMatrices( [ [0.41239079926595934, 0.357584339383878, 0.1804807884018343], [0.21263900587151027, 0.715168678767756, 0.07219231536073371], [0.01933081871559182, 0.11919477979462598, 0.9505321522496607], ], $rgb->returnAsArray() - ), options: ["whitepoint" => 'D65']); + )), options: ["whitepoint" => 'D65']); } /** @@ -271,14 +286,14 @@ class CieXyz private static function xyzD65ToLinRgb(XYZ $xyz): RGB { // xyz D65 to linrgb - return new RGB(Math::multiplyMatrices( + return new RGB(self::convertArray(Math::multiplyMatrices( a : [ [ 3.2409699419045226, -1.537383177570094, -0.4986107602930034 ], [ -0.9692436362808796, 1.8759675015077202, 0.04155505740717559 ], [ 0.05563007969699366, -0.20397695888897652, 1.0569715142428786 ], ], b : $xyz->returnAsArray() - ), options: ["linear" => true]); + )), options: ["linear" => true]); } // MARK: xyzD65 <-> OkLab @@ -291,14 +306,14 @@ class CieXyz */ private static function xyzD65ToOkLab(XYZ $xyz): Lab { - return new Lab(Math::multiplyMatrices( + return new Lab(self::convertArray(Math::multiplyMatrices( [ [0.2104542553, 0.7936177850, -0.0040720468], [1.9779984951, -2.4285922050, 0.4505937099], [0.0259040371, 0.7827717662, -0.8086757660], ], array_map( - callback: fn ($v) => pow($v, 1 / 3), + callback: fn ($v) => pow((float)$v, 1 / 3), array: Math::multiplyMatrices( a: [ [0.8190224432164319, 0.3619062562801221, -0.12887378261216414], @@ -308,7 +323,7 @@ class CieXyz b: $xyz->returnAsArray(), ), ) - ), colorspace: 'OkLab'); + )), colorspace: 'OkLab'); } /** @@ -319,7 +334,7 @@ class CieXyz */ private static function okLabToXyzD65(Lab $lab): XYZ { - return new XYZ(Math::multiplyMatrices( + return new XYZ(self::convertArray(Math::multiplyMatrices( a: [ [1.2268798733741557, -0.5578149965554813, 0.28139105017721583], [-0.04057576262431372, 1.1122868293970594, -0.07171106666151701], @@ -337,7 +352,7 @@ class CieXyz b: $lab->returnAsArray(), ), ), - ), options: ["whitepoint" => 'D65']); + )), options: ["whitepoint" => 'D65']); } } diff --git a/www/lib/CoreLibs/Convert/Color/Color.php b/www/lib/CoreLibs/Convert/Color/Color.php index 744491fc..016d71af 100644 --- a/www/lib/CoreLibs/Convert/Color/Color.php +++ b/www/lib/CoreLibs/Convert/Color/Color.php @@ -55,13 +55,13 @@ class Color private static function __labToLch(Lab $lab): array { // cieLab to cieLch - $a = $lab->a; - $b = $lab->b; + $a = (float)$lab->get('a'); + $b = (float)$lab->get('b'); $hue = atan2($b, $a) * 180 / pi(); return [ - $lab->L, + (float)$lab->get('L'), sqrt($a ** 2 + $b ** 2), $hue >= 0 ? $hue : $hue + 360, ]; @@ -76,9 +76,9 @@ class Color private static function __lchToLab(LCH $lch): array { return [ - $lch->L, - $lch->C * cos($lch->H * pi() / 180), // a - $lch->C * sin($lch->H * pi() / 180), // b + (float)$lch->get('L'), + (float)$lch->get('C') * cos((float)$lch->get('H') * pi() / 180), // a + (float)$lch->get('C') * sin((float)$lch->get('H') * pi() / 180), // b ]; } @@ -94,9 +94,9 @@ class Color */ public static function rgbToHsl(RGB $rgb): HSL { - $red = $rgb->R / 255; - $green = $rgb->G / 255; - $blue = $rgb->B / 255; + $red = (float)$rgb->get('R') / 255; + $green = (float)$rgb->get('G') / 255; + $blue = (float)$rgb->get('B') / 255; $min = min($red, $green, $blue); $max = max($red, $green, $blue); @@ -147,9 +147,9 @@ class Color */ public static function hslToRgb(HSL $hsl): RGB { - $hue = $hsl->H; - $sat = $hsl->S; - $lum = $hsl->L; + $hue = (float)$hsl->get('H'); + $sat = (float)$hsl->get('S'); + $lum = (float)$hsl->get('L'); // calc to internal convert value for hue $hue = (1 / 360) * $hue; // convert to internal 0-1 format @@ -201,9 +201,9 @@ class Color */ public static function rgbToHsb(RGB $rgb): HSB { - $red = $rgb->R / 255; - $green = $rgb->G / 255; - $blue = $rgb->B / 255; + $red = (float)$rgb->get('R') / 255; + $green = (float)$rgb->get('G') / 255; + $blue = (float)$rgb->get('B') / 255; $MAX = max($red, $green, $blue); $MIN = min($red, $green, $blue); @@ -246,9 +246,9 @@ class Color */ public static function hsbToRgb(HSB $hsb): RGB { - $H = $hsb->H; - $S = $hsb->S; - $V = $hsb->B; + $H = (float)$hsb->get('H'); + $S = (float)$hsb->get('S'); + $V = (float)$hsb->get('B'); // convert to internal 0-1 format $S /= 100; $V /= 100; @@ -352,8 +352,8 @@ class Color */ public static function hslToHsb(HSL $hsl): HSB { - $saturation = $hsl->S / 100; - $lightness = $hsl->L / 100; + $saturation = (float)$hsl->get('S') / 100; + $lightness = (float)$hsl->get('L') / 100; // if lightness is 0, then we cannot return convert to hsb $value = $lightness + $saturation * min($lightness, 1 - $lightness); // print "Orig: " . print_r($hsl, true) . "\n"; @@ -366,7 +366,7 @@ class Color 200 * (1 - $lightness / $value); $value *= 100; return new HSB([ - $hsl->H, + (float)$hsl->get('H'), $saturation, $value, ]); @@ -381,9 +381,9 @@ class Color public static function hsbToHsl(HSB $hsb): HSL { // hsv/toHsl - $hue = $hsb->H; - $saturation = $hsb->S / 100; - $value = $hsb->B / 100; + $hue = (float)$hsb->get('H'); + $saturation = (float)$hsb->get('S') / 100; + $value = (float)$hsb->get('B') / 100; $lightness = $value * (1 - $saturation / 2); // check for B/W @@ -443,9 +443,9 @@ class Color { // hsv\Hwb return new HWB([ - $hsb->H, // hue, - $hsb->B * (100 - $hsb->S) / 100, // 2: brightness, 1: saturation - 100 - $hsb->B, + (float)$hsb->get('H'), // hue, + (float)$hsb->get('B') * (100 - (float)$hsb->get('S')) / 100, // 2: brightness, 1: saturation + 100 - (float)$hsb->get('B'), ]); } @@ -457,9 +457,9 @@ class Color */ public static function hwbToHsb(HWB $hwb): HSB { - $hue = $hwb->H; - $whiteness = $hwb->W / 100; - $blackness = $hwb->B / 100; + $hue = (float)$hwb->get('H'); + $whiteness = (float)$hwb->get('W') / 100; + $blackness = (float)$hwb->get('B') / 100; $sum = $whiteness + $blackness; // print "S: B/W: " . $sum . " /W: " . $whiteness . " /B: " . $blackness . "\n"; @@ -469,7 +469,7 @@ class Color $value = $whiteness / $sum * 100; } else { $value = 1 - $blackness; - $saturation = $value === 0 ? 0 : (1 - $whiteness / $value) * 100; + $saturation = $value === 0.0 ? 0 : (1 - $whiteness / $value) * 100; $value *= 100; } diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/HSB.php b/www/lib/CoreLibs/Convert/Color/Coordinates/HSB.php index 62d83a5b..1b15bd98 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/HSB.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/HSB.php @@ -124,7 +124,7 @@ class HSB implements Interface\CoordinatesInterface * @param string $name * @return float */ - public function __get(string $name): float|string|bool + public function get(string $name): float|string|bool { $name = strtoupper($name); if (!property_exists($this, $name)) { diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/HSL.php b/www/lib/CoreLibs/Convert/Color/Coordinates/HSL.php index c5198c23..1adbe5c9 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/HSL.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/HSL.php @@ -123,7 +123,7 @@ class HSL implements Interface\CoordinatesInterface * @param string $name * @return float */ - public function __get(string $name): float|string|bool + public function get(string $name): float|string|bool { if (!property_exists($this, $name)) { throw new \ErrorException('Creation of dynamic property is not allowed', 0); diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/HWB.php b/www/lib/CoreLibs/Convert/Color/Coordinates/HWB.php index dbad9861..898cf082 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/HWB.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/HWB.php @@ -123,7 +123,7 @@ class HWB implements Interface\CoordinatesInterface * @param string $name * @return float */ - public function __get(string $name): float|string|bool + public function get(string $name): float|string|bool { if (!property_exists($this, $name)) { throw new \ErrorException('Creation of dynamic property is not allowed', 0); diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/Interface/CoordinatesInterface.php b/www/lib/CoreLibs/Convert/Color/Coordinates/Interface/CoordinatesInterface.php index 72c056de..4606de42 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/Interface/CoordinatesInterface.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/Interface/CoordinatesInterface.php @@ -31,7 +31,7 @@ interface CoordinatesInterface * @param string $name * @return float */ - public function __get(string $name): float|string|bool; + public function get(string $name): float|string|bool; /** * Returns the color as array diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/LCH.php b/www/lib/CoreLibs/Convert/Color/Coordinates/LCH.php index 8f886292..ac57bdaa 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/LCH.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/LCH.php @@ -60,7 +60,7 @@ class LCH implements Interface\CoordinatesInterface * set from array * where 0: Lightness, 1: Chroma, 2: Hue * - * @param string|{0:float,1:float,2:float} $colors + * @param string|array{0:float,1:float,2:float} $colors * @param string $colorspace [default=''] * @param array $options [default=[]] * @return self @@ -145,7 +145,7 @@ class LCH implements Interface\CoordinatesInterface * @param string $name * @return float */ - public function __get(string $name): float|string|bool + public function get(string $name): float|string|bool { if (!property_exists($this, $name)) { throw new \ErrorException('Creation of dynamic property is not allowed', 0); @@ -214,9 +214,9 @@ class LCH implements Interface\CoordinatesInterface $string .= '(' . $this->L . ' ' - . $this->c + . $this->C . ' ' - . $this->h + . $this->H . Utils::setOpacity($opacity) . ');'; diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/Lab.php b/www/lib/CoreLibs/Convert/Color/Coordinates/Lab.php index d4e0b47e..3652187b 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/Lab.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/Lab.php @@ -151,7 +151,7 @@ class Lab implements Interface\CoordinatesInterface * @param string $name * @return float */ - public function __get(string $name): float|string|bool + public function get(string $name): float|string|bool { if (!property_exists($this, $name)) { throw new \ErrorException('Creation of dynamic property is not allowed', 0); diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/RGB.php b/www/lib/CoreLibs/Convert/Color/Coordinates/RGB.php index ce69e855..51d6a2b2 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/RGB.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/RGB.php @@ -111,7 +111,7 @@ class RGB implements Interface\CoordinatesInterface * @param string $name * @return float|bool */ - public function __get(string $name): float|string|bool + public function get(string $name): float|string|bool { if (!property_exists($this, $name)) { throw new \ErrorException('Creation of dynamic property is not allowed', 0); diff --git a/www/lib/CoreLibs/Convert/Color/Coordinates/XYZ.php b/www/lib/CoreLibs/Convert/Color/Coordinates/XYZ.php index 0909badb..3b0b9f9b 100644 --- a/www/lib/CoreLibs/Convert/Color/Coordinates/XYZ.php +++ b/www/lib/CoreLibs/Convert/Color/Coordinates/XYZ.php @@ -118,7 +118,7 @@ class XYZ implements Interface\CoordinatesInterface * @param string $name * @return float */ - public function __get(string $name): float|string|bool + public function get(string $name): float|string|bool { if (!property_exists($this, $name)) { throw new \ErrorException('Creation of dynamic property is not allowed', 0); diff --git a/www/lib/CoreLibs/Convert/Color/Utils.php b/www/lib/CoreLibs/Convert/Color/Utils.php index b273f001..d4f10c37 100644 --- a/www/lib/CoreLibs/Convert/Color/Utils.php +++ b/www/lib/CoreLibs/Convert/Color/Utils.php @@ -15,11 +15,11 @@ use CoreLibs\Convert\Math; class Utils { - /** @var int deviation allowed for valid data checks, small */ + /** @var float deviation allowed for valid data checks, small */ public const EPSILON_SMALL = 0.000000000001; - /** @var int deviation allowed for valid data checks, medium */ + /** @var float deviation allowed for valid data checks, medium */ public const EPSILON_MEDIUM = 0.0000001; - /** @var int deviation allowed for valid data checks, big */ + /** @var float deviation allowed for valid data checks, big */ public const ESPILON_BIG = 0.0001; public static function compare(float $lower, float $value, float $upper, float $epslion): bool diff --git a/www/lib/CoreLibs/Convert/Math.php b/www/lib/CoreLibs/Convert/Math.php index 8b8043db..a20085f7 100644 --- a/www/lib/CoreLibs/Convert/Math.php +++ b/www/lib/CoreLibs/Convert/Math.php @@ -113,7 +113,6 @@ class Math break; case '==': return self::equalWithEpsilon($value, $limit, $epsilon); - break; case '>': if ($value > ($limit + $epsilon)) { return true; @@ -148,7 +147,7 @@ class Math if (!is_array($a[0] ?? null)) { // $a is vector, convert to [[a, b, c, ...]] - $a = [ $a ]; + $a = [$a]; } if (!is_array($b[0])) { @@ -164,7 +163,7 @@ class Math // transpose $b: $bCols = array_map( callback: fn ($k) => \array_map( - (fn ($i) => $i[$k]), + (fn ($i) => is_array($i) ? $i[$k] : 0), $b, ), array: array_keys($b[0]), @@ -176,7 +175,7 @@ class Math array_reduce( array: $row, callback: fn ($a, $v, $i = null) => $a + $v * ( - $col[$i ?? array_search($v, $row)] ?? 0 + $col[$i ?? array_search($v, $row) ?: 0] ), initial: 0, ) : @@ -198,7 +197,7 @@ class Math if ($p === 1) { // Avoid [[a], [b], [c], ...]]: return array_map( - callback: fn ($v) => $v[0], + callback: fn ($v) => $v[0] ?? 0, array: $product, ); } From 497833ca71311d17dc580be908987eb068e9f744 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Fri, 15 Nov 2024 19:45:36 +0900 Subject: [PATCH 13/13] phpunit test updated to removal of __get --- .../Convert/CoreLibsConvertColorTest.php | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/4dev/tests/Convert/CoreLibsConvertColorTest.php b/4dev/tests/Convert/CoreLibsConvertColorTest.php index 543ff537..d95d51b2 100644 --- a/4dev/tests/Convert/CoreLibsConvertColorTest.php +++ b/4dev/tests/Convert/CoreLibsConvertColorTest.php @@ -353,7 +353,7 @@ final class CoreLibsConvertColorTest extends TestCase $rgb = (new Color\Coordinates\RGB([10, 20, 30]))->toLinear(); $this->assertEquals( true, - $rgb->linear, + $rgb->get('linear'), 'On create flagged linear missing' ); $rgb_color = $rgb->returnAsArray(); @@ -366,7 +366,7 @@ final class CoreLibsConvertColorTest extends TestCase $rgb->fromLinear(); $this->assertEquals( false, - $rgb->linear, + $rgb->get('linear'), 'On reverse linear, flag is missing' ); $rgb_color = $rgb->returnAsArray(); @@ -379,13 +379,13 @@ final class CoreLibsConvertColorTest extends TestCase $rgb_color = $rgb->returnAsArray(); $this->assertEquals( false, - $rgb->linear, + $rgb->get('linear'), 'On create without linear flag is linear' ); $rgb->toLinear(); $this->assertEquals( true, - $rgb->linear, + $rgb->get('linear'), 'On linear call flag is not linear' ); $rgb->fromLinear(); @@ -695,7 +695,7 @@ final class CoreLibsConvertColorTest extends TestCase $this->expectException(\ErrorException::class); $this->expectExceptionCode(0); $this->expectExceptionMessage("Creation of dynamic property is not allowed"); - $b->g; + $b->get('o'); $this->expectException(\InvalidArgumentException::class); $this->expectExceptionCode(0); @@ -755,7 +755,7 @@ final class CoreLibsConvertColorTest extends TestCase $this->expectException(\ErrorException::class); $this->expectExceptionCode(0); $this->expectExceptionMessage("Creation of dynamic property is not allowed"); - $b->g; + $b->get('o'); $this->expectException(\InvalidArgumentException::class); $this->expectExceptionCode(0); @@ -815,7 +815,7 @@ final class CoreLibsConvertColorTest extends TestCase $this->expectException(\ErrorException::class); $this->expectExceptionCode(0); $this->expectExceptionMessage("Creation of dynamic property is not allowed"); - $b->g; + $b->get('o'); $this->expectException(\InvalidArgumentException::class); $this->expectExceptionCode(0); @@ -947,7 +947,7 @@ final class CoreLibsConvertColorTest extends TestCase $this->expectException(\ErrorException::class); $this->expectExceptionCode(0); $this->expectExceptionMessage("Creation of dynamic property is not allowed"); - $b->h; + $b->get('o'); $this->expectException(\InvalidArgumentException::class); $this->expectExceptionCode(0); @@ -1100,7 +1100,7 @@ final class CoreLibsConvertColorTest extends TestCase $this->expectException(\ErrorException::class); $this->expectExceptionCode(0); $this->expectExceptionMessage("Creation of dynamic property is not allowed"); - $b->x; + $b->get('o'); $this->expectException(\InvalidArgumentException::class); $this->expectExceptionCode(0); @@ -1134,7 +1134,7 @@ final class CoreLibsConvertColorTest extends TestCase $this->expectException(\ErrorException::class); $this->expectExceptionCode(0); $this->expectExceptionMessage("Creation of dynamic property is not allowed"); - $b->x; + $b->get('o'); $this->expectException(\InvalidArgumentException::class); $this->expectExceptionCode(0); @@ -1169,7 +1169,7 @@ final class CoreLibsConvertColorTest extends TestCase $this->expectException(\ErrorException::class); $this->expectExceptionCode(0); $this->expectExceptionMessage("Creation of dynamic property is not allowed"); - $b->x; + $b->get('o'); $this->expectException(\InvalidArgumentException::class); $this->expectExceptionCode(0);