@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,135 @@
|
|
|
1
|
+
|
|
2
|
+
import { Color } from '../color/index.js';
|
|
3
|
+
import expandBraces from 'brace-expansion';
|
|
4
|
+
import maplibreProperties from '../shortbread/properties.js';
|
|
5
|
+
import { deepMerge } from '../lib/utils.js';
|
|
6
|
+
import type { MaplibreLayer } from '../types/index.js';
|
|
7
|
+
import type { StyleRule, StyleRuleValue, StyleRules } from './types.js';
|
|
8
|
+
import type { CachedRecolor } from './recolor.js';
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
export function decorate(layers: MaplibreLayer[], rules: StyleRules, recolor: CachedRecolor): MaplibreLayer[] {
|
|
13
|
+
const layerIds = layers.map(l => l.id);
|
|
14
|
+
const layerIdSet = new Set(layerIds);
|
|
15
|
+
|
|
16
|
+
// Initialize a new map to hold final styles for layers
|
|
17
|
+
const layerStyles = new Map<string, StyleRule>();
|
|
18
|
+
|
|
19
|
+
// Iterate through the generated layer style rules
|
|
20
|
+
Object.entries(rules).forEach(([idDef, layerStyle]) => {
|
|
21
|
+
if (layerStyle == null) return;
|
|
22
|
+
|
|
23
|
+
// Expand any braces in IDs and filter them through a RegExp if necessary
|
|
24
|
+
const ids = expandBraces(idDef).flatMap(id => {
|
|
25
|
+
if (!id.includes('*')) return id;
|
|
26
|
+
const regExpString = id.replace(/[^a-z_:-]/g, c => {
|
|
27
|
+
if (c === '*') return '[a-z_-]*';
|
|
28
|
+
throw new Error('unknown char to process. Do not know how to make a RegExp from: ' + JSON.stringify(c));
|
|
29
|
+
});
|
|
30
|
+
const regExp = new RegExp(`^${regExpString}$`, 'i');
|
|
31
|
+
return layerIds.filter(layerId => regExp.test(layerId));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
ids.forEach(id => {
|
|
35
|
+
if (!layerIdSet.has(id)) return;
|
|
36
|
+
layerStyles.set(id, deepMerge(layerStyles.get(id) ?? {}, layerStyle));
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Deep clone the original layers and apply styles
|
|
41
|
+
return layers.flatMap(layer => {
|
|
42
|
+
// Get the id and style of the layer
|
|
43
|
+
const layerStyle = layerStyles.get(layer.id);
|
|
44
|
+
|
|
45
|
+
// Don't export layers that have no style
|
|
46
|
+
if (!layerStyle) return [];
|
|
47
|
+
|
|
48
|
+
processStyling(layer, layerStyle);
|
|
49
|
+
|
|
50
|
+
return [layer];
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Function to process each style attribute for the layer
|
|
54
|
+
function processStyling(layer: MaplibreLayer, styleRule: StyleRule): void {
|
|
55
|
+
|
|
56
|
+
for (const [ruleKeyCamelCase, ruleValue] of Object.entries(styleRule)) {
|
|
57
|
+
if (ruleValue == null) continue;
|
|
58
|
+
|
|
59
|
+
// CamelCase to not-camel-case
|
|
60
|
+
const ruleKey = ruleKeyCamelCase.replace(/[A-Z]/g, c => '-' + c.toLowerCase());
|
|
61
|
+
|
|
62
|
+
const propertyDefs = maplibreProperties.get(layer.type + '/' + ruleKey);
|
|
63
|
+
if (!propertyDefs) continue;
|
|
64
|
+
|
|
65
|
+
propertyDefs.forEach(propertyDef => {
|
|
66
|
+
const { key } = propertyDef;
|
|
67
|
+
let value: StyleRuleValue = ruleValue;
|
|
68
|
+
|
|
69
|
+
switch (propertyDef.valueType) {
|
|
70
|
+
case 'color': value = processExpression(value, processColor); break;
|
|
71
|
+
case 'fonts': value = processExpression(value, processFont); break;
|
|
72
|
+
case 'resolvedImage':
|
|
73
|
+
case 'formatted':
|
|
74
|
+
case 'array':
|
|
75
|
+
case 'boolean':
|
|
76
|
+
case 'enum':
|
|
77
|
+
case 'number': value = processExpression(value); break;
|
|
78
|
+
default: throw new Error(`unknown propertyDef.valueType "${propertyDef.valueType}" for key "${key}"`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
switch (propertyDef.parent) {
|
|
82
|
+
case 'layer':
|
|
83
|
+
// @ts-expect-error: too complex to handle
|
|
84
|
+
layer[key] = value;
|
|
85
|
+
break;
|
|
86
|
+
case 'layout':
|
|
87
|
+
if (!layer.layout) layer.layout = {};
|
|
88
|
+
// @ts-expect-error: too complex to handle
|
|
89
|
+
layer.layout[key] = value;
|
|
90
|
+
break;
|
|
91
|
+
case 'paint':
|
|
92
|
+
if (!layer.paint) layer.paint = {};
|
|
93
|
+
// @ts-expect-error: too complex to handle
|
|
94
|
+
layer.paint[key] = value;
|
|
95
|
+
break;
|
|
96
|
+
default:
|
|
97
|
+
|
|
98
|
+
throw new Error(`unknown parent "${propertyDef.parent}" for key "${key}"`);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function processColor(value: StyleRuleValue): string {
|
|
104
|
+
if (typeof value === 'string') value = Color.parse(value);
|
|
105
|
+
if (value instanceof Color) {
|
|
106
|
+
const color = recolor.do(value as Color);
|
|
107
|
+
return color.asString()
|
|
108
|
+
}
|
|
109
|
+
throw new Error(`unknown color type "${typeof value}"`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function processFont(value: StyleRuleValue): string[] {
|
|
113
|
+
if (typeof value === 'string') return [value];
|
|
114
|
+
throw new Error(`unknown font type "${typeof value}"`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function processExpression(value: StyleRuleValue, cbValue?: (value: StyleRuleValue) => StyleRuleValue): StyleRuleValue {
|
|
118
|
+
if (typeof value === 'object') {
|
|
119
|
+
if (value instanceof Color) return processColor(value);
|
|
120
|
+
if (!Array.isArray(value)) {
|
|
121
|
+
return processZoomStops(value as Record<string, StyleRuleValue>, cbValue);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return cbValue ? cbValue(value) : value;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function processZoomStops(obj: Record<string, StyleRuleValue>, cbValue?: (value: StyleRuleValue) => StyleRuleValue): { stops: StyleRuleValue[] } {
|
|
128
|
+
return {
|
|
129
|
+
stops: Object.entries(obj)
|
|
130
|
+
.map(([z, v]) => [parseInt(z, 10), cbValue ? cbValue(v) : v] as [number, StyleRuleValue])
|
|
131
|
+
.sort((a, b) => a[0] - b[0]),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { Color } from '../color/index.js';
|
|
2
|
+
import { CachedRecolor, getDefaultRecolorFlags, recolorArray, recolorObject } from './recolor.js';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
describe('recolor', () => {
|
|
6
|
+
describe('getDefaultRecolorFlags', () => {
|
|
7
|
+
it('should return the default color transformer flags', () => {
|
|
8
|
+
const defaultFlags = getDefaultRecolorFlags();
|
|
9
|
+
expect(defaultFlags).toEqual({
|
|
10
|
+
invertBrightness: false,
|
|
11
|
+
rotate: 0,
|
|
12
|
+
saturate: 0,
|
|
13
|
+
gamma: 1,
|
|
14
|
+
contrast: 1,
|
|
15
|
+
brightness: 0,
|
|
16
|
+
tint: 0,
|
|
17
|
+
tintColor: '#FF0000',
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should not alter the colors if no flags are provided', () => {
|
|
23
|
+
const colors = getTestColors();
|
|
24
|
+
recolorArray(colors, {});
|
|
25
|
+
expect(colors).toEqual(getTestColors());
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('invertBrightness', () => {
|
|
29
|
+
it('should invert brightness when invert flag is true', () => {
|
|
30
|
+
const colors = getTestColors();
|
|
31
|
+
recolorArray(colors, { invertBrightness: true });
|
|
32
|
+
expect(colors2string(colors)).toBe('AA550000,00FFAA55,5500FFAA,FFAA55,AA7755');
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('rotate', () => {
|
|
37
|
+
it('should rotate colors 120°', () => {
|
|
38
|
+
const colors = getTestColors();
|
|
39
|
+
recolorArray(colors, { rotate: 120 });
|
|
40
|
+
expect(colors2string(colors)).toBe('55FFAA00,AA00FF55,FF5500AA,00AA55,55AA77');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should rotate colors 180°', () => {
|
|
44
|
+
const colors = getTestColors();
|
|
45
|
+
recolorArray(colors, { rotate: 180 });
|
|
46
|
+
expect(colors2string(colors)).toBe('55AAFF00,FF005555,AAFF00AA,0055AA,5588AA');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should rotate colors 240°', () => {
|
|
50
|
+
const colors = getTestColors();
|
|
51
|
+
recolorArray(colors, { rotate: 240 });
|
|
52
|
+
expect(colors2string(colors)).toBe('AA55FF00,FFAA0055,00FF55AA,5500AA,7755AA');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('saturation', () => {
|
|
57
|
+
it('should remove any saturation', () => {
|
|
58
|
+
const colors = getTestColors();
|
|
59
|
+
recolorArray(colors, { saturate: -1.0 });
|
|
60
|
+
expect(colors2string(colors)).toBe('AAAAAA00,80808055,808080AA,555555,808080');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should decrease saturation', () => {
|
|
64
|
+
const colors = getTestColors();
|
|
65
|
+
recolorArray(colors, { saturate: -0.5 });
|
|
66
|
+
expect(colors2string(colors)).toBe('D4AA7F00,40BF9555,6A40BFAA,7F552A,957B6A');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should increase saturation', () => {
|
|
70
|
+
const colors = getTestColors();
|
|
71
|
+
recolorArray(colors, { saturate: 0.5 });
|
|
72
|
+
expect(colors2string(colors)).toBe('FFAA5500,00FFAA55,5500FFAA,AA5500,BF7340');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should maximize saturation', () => {
|
|
76
|
+
const colors = getTestColors();
|
|
77
|
+
recolorArray(colors, { saturate: 1.0 });
|
|
78
|
+
expect(colors2string(colors)).toBe('FFAA5500,00FFAA55,5500FFAA,AA5500,D46F2B');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('gamma', () => {
|
|
83
|
+
it('should decrease gamma', () => {
|
|
84
|
+
const colors = getTestColors();
|
|
85
|
+
recolorArray(colors, { gamma: 0.5 });
|
|
86
|
+
expect(colors2string(colors)).toBe('FFD09300,00FFD055,9300FFAA,D09300,D0AE93');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should increase gamma', () => {
|
|
90
|
+
const colors = getTestColors();
|
|
91
|
+
recolorArray(colors, { gamma: 2 });
|
|
92
|
+
expect(colors2string(colors)).toBe('FF711C00,00FF7155,1C00FFAA,711C00,71381C');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('contrast', () => {
|
|
97
|
+
it('should remove any contrast', () => {
|
|
98
|
+
const colors = getTestColors();
|
|
99
|
+
recolorArray(colors, { contrast: 0 });
|
|
100
|
+
expect(colors2string(colors)).toBe('80808000,80808055,808080AA,808080,808080');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should decrease contrast', () => {
|
|
104
|
+
const colors = getTestColors();
|
|
105
|
+
recolorArray(colors, { contrast: 0.5 });
|
|
106
|
+
expect(colors2string(colors)).toBe('BF956A00,40BF9555,6A40BFAA,956A40,957B6A');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should increase contrast', () => {
|
|
110
|
+
const colors = getTestColors();
|
|
111
|
+
recolorArray(colors, { contrast: 2 });
|
|
112
|
+
expect(colors2string(colors)).toBe('FFD52B00,00FFD555,2B00FFAA,D52B00,D56F2B');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should maximize contrast', () => {
|
|
116
|
+
const colors = getTestColors();
|
|
117
|
+
recolorArray(colors, { contrast: Infinity });
|
|
118
|
+
expect(colors2string(colors)).toBe('FFFF0000,00FFFF55,0000FFAA,FF0000,FF0000');
|
|
119
|
+
});
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
describe('brightness', () => {
|
|
123
|
+
it('should remove any brightness', () => {
|
|
124
|
+
const colors = getTestColors();
|
|
125
|
+
recolorArray(colors, { brightness: -1 });
|
|
126
|
+
expect(colors2string(colors)).toBe('00000000,00000055,000000AA,000000,000000');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should decrease brightness', () => {
|
|
130
|
+
const colors = getTestColors();
|
|
131
|
+
recolorArray(colors, { brightness: -0.5 });
|
|
132
|
+
expect(colors2string(colors)).toBe('80552B00,00805555,2B0080AA,552B00,553C2B');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should increase brightness', () => {
|
|
136
|
+
const colors = getTestColors();
|
|
137
|
+
recolorArray(colors, { brightness: 0.5 });
|
|
138
|
+
expect(colors2string(colors)).toBe('FFD5AA00,80FFD555,AA80FFAA,D5AA80,D5BBAA');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should maximize brightness', () => {
|
|
142
|
+
const colors = getTestColors();
|
|
143
|
+
recolorArray(colors, { brightness: 1 });
|
|
144
|
+
expect(colors2string(colors)).toBe('FFFFFF00,FFFFFF55,FFFFFFAA,FFFFFF,FFFFFF');
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('tint', () => {
|
|
149
|
+
it('should not tint at all', () => {
|
|
150
|
+
const colors = getTestColors();
|
|
151
|
+
recolorArray(colors, { tint: 0, tintColor: '#F00' });
|
|
152
|
+
expect(colors2string(colors)).toBe('FFAA5500,00FFAA55,5500FFAA,AA5500,AA7755');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should tint a little bit red', () => {
|
|
156
|
+
const colors = getTestColors();
|
|
157
|
+
recolorArray(colors, { tint: 0.5, tintColor: '#F00' });
|
|
158
|
+
expect(colors2string(colors)).toBe('FF805500,80805555,AA0080AA,AA2B00,AA6655');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should tint a little bit yellow', () => {
|
|
162
|
+
const colors = getTestColors();
|
|
163
|
+
recolorArray(colors, { tint: 0.2, tintColor: '#FF0' });
|
|
164
|
+
expect(colors2string(colors)).toBe('FFBB5500,33FF8855,7733CCAA,AA6600,AA8155');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should tint a little bit green', () => {
|
|
168
|
+
const colors = getTestColors();
|
|
169
|
+
recolorArray(colors, { tint: 0.2, tintColor: '#0F0' });
|
|
170
|
+
expect(colors2string(colors)).toBe('DDBB5500,00FF8855,4433CCAA,886600,998155');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should tint a little bit blue', () => {
|
|
174
|
+
const colors = getTestColors();
|
|
175
|
+
recolorArray(colors, { tint: 0.2, tintColor: '#00F' });
|
|
176
|
+
expect(colors2string(colors)).toBe('DD997700,00CCBB55,4400FFAA,884422,997066');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should tint strongly orange', () => {
|
|
180
|
+
const colors = getTestColors();
|
|
181
|
+
recolorArray(colors, { tint: 0.8, tintColor: '#F80' });
|
|
182
|
+
expect(colors2string(colors)).toBe('FFAF5500,CCA02255,DD6D33AA,AA5A00,AA8055');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should tint a strongly blue', () => {
|
|
186
|
+
const colors = getTestColors();
|
|
187
|
+
recolorArray(colors, { tint: 0.8, tintColor: '#00F' });
|
|
188
|
+
expect(colors2string(colors)).toBe('7766DD00,0033EE55,1100FFAA,221188,665C99');
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('recolorObject', () => {
|
|
194
|
+
it('should recolor an object of colors', () => {
|
|
195
|
+
const colors = {
|
|
196
|
+
color1: Color.parse('#FA50'),
|
|
197
|
+
color2: Color.parse('#0FA5'),
|
|
198
|
+
color3: Color.parse('#50FA'),
|
|
199
|
+
};
|
|
200
|
+
recolorObject(colors, { rotate: 120 });
|
|
201
|
+
expect(colors.color1.asHex()).toBe('#55FFAA00');
|
|
202
|
+
expect(colors.color2.asHex()).toBe('#AA00FF55');
|
|
203
|
+
expect(colors.color3.asHex()).toBe('#FF5500AA');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should not alter colors if options are invalid', () => {
|
|
207
|
+
const colors = {
|
|
208
|
+
color1: Color.parse('#FA50'),
|
|
209
|
+
color2: Color.parse('#0FA5'),
|
|
210
|
+
color3: Color.parse('#50FA'),
|
|
211
|
+
};
|
|
212
|
+
const original = { ...colors };
|
|
213
|
+
recolorObject(colors, {});
|
|
214
|
+
expect(colors).toEqual(original);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe('recolorArray', () => {
|
|
219
|
+
it('should recolor an array of colors with valid options', () => {
|
|
220
|
+
const colors = getTestColors();
|
|
221
|
+
recolorArray(colors, { rotate: 120 });
|
|
222
|
+
expect(colors.map((c) => c.asHex())).toEqual([
|
|
223
|
+
'#55FFAA00',
|
|
224
|
+
'#AA00FF55',
|
|
225
|
+
'#FF5500AA',
|
|
226
|
+
'#00AA55',
|
|
227
|
+
'#55AA77',
|
|
228
|
+
]);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should not alter the array if options are invalid', () => {
|
|
232
|
+
const colors = getTestColors();
|
|
233
|
+
const original = [...colors];
|
|
234
|
+
recolorArray(colors, {});
|
|
235
|
+
expect(colors).toEqual(original);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should handle multiple transformations on the same array', () => {
|
|
239
|
+
const colors = getTestColors();
|
|
240
|
+
recolorArray(colors, { saturate: 0.5 });
|
|
241
|
+
expect(colors.map((c) => c.asHex())).toEqual([
|
|
242
|
+
'#FFAA5500',
|
|
243
|
+
'#00FFAA55',
|
|
244
|
+
'#5500FFAA',
|
|
245
|
+
'#AA5500',
|
|
246
|
+
'#BF7340',
|
|
247
|
+
]);
|
|
248
|
+
|
|
249
|
+
recolorArray(colors, { brightness: 0.5 });
|
|
250
|
+
expect(colors.map((c) => c.asHex())).toEqual([
|
|
251
|
+
'#FFD5AA00',
|
|
252
|
+
'#80FFD555',
|
|
253
|
+
'#AA80FFAA',
|
|
254
|
+
'#D5AA80',
|
|
255
|
+
'#DFB99F',
|
|
256
|
+
]);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe('CachedRecolor', () => {
|
|
261
|
+
it('should apply recolor transformations and cache the results', () => {
|
|
262
|
+
const cachedRecolor = new CachedRecolor({ rotate: 120 });
|
|
263
|
+
const color = Color.parse('#FA50');
|
|
264
|
+
const recolored = cachedRecolor.do(color);
|
|
265
|
+
expect(recolored.asHex()).toBe('#55FFAA00');
|
|
266
|
+
|
|
267
|
+
// Verify cached result
|
|
268
|
+
const cachedResult = cachedRecolor.do(color);
|
|
269
|
+
expect(cachedResult).toBe(recolored); // Cached object should be the same instance
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('should skip recoloring if options are invalid', () => {
|
|
273
|
+
const cachedRecolor = new CachedRecolor({});
|
|
274
|
+
const color = Color.parse('#FA50');
|
|
275
|
+
const recolored = cachedRecolor.do(color);
|
|
276
|
+
expect(recolored).toBe(color); // No changes applied
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should handle multiple recoloring operations', () => {
|
|
280
|
+
const cachedRecolor = new CachedRecolor({ saturate: 0.5 });
|
|
281
|
+
const colors = getTestColors();
|
|
282
|
+
|
|
283
|
+
const recoloredColors = colors.map((color) => cachedRecolor.do(color));
|
|
284
|
+
expect(recoloredColors.map((c) => c.asHex())).toEqual([
|
|
285
|
+
'#FFAA5500',
|
|
286
|
+
'#00FFAA55',
|
|
287
|
+
'#5500FFAA',
|
|
288
|
+
'#AA5500',
|
|
289
|
+
'#BF7340',
|
|
290
|
+
]);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
function getTestColors(): Color[] {
|
|
295
|
+
return [
|
|
296
|
+
Color.parse('#FA50'),
|
|
297
|
+
Color.parse('#0FA5'),
|
|
298
|
+
Color.parse('#50FA'),
|
|
299
|
+
Color.parse('#A50F'),
|
|
300
|
+
Color.parse('#A75F'),
|
|
301
|
+
];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function colors2string(colors: Color[]): string {
|
|
305
|
+
return colors.map(c => c.asHex().slice(1)).join(',');
|
|
306
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Color } from '../color/index.js';
|
|
2
|
+
|
|
3
|
+
export interface RecolorOptions {
|
|
4
|
+
// If true, inverts all colors.
|
|
5
|
+
invertBrightness?: boolean;
|
|
6
|
+
|
|
7
|
+
// Rotate the hue of all colors (in degrees).
|
|
8
|
+
rotate?: number;
|
|
9
|
+
|
|
10
|
+
// Adjusts the saturation level of all colors. Positive values increase saturation, negative values decrease it.
|
|
11
|
+
saturate?: number;
|
|
12
|
+
|
|
13
|
+
// Adjusts the gamma of all colors. Affects the brightness in a non-linear manner.
|
|
14
|
+
gamma?: number;
|
|
15
|
+
|
|
16
|
+
// Adjusts the contrast of all colors. Higher values produce more contrast.
|
|
17
|
+
contrast?: number;
|
|
18
|
+
|
|
19
|
+
// Adjusts the brightness of all colors. Positive values make it brighter, negative values make it darker.
|
|
20
|
+
brightness?: number;
|
|
21
|
+
|
|
22
|
+
// Specifies the intensity of the tinting effect. Ranges from 0 (no effect) to 1 (full effect).
|
|
23
|
+
tint?: number;
|
|
24
|
+
|
|
25
|
+
// Specifies the color used for tinting, in a string format (e.g., '#FF0000').
|
|
26
|
+
tintColor?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getDefaultRecolorFlags(): RecolorOptions {
|
|
30
|
+
return {
|
|
31
|
+
invertBrightness: false,
|
|
32
|
+
rotate: 0,
|
|
33
|
+
saturate: 0,
|
|
34
|
+
gamma: 1,
|
|
35
|
+
contrast: 1,
|
|
36
|
+
brightness: 0,
|
|
37
|
+
tint: 0,
|
|
38
|
+
tintColor: '#FF0000',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isValidRecolorOptions(opt?: RecolorOptions): opt is RecolorOptions {
|
|
43
|
+
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;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function recolorObject(colors: Record<string, Color>, opt?: RecolorOptions): void {
|
|
56
|
+
if (!isValidRecolorOptions(opt)) return;
|
|
57
|
+
|
|
58
|
+
for (const [k, c] of Object.entries(colors)) {
|
|
59
|
+
colors[k] = recolor(c, opt);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function recolorArray(colors: Color[], opt?: RecolorOptions): void {
|
|
64
|
+
if (!isValidRecolorOptions(opt)) return;
|
|
65
|
+
|
|
66
|
+
for (let i = 0; i < colors.length; i++) {
|
|
67
|
+
colors[i] = recolor(colors[i], opt);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class CachedRecolor {
|
|
72
|
+
private readonly skip: boolean;
|
|
73
|
+
|
|
74
|
+
private readonly opt?: RecolorOptions;
|
|
75
|
+
|
|
76
|
+
private readonly cache: Map<string, Color>;
|
|
77
|
+
|
|
78
|
+
public constructor(opt?: RecolorOptions) {
|
|
79
|
+
this.skip = !isValidRecolorOptions(opt);
|
|
80
|
+
this.cache = new Map();
|
|
81
|
+
this.opt = opt;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public do(color: Color): Color {
|
|
85
|
+
if (this.skip) return color;
|
|
86
|
+
|
|
87
|
+
const key = color.asHex();
|
|
88
|
+
|
|
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;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function recolor(color: Color, opt?: RecolorOptions): Color {
|
|
99
|
+
if (!isValidRecolorOptions(opt)) return color;
|
|
100
|
+
|
|
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));
|
|
108
|
+
|
|
109
|
+
return color;
|
|
110
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
|
|
2
|
+
import { Color } from '../color/index.js';
|
|
3
|
+
import type { StyleRules, StyleRulesOptions } from './types.js';
|
|
4
|
+
import { StyleBuilder } from './style_builder.js';
|
|
5
|
+
import { VectorSourceSpecification } from '@maplibre/maplibre-gl-style-spec';
|
|
6
|
+
import Colorful from '../styles/colorful.js';
|
|
7
|
+
|
|
8
|
+
// Mock class for abstract class StyleBuilder
|
|
9
|
+
class MockStyleBuilder extends Colorful {
|
|
10
|
+
public readonly name = 'mock';
|
|
11
|
+
|
|
12
|
+
public defaultFonts = { regular: 'Arial', bold: 'Courier' };
|
|
13
|
+
|
|
14
|
+
public invertColors(): void {
|
|
15
|
+
this.transformDefaultColors(color => color.invert());
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
protected getStyleRules(opt: StyleRulesOptions): StyleRules {
|
|
19
|
+
for (const color of Object.values(opt.colors)) if (!(color instanceof Color)) throw Error();
|
|
20
|
+
for (const font of Object.values(opt.fonts)) if (typeof font !== 'string') throw Error();
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
'water-area': {
|
|
24
|
+
textColor: opt.colors.land,
|
|
25
|
+
textSize: 12,
|
|
26
|
+
textFont: opt.fonts.regular,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('StyleBuilder', () => {
|
|
33
|
+
let builder: MockStyleBuilder;
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
builder = new MockStyleBuilder();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should create an instance of StyleBuilder', () => {
|
|
40
|
+
expect(builder).toBeInstanceOf(StyleBuilder);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should build a MaplibreStyle object', () => {
|
|
44
|
+
const style = builder.build();
|
|
45
|
+
expect(style).toBeDefined();
|
|
46
|
+
expect(style).toHaveProperty('name');
|
|
47
|
+
expect(style).toHaveProperty('layers');
|
|
48
|
+
expect(style).toHaveProperty('glyphs');
|
|
49
|
+
expect(style).toHaveProperty('sprite');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should transform colors correctly', () => {
|
|
53
|
+
const initialColor = Color.parse(builder.defaultColors.land).asHex();
|
|
54
|
+
builder.invertColors();
|
|
55
|
+
const newColor = Color.parse(builder.defaultColors.land).asHex();
|
|
56
|
+
expect(newColor).not.toBe(initialColor);
|
|
57
|
+
expect(newColor).toBe(Color.parse(initialColor).invert().asHex());
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should create default options', () => {
|
|
61
|
+
expect(builder.getDefaultOptions()).toStrictEqual({
|
|
62
|
+
baseUrl: '',
|
|
63
|
+
colors: expect.any(Object),
|
|
64
|
+
fonts: { regular: 'Arial', bold: 'Courier' },
|
|
65
|
+
glyphs: '',
|
|
66
|
+
hideLabels: false,
|
|
67
|
+
language: undefined,
|
|
68
|
+
recolor: {
|
|
69
|
+
brightness: 0,
|
|
70
|
+
contrast: 1,
|
|
71
|
+
gamma: 1,
|
|
72
|
+
invertBrightness: false,
|
|
73
|
+
rotate: 0,
|
|
74
|
+
saturate: 0,
|
|
75
|
+
tint: 0,
|
|
76
|
+
tintColor: '#FF0000',
|
|
77
|
+
},
|
|
78
|
+
sprite: '',
|
|
79
|
+
tiles: [],
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('build method', () => {
|
|
84
|
+
it('should create a style object', () => {
|
|
85
|
+
const style = builder.build();
|
|
86
|
+
expect(style).toBeDefined();
|
|
87
|
+
expect(style).toHaveProperty('layers');
|
|
88
|
+
expect(style).toHaveProperty('name');
|
|
89
|
+
expect(style).toHaveProperty('glyphs');
|
|
90
|
+
expect(style).toHaveProperty('sprite');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should resolve urls correctly', () => {
|
|
94
|
+
const style = builder.build({ baseUrl: 'https://my.base.url/' });
|
|
95
|
+
expect(style.glyphs).toBe('https://my.base.url/assets/glyphs/{fontstack}/{range}.pbf');
|
|
96
|
+
expect(style.sprite).toStrictEqual([{ id: 'basics', url: 'https://my.base.url/assets/sprites/basics/sprites' }]);
|
|
97
|
+
|
|
98
|
+
const source = style.sources['versatiles-shortbread'] as VectorSourceSpecification;
|
|
99
|
+
expect(source).toHaveProperty('tiles');
|
|
100
|
+
expect(source.tiles).toStrictEqual(['https://my.base.url/tiles/osm/{z}/{x}/{y}']);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|