@versatiles/style 5.2.6 → 5.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +274 -14
- package/dist/index.js +3650 -11
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/color/abstract.ts +83 -0
- package/src/color/hsl.test.ts +182 -0
- package/src/color/hsl.ts +122 -0
- package/src/color/hsv.test.ts +174 -0
- package/src/color/hsv.ts +100 -0
- package/src/color/index.test.ts +119 -0
- package/src/color/index.ts +38 -0
- package/src/color/random.test.ts +35 -0
- package/src/color/random.ts +165 -0
- package/src/color/rgb.test.ts +227 -0
- package/src/color/rgb.ts +248 -0
- package/src/color/utils.test.ts +86 -0
- package/src/color/utils.ts +13 -0
- package/src/guess_style/guess_style.test.ts +134 -0
- package/src/guess_style/guess_style.ts +166 -0
- package/{dist/guess_style/index.d.ts → src/guess_style/index.ts} +1 -0
- package/src/index.test.ts +77 -0
- package/src/index.ts +18 -0
- package/src/lib/utils.test.ts +197 -0
- package/src/lib/utils.ts +134 -0
- package/{dist/shortbread/index.d.ts → src/shortbread/index.ts} +1 -0
- package/src/shortbread/layers.test.ts +36 -0
- package/src/shortbread/layers.ts +564 -0
- package/src/shortbread/properties.test.ts +44 -0
- package/src/shortbread/properties.ts +142 -0
- package/src/shortbread/template.test.ts +43 -0
- package/src/shortbread/template.ts +343 -0
- package/src/style_builder/decorator.test.ts +67 -0
- package/src/style_builder/decorator.ts +135 -0
- package/src/style_builder/recolor.test.ts +306 -0
- package/src/style_builder/recolor.ts +110 -0
- package/src/style_builder/style_builder.test.ts +103 -0
- package/src/style_builder/style_builder.ts +134 -0
- package/src/style_builder/types.ts +141 -0
- package/src/styles/LICENSE.md +41 -0
- package/src/styles/colorful.ts +1041 -0
- package/src/styles/eclipse.ts +11 -0
- package/{dist/styles/empty.d.ts → src/styles/empty.ts} +7 -3
- package/src/styles/graybeard.ts +11 -0
- package/src/styles/index.ts +33 -0
- package/src/styles/neutrino.ts +429 -0
- package/{dist/types/index.d.ts → src/types/index.ts} +1 -0
- package/{dist/types/maplibre.d.ts → src/types/maplibre.ts} +3 -0
- package/src/types/tilejson.test.ts +94 -0
- package/src/types/tilejson.ts +125 -0
- package/src/types/vector_layer.test.ts +64 -0
- package/src/types/vector_layer.ts +69 -0
- package/dist/color/abstract.d.ts +0 -34
- package/dist/color/abstract.js +0 -53
- package/dist/color/abstract.js.map +0 -1
- package/dist/color/hsl.d.ts +0 -23
- package/dist/color/hsl.js +0 -98
- package/dist/color/hsl.js.map +0 -1
- package/dist/color/hsv.d.ts +0 -20
- package/dist/color/hsv.js +0 -100
- package/dist/color/hsv.js.map +0 -1
- package/dist/color/index.d.ts +0 -6
- package/dist/color/index.js +0 -29
- package/dist/color/index.js.map +0 -1
- package/dist/color/random.d.ts +0 -9
- package/dist/color/random.js +0 -134
- package/dist/color/random.js.map +0 -1
- package/dist/color/rgb.d.ts +0 -28
- package/dist/color/rgb.js +0 -195
- package/dist/color/rgb.js.map +0 -1
- package/dist/color/utils.d.ts +0 -3
- package/dist/color/utils.js +0 -10
- package/dist/color/utils.js.map +0 -1
- package/dist/guess_style/guess_style.d.ts +0 -8
- package/dist/guess_style/guess_style.js +0 -147
- package/dist/guess_style/guess_style.js.map +0 -1
- package/dist/guess_style/index.js +0 -2
- package/dist/guess_style/index.js.map +0 -1
- package/dist/lib/utils.d.ts +0 -6
- package/dist/lib/utils.js +0 -126
- package/dist/lib/utils.js.map +0 -1
- package/dist/shortbread/index.js +0 -3
- package/dist/shortbread/index.js.map +0 -1
- package/dist/shortbread/layers.d.ts +0 -5
- package/dist/shortbread/layers.js +0 -521
- package/dist/shortbread/layers.js.map +0 -1
- package/dist/shortbread/properties.d.ts +0 -7
- package/dist/shortbread/properties.js +0 -125
- package/dist/shortbread/properties.js.map +0 -1
- package/dist/shortbread/template.d.ts +0 -4
- package/dist/shortbread/template.js +0 -339
- package/dist/shortbread/template.js.map +0 -1
- package/dist/style_builder/decorator.d.ts +0 -4
- package/dist/style_builder/decorator.js +0 -127
- package/dist/style_builder/decorator.js.map +0 -1
- package/dist/style_builder/recolor.d.ts +0 -22
- package/dist/style_builder/recolor.js +0 -89
- package/dist/style_builder/recolor.js.map +0 -1
- package/dist/style_builder/style_builder.d.ts +0 -15
- package/dist/style_builder/style_builder.js +0 -106
- package/dist/style_builder/style_builder.js.map +0 -1
- package/dist/style_builder/types.d.ts +0 -122
- package/dist/style_builder/types.js +0 -3
- package/dist/style_builder/types.js.map +0 -1
- package/dist/styles/colorful.d.ts +0 -11
- package/dist/styles/colorful.js +0 -956
- package/dist/styles/colorful.js.map +0 -1
- package/dist/styles/eclipse.d.ts +0 -5
- package/dist/styles/eclipse.js +0 -9
- package/dist/styles/eclipse.js.map +0 -1
- package/dist/styles/empty.js +0 -8
- package/dist/styles/empty.js.map +0 -1
- package/dist/styles/graybeard.d.ts +0 -5
- package/dist/styles/graybeard.js +0 -9
- package/dist/styles/graybeard.js.map +0 -1
- package/dist/styles/index.d.ts +0 -11
- package/dist/styles/index.js +0 -20
- package/dist/styles/index.js.map +0 -1
- package/dist/styles/neutrino.d.ts +0 -11
- package/dist/styles/neutrino.js +0 -401
- package/dist/styles/neutrino.js.map +0 -1
- package/dist/types/index.js +0 -3
- package/dist/types/index.js.map +0 -1
- package/dist/types/maplibre.js +0 -2
- package/dist/types/maplibre.js.map +0 -1
- package/dist/types/tilejson.d.ts +0 -32
- package/dist/types/tilejson.js +0 -87
- package/dist/types/tilejson.js.map +0 -1
- package/dist/types/vector_layer.d.ts +0 -14
- package/dist/types/vector_layer.js +0 -51
- package/dist/types/vector_layer.js.map +0 -1
package/src/color/hsl.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { Color } from './abstract.js';
|
|
2
|
+
import { HSV } from './hsv.js';
|
|
3
|
+
import { RGB } from './rgb.js';
|
|
4
|
+
import { clamp, formatFloat, mod } from './utils.js';
|
|
5
|
+
|
|
6
|
+
export class HSL extends Color {
|
|
7
|
+
readonly h: number = 0; // between 0 and 360
|
|
8
|
+
readonly s: number = 0; // between 0 and 100
|
|
9
|
+
readonly l: number = 0; // between 0 and 100
|
|
10
|
+
readonly a: number = 1; // between 0 and 1
|
|
11
|
+
|
|
12
|
+
constructor(h: number, s: number, l: number, a: number = 1) {
|
|
13
|
+
super();
|
|
14
|
+
this.h = mod(h, 360);
|
|
15
|
+
this.s = clamp(s, 0, 100);
|
|
16
|
+
this.l = clamp(l, 0, 100);
|
|
17
|
+
this.a = clamp(a, 0, 1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
asArray(): [number, number, number, number] {
|
|
21
|
+
return [this.h, this.s, this.l, this.a];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
round(): HSL {
|
|
25
|
+
return new HSL(
|
|
26
|
+
Math.round(this.h),
|
|
27
|
+
Math.round(this.s),
|
|
28
|
+
Math.round(this.l),
|
|
29
|
+
Math.round(this.a * 1000) / 1000,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
clone(): HSL {
|
|
34
|
+
return new HSL(this.h, this.s, this.l, this.a);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
asString(): string {
|
|
38
|
+
if (this.a === 1) {
|
|
39
|
+
return `hsl(${this.h.toFixed(0)},${this.s.toFixed(0)}%,${this.l.toFixed(0)}%)`;
|
|
40
|
+
} else {
|
|
41
|
+
return `hsla(${this.h.toFixed(0)},${this.s.toFixed(0)}%,${this.l.toFixed(0)}%,${formatFloat(this.a, 3)})`;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
asHSL(): HSL {
|
|
46
|
+
return this.clone();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
toHSL(): HSL {
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
asHSV(): HSV {
|
|
54
|
+
const s = this.s / 100, l = this.l / 100;
|
|
55
|
+
const v = l + s * Math.min(l, 1 - l);
|
|
56
|
+
const sv = v === 0 ? 0 : 2 * (1 - l / v);
|
|
57
|
+
return new HSV(this.h, sv * 100, v * 100, this.a);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
asRGB(): RGB {
|
|
61
|
+
const h = this.h / 360;
|
|
62
|
+
const s = this.s / 100;
|
|
63
|
+
const l = this.l / 100;
|
|
64
|
+
|
|
65
|
+
// Achromatic (grey)
|
|
66
|
+
if (s === 0) return new RGB(l * 255, l * 255, l * 255, this.a);
|
|
67
|
+
|
|
68
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
69
|
+
const p = 2 * l - q;
|
|
70
|
+
|
|
71
|
+
const hueToRgb = (t: number): number => {
|
|
72
|
+
if (t < 0) t += 1;
|
|
73
|
+
if (t > 1) t -= 1;
|
|
74
|
+
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
75
|
+
if (t < 1 / 2) return q;
|
|
76
|
+
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
77
|
+
return p;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Convert to RGB in the 0-255 range and return
|
|
81
|
+
return new RGB(
|
|
82
|
+
255 * hueToRgb(h + 1 / 3),
|
|
83
|
+
255 * hueToRgb(h),
|
|
84
|
+
255 * hueToRgb(h - 1 / 3),
|
|
85
|
+
this.a
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
static parse(input: string | Color): HSL {
|
|
90
|
+
if (input instanceof Color) return input.asHSL();
|
|
91
|
+
|
|
92
|
+
input = input.replace(/\s+/g, '').toLowerCase();
|
|
93
|
+
|
|
94
|
+
let match = input.match(/^hsl\((?<h>[-+0-9.]+)(?:deg)?,(?<s>[-+0-9.]+)%,(?<l>[-+0-9.]+)%\)$/);
|
|
95
|
+
if (match) {
|
|
96
|
+
return new HSL(parseFloat(match.groups!.h), parseFloat(match.groups!.s), parseFloat(match.groups!.l));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
match = input.match(/^hsla\((?<h>[-+0-9.]+)(?:deg)?,(?<s>[-+0-9.]+)%,(?<l>[-+0-9.]+)%,(?<a>[-+0-9.]+)\)$/);
|
|
100
|
+
if (match) {
|
|
101
|
+
return new HSL(parseFloat(match.groups!.h), parseFloat(match.groups!.s), parseFloat(match.groups!.l), parseFloat(match.groups!.a));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
throw new Error(`Invalid HSL color string: "${input}"`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
invertLuminosity(): HSL {
|
|
108
|
+
return new HSL(this.h, this.s, 100 - this.l, this.a);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
rotateHue(offset: number): HSL {
|
|
112
|
+
return new HSL(mod(this.h + offset, 360), this.s, this.l, this.a);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
saturate(ratio: number): HSL {
|
|
116
|
+
return new HSL(this.h, clamp(this.s * (1 + ratio), 0, 100), this.l, this.a);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
fade(value: number): HSL {
|
|
120
|
+
return new HSL(this.h, this.s, this.l, this.a * (1 - value));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { HSV } from './hsv.js';
|
|
2
|
+
import { HSL } from './hsl.js';
|
|
3
|
+
import { RGB } from './rgb.js';
|
|
4
|
+
|
|
5
|
+
describe('HSV Class', () => {
|
|
6
|
+
|
|
7
|
+
test('constructor initializes values correctly with clamping', () => {
|
|
8
|
+
const color = new HSV(400, 120, 120, 2);
|
|
9
|
+
expect(color.asArray()).toStrictEqual([40, 100, 100, 1]);
|
|
10
|
+
|
|
11
|
+
const colorNegative = new HSV(-60, -10, -10, -1);
|
|
12
|
+
expect(colorNegative.asArray()).toStrictEqual([300, 0, 0, 0]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('asArray returns correct array representation', () => {
|
|
16
|
+
const color = new HSV(120, 50, 50, 0.5);
|
|
17
|
+
expect(color.asArray()).toStrictEqual([120, 50, 50, 0.5]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('clone returns a new instance with identical values', () => {
|
|
21
|
+
const color = new HSV(180, 70, 70, 0.7);
|
|
22
|
+
const clone = color.clone();
|
|
23
|
+
expect(clone).toBeInstanceOf(HSV);
|
|
24
|
+
expect(clone).toEqual(color);
|
|
25
|
+
expect(clone).not.toBe(color);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('asString', () => {
|
|
29
|
+
test('converts fully saturated colors correctly', () => {
|
|
30
|
+
expect(new HSV(0, 100, 100).asString()).toBe('hsl(0,100%,50%)');
|
|
31
|
+
expect(new HSV(120, 100, 100).asString()).toBe('hsl(120,100%,50%)');
|
|
32
|
+
expect(new HSV(240, 100, 100).asString()).toBe('hsl(240,100%,50%)');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('handles partially saturated colors', () => {
|
|
36
|
+
expect(new HSV(60, 50, 100).asString()).toBe('hsl(60,100%,75%)');
|
|
37
|
+
expect(new HSV(300, 25, 50).asString()).toBe('hsl(300,14%,44%)');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('handles achromatic (grey) colors', () => {
|
|
41
|
+
expect(new HSV(0, 0, 0).asString()).toBe('hsl(0,0%,0%)');
|
|
42
|
+
expect(new HSV(0, 0, 50).asString()).toBe('hsl(0,0%,50%)');
|
|
43
|
+
expect(new HSV(0, 0, 100).asString()).toBe('hsl(0,0%,100%)');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('handles hue wrapping and extreme values', () => {
|
|
47
|
+
expect(new HSV(-60, 100, 100).asString()).toBe('hsl(300,100%,50%)');
|
|
48
|
+
expect(new HSV(420, 100, 100).asString()).toBe('hsl(60,100%,50%)');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('handles alpha transparency', () => {
|
|
52
|
+
expect(new HSV(0, 100, 100, 0.5).asString()).toBe('hsla(0,100%,50%,0.5)');
|
|
53
|
+
expect(new HSV(120, 100, 100, 0.25).asString()).toBe('hsla(120,100%,50%,0.25)');
|
|
54
|
+
expect(new HSV(240, 100, 100, 1).asString()).toBe('hsl(240,100%,50%)');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('produces consistent results for repeated calls', () => {
|
|
58
|
+
const color = new HSV(60, 50, 50);
|
|
59
|
+
expect(color.asString()).toBe('hsl(60,33%,38%)');
|
|
60
|
+
expect(color.asString()).toBe('hsl(60,33%,38%)');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('color conversion', () => {
|
|
65
|
+
|
|
66
|
+
test('asHSL converts HSV to HSL correctly', () => {
|
|
67
|
+
function check(input: [number, number, number], output: [number, number, number]) {
|
|
68
|
+
const hsv = new HSV(...input);
|
|
69
|
+
const hsl = hsv.asHSL();
|
|
70
|
+
expect(hsl).toBeInstanceOf(HSL);
|
|
71
|
+
expect(hsl.asArray().map(Math.round))
|
|
72
|
+
.toStrictEqual([...output, 1]);
|
|
73
|
+
|
|
74
|
+
expect(hsv.asHex()).toStrictEqual(hsl.asHex());
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
check([10, 0, 0], [10, 0, 0]);
|
|
78
|
+
check([11, 0, 50], [11, 0, 50]);
|
|
79
|
+
check([12, 0, 100], [12, 0, 100]);
|
|
80
|
+
check([13, 50, 0], [13, 0, 0]);
|
|
81
|
+
check([14, 50, 50], [14, 33, 38]);
|
|
82
|
+
check([15, 50, 100], [15, 100, 75]);
|
|
83
|
+
check([16, 100, 0], [16, 0, 0]);
|
|
84
|
+
check([17, 100, 50], [17, 100, 25]);
|
|
85
|
+
check([18, 100, 100], [18, 100, 50]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('asRGB converts HSV to RGB correctly', () => {
|
|
89
|
+
const color = new HSV(120, 100, 100);
|
|
90
|
+
const rgb = color.asRGB();
|
|
91
|
+
expect(rgb).toBeInstanceOf(RGB);
|
|
92
|
+
expect(rgb.asArray().map(value => Math.round(value)))
|
|
93
|
+
.toStrictEqual([0, 255, 0, 1]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('asHSV and toHSV return the same instance or clone', () => {
|
|
97
|
+
const color = new HSV(240, 100, 50, 1);
|
|
98
|
+
expect(color.asHSV()).toStrictEqual(color);
|
|
99
|
+
expect(color.toHSV()).toStrictEqual(color);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('asRGB conversion S and V', () => {
|
|
103
|
+
function check(input: [number, number, number], output: [number, number, number]) {
|
|
104
|
+
const color = new HSV(...input);
|
|
105
|
+
const rgb = color.asRGB();
|
|
106
|
+
expect(rgb.asArray().map(value => Math.round(value)))
|
|
107
|
+
.toStrictEqual([...output, 1]);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
check([10, 0, 0], [0, 0, 0]);
|
|
111
|
+
check([11, 0, 50], [128, 128, 128]);
|
|
112
|
+
check([12, 0, 100], [255, 255, 255]);
|
|
113
|
+
check([13, 50, 0], [0, 0, 0]);
|
|
114
|
+
check([14, 50, 50], [128, 79, 64]);
|
|
115
|
+
check([15, 50, 100], [255, 159, 128]);
|
|
116
|
+
check([16, 100, 0], [0, 0, 0]);
|
|
117
|
+
check([17, 100, 50], [128, 36, 0]);
|
|
118
|
+
check([18, 100, 100], [255, 77, 0]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('asRGB conversion H', () => {
|
|
122
|
+
function check(hue: number, output: string) {
|
|
123
|
+
const color = new HSV(hue, 100, 100);
|
|
124
|
+
expect(color.asRGB().asHex()).toBe(output);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
check(-1, '#FF0004');
|
|
128
|
+
check(0, '#FF0000');
|
|
129
|
+
check(1, '#FF0400');
|
|
130
|
+
check(30, '#FF8000');
|
|
131
|
+
check(60, '#FFFF00');
|
|
132
|
+
check(90, '#80FF00');
|
|
133
|
+
check(120, '#00FF00');
|
|
134
|
+
check(150, '#00FF80');
|
|
135
|
+
check(180, '#00FFFF');
|
|
136
|
+
check(210, '#0080FF');
|
|
137
|
+
check(240, '#0000FF');
|
|
138
|
+
check(270, '#8000FF');
|
|
139
|
+
check(300, '#FF00FF');
|
|
140
|
+
check(330, '#FF0080');
|
|
141
|
+
check(359, '#FF0004');
|
|
142
|
+
check(360, '#FF0000');
|
|
143
|
+
check(361, '#FF0400');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('parse errors and validations', () => {
|
|
149
|
+
test('constructor clamps out-of-bound values', () => {
|
|
150
|
+
const color = new HSV(400, 150, 150, 2);
|
|
151
|
+
expect(color.asArray()).toStrictEqual([40, 100, 100, 1]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('negative values are handled correctly', () => {
|
|
155
|
+
const color = new HSV(-360, -50, -50, -1);
|
|
156
|
+
expect(color.asArray()).toStrictEqual([0, 0, 0, 0]);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('fade', () => {
|
|
161
|
+
test('reduces alpha correctly', () => {
|
|
162
|
+
const color = new HSV(120, 50, 50, 0.8);
|
|
163
|
+
expect(color.fade(0.5).asArray()).toStrictEqual([120, 50, 50, 0.4]); // Alpha reduced by 50%
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('handles edge cases for fading', () => {
|
|
167
|
+
const opaque = new HSV(0, 50, 50, 1);
|
|
168
|
+
expect(opaque.fade(1).asArray()).toStrictEqual([0, 50, 50, 0]); // Fully faded to transparent
|
|
169
|
+
|
|
170
|
+
const transparent = new HSV(0, 50, 50, 0);
|
|
171
|
+
expect(transparent.fade(0.5).asArray()).toStrictEqual([0, 50, 50, 0]); // Remains fully transparent
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
});
|
package/src/color/hsv.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Color } from './abstract.js';
|
|
2
|
+
import { HSL } from './hsl.js';
|
|
3
|
+
import { RGB } from './rgb.js';
|
|
4
|
+
import { clamp, mod } from './utils.js';
|
|
5
|
+
|
|
6
|
+
export class HSV extends Color {
|
|
7
|
+
readonly h: number = 0; // between 0 and 360
|
|
8
|
+
readonly s: number = 0; // between 0 and 100
|
|
9
|
+
readonly v: number = 0; // between 0 and 100
|
|
10
|
+
readonly a: number = 1; // between 0 and 1
|
|
11
|
+
|
|
12
|
+
constructor(h: number, s: number, v: number, a: number = 1) {
|
|
13
|
+
super();
|
|
14
|
+
this.h = mod(h, 360);
|
|
15
|
+
this.s = clamp(s, 0, 100);
|
|
16
|
+
this.v = clamp(v, 0, 100);
|
|
17
|
+
this.a = clamp(a, 0, 1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
asArray(): [number, number, number, number] {
|
|
21
|
+
return [this.h, this.s, this.v, this.a];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
round(): HSV {
|
|
25
|
+
return new HSV(
|
|
26
|
+
Math.round(this.h),
|
|
27
|
+
Math.round(this.s),
|
|
28
|
+
Math.round(this.v),
|
|
29
|
+
Math.round(this.a * 1000) / 1000
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
asString(): string {
|
|
34
|
+
return this.asHSL().asString();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
clone(): HSV {
|
|
38
|
+
return new HSV(this.h, this.s, this.v, this.a);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
asHSL(): HSL {
|
|
42
|
+
const s = this.s / 100;
|
|
43
|
+
const v = this.v / 100;
|
|
44
|
+
const k = (2 - s) * v;
|
|
45
|
+
const q = k < 1 ? k : 2 - k
|
|
46
|
+
return new HSL(
|
|
47
|
+
this.h,
|
|
48
|
+
q == 0 ? 0 : 100 * s * v / q,
|
|
49
|
+
100 * k / 2,
|
|
50
|
+
this.a
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
asHSV(): HSV {
|
|
55
|
+
return this.clone();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
toHSV(): HSV {
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
asRGB(): RGB {
|
|
63
|
+
const h = this.h / 360; // Normalize h to range [0, 1]
|
|
64
|
+
const s = this.s / 100; // Normalize s to range [0, 1]
|
|
65
|
+
const v = this.v / 100; // Normalize v to range [0, 1]
|
|
66
|
+
|
|
67
|
+
let r = 0, g = 0, b = 0;
|
|
68
|
+
|
|
69
|
+
if (s === 0) {
|
|
70
|
+
// Achromatic (grey)
|
|
71
|
+
r = g = b = v;
|
|
72
|
+
} else {
|
|
73
|
+
const i = Math.floor(h * 6); // Determine the sector of the color wheel
|
|
74
|
+
const f = h * 6 - i; // Fractional part of h * 6
|
|
75
|
+
const p = v * (1 - s);
|
|
76
|
+
const q = v * (1 - s * f);
|
|
77
|
+
const t = v * (1 - s * (1 - f));
|
|
78
|
+
|
|
79
|
+
switch (i % 6) {
|
|
80
|
+
case 0: r = v; g = t; b = p; break;
|
|
81
|
+
case 1: r = q; g = v; b = p; break;
|
|
82
|
+
case 2: r = p; g = v; b = t; break;
|
|
83
|
+
case 3: r = p; g = q; b = v; break;
|
|
84
|
+
case 4: r = t; g = p; b = v; break;
|
|
85
|
+
case 5: r = v; g = p; b = q; break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Convert to RGB in the 0-255 range and return
|
|
90
|
+
return new RGB(r * 255, g * 255, b * 255, this.a);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
fade(value: number): HSV {
|
|
94
|
+
return new HSV(this.h, this.s, this.v, this.a * (1 - value));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
setHue(value: number): HSV {
|
|
98
|
+
return new HSV(value, this.s, this.v, this.a);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { Color } from './index.js';
|
|
2
|
+
import { HSL } from './hsl.js';
|
|
3
|
+
import { HSV } from './hsv.js';
|
|
4
|
+
import { RGB } from './rgb.js';
|
|
5
|
+
|
|
6
|
+
describe('Color Conversions', () => {
|
|
7
|
+
|
|
8
|
+
const scenarios: [number, number, number, number][] = [
|
|
9
|
+
[-100, 14, 15, 0],
|
|
10
|
+
[0, 0, 0, 0.1],
|
|
11
|
+
[100, 0, 50, 0.2],
|
|
12
|
+
[200, 0, 100, 0.3],
|
|
13
|
+
[300, 50, 0, 0.4],
|
|
14
|
+
[400, 50, 50, 0.5],
|
|
15
|
+
[500, 50, 100, 0.6],
|
|
16
|
+
[600, 100, 0, 0.7],
|
|
17
|
+
[700, 100, 50, 0.8],
|
|
18
|
+
[800, 100, 100, 0.9],
|
|
19
|
+
[900, 12, 13, 1.0],
|
|
20
|
+
]
|
|
21
|
+
test('test HSV -> HSL -> RGB', () => {
|
|
22
|
+
for (const v of scenarios) {
|
|
23
|
+
const hsv = new HSV(...v);
|
|
24
|
+
expect(hsv.a).toEqual(v[3]);
|
|
25
|
+
const hsl = hsv.asHSL();
|
|
26
|
+
const a1 = hsv.asRGB().asArray();
|
|
27
|
+
const a2 = hsl.asRGB().asArray();
|
|
28
|
+
for (let i = 0; i < 4; i++) expect(a1[i]).toBeCloseTo(a2[i]);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('test HSL -> HSV -> RGB', () => {
|
|
33
|
+
for (const v of scenarios) {
|
|
34
|
+
const hsl = new HSL(...v);
|
|
35
|
+
expect(hsl.a).toEqual(v[3]);
|
|
36
|
+
const hsv = hsl.asHSV();
|
|
37
|
+
const a1 = hsv.asRGB().asArray();
|
|
38
|
+
const a2 = hsl.asRGB().asArray();
|
|
39
|
+
for (let i = 0; i < 4; i++) expect(a1[i]).toBeCloseTo(a2[i]);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('Color.parse', () => {
|
|
45
|
+
test('parses hexadecimal color strings correctly', () => {
|
|
46
|
+
const color = Color.parse('#ff8040');
|
|
47
|
+
expect(color).toBeInstanceOf(RGB);
|
|
48
|
+
expect(color.asArray()).toStrictEqual([255, 128, 64, 1]);
|
|
49
|
+
|
|
50
|
+
const colorWithAlpha = Color.parse('#ff80407f');
|
|
51
|
+
expect(colorWithAlpha).toBeInstanceOf(RGB);
|
|
52
|
+
expect(colorWithAlpha.asHex()).toStrictEqual('#FF80407F');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('parses RGB strings correctly', () => {
|
|
56
|
+
const color = Color.parse('rgb(255, 128, 64)');
|
|
57
|
+
expect(color).toBeInstanceOf(RGB);
|
|
58
|
+
expect(color.asArray()).toStrictEqual([255, 128, 64, 1]);
|
|
59
|
+
|
|
60
|
+
const colorWithAlpha = Color.parse('rgba(255, 128, 64, 0.5)');
|
|
61
|
+
expect(colorWithAlpha).toBeInstanceOf(RGB);
|
|
62
|
+
expect(colorWithAlpha.asArray()).toStrictEqual([255, 128, 64, 0.5]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('parses HSL strings correctly', () => {
|
|
66
|
+
const color = Color.parse('hsl(120, 50%, 50%)');
|
|
67
|
+
expect(color).toBeInstanceOf(HSL);
|
|
68
|
+
expect(color.asArray()).toStrictEqual([120, 50, 50, 1]);
|
|
69
|
+
|
|
70
|
+
const colorWithAlpha = Color.parse('hsla(120, 50%, 50%, 0.5)');
|
|
71
|
+
expect(colorWithAlpha).toBeInstanceOf(HSL);
|
|
72
|
+
expect(colorWithAlpha.asArray()).toStrictEqual([120, 50, 50, 0.5]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('throws an error for unsupported formats', () => {
|
|
76
|
+
expect(() => Color.parse('invalid color string')).toThrow('Unknown color format: invalid color string');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('Color.random', () => {
|
|
81
|
+
test('generates random HSV colors', () => {
|
|
82
|
+
const random = Color.random();
|
|
83
|
+
expect(random).toBeInstanceOf(HSV);
|
|
84
|
+
const array = random.asArray();
|
|
85
|
+
expect(array[0]).toBeGreaterThanOrEqual(0);
|
|
86
|
+
expect(array[0]).toBeLessThanOrEqual(360);
|
|
87
|
+
expect(array[1]).toBeGreaterThanOrEqual(0);
|
|
88
|
+
expect(array[1]).toBeLessThanOrEqual(100);
|
|
89
|
+
expect(array[2]).toBeGreaterThanOrEqual(0);
|
|
90
|
+
expect(array[2]).toBeLessThanOrEqual(100);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('supports options for generating random colors', () => {
|
|
94
|
+
const random = Color.random({ hue: 'red', luminosity: 'bright' });
|
|
95
|
+
expect(random).toBeInstanceOf(HSV);
|
|
96
|
+
// Additional checks based on the options provided can be added here
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('Color Class Properties', () => {
|
|
101
|
+
test('Color.HSL is accessible', () => {
|
|
102
|
+
expect(Color.HSL).toBe(HSL);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('Color.HSV is accessible', () => {
|
|
106
|
+
expect(Color.HSV).toBe(HSV);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('Color.RGB is accessible', () => {
|
|
110
|
+
expect(Color.RGB).toBe(RGB);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('Exported Module', () => {
|
|
115
|
+
test('named export is Color', async () => {
|
|
116
|
+
const module = await import('./index.js');
|
|
117
|
+
expect(module.Color).toBe(Color);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Color } from './abstract.js';
|
|
2
|
+
import { HSL } from './hsl.js';
|
|
3
|
+
import { HSV } from './hsv.js';
|
|
4
|
+
import randomColor from './random.js';
|
|
5
|
+
import { RGB } from './rgb.js';
|
|
6
|
+
|
|
7
|
+
Color.parse = function (input: string | Color): Color {
|
|
8
|
+
if (input instanceof Color) return input;
|
|
9
|
+
|
|
10
|
+
input = input.trim().toLowerCase();
|
|
11
|
+
|
|
12
|
+
if (input.startsWith('#')) return RGB.parse(input);
|
|
13
|
+
|
|
14
|
+
const prefix = input.replace(/\d.*/, '').trim().toLowerCase();
|
|
15
|
+
|
|
16
|
+
switch (prefix) {
|
|
17
|
+
case 'rgb(':
|
|
18
|
+
case 'rgba(':
|
|
19
|
+
return RGB.parse(input);
|
|
20
|
+
case 'hsl(':
|
|
21
|
+
case 'hsla(':
|
|
22
|
+
return HSL.parse(input);
|
|
23
|
+
default:
|
|
24
|
+
throw Error('Unknown color format: ' + input);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
Color.HSL = HSL;
|
|
29
|
+
Color.HSV = HSV;
|
|
30
|
+
Color.RGB = RGB;
|
|
31
|
+
|
|
32
|
+
Color.random = randomColor;
|
|
33
|
+
|
|
34
|
+
export type { RandomColorOptions } from './random.js';
|
|
35
|
+
export type { HSL } from './hsl.js';
|
|
36
|
+
export type { HSV } from './hsv.js';
|
|
37
|
+
export type { RGB } from './rgb.js';
|
|
38
|
+
export { Color };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { RandomColorOptions } from './random.js';
|
|
2
|
+
import randomColor from './random.js';
|
|
3
|
+
|
|
4
|
+
describe('RandomColor', () => {
|
|
5
|
+
test('constructor initializes without errors', () => {
|
|
6
|
+
expect(randomColor).toBeDefined();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
describe('randomColor method', () => {
|
|
10
|
+
test('returns correct color string for some test cases', () => {
|
|
11
|
+
function t(options: RandomColorOptions): string {
|
|
12
|
+
return randomColor(options).asHSL().asString();
|
|
13
|
+
}
|
|
14
|
+
expect(t({ seed: 'testSeed', hue: 'red' })).toBe('hsl(356,90%,30%)');
|
|
15
|
+
expect(t({ seed: 'testSeed', hue: 120 })).toBe('hsl(120,92%,26%)');
|
|
16
|
+
expect(t({ seed: 'testSeed', luminosity: 'dark' })).toBe('hsl(185,98%,19%)');
|
|
17
|
+
expect(t({ seed: 'testSeed', luminosity: 12 })).toBe('hsl(185,90%,6%)');
|
|
18
|
+
expect(t({ seed: 'testSeed', saturation: 'strong' })).toBe('hsl(185,100%,48%)');
|
|
19
|
+
expect(t({ seed: 'testSeed', opacity: 0.5 })).toBe('hsla(185,90%,23%,0.5)');
|
|
20
|
+
expect(t({ seed: 'testSeed' })).toBe('hsl(185,90%,23%)');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('consistent color generation with a seed', () => {
|
|
24
|
+
const color1 = randomColor({ seed: 'consistentSeed' });
|
|
25
|
+
const color2 = randomColor({ seed: 'consistentSeed' });
|
|
26
|
+
expect(color1.asHex()).toBe(color2.asHex());
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('different color generation without a seed', () => {
|
|
30
|
+
const color1 = randomColor({ seed: 'seed1' });
|
|
31
|
+
const color2 = randomColor({ seed: 'seed2' });
|
|
32
|
+
expect(color1.asHex()).not.toBe(color2.asHex());
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
});
|