@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.
Files changed (130) hide show
  1. package/dist/index.d.ts +274 -14
  2. package/dist/index.js +3650 -11
  3. package/dist/index.js.map +1 -1
  4. package/package.json +8 -8
  5. package/src/color/abstract.ts +83 -0
  6. package/src/color/hsl.test.ts +182 -0
  7. package/src/color/hsl.ts +122 -0
  8. package/src/color/hsv.test.ts +174 -0
  9. package/src/color/hsv.ts +100 -0
  10. package/src/color/index.test.ts +119 -0
  11. package/src/color/index.ts +38 -0
  12. package/src/color/random.test.ts +35 -0
  13. package/src/color/random.ts +165 -0
  14. package/src/color/rgb.test.ts +227 -0
  15. package/src/color/rgb.ts +248 -0
  16. package/src/color/utils.test.ts +86 -0
  17. package/src/color/utils.ts +13 -0
  18. package/src/guess_style/guess_style.test.ts +134 -0
  19. package/src/guess_style/guess_style.ts +166 -0
  20. package/{dist/guess_style/index.d.ts → src/guess_style/index.ts} +1 -0
  21. package/src/index.test.ts +77 -0
  22. package/src/index.ts +18 -0
  23. package/src/lib/utils.test.ts +197 -0
  24. package/src/lib/utils.ts +134 -0
  25. package/{dist/shortbread/index.d.ts → src/shortbread/index.ts} +1 -0
  26. package/src/shortbread/layers.test.ts +36 -0
  27. package/src/shortbread/layers.ts +564 -0
  28. package/src/shortbread/properties.test.ts +44 -0
  29. package/src/shortbread/properties.ts +142 -0
  30. package/src/shortbread/template.test.ts +43 -0
  31. package/src/shortbread/template.ts +343 -0
  32. package/src/style_builder/decorator.test.ts +67 -0
  33. package/src/style_builder/decorator.ts +135 -0
  34. package/src/style_builder/recolor.test.ts +306 -0
  35. package/src/style_builder/recolor.ts +110 -0
  36. package/src/style_builder/style_builder.test.ts +103 -0
  37. package/src/style_builder/style_builder.ts +134 -0
  38. package/src/style_builder/types.ts +141 -0
  39. package/src/styles/LICENSE.md +41 -0
  40. package/src/styles/colorful.ts +1041 -0
  41. package/src/styles/eclipse.ts +11 -0
  42. package/{dist/styles/empty.d.ts → src/styles/empty.ts} +7 -3
  43. package/src/styles/graybeard.ts +11 -0
  44. package/src/styles/index.ts +33 -0
  45. package/src/styles/neutrino.ts +429 -0
  46. package/{dist/types/index.d.ts → src/types/index.ts} +1 -0
  47. package/{dist/types/maplibre.d.ts → src/types/maplibre.ts} +3 -0
  48. package/src/types/tilejson.test.ts +94 -0
  49. package/src/types/tilejson.ts +125 -0
  50. package/src/types/vector_layer.test.ts +64 -0
  51. package/src/types/vector_layer.ts +69 -0
  52. package/dist/color/abstract.d.ts +0 -34
  53. package/dist/color/abstract.js +0 -53
  54. package/dist/color/abstract.js.map +0 -1
  55. package/dist/color/hsl.d.ts +0 -23
  56. package/dist/color/hsl.js +0 -98
  57. package/dist/color/hsl.js.map +0 -1
  58. package/dist/color/hsv.d.ts +0 -20
  59. package/dist/color/hsv.js +0 -100
  60. package/dist/color/hsv.js.map +0 -1
  61. package/dist/color/index.d.ts +0 -6
  62. package/dist/color/index.js +0 -29
  63. package/dist/color/index.js.map +0 -1
  64. package/dist/color/random.d.ts +0 -9
  65. package/dist/color/random.js +0 -134
  66. package/dist/color/random.js.map +0 -1
  67. package/dist/color/rgb.d.ts +0 -28
  68. package/dist/color/rgb.js +0 -195
  69. package/dist/color/rgb.js.map +0 -1
  70. package/dist/color/utils.d.ts +0 -3
  71. package/dist/color/utils.js +0 -10
  72. package/dist/color/utils.js.map +0 -1
  73. package/dist/guess_style/guess_style.d.ts +0 -8
  74. package/dist/guess_style/guess_style.js +0 -147
  75. package/dist/guess_style/guess_style.js.map +0 -1
  76. package/dist/guess_style/index.js +0 -2
  77. package/dist/guess_style/index.js.map +0 -1
  78. package/dist/lib/utils.d.ts +0 -6
  79. package/dist/lib/utils.js +0 -126
  80. package/dist/lib/utils.js.map +0 -1
  81. package/dist/shortbread/index.js +0 -3
  82. package/dist/shortbread/index.js.map +0 -1
  83. package/dist/shortbread/layers.d.ts +0 -5
  84. package/dist/shortbread/layers.js +0 -521
  85. package/dist/shortbread/layers.js.map +0 -1
  86. package/dist/shortbread/properties.d.ts +0 -7
  87. package/dist/shortbread/properties.js +0 -125
  88. package/dist/shortbread/properties.js.map +0 -1
  89. package/dist/shortbread/template.d.ts +0 -4
  90. package/dist/shortbread/template.js +0 -339
  91. package/dist/shortbread/template.js.map +0 -1
  92. package/dist/style_builder/decorator.d.ts +0 -4
  93. package/dist/style_builder/decorator.js +0 -127
  94. package/dist/style_builder/decorator.js.map +0 -1
  95. package/dist/style_builder/recolor.d.ts +0 -22
  96. package/dist/style_builder/recolor.js +0 -89
  97. package/dist/style_builder/recolor.js.map +0 -1
  98. package/dist/style_builder/style_builder.d.ts +0 -15
  99. package/dist/style_builder/style_builder.js +0 -106
  100. package/dist/style_builder/style_builder.js.map +0 -1
  101. package/dist/style_builder/types.d.ts +0 -122
  102. package/dist/style_builder/types.js +0 -3
  103. package/dist/style_builder/types.js.map +0 -1
  104. package/dist/styles/colorful.d.ts +0 -11
  105. package/dist/styles/colorful.js +0 -956
  106. package/dist/styles/colorful.js.map +0 -1
  107. package/dist/styles/eclipse.d.ts +0 -5
  108. package/dist/styles/eclipse.js +0 -9
  109. package/dist/styles/eclipse.js.map +0 -1
  110. package/dist/styles/empty.js +0 -8
  111. package/dist/styles/empty.js.map +0 -1
  112. package/dist/styles/graybeard.d.ts +0 -5
  113. package/dist/styles/graybeard.js +0 -9
  114. package/dist/styles/graybeard.js.map +0 -1
  115. package/dist/styles/index.d.ts +0 -11
  116. package/dist/styles/index.js +0 -20
  117. package/dist/styles/index.js.map +0 -1
  118. package/dist/styles/neutrino.d.ts +0 -11
  119. package/dist/styles/neutrino.js +0 -401
  120. package/dist/styles/neutrino.js.map +0 -1
  121. package/dist/types/index.js +0 -3
  122. package/dist/types/index.js.map +0 -1
  123. package/dist/types/maplibre.js +0 -2
  124. package/dist/types/maplibre.js.map +0 -1
  125. package/dist/types/tilejson.d.ts +0 -32
  126. package/dist/types/tilejson.js +0 -87
  127. package/dist/types/tilejson.js.map +0 -1
  128. package/dist/types/vector_layer.d.ts +0 -14
  129. package/dist/types/vector_layer.js +0 -51
  130. 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
+ });
@@ -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
+ }