Skip to content

Commit 5214cd2

Browse files
committed
Added gradient method for generating color scales between two or more colors (#50, #51)
1 parent 8a5e2fa commit 5214cd2

4 files changed

Lines changed: 261 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@
22

33
All Notable changes to `iris` will be documented in this file.
44

5+
## 2025-12-05
6+
7+
### Added
8+
9+
- `gradient()` method to generate color scales between two or more colors (#51)
10+
- Supports two-color gradients: `$colorA->gradient($colorB, $steps)`
11+
- Supports multi-color gradients with pivot colors: `$colorA->gradient([$colorB, $colorC], $steps)`
12+
- Works with all color types (Hex, RGB, HSL, HSV, CMYK, OKLCH, etc.)
13+
- Returns array of colors matching the starting color type
14+
15+
### Fixed
16+
17+
- CMYK to RGB conversion now rounds values to produce valid integers (#52)
18+
- Previously, CMYK values that didn't produce clean 0 or 255 RGB values would fail validation
19+
520
## 2025-12-02
621

722
### Added

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,27 @@ echo $hex->isLight(); // false
337337
echo $hex->isDark(); // true
338338
```
339339

340+
#### Gradient
341+
342+
Generate an array of colors (gradient) between two or more colors.
343+
344+
``` php
345+
use OzdemirBurak\Iris\Color\Hex;
346+
use OzdemirBurak\Iris\Color\Rgb;
347+
348+
// Two-color gradient: 5 steps from red to blue
349+
$gradient = (new Hex('#ff0000'))->gradient(new Hex('#0000ff'), 5);
350+
// ['#ff0000', '#bf0040', '#800080', '#4000bf', '#0000ff']
351+
352+
// Multi-color gradient with pivot: red → yellow → green in 7 steps
353+
$gradient = (new Hex('#ff0000'))->gradient([new Hex('#ffff00'), new Hex('#00ff00')], 7);
354+
// ['#ff0000', '#ff5500', '#ffaa00', '#ffff00', '#aaff00', '#55ff00', '#00ff00']
355+
356+
// Works with any color type - output matches the starting color type
357+
$gradient = (new Rgb('255,0,0'))->gradient(new Rgb('0,0,255'), 3);
358+
// [rgb(255,0,0), rgb(128,0,128), rgb(0,0,255)]
359+
```
360+
340361
## Change log
341362

342363
Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently.

src/BaseColor.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,59 @@ public function shade($percent = 50)
232232
return $this->mix($black, $percent);
233233
}
234234

235+
/**
236+
* Generate a gradient (array of colors) between this color and one or more target colors.
237+
*
238+
* @param \OzdemirBurak\Iris\BaseColor|array $colors Target color(s) - single color or array of colors
239+
* @param int $steps Number of colors to generate (including start and end)
240+
*
241+
* @return array Array of colors of the same type as the starting color
242+
*/
243+
public function gradient(BaseColor|array $colors, int $steps = 10): array
244+
{
245+
if ($steps < 2) {
246+
return [$this->clone()];
247+
}
248+
$colorArray = is_array($colors) ? $colors : [$colors];
249+
array_unshift($colorArray, $this);
250+
$totalColors = count($colorArray);
251+
if ($totalColors === 2) {
252+
return $this->generateGradientBetweenTwo($colorArray[0], $colorArray[1], $steps);
253+
}
254+
$segments = $totalColors - 1;
255+
$stepsPerSegment = (int) floor(($steps - 1) / $segments);
256+
$remainder = ($steps - 1) % $segments;
257+
$result = [];
258+
for ($i = 0; $i < $segments; $i++) {
259+
$segmentSteps = $stepsPerSegment + ($i < $remainder ? 1 : 0) + 1;
260+
$segmentColors = $this->generateGradientBetweenTwo($colorArray[$i], $colorArray[$i + 1], $segmentSteps);
261+
if ($i > 0) {
262+
array_shift($segmentColors);
263+
}
264+
$result = array_merge($result, $segmentColors);
265+
}
266+
return $result;
267+
}
268+
269+
/**
270+
* Generate gradient between exactly two colors.
271+
*
272+
* @param \OzdemirBurak\Iris\BaseColor $start
273+
* @param \OzdemirBurak\Iris\BaseColor $end
274+
* @param int $steps
275+
*
276+
* @return array
277+
*/
278+
protected function generateGradientBetweenTwo(BaseColor $start, BaseColor $end, int $steps): array
279+
{
280+
$result = [];
281+
for ($i = 0; $i < $steps; $i++) {
282+
$percent = ($steps > 1) ? ($i / ($steps - 1)) * 100 : 0;
283+
$result[] = $start->clone()->mix($end, $percent)->back($this);
284+
}
285+
return $result;
286+
}
287+
235288
/**
236289
* @link https://github.com/less/less.js/blob/master/packages/less/src/less/functions/color.js
237290
*

tests/OperationsTest.php

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22

33
namespace OzdemirBurak\Iris\Tests;
44

5+
use OzdemirBurak\Iris\Color\Cmyk;
56
use OzdemirBurak\Iris\Color\Hex;
67
use OzdemirBurak\Iris\Color\Hsl;
78
use OzdemirBurak\Iris\Color\Hsla;
9+
use OzdemirBurak\Iris\Color\Hsv;
10+
use OzdemirBurak\Iris\Color\Oklch;
811
use OzdemirBurak\Iris\Color\Rgb;
912
use OzdemirBurak\Iris\Color\Rgba;
1013
use PHPUnit\Framework\Attributes\Group;
@@ -97,4 +100,173 @@ public function testFadeOut()
97100
$this->assertEquals('hsla(90,90%,50%,0.2)', (string) (new Hsla('90,90,50,0.3'))->fadeOut(10));
98101
$this->assertEquals('rgba(128,242,13,0.2)', (string) (new Rgba('128,242,13,0.3'))->fadeOut(10));
99102
}
103+
104+
#[Group('operations-gradient')]
105+
public function testGradientTwoColors()
106+
{
107+
$gradient = (new Hex('#000000'))->gradient(new Hex('#ffffff'), 5);
108+
109+
$this->assertCount(5, $gradient);
110+
$this->assertEquals('#000000', (string) $gradient[0]);
111+
$this->assertEquals('#404040', (string) $gradient[1]);
112+
$this->assertEquals('#808080', (string) $gradient[2]);
113+
$this->assertEquals('#bfbfbf', (string) $gradient[3]);
114+
$this->assertEquals('#ffffff', (string) $gradient[4]);
115+
}
116+
117+
#[Group('operations-gradient')]
118+
public function testGradientMultipleColors()
119+
{
120+
$gradient = (new Hex('#ff0000'))->gradient([new Hex('#00ff00'), new Hex('#0000ff')], 5);
121+
122+
$this->assertCount(5, $gradient);
123+
$this->assertEquals('#ff0000', (string) $gradient[0]);
124+
$this->assertEquals('#808000', (string) $gradient[1]);
125+
$this->assertEquals('#00ff00', (string) $gradient[2]);
126+
$this->assertEquals('#008080', (string) $gradient[3]);
127+
$this->assertEquals('#0000ff', (string) $gradient[4]);
128+
}
129+
130+
#[Group('operations-gradient')]
131+
public function testGradientPreservesColorType()
132+
{
133+
$gradient = (new Rgb('255,0,0'))->gradient(new Rgb('0,0,255'), 3);
134+
135+
$this->assertCount(3, $gradient);
136+
$this->assertInstanceOf(Rgb::class, $gradient[0]);
137+
$this->assertInstanceOf(Rgb::class, $gradient[1]);
138+
$this->assertInstanceOf(Rgb::class, $gradient[2]);
139+
$this->assertEquals('rgb(255,0,0)', (string) $gradient[0]);
140+
$this->assertEquals('rgb(128,0,128)', (string) $gradient[1]);
141+
$this->assertEquals('rgb(0,0,255)', (string) $gradient[2]);
142+
}
143+
144+
#[Group('operations-gradient')]
145+
public function testGradientMinimumSteps()
146+
{
147+
$gradient = (new Hex('#000000'))->gradient(new Hex('#ffffff'), 1);
148+
$this->assertCount(1, $gradient);
149+
$this->assertEquals('#000000', (string) $gradient[0]);
150+
}
151+
152+
#[Group('operations-gradient')]
153+
public function testGradientTwoSteps()
154+
{
155+
$gradient = (new Hex('#000000'))->gradient(new Hex('#ffffff'), 2);
156+
$this->assertCount(2, $gradient);
157+
$this->assertEquals('#000000', (string) $gradient[0]);
158+
$this->assertEquals('#ffffff', (string) $gradient[1]);
159+
}
160+
161+
#[Group('operations-gradient')]
162+
public function testGradientSameColor()
163+
{
164+
$gradient = (new Hex('#ff0000'))->gradient(new Hex('#ff0000'), 5);
165+
$this->assertCount(5, $gradient);
166+
foreach ($gradient as $color) {
167+
$this->assertEquals('#ff0000', (string) $color);
168+
}
169+
}
170+
171+
#[Group('operations-gradient')]
172+
public function testGradientWithHsl()
173+
{
174+
$gradient = (new Hsl('0,100%,50%'))->gradient(new Hsl('240,100%,50%'), 3);
175+
$this->assertCount(3, $gradient);
176+
$this->assertInstanceOf(Hsl::class, $gradient[0]);
177+
$this->assertInstanceOf(Hsl::class, $gradient[1]);
178+
$this->assertInstanceOf(Hsl::class, $gradient[2]);
179+
}
180+
181+
#[Group('operations-gradient')]
182+
public function testGradientWithHsv()
183+
{
184+
$gradient = (new Hsv('0,100%,100%'))->gradient(new Hsv('240,100%,100%'), 3);
185+
$this->assertCount(3, $gradient);
186+
$this->assertInstanceOf(Hsv::class, $gradient[0]);
187+
$this->assertInstanceOf(Hsv::class, $gradient[1]);
188+
$this->assertInstanceOf(Hsv::class, $gradient[2]);
189+
}
190+
191+
#[Group('operations-gradient')]
192+
public function testGradientWithCmyk()
193+
{
194+
$gradient = (new Cmyk('0,100,100,0'))->gradient(new Cmyk('100,0,0,0'), 3);
195+
$this->assertCount(3, $gradient);
196+
$this->assertInstanceOf(Cmyk::class, $gradient[0]);
197+
$this->assertInstanceOf(Cmyk::class, $gradient[1]);
198+
$this->assertInstanceOf(Cmyk::class, $gradient[2]);
199+
}
200+
201+
#[Group('operations-gradient')]
202+
public function testGradientWithOklch()
203+
{
204+
$gradient = (new Oklch('0.63 0.26 29'))->gradient(new Oklch('0.45 0.31 264'), 3);
205+
$this->assertCount(3, $gradient);
206+
$this->assertInstanceOf(Oklch::class, $gradient[0]);
207+
$this->assertInstanceOf(Oklch::class, $gradient[1]);
208+
$this->assertInstanceOf(Oklch::class, $gradient[2]);
209+
}
210+
211+
#[Group('operations-gradient')]
212+
public function testGradientWithRgba()
213+
{
214+
$gradient = (new Rgba('255,0,0,0.5'))->gradient(new Rgba('0,0,255,1.0'), 3);
215+
$this->assertCount(3, $gradient);
216+
$this->assertInstanceOf(Rgba::class, $gradient[0]);
217+
$this->assertInstanceOf(Rgba::class, $gradient[1]);
218+
$this->assertInstanceOf(Rgba::class, $gradient[2]);
219+
}
220+
221+
#[Group('operations-gradient')]
222+
public function testGradientWithHsla()
223+
{
224+
$gradient = (new Hsla('0,100%,50%,0.5'))->gradient(new Hsla('240,100%,50%,1.0'), 3);
225+
$this->assertCount(3, $gradient);
226+
$this->assertInstanceOf(Hsla::class, $gradient[0]);
227+
$this->assertInstanceOf(Hsla::class, $gradient[1]);
228+
$this->assertInstanceOf(Hsla::class, $gradient[2]);
229+
}
230+
231+
#[Group('operations-gradient')]
232+
public function testGradientFourColors()
233+
{
234+
$gradient = (new Hex('#ff0000'))->gradient([
235+
new Hex('#ffff00'),
236+
new Hex('#00ff00'),
237+
new Hex('#0000ff')
238+
], 7);
239+
240+
$this->assertCount(7, $gradient);
241+
$this->assertEquals('#ff0000', (string) $gradient[0]);
242+
$this->assertEquals('#0000ff', (string) $gradient[6]);
243+
}
244+
245+
#[Group('operations-gradient')]
246+
public function testGradientDefaultSteps()
247+
{
248+
$gradient = (new Hex('#000000'))->gradient(new Hex('#ffffff'));
249+
$this->assertCount(10, $gradient);
250+
}
251+
252+
#[Group('operations-gradient')]
253+
public function testGradientDoesNotMutateOriginal()
254+
{
255+
$start = new Hex('#ff0000');
256+
$end = new Hex('#0000ff');
257+
258+
$gradient = $start->gradient($end, 5);
259+
260+
$this->assertEquals('#ff0000', (string) $start);
261+
$this->assertEquals('#0000ff', (string) $end);
262+
}
263+
264+
#[Group('operations-gradient')]
265+
public function testGradientLargeSteps()
266+
{
267+
$gradient = (new Hex('#000000'))->gradient(new Hex('#ffffff'), 100);
268+
$this->assertCount(100, $gradient);
269+
$this->assertEquals('#000000', (string) $gradient[0]);
270+
$this->assertEquals('#ffffff', (string) $gradient[99]);
271+
}
100272
}

0 commit comments

Comments
 (0)