Skip to content

Commit 84ebaa9

Browse files
committed
fix: correct hsl saturation and hue
close #93 and #90
1 parent 7a21583 commit 84ebaa9

2 files changed

Lines changed: 116 additions & 4 deletions

File tree

src/FastColor.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,14 @@ export class FastColor {
227227
return this._h;
228228
}
229229

230+
/**
231+
* @deprecated should use getHSVSaturation or getHSLSaturation instead
232+
*/
230233
getSaturation(): number {
234+
return this.getHSVSaturation();
235+
}
236+
237+
getHSVSaturation(): number {
231238
if (typeof this._s === 'undefined') {
232239
const delta = this.getMax() - this.getMin();
233240
if (delta === 0) {
@@ -239,6 +246,19 @@ export class FastColor {
239246
return this._s;
240247
}
241248

249+
getHSLSaturation(): number {
250+
if (typeof this._s === 'undefined') {
251+
const delta = this.getMax() - this.getMin();
252+
if (delta === 0) {
253+
this._s = 0;
254+
} else {
255+
const l = this.getLightness();
256+
this._s = (delta/255) / (1 - Math.abs(2 * l - 1));
257+
}
258+
}
259+
return this._s;
260+
}
261+
242262
getLightness(): number {
243263
if (typeof this._l === 'undefined') {
244264
this._l = (this.getMax() + this.getMin()) / 510;
@@ -384,7 +404,7 @@ export class FastColor {
384404
toHsl(): HSL {
385405
return {
386406
h: this.getHue(),
387-
s: this.getSaturation(),
407+
s: this.getHSLSaturation(),
388408
l: this.getLightness(),
389409
a: this.a,
390410
};
@@ -393,7 +413,7 @@ export class FastColor {
393413
/** CSS support color pattern */
394414
toHslString(): string {
395415
const h = this.getHue();
396-
const s = round(this.getSaturation() * 100);
416+
const s = round(this.getHSLSaturation() * 100);
397417
const l = round(this.getLightness() * 100);
398418

399419
return this.a !== 1
@@ -405,7 +425,7 @@ export class FastColor {
405425
toHsv(): HSV {
406426
return {
407427
h: this.getHue(),
408-
s: this.getSaturation(),
428+
s: this.getHSVSaturation(),
409429
v: this.getValue(),
410430
a: this.a,
411431
};
@@ -482,7 +502,7 @@ export class FastColor {
482502
}
483503

484504
private fromHsl({ h, s, l, a }: OptionalA<HSL>): void {
485-
this._h = h % 360;
505+
this._h = h = h % 360;
486506
this._s = s;
487507
this._l = l;
488508
this.a = typeof a === 'number' ? a : 1;
@@ -492,6 +512,7 @@ export class FastColor {
492512
this.r = rgb;
493513
this.g = rgb;
494514
this.b = rgb;
515+
return;
495516
}
496517

497518
let r = 0,

tests/hsl.test.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { FastColor } from '../src';
2+
3+
describe('hsl', () => {
4+
// Unified hex<->hsl fixtures
5+
const hexHslFixtures: Array<{ hex: string; hsl: { h: number; s: number; l: number } }> = [
6+
// Boundaries
7+
{ hex: '#000000', hsl: { h: 0, s: 0, l: 0 } },
8+
{ hex: '#ffffff', hsl: { h: 0, s: 0, l: 1 } },
9+
10+
// Primaries
11+
{ hex: '#ff0000', hsl: { h: 0, s: 1, l: 0.5 } },
12+
{ hex: '#00ff00', hsl: { h: 120, s: 1, l: 0.5 } },
13+
{ hex: '#0000ff', hsl: { h: 240, s: 1, l: 0.5 } },
14+
15+
// Secondaries
16+
{ hex: '#ffff00', hsl: { h: 60, s: 1, l: 0.5 } },
17+
{ hex: '#00ffff', hsl: { h: 180, s: 1, l: 0.5 } },
18+
{ hex: '#ff00ff', hsl: { h: 300, s: 1, l: 0.5 } },
19+
20+
// Specific samples
21+
{ hex: '#2400c2', hsl: { h: 251, s: 1, l: 0.3804 } },
22+
{ hex: '#3d5dff', hsl: { h: 230, s: 1, l: 0.6196 } },
23+
];
24+
25+
// hex -> hsl object values and roundtrip
26+
hexHslFixtures.forEach(({ hex, hsl: expected }) => {
27+
it(`hex to hsl object values: ${hex}`, () => {
28+
const hsl = new FastColor(hex).toHsl();
29+
expect(hsl.h).toBe(expected.h);
30+
expect(hsl.s).toBe(expected.s);
31+
expect(hsl.l).toBeCloseTo(expected.l, 4);
32+
expect(hsl.a).toBe(1);
33+
34+
const back = new FastColor(hsl).toHexString();
35+
expect(back).toBe(hex);
36+
});
37+
});
38+
39+
it('setHue should not change lightness', () => {
40+
const base = new FastColor('#1677ff');
41+
expect(base.getLightness()).toBeCloseTo(new FastColor('#1677ff').getLightness(), 4);
42+
43+
const turn = base.setHue(233);
44+
expect(turn.getLightness()).toBeCloseTo(base.getLightness(), 4);
45+
});
46+
47+
const hslaAlphaCases: Array<[string, string, number]> = [
48+
['hsla(251, 100%, 38%, 0.5)', 'hsla(251,100%,38%,0.5)', 0.5],
49+
['hsla(120, 25%, 33%, 0.7)', 'hsla(120,25%,33%,0.7)', 0.7],
50+
];
51+
52+
hslaAlphaCases.forEach(([str, normalized, alpha]) => {
53+
it(`supports hsla alpha: ${str}`, () => {
54+
expect(new FastColor(str).toHslString()).toBe(normalized);
55+
expect(new FastColor(str).toRgb().a).toBe(alpha);
56+
});
57+
});
58+
59+
it('hue 0 and 360 are equivalent', () => {
60+
const c0 = new FastColor('hsl(0, 100%, 50%)');
61+
const c360 = new FastColor('hsl(360, 100%, 50%)');
62+
expect(c0.toHexString()).toBe(c360.toHexString());
63+
});
64+
65+
it('s=0 yields grayscale regardless of hue', () => {
66+
const a = new FastColor('hsl(0, 0%, 40%)');
67+
const b = new FastColor('hsl(200, 0%, 40%)');
68+
expect(a.toHexString()).toBe(b.toHexString());
69+
});
70+
71+
it('roundtrip toHslString keeps values', () => {
72+
const c = new FastColor('hsla(120, 25%, 33%, 0.7)');
73+
expect(c.toHslString()).toBe('hsla(120,25%,33%,0.7)');
74+
const parsed = new FastColor(c.toHslString());
75+
expect(parsed.toHslString()).toBe('hsla(120,25%,33%,0.7)');
76+
});
77+
78+
it('darken and lighten adjust lightness bounds', () => {
79+
const c = new FastColor('hsl(200, 50%, 50%)');
80+
const darker = c.darken(20);
81+
const lighter = c.lighten(20);
82+
expect(darker.getLightness()).toBeCloseTo(c.getLightness() - 0.2, 4);
83+
expect(lighter.getLightness()).toBeCloseTo(c.getLightness() + 0.2, 4);
84+
85+
const minCap = c.darken(100);
86+
const maxCap = c.lighten(100);
87+
expect(minCap.getLightness()).toBe(0);
88+
expect(maxCap.getLightness()).toBe(1);
89+
});
90+
});
91+

0 commit comments

Comments
 (0)