@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
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { HSV } from './hsv.js';
|
|
2
|
+
import { mod } from './utils.js';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
type Range = [number, number];
|
|
6
|
+
interface ColorInfo {
|
|
7
|
+
hueRange: Range | null;
|
|
8
|
+
lowerBounds: Range[];
|
|
9
|
+
saturationRange: Range;
|
|
10
|
+
brightnessRange: Range;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface RandomColorOptions {
|
|
14
|
+
seed?: string;
|
|
15
|
+
hue?: number | string;
|
|
16
|
+
opacity?: number;
|
|
17
|
+
luminosity?: number | string;
|
|
18
|
+
saturation?: number | string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let colorDictionary = new Map<string, ColorInfo>();
|
|
22
|
+
|
|
23
|
+
export default function randomColor(options?: RandomColorOptions): HSV {
|
|
24
|
+
if (colorDictionary.size === 0) colorDictionary = initColorDictionary();
|
|
25
|
+
|
|
26
|
+
options ??= {};
|
|
27
|
+
|
|
28
|
+
let seed = inputToSeed(options.seed);
|
|
29
|
+
|
|
30
|
+
const H = pickHue(options);
|
|
31
|
+
const S = pickSaturation(H, options);
|
|
32
|
+
const V = pickBrightness(H, S, options);
|
|
33
|
+
|
|
34
|
+
return new HSV(H, S, V, options.opacity ?? 1);
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
function pickHue(options: RandomColorOptions): number {
|
|
39
|
+
return mod(randomWithin(getHueRange(options.hue)), 360);
|
|
40
|
+
|
|
41
|
+
function getHueRange(hue?: number | string): Range {
|
|
42
|
+
if (typeof hue === 'number') {
|
|
43
|
+
hue = mod(hue, 360);
|
|
44
|
+
return [hue, hue];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (typeof hue === 'string') {
|
|
48
|
+
const color = colorDictionary.get(hue);
|
|
49
|
+
if (color?.hueRange) return color.hueRange;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return [0, 360];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function pickSaturation(hue: number, options: RandomColorOptions): number {
|
|
57
|
+
if (options.hue === 'monochrome') return 0;
|
|
58
|
+
if (options.luminosity === 'random') return randomWithin([0, 100]);
|
|
59
|
+
|
|
60
|
+
let [sMin, sMax] = getColorInfo(hue).saturationRange;
|
|
61
|
+
|
|
62
|
+
if (options.saturation === 'strong') return sMax;
|
|
63
|
+
|
|
64
|
+
switch (options.luminosity) {
|
|
65
|
+
case 'bright': sMin = 55; break;
|
|
66
|
+
case 'dark': sMin = sMax - 10; break;
|
|
67
|
+
case 'light': sMax = 55; break;
|
|
68
|
+
default:
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return randomWithin([sMin, sMax]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function pickBrightness(h: number, s: number, options: RandomColorOptions): number {
|
|
75
|
+
let bMin = getMinimumBrightness(h, s), bMax = 100;
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if (typeof options.luminosity === 'number') {
|
|
79
|
+
bMin = options.luminosity;
|
|
80
|
+
bMax = options.luminosity;
|
|
81
|
+
} else {
|
|
82
|
+
switch (options.luminosity) {
|
|
83
|
+
case 'dark': bMax = Math.min(100, bMin + 20); break;
|
|
84
|
+
case 'light': bMin = (bMax + bMin) / 2; break;
|
|
85
|
+
case 'random': bMin = 0; bMax = 100; break;
|
|
86
|
+
default:
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return randomWithin([bMin, bMax]);
|
|
91
|
+
|
|
92
|
+
function getMinimumBrightness(h: number, s: number): number {
|
|
93
|
+
const { lowerBounds } = getColorInfo(h);
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < lowerBounds.length - 1; i++) {
|
|
96
|
+
const [s1, v1] = lowerBounds[i];
|
|
97
|
+
const [s2, v2] = lowerBounds[i + 1];
|
|
98
|
+
if (s >= s1 && s <= s2) {
|
|
99
|
+
const m = (v2 - v1) / (s2 - s1), b = v1 - m * s1;
|
|
100
|
+
return m * s + b;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function randomWithin(range: Range): number {
|
|
109
|
+
//Seeded random algorithm from http://indiegamr.com/generate-repeatable-random-numbers-in-js/
|
|
110
|
+
seed = (seed * 9301 + 49297) % 233280;
|
|
111
|
+
return Math.floor(range[0] + seed / 233280.0 * (range[1] - range[0]));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
function inputToSeed(input: number | string | null | undefined): number {
|
|
117
|
+
if (input == null) return 0;
|
|
118
|
+
if (typeof input === 'number') return input;
|
|
119
|
+
|
|
120
|
+
let i = 0;
|
|
121
|
+
for (let p = 0; p < input.length; p++) i = (i * 0x101 + input.charCodeAt(p)) % 0x100000000;
|
|
122
|
+
return i;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
function initColorDictionary(): Map<string, ColorInfo> {
|
|
128
|
+
const dict = new Map<string, ColorInfo>();
|
|
129
|
+
|
|
130
|
+
const defineColor = (name: string, hueRange: [number, number] | null, lowerBounds: [number, number][]): void => {
|
|
131
|
+
const [greyest] = lowerBounds;
|
|
132
|
+
const colorful = lowerBounds[lowerBounds.length - 1];
|
|
133
|
+
|
|
134
|
+
dict.set(name, {
|
|
135
|
+
hueRange,
|
|
136
|
+
lowerBounds,
|
|
137
|
+
saturationRange: [greyest[0], colorful[0]],
|
|
138
|
+
brightnessRange: [colorful[1], greyest[1]],
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
defineColor('monochrome', null, [[0, 0], [100, 0]]);
|
|
143
|
+
defineColor('red', [- 26, 18], [[20, 100], [30, 92], [40, 89], [50, 85], [60, 78], [70, 70], [80, 60], [90, 55], [100, 50]]);
|
|
144
|
+
defineColor('orange', [18, 46], [[20, 100], [30, 93], [40, 88], [50, 86], [60, 85], [70, 70], [100, 70]]);
|
|
145
|
+
defineColor('yellow', [46, 62], [[25, 100], [40, 94], [50, 89], [60, 86], [70, 84], [80, 82], [90, 80], [100, 75]]);
|
|
146
|
+
defineColor('green', [62, 178], [[30, 100], [40, 90], [50, 85], [60, 81], [70, 74], [80, 64], [90, 50], [100, 40]]);
|
|
147
|
+
defineColor('blue', [178, 257], [[20, 100], [30, 86], [40, 80], [50, 74], [60, 60], [70, 52], [80, 44], [90, 39], [100, 35]]);
|
|
148
|
+
defineColor('purple', [257, 282], [[20, 100], [30, 87], [40, 79], [50, 70], [60, 65], [70, 59], [80, 52], [90, 45], [100, 42]]);
|
|
149
|
+
defineColor('pink', [282, 334], [[20, 100], [30, 90], [40, 86], [60, 84], [80, 80], [90, 75], [100, 73]]);
|
|
150
|
+
|
|
151
|
+
return dict;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
function getColorInfo(hue: number): ColorInfo {
|
|
156
|
+
hue = mod(hue, 360);
|
|
157
|
+
if (hue >= 334) hue -= 360;
|
|
158
|
+
|
|
159
|
+
for (const color of colorDictionary.values()) {
|
|
160
|
+
if (color.hueRange && hue >= color.hueRange[0] && hue <= color.hueRange[1]) {
|
|
161
|
+
return color;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
throw Error('Color hue value not found');
|
|
165
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { RGB } from './rgb.js';
|
|
2
|
+
import { HSL } from './hsl.js';
|
|
3
|
+
import { HSV } from './hsv.js';
|
|
4
|
+
|
|
5
|
+
describe('RGB Class', () => {
|
|
6
|
+
|
|
7
|
+
test('constructor initializes values correctly with clamping', () => {
|
|
8
|
+
const color = new RGB(300, -50, 500, 2);
|
|
9
|
+
expect(color.asArray()).toStrictEqual([255, 0, 255, 1]);
|
|
10
|
+
|
|
11
|
+
const colorNegative = new RGB(-10, -20, -30, -1);
|
|
12
|
+
expect(colorNegative.asArray()).toStrictEqual([0, 0, 0, 0]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('asArray returns the correct array representation', () => {
|
|
16
|
+
const color = new RGB(128, 64, 255, 0.5);
|
|
17
|
+
expect(color.asArray()).toStrictEqual([128, 64, 255, 0.5]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('clone creates a new instance with identical values', () => {
|
|
21
|
+
const color = new RGB(128, 255, 64, 0.8);
|
|
22
|
+
const clone = color.clone();
|
|
23
|
+
expect(clone).toBeInstanceOf(RGB);
|
|
24
|
+
expect(clone).toEqual(color);
|
|
25
|
+
expect(clone).not.toBe(color);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('asString returns correct RGB/RGBA string', () => {
|
|
29
|
+
const color1 = new RGB(255, 128, 64);
|
|
30
|
+
expect(color1.asString()).toBe('rgb(255,128,64)');
|
|
31
|
+
|
|
32
|
+
const color2 = new RGB(255, 128, 64, 0.5);
|
|
33
|
+
expect(color2.asString()).toBe('rgba(255,128,64,0.5)');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('asHex returns correct hexadecimal representation', () => {
|
|
37
|
+
const color1 = new RGB(255, 128, 64);
|
|
38
|
+
expect(color1.asHex()).toBe('#FF8040');
|
|
39
|
+
|
|
40
|
+
const color2 = new RGB(255, 128, 64, 0.5);
|
|
41
|
+
expect(color2.asHex()).toBe('#FF804080');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('color conversion', () => {
|
|
45
|
+
test('asHSL converts RGB to HSL correctly', () => {
|
|
46
|
+
const hsl = new RGB(255, 0, 0).asHSL();
|
|
47
|
+
expect(hsl).toBeInstanceOf(HSL);
|
|
48
|
+
expect(hsl.asArray().map(value => Math.round(value)))
|
|
49
|
+
.toStrictEqual([0, 100, 50, 1]);
|
|
50
|
+
|
|
51
|
+
expect(RGB.parse('#000000').asHSL().round().asArray()).toStrictEqual([0, 0, 0, 1]);
|
|
52
|
+
expect(RGB.parse('#FFFFFF').asHSL().round().asArray()).toStrictEqual([0, 0, 100, 1]);
|
|
53
|
+
expect(RGB.parse('#FF0000').asHSL().round().asArray()).toStrictEqual([0, 100, 50, 1]);
|
|
54
|
+
expect(RGB.parse('#FF8000').asHSL().round().asArray()).toStrictEqual([30, 100, 50, 1]);
|
|
55
|
+
expect(RGB.parse('#FFFF00').asHSL().round().asArray()).toStrictEqual([60, 100, 50, 1]);
|
|
56
|
+
expect(RGB.parse('#80FF00').asHSL().round().asArray()).toStrictEqual([90, 100, 50, 1]);
|
|
57
|
+
expect(RGB.parse('#00FF00').asHSL().round().asArray()).toStrictEqual([120, 100, 50, 1]);
|
|
58
|
+
expect(RGB.parse('#00FF80').asHSL().round().asArray()).toStrictEqual([150, 100, 50, 1]);
|
|
59
|
+
expect(RGB.parse('#00FFFF').asHSL().round().asArray()).toStrictEqual([180, 100, 50, 1]);
|
|
60
|
+
expect(RGB.parse('#0080FF').asHSL().round().asArray()).toStrictEqual([210, 100, 50, 1]);
|
|
61
|
+
expect(RGB.parse('#0000FF').asHSL().round().asArray()).toStrictEqual([240, 100, 50, 1]);
|
|
62
|
+
expect(RGB.parse('#8000FF').asHSL().round().asArray()).toStrictEqual([270, 100, 50, 1]);
|
|
63
|
+
expect(RGB.parse('#FF00FF').asHSL().round().asArray()).toStrictEqual([300, 100, 50, 1]);
|
|
64
|
+
expect(RGB.parse('#FF0080').asHSL().round().asArray()).toStrictEqual([330, 100, 50, 1]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('asHSV converts RGB to HSV correctly', () => {
|
|
68
|
+
const hsv = new RGB(255, 0, 0).asHSV();
|
|
69
|
+
expect(hsv).toBeInstanceOf(HSV);
|
|
70
|
+
expect(hsv.asArray().map(value => Math.round(value)))
|
|
71
|
+
.toStrictEqual([0, 100, 100, 1]);
|
|
72
|
+
|
|
73
|
+
expect(RGB.parse('#000000').asHSV().round().asArray()).toStrictEqual([0, 0, 0, 1]);
|
|
74
|
+
expect(RGB.parse('#FFFFFF').asHSV().round().asArray()).toStrictEqual([0, 0, 100, 1]);
|
|
75
|
+
expect(RGB.parse('#FF0000').asHSV().round().asArray()).toStrictEqual([0, 100, 100, 1]);
|
|
76
|
+
expect(RGB.parse('#FF8000').asHSV().round().asArray()).toStrictEqual([30, 100, 100, 1]);
|
|
77
|
+
expect(RGB.parse('#FFFF00').asHSV().round().asArray()).toStrictEqual([60, 100, 100, 1]);
|
|
78
|
+
expect(RGB.parse('#80FF00').asHSV().round().asArray()).toStrictEqual([90, 100, 100, 1]);
|
|
79
|
+
expect(RGB.parse('#00FF00').asHSV().round().asArray()).toStrictEqual([120, 100, 100, 1]);
|
|
80
|
+
expect(RGB.parse('#00FF80').asHSV().round().asArray()).toStrictEqual([150, 100, 100, 1]);
|
|
81
|
+
expect(RGB.parse('#00FFFF').asHSV().round().asArray()).toStrictEqual([180, 100, 100, 1]);
|
|
82
|
+
expect(RGB.parse('#0080FF').asHSV().round().asArray()).toStrictEqual([210, 100, 100, 1]);
|
|
83
|
+
expect(RGB.parse('#0000FF').asHSV().round().asArray()).toStrictEqual([240, 100, 100, 1]);
|
|
84
|
+
expect(RGB.parse('#8000FF').asHSV().round().asArray()).toStrictEqual([270, 100, 100, 1]);
|
|
85
|
+
expect(RGB.parse('#FF00FF').asHSV().round().asArray()).toStrictEqual([300, 100, 100, 1]);
|
|
86
|
+
expect(RGB.parse('#FF0080').asHSV().round().asArray()).toStrictEqual([330, 100, 100, 1]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('asRGB and toRGB return the same instance or clone', () => {
|
|
90
|
+
const color = new RGB(255, 128, 64, 0.5);
|
|
91
|
+
expect(color.asRGB()).toStrictEqual(color);
|
|
92
|
+
expect(color.toRGB()).toStrictEqual(color);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('handles black correctly', () => {
|
|
96
|
+
const color = new RGB(0, 0, 0);
|
|
97
|
+
expect(color.asHSL().asArray().map(value => Math.round(value))).toStrictEqual([0, 0, 0, 1]);
|
|
98
|
+
expect(color.asHSV().asArray().map(value => Math.round(value))).toStrictEqual([0, 0, 0, 1]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('handles white correctly', () => {
|
|
102
|
+
const color = new RGB(255, 255, 255);
|
|
103
|
+
expect(color.asHSL().asArray().map(value => Math.round(value))).toStrictEqual([0, 0, 100, 1]);
|
|
104
|
+
expect(color.asHSV().asArray().map(value => Math.round(value))).toStrictEqual([0, 0, 100, 1]);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('parse', () => {
|
|
109
|
+
test('parses hexadecimal color strings correctly', () => {
|
|
110
|
+
expect(RGB.parse('#ff8040').asArray()).toStrictEqual([255, 128, 64, 1]);
|
|
111
|
+
expect(RGB.parse('#ff804066').asArray()).toStrictEqual([255, 128, 64, 0.4]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('parses shorthand hexadecimal strings correctly', () => {
|
|
115
|
+
expect(RGB.parse('#fa4').asArray()).toStrictEqual([255, 170, 68, 1]);
|
|
116
|
+
expect(RGB.parse('#fa43').asArray()).toStrictEqual([255, 170, 68, 0.2]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('parses RGB strings correctly', () => {
|
|
120
|
+
expect(RGB.parse('rgb(255, 128, 64)').asArray()).toStrictEqual([255, 128, 64, 1]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('parses RGBA strings correctly', () => {
|
|
124
|
+
expect(RGB.parse('rgba(255, 128, 64, 0.5)').asArray()).toStrictEqual([255, 128, 64, 0.5]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('throws an error for invalid strings', () => {
|
|
128
|
+
expect(() => RGB.parse('invalid')).toThrow('Invalid RGB color string: "invalid"');
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('filter methods', () => {
|
|
133
|
+
function pc(cb: (c: RGB) => RGB): [number, number, number, number] {
|
|
134
|
+
return cb(new RGB(50, 150, 200, 0.8)).round().asArray();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
describe('gamma', () => {
|
|
138
|
+
test('adjusts gamma correctly', () => {
|
|
139
|
+
expect(pc(c => c.gamma(2.2))).toStrictEqual([7, 79, 149, 0.8]);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('clamps extreme gamma values', () => {
|
|
143
|
+
expect(pc(c => c.gamma(0.001))).toStrictEqual([255, 255, 255, 0.8]);
|
|
144
|
+
expect(pc(c => c.gamma(1000))).toStrictEqual([0, 0, 0, 0.8]);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('invert', () => {
|
|
149
|
+
test('inverts RGB values correctly', () => {
|
|
150
|
+
expect(pc(c => c.invert())).toStrictEqual([205, 105, 55, 0.8]);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('contrast', () => {
|
|
155
|
+
test('adjusts contrast correctly', () => {
|
|
156
|
+
expect(pc(c => c.contrast(1.5))).toStrictEqual([11, 161, 236, 0.8]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('clamps extreme contrast values', () => {
|
|
160
|
+
expect(pc(c => c.contrast(1e6))).toStrictEqual([0, 255, 255, 0.8]);
|
|
161
|
+
expect(pc(c => c.contrast(0))).toStrictEqual([128, 128, 128, 0.8]);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('brightness', () => {
|
|
166
|
+
test('increases brightness correctly', () => {
|
|
167
|
+
expect(pc(c => c.brightness(0.5))).toStrictEqual([153, 203, 228, 0.8]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('decreases brightness correctly', () => {
|
|
171
|
+
expect(pc(c => c.brightness(-0.5))).toStrictEqual([25, 75, 100, 0.8]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('clamps brightness values', () => {
|
|
175
|
+
expect(pc(c => c.brightness(2))).toStrictEqual([255, 255, 255, 0.8]);
|
|
176
|
+
expect(pc(c => c.brightness(-2))).toStrictEqual([0, 0, 0, 0.8]);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('tint', () => {
|
|
181
|
+
test('tints color correctly', () => {
|
|
182
|
+
const tintColor = new RGB(255, 0, 0);
|
|
183
|
+
expect(pc(c => c.tint(0.5, tintColor))).toStrictEqual([125, 100, 125, 0.8]);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('handles extreme tint values', () => {
|
|
187
|
+
const tintColor = new RGB(255, 0, 0);
|
|
188
|
+
expect(pc(c => c.tint(1, tintColor))).toStrictEqual([200, 50, 50, 0.8]);
|
|
189
|
+
expect(pc(c => c.tint(0, tintColor))).toStrictEqual([50, 150, 200, 0.8]);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('lighten', () => {
|
|
194
|
+
test('lightens the color correctly', () => {
|
|
195
|
+
expect(pc(c => c.lighten(0.5))).toStrictEqual([153, 203, 228, 0.8]);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('clamps lighten ratio', () => {
|
|
199
|
+
expect(pc(c => c.lighten(2))).toStrictEqual([255, 255, 255, 0.8]);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('darken', () => {
|
|
204
|
+
test('darkens the color correctly', () => {
|
|
205
|
+
expect(pc(c => c.darken(0.5))).toStrictEqual([25, 75, 100, 0.8]);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('clamps darken ratio', () => {
|
|
209
|
+
expect(pc(c => c.darken(1))).toStrictEqual([0, 0, 0, 0.8]);
|
|
210
|
+
expect(pc(c => c.darken(2))).toStrictEqual([0, 0, 0, 0.8]);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('fade', () => {
|
|
215
|
+
test('reduces alpha correctly', () => {
|
|
216
|
+
expect(pc(c => c.fade(0.5))).toStrictEqual([50, 150, 200, 0.4]);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('handles extreme fade values', () => {
|
|
220
|
+
expect(pc(c => c.fade(1))).toStrictEqual([50, 150, 200, 0]);
|
|
221
|
+
|
|
222
|
+
const fullyOpaque = new RGB(50, 150, 200, 1);
|
|
223
|
+
expect(fullyOpaque.fade(0).asArray()).toStrictEqual([50, 150, 200, 1]);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
});
|
package/src/color/rgb.ts
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { HSL } from './hsl.js';
|
|
2
|
+
import { HSV } from './hsv.js';
|
|
3
|
+
import { Color } from './abstract.js';
|
|
4
|
+
import { clamp, formatFloat } from './utils.js';
|
|
5
|
+
|
|
6
|
+
export class RGB extends Color {
|
|
7
|
+
readonly r: number = 0; // between 0 and 255
|
|
8
|
+
readonly g: number = 0; // between 0 and 255
|
|
9
|
+
readonly b: number = 0; // between 0 and 255
|
|
10
|
+
readonly a: number = 1; // between 0 and 1
|
|
11
|
+
|
|
12
|
+
constructor(r: number, g: number, b: number, a: number = 1) {
|
|
13
|
+
super();
|
|
14
|
+
this.r = clamp(r, 0, 255);
|
|
15
|
+
this.g = clamp(g, 0, 255);
|
|
16
|
+
this.b = clamp(b, 0, 255);
|
|
17
|
+
this.a = clamp(a, 0, 1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
clone(): RGB {
|
|
21
|
+
return new RGB(this.r, this.g, this.b, this.a);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
asArray(): [number, number, number, number] {
|
|
25
|
+
return [this.r, this.g, this.b, this.a];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
round(): RGB {
|
|
29
|
+
return new RGB(
|
|
30
|
+
Math.round(this.r),
|
|
31
|
+
Math.round(this.g),
|
|
32
|
+
Math.round(this.b),
|
|
33
|
+
Math.round(this.a * 1000) / 1000,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
asString(): string {
|
|
38
|
+
if (this.a === 1) {
|
|
39
|
+
return `rgb(${this.r.toFixed(0)},${this.g.toFixed(0)},${this.b.toFixed(0)})`;
|
|
40
|
+
} else {
|
|
41
|
+
return `rgba(${this.r.toFixed(0)},${this.g.toFixed(0)},${this.b.toFixed(0)},${formatFloat(this.a, 3)})`;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
asHex(): string {
|
|
46
|
+
const r = Math.round(this.r).toString(16).padStart(2, '0');
|
|
47
|
+
const g = Math.round(this.g).toString(16).padStart(2, '0');
|
|
48
|
+
const b = Math.round(this.b).toString(16).padStart(2, '0');
|
|
49
|
+
|
|
50
|
+
if (this.a === 1) {
|
|
51
|
+
return `#${r}${g}${b}`.toUpperCase();
|
|
52
|
+
} else {
|
|
53
|
+
const a = Math.round(this.a * 255).toString(16).padStart(2, '0');
|
|
54
|
+
return `#${r}${g}${b}${a}`.toUpperCase();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
asHSL(): HSL {
|
|
59
|
+
const r = this.r / 255;
|
|
60
|
+
const g = this.g / 255;
|
|
61
|
+
const b = this.b / 255;
|
|
62
|
+
const min = Math.min(r, g, b);
|
|
63
|
+
const max = Math.max(r, g, b);
|
|
64
|
+
const delta = max - min;
|
|
65
|
+
let h = 0;
|
|
66
|
+
let s = 0;
|
|
67
|
+
|
|
68
|
+
if (max === min) h = 0;
|
|
69
|
+
else if (r === max) h = (g - b) / delta;
|
|
70
|
+
else if (g === max) h = 2 + (b - r) / delta;
|
|
71
|
+
else if (b === max) h = 4 + (r - g) / delta;
|
|
72
|
+
|
|
73
|
+
h = Math.min(h * 60, 360);
|
|
74
|
+
if (h < 0) h += 360;
|
|
75
|
+
|
|
76
|
+
const l = (min + max) / 2;
|
|
77
|
+
|
|
78
|
+
if (max === min) s = 0;
|
|
79
|
+
else if (l <= 0.5) s = delta / (max + min);
|
|
80
|
+
else s = delta / (2 - max - min);
|
|
81
|
+
|
|
82
|
+
return new HSL(h, s * 100, l * 100, this.a);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
asHSV(): HSV {
|
|
86
|
+
const r = this.r / 255;
|
|
87
|
+
const g = this.g / 255;
|
|
88
|
+
const b = this.b / 255;
|
|
89
|
+
const v = Math.max(r, g, b);
|
|
90
|
+
const diff = v - Math.min(r, g, b);
|
|
91
|
+
|
|
92
|
+
let h = 0;
|
|
93
|
+
let s = 0;
|
|
94
|
+
if (diff !== 0) {
|
|
95
|
+
function diffc(c: number): number {
|
|
96
|
+
return (v - c) / 6 / diff + 1 / 2;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
s = diff / v;
|
|
100
|
+
const rdif = diffc(r);
|
|
101
|
+
const gdif = diffc(g);
|
|
102
|
+
const bdif = diffc(b);
|
|
103
|
+
|
|
104
|
+
if (r === v) h = bdif - gdif;
|
|
105
|
+
else if (g === v) h = (1 / 3) + rdif - bdif;
|
|
106
|
+
else if (b === v) h = (2 / 3) + gdif - rdif;
|
|
107
|
+
|
|
108
|
+
if (h < 0) h += 1;
|
|
109
|
+
else if (h > 1) h -= 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return new HSV(h * 360, s * 100, v * 100, this.a);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
asRGB(): RGB {
|
|
116
|
+
return this.clone();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
toRGB(): RGB {
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
static parse(input: string | Color): RGB {
|
|
124
|
+
if (input instanceof Color) return input.asRGB();
|
|
125
|
+
|
|
126
|
+
input = input.toLowerCase().replaceAll(/[^0-9a-z.#,()]/g, '')
|
|
127
|
+
|
|
128
|
+
let match;
|
|
129
|
+
|
|
130
|
+
match = input.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})?$/);
|
|
131
|
+
if (match) {
|
|
132
|
+
const r = parseInt(match[1], 16);
|
|
133
|
+
const g = parseInt(match[2], 16);
|
|
134
|
+
const b = parseInt(match[3], 16);
|
|
135
|
+
const a = match[4] ? parseInt(match[4], 16) / 255 : 1;
|
|
136
|
+
return new RGB(r, g, b, a);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
match = input.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])?$/);
|
|
140
|
+
if (match) {
|
|
141
|
+
const r = parseInt(match[1], 16) * 17;
|
|
142
|
+
const g = parseInt(match[2], 16) * 17;
|
|
143
|
+
const b = parseInt(match[3], 16) * 17;
|
|
144
|
+
const a = match[4] ? parseInt(match[4], 16) / 15 : 1;
|
|
145
|
+
return new RGB(r, g, b, a);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
input = input.trim().toLowerCase().replaceAll(' ', '');
|
|
149
|
+
|
|
150
|
+
match = input.match(/^rgb\((\d+),(\d+),(\d+)\)$/);
|
|
151
|
+
if (match) {
|
|
152
|
+
const r = parseInt(match[1]);
|
|
153
|
+
const g = parseInt(match[2]);
|
|
154
|
+
const b = parseInt(match[3]);
|
|
155
|
+
return new RGB(r, g, b);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
match = input.match(/^rgba\((\d+),(\d+),(\d+),([.\d]+)\)$/);
|
|
159
|
+
if (match) {
|
|
160
|
+
const r = parseInt(match[1]);
|
|
161
|
+
const g = parseInt(match[2]);
|
|
162
|
+
const b = parseInt(match[3]);
|
|
163
|
+
const a = parseFloat(match[4]);
|
|
164
|
+
return new RGB(r, g, b, a);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
throw new Error(`Invalid RGB color string: "${input}"`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
gamma(value: number): RGB {
|
|
172
|
+
if (value < 1e-3) value = 1e-3;
|
|
173
|
+
if (value > 1e3) value = 1e3;
|
|
174
|
+
return new RGB(
|
|
175
|
+
Math.pow(this.r / 255, value) * 255,
|
|
176
|
+
Math.pow(this.g / 255, value) * 255,
|
|
177
|
+
Math.pow(this.b / 255, value) * 255,
|
|
178
|
+
this.a
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
invert(): RGB {
|
|
183
|
+
return new RGB(
|
|
184
|
+
255 - this.r,
|
|
185
|
+
255 - this.g,
|
|
186
|
+
255 - this.b,
|
|
187
|
+
this.a
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
contrast(value: number): RGB {
|
|
192
|
+
if (value < 0) value = 0;
|
|
193
|
+
if (value > 1e6) value = 1e6;
|
|
194
|
+
return new RGB(
|
|
195
|
+
clamp((this.r - 127.5) * value + 127.5, 0, 255),
|
|
196
|
+
clamp((this.g - 127.5) * value + 127.5, 0, 255),
|
|
197
|
+
clamp((this.b - 127.5) * value + 127.5, 0, 255),
|
|
198
|
+
this.a
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
brightness(value: number): RGB {
|
|
203
|
+
if (value < -1) value = -1;
|
|
204
|
+
if (value > 1) value = 1;
|
|
205
|
+
const a = 1 - Math.abs(value);
|
|
206
|
+
const b = (value < 0) ? 0 : 255 * value;
|
|
207
|
+
return new RGB(
|
|
208
|
+
this.r * a + b,
|
|
209
|
+
this.g * a + b,
|
|
210
|
+
this.b * a + b,
|
|
211
|
+
this.a
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
tint(value: number, tintColor: Color): RGB {
|
|
216
|
+
if (value < 0) value = 0;
|
|
217
|
+
if (value > 1) value = 1;
|
|
218
|
+
const rgbNew = this.setHue(tintColor.toHSV().h).toRGB();
|
|
219
|
+
return new RGB(
|
|
220
|
+
this.r * (1 - value) + value * rgbNew.r,
|
|
221
|
+
this.g * (1 - value) + value * rgbNew.g,
|
|
222
|
+
this.b * (1 - value) + value * rgbNew.b,
|
|
223
|
+
this.a
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
lighten(ratio: number): RGB {
|
|
228
|
+
return new RGB(
|
|
229
|
+
clamp(255 - (255 - this.r) * (1 - ratio), 0, 255),
|
|
230
|
+
clamp(255 - (255 - this.g) * (1 - ratio), 0, 255),
|
|
231
|
+
clamp(255 - (255 - this.b) * (1 - ratio), 0, 255),
|
|
232
|
+
this.a
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
darken(ratio: number): RGB {
|
|
237
|
+
return new RGB(
|
|
238
|
+
clamp(this.r * (1 - ratio), 0, 255),
|
|
239
|
+
clamp(this.g * (1 - ratio), 0, 255),
|
|
240
|
+
clamp(this.b * (1 - ratio), 0, 255),
|
|
241
|
+
this.a
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
fade(value: number): RGB {
|
|
246
|
+
return new RGB(this.r, this.g, this.b, this.a * (1 - value));
|
|
247
|
+
}
|
|
248
|
+
}
|