@versatiles/style 5.5.1 → 5.6.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@versatiles/style",
3
- "version": "5.5.1",
3
+ "version": "5.6.0",
4
4
  "description": "Generate StyleJSON for MapLibre",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -41,27 +41,27 @@
41
41
  "devDependencies": {
42
42
  "@maplibre/maplibre-gl-native": "^6.0.0",
43
43
  "@maplibre/maplibre-gl-style-spec": "^23.1.0",
44
- "@rollup/plugin-commonjs": "^28.0.2",
45
- "@rollup/plugin-node-resolve": "^16.0.0",
44
+ "@rollup/plugin-commonjs": "^28.0.3",
45
+ "@rollup/plugin-node-resolve": "^16.0.1",
46
46
  "@rollup/plugin-terser": "^0.4.4",
47
47
  "@rollup/plugin-typescript": "^12.1.2",
48
48
  "@types/bin-pack": "^1.0.3",
49
49
  "@types/brace-expansion": "^1.1.2",
50
50
  "@types/inquirer": "^9.0.7",
51
51
  "@types/jest": "^29.5.14",
52
- "@types/node": "^22.13.5",
52
+ "@types/node": "^22.13.10",
53
53
  "@types/tar-stream": "^3.1.3",
54
- "@typescript-eslint/eslint-plugin": "^8.25.0",
55
- "@typescript-eslint/parser": "^8.25.0",
56
- "@versatiles/release-tool": "^2.2.4",
54
+ "@typescript-eslint/eslint-plugin": "^8.26.1",
55
+ "@typescript-eslint/parser": "^8.26.1",
56
+ "@versatiles/release-tool": "^2.4.1",
57
57
  "bin-pack": "^1.0.2",
58
- "esbuild": "^0.25.0",
59
- "eslint": "^9.21.0",
60
- "inquirer": "^12.4.2",
58
+ "esbuild": "^0.25.1",
59
+ "eslint": "^9.22.0",
60
+ "inquirer": "^12.4.3",
61
61
  "jest": "^29.7.0",
62
62
  "jest-environment-jsdom": "^29.7.0",
63
63
  "jest-ts-webcompat-resolver": "^1.0.0",
64
- "rollup": "^4.34.8",
64
+ "rollup": "^4.35.0",
65
65
  "rollup-plugin-dts": "^6.1.1",
66
66
  "rollup-plugin-sourcemaps2": "^0.5.0",
67
67
  "sharp": "^0.33.5",
@@ -69,7 +69,7 @@
69
69
  "ts-jest": "^29.2.6",
70
70
  "ts-node": "^10.9.2",
71
71
  "tsx": "^4.19.3",
72
- "typescript": "^5.7.3",
73
- "typescript-eslint": "^8.25.0"
72
+ "typescript": "^5.8.2",
73
+ "typescript-eslint": "^8.26.1"
74
74
  }
75
75
  }
@@ -75,6 +75,10 @@ export abstract class Color {
75
75
  return this.toRGB().tint(value, tintColor);
76
76
  }
77
77
 
78
+ blend(value: number, blendColor: Color): RGB {
79
+ return this.toRGB().blend(value, blendColor);
80
+ }
81
+
78
82
  setHue(value: number): HSV {
79
83
  return this.toHSV().setHue(value);
80
84
  }
@@ -134,94 +134,62 @@ describe('RGB Class', () => {
134
134
  return cb(new RGB(50, 150, 200, 0.8)).round().asArray();
135
135
  }
136
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
- });
137
+ test('adjusts gamma correctly', () => {
138
+ expect(pc(c => c.gamma(2.2))).toStrictEqual([7, 79, 149, 0.8]);
139
+ expect(pc(c => c.gamma(0.001))).toStrictEqual([255, 255, 255, 0.8]);
140
+ expect(pc(c => c.gamma(1000))).toStrictEqual([0, 0, 0, 0.8]);
146
141
  });
147
142
 
148
- describe('invert', () => {
149
- test('inverts RGB values correctly', () => {
150
- expect(pc(c => c.invert())).toStrictEqual([205, 105, 55, 0.8]);
151
- });
143
+ test('inverts RGB values correctly', () => {
144
+ expect(pc(c => c.invert())).toStrictEqual([205, 105, 55, 0.8]);
152
145
  });
153
146
 
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
- });
147
+ test('adjusts contrast correctly', () => {
148
+ expect(pc(c => c.contrast(1.5))).toStrictEqual([11, 161, 236, 0.8]);
149
+ expect(pc(c => c.contrast(1e6))).toStrictEqual([0, 255, 255, 0.8]);
150
+ expect(pc(c => c.contrast(0))).toStrictEqual([128, 128, 128, 0.8]);
163
151
  });
164
152
 
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
- });
153
+ test('increases brightness correctly', () => {
154
+ expect(pc(c => c.brightness(0.5))).toStrictEqual([153, 203, 228, 0.8]);
155
+ expect(pc(c => c.brightness(-0.5))).toStrictEqual([25, 75, 100, 0.8]);
156
+ expect(pc(c => c.brightness(2))).toStrictEqual([255, 255, 255, 0.8]);
157
+ expect(pc(c => c.brightness(-2))).toStrictEqual([0, 0, 0, 0.8]);
178
158
  });
179
159
 
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
- });
160
+ test('tints color correctly', () => {
161
+ const tintColor = new RGB(255, 0, 0);
162
+ expect(pc(c => c.tint(0.5, tintColor))).toStrictEqual([125, 100, 125, 0.8]);
163
+ expect(pc(c => c.tint(1, tintColor))).toStrictEqual([200, 50, 50, 0.8]);
164
+ expect(pc(c => c.tint(0, tintColor))).toStrictEqual([50, 150, 200, 0.8]);
191
165
  });
192
166
 
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
- });
167
+ test('blends color correctly', () => {
168
+ const blendColor = new RGB(255, 0, 0);
169
+ expect(pc(c => c.blend(0.2, blendColor))).toStrictEqual([91, 120, 160, 0.8]);
170
+ expect(pc(c => c.blend(0.5, blendColor))).toStrictEqual([153, 75, 100, 0.8]);
171
+ expect(pc(c => c.blend(0.8, blendColor))).toStrictEqual([214, 30, 40, 0.8]);
172
+ expect(pc(c => c.blend(1, blendColor))).toStrictEqual([255, 0, 0, 0.8]);
173
+ expect(pc(c => c.blend(0, blendColor))).toStrictEqual([50, 150, 200, 0.8]);
201
174
  });
202
175
 
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
- });
176
+ test('lightens the color correctly', () => {
177
+ expect(pc(c => c.lighten(0.5))).toStrictEqual([153, 203, 228, 0.8]);
178
+ expect(pc(c => c.lighten(2))).toStrictEqual([255, 255, 255, 0.8]);
212
179
  });
213
180
 
214
- describe('fade', () => {
215
- test('reduces alpha correctly', () => {
216
- expect(pc(c => c.fade(0.5))).toStrictEqual([50, 150, 200, 0.4]);
217
- });
181
+ test('darkens the color correctly', () => {
182
+ expect(pc(c => c.darken(0.5))).toStrictEqual([25, 75, 100, 0.8]);
183
+ expect(pc(c => c.darken(1))).toStrictEqual([0, 0, 0, 0.8]);
184
+ expect(pc(c => c.darken(2))).toStrictEqual([0, 0, 0, 0.8]);
185
+ });
218
186
 
219
- test('handles extreme fade values', () => {
220
- expect(pc(c => c.fade(1))).toStrictEqual([50, 150, 200, 0]);
187
+ test('fades color correctly', () => {
188
+ expect(pc(c => c.fade(0.5))).toStrictEqual([50, 150, 200, 0.4]);
189
+ expect(pc(c => c.fade(1))).toStrictEqual([50, 150, 200, 0]);
221
190
 
222
- const fullyOpaque = new RGB(50, 150, 200, 1);
223
- expect(fullyOpaque.fade(0).asArray()).toStrictEqual([50, 150, 200, 1]);
224
- });
191
+ const fullyOpaque = new RGB(50, 150, 200, 1);
192
+ expect(fullyOpaque.fade(0).asArray()).toStrictEqual([50, 150, 200, 1]);
225
193
  });
226
194
  });
227
195
  });
package/src/color/rgb.ts CHANGED
@@ -224,6 +224,17 @@ export class RGB extends Color {
224
224
  )
225
225
  }
226
226
 
227
+ blend(value: number, blendColor: Color): RGB {
228
+ value = clamp(value ?? 0, 0, 1);
229
+ const rgbNew = blendColor.toRGB();
230
+ return new RGB(
231
+ this.r * (1 - value) + value * rgbNew.r,
232
+ this.g * (1 - value) + value * rgbNew.g,
233
+ this.b * (1 - value) + value * rgbNew.b,
234
+ this.a
235
+ );
236
+ }
237
+
227
238
  lighten(ratio: number): RGB {
228
239
  return new RGB(
229
240
  clamp(255 - (255 - this.r) * (1 - ratio), 0, 255),
@@ -1,11 +1,17 @@
1
1
 
2
2
 
3
- export function clamp(num: number, min: number, max: number): number {
4
- return Math.min(Math.max(min, num), max);
3
+ export function clamp(value: number, min: number, max: number): number {
4
+ if (value == null || isNaN(value)) return min;
5
+ if (value < min) return min;
6
+ if (value > max) return max;
7
+ return value;
5
8
  }
6
9
 
7
- export function mod(num: number, max: number): number {
8
- return ((num % max) + max) % max;
10
+ export function mod(value: number, max: number): number {
11
+ value = value % max;
12
+ if (value < 0) value += max;
13
+ if (value == 0) return 0;
14
+ return value;
9
15
  }
10
16
 
11
17
  export function formatFloat(num: number, precision: number): string {
@@ -61,7 +61,7 @@ describe('guessStyle', () => {
61
61
  it('should build shortbread vector styles', () => {
62
62
  const style = guessStyle({ tiles, vector_layers: vectorLayersShortbread }, { baseUrl: 'http://example.com' });
63
63
 
64
- expect(style.layers.length).toBe(297);
64
+ expect(style.layers.length).toBe(309);
65
65
  style.layers = [];
66
66
 
67
67
  expect(style).toStrictEqual({
@@ -193,7 +193,6 @@ export function getShortbreadLayers(option: { readonly language: Language }): Ma
193
193
  });
194
194
 
195
195
  // no links
196
- const noDrivewayExpression: LegacyFilterSpecification = ['!=', 'service', 'driveway'];
197
196
  ['track', 'pedestrian', 'service', 'living_street', 'residential', 'unclassified'].forEach(t => {
198
197
  results.push({
199
198
  id: prefix + 'street-' + t.replace(/_/g, '') + suffix,
@@ -202,7 +201,6 @@ export function getShortbreadLayers(option: { readonly language: Language }): Ma
202
201
  filter: ['all',
203
202
  ['==', 'kind', t],
204
203
  ...filter,
205
- ...(t === 'service') ? [noDrivewayExpression] : [], // ignore driveways
206
204
  ],
207
205
  });
208
206
  });
@@ -217,7 +215,6 @@ export function getShortbreadLayers(option: { readonly language: Language }): Ma
217
215
  ['==', 'kind', t],
218
216
  ['==', 'bicycle', 'designated'],
219
217
  ...filter,
220
- ...(t === 'service') ? [noDrivewayExpression] : [], // ignore driveways
221
218
  ],
222
219
  });
223
220
  });
@@ -254,8 +251,11 @@ export function getShortbreadLayers(option: { readonly language: Language }): Ma
254
251
 
255
252
  // separate outline for trains
256
253
  [':outline', ''].forEach(suffix => {
257
- // transport
258
- ['rail', 'light_rail', 'subway', 'narrow_gauge', 'tram', 'funicular', 'monorail', 'bus_guideway', 'busway'].reverse().forEach((t) => {
254
+
255
+ // with service distinction
256
+ ['rail', 'light_rail', 'subway', 'narrow_gauge', 'tram'].reverse().forEach((t) => {
257
+
258
+ // main rail
259
259
  results.push({
260
260
  id: prefix + 'transport-' + t.replace(/_/g, '') + suffix,
261
261
  type: 'line',
@@ -266,6 +266,31 @@ export function getShortbreadLayers(option: { readonly language: Language }): Ma
266
266
  ...filter,
267
267
  ],
268
268
  });
269
+
270
+ // service rail (crossover, siding, spur, yard)
271
+ results.push({
272
+ id: prefix + 'transport-' + t.replace(/_/g, '') + '-service' + suffix,
273
+ type: 'line',
274
+ 'source-layer': 'streets',
275
+ filter: ['all',
276
+ ['in', 'kind', t],
277
+ ['has', 'service'],
278
+ ...filter,
279
+ ],
280
+ });
281
+ });
282
+
283
+ // other transport
284
+ ['funicular', 'monorail', 'bus_guideway', 'busway'].reverse().forEach((t) => {
285
+ results.push({
286
+ id: prefix + 'transport-' + t.replace(/_/g, '') + suffix,
287
+ type: 'line',
288
+ 'source-layer': 'streets',
289
+ filter: ['all',
290
+ ['in', 'kind', t],
291
+ ...filter,
292
+ ],
293
+ });
269
294
  });
270
295
 
271
296
  if (c === 'street') {
@@ -15,6 +15,8 @@ describe('recolor', () => {
15
15
  brightness: 0,
16
16
  tint: 0,
17
17
  tintColor: '#FF0000',
18
+ blend: 0,
19
+ blendColor: '#000000',
18
20
  });
19
21
  });
20
22
  });
@@ -188,6 +190,50 @@ describe('recolor', () => {
188
190
  expect(colors2string(colors)).toBe('7766DD00,0033EE55,1100FFAA,221188,665C99');
189
191
  });
190
192
  });
193
+
194
+ describe('blend', () => {
195
+ it('should not blend at all', () => {
196
+ const colors = getTestColors();
197
+ recolorArray(colors, { blend: 0, blendColor: '#F00' });
198
+ expect(colors2string(colors)).toBe('FFAA5500,00FFAA55,5500FFAA,AA5500,AA7755');
199
+ });
200
+
201
+ it('should blend a little bit red', () => {
202
+ const colors = getTestColors();
203
+ recolorArray(colors, { blend: 0.5, blendColor: '#F00' });
204
+ expect(colors2string(colors)).toBe('FF552B00,80805555,AA0080AA,D52B00,D53C2B');
205
+ });
206
+
207
+ it('should blend a little bit yellow', () => {
208
+ const colors = getTestColors();
209
+ recolorArray(colors, { blend: 0.2, blendColor: '#FF0' });
210
+ expect(colors2string(colors)).toBe('FFBB4400,33FF8855,7733CCAA,BB7700,BB9244');
211
+ });
212
+
213
+ it('should blend a little bit green', () => {
214
+ const colors = getTestColors();
215
+ recolorArray(colors, { blend: 0.2, blendColor: '#0F0' });
216
+ expect(colors2string(colors)).toBe('CCBB4400,00FF8855,4433CCAA,887700,889244');
217
+ });
218
+
219
+ it('should blend a little bit blue', () => {
220
+ const colors = getTestColors();
221
+ recolorArray(colors, { blend: 0.2, blendColor: '#00F' });
222
+ expect(colors2string(colors)).toBe('CC887700,00CCBB55,4400FFAA,884433,885F77');
223
+ });
224
+
225
+ it('should blend strongly orange', () => {
226
+ const colors = getTestColors();
227
+ recolorArray(colors, { blend: 0.8, blendColor: '#F80' });
228
+ expect(colors2string(colors)).toBe('FF8F1100,CCA02255,DD6D33AA,EE7E00,EE8511');
229
+ });
230
+
231
+ it('should blend a strongly blue', () => {
232
+ const colors = getTestColors();
233
+ recolorArray(colors, { blend: 0.8, blendColor: '#00F' });
234
+ expect(colors2string(colors)).toBe('3322DD00,0033EE55,1100FFAA,2211CC,2218DD');
235
+ });
236
+ });
191
237
  });
192
238
 
193
239
  describe('recolorObject', () => {
@@ -1,31 +1,124 @@
1
+ /**
2
+ * Module for applying various color transformations such as hue rotation, saturation, contrast, brightness,
3
+ * tinting, and blending. These transformations are defined through the `RecolorOptions` interface.
4
+ */
5
+
1
6
  import { Color } from '../color/index.js';
2
7
 
8
+ /**
9
+ * Configuration options for recoloring all map colors.
10
+ *
11
+ * The transformations (if specified) are done in the following order:
12
+ * 1. [Invert brightness](#invertbrightness)
13
+ * 2. [Rotate hue](#rotate)
14
+ * 3. [Saturate](#saturate)
15
+ * 4. [Gamma correction](#gamma)
16
+ * 5. [Contrast adjustment](#contrast)
17
+ * 6. [Brightness adjustment](#brightness)
18
+ * 7. [Tinting](#tint)
19
+ * 8. [Blending](#blend)
20
+ *
21
+ * Usage Examples
22
+ *
23
+ * ```typescript
24
+ * const style = VersaTilesStyle.colorful({
25
+ * recolor: {
26
+ * rotate: 180,
27
+ * saturate: 0.5,
28
+ * brightness: 0.2,
29
+ * }
30
+ * };
31
+ * ```
32
+ *
33
+ * If you want do make you map simply brighter or darker, you can use the `blend` option:
34
+ * ```typescript
35
+ * const style = VersaTilesStyle.colorful({
36
+ * recolor: {
37
+ * blend: 0.5,
38
+ * blendColor: '#000000', // to blend all colors with black
39
+ * // or blendColor: '#FFFFFF', // to blend all colors with white
40
+ * }
41
+ * };
42
+ * ```
43
+ *
44
+ */
45
+
3
46
  export interface RecolorOptions {
4
- // If true, inverts all colors.
47
+ /**
48
+ * If true, inverts all colors' brightness.
49
+ * See also {@link HSL.invertLuminosity}
50
+ */
5
51
  invertBrightness?: boolean;
6
52
 
7
- // Rotate the hue of all colors (in degrees).
53
+ /**
54
+ * Rotate the hue of all colors in degrees (0-360).
55
+ * See also {@link HSL.rotateHue}
56
+ */
8
57
  rotate?: number;
9
58
 
10
- // Adjusts the saturation level of all colors. Positive values increase saturation, negative values decrease it.
59
+ /**
60
+ * Adjust the saturation level. Positive values increase, negative values decrease saturation.
61
+ * |value|effect |
62
+ * |----:|-----------------|
63
+ * | -1|grayscale |
64
+ * | 0|no effect |
65
+ * | 1|double saturation|
66
+ *
67
+ * See also {@link HSL.saturate}
68
+ */
11
69
  saturate?: number;
12
70
 
13
- // Adjusts the gamma of all colors. Affects the brightness in a non-linear manner.
71
+ /**
72
+ * Adjust the gamma (non-linear brightness adjustment).
73
+ * Defaults to 1.
74
+ * See also {@link RGB.gamma}
75
+ */
14
76
  gamma?: number;
15
77
 
16
- // Adjusts the contrast of all colors. Higher values produce more contrast.
78
+ /**
79
+ * Adjust the contrast level.
80
+ * Values > 1 increase contrast, values < 1 decrease it.
81
+ * Defaults to 1.
82
+ * See also {@link RGB.contrast}
83
+ */
17
84
  contrast?: number;
18
85
 
19
- // Adjusts the brightness of all colors. Positive values make it brighter, negative values make it darker.
86
+ /**
87
+ * Adjust the brightness level.
88
+ * Positive values make it brighter, negative values make it darker.
89
+ * Defaults to 0.
90
+ * See also {@link RGB.brightness}
91
+ */
20
92
  brightness?: number;
21
93
 
22
- // Specifies the intensity of the tinting effect. Ranges from 0 (no effect) to 1 (full effect).
94
+ /**
95
+ * Intensity of the tinting effect (0 = none, 1 = full effect).
96
+ * See also {@link RGB.tint}
97
+ */
23
98
  tint?: number;
24
99
 
25
- // Specifies the color used for tinting, in a string format (e.g., '#FF0000').
100
+ /**
101
+ * The tinting color in hex format (default: '#FF0000').
102
+ * See also {@link RGB.tint}
103
+ */
26
104
  tintColor?: string;
105
+
106
+ /**
107
+ * Intensity of the blending effect (0 = none, 1 = full effect).
108
+ * See also {@link RGB.blend}
109
+ */
110
+ blend?: number;
111
+
112
+ /**
113
+ * The blending color in hex format (default: '#000000').
114
+ * See also {@link RGB.blend}
115
+ */
116
+ blendColor?: string;
27
117
  }
28
118
 
119
+ /**
120
+ * Returns the default recolor settings.
121
+ */
29
122
  export function getDefaultRecolorFlags(): RecolorOptions {
30
123
  return {
31
124
  invertBrightness: false,
@@ -36,30 +129,49 @@ export function getDefaultRecolorFlags(): RecolorOptions {
36
129
  brightness: 0,
37
130
  tint: 0,
38
131
  tintColor: '#FF0000',
132
+ blend: 0,
133
+ blendColor: '#000000',
39
134
  };
40
135
  }
41
136
 
137
+ /**
138
+ * Checks if the given options object contains any active recolor transformations.
139
+ * @param opt The recolor options to validate.
140
+ */
42
141
  function isValidRecolorOptions(opt?: RecolorOptions): opt is RecolorOptions {
43
142
  if (!opt) return false;
44
- if ((opt.invertBrightness != null) && opt.invertBrightness) return true;
45
- if ((opt.rotate != null) && (opt.rotate !== 0)) return true;
46
- if ((opt.saturate != null) && (opt.saturate !== 0)) return true;
47
- if ((opt.gamma != null) && (opt.gamma !== 1)) return true;
48
- if ((opt.contrast != null) && (opt.contrast !== 1)) return true;
49
- if ((opt.brightness != null) && (opt.brightness !== 0)) return true;
50
- if ((opt.tint != null) && (opt.tint !== 0)) return true;
51
- if ((opt.tintColor != null) && (opt.tintColor !== '#FF0000')) return true;
52
- return false;
143
+ return (
144
+ opt.invertBrightness ||
145
+ (opt.rotate != null && opt.rotate !== 0) ||
146
+ (opt.saturate != null && opt.saturate !== 0) ||
147
+ (opt.gamma != null && opt.gamma !== 1) ||
148
+ (opt.contrast != null && opt.contrast !== 1) ||
149
+ (opt.brightness != null && opt.brightness !== 0) ||
150
+ (opt.tint != null && opt.tint !== 0) ||
151
+ (opt.tintColor != null && opt.tintColor !== '#FF0000') ||
152
+ (opt.blend != null && opt.blend !== 0) ||
153
+ (opt.blendColor != null && opt.blendColor !== '#000000')
154
+ );
53
155
  }
54
156
 
157
+ /**
158
+ * Applies recoloring transformations to a record of colors.
159
+ * @param colors A record of color names to `Color` instances.
160
+ * @param opt Optional recolor options.
161
+ */
55
162
  export function recolorObject(colors: Record<string, Color>, opt?: RecolorOptions): void {
56
163
  if (!isValidRecolorOptions(opt)) return;
57
164
 
58
- for (const [k, c] of Object.entries(colors)) {
59
- colors[k] = recolor(c, opt);
165
+ for (const [key, color] of Object.entries(colors)) {
166
+ colors[key] = recolor(color, opt);
60
167
  }
61
168
  }
62
169
 
170
+ /**
171
+ * Applies recoloring transformations to an array of colors.
172
+ * @param colors An array of `Color` instances.
173
+ * @param opt Optional recolor options.
174
+ */
63
175
  export function recolorArray(colors: Color[], opt?: RecolorOptions): void {
64
176
  if (!isValidRecolorOptions(opt)) return;
65
177
 
@@ -68,43 +180,58 @@ export function recolorArray(colors: Color[], opt?: RecolorOptions): void {
68
180
  }
69
181
  }
70
182
 
183
+ /**
184
+ * Caches recolored colors to optimize performance.
185
+ */
71
186
  export class CachedRecolor {
72
187
  private readonly skip: boolean;
73
-
74
188
  private readonly opt?: RecolorOptions;
75
-
76
189
  private readonly cache: Map<string, Color>;
77
190
 
191
+ /**
192
+ * Creates a cached recolor instance.
193
+ * @param opt Optional recolor options.
194
+ */
78
195
  public constructor(opt?: RecolorOptions) {
79
196
  this.skip = !isValidRecolorOptions(opt);
80
197
  this.cache = new Map();
81
198
  this.opt = opt;
82
199
  }
83
200
 
201
+ /**
202
+ * Applies cached recoloring to a given color.
203
+ * @param color The color to recolor.
204
+ * @returns The recolored color, either from cache or newly computed.
205
+ */
84
206
  public do(color: Color): Color {
85
207
  if (this.skip) return color;
86
208
 
87
209
  const key = color.asHex();
210
+ if (this.cache.has(key)) return this.cache.get(key)!;
88
211
 
89
- const result = this.cache.get(key);
90
- if (result) return result;
91
-
92
- color = recolor(color, this.opt);
93
- this.cache.set(key, color);
94
- return color;
212
+ const recolored = recolor(color, this.opt);
213
+ this.cache.set(key, recolored);
214
+ return recolored;
95
215
  }
96
216
  }
97
217
 
218
+ /**
219
+ * Applies the specified recoloring transformations to a single color.
220
+ * @param color The original color.
221
+ * @param opt Optional recolor options.
222
+ * @returns A new `Color` instance with applied transformations.
223
+ */
98
224
  export function recolor(color: Color, opt?: RecolorOptions): Color {
99
225
  if (!isValidRecolorOptions(opt)) return color;
100
226
 
101
- if (opt.invertBrightness ?? false) color = color.invertLuminosity();
102
- if ((opt.rotate !== undefined) && (opt.rotate !== 0)) color = color.rotateHue(opt.rotate);
103
- if ((opt.saturate !== undefined) && (opt.saturate !== 0)) color = color.saturate(opt.saturate);
104
- if ((opt.gamma !== undefined) && (opt.gamma !== 1)) color = color.gamma(opt.gamma);
105
- if ((opt.contrast !== undefined) && (opt.contrast !== 1)) color = color.contrast(opt.contrast);
106
- if ((opt.brightness !== undefined) && (opt.brightness !== 0)) color = color.brightness(opt.brightness);
107
- if ((opt.tint !== undefined) && (opt.tintColor !== undefined) && (opt.tint !== 0)) color = color.tint(opt.tint, Color.parse(opt.tintColor));
227
+ if (opt.invertBrightness) color = color.invertLuminosity();
228
+ if (opt.rotate) color = color.rotateHue(opt.rotate);
229
+ if (opt.saturate) color = color.saturate(opt.saturate);
230
+ if (opt.gamma != null && opt.gamma != 1) color = color.gamma(opt.gamma);
231
+ if (opt.contrast != null && opt.contrast != 1) color = color.contrast(opt.contrast);
232
+ if (opt.brightness) color = color.brightness(opt.brightness);
233
+ if (opt.tint && opt.tintColor != null) color = color.tint(opt.tint, Color.parse(opt.tintColor));
234
+ if (opt.blend && opt.blendColor != null) color = color.blend(opt.blend, Color.parse(opt.blendColor));
108
235
 
109
236
  return color;
110
237
  }
@@ -80,6 +80,8 @@ describe('StyleBuilder', () => {
80
80
  saturate: 0,
81
81
  tint: 0,
82
82
  tintColor: '#FF0000',
83
+ blend: 0,
84
+ blendColor: '#000000',
83
85
  },
84
86
  sprite: '',
85
87
  tiles: [],