11<?php
22
3+ declare (strict_types=1 );
34/**
45 * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
56 * SPDX-License-Identifier: AGPL-3.0-or-later
910
1011/**
1112 * Simple RGB color container
13+ *
1214 * @since 25.0.0
1315 */
1416class Color {
15- private int $ r ;
16- private int $ g ;
17- private int $ b ;
18-
1917 /**
2018 * @since 25.0.0
2119 */
22- public function __construct ($ r , $ g , $ b ) {
23- $ this ->r = $ r ;
24- $ this ->g = $ g ;
25- $ this ->b = $ b ;
20+ public function __construct (
21+ private int $ r ,
22+ private int $ g ,
23+ private int $ b ,
24+ ) {
25+ self ::assertChannelInRange ($ this ->r , 'r ' );
26+ self ::assertChannelInRange ($ this ->g , 'g ' );
27+ self ::assertChannelInRange ($ this ->b , 'b ' );
2628 }
2729
2830 /**
@@ -62,7 +64,7 @@ public function greenF(): float {
6264 }
6365
6466 /**
65- * Returns the green blue component of this color as an int from 0 to 255
67+ * Returns the blue color component of this color as an int from 0 to 255
6668 *
6769 * @since 25.0.0
6870 */
@@ -76,65 +78,99 @@ public function blue(): int {
7678 * @since 25.0.0
7779 */
7880 public function blueF (): float {
79- return $ this ->g / 255 ;
81+ return $ this ->b / 255 ;
8082 }
8183
8284 /**
83- * Returns the name of the color in the format "#RRGGBB"; i.e. a "#" character followed by three two-digit hexadecimal numbers.
85+ * Returns the hex triplet color value as a string ( "#RRGGBB")
8486 *
8587 * @since 25.0.0
8688 */
8789 public function name (): string {
8890 return sprintf ('#%02x%02x%02x ' , $ this ->r , $ this ->g , $ this ->b );
8991 }
9092
93+ // Utility Functions
94+
9195 /**
92- * Mix two colors
96+ * Generate a progression of colors starting with $color1 and moving toward $color2.
9397 *
94- * @param int $steps the number of intermediate colors that should be generated for the palette
95- * @param Color $color1 the first color
96- * @param Color $color2 the second color
97- * @return list<Color>
98+ * @param int $steps Total number of colors to return (including $color1, but excluding $color2); should be at least 2
99+ * @param Color $color1 The starting color (index 0 of the returned list)
100+ * @param Color $color2 The target color used to calculate the transition
101+ * @return list<Color> The list of colors starting with $color1 up to but not including $color2
98102 * @since 25.0.0
99103 */
100104 public static function mixPalette (int $ steps , Color $ color1 , Color $ color2 ): array {
105+ if ($ steps < 1 ) {
106+ // 1 is a hard requirement; 2 is a practical requirement
107+ throw new \InvalidArgumentException ('Palette steps must be at least 1 (and should be at least 2). ' );
108+ }
109+
101110 $ palette = [$ color1 ];
102- $ step = self ::stepCalc ($ steps , [$ color1 , $ color2 ]);
111+ [$ rDelta , $ gDelta , $ bDelta ] = self ::calculateDeltas ($ steps , $ color1 , $ color2 );
112+
103113 for ($ i = 1 ; $ i < $ steps ; $ i ++) {
104- $ r = intval ($ color1 ->red () + ($ step [0 ] * $ i ));
105- $ g = intval ($ color1 ->green () + ($ step [1 ] * $ i ));
106- $ b = intval ($ color1 ->blue () + ($ step [2 ] * $ i ));
107- $ palette [] = new Color ($ r , $ g , $ b );
114+ $ palette [] = new Color (
115+ // TODO: Consider using round() instead of (int) truncation for more accurate color transitions.
116+ (int )($ color1 ->red () + ($ rDelta * $ i )),
117+ (int )($ color1 ->green () + ($ gDelta * $ i )),
118+ (int )($ color1 ->blue () + ($ bDelta * $ i )),
119+ );
108120 }
121+
109122 return $ palette ;
110123 }
111124
112125 /**
113- * Alpha blend another color with a given opacity to this color
126+ * Blend this color over a source color.
127+ *
128+ * An opacity of 0 returns $source, and 1 returns this color.
114129 *
115- * @return Color The new color
130+ * @param float $opacity Opacity of this color, expected in the range 0.0 to 1.0
131+ * @param Color $source The source/background color
132+ * @return Color The blended color
116133 * @since 25.0.0
117134 */
118135 public function alphaBlending (float $ opacity , Color $ source ): Color {
136+ if ($ opacity < 0.0 || $ opacity > 1.0 ) {
137+ throw new \InvalidArgumentException ('Opacity must be between 0.0 and 1.0. ' );
138+ }
139+
119140 return new Color (
141+ // TODO: Consider using round() instead of (int) truncation for more accurate color transitions.
120142 (int )((1 - $ opacity ) * $ source ->red () + $ opacity * $ this ->red ()),
121143 (int )((1 - $ opacity ) * $ source ->green () + $ opacity * $ this ->green ()),
122144 (int )((1 - $ opacity ) * $ source ->blue () + $ opacity * $ this ->blue ())
123145 );
124146 }
125147
126148 /**
127- * Calculate steps between two Colors
128- * @param int $steps start color
129- * @param Color[] $ends end color
130- * @return array{0: float, 1: float, 2: float} [r,g,b] steps for each color to go from $steps to $ends
149+ * Calculate the per-channel change (RGB deltas) required to transition between two colors.
150+ *
151+ * @param int $count The number of intervals to divide the transition into >0
152+ * @param Color $start The starting color
153+ * @param Color $end The target color
154+ * @return array{0: float, 1: float, 2: float} The per-channel [r, g, b] increment required for each interval
131155 * @since 25.0.0
132156 */
133- private static function stepCalc (int $ steps , array $ ends ): array {
134- $ step = [];
135- $ step [0 ] = ($ ends [1 ]->red () - $ ends [0 ]->red ()) / $ steps ;
136- $ step [1 ] = ($ ends [1 ]->green () - $ ends [0 ]->green ()) / $ steps ;
137- $ step [2 ] = ($ ends [1 ]->blue () - $ ends [0 ]->blue ()) / $ steps ;
138- return $ step ;
157+ private static function calculateDeltas (int $ count , Color $ start , Color $ end ): array {
158+ $ deltas = [];
159+
160+ $ deltas [0 ] = ($ end ->red () - $ start ->red ()) / $ count ;
161+ $ deltas [1 ] = ($ end ->green () - $ start ->green ()) / $ count ;
162+ $ deltas [2 ] = ($ end ->blue () - $ start ->blue ()) / $ count ;
163+
164+ return $ deltas ;
165+ }
166+
167+ private static function assertChannelInRange (int $ value , string $ channel ): void {
168+ if ($ value < 0 || $ value > 255 ) {
169+ throw new \InvalidArgumentException (sprintf (
170+ 'Color channel "%s" must be between 0 and 255, got %d. ' ,
171+ $ channel ,
172+ $ value ,
173+ ));
174+ }
139175 }
140176}
0 commit comments