Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 0 additions & 18 deletions lib/private/Color.php

This file was deleted.

102 changes: 69 additions & 33 deletions lib/public/Color.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
Expand All @@ -8,20 +9,21 @@

/**
* Simple RGB color container
*
* @since 25.0.0
*/
class Color {
private int $r;
private int $g;
private int $b;

/**
* @since 25.0.0
*/
public function __construct($r, $g, $b) {
$this->r = $r;
$this->g = $g;
$this->b = $b;
public function __construct(
private int $r,
private int $g,
private int $b,
) {
self::assertChannelInRange($this->r, 'r');
self::assertChannelInRange($this->g, 'g');
self::assertChannelInRange($this->b, 'b');
}

/**
Expand Down Expand Up @@ -61,7 +63,7 @@ public function greenF(): float {
}

/**
* Returns the green 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
*/
Expand All @@ -75,65 +77,99 @@ public function blue(): int {
* @since 25.0.0
*/
public function blueF(): float {
return $this->g / 255;
return $this->b / 255;
}

/**
* 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
*/
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<Color>
* @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<Color> 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 {
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];
$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.
*
* An opacity of 0 returns $source, and 1 returns this color.
*
* @return Color The new 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 {
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()),
(int)((1 - $opacity) * $source->green() + $opacity * $this->green()),
(int)((1 - $opacity) * $source->blue() + $opacity * $this->blue())
);
}

/**
* 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;
}

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,
));
}
}
}
6 changes: 3 additions & 3 deletions tests/lib/Avatar/UserAvatarTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
$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));
}
Expand Down
Loading