From 46860dbb2488b3428ff2e362b58f65bae2c556c1 Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 19 Apr 2026 18:41:38 -0400 Subject: [PATCH 1/6] fix(Color): blueF() returning green color component for blue Signed-off-by: Josh --- lib/public/Color.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/public/Color.php b/lib/public/Color.php index ed955b8f05618..30ea85c257700 100644 --- a/lib/public/Color.php +++ b/lib/public/Color.php @@ -61,7 +61,7 @@ public function greenF(): float { } /** - * Returns the green blue component of this color as an int from 0 to 255 + * Returns the blue component of this color as an int from 0 to 255 * * @since 25.0.0 */ @@ -75,7 +75,7 @@ public function blue(): int { * @since 25.0.0 */ public function blueF(): float { - return $this->g / 255; + return $this->b / 255; } /** From 42e5920474a2ae16865b312dfb851784c8708f29 Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 19 Apr 2026 18:45:49 -0400 Subject: [PATCH 2/6] refactor(Color): drop dead code in OC already replaced by OCP See #32973 Signed-off-by: Josh --- lib/private/Color.php | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 lib/private/Color.php diff --git a/lib/private/Color.php b/lib/private/Color.php deleted file mode 100644 index 7b8024891988b..0000000000000 --- a/lib/private/Color.php +++ /dev/null @@ -1,18 +0,0 @@ - Date: Sun, 19 Apr 2026 20:52:01 -0400 Subject: [PATCH 3/6] refactor(Color): add stronger typing and update inaccurate comments Signed-off-by: Josh --- lib/public/Color.php | 78 ++++++++++++++++++++++++++------------------ 1 file changed, 46 insertions(+), 32 deletions(-) diff --git a/lib/public/Color.php b/lib/public/Color.php index 30ea85c257700..f8c0514e26cdb 100644 --- a/lib/public/Color.php +++ b/lib/public/Color.php @@ -1,5 +1,6 @@ r = $r; - $this->g = $g; - $this->b = $b; + public function __construct( + private int $r, + private int $g, + private int $b, + ) { } /** @@ -61,7 +60,7 @@ public function greenF(): float { } /** - * Returns the blue component of this color as an int from 0 to 255 + * Returns the blue color component of this color as an int from 0 to 255 * * @since 25.0.0 */ @@ -79,7 +78,7 @@ public function blueF(): float { } /** - * Returns the name of the color in the format "#RRGGBB"; i.e. a "#" character followed by three two-digit hexadecimal numbers. + * Returns the hex triplet color value as a string ("#RRGGBB") * * @since 25.0.0 */ @@ -87,35 +86,46 @@ public function name(): string { return sprintf('#%02x%02x%02x', $this->r, $this->g, $this->b); } + // Utility Functions + /** - * Mix two colors + * Generate a progression of colors starting with $color1 and moving toward $color2. * - * @param int $steps the number of intermediate colors that should be generated for the palette - * @param Color $color1 the first color - * @param Color $color2 the second color - * @return list + * @param int $steps Total number of colors to return (including $color1, but excluding $color2); should be at least 2 + * @param Color $color1 The starting color (index 0 of the returned list) + * @param Color $color2 The target color used to calculate the transition + * @return list The list of colors starting with $color1 up to but not including $color2 * @since 25.0.0 */ public static function mixPalette(int $steps, Color $color1, Color $color2): array { $palette = [$color1]; - $step = self::stepCalc($steps, [$color1, $color2]); + [$rDelta, $gDelta, $bDelta] = self::calculateDeltas($steps, $color1, $color2); + for ($i = 1; $i < $steps; $i++) { - $r = intval($color1->red() + ($step[0] * $i)); - $g = intval($color1->green() + ($step[1] * $i)); - $b = intval($color1->blue() + ($step[2] * $i)); - $palette[] = new Color($r, $g, $b); + $palette[] = new Color( + // TODO: Consider using round() instead of (int) truncation for more accurate color transitions. + (int)($color1->red() + ($rDelta * $i)), + (int)($color1->green() + ($gDelta * $i)), + (int)($color1->blue() + ($bDelta * $i)), + ); } + return $palette; } /** - * Alpha blend another color with a given opacity to this color + * Blend this color over a source color. * - * @return Color The new color + * An opacity of 0 returns $source, and 1 returns this color. + * + * @param float $opacity Opacity of this color, expected in the range 0.0 to 1.0 + * @param Color $source The source/background color + * @return Color The blended color * @since 25.0.0 */ public function alphaBlending(float $opacity, Color $source): Color { return new Color( + // TODO: Consider using round() instead of (int) truncation for more accurate color transitions. (int)((1 - $opacity) * $source->red() + $opacity * $this->red()), (int)((1 - $opacity) * $source->green() + $opacity * $this->green()), (int)((1 - $opacity) * $source->blue() + $opacity * $this->blue()) @@ -123,17 +133,21 @@ public function alphaBlending(float $opacity, Color $source): Color { } /** - * Calculate steps between two Colors - * @param int $steps start color - * @param Color[] $ends end color - * @return array{0: float, 1: float, 2: float} [r,g,b] steps for each color to go from $steps to $ends + * Calculate the per-channel change (RGB deltas) required to transition between two colors. + * + * @param int $count The number of intervals to divide the transition into >0 + * @param Color $start The starting color + * @param Color $end The target color + * @return array{0: float, 1: float, 2: float} The per-channel [r, g, b] increment required for each interval * @since 25.0.0 */ - private static function stepCalc(int $steps, array $ends): array { - $step = []; - $step[0] = ($ends[1]->red() - $ends[0]->red()) / $steps; - $step[1] = ($ends[1]->green() - $ends[0]->green()) / $steps; - $step[2] = ($ends[1]->blue() - $ends[0]->blue()) / $steps; - return $step; + private static function calculateDeltas(int $count, Color $start, Color $end): array { + $deltas = []; + + $deltas[0] = ($end->red() - $start->red()) / $count; + $deltas[1] = ($end->green() - $start->green()) / $count; + $deltas[2] = ($end->blue() - $start->blue()) / $count; + + return $deltas; } } From 53b846e9d85ff3b2bb67ca3fcdde39e816f49a07 Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 19 Apr 2026 21:06:14 -0400 Subject: [PATCH 4/6] chore(Color): add guards for contract requirements Signed-off-by: Josh --- lib/public/Color.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lib/public/Color.php b/lib/public/Color.php index f8c0514e26cdb..55064c2c8c7b0 100644 --- a/lib/public/Color.php +++ b/lib/public/Color.php @@ -21,6 +21,9 @@ public function __construct( private int $g, private int $b, ) { + self::assertChannelInRange($this->r, 'r'); + self::assertChannelInRange($this->g, 'g'); + self::assertChannelInRange($this->b, 'b'); } /** @@ -98,6 +101,11 @@ public function name(): string { * @since 25.0.0 */ public static function mixPalette(int $steps, Color $color1, Color $color2): array { + if ($steps < 1) { + // 1 is a hard requirement; 2 is a practical requirement + throw new \InvalidArgumentException('Palette steps must be at least 1 (and should be at least 2).'); + } + $palette = [$color1]; [$rDelta, $gDelta, $bDelta] = self::calculateDeltas($steps, $color1, $color2); @@ -124,6 +132,10 @@ public static function mixPalette(int $steps, Color $color1, Color $color2): arr * @since 25.0.0 */ public function alphaBlending(float $opacity, Color $source): Color { + if ($opacity < 0.0 || $opacity > 1.0) { + throw new \InvalidArgumentException('Opacity must be between 0.0 and 1.0.'); + } + return new Color( // TODO: Consider using round() instead of (int) truncation for more accurate color transitions. (int)((1 - $opacity) * $source->red() + $opacity * $this->red()), @@ -150,4 +162,14 @@ private static function calculateDeltas(int $count, Color $start, Color $end): a return $deltas; } + + private static function assertChannelInRange(int $value, string $channel): void { + if ($value < 0 || $value > 255) { + throw new \InvalidArgumentException(sprintf( + 'Color channel "%s" must be between 0 and 255, got %d.', + $channel, + $value, + )); + } + } } From 9f7d3ecf64f612774502e1f0310cdd8c9af1c9b7 Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 19 Apr 2026 21:07:43 -0400 Subject: [PATCH 5/6] test(UserAvatar): cast color progressions to int to match contract Signed-off-by: Josh --- tests/lib/Avatar/UserAvatarTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/lib/Avatar/UserAvatarTest.php b/tests/lib/Avatar/UserAvatarTest.php index 0bd4637fc4736..9f2bdf3015f15 100644 --- a/tests/lib/Avatar/UserAvatarTest.php +++ b/tests/lib/Avatar/UserAvatarTest.php @@ -263,9 +263,9 @@ public function testMixPalette(): void { $palette = Color::mixPalette($steps, $colorFrom, $colorTo); foreach ($palette as $j => $color) { // calc increment - $incR = $colorTo->red() / $steps * $j; - $incG = $colorTo->green() / $steps * $j; - $incB = $colorTo->blue() / $steps * $j; + (int)$incR = $colorTo->red() / $steps * $j; + (int)$incG = $colorTo->green() / $steps * $j; + (int)$incB = $colorTo->blue() / $steps * $j; // ensure everything is equal $this->assertEquals($color, new Color($incR, $incG, $incB)); } From d123a3824f241e44aea13b047b692f210031cb9d Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 19 Apr 2026 21:13:22 -0400 Subject: [PATCH 6/6] test(UserAvatar): fixup casting typos/oops Signed-off-by: Josh --- tests/lib/Avatar/UserAvatarTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/lib/Avatar/UserAvatarTest.php b/tests/lib/Avatar/UserAvatarTest.php index 9f2bdf3015f15..acbc9488e8c93 100644 --- a/tests/lib/Avatar/UserAvatarTest.php +++ b/tests/lib/Avatar/UserAvatarTest.php @@ -263,9 +263,9 @@ public function testMixPalette(): void { $palette = Color::mixPalette($steps, $colorFrom, $colorTo); foreach ($palette as $j => $color) { // calc increment - (int)$incR = $colorTo->red() / $steps * $j; - (int)$incG = $colorTo->green() / $steps * $j; - (int)$incB = $colorTo->blue() / $steps * $j; + $incR = (int)($colorTo->red() / $steps * $j); + $incG = (int)($colorTo->green() / $steps * $j); + $incB = (int)($colorTo->blue() / $steps * $j); // ensure everything is equal $this->assertEquals($color, new Color($incR, $incG, $incB)); }