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__