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
This commit is contained in:
Clemens Schwaighofer
2024-11-15 18:13:16 +09:00
parent 3845bc7ff5
commit a9f1d878f7
15 changed files with 1246 additions and 152 deletions

View File

@@ -72,7 +72,7 @@ print '<div><h1>' . $PAGE_NAME . '</h1></div>';
// 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) . "<br>";
$hsb = Color::hwbToHsb($hwb);
print "HSB: " . DgS::printAr($hsb) . "<br>";
$oklch = Color::rgbToOkLch(Coordinates\RGB::__constructFromArray([
$oklch = Color::rgbToOkLch(Coordinates\RGB::create([
250,
0,
0
@@ -90,7 +90,7 @@ print "OkLch: " . DgS::printAr($oklch) . "<br>";
$rgb = Color::okLchToRgb($oklch);
print "OkLch -> RGB: " . DgS::printAr($rgb) . "<br>";
$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) . "<br>";
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) . "<br>";
$rgb = Coordinates\RGB::__constructFromArray([0, 0, 0])->toLinear();
$rgb = Coordinates\RGB::create([0, 0, 0])->toLinear();
print "RGBlinear: " . DgS::printAr($rgb) . "<br>";
$cie_lab = Color::okLabToLab($oklab);
print "CieLab: " . DgS::printAr($cie_lab) . "<br>";
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) . "<br>";
print "RGB->HSB: " . DgS::printAr($hsb) . "<br>";
print "HSB->RGB: " . DgS::printAr($rgb_b) . "<br>";
$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) . "<br>";

View File

@@ -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(
[

View File

@@ -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)

View File

@@ -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<string> 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

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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();

View File

@@ -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<string> 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;
}

View File

@@ -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
*

View File

@@ -0,0 +1,56 @@
<?php
/**
* AUTHOR: Clemens Schwaighofer
* CREATED: 2024/11/14
* DESCRIPTION:
* Utils for color
*/
declare(strict_types=1);
namespace CoreLibs\Convert\Color;
use CoreLibs\Convert\Math;
class Utils
{
/** @var int deviation allowed for valid data checks, small */
public const EPSILON_SMALL = 0.000000000001;
/** @var int deviation allowed for valid data checks, medium */
public const EPSILON_MEDIUM = 0.0000001;
/** @var int 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
{
if (
Math::compareWithEpsilon($value, '<', $lower, $epslion) ||
Math::compareWithEpsilon($value, '>', $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__

View File

@@ -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.