@tomtom-org/maps-sdk-plugin-map-effects 0.3.1 → 0.4.1
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/README.md +11 -4
- package/THIRD_PARTY.txt +1 -1
- package/dist/THIRD_PARTY.txt +1 -1
- package/dist/index.d.ts +257 -86
- package/dist/index.es.js +1 -1
- package/dist/third-party.json +1 -1
- package/package.json +12 -12
package/README.md
CHANGED
|
@@ -2,25 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
This plugin for the [TomTom Maps SDK for JavaScript](https://docs.tomtom.com/maps-sdk-js/introduction/overview) adds practical post-processing to a `TomTomMap`. It computes its effects from the rendered map's own pixels, so they work on every style and survive every style release. Editing the style can't reproduce them.
|
|
4
4
|
|
|
5
|
+
> **Public preview.** This plugin is in [Public Preview](https://docs.tomtom.com/legal/public-preview) until its 1.0.0 release.
|
|
6
|
+
|
|
5
7
|
Every effect is off by default. Here is each one, with the problem it solves:
|
|
6
8
|
|
|
7
9
|
| Effect | Knobs | Solves |
|
|
8
10
|
| --- | --- | --- |
|
|
9
11
|
| Bloom | `bloom.intensity`, `bloom.radius`, `bloom.threshold`, `bloom.only`, `bloom.onlyTolerance` | Emphasis on dark and driving styles. Lit roads, traffic tubes and route lines glow, optionally only in the colours `bloom.only` lists |
|
|
12
|
+
| Glare | `glare.intensity`, `glare.length`, `glare.rays`, `glare.angle`, `glare.threshold`, `glare.only`, `glare.onlyTolerance` | Lights that read as lights. The colours `glare.only` lists, the major roads by default, cast streaks as street lamps do through a lens |
|
|
10
13
|
| Grade | `grade.brightness`, `grade.contrast`, `grade.saturation` | Muting the base map under a data overlay |
|
|
11
14
|
| Tint | `tint.color`, `tint.opacity` | Muting the base map, or matching a brand |
|
|
12
|
-
| Fog | `fog.intensity`, `fog.reach` | Depth cue and de-emphasis of far context |
|
|
15
|
+
| Fog | `fog.intensity`, `fog.reach`, `fog.color` | Depth cue and de-emphasis of far context, in the air of the map's theme or a colour of your own |
|
|
13
16
|
| Edge blur | `edgeBlur.intensity`, `edgeBlur.reach` | Focus on the centre of the view |
|
|
14
17
|
| Depth of field | `depthOfField.intensity`, `depthOfField.focus`, `depthOfField.band`, `depthOfField.bokeh` | Depth on a tilted map. A GPU pass blurs more with distance and returns highlights as discs. A flat map has no depth, so the effect ends there |
|
|
15
18
|
| Vignette | `vignette.intensity`, `vignette.reach` | Focus on the centre of the view |
|
|
16
19
|
|
|
17
|
-
Your own stylisation, such as a watercolour or a posterise, runs inside the same GPU chain with `addPass(pass)`. Every effect above then works on your picture.
|
|
20
|
+
Your own stylisation, such as a watercolour or a posterise, runs inside the same GPU chain with `addPass(pass)`. Every effect above then works on your picture. Your own 2D layer, such as a paper texture or film grain, takes a place between the effects with `addLayer(layer)`, and `capture()` keeps it there.
|
|
18
21
|
|
|
19
22
|
## Docs & examples
|
|
20
23
|
|
|
21
24
|
- Developer guide: https://docs.tomtom.com/maps-sdk-js/guides/plugins/map-effects
|
|
22
25
|
- Example: https://docs.tomtom.com/maps-sdk-js/examples/map-effects-playground
|
|
23
|
-
- Example with a pass of your own: https://docs.tomtom.com/maps-sdk-js/examples/mixed-map-effects
|
|
26
|
+
- Example with a pass and a layer of your own: https://docs.tomtom.com/maps-sdk-js/examples/mixed-map-effects
|
|
24
27
|
|
|
25
28
|
## Quickstart
|
|
26
29
|
|
|
@@ -30,7 +33,7 @@ This plugin declares `@tomtom-org/maps-sdk` and `maplibre-gl` as peer dependenci
|
|
|
30
33
|
npm install @tomtom-org/maps-sdk maplibre-gl @tomtom-org/maps-sdk-plugin-map-effects
|
|
31
34
|
```
|
|
32
35
|
|
|
33
|
-
1. Follow the SDK [Project setup](https://docs.tomtom.com/maps-sdk-js/introduction/project-setup) or the Map [quickstart](https://docs.tomtom.com/maps-sdk-js/guides/map/quickstart) to create and initialize a `TomTomMap`.
|
|
36
|
+
1. Follow the SDK [Project setup](https://docs.tomtom.com/maps-sdk-js/introduction/project-setup) or the Map [quickstart](https://docs.tomtom.com/maps-sdk-js/guides/map/quickstart) to create and initialize a `TomTomMap`. The effects draw in the same frame as the map, so it needs no `preserveDrawingBuffer`.
|
|
34
37
|
|
|
35
38
|
2. Import and use the plugin:
|
|
36
39
|
|
|
@@ -57,3 +60,7 @@ await effects.capture({ pixelRatio: 3 }); // map + effects as a canvas, for prin
|
|
|
57
60
|
## License
|
|
58
61
|
|
|
59
62
|
See `LICENSE.txt`.
|
|
63
|
+
|
|
64
|
+
## Security
|
|
65
|
+
|
|
66
|
+
To report a security vulnerability, follow our [security policy](https://github.com/tomtom-international/maps-sdk-js/security/policy), which keeps it private.
|
package/THIRD_PARTY.txt
CHANGED
|
@@ -14,7 +14,7 @@ by the licenses reproduced here.
|
|
|
14
14
|
--------------------------------------------------------------------------------
|
|
15
15
|
|
|
16
16
|
Name: maplibre-gl
|
|
17
|
-
Version: 6.
|
|
17
|
+
Version: 6.15.0
|
|
18
18
|
License: BSD-3-Clause
|
|
19
19
|
Bundled: false (peer dependency)
|
|
20
20
|
Description: BSD licensed community fork of mapbox-gl, a WebGL interactive maps library
|
package/dist/THIRD_PARTY.txt
CHANGED
|
@@ -4,7 +4,7 @@ your application bundles them. The licenses below apply to those copies, and eac
|
|
|
4
4
|
these components may in turn install its own dependencies under their own licenses.
|
|
5
5
|
|
|
6
6
|
Name: maplibre-gl
|
|
7
|
-
Version: 6.
|
|
7
|
+
Version: 6.15.0
|
|
8
8
|
License: BSD-3-Clause
|
|
9
9
|
Bundled: false (peer dependency)
|
|
10
10
|
Description: BSD licensed community fork of mapbox-gl, a WebGL interactive maps library
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
import { KnobConfigOf, KnobKind, KnobRange, StyleColorKnobId, TomTomMap } from "@tomtom-org/maps-sdk/map";
|
|
2
|
+
//#region src/gpu/screenPass.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The WebGL2 substrate the pixel-derived effects run on: one context, one full-screen triangle, and
|
|
5
|
+
* the render targets a chain of passes ping-pongs between, two or a third while a stage keeps its
|
|
6
|
+
* input. No effect lives here — a pass brings its own fragment shader and its own uniforms, and this
|
|
7
|
+
* module is what stops two of them being two implementations of context creation, texture upload,
|
|
8
|
+
* resizing and context loss.
|
|
9
|
+
*
|
|
10
|
+
* **One context for the plugin, and that is a requirement rather than a saving.** A browser caps
|
|
11
|
+
* how many live WebGL contexts a page may hold, and the map itself already holds one.
|
|
12
|
+
*
|
|
13
|
+
* The two shaders it owns — the triangle's vertex half and the preamble every fragment stage is
|
|
14
|
+
* composed onto — are real GLSL files under `glsl/`, loaded with Vite's `?raw`, as every shader in
|
|
15
|
+
* this plugin is: a backtick inside a template literal's comment closes the literal, and the
|
|
16
|
+
* program it silently truncates still compiles to a blank picture.
|
|
17
|
+
*
|
|
18
|
+
* @module
|
|
19
|
+
* @ignore
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* The uniforms a {@link MapEffectsPass} reads, by the name its shader declares. A number sets a
|
|
23
|
+
* `float`, and an array of two to four numbers a `vec2` to `vec4`. Only floats can be set, because
|
|
24
|
+
* WebGL silently refuses a float for an `int` or `bool` uniform, which then stays 0.
|
|
25
|
+
*
|
|
26
|
+
* @group Map Effects
|
|
27
|
+
*/
|
|
28
|
+
type MapEffectsPassUniforms = Record<string, number | readonly [number, number] | readonly [number, number, number] | readonly [number, number, number, number]>;
|
|
29
|
+
//#endregion
|
|
2
30
|
//#region src/bloomScope.d.ts
|
|
3
31
|
type GroupsOf<ID extends string> = ID extends `${infer Head}.${infer Rest}` ? Head | `${Head}.${GroupsOf<Rest>}` : never;
|
|
4
32
|
/**
|
|
@@ -9,14 +37,16 @@ type GroupsOf<ID extends string> = ID extends `${infer Head}.${infer Rest}` ? He
|
|
|
9
37
|
*/
|
|
10
38
|
type StyleColorKnobGroup = GroupsOf<StyleColorKnobId>;
|
|
11
39
|
/**
|
|
12
|
-
* One entry of a bloom scope. It's a style colour knob, a group of them, or a CSS colour for what no
|
|
40
|
+
* One entry of a bloom or glare scope. It's a style colour knob, a group of them, or a CSS colour for what no
|
|
13
41
|
* style knob paints, such as a route line in the `color` its `RoutingModule` was given.
|
|
14
42
|
*
|
|
15
43
|
* @group Map Effects
|
|
16
44
|
*/
|
|
17
45
|
type BloomScopeEntry = StyleColorKnobId | StyleColorKnobGroup | (string & {});
|
|
18
46
|
/**
|
|
19
|
-
* The colours bloom may light
|
|
47
|
+
* The colours an effect keys on: the ones bloom may light in `bloom.only`, and the lights that cast
|
|
48
|
+
* streaks in `glare.only`. An empty scope keys the whole frame. By default, `bloom.only` is empty and
|
|
49
|
+
* `glare.only` is `['colors.roadMajor']`.
|
|
20
50
|
*
|
|
21
51
|
* @group Map Effects
|
|
22
52
|
*/
|
|
@@ -27,6 +57,10 @@ type BloomScope = readonly BloomScopeEntry[];
|
|
|
27
57
|
* The range of a numeric effect knob. Every one ships a slider `step`, which the shared
|
|
28
58
|
* {@link KnobRange} leaves optional.
|
|
29
59
|
*
|
|
60
|
+
* @remarks
|
|
61
|
+
* A knob's bound is `hard` only where the maths breaks past it. Elsewhere it is `hard-min`: the
|
|
62
|
+
* `min` holds, and the `max` is where a slider ends, so a larger value still applies.
|
|
63
|
+
*
|
|
30
64
|
* @group Map Effects
|
|
31
65
|
*/
|
|
32
66
|
type EffectKnobRange = KnobRange & {
|
|
@@ -34,7 +68,8 @@ type EffectKnobRange = KnobRange & {
|
|
|
34
68
|
};
|
|
35
69
|
/**
|
|
36
70
|
* One effect knob, drawn from the SDK's shared knob vocabulary. Discriminated on `kind`: a range
|
|
37
|
-
* belongs to a numeric knob and the accepted names to a `colors` one.
|
|
71
|
+
* belongs to a numeric knob and the accepted names to a `colors` one. A colour knob has no
|
|
72
|
+
* `default` where the colour in force follows the map, as `fog.color` follows its light/dark theme.
|
|
38
73
|
* @ignore
|
|
39
74
|
*/
|
|
40
75
|
type EffectKnobDefinition = {
|
|
@@ -47,7 +82,7 @@ type EffectKnobDefinition = {
|
|
|
47
82
|
range: EffectKnobRange;
|
|
48
83
|
} | {
|
|
49
84
|
kind: Extract<KnobKind, 'color'>;
|
|
50
|
-
default
|
|
85
|
+
default?: string;
|
|
51
86
|
} | {
|
|
52
87
|
kind: Extract<KnobKind, 'colors'>;
|
|
53
88
|
default: BloomScope;
|
|
@@ -56,8 +91,8 @@ type EffectKnobDefinition = {
|
|
|
56
91
|
/**
|
|
57
92
|
* The effects catalogue. Every effect is off at its default; each is named by the product problem it
|
|
58
93
|
* solves. Ordering is the compositing order the overlays stack in — bloom → grade → fog → edge blur
|
|
59
|
-
* → tint → vignette — with the
|
|
60
|
-
*
|
|
94
|
+
* → tint → vignette — with the glare listed beside the bloom, the other light, and the depth of field
|
|
95
|
+
* beside the other defocus, though both are GPU passes under all of them rather than among them.
|
|
61
96
|
* @ignore
|
|
62
97
|
*/
|
|
63
98
|
declare const effectKnobDefinitions: {
|
|
@@ -76,7 +111,7 @@ declare const effectKnobDefinitions: {
|
|
|
76
111
|
readonly min: 2;
|
|
77
112
|
readonly max: 40;
|
|
78
113
|
readonly step: 1;
|
|
79
|
-
readonly bounds: "hard";
|
|
114
|
+
readonly bounds: "hard-min";
|
|
80
115
|
};
|
|
81
116
|
readonly useCase: "Emphasis on dark and driving styles.";
|
|
82
117
|
};
|
|
@@ -101,28 +136,82 @@ declare const effectKnobDefinitions: {
|
|
|
101
136
|
readonly range: EffectKnobRange;
|
|
102
137
|
readonly useCase: "Emphasis on dark and driving styles.";
|
|
103
138
|
};
|
|
104
|
-
readonly '
|
|
139
|
+
readonly 'glare.intensity': {
|
|
105
140
|
readonly kind: "number";
|
|
106
|
-
readonly description: "
|
|
107
|
-
readonly default:
|
|
141
|
+
readonly description: "How strongly the lights cast streaks, as a star filter or an anamorphic lens draws them from a lamp. 0 = off.";
|
|
142
|
+
readonly default: 0;
|
|
143
|
+
readonly range: EffectKnobRange;
|
|
144
|
+
readonly useCase: "Lights that read as lights on dark styles: lit roads, icons and routes cast streaks, as lamps do through a lens.";
|
|
145
|
+
};
|
|
146
|
+
readonly 'glare.length': {
|
|
147
|
+
readonly kind: "number";
|
|
148
|
+
readonly description: "How far each streak reaches from its light, in CSS pixels.";
|
|
149
|
+
readonly default: 32;
|
|
108
150
|
readonly range: {
|
|
109
|
-
readonly min:
|
|
110
|
-
readonly max:
|
|
111
|
-
readonly step:
|
|
151
|
+
readonly min: 4;
|
|
152
|
+
readonly max: 120;
|
|
153
|
+
readonly step: 1;
|
|
154
|
+
readonly bounds: "hard-min";
|
|
155
|
+
};
|
|
156
|
+
readonly useCase: "Lights that read as lights on dark styles: lit roads, icons and routes cast streaks, as lamps do through a lens.";
|
|
157
|
+
};
|
|
158
|
+
readonly 'glare.rays': {
|
|
159
|
+
readonly kind: "number";
|
|
160
|
+
readonly description: "How many streaks each light casts: 2 is an anamorphic lens’s line, 4 a cross, 6 and 8 a star.";
|
|
161
|
+
readonly default: 4;
|
|
162
|
+
readonly range: {
|
|
163
|
+
readonly min: 2;
|
|
164
|
+
readonly max: 8;
|
|
165
|
+
readonly step: 2;
|
|
112
166
|
readonly bounds: "hard";
|
|
113
167
|
};
|
|
168
|
+
readonly useCase: "Lights that read as lights on dark styles: lit roads, icons and routes cast streaks, as lamps do through a lens.";
|
|
169
|
+
};
|
|
170
|
+
readonly 'glare.angle': {
|
|
171
|
+
readonly kind: "number";
|
|
172
|
+
readonly description: "The first streak’s direction, in degrees anticlockwise from the horizontal; the others share the turn evenly.";
|
|
173
|
+
readonly default: 45;
|
|
174
|
+
readonly range: {
|
|
175
|
+
readonly min: 0;
|
|
176
|
+
readonly max: 180;
|
|
177
|
+
readonly step: 5;
|
|
178
|
+
readonly bounds: "hard";
|
|
179
|
+
};
|
|
180
|
+
readonly useCase: "Lights that read as lights on dark styles: lit roads, icons and routes cast streaks, as lamps do through a lens.";
|
|
181
|
+
};
|
|
182
|
+
readonly 'glare.threshold': {
|
|
183
|
+
readonly kind: "number";
|
|
184
|
+
readonly description: "Brightness below which a light casts nothing.";
|
|
185
|
+
readonly default: 0.3;
|
|
186
|
+
readonly range: EffectKnobRange;
|
|
187
|
+
readonly useCase: "Lights that read as lights on dark styles: lit roads, icons and routes cast streaks, as lamps do through a lens.";
|
|
188
|
+
};
|
|
189
|
+
readonly 'glare.only': {
|
|
190
|
+
readonly kind: "colors";
|
|
191
|
+
readonly description: "The colours that count as lights, as `bloom.only` names them. By default the major roads, read from the loaded style; empty lets every bright pixel streak, labels included.";
|
|
192
|
+
readonly default: readonly ["colors.roadMajor"];
|
|
193
|
+
readonly options: readonly string[];
|
|
194
|
+
readonly useCase: "Lights that read as lights on dark styles: lit roads, icons and routes cast streaks, as lamps do through a lens.";
|
|
195
|
+
};
|
|
196
|
+
readonly 'glare.onlyTolerance': {
|
|
197
|
+
readonly kind: "number";
|
|
198
|
+
readonly description: "How far a pixel may sit from a light's colour and still streak.";
|
|
199
|
+
readonly default: 0.15;
|
|
200
|
+
readonly range: EffectKnobRange;
|
|
201
|
+
readonly useCase: "Lights that read as lights on dark styles: lit roads, icons and routes cast streaks, as lamps do through a lens.";
|
|
202
|
+
};
|
|
203
|
+
readonly 'grade.brightness': {
|
|
204
|
+
readonly kind: "number";
|
|
205
|
+
readonly description: "Brightness of the whole base map (1 = unchanged).";
|
|
206
|
+
readonly default: 1;
|
|
207
|
+
readonly range: EffectKnobRange;
|
|
114
208
|
readonly useCase: "Muting the base map under a data overlay.";
|
|
115
209
|
};
|
|
116
210
|
readonly 'grade.contrast': {
|
|
117
211
|
readonly kind: "number";
|
|
118
212
|
readonly description: "Contrast of the whole base map (1 = unchanged).";
|
|
119
213
|
readonly default: 1;
|
|
120
|
-
readonly range:
|
|
121
|
-
readonly min: 0.5;
|
|
122
|
-
readonly max: 1.5;
|
|
123
|
-
readonly step: 0.05;
|
|
124
|
-
readonly bounds: "hard";
|
|
125
|
-
};
|
|
214
|
+
readonly range: EffectKnobRange;
|
|
126
215
|
readonly useCase: "Muting the base map under a data overlay.";
|
|
127
216
|
};
|
|
128
217
|
readonly 'grade.saturation': {
|
|
@@ -133,13 +222,13 @@ declare const effectKnobDefinitions: {
|
|
|
133
222
|
readonly min: 0;
|
|
134
223
|
readonly max: 1.5;
|
|
135
224
|
readonly step: 0.05;
|
|
136
|
-
readonly bounds: "hard";
|
|
225
|
+
readonly bounds: "hard-min";
|
|
137
226
|
};
|
|
138
227
|
readonly useCase: "Muting the base map under a data overlay.";
|
|
139
228
|
};
|
|
140
229
|
readonly 'fog.intensity': {
|
|
141
230
|
readonly kind: "number";
|
|
142
|
-
readonly description: "Haze over the edges of the view: blur, plus a wash towards the colour of the air
|
|
231
|
+
readonly description: "Haze over the edges of the view: blur, plus a wash towards the colour of the air (`fog.color`). Reads as distance. 0 = off.";
|
|
143
232
|
readonly default: 0;
|
|
144
233
|
readonly range: EffectKnobRange;
|
|
145
234
|
readonly useCase: "Depth cue: the far context recedes as if seen through air.";
|
|
@@ -151,16 +240,16 @@ declare const effectKnobDefinitions: {
|
|
|
151
240
|
readonly range: EffectKnobRange;
|
|
152
241
|
readonly useCase: "Depth cue: the far context recedes as if seen through air.";
|
|
153
242
|
};
|
|
243
|
+
readonly 'fog.color': {
|
|
244
|
+
readonly kind: "color";
|
|
245
|
+
readonly description: "The colour of the air the fog washes towards; its lightness brightens or darkens the rim with it, and a translucent colour does both less. Unset, it follows the map's light/dark theme: pale on a light map, dark on a dark one.";
|
|
246
|
+
readonly useCase: "Depth cue: the far context recedes as if seen through air.";
|
|
247
|
+
};
|
|
154
248
|
readonly 'edgeBlur.intensity': {
|
|
155
249
|
readonly kind: "number";
|
|
156
250
|
readonly description: "Blur over the edges of the view, in CSS pixels, leaving their colour untouched. Reads as a lens, not as distance — `fog` is the effect that also washes the rim out. 0 = off.";
|
|
157
251
|
readonly default: 0;
|
|
158
|
-
readonly range:
|
|
159
|
-
readonly min: 0;
|
|
160
|
-
readonly max: 30;
|
|
161
|
-
readonly step: 1;
|
|
162
|
-
readonly bounds: "hard";
|
|
163
|
-
};
|
|
252
|
+
readonly range: EffectKnobRange;
|
|
164
253
|
readonly useCase: "Focus: the centre stays sharp and the edges lose their detail.";
|
|
165
254
|
};
|
|
166
255
|
readonly 'edgeBlur.reach': {
|
|
@@ -174,12 +263,7 @@ declare const effectKnobDefinitions: {
|
|
|
174
263
|
readonly kind: "number";
|
|
175
264
|
readonly description: "The widest circle of confusion, in CSS pixels — the blur the most out-of-focus distance gets. Needs a tilted camera: a map seen from straight above has no depth, and the effect ends there on its own. 0 = off.";
|
|
176
265
|
readonly default: 0;
|
|
177
|
-
readonly range:
|
|
178
|
-
readonly min: 0;
|
|
179
|
-
readonly max: 30;
|
|
180
|
-
readonly step: 1;
|
|
181
|
-
readonly bounds: "hard";
|
|
182
|
-
};
|
|
266
|
+
readonly range: EffectKnobRange;
|
|
183
267
|
readonly useCase: "Depth on a tilted map: one band of distance stays sharp, the rest defocuses with distance.";
|
|
184
268
|
};
|
|
185
269
|
readonly 'depthOfField.focus': {
|
|
@@ -253,9 +337,9 @@ export declare const effectKnobIds: EffectKnobId[];
|
|
|
253
337
|
*
|
|
254
338
|
* @group Map Effects
|
|
255
339
|
*/
|
|
256
|
-
type EffectKnobValueOf<ID extends EffectKnobId> = Extract<EffectKnobDefinition, {
|
|
340
|
+
type EffectKnobValueOf<ID extends EffectKnobId> = NonNullable<Extract<EffectKnobDefinition, {
|
|
257
341
|
kind: (typeof effectKnobDefinitions)[ID]['kind'];
|
|
258
|
-
}>['default']
|
|
342
|
+
}>['default']>;
|
|
259
343
|
/**
|
|
260
344
|
* One entry of {@link effectKnobCatalogue}, with what a UI or a coding agent needs to offer the knob
|
|
261
345
|
* and validate a value for it. The SDK's `getKnob` reads the value in force.
|
|
@@ -274,32 +358,16 @@ type EffectKnob = EffectKnobDefinition & {
|
|
|
274
358
|
*/
|
|
275
359
|
export declare const effectKnobCatalogue: readonly EffectKnob[];
|
|
276
360
|
//#endregion
|
|
277
|
-
//#region src/
|
|
361
|
+
//#region src/composite.d.ts
|
|
278
362
|
/**
|
|
279
|
-
* The
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
* being two implementations of context creation, texture upload, resizing and context loss.
|
|
283
|
-
*
|
|
284
|
-
* **One context for the plugin, and that is a requirement rather than a saving.** A browser caps
|
|
285
|
-
* how many live WebGL contexts a page may hold, and the map itself already holds one.
|
|
286
|
-
*
|
|
287
|
-
* The two shaders it owns — the triangle's vertex half and the preamble every fragment stage is
|
|
288
|
-
* composed onto — are real GLSL files under `glsl/`, loaded with Vite's `?raw`, as every shader in
|
|
289
|
-
* this plugin is: a backtick inside a template literal's comment closes the literal, and the
|
|
290
|
-
* program it silently truncates still compiles to a blank picture.
|
|
291
|
-
*
|
|
292
|
-
* @module
|
|
363
|
+
* The overlay effects in compositing order, from the bottom. Live, each is one absolutely positioned
|
|
364
|
+
* element over the map, and the capture applies them in the same order. The GPU chain is not among
|
|
365
|
+
* them: it renders the picture they all read, so it sits under them.
|
|
293
366
|
* @ignore
|
|
294
367
|
*/
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
* for an `int` or `bool` uniform, which then stays 0.
|
|
299
|
-
*
|
|
300
|
-
* @group Map Effects
|
|
301
|
-
*/
|
|
302
|
-
type MapEffectsPassUniforms = Record<string, number | readonly [number, number]>;
|
|
368
|
+
declare const OVERLAYS: readonly ["bloom", "grade", "fog", "edgeBlur", "tint", "vignette"];
|
|
369
|
+
/** @ignore */
|
|
370
|
+
type OverlayName = (typeof OVERLAYS)[number];
|
|
303
371
|
//#endregion
|
|
304
372
|
//#region src/MapEffects.d.ts
|
|
305
373
|
/**
|
|
@@ -342,9 +410,65 @@ type MapEffectsPass = {
|
|
|
342
410
|
* skips the pass for that draw, with one warning.
|
|
343
411
|
*/
|
|
344
412
|
uniforms?: MapEffectsPassUniforms | (() => MapEffectsPassUniforms);
|
|
413
|
+
/**
|
|
414
|
+
* The textures the shader samples besides `uSource`, each under the name of the `uniform sampler2D`
|
|
415
|
+
* it declares, or a function returning one, called on every draw. They bind to texture units 1 to
|
|
416
|
+
* 15, in the order given.
|
|
417
|
+
*
|
|
418
|
+
* @remarks
|
|
419
|
+
* - A video or a canvas is uploaded on every draw, since its pixels change under the same object. An
|
|
420
|
+
* image, an `ImageBitmap`, a `VideoFrame` or `ImageData` is uploaded once: to change its picture,
|
|
421
|
+
* hand a new object.
|
|
422
|
+
* - A texture is uploaded the same way up as `uSource`, so `texture(uPaper, vUv)` lays it over the
|
|
423
|
+
* frame upright. It is sampled linearly, repeats past its edges and has no mip levels.
|
|
424
|
+
* - An image that has not loaded, a video without a frame, or a function returning `undefined`, skips
|
|
425
|
+
* the pass for that draw. Its textures are freed when the pass is removed.
|
|
426
|
+
*/
|
|
427
|
+
textures?: Record<string, TexImageSource | (() => TexImageSource | undefined)>;
|
|
428
|
+
/**
|
|
429
|
+
* Whether the shader reads `uSource` at mip levels other than 0, with `textureLod`. The plugin then
|
|
430
|
+
* builds the picture's mip levels before the pass. Without it, every level reads as level 0.
|
|
431
|
+
*/
|
|
432
|
+
mipmapped?: boolean;
|
|
345
433
|
/** Defaults to `before-depth-of-field`. */
|
|
346
434
|
placement?: MapEffectsPassPlacement;
|
|
347
435
|
};
|
|
436
|
+
/**
|
|
437
|
+
* Where a {@link MapEffectsLayer} sits in the overlay stack: right above the effect it names. Every
|
|
438
|
+
* overlay effect above it then works on it, and the ones at or below it don't.
|
|
439
|
+
*
|
|
440
|
+
* @remarks
|
|
441
|
+
* From the bottom, the overlay stack is `bloom`, `grade`, `fog`, `edgeBlur`, `tint` and `vignette`,
|
|
442
|
+
* over the GPU chain. So `after-edgeBlur` puts a layer under the tint and the vignette, and
|
|
443
|
+
* `after-vignette` over every effect.
|
|
444
|
+
*
|
|
445
|
+
* @group Map Effects
|
|
446
|
+
*/
|
|
447
|
+
type MapEffectsLayerPlacement = `after-${OverlayName}`;
|
|
448
|
+
/**
|
|
449
|
+
* A 2D canvas layer of your own, such as a paper texture or film grain, placed between the plugin's
|
|
450
|
+
* overlay effects and drawn by {@link MapEffects.capture} in the same place.
|
|
451
|
+
*
|
|
452
|
+
* @group Map Effects
|
|
453
|
+
*/
|
|
454
|
+
type MapEffectsLayer = {
|
|
455
|
+
/** Names the layer for {@link MapEffects.removeLayer}. Adding a layer under an id in use replaces it in place. */
|
|
456
|
+
id: string;
|
|
457
|
+
placement: MapEffectsLayerPlacement;
|
|
458
|
+
/**
|
|
459
|
+
* Paints the layer onto a cleared context the size of the map's canvas, in device pixels.
|
|
460
|
+
*
|
|
461
|
+
* @remarks
|
|
462
|
+
* - `pixelRatio` is the device pixels per CSS pixel, to scale CSS lengths by.
|
|
463
|
+
* - It is called whenever the map renders, and by {@link MapEffects.capture} at the capture's own size.
|
|
464
|
+
* - A draw that throws leaves the layer empty for that draw, with one warning.
|
|
465
|
+
*/
|
|
466
|
+
draw: (context: CanvasRenderingContext2D, raster: {
|
|
467
|
+
width: number;
|
|
468
|
+
height: number;
|
|
469
|
+
pixelRatio: number;
|
|
470
|
+
}) => void;
|
|
471
|
+
};
|
|
348
472
|
/**
|
|
349
473
|
* Options for {@link MapEffects.capture}.
|
|
350
474
|
*
|
|
@@ -364,12 +488,16 @@ type CaptureOptions = {
|
|
|
364
488
|
* and cannot be reproduced by editing the style at all.
|
|
365
489
|
*
|
|
366
490
|
* @remarks
|
|
491
|
+
* **[Public Preview](https://docs.tomtom.com/legal/public-preview)** until the plugin's 1.0.0 release.
|
|
492
|
+
*
|
|
367
493
|
* Every effect is named by the problem it solves, and off by default:
|
|
368
494
|
* - **bloom** - Emphasis on dark and driving styles. Lit roads, traffic tubes and route lines glow.
|
|
495
|
+
* - **glare** - Lights that read as lights. The colours you name, by default the major roads, cast
|
|
496
|
+
* streaks as lamps do through a star filter or an anamorphic lens.
|
|
369
497
|
* - **grade** (brightness, contrast, saturation) and **tint** - Muting the base map so a data overlay
|
|
370
498
|
* reads first.
|
|
371
|
-
* - **fog** - Depth. The rim is blurred *and* washed towards the colour of the air, which
|
|
372
|
-
* map's light/dark theme.
|
|
499
|
+
* - **fog** - Depth. The rim is blurred *and* washed towards the colour of the air, `fog.color`, which
|
|
500
|
+
* follows the map's light/dark theme while unset.
|
|
373
501
|
* - **edge blur** - Focus. The rim is blurred and keeps its colour, so it reads as a lens rather than
|
|
374
502
|
* as distance.
|
|
375
503
|
* - **depth of field** - Depth on a tilted map. One band of *distance* stays sharp and the rest
|
|
@@ -378,12 +506,14 @@ type CaptureOptions = {
|
|
|
378
506
|
*
|
|
379
507
|
* Effects render live as layers over the map canvas, and are composited the same way into
|
|
380
508
|
* {@link capture}, so what is captured is what was on screen. From the bottom, the layers are a
|
|
381
|
-
* WebGL2 chain that also runs your own passes from {@link addPass},
|
|
382
|
-
* in `screen` mode, and the CSS
|
|
509
|
+
* WebGL2 chain that also runs your own passes from {@link addPass}, the glare and a bloom scoped
|
|
510
|
+
* with `bloom.only`, an unscoped bloom as a small canvas blended in `screen` mode, and the CSS
|
|
511
|
+
* overlays. Your own 2D layers from {@link addLayer} take a place among the overlays. HTML
|
|
512
|
+
* markers, popups and the map's controls sit above every effect.
|
|
383
513
|
*
|
|
384
|
-
*
|
|
385
|
-
* `
|
|
386
|
-
*
|
|
514
|
+
* The effects draw on the map's `render` event, in the same frame as the map, so the map needs no
|
|
515
|
+
* `preserveDrawingBuffer`. Adding a pass, or setting a knob of bloom, the glare or the depth of field,
|
|
516
|
+
* asks the map for one frame to draw in. The CSS overlays need none.
|
|
387
517
|
*
|
|
388
518
|
* {@link effectKnobCatalogue} lists every knob as static data, the way `stylingFoundationsKnobCatalogue` lists
|
|
389
519
|
* the SDK's styling knobs. Each id is its path into {@link MapEffectsConfig}. The SDK's `setKnob`,
|
|
@@ -410,19 +540,19 @@ export declare class MapEffects {
|
|
|
410
540
|
private readonly map;
|
|
411
541
|
private config;
|
|
412
542
|
private passes;
|
|
543
|
+
private layers;
|
|
413
544
|
private readonly container;
|
|
414
545
|
private readonly overlays;
|
|
415
546
|
private readonly bloomCanvas;
|
|
416
547
|
private readonly artworkCanvas;
|
|
417
548
|
private readonly chain;
|
|
418
|
-
private
|
|
419
|
-
private
|
|
420
|
-
private
|
|
421
|
-
private scopeCanvas;
|
|
422
|
-
private warnedBuffer;
|
|
423
|
-
private warnedScope;
|
|
549
|
+
private scopeMasks;
|
|
550
|
+
private styleGeneration;
|
|
551
|
+
private readonly warnedScopes;
|
|
424
552
|
private warnedNoWebGL2;
|
|
425
|
-
private
|
|
553
|
+
private warnedFrame;
|
|
554
|
+
private readonly warnedThrows;
|
|
555
|
+
private readonly passSources;
|
|
426
556
|
private readonly onStyleData;
|
|
427
557
|
private readonly onRender;
|
|
428
558
|
/**
|
|
@@ -488,11 +618,13 @@ export declare class MapEffects {
|
|
|
488
618
|
* @remarks
|
|
489
619
|
* A pass redraws with the map. If its uniforms change on their own, as in a time-driven animation,
|
|
490
620
|
* ask for frames with `map.mapLibreMap.triggerRepaint()`. A pass that shifts hues runs before
|
|
491
|
-
* bloom's colour
|
|
621
|
+
* the bloom's and the glare's colour keys too, so a `bloom.only` or `glare.only` scope may need a
|
|
622
|
+
* wider `bloom.onlyTolerance` or `glare.onlyTolerance`.
|
|
492
623
|
*
|
|
493
624
|
* @throws `Error` when the pass has no id or its shader does not compile, with the compiler's log;
|
|
494
|
-
* `RangeError` for an unknown placement
|
|
495
|
-
*
|
|
625
|
+
* `RangeError` for an unknown placement, or for more than 15 textures or one named `uSource`. A
|
|
626
|
+
* shader is checked only while there is a WebGL2 context: without one no pass runs and the plugin
|
|
627
|
+
* warns once, and after a lost one it is checked on restore.
|
|
496
628
|
*
|
|
497
629
|
* @example
|
|
498
630
|
* ```typescript
|
|
@@ -510,15 +642,42 @@ export declare class MapEffects {
|
|
|
510
642
|
addPass(pass: MapEffectsPass): void;
|
|
511
643
|
/** Takes a pass added with {@link addPass} out of the chain. An id with no pass is ignored. */
|
|
512
644
|
removePass(id: string): void;
|
|
645
|
+
/**
|
|
646
|
+
* Draws a 2D canvas layer of your own between the overlay effects, and in {@link capture} in the
|
|
647
|
+
* same place. Layers in one placement stack in the order they were added.
|
|
648
|
+
*
|
|
649
|
+
* @remarks
|
|
650
|
+
* A layer redraws with the map. If it changes on its own, as film grain does, ask for frames with
|
|
651
|
+
* `map.mapLibreMap.triggerRepaint()`. A layer needs no WebGL2 context and reads no canvas.
|
|
652
|
+
*
|
|
653
|
+
* @throws `Error` when the layer has no id; `RangeError` for an unknown placement.
|
|
654
|
+
*
|
|
655
|
+
* @example
|
|
656
|
+
* ```typescript
|
|
657
|
+
* // A paper texture under the tint and the vignette, which then work on the paper too.
|
|
658
|
+
* effects.addLayer({
|
|
659
|
+
* id: 'paper',
|
|
660
|
+
* placement: 'after-edgeBlur',
|
|
661
|
+
* draw: (context, { width, height }) => {
|
|
662
|
+
* context.globalAlpha = 0.25;
|
|
663
|
+
* context.fillStyle = context.createPattern(paper, 'repeat') ?? 'transparent';
|
|
664
|
+
* context.fillRect(0, 0, width, height);
|
|
665
|
+
* },
|
|
666
|
+
* });
|
|
667
|
+
* ```
|
|
668
|
+
*/
|
|
669
|
+
addLayer(layer: MapEffectsLayer): void;
|
|
670
|
+
/** Takes a layer added with {@link addLayer} off the map. An id with no layer is ignored. */
|
|
671
|
+
removeLayer(id: string): void;
|
|
513
672
|
/**
|
|
514
673
|
* Renders the current view, with its effects, into a canvas, optionally at a higher pixel ratio
|
|
515
674
|
* for print. The map is re-rendered at that ratio (never resampled), read back once every tile is
|
|
516
675
|
* drawn, and then set back to the ratio it had.
|
|
517
676
|
*
|
|
518
677
|
* @remarks
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
*
|
|
678
|
+
* - Viewers cannot download from a sandboxed page, so hand the canvas to your own upload or `toBlob` flow.
|
|
679
|
+
* - It rejects when the frame fails to draw, such as with a pass texture from another origin that
|
|
680
|
+
* was loaded without CORS.
|
|
522
681
|
*/
|
|
523
682
|
capture(options?: CaptureOptions): Promise<HTMLCanvasElement>;
|
|
524
683
|
/**
|
|
@@ -529,29 +688,41 @@ export declare class MapEffects {
|
|
|
529
688
|
remove(): void;
|
|
530
689
|
private show;
|
|
531
690
|
private render;
|
|
691
|
+
private readsFrame;
|
|
692
|
+
private bloomInChain;
|
|
693
|
+
private routeBloom;
|
|
532
694
|
private showFog;
|
|
533
695
|
/**
|
|
534
696
|
* The passes the chain runs over the map's pixels, in order: the app's placed before the depth of
|
|
535
|
-
* field, the depth of field,
|
|
536
|
-
*
|
|
697
|
+
* field, the depth of field, the app's placed after it, the glare, and a scoped bloom over all of
|
|
698
|
+
* it, where the overlay canvas of an unscoped one would read. Each reads what the one before left,
|
|
699
|
+
* without the picture leaving the GPU, so either bloom glows along the streaks too.
|
|
537
700
|
*/
|
|
538
701
|
private chainStages;
|
|
702
|
+
private glareStages;
|
|
703
|
+
private bloomStages;
|
|
704
|
+
private rasterOf;
|
|
539
705
|
private passStages;
|
|
540
|
-
private
|
|
706
|
+
private inputsOf;
|
|
707
|
+
private guarded;
|
|
708
|
+
private layerCanvas;
|
|
709
|
+
private mountLayers;
|
|
710
|
+
private drawLayers;
|
|
711
|
+
private paintLayer;
|
|
712
|
+
private captureLayersAfter;
|
|
541
713
|
private releaseUnused;
|
|
714
|
+
private releaseUnsampled;
|
|
715
|
+
private holdSource;
|
|
542
716
|
private depthOfFieldStages;
|
|
543
717
|
private drawChain;
|
|
544
|
-
private scheduleBloom;
|
|
545
718
|
private drawBloom;
|
|
546
|
-
private scopedSource;
|
|
547
719
|
private currentScopeMask;
|
|
548
720
|
private buildScopeMaskFor;
|
|
549
721
|
private warnScopeEmpty;
|
|
550
|
-
private dropScopeMask;
|
|
551
722
|
private readBack;
|
|
723
|
+
private compositeFrame;
|
|
552
724
|
private settled;
|
|
553
|
-
private warnIfBufferNotPreserved;
|
|
554
725
|
private warnNoWebGL2;
|
|
555
726
|
}
|
|
556
727
|
//#endregion
|
|
557
|
-
export type { BloomScope, BloomScopeEntry, CaptureOptions, EffectKnob, EffectKnobId, EffectKnobRange, EffectKnobValueOf, MapEffectsConfig, MapEffectsPass, MapEffectsPassPlacement, MapEffectsPassUniforms, StyleColorKnobGroup };
|
|
728
|
+
export type { BloomScope, BloomScopeEntry, CaptureOptions, EffectKnob, EffectKnobId, EffectKnobRange, EffectKnobValueOf, MapEffectsConfig, MapEffectsLayer, MapEffectsLayerPlacement, MapEffectsPass, MapEffectsPassPlacement, MapEffectsPassUniforms, StyleColorKnobGroup };
|
package/dist/index.es.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{configWithKnobs,configWithoutKnob,getStyleColor,knobValueIn,parseCssColor,styleColorKnobIds}from"@tomtom-org/maps-sdk/map";var groupsOf=e=>e.split(".").slice(0,-1).map((e,t,a)=>a.slice(0,t+1).join(".")),NAMED_KNOBS=/* @__PURE__ */(()=>{const e=/* @__PURE__ */new Map;for(const t of styleColorKnobIds)for(const a of[...groupsOf(t),t])e.set(a,[...e.get(a)??[],t]);return e})(),cubeIndex=(e,t,a)=>32*(32*(e>>3)+(t>>3))+(a>>3),chromaticity=([e,t,a])=>{const s=Math.max(e,t,a);if(0!==s)return[e/s,t/s,a/s]},applyScopeMask=(e,t)=>{const a=e.getImageData(0,0,e.canvas.width,e.canvas.height);((e,t)=>{for(let a=0;a<e.length;a+=4)t[cubeIndex(e[a],e[a+1],e[a+2])]||(e[a]=0,e[a+1]=0,e[a+2]=0)})(a.data,t),e.putImageData(a,0,0)},FRACTION={min:0,max:1,step:.05,bounds:"hard"},USE_CASE_bloom="Emphasis on dark and driving styles.",USE_CASE_grade="Muting the base map under a data overlay.",USE_CASE_fog="Depth cue: the far context recedes as if seen through air.",USE_CASE_edgeBlur="Focus: the centre stays sharp and the edges lose their detail.",USE_CASE_depthOfField="Depth on a tilted map: one band of distance stays sharp, the rest defocuses with distance.",USE_CASE_tint="Muting the base map under a data overlay; matching a brand.",effectKnobDefinitions={"bloom.intensity":{kind:"number",description:"How strongly bright features (lit roads, traffic tubes, route lines) glow. 0 = off.",default:0,range:FRACTION,useCase:USE_CASE_bloom},"bloom.radius":{kind:"number",description:"How far the glow spreads, in CSS pixels.",default:12,range:{min:2,max:40,step:1,bounds:"hard"},useCase:USE_CASE_bloom},"bloom.threshold":{kind:"number",description:"Brightness below which nothing glows. Raise it so only the brightest features light up.",default:.55,range:FRACTION,useCase:USE_CASE_bloom},"bloom.only":{kind:"colors",description:"The colours bloom may light: CSS colours, style colour knobs or groups of them (`traffic`, `traffic.incidents.colors.major`), read from the loaded style. Only the pixels wearing one of them glow; empty lights the whole frame.",default:[],options:/* @__PURE__ */(()=>[...NAMED_KNOBS.keys()])(),useCase:USE_CASE_bloom},"bloom.onlyTolerance":{kind:"number",description:"How far a pixel may sit from one of the scope's colours and still glow. Raise it when antialiasing has mixed the colour with the map underneath; lower it when a neighbouring severity is glowing too.",default:.15,range:FRACTION,useCase:USE_CASE_bloom},"grade.brightness":{kind:"number",description:"Brightness of the whole base map (1 = unchanged).",default:1,range:{min:.5,max:1.5,step:.05,bounds:"hard"},useCase:USE_CASE_grade},"grade.contrast":{kind:"number",description:"Contrast of the whole base map (1 = unchanged).",default:1,range:{min:.5,max:1.5,step:.05,bounds:"hard"},useCase:USE_CASE_grade},"grade.saturation":{kind:"number",description:"Colour saturation of the whole base map (1 = unchanged, 0 = greyscale).",default:1,range:{min:0,max:1.5,step:.05,bounds:"hard"},useCase:USE_CASE_grade},"fog.intensity":{kind:"number",description:"Haze over the edges of the view: blur, plus a wash towards the colour of the air — pale on a light map, dark on a dark one. Reads as distance. 0 = off.",default:0,range:FRACTION,useCase:USE_CASE_fog},"fog.reach":{kind:"number",description:"How far the fog reaches in from the edges (1 = to the centre).",default:.4,range:FRACTION,useCase:USE_CASE_fog},"edgeBlur.intensity":{kind:"number",description:"Blur over the edges of the view, in CSS pixels, leaving their colour untouched. Reads as a lens, not as distance — `fog` is the effect that also washes the rim out. 0 = off.",default:0,range:{min:0,max:30,step:1,bounds:"hard"},useCase:USE_CASE_edgeBlur},"edgeBlur.reach":{kind:"number",description:"How far the edge blur reaches in from the edges (1 = to the centre).",default:.5,range:FRACTION,useCase:USE_CASE_edgeBlur},"depthOfField.intensity":{kind:"number",description:"The widest circle of confusion, in CSS pixels — the blur the most out-of-focus distance gets. Needs a tilted camera: a map seen from straight above has no depth, and the effect ends there on its own. 0 = off.",default:0,range:{min:0,max:30,step:1,bounds:"hard"},useCase:USE_CASE_depthOfField},"depthOfField.focus":{kind:"number",description:"Which distance is sharp, across what the frame shows: 0 the nearest ground at the bottom of the view, 1 the farthest at the top.",default:0,range:FRACTION,useCase:USE_CASE_depthOfField},"depthOfField.band":{kind:"number",description:"How much of that depth stays sharp — the field itself. Thin is the miniature; wide holds a whole run of the map. A gentle tilt has little depth, so the same value covers more of the view.",default:.3,range:FRACTION,useCase:USE_CASE_depthOfField},"depthOfField.bokeh":{kind:"number",description:"Whether an out-of-focus highlight fades or comes back as a disc of light. 0 averages the light away, as a blur does; turned up, a lit road or a traffic tube keeps its brightness across the whole circle it is spread over.",default:.5,range:FRACTION,useCase:USE_CASE_depthOfField},"tint.color":{kind:"color",description:"A flat colour laid over the whole map.",default:"#1f2937",useCase:USE_CASE_tint},"tint.opacity":{kind:"number",description:"Opacity of the tint. 0 = off.",default:0,range:FRACTION,useCase:USE_CASE_tint},"vignette.intensity":{kind:"number",description:"Darkening (negative) or lightening (positive) towards the edges. 0 = off.",default:0,range:{min:-1,max:1,step:.05,bounds:"hard"},useCase:"Focus on the centre of the view."},"vignette.reach":{kind:"number",description:"How far the vignette reaches in from the edges (1 = to the centre).",default:.45,range:FRACTION,useCase:"Focus on the centre of the view."}},effectKnobIds=/* @__PURE__ */Object.keys(effectKnobDefinitions),effectKnobCatalogue=/* @__PURE__ */effectKnobIds.map(e=>({id:e,...effectKnobDefinitions[e]})),clamp=(e,t,a)=>Math.min(Math.max(e,t),a),asFraction=e=>clamp(e,0,1),bloomFilter=(e,t,a=1)=>{const s=.92*asFraction(t),r=(1+s)/(1-s);return`brightness(${(1/(1+s)).toFixed(4)}) contrast(${r.toFixed(3)}) blur(${e*a}px) saturate(1.35)`},gradeFilter=(e,t,a)=>[1===e?"":`brightness(${e})`,1===t?"":`contrast(${t})`,1===a?"":`saturate(${a})`].filter(Boolean).join(" "),rimGradient=(e,t,a)=>`radial-gradient(ellipse farthest-corner at center, ${e} ${Math.round(100*(1-asFraction(a)))}%, ${t} 100%)`,vignetteGradient=(e,t)=>{const{inner:a,outer:s}=vignetteStops(e);return rimGradient(a,s,t)},vignetteStops=e=>{const t=e<0?"0, 0, 0":"255, 255, 255";return{inner:`rgba(${t}, 0)`,outer:`rgba(${t}, ${Math.min(Math.abs(e),1)})`}},rimMask=e=>rimGradient("transparent","black",e),toRadians=e=>e*Math.PI/180,depthRamp=({pitch:e,verticalFieldOfView:t})=>{const a=Math.tan(toRadians(clamp(e,0,89))),s=2*Math.tan(toRadians(clamp(t,1,175))/2)*a;return s/(1+s/2)},FOG_BY_THEME={light:{rgb:"238, 240, 244",grade:"brightness(1.08) contrast(0.82) saturate(0.7)"},dark:{rgb:"16, 22, 33",grade:"brightness(0.92) contrast(0.82) saturate(0.7)"}},fogGradeFilter=e=>FOG_BY_THEME[e].grade,fogBlurPx=e=>2+6*e,fogFilter=(e,t)=>`blur(${fogBlurPx(e)}px) ${fogGradeFilter(t)}`,fogVeilStops=(e,t)=>{const{rgb:a}=FOG_BY_THEME[t];return{inner:`rgba(${a}, 0)`,outer:`rgba(${a}, ${.65*e})`}},fogVeil=(e,t,a)=>{const{inner:s,outer:r}=fogVeilStops(e,a);return rimGradient(s,r,t)},filteredCopy=(e,t,a,s)=>{const r=document.createElement("canvas");r.width=t,r.height=a;const n=r.getContext("2d");if(n)return n.filter=s,n.drawImage(e.canvas,0,0),n.filter="none",n},drawOver=(e,t,a,s=1)=>{e.save(),e.globalCompositeOperation=a,e.globalAlpha=s,e.drawImage(t.canvas,0,0),e.restore()},fillEllipticalRadial=(e,t,a,s,r,n)=>{const i=t/Math.SQRT2,o=a/Math.SQRT2,h=e.createRadialGradient(0,0,1-s,0,0,1);h.addColorStop(0,r),h.addColorStop(1,n),e.save(),e.translate(t/2,a/2),e.scale(i,o),e.fillStyle=h,e.fillRect(-t/2/i,-a/2/o,t/i,a/o),e.restore()},applyMaskedBlur=(e,t,a,s,r,n,i)=>{const o=filteredCopy(e,t,a,`blur(${r*s}px) ${n}`.trim());o&&(o.globalCompositeOperation="destination-in",i(o),e.drawImage(o.canvas,0,0))},compositeEffects=(e,t,a,s,r,n,i)=>{r["bloom.intensity"]>0&&((e,t,a,s,r,n)=>{const i=bloomFilter(r["bloom.radius"],r["bloom.threshold"],s),o=n?((e,t,a,s)=>{const r=filteredCopy(e,t,a,"none");return r&&applyScopeMask(r,s),r})(e,t,a,n):e;if(!o)return;const h=filteredCopy(o,t,a,i);h&&drawOver(e,h,"screen",r["bloom.intensity"])})(e,t,a,s,r,i);const o=gradeFilter(r["grade.brightness"],r["grade.contrast"],r["grade.saturation"]);o&&((e,t,a,s)=>{const r=filteredCopy(e,t,a,s);r&&drawOver(e,r,"copy")})(e,t,a,o);const h=e=>s=>fillEllipticalRadial(s,t,a,e,"rgba(0,0,0,0)","rgba(0,0,0,1)");if(r["fog.intensity"]>0){const i=r["fog.intensity"],o=r["fog.reach"],{inner:l,outer:c}=fogVeilStops(i,n),d=fogBlurPx(i);applyMaskedBlur(e,t,a,s,d,fogGradeFilter(n),h(o)),fillEllipticalRadial(e,t,a,o,l,c)}if(r["edgeBlur.intensity"]>0){const n=r["edgeBlur.intensity"];applyMaskedBlur(e,t,a,s,n,"saturate(1.08)",h(r["edgeBlur.reach"]))}if(r["tint.opacity"]>0&&(e.save(),e.globalAlpha=r["tint.opacity"],e.fillStyle=r["tint.color"],e.fillRect(0,0,t,a),e.restore()),0!==r["vignette.intensity"]){const{inner:s,outer:n}=vignetteStops(r["vignette.intensity"]);fillEllipticalRadial(e,t,a,r["vignette.reach"],s,n)}},effectValueOf=(e,t)=>knobValueIn(e,t)??effectKnobDefinitions[t].default,withDefaults=e=>Object.fromEntries(effectKnobIds.map(t=>[t,effectValueOf(e,t)])),clampAndFilter=e=>{e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_S,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_T,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MAG_FILTER,e.LINEAR),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,e.LINEAR)},resizeCanvas=(e,t,a)=>{e.width===t&&e.height===a||(e.width=t,e.height=a)},compile=(e,t,a)=>{const s=e.createShader(t);if(!s)return"the context would not create a shader";if(e.shaderSource(s,a),e.compileShader(s),e.getShaderParameter(s,e.COMPILE_STATUS))return s;const r=e.getShaderInfoLog(s)??"";return e.deleteShader(s),`failed to compile: ${r}`},ScreenPassRenderer=class{canvas;context;programs=/* @__PURE__ */new Map;failures=/* @__PURE__ */new Set;sourceTexture;targets=[];targetSize={width:0,height:0};lost=!1;constructor(){this.canvas=document.createElement("canvas"),this.canvas.addEventListener("webglcontextlost",e=>{e.preventDefault(),this.lost=!0}),this.canvas.addEventListener("webglcontextrestored",()=>{this.programs.clear(),this.failures.clear(),this.sourceTexture=void 0,this.targets=[],this.targetSize={width:0,height:0},this.lost=!1})}get gl(){return void 0===this.context&&(this.context=this.canvas.getContext("webgl2",{alpha:!0,antialias:!1,premultipliedAlpha:!1,preserveDrawingBuffer:!0})),this.context??void 0}render(e,t){if(!t.length||!e.width||!e.height)return;const a=this.gl;if(!a||this.lost)return;resizeCanvas(this.canvas,e.width,e.height);let s=this.uploadSource(e,t[0].mipmapped);if(!(!s||t.length>1&&!this.holdTargets(a))){for(let e=0;e<t.length;e++){const r=e===t.length-1?void 0:this.targets[e%this.targets.length];this.runStage(a,t[e],s,r),r&&(s=r.texture,t[e+1].mipmapped&&this.mipmap(a,s))}return this.canvas}}get hasContext(){return void 0!==this.gl}compileError(e){const t=this.gl;if(!t||this.lost)return;const a=this.cachedProgram(t,e);return"string"==typeof a?a:void 0}release(e){const t=this.programs.get(e);t&&this.context?.deleteProgram(t),this.programs.delete(e),this.failures.delete(e)}remove(){this.context?.getExtension("WEBGL_lose_context")?.loseContext(),this.context=null}holdTargets(e){const{width:t,height:a}=this.canvas;if(2===this.targets.length&&this.targetSize.width===t&&this.targetSize.height===a)return!0;for(this.releaseTargets(e);this.targets.length<2;){const s=e.createTexture(),r=e.createFramebuffer();if(!s||!r)return!1;e.bindTexture(e.TEXTURE_2D,s),e.texImage2D(e.TEXTURE_2D,0,e.RGBA,t,a,0,e.RGBA,e.UNSIGNED_BYTE,null),clampAndFilter(e),e.bindFramebuffer(e.FRAMEBUFFER,r),e.framebufferTexture2D(e.FRAMEBUFFER,e.COLOR_ATTACHMENT0,e.TEXTURE_2D,s,0),this.targets.push({texture:s,framebuffer:r})}return this.targetSize={width:t,height:a},!0}releaseTargets(e){for(const{texture:t,framebuffer:a}of this.targets)e.deleteTexture(t),e.deleteFramebuffer(a);this.targets=[]}mipmap(e,t){e.bindTexture(e.TEXTURE_2D,t),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,e.LINEAR_MIPMAP_LINEAR),e.generateMipmap(e.TEXTURE_2D)}uploadSource(e,t=!1){const a=this.gl;if(a&&(this.sourceTexture??=a.createTexture()??void 0,this.sourceTexture))return a.bindTexture(a.TEXTURE_2D,this.sourceTexture),a.pixelStorei(a.UNPACK_FLIP_Y_WEBGL,!0),a.texImage2D(a.TEXTURE_2D,0,a.RGBA,a.RGBA,a.UNSIGNED_BYTE,e),clampAndFilter(a),t&&this.mipmap(a,this.sourceTexture),this.sourceTexture}runStage(e,t,a,s){const r=this.programOf(e,t.fragment);if(r){e.bindFramebuffer(e.FRAMEBUFFER,s?.framebuffer??null),e.viewport(0,0,this.canvas.width,this.canvas.height),e.useProgram(r),e.activeTexture(e.TEXTURE0),e.bindTexture(e.TEXTURE_2D,a),e.uniform1i(e.getUniformLocation(r,"uSource"),0);for(const[a,s]of Object.entries(t.uniforms)){const t=e.getUniformLocation(r,a);t&&("number"==typeof s?e.uniform1f(t,s):e.uniform2f(t,s[0],s[1]))}e.drawArrays(e.TRIANGLES,0,3)}}programOf(e,t){if(this.failures.has(t))return;const a=this.cachedProgram(e,t);if("string"!=typeof a)return a;this.failures.add(t),console.warn(`[MapEffects] shader ${a}`)}cachedProgram(e,t){const a=this.programs.get(t);if(a)return a;const s=this.build(e,t);return"string"!=typeof s&&this.programs.set(t,s),s}build(e,t){const a=[compile(e,e.VERTEX_SHADER,"#version 300 es\n// ⚠️ The directive above has to be the literal first line — ANGLE rejects even a comment before it,\n// and the whole program then fails to compile.\n//\n// The vertex half of every pass the substrate runs — see ../screenPass.ts, which loads this file\n// with Vite's `?raw`.\n//\n// It belongs to the substrate rather than to any one pass, because what it interprets is the\n// substrate's own geometry: a single full-screen triangle. One primitive rather than a quad's two,\n// so no diagonal seam is rasterised twice, and the corners fall out of `gl_VertexID` so there is no\n// buffer to bind.\n//\n// ⚠️ `vUv` runs with v increasing upward, matching the flipped upload in `uploadSource` — so v = 0\n// is the bottom of the frame, which on a tilted map is the nearest ground. The depth of field\n// depends on that, so it is a contract rather than an accident.\n\nout vec2 vUv;\n\nvoid main() {\n vec2 corner = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);\n vUv = corner;\n gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0);\n}\n"),compile(e,e.FRAGMENT_SHADER,`#version 300 es\n// ⚠️ The directive above has to be the literal first line — ANGLE rejects even a comment before it —\n// which is also why it lives here rather than in a stage: a stage is appended to this file, and a\n// second directive anywhere is a compile error. ../../tests/effectFilters.test.ts holds that.\n//\n// Not a program: the preamble ../screenPass.ts prepends to every fragment stage it compiles. It\n// carries what is the same for all of them — the source texture, the varying it is read at, the\n// colour that is written — so a pass declares only what is its own, and the substrate stays the one\n// place those names are spelled.\n//\n// GLSL ES 3.00, for the two things ES 1.00 cannot give a gather: \`textureLod\`, without which a tap\n// cannot read a prefiltered mip level, and a loop it may leave early.\n\nprecision highp float;\nprecision highp sampler2D;\n\nuniform sampler2D uSource;\nin vec2 vUv;\nout vec4 fragColor;\n${t}`)],s=a.find(e=>"string"==typeof e),r=a.filter(e=>"string"!=typeof e),n=s?null:e.createProgram();if(n){for(const t of r)e.attachShader(n,t);e.linkProgram(n)}for(const o of r)e.deleteShader(o);if(s)return s;if(!n)return"the context would not create a program";if(e.getProgramParameter(n,e.LINK_STATUS))return n;const i=e.getProgramInfoLog(n)??"";return e.deleteProgram(n),`failed to link: ${i}`}},PASS_PLACEMENTS={"before-depth-of-field":!0,"after-depth-of-field":!0},OVERLAYS=["bloom","grade","fog","edgeBlur","tint","vignette"],backdropFilterStyle=e=>({backdropFilter:e,webkitBackdropFilter:e}),maskStyle=e=>({maskImage:e,webkitMaskImage:e}),LAYER_STYLE={position:"absolute",inset:"0",width:"100%",height:"100%",pointerEvents:"none",display:"none"},rasterScaleOf=e=>e.width/Math.max(e.clientWidth,1),drawOncePerFrame=e=>{let t;return{schedule:()=>{void 0===t&&(t=requestAnimationFrame(()=>{t=void 0,e()}))},cancel:()=>{void 0!==t&&cancelAnimationFrame(t),t=void 0}}},MapEffects=class{map;config={};passes=[];container;overlays;bloomCanvas;artworkCanvas;chain=new ScreenPassRenderer;chainDraw=drawOncePerFrame(()=>this.drawChain());bloomDraw=drawOncePerFrame(()=>this.drawBloom());scopeMask;scopeCanvas;warnedBuffer=!1;warnedScope=!1;warnedNoWebGL2=!1;warnedUniforms=/* @__PURE__ */new WeakSet;onStyleData=()=>{this.dropScopeMask(),this.showFog(withDefaults(this.config))};onRender=()=>{this.chainDraw.schedule(),this.scheduleBloom()};constructor(e,t={}){this.map=e;const a=this.map.mapLibreMap.getContainer();this.container=document.createElement("div"),this.container.className="tomtom-map-effects",Object.assign(this.container.style,{position:"absolute",inset:"0",pointerEvents:"none",overflow:"hidden"}),a.querySelector(".maplibregl-canvas-container")?.insertAdjacentElement("afterend",this.container)??a.appendChild(this.container),this.bloomCanvas=document.createElement("canvas"),this.overlays=Object.fromEntries(OVERLAYS.map(e=>{const t="bloom"===e?this.bloomCanvas:document.createElement("div");return t.dataset.effect=e,Object.assign(t.style,LAYER_STYLE),this.container.appendChild(t),[e,t]})),this.bloomCanvas.style.mixBlendMode="screen",this.artworkCanvas=this.chain.canvas,this.artworkCanvas.dataset.effect="artwork",Object.assign(this.artworkCanvas.style,LAYER_STYLE),this.container.insertAdjacentElement("afterbegin",this.artworkCanvas),this.map.mapLibreMap.on("render",this.onRender),this.map.mapLibreMap.on("styledata",this.onStyleData),this.applyConfig(t)}cameraAngles(){const e=this.map.mapLibreMap;return{pitch:e.getPitch(),verticalFieldOfView:e.getVerticalFieldOfView()}}getConfig(){return structuredClone(this.config)}applyConfig(e){this.config=e,this.render()}updateConfig(e){this.applyConfig({...this.config,...e})}resetConfig(){this.applyConfig({})}_knobValue(e){return knobValueIn(this.config,e)}_setKnobValues(e){this.applyConfig(configWithKnobs(this.config,e))}_resetKnobValue(e){this.applyConfig(configWithoutKnob(this.config,e))}addPass(e){if(!e.id)throw new Error("A pass needs an id, to be replaced or removed by.");if(void 0!==e.placement&&!Object.hasOwn(PASS_PLACEMENTS,e.placement))throw new RangeError(`Pass '${e.id}' expects a placement of ${Object.keys(PASS_PLACEMENTS).join(" or ")}; got ${String(e.placement)}.`);this.chain.hasContext||this.warnNoWebGL2();const t=this.chain.compileError(e.fragment);if(t)throw new Error(`Pass '${e.id}' ${t}`);const a={...e},s=this.passes.find(t=>t.id===e.id);this.passes=s?this.passes.map(e=>e===s?a:e):[...this.passes,a],s&&this.releaseUnused(s.fragment),this.warnIfBufferNotPreserved(),this.render()}removePass(e){const t=this.passes.find(t=>t.id===e);t&&(this.passes=this.passes.filter(e=>e!==t),this.releaseUnused(t.fragment),this.render())}async capture(e={}){this.warnIfBufferNotPreserved();const t=this.map.mapLibreMap,a=t,s=t.getPixelRatio(),r=e.pixelRatio??s,n=s!==(globalThis.devicePixelRatio||1),i=Math.abs(r-s)>.01;i&&(a.setPixelRatio(r),await this.settled());try{return await this.readBack()}finally{i&&a.setPixelRatio(n?s:null)}}remove(){this.map.mapLibreMap.off("render",this.onRender),this.map.mapLibreMap.off("styledata",this.onStyleData),this.bloomDraw.cancel(),this.chainDraw.cancel(),this.chain.remove(),this.container.remove()}show(e,t,a={}){const s=this.overlays[e];s.style.display=t?"block":"none",Object.assign(s.style,a)}render(){const e=withDefaults(this.config),t=gradeFilter(e["grade.brightness"],e["grade.contrast"],e["grade.saturation"]);var a;this.show("grade",""!==t,backdropFilterStyle(t)),this.showFog(e),this.show("edgeBlur",e["edgeBlur.intensity"]>0,{...backdropFilterStyle((a=e["edgeBlur.intensity"],`blur(${a}px) saturate(1.08)`)),...maskStyle(rimMask(e["edgeBlur.reach"]))}),e["depthOfField.intensity"]>0&&this.warnIfBufferNotPreserved(),this.drawChain(e),this.show("tint",e["tint.opacity"]>0,{backgroundColor:e["tint.color"],opacity:String(e["tint.opacity"])}),this.show("vignette",0!==e["vignette.intensity"],{backgroundImage:vignetteGradient(e["vignette.intensity"],e["vignette.reach"])});const s=e["bloom.intensity"]>0;this.show("bloom",s,{opacity:String(e["bloom.intensity"])}),s&&(this.warnIfBufferNotPreserved(),this.scheduleBloom())}showFog(e){const t=this.map.styleLightDarkTheme;this.show("fog",e["fog.intensity"]>0,{...backdropFilterStyle(fogFilter(e["fog.intensity"],t)),...maskStyle(rimMask(e["fog.reach"])),backgroundImage:fogVeil(e["fog.intensity"],e["fog.reach"],t)})}chainStages(e){return[...this.passStages("before-depth-of-field"),...this.depthOfFieldStages(e),...this.passStages("after-depth-of-field")]}passStages(e){return this.passes.filter(t=>(t.placement??"before-depth-of-field")===e).flatMap(e=>{const t=this.uniformsOf(e);return t?[{fragment:e.fragment,uniforms:t}]:[]})}uniformsOf(e){const{uniforms:t={}}=e;if("function"!=typeof t)return t;try{return t()}catch(a){return void(this.warnedUniforms.has(e)||(this.warnedUniforms.add(e),console.warn(`[MapEffects] pass '${e.id}' is skipped while its uniforms() throws:`,a)))}}releaseUnused(e){this.passes.some(t=>t.fragment===e)||this.chain.release(e)}depthOfFieldStages(e){const t=this.cameraAngles();if(e["depthOfField.intensity"]<=0||depthRamp(t)<=0)return[];const a=this.map.mapLibreMap.getCanvas(),s=((e,t,a)=>{const s=depthRamp(t);if(!(s<=0||e.blurCssPx<=0))return{uDepth:s,uFocus:Math.min(asFraction(e.focus)*s,1),uBand:.5*asFraction(e.band),uBlur:Math.max(e.blurCssPx,0)*a.scale,uBokeh:asFraction(e.bokeh),uTexel:[1/Math.max(a.width,1),1/Math.max(a.height,1)]}})({focus:e["depthOfField.focus"],band:e["depthOfField.band"],blurCssPx:e["depthOfField.intensity"],bokeh:e["depthOfField.bokeh"]},t,{width:a.width,height:a.height,scale:rasterScaleOf(a)});return s?[{fragment:"// The depth-of-field pass: the circle of confusion a tilted camera implies at each row of the\n// picture, and the aperture gathered over it. See ../defocusShader.ts, which loads this file and\n// names the uniforms below, and `defocusUniforms` in ../../effectFilters.ts, which works out the\n// optics this only applies.\n//\n// The declarations the substrate already made — `uSource`, `vUv`, `fragColor`, the precisions and\n// the version directive — are in ./fragmentPreamble.glsl and must not be repeated here.\n\n// What each of these means is on `DefocusUniforms` in ../defocusShader.ts, which is also where the\n// spelling is held to match.\nuniform float uDepth;\nuniform float uFocus;\nuniform float uBand;\nuniform float uBlur;\nuniform float uBokeh;\nuniform vec2 uTexel;\n\n// A tap budget rather than a tap count: the loop is bounded by a constant so a wide disc stays\n// affordable, and filled to `taps` so a narrow one stays smooth.\nconst int MAX_TAPS = 48;\n// The golden angle: successive taps land as far from every previous one as a turn can put them, so\n// a partly filled disc is still an even disc.\nconst float GOLDEN_ANGLE = 2.39996323;\n\n// Where a row falls on the depth scale: 0 at the nearest ground, 1 at the horizon. Linear in the\n// row because reciprocal distance across a plane really is affine in screen space.\nfloat depthAt(float v) {\n return uDepth * v;\n}\n\n/**\n * The circle of confusion at a row, in device pixels. Proportional to the distance from the plane\n * of focus in reciprocal units, which is what a thin lens does. The sharp band is taken out of the\n * ramp and the remainder stretched back over the full range, so widening the field moves where the\n * blur starts without also weakening how far it goes.\n */\nfloat circleOfConfusion(float v) {\n float defocus = abs(depthAt(v) - uFocus) - uBand;\n return uBlur * clamp(defocus / max(1.0 - uBand, 0.001), 0.0, 1.0);\n}\n\n// Rec. 709 luma.\nfloat luma(vec3 colour) {\n return dot(colour, vec3(0.2126, 0.7152, 0.0722));\n}\n\n/**\n * What makes it a bokeh rather than a blur: a lens does not average the light it cannot resolve, it\n * spreads it, so every point arrives as an image of the aperture at its own brightness. Weighting a\n * sample by the fourth power of its brightness keeps a highlight's value across the whole disc it\n * is spread over — at half brightness the extra weight is a sixteenth, so ordinary map colour still\n * averages as it should. Deliberately not energy-conserving: normalising that away leaves a blur.\n */\nfloat bokehWeight(vec3 colour) {\n float bright = luma(colour);\n float fourth = bright * bright * bright * bright;\n return 1.0 + uBokeh * 8.0 * fourth;\n}\n\n// Per-pixel and stable, so a still picture is still: the angle the disc below is spun by.\nfloat hash(vec2 at) {\n vec3 spread = fract(vec3(at.xyx) * 0.1031);\n spread += dot(spread, spread.yzx + 33.33);\n return fract((spread.x + spread.y) * spread.z);\n}\n\n/**\n * The aperture, gathered: a disc of `coc` diameter, weighted so the highlights survive it.\n *\n * Two dozen taps across a wide disc is a scatter of ghosts rather than a blur, and the fix is not\n * more taps but bigger ones — each is read from the mip level whose texels are about as wide as the\n * gaps between them, so what is sampled is already the average of what was skipped. The two numbers\n * are tied to each other rather than tuned apart, which is what keeps a wide disc the same picture\n * as a narrow one instead of a coarser one.\n */\nvec4 defocus(vec2 uv, float coc) {\n float radius = coc * 0.5;\n // Inside a texel there is nothing to gather, and the sharp band all comes through here — it has\n // to hand back the source unchanged.\n if (radius < 0.75) return textureLod(uSource, uv, 0.0);\n\n int taps = int(clamp(radius * 1.5, 12.0, float(MAX_TAPS)));\n float spacing = radius * sqrt(3.14159265 / float(taps));\n float level = max(log2(spacing), 0.0);\n // Without the spin every pixel samples the same directions and the residual undersampling lines\n // up into streaks; broken up, what is left reads as grain.\n float spin = hash(gl_FragCoord.xy) * 6.28318531;\n\n vec4 centre = textureLod(uSource, uv, level);\n float weight = bokehWeight(centre.rgb);\n vec4 total = vec4(centre.rgb * centre.a, centre.a) * weight;\n float carried = weight;\n\n for (int index = 0; index < MAX_TAPS; index++) {\n if (index >= taps) break;\n\n // The square root of the fraction, not the fraction: a disc's area grows with the square of\n // its radius, so evenly spaced radii would crowd every tap into the middle.\n float fraction = (float(index) + 0.5) / float(taps);\n float distance = radius * sqrt(fraction);\n float angle = float(index) * GOLDEN_ANGLE + spin;\n vec2 offset = vec2(cos(angle), sin(angle)) * distance;\n\n vec4 sampled = textureLod(uSource, uv + offset * uTexel, level);\n float tapWeight = bokehWeight(sampled.rgb);\n total += vec4(sampled.rgb * sampled.a, sampled.a) * tapWeight;\n carried += tapWeight;\n }\n\n // Colour comes back out of the premultiplied sum by its own alpha, the alpha by the weight.\n return vec4(total.rgb / max(total.a, 0.0001), total.a / carried);\n}\n\nvoid main() {\n // The texture is uploaded flipped, so this row runs from the bottom of the frame — the nearest\n // ground — which is the direction the depth ramp is measured in.\n fragColor = defocus(vUv, circleOfConfusion(vUv.y));\n}\n",uniforms:s,mipmapped:!0}]:[]}drawChain(e=withDefaults(this.config)){const t=this.chainStages(e),a=t.length?this.chain.render(this.map.mapLibreMap.getCanvas(),t):void 0;this.artworkCanvas.style.display=a?"block":"none"}scheduleBloom(){effectValueOf(this.config,"bloom.intensity")>0&&this.bloomDraw.schedule()}drawBloom(){const e="block"===this.artworkCanvas.style.display?this.artworkCanvas:this.map.mapLibreMap.getCanvas(),t=Math.max(1,Math.round(.5*e.width)),a=Math.max(1,Math.round(.5*e.height));resizeCanvas(this.bloomCanvas,t,a);const s=this.bloomCanvas.getContext("2d");if(!s)return;const r=this.scopedSource(e,t,a)??e;s.filter=bloomFilter(effectValueOf(this.config,"bloom.radius"),effectValueOf(this.config,"bloom.threshold"),.5),s.clearRect(0,0,t,a),s.drawImage(r,0,0,t,a)}scopedSource(e,t,a){const s=this.currentScopeMask();if(!s)return;this.scopeCanvas??=document.createElement("canvas");const r=this.scopeCanvas;resizeCanvas(r,t,a);const n=r.getContext("2d",{willReadFrequently:!0});return n?(n.clearRect(0,0,t,a),n.drawImage(e,0,0,t,a),applyScopeMask(n,s),r):void 0}currentScopeMask(){const e=effectValueOf(this.config,"bloom.only");if(0===e.length)return;const t=e.join("\n"),a=effectValueOf(this.config,"bloom.onlyTolerance");return this.scopeMask?.key===t&&this.scopeMask.tolerance===a||(this.scopeMask={key:t,tolerance:a,mask:this.buildScopeMaskFor(e,a)}),this.scopeMask.mask}buildScopeMaskFor(e,t){const a=((e,t)=>t.flatMap(t=>{const a=NAMED_KNOBS.get(t);return(a?a.map(t=>getStyleColor(e,t)):[t]).map(e=>"string"==typeof e?parseCssColor(e):void 0).filter(e=>void 0!==e)}))(this.map,e);return 0===a.length&&this.warnScopeEmpty(e),((e,t)=>{const a=e.map(chromaticity).filter(e=>void 0!==e);if(0===a.length)return;const s=t*t,r=new Uint8Array(32768);for(let n=0;n<256;n+=8)for(let e=0;e<256;e+=8)for(let t=0;t<256;t+=8){const i=chromaticity([n,e,t]);i&&a.some(e=>{const t=i[0]-e[0],a=i[1]-e[1],r=i[2]-e[2];return t*t+a*a+r*r<=s})&&(r[cubeIndex(n,e,t)]=1)}return r})(a,t)}warnScopeEmpty(e){this.warnedScope||(this.warnedScope=!0,console.warn(`[MapEffects] 'bloom.only: ${e.join(", ")}' found no colour to key on in the loaded style, so bloom is lighting the whole frame.`))}dropScopeMask(){this.scopeMask=void 0,this.scheduleBloom()}readBack(){const e=this.map.mapLibreMap;return new Promise(t=>{e.once("render",()=>{const a=e.getCanvas(),s=withDefaults(this.config),r=document.createElement("canvas");r.width=a.width,r.height=a.height;const n=r.getContext("2d");if(n){const e=this.chainStages(s);n.drawImage(this.chain.render(a,e)??a,0,0),compositeEffects(n,a.width,a.height,rasterScaleOf(a),s,this.map.styleLightDarkTheme,this.currentScopeMask())}t(r)}),e.triggerRepaint()})}settled(e=15e3){const t=this.map.mapLibreMap;return new Promise(a=>{const s=()=>{clearTimeout(r),t.off("idle",s),a()},r=setTimeout(s,e);t.once("idle",s)})}warnIfBufferNotPreserved(){if(this.warnedBuffer)return;const e=this.map.mapLibreMap.getCanvas(),t=(e.getContext("webgl2")??e.getContext("webgl"))?.getContextAttributes?.();t&&!t.preserveDrawingBuffer&&(this.warnedBuffer=!0,console.warn("[MapEffects] bloom, the depth of field, app passes and capture read the map canvas, which needs `mapLibre: { canvasContextAttributes: { preserveDrawingBuffer: true } }` on the map's options."))}warnNoWebGL2(){this.warnedNoWebGL2||(this.warnedNoWebGL2=!0,console.warn("[MapEffects] this browser gives no WebGL2 context, so passes from addPass neither compile nor run."))}};export{MapEffects,effectKnobCatalogue,effectKnobIds};
|
|
1
|
+
import{configWithKnobs,configWithoutKnob,getStyleColor,knobValueIn,parseCssColor,styleColorKnobIds,toHsl}from"@tomtom-org/maps-sdk/map";var everyColor,groupsOf=e=>e.split(".").slice(0,-1).map((e,t,a)=>a.slice(0,t+1).join(".")),NAMED_KNOBS=/* @__PURE__ */(()=>{const e=/* @__PURE__ */new Map;for(const t of styleColorKnobIds)for(const a of[...groupsOf(t),t])e.set(a,[...e.get(a)??[],t]);return e})(),bloomScopeNames=/* @__PURE__ */(()=>[...NAMED_KNOBS.keys()])(),cubeIndex=(e,t,a)=>32*(32*(e>>3)+(t>>3))+(a>>3),chromaticity=([e,t,a])=>{const r=Math.max(e,t,a);if(0!==r)return[e/r,t/r,a/r]},FRACTION={min:0,max:1,step:.05,bounds:"hard"},TOLERANCE={min:0,max:1,step:.05,bounds:"hard-min"},GRADE_TERM={min:.5,max:1.5,step:.05,bounds:"hard-min"},BLUR_CSS_PX={min:0,max:30,step:1,bounds:"hard-min"},USE_CASE_bloom="Emphasis on dark and driving styles.",USE_CASE_glare="Lights that read as lights on dark styles: lit roads, icons and routes cast streaks, as lamps do through a lens.",USE_CASE_grade="Muting the base map under a data overlay.",USE_CASE_fog="Depth cue: the far context recedes as if seen through air.",USE_CASE_edgeBlur="Focus: the centre stays sharp and the edges lose their detail.",USE_CASE_depthOfField="Depth on a tilted map: one band of distance stays sharp, the rest defocuses with distance.",USE_CASE_tint="Muting the base map under a data overlay; matching a brand.",effectKnobDefinitions={"bloom.intensity":{kind:"number",description:"How strongly bright features (lit roads, traffic tubes, route lines) glow. 0 = off.",default:0,range:FRACTION,useCase:USE_CASE_bloom},"bloom.radius":{kind:"number",description:"How far the glow spreads, in CSS pixels.",default:12,range:{min:2,max:40,step:1,bounds:"hard-min"},useCase:USE_CASE_bloom},"bloom.threshold":{kind:"number",description:"Brightness below which nothing glows. Raise it so only the brightest features light up.",default:.55,range:FRACTION,useCase:USE_CASE_bloom},"bloom.only":{kind:"colors",description:"The colours bloom may light: CSS colours, style colour knobs or groups of them (`traffic`, `traffic.incidents.colors.major`), read from the loaded style. Only the pixels wearing one of them glow; empty lights the whole frame.",default:[],options:bloomScopeNames,useCase:USE_CASE_bloom},"bloom.onlyTolerance":{kind:"number",description:"How far a pixel may sit from one of the scope's colours and still glow. Raise it when antialiasing has mixed the colour with the map underneath; lower it when a neighbouring severity is glowing too.",default:.15,range:TOLERANCE,useCase:USE_CASE_bloom},"glare.intensity":{kind:"number",description:"How strongly the lights cast streaks, as a star filter or an anamorphic lens draws them from a lamp. 0 = off.",default:0,range:FRACTION,useCase:USE_CASE_glare},"glare.length":{kind:"number",description:"How far each streak reaches from its light, in CSS pixels.",default:32,range:{min:4,max:120,step:1,bounds:"hard-min"},useCase:USE_CASE_glare},"glare.rays":{kind:"number",description:"How many streaks each light casts: 2 is an anamorphic lens’s line, 4 a cross, 6 and 8 a star.",default:4,range:{min:2,max:8,step:2,bounds:"hard"},useCase:USE_CASE_glare},"glare.angle":{kind:"number",description:"The first streak’s direction, in degrees anticlockwise from the horizontal; the others share the turn evenly.",default:45,range:{min:0,max:180,step:5,bounds:"hard"},useCase:USE_CASE_glare},"glare.threshold":{kind:"number",description:"Brightness below which a light casts nothing.",default:.3,range:FRACTION,useCase:USE_CASE_glare},"glare.only":{kind:"colors",description:"The colours that count as lights, as `bloom.only` names them. By default the major roads, read from the loaded style; empty lets every bright pixel streak, labels included.",default:["colors.roadMajor"],options:bloomScopeNames,useCase:USE_CASE_glare},"glare.onlyTolerance":{kind:"number",description:"How far a pixel may sit from a light's colour and still streak.",default:.15,range:TOLERANCE,useCase:USE_CASE_glare},"grade.brightness":{kind:"number",description:"Brightness of the whole base map (1 = unchanged).",default:1,range:GRADE_TERM,useCase:USE_CASE_grade},"grade.contrast":{kind:"number",description:"Contrast of the whole base map (1 = unchanged).",default:1,range:GRADE_TERM,useCase:USE_CASE_grade},"grade.saturation":{kind:"number",description:"Colour saturation of the whole base map (1 = unchanged, 0 = greyscale).",default:1,range:{min:0,max:1.5,step:.05,bounds:"hard-min"},useCase:USE_CASE_grade},"fog.intensity":{kind:"number",description:"Haze over the edges of the view: blur, plus a wash towards the colour of the air (`fog.color`). Reads as distance. 0 = off.",default:0,range:FRACTION,useCase:USE_CASE_fog},"fog.reach":{kind:"number",description:"How far the fog reaches in from the edges (1 = to the centre).",default:.4,range:FRACTION,useCase:USE_CASE_fog},"fog.color":{kind:"color",description:"The colour of the air the fog washes towards; its lightness brightens or darkens the rim with it, and a translucent colour does both less. Unset, it follows the map's light/dark theme: pale on a light map, dark on a dark one.",useCase:USE_CASE_fog},"edgeBlur.intensity":{kind:"number",description:"Blur over the edges of the view, in CSS pixels, leaving their colour untouched. Reads as a lens, not as distance — `fog` is the effect that also washes the rim out. 0 = off.",default:0,range:BLUR_CSS_PX,useCase:USE_CASE_edgeBlur},"edgeBlur.reach":{kind:"number",description:"How far the edge blur reaches in from the edges (1 = to the centre).",default:.5,range:FRACTION,useCase:USE_CASE_edgeBlur},"depthOfField.intensity":{kind:"number",description:"The widest circle of confusion, in CSS pixels — the blur the most out-of-focus distance gets. Needs a tilted camera: a map seen from straight above has no depth, and the effect ends there on its own. 0 = off.",default:0,range:BLUR_CSS_PX,useCase:USE_CASE_depthOfField},"depthOfField.focus":{kind:"number",description:"Which distance is sharp, across what the frame shows: 0 the nearest ground at the bottom of the view, 1 the farthest at the top.",default:0,range:FRACTION,useCase:USE_CASE_depthOfField},"depthOfField.band":{kind:"number",description:"How much of that depth stays sharp — the field itself. Thin is the miniature; wide holds a whole run of the map. A gentle tilt has little depth, so the same value covers more of the view.",default:.3,range:FRACTION,useCase:USE_CASE_depthOfField},"depthOfField.bokeh":{kind:"number",description:"Whether an out-of-focus highlight fades or comes back as a disc of light. 0 averages the light away, as a blur does; turned up, a lit road or a traffic tube keeps its brightness across the whole circle it is spread over.",default:.5,range:FRACTION,useCase:USE_CASE_depthOfField},"tint.color":{kind:"color",description:"A flat colour laid over the whole map.",default:"#1f2937",useCase:USE_CASE_tint},"tint.opacity":{kind:"number",description:"Opacity of the tint. 0 = off.",default:0,range:FRACTION,useCase:USE_CASE_tint},"vignette.intensity":{kind:"number",description:"Darkening (negative) or lightening (positive) towards the edges. 0 = off.",default:0,range:{min:-1,max:1,step:.05,bounds:"hard"},useCase:"Focus on the centre of the view."},"vignette.reach":{kind:"number",description:"How far the vignette reaches in from the edges (1 = to the centre).",default:.45,range:FRACTION,useCase:"Focus on the centre of the view."}},effectKnobIds=/* @__PURE__ */Object.keys(effectKnobDefinitions),effectKnobCatalogue=/* @__PURE__ */effectKnobIds.map(e=>({id:e,...effectKnobDefinitions[e]})),clamp=(e,t,a)=>Math.min(Math.max(e,t),a),asFraction=e=>clamp(e,0,1),bloomThreshold=e=>{const t=.92*asFraction(e);return{brightness:1/(1+t),contrast:(1+t)/(1-t)}},bloomFilter=(e,t,a=1)=>{const{brightness:r,contrast:s}=bloomThreshold(t);return`brightness(${r.toFixed(4)}) contrast(${s.toFixed(3)}) blur(${e*a}px) saturate(1.35)`},texelOf=e=>[1/Math.max(e.width,1),1/Math.max(e.height,1)],lightUniforms=(e,t)=>{const{brightness:a,contrast:r}=bloomThreshold(e.threshold);return{uBrightness:a,uContrast:r,uTexel:texelOf(t),uIntensity:asFraction(e.intensity),uSaturation:1.35}},toRadians=e=>e*Math.PI/180,gradeFilter=(e,t,a)=>[1===e?"":`brightness(${e})`,1===t?"":`contrast(${t})`,1===a?"":`saturate(${a})`].filter(Boolean).join(" "),rimGradient=(e,t,a)=>`radial-gradient(ellipse farthest-corner at center, ${e} ${Math.round(100*(1-asFraction(a)))}%, ${t} 100%)`,vignetteGradient=(e,t)=>{const{inner:a,outer:r}=vignetteStops(e);return rimGradient(a,r,t)},vignetteStops=e=>{const t=e<0?"0, 0, 0":"255, 255, 255";return{inner:`rgba(${t}, 0)`,outer:`rgba(${t}, ${Math.min(Math.abs(e),1)})`}},rimMask=e=>rimGradient("transparent","black",e),depthRamp=({pitch:e,verticalFieldOfView:t})=>{const a=Math.tan(toRadians(clamp(e,0,89))),r=2*Math.tan(toRadians(clamp(t,1,175))/2)*a;return r/(1+r/2)},THEME_AIR={light:{rgb:[238,240,244],brightness:1.08},dark:{rgb:[16,22,33],brightness:.92}},lightnessOf=e=>toHsl(`rgb(${e.join(", ")})`)?.l??0,fogAir=(e,t)=>{const a=THEME_AIR[t],r=void 0===e?void 0:parseCssColor(e),s=r?[Math.round(255*r[0]),Math.round(255*r[1]),Math.round(255*r[2])]:a.rgb,n=r?.[3]??1,i=a.brightness+n*((e=>{const t=lightnessOf(THEME_AIR.dark.rgb),a=(lightnessOf(e)-t)/(lightnessOf(THEME_AIR.light.rgb)-t);return THEME_AIR.dark.brightness+a*(THEME_AIR.light.brightness-THEME_AIR.dark.brightness)})(s)-a.brightness);return{rgb:s.join(", "),alpha:n,grade:`brightness(${Math.round(1e3*i)/1e3}) contrast(0.82) saturate(0.7)`}},fogBlurPx=e=>2+6*e,fogFilter=(e,t)=>`blur(${fogBlurPx(e)}px) ${t.grade}`,fogVeilStops=(e,t)=>({inner:`rgba(${t.rgb}, 0)`,outer:`rgba(${t.rgb}, ${.65*e*t.alpha})`}),fogVeil=(e,t,a)=>{const{inner:r,outer:s}=fogVeilStops(e,a);return rimGradient(r,s,t)},filteredCopy=(e,t,a,r)=>{const s=document.createElement("canvas");s.width=t,s.height=a;const n=s.getContext("2d");if(n)return n.filter=r,n.drawImage(e.canvas,0,0),n.filter="none",n},drawOver=(e,t,a,r=1)=>{e.save(),e.globalCompositeOperation=a,e.globalAlpha=r,e.drawImage(t.canvas,0,0),e.restore()},fillEllipticalRadial=(e,t,a,r,s,n)=>{const i=t/Math.SQRT2,o=a/Math.SQRT2,l=e.createRadialGradient(0,0,1-r,0,0,1);l.addColorStop(0,s),l.addColorStop(1,n),e.save(),e.translate(t/2,a/2),e.scale(i,o),e.fillStyle=l,e.fillRect(-t/2/i,-a/2/o,t/i,a/o),e.restore()},applyMaskedBlur=(e,t,a,r,s,n,i)=>{const o=filteredCopy(e,t,a,`blur(${s*r}px) ${n}`.trim());o&&(o.globalCompositeOperation="destination-in",i(o),e.drawImage(o.canvas,0,0))},OVERLAYS=["bloom","grade","fog","edgeBlur","tint","vignette"],effectValueOf=(e,t)=>{const a=effectKnobDefinitions[t];return knobValueIn(e,t)??a.default},withDefaults=e=>Object.fromEntries(effectKnobIds.map(t=>[t,effectValueOf(e,t)])),bloomBlur_default="uniform float uSigma;\nuniform vec2 uTexel;\nuniform vec2 uDirection;\nconst int BLUR_TAPS = 12;\nvec3 blurAlong(vec2 uv) {\nfloat spacing = max(3.0 * uSigma / float(BLUR_TAPS), 1.0);\nfloat level = log2(spacing);\nvec3 total = vec3(0.0);\nfloat weights = 0.0;\nfor (int index = -BLUR_TAPS; index <= BLUR_TAPS; index++) {\nfloat offset = float(index) * spacing;\nfloat weight = exp(-0.5 * offset * offset / (uSigma * uSigma));\nvec2 at = uv + uDirection * offset * uTexel;\nfloat inside = float(all(greaterThanEqual(at, vec2(0.0))) && all(lessThanEqual(at, vec2(1.0))));\ntotal += textureLod(uSource, at, level).rgb * weight * inside;\nweights += weight;\n}\nreturn total / weights;\n}\n",shared_default="float luma(vec3 colour) {\nreturn dot(colour, vec3(0.2126, 0.7152, 0.0722));\n}\nvec4 screenLight(vec4 picture, vec3 light, float intensity, float saturation) {\nlight = clamp(mix(vec3(luma(light)), light, saturation), 0.0, 1.0);\nreturn vec4(picture.rgb + intensity * light * (1.0 - picture.rgb), picture.a);\n}\nfloat hash(vec2 at) {\nvec3 spread = fract(vec3(at.xyx) * 0.1031);\nspread += dot(spread, spread.yzx + 33.33);\nreturn fract((spread.x + spread.y) * spread.z);\n}\n",BLOOM_KEY_SHADER="uniform highp usampler3D uLookup;\nuniform float uBrightness;\nuniform float uContrast;\nvoid main() {\nvec4 colour = texture(uSource, vUv);\nivec3 cell = ivec3(colour.bgr * 255.0 + 0.5) >> 3;\nfloat lit = float(texelFetch(uLookup, cell, 0).r);\nvec3 cut = clamp((colour.rgb * lit * uBrightness - 0.5) * uContrast + 0.5, 0.0, 1.0);\nfragColor = vec4(cut, 1.0);\n}\n",BLOOM_BLUR_SHADER=/* @__PURE__ */(()=>`${bloomBlur_default}void main() {\nfragColor = vec4(blurAlong(vUv), 1.0);\n}\n`)(),BLOOM_COMPOSITE_SHADER=/* @__PURE__ */(()=>`${shared_default}${bloomBlur_default}uniform sampler2D uKept;\nuniform float uIntensity;\nuniform float uSaturation;\nvoid main() {\nfragColor = screenLight(texture(uKept, vUv), blurAlong(vUv), uIntensity, uSaturation);\n}\n`)(),DEFOCUS_SHADER=/* @__PURE__ */(()=>`${shared_default}uniform float uDepth;\nuniform float uFocus;\nuniform float uBand;\nuniform float uBlur;\nuniform float uBokeh;\nuniform vec2 uTexel;\nconst int MAX_TAPS = 48;\nconst float GOLDEN_ANGLE = 2.39996323;\nfloat depthAt(float v) {\nreturn uDepth * v;\n}\nfloat circleOfConfusion(float v) {\nfloat defocus = abs(depthAt(v) - uFocus) - uBand;\nreturn uBlur * clamp(defocus / max(1.0 - uBand, 0.001), 0.0, 1.0);\n}\nfloat bokehWeight(vec3 colour) {\nfloat bright = luma(colour);\nfloat fourth = bright * bright * bright * bright;\nreturn 1.0 + uBokeh * 8.0 * fourth;\n}\nvec4 defocus(vec2 uv, float coc) {\nfloat radius = coc * 0.5;\nif (radius < 0.75) return textureLod(uSource, uv, 0.0);\nint taps = int(clamp(radius * 1.5, 12.0, float(MAX_TAPS)));\nfloat spacing = radius * sqrt(3.14159265 / float(taps));\nfloat level = max(log2(spacing), 0.0);\nfloat spin = hash(gl_FragCoord.xy) * 6.28318531;\nvec4 centre = textureLod(uSource, uv, level);\nfloat weight = bokehWeight(centre.rgb);\nvec4 total = vec4(centre.rgb * centre.a, centre.a) * weight;\nfloat carried = weight;\nfor (int index = 0; index < MAX_TAPS; index++) {\nif (index >= taps) break;\nfloat fraction = (float(index) + 0.5) / float(taps);\nfloat distance = radius * sqrt(fraction);\nfloat angle = float(index) * GOLDEN_ANGLE + spin;\nvec2 offset = vec2(cos(angle), sin(angle)) * distance;\nvec4 sampled = textureLod(uSource, uv + offset * uTexel, level);\nfloat tapWeight = bokehWeight(sampled.rgb);\ntotal += vec4(sampled.rgb * sampled.a, sampled.a) * tapWeight;\ncarried += tapWeight;\n}\nreturn vec4(total.rgb / max(total.a, 0.0001), total.a / carried);\n}\nvoid main() {\nfragColor = defocus(vUv, circleOfConfusion(vUv.y));\n}\n`)(),GLARE_SHADER=/* @__PURE__ */(()=>`${shared_default}uniform sampler2D uKept;\nuniform float uIntensity;\nuniform float uLength;\nuniform float uAxes;\nuniform float uAngle;\nuniform float uGain;\nuniform float uSaturation;\nuniform vec2 uTexel;\nconst int MAX_AXES = 4;\nconst int RAY_TAPS = 16;\nconst float PI = 3.14159265;\nfloat inFrame(vec2 at) {\nvec2 inside = step(vec2(0.0), at) * step(at, vec2(1.0));\nreturn inside.x * inside.y;\n}\nvec3 streakAlong(vec2 uv, vec2 direction, float jitter) {\nfloat spacing = max(uLength / float(RAY_TAPS), 1.0);\nfloat level = max(log2(spacing), 0.0);\nvec3 total = vec3(0.0);\nfor (int index = 0; index < RAY_TAPS; index++) {\nfloat distance = (float(index) + jitter) * spacing;\nfloat remaining = max(1.0 - distance / uLength, 0.0);\nvec2 offset = direction * distance * uTexel;\nvec3 both = textureLod(uSource, uv + offset, level).rgb * inFrame(uv + offset) +\ntextureLod(uSource, uv - offset, level).rgb * inFrame(uv - offset);\ntotal += both * remaining * remaining;\n}\nreturn total * spacing;\n}\nvoid main() {\nfloat jitter = 0.5 + 0.5 * hash(gl_FragCoord.xy);\nvec3 glare = vec3(0.0);\nfor (int axis = 0; axis < MAX_AXES; axis++) {\nif (float(axis) >= uAxes) break;\nfloat angle = uAngle + float(axis) * PI / uAxes;\nglare += streakAlong(vUv, vec2(cos(angle), sin(angle)), jitter);\n}\nfragColor = screenLight(texture(uKept, vUv), clamp(glare * uGain, 0.0, 1.0), uIntensity, uSaturation);\n}\n`)(),setSampling=(e,t=e.CLAMP_TO_EDGE)=>{e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_S,t),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_T,t),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MAG_FILTER,e.LINEAR),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,e.LINEAR)},hasPixels=e=>"naturalWidth"in e?e.complete&&e.naturalWidth>0:"readyState"in e?e.readyState>=e.HAVE_CURRENT_DATA:"displayWidth"in e?e.displayWidth>0:e.width>0,resizeCanvas=(e,t,a)=>{e.width===t&&e.height===a||(e.width=t,e.height=a)},compile=(e,t,a)=>{const r=e.createShader(t);if(!r)return"the context would not create a shader";if(e.shaderSource(r,a),e.compileShader(r),e.getShaderParameter(r,e.COMPILE_STATUS))return r;const s=e.getShaderInfoLog(r)??"";return e.deleteShader(r),`failed to compile: ${s}`},ScreenPassRenderer=class{canvas;context;programs=/* @__PURE__ */new Map;failures=/* @__PURE__ */new Set;sourceTexture;stageTextures=/* @__PURE__ */new Map;lookupTextures=[];targets=[];targetSize={width:0,height:0};lost=!1;constructor(){this.canvas=document.createElement("canvas"),this.canvas.addEventListener("webglcontextlost",e=>{e.preventDefault(),this.lost=!0}),this.canvas.addEventListener("webglcontextrestored",()=>{this.programs.clear(),this.failures.clear(),this.sourceTexture=void 0,this.stageTextures.clear(),this.lookupTextures=[],this.targets=[],this.targetSize={width:0,height:0},this.lost=!1})}get gl(){return void 0===this.context&&(this.context=this.canvas.getContext("webgl2",{alpha:!0,antialias:!1,premultipliedAlpha:!1})),this.context??void 0}render(e,t){if(!t.length||!e.width||!e.height)return;const a=this.gl;if(!a||this.lost)return;resizeCanvas(this.canvas,e.width,e.height);let r=this.uploadSource(e,t[0].mipmapped);const s=t.slice(1).some(e=>e.keepsInput)?3:2;if(!r||t.length>1&&!this.holdTargets(a,s))return;let n;for(let i=0;i<t.length;i++){const e=t[i];e.keepsInput&&(n=r);const s=i===t.length-1?void 0:this.targets.find(({texture:e})=>e!==r&&e!==n);this.runStage(a,e,r,s,n),s&&(r=s.texture,t[i+1].mipmapped&&this.mipmap(a,r))}return this.canvas}get hasContext(){return void 0!==this.gl}compileError(e){const t=this.gl;if(!t||this.lost)return;const a=this.cachedProgram(t,e);return"string"==typeof a?a:void 0}release(e){const t=this.programs.get(e);t&&this.context?.deleteProgram(t),this.programs.delete(e),this.failures.delete(e)}releaseTextures(e){for(const t of e){const e=this.stageTextures.get(t);e&&this.context?.deleteTexture(e),this.stageTextures.delete(t)}}remove(){this.context?.getExtension("WEBGL_lose_context")?.loseContext(),this.context=null,this.stageTextures.clear()}holdTargets(e,t){const{width:a,height:r}=this.canvas,s=this.targetSize.width!==a||this.targetSize.height!==r;for(this.releaseTargets(e,s?0:t);this.targets.length<t;){const t=e.createTexture(),s=e.createFramebuffer();if(!t||!s)return!1;e.bindTexture(e.TEXTURE_2D,t),e.texImage2D(e.TEXTURE_2D,0,e.RGBA,a,r,0,e.RGBA,e.UNSIGNED_BYTE,null),setSampling(e),e.bindFramebuffer(e.FRAMEBUFFER,s),e.framebufferTexture2D(e.FRAMEBUFFER,e.COLOR_ATTACHMENT0,e.TEXTURE_2D,t,0),this.targets.push({texture:t,framebuffer:s})}return this.targetSize={width:a,height:r},!0}releaseTargets(e,t=0){for(const{texture:a,framebuffer:r}of this.targets.splice(t))e.deleteTexture(a),e.deleteFramebuffer(r)}bindStageTextures(e,t,a){Object.entries(a.textures??{}).forEach(([a,r],s)=>{const n=s+1;e.activeTexture(e.TEXTURE0+n);const i=this.stageTextures.get(r),o=i??e.createTexture();o&&(e.bindTexture(e.TEXTURE_2D,o),i&&!(e=>"readyState"in e||"getContext"in e)(r)||(e.pixelStorei(e.UNPACK_FLIP_Y_WEBGL,!0),e.texImage2D(e.TEXTURE_2D,0,e.RGBA,e.RGBA,e.UNSIGNED_BYTE,r)),i||(setSampling(e,e.REPEAT),this.stageTextures.set(r,o)),e.uniform1i(e.getUniformLocation(t,a),n))})}mipmap(e,t){e.bindTexture(e.TEXTURE_2D,t),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,e.LINEAR_MIPMAP_LINEAR),e.generateMipmap(e.TEXTURE_2D)}uploadSource(e,t=!1){const a=this.gl;if(a&&(this.sourceTexture??=a.createTexture()??void 0,this.sourceTexture))return a.bindTexture(a.TEXTURE_2D,this.sourceTexture),a.pixelStorei(a.UNPACK_FLIP_Y_WEBGL,!0),a.texImage2D(a.TEXTURE_2D,0,a.RGBA,a.RGBA,a.UNSIGNED_BYTE,e),setSampling(a),t&&this.mipmap(a,this.sourceTexture),this.sourceTexture}lookupTextureOf(e,t){const a=this.lookupTextures.find(e=>e.cube===t);if(a)return this.lookupTextures=[a,...this.lookupTextures.filter(e=>e!==a)],a.texture;const r=(this.lookupTextures.length>=2?this.lookupTextures.pop():void 0)?.texture??e.createTexture();if(!r)return;e.bindTexture(e.TEXTURE_3D,r),e.pixelStorei(e.UNPACK_FLIP_Y_WEBGL,!1),e.pixelStorei(e.UNPACK_ALIGNMENT,1);const{side:s,texels:n}=t;return e.texImage3D(e.TEXTURE_3D,0,e.R8UI,s,s,s,0,e.RED_INTEGER,e.UNSIGNED_BYTE,n),e.texParameteri(e.TEXTURE_3D,e.TEXTURE_MIN_FILTER,e.NEAREST),e.texParameteri(e.TEXTURE_3D,e.TEXTURE_MAG_FILTER,e.NEAREST),this.lookupTextures.unshift({cube:t,texture:r}),r}runStage(e,t,a,r,s){const n=this.programOf(e,t.fragment);if(!n)return;const i=t.lookup&&this.lookupTextureOf(e,t.lookup);if(!t.lookup||i){e.bindFramebuffer(e.FRAMEBUFFER,r?.framebuffer??null),e.viewport(0,0,this.canvas.width,this.canvas.height),e.useProgram(n),i&&(e.activeTexture(e.TEXTURE0+2),e.bindTexture(e.TEXTURE_3D,i),e.uniform1i(e.getUniformLocation(n,"uLookup"),2)),s&&(e.activeTexture(e.TEXTURE0+1),e.bindTexture(e.TEXTURE_2D,s),e.uniform1i(e.getUniformLocation(n,"uKept"),1)),this.bindStageTextures(e,n,t),e.activeTexture(e.TEXTURE0+0),e.bindTexture(e.TEXTURE_2D,a),e.uniform1i(e.getUniformLocation(n,"uSource"),0);for(const[a,r]of Object.entries(t.uniforms)){const t=e.getUniformLocation(n,a);t&&("number"==typeof r?e.uniform1f(t,r):2===r.length?e.uniform2f(t,r[0],r[1]):3===r.length?e.uniform3f(t,r[0],r[1],r[2]):e.uniform4f(t,r[0],r[1],r[2],r[3]))}e.drawArrays(e.TRIANGLES,0,3)}}programOf(e,t){if(this.failures.has(t))return;const a=this.cachedProgram(e,t);if("string"!=typeof a)return a;this.failures.add(t),console.warn(`[MapEffects] shader ${a}`)}cachedProgram(e,t){const a=this.programs.get(t);if(a)return a;const r=this.build(e,t);return"string"!=typeof r&&this.programs.set(t,r),r}build(e,t){const a=[compile(e,e.VERTEX_SHADER,"#version 300 es\nout vec2 vUv;\nvoid main() {\nvec2 corner = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);\nvUv = corner;\ngl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0);\n}\n"),compile(e,e.FRAGMENT_SHADER,`#version 300 es\nprecision highp float;\nprecision highp sampler2D;\nuniform sampler2D uSource;\nin vec2 vUv;\nout vec4 fragColor;\n${t}`)],r=a.find(e=>"string"==typeof e),s=a.filter(e=>"string"!=typeof e),n=r?null:e.createProgram();if(n){for(const t of s)e.attachShader(n,t);e.linkProgram(n)}for(const o of s)e.deleteShader(o);if(r)return r;if(!n)return"the context would not create a program";if(e.getProgramParameter(n,e.LINK_STATUS))return n;const i=e.getProgramInfoLog(n)??"";return e.deleteProgram(n),`failed to link: ${i}`}},PASS_PLACEMENTS={"before-depth-of-field":!0,"after-depth-of-field":!0},layerPlacementOf=e=>`after-${e}`,backdropFilterStyle=e=>({backdropFilter:e,webkitBackdropFilter:e}),maskStyle=e=>({maskImage:e,webkitMaskImage:e}),LAYER_STYLE={position:"absolute",inset:"0",width:"100%",height:"100%",pointerEvents:"none",display:"none"},rasterScaleOf=e=>e.width/Math.max(e.clientWidth,1),MapEffects=class{map;config={};passes=[];layers=[];container;overlays;bloomCanvas;artworkCanvas;chain=new ScreenPassRenderer;scopeMasks={};styleGeneration=0;warnedScopes=/* @__PURE__ */new Set;warnedNoWebGL2=!1;warnedFrame=!1;warnedThrows=/* @__PURE__ */new WeakSet;passSources=/* @__PURE__ */new Map;onStyleData=()=>{this.styleGeneration++,this.map.mapLibreMap.triggerRepaint(),this.showFog(withDefaults(this.config))};onRender=()=>{try{this.drawChain(withDefaults(this.config)),"block"===this.bloomCanvas.style.display&&this.drawBloom(),this.layers.length&&this.drawLayers()}catch(e){if(this.artworkCanvas.style.display="none",this.warnedFrame)return;this.warnedFrame=!0,console.warn("[MapEffects] the effects are skipped for each frame they fail to draw:",e)}};constructor(e,t={}){this.map=e,this.container=document.createElement("div"),this.container.className="tomtom-map-effects",Object.assign(this.container.style,{position:"absolute",inset:"0",pointerEvents:"none",overflow:"hidden"}),this.map.mapLibreMap.getCanvas().insertAdjacentElement("afterend",this.container),this.bloomCanvas=document.createElement("canvas"),this.overlays=Object.fromEntries(OVERLAYS.map(e=>{const t="bloom"===e?this.bloomCanvas:document.createElement("div");return t.dataset.effect=e,Object.assign(t.style,LAYER_STYLE),this.container.appendChild(t),[e,t]})),this.bloomCanvas.style.mixBlendMode="screen",this.artworkCanvas=this.chain.canvas,this.artworkCanvas.dataset.effect="artwork",Object.assign(this.artworkCanvas.style,LAYER_STYLE),this.container.insertAdjacentElement("afterbegin",this.artworkCanvas),this.map.mapLibreMap.on("render",this.onRender),this.map.mapLibreMap.on("styledata",this.onStyleData),this.applyConfig(t)}cameraAngles(){const e=this.map.mapLibreMap;return{pitch:e.getPitch(),verticalFieldOfView:e.getVerticalFieldOfView()}}getConfig(){return structuredClone(this.config)}applyConfig(e){this.config=e,this.render()}updateConfig(e){this.applyConfig({...this.config,...e})}resetConfig(){this.applyConfig({})}_knobValue(e){return knobValueIn(this.config,e)}_setKnobValues(e){this.applyConfig(configWithKnobs(this.config,e))}_resetKnobValue(e){this.applyConfig(configWithoutKnob(this.config,e))}addPass(e){if(!e.id)throw new Error("A pass needs an id, to be replaced or removed by.");if(void 0!==e.placement&&!Object.hasOwn(PASS_PLACEMENTS,e.placement))throw new RangeError(`Pass '${e.id}' expects a placement of ${Object.keys(PASS_PLACEMENTS).join(" or ")}; got ${String(e.placement)}.`);const t=Object.keys(e.textures??{});if(t.length>15||t.includes("uSource"))throw new RangeError(`Pass '${e.id}' takes up to 15 textures, none named uSource; got ${t.join(", ")}.`);this.chain.hasContext||this.warnNoWebGL2();const a=this.chain.compileError(e.fragment);if(a)throw new Error(`Pass '${e.id}' ${a}`);const r={...e},s=this.passes.find(t=>t.id===e.id);this.passes=s?this.passes.map(e=>e===s?r:e):[...this.passes,r],s&&this.releaseUnused(s),this.render()}removePass(e){const t=this.passes.find(t=>t.id===e);t&&(this.passes=this.passes.filter(e=>e!==t),this.releaseUnused(t),this.render())}addLayer(e){if(!e.id)throw new Error("A layer needs an id, to be replaced or removed by.");if(t=e.placement,!OVERLAYS.some(e=>t===layerPlacementOf(e)))throw new RangeError(`Layer '${e.id}' expects a placement of ${OVERLAYS.map(layerPlacementOf).join(", ")}; got ${String(e.placement)}.`);var t;const a=this.layers.find(t=>t.layer.id===e.id),r={layer:{...e},canvas:a?.canvas??this.layerCanvas(e.id)};this.layers=a?this.layers.map(e=>e===a?r:e):[...this.layers,r],this.mountLayers(),this.paintLayer(r.layer,r.canvas,this.rasterOf())}removeLayer(e){const t=this.layers.find(({layer:t})=>t.id===e);t&&(this.layers=this.layers.filter(e=>e!==t),t.canvas.remove())}async capture(e={}){const t=this.map.mapLibreMap,a=t,r=t.getPixelRatio(),s=e.pixelRatio??r,n=r!==(globalThis.devicePixelRatio||1),i=Math.abs(s-r)>.01;i&&(a.setPixelRatio(s),await this.settled());try{return await this.readBack()}finally{i&&a.setPixelRatio(n?r:null)}}remove(){this.map.mapLibreMap.off("render",this.onRender),this.map.mapLibreMap.off("styledata",this.onStyleData),this.chain.remove(),this.passSources.clear(),this.container.remove()}show(e,t,a={}){const r=this.overlays[e];r.style.display=t?"block":"none",Object.assign(r.style,a)}render(){const e=withDefaults(this.config),t=gradeFilter(e["grade.brightness"],e["grade.contrast"],e["grade.saturation"]);var a;this.show("grade",""!==t,backdropFilterStyle(t)),this.showFog(e),this.show("edgeBlur",e["edgeBlur.intensity"]>0,{...backdropFilterStyle((a=e["edgeBlur.intensity"],`blur(${a}px) saturate(1.08)`)),...maskStyle(rimMask(e["edgeBlur.reach"]))}),e["glare.intensity"]>0&&!this.chain.hasContext&&this.warnNoWebGL2(),this.show("tint",e["tint.opacity"]>0,{backgroundColor:e["tint.color"],opacity:String(e["tint.opacity"])}),this.show("vignette",0!==e["vignette.intensity"],{backgroundImage:vignetteGradient(e["vignette.intensity"],e["vignette.reach"])}),("block"===this.artworkCanvas.style.display||"block"===this.bloomCanvas.style.display||this.readsFrame(e))&&this.map.mapLibreMap.triggerRepaint()}readsFrame(e){return this.passes.length>0||e["bloom.intensity"]>0||e["glare.intensity"]>0||e["depthOfField.intensity"]>0}bloomInChain(e){return e["bloom.intensity"]>0&&void 0!==this.currentScopeMask("bloom")}routeBloom(e){const t=e["bloom.intensity"]>0,a=this.bloomInChain(e);this.show("bloom",t&&!a,{opacity:String(e["bloom.intensity"])}),a&&!this.chain.hasContext&&this.warnNoWebGL2()}showFog(e){const t=fogAir(e["fog.color"],this.map.styleLightDarkTheme);this.show("fog",e["fog.intensity"]>0,{...backdropFilterStyle(fogFilter(e["fog.intensity"],t)),...maskStyle(rimMask(e["fog.reach"])),backgroundImage:fogVeil(e["fog.intensity"],e["fog.reach"],t)})}chainStages(e){return[...this.passStages("before-depth-of-field"),...this.depthOfFieldStages(e),...this.passStages("after-depth-of-field"),...this.glareStages(e),...this.bloomStages(e)]}glareStages(e){if(e["glare.intensity"]<=0)return[];const t=((e,t)=>({...lightUniforms(e,t),uLength:Math.max(e.lengthCssPx*t.scale,1),uAxes:clamp(Math.ceil(e.rays/2),1,4),uAngle:toRadians(e.angleDegrees),uGain:1/(4*t.scale)}))({intensity:e["glare.intensity"],lengthCssPx:e["glare.length"],rays:e["glare.rays"],angleDegrees:e["glare.angle"],threshold:e["glare.threshold"]},this.rasterOf()),a=this.currentScopeMask("glare")??(everyColor??={texels:new Uint8Array(32768).fill(1),side:32},everyColor);return[{fragment:BLOOM_KEY_SHADER,uniforms:t,keepsInput:!0,lookup:a},{fragment:GLARE_SHADER,uniforms:t,mipmapped:!0}]}bloomStages(e){const t=e["bloom.intensity"]>0?this.currentScopeMask("bloom"):void 0;if(!t)return[];const a=((e,t)=>({...lightUniforms(e,t),uSigma:Math.max(e.radiusCssPx*t.scale,1)}))({intensity:e["bloom.intensity"],radiusCssPx:e["bloom.radius"],threshold:e["bloom.threshold"]},this.rasterOf());return[{fragment:BLOOM_KEY_SHADER,uniforms:a,keepsInput:!0,lookup:t},{fragment:BLOOM_BLUR_SHADER,uniforms:{...a,uDirection:[1,0]},mipmapped:!0},{fragment:BLOOM_COMPOSITE_SHADER,uniforms:{...a,uDirection:[0,1]},mipmapped:!0}]}rasterOf(){const e=this.map.mapLibreMap.getCanvas();return{width:e.width,height:e.height,scale:this.map.mapLibreMap.getPixelRatio()}}passStages(e){return this.passes.filter(t=>(t.placement??"before-depth-of-field")===e).flatMap(e=>{const t=this.guarded(e,`pass '${e.id}' is skipped while its uniforms or textures throw`,()=>this.inputsOf(e));return t?[{fragment:e.fragment,mipmapped:e.mipmapped,...t}]:[]})}inputsOf(e){const t={};for(const[r,s]of Object.entries(e.textures??{})){const a="function"==typeof s?s():s;if(!a||!hasPixels(a))return;this.holdSource(e,r,a),t[r]=a}const{uniforms:a={}}=e;return{uniforms:"function"==typeof a?a():a,textures:t}}guarded(e,t,a){try{return a()}catch(r){return void(this.warnedThrows.has(e)||(this.warnedThrows.add(e),console.warn(`[MapEffects] ${t}:`,r)))}}layerCanvas(e){const t=document.createElement("canvas");return t.dataset.layer=e,Object.assign(t.style,LAYER_STYLE,{display:"block"}),t}mountLayers(){for(const e of OVERLAYS){const t=layerPlacementOf(e),a=this.layers.filter(({layer:e})=>e.placement===t);this.overlays[e].after(...a.map(({canvas:e})=>e))}}drawLayers(){const e=this.rasterOf();for(const{layer:t,canvas:a}of this.layers)this.paintLayer(t,a,e)}paintLayer(e,t,{width:a,height:r,scale:s}){resizeCanvas(t,a,r);const n=t.getContext("2d");n&&(n.clearRect(0,0,a,r),n.save(),this.guarded(e,`layer '${e.id}' is left empty while its draw() throws`,()=>e.draw(n,{width:a,height:r,pixelRatio:s})),n.restore())}captureLayersAfter(e,t,a){const r=layerPlacementOf(e);for(const{layer:s}of this.layers.filter(e=>e.layer.placement===r)){const e=document.createElement("canvas");this.paintLayer(s,e,a),t.drawImage(e,0,0)}}releaseUnused(e){this.passes.some(t=>t.fragment===e.fragment)||this.chain.release(e.fragment);const t=this.passSources.get(e);this.passSources.delete(e),t&&this.releaseUnsampled([...t.values()])}releaseUnsampled(e){const t=new Set([...this.passSources.values()].flatMap(e=>[...e.values()]));this.chain.releaseTextures(e.filter(e=>!t.has(e)))}holdSource(e,t,a){const r=this.passSources.get(e)??/* @__PURE__ */new Map;this.passSources.set(e,r);const s=r.get(t);r.set(t,a),s&&s!==a&&this.releaseUnsampled([s])}depthOfFieldStages(e){const t=this.cameraAngles();if(e["depthOfField.intensity"]<=0||depthRamp(t)<=0)return[];const a=this.map.mapLibreMap.getCanvas(),r=((e,t,a)=>{const r=depthRamp(t);if(!(r<=0||e.blurCssPx<=0))return{uDepth:r,uFocus:Math.min(asFraction(e.focus)*r,1),uBand:.5*asFraction(e.band),uBlur:Math.max(e.blurCssPx,0)*a.scale,uBokeh:asFraction(e.bokeh),uTexel:texelOf(a)}})({focus:e["depthOfField.focus"],band:e["depthOfField.band"],blurCssPx:e["depthOfField.intensity"],bokeh:e["depthOfField.bokeh"]},t,{width:a.width,height:a.height,scale:rasterScaleOf(a)});return r?[{fragment:DEFOCUS_SHADER,uniforms:r,mipmapped:!0}]:[]}drawChain(e){const t=this.chainStages(e),a=t.length?this.chain.render(this.map.mapLibreMap.getCanvas(),t):void 0;this.artworkCanvas.style.display=a?"block":"none",this.routeBloom(e)}drawBloom(){const e="block"===this.artworkCanvas.style.display?this.artworkCanvas:this.map.mapLibreMap.getCanvas(),t=Math.max(1,Math.round(.5*e.width)),a=Math.max(1,Math.round(.5*e.height));resizeCanvas(this.bloomCanvas,t,a);const r=this.bloomCanvas.getContext("2d");r&&(r.filter=bloomFilter(effectValueOf(this.config,"bloom.radius"),effectValueOf(this.config,"bloom.threshold"),.5*this.rasterOf().scale),r.clearRect(0,0,t,a),r.drawImage(e,0,0,t,a))}currentScopeMask(e){const t=effectValueOf(this.config,`${e}.only`);if(0===t.length)return;const a=t.join("\n"),r=effectValueOf(this.config,`${e}.onlyTolerance`),s=this.scopeMasks[e],n=s?.key===a&&s.tolerance===r,i=(e=>e.some(e=>NAMED_KNOBS.has(e)))(t);if(i&&!this.map.mapReady)return n?s.mask:void 0;if(n&&(!i||s.styleGeneration===this.styleGeneration))return s.mask;const o=this.buildScopeMaskFor(e,t,r);return this.scopeMasks[e]={key:a,tolerance:r,styleGeneration:this.styleGeneration,mask:o},o}buildScopeMaskFor(e,t,a){const r=((e,t)=>t.flatMap(t=>{const a=NAMED_KNOBS.get(t);return(a?a.map(t=>getStyleColor(e,t)):[t]).map(e=>"string"==typeof e?parseCssColor(e):void 0).filter(e=>void 0!==e)}))(this.map,t);return 0===r.length&&this.warnScopeEmpty(e,t),((e,t)=>{const a=e.map(chromaticity).filter(e=>void 0!==e);if(0===a.length)return;const r=t*t,s=new Uint8Array(32768);for(let n=0;n<256;n+=8)for(let e=0;e<256;e+=8)for(let t=0;t<256;t+=8){const i=chromaticity([n,e,t]);i&&a.some(e=>{const t=i[0]-e[0],a=i[1]-e[1],s=i[2]-e[2];return t*t+a*a+s*s<=r})&&(s[cubeIndex(n,e,t)]=1)}return{texels:s,side:32}})(r,a)}warnScopeEmpty(e,t){this.warnedScopes.has(e)||(this.warnedScopes.add(e),console.warn(`[MapEffects] '${e}.only: ${t.join(", ")}' found no colour to key on in the loaded style, so ${e} is lighting the whole frame.`))}readBack(){const e=this.map.mapLibreMap;return new Promise((t,a)=>{e.once("render",()=>{try{t(this.compositeFrame(e.getCanvas()))}catch(r){a(r)}}),e.triggerRepaint()})}compositeFrame(e){const t=withDefaults(this.config),a=document.createElement("canvas");a.width=e.width,a.height=e.height;const r=a.getContext("2d");if(!r)return a;const s=this.chainStages(t);r.drawImage(this.chain.render(e,s)??e,0,0);const n={width:e.width,height:e.height,scale:rasterScaleOf(e)};return((e,t,a,r,s,n,i,o=()=>{})=>{const l=e=>r=>fillEllipticalRadial(r,t,a,e,"rgba(0,0,0,0)","rgba(0,0,0,1)"),h={bloom:()=>{s["bloom.intensity"]>0&&!i&&((e,t,a,r,s)=>{const n=filteredCopy(e,t,a,bloomFilter(s["bloom.radius"],s["bloom.threshold"],r));n&&drawOver(e,n,"screen",s["bloom.intensity"])})(e,t,a,r,s)},grade:()=>{const r=gradeFilter(s["grade.brightness"],s["grade.contrast"],s["grade.saturation"]);r&&((e,t,a,r)=>{const s=filteredCopy(e,t,a,r);s&&drawOver(e,s,"copy")})(e,t,a,r)},fog:()=>{if(s["fog.intensity"]<=0)return;const i=s["fog.intensity"],o=s["fog.reach"],h=fogAir(s["fog.color"],n),{inner:c,outer:u}=fogVeilStops(i,h),d=fogBlurPx(i);applyMaskedBlur(e,t,a,r,d,h.grade,l(o)),fillEllipticalRadial(e,t,a,o,c,u)},edgeBlur:()=>{if(s["edgeBlur.intensity"]<=0)return;const n=s["edgeBlur.intensity"];applyMaskedBlur(e,t,a,r,n,"saturate(1.08)",l(s["edgeBlur.reach"]))},tint:()=>{s["tint.opacity"]<=0||(e.save(),e.globalAlpha=s["tint.opacity"],e.fillStyle=s["tint.color"],e.fillRect(0,0,t,a),e.restore())},vignette:()=>{if(0===s["vignette.intensity"])return;const{inner:r,outer:n}=vignetteStops(s["vignette.intensity"]);fillEllipticalRadial(e,t,a,s["vignette.reach"],r,n)}};for(const c of OVERLAYS)h[c](),o(c)})(r,n.width,n.height,n.scale,t,this.map.styleLightDarkTheme,this.bloomInChain(t),e=>this.captureLayersAfter(e,r,n)),a}settled(e=15e3){const t=this.map.mapLibreMap;return new Promise(a=>{const r=()=>{clearTimeout(s),t.off("idle",r),a()},s=setTimeout(r,e);t.once("idle",r)})}warnNoWebGL2(){this.warnedNoWebGL2||(this.warnedNoWebGL2=!0,console.warn("[MapEffects] this browser gives no WebGL2 context, so passes from addPass neither compile nor run, the glare draws no streaks, and a bloom.only scope draws no glow."))}};export{MapEffects,effectKnobCatalogue,effectKnobIds};
|
package/dist/third-party.json
CHANGED
package/package.json
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tomtom-org/maps-sdk-plugin-map-effects",
|
|
3
3
|
"author": "TomTom International B.V.",
|
|
4
|
-
"license": "
|
|
5
|
-
"version": "0.
|
|
4
|
+
"license": "SEE LICENSE IN LICENSE.txt",
|
|
5
|
+
"version": "0.4.1",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"description": "Practical post-processing for a TomTomMap: bloom for emphasis, tint/grade/vignette to mute the base map under data, fog and edge blur for focus, a WebGL depth of field with bokeh for a tilted 3D view — plus high-DPI capture",
|
|
7
|
+
"description": "Practical post-processing for a TomTomMap: bloom for emphasis, glare that makes lights cast streaks, tint/grade/vignette to mute the base map under data, fog and edge blur for focus, a WebGL depth of field with bokeh for a tilted 3D view — plus high-DPI capture",
|
|
8
8
|
"types": "dist/index.d.ts",
|
|
9
9
|
"sideEffects": false,
|
|
10
10
|
"peerDependencies": {
|
|
11
|
-
"@tomtom-org/maps-sdk": "
|
|
12
|
-
"maplibre-gl": "^6.
|
|
11
|
+
"@tomtom-org/maps-sdk": "^1.0.0-rc.2",
|
|
12
|
+
"maplibre-gl": "^6.15.0"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"@playwright/test": "1.60.0",
|
|
16
16
|
"@tsconfig/recommended": "^1.0.13",
|
|
17
|
-
"@types/node": "^26.6.
|
|
18
|
-
"@vitest/coverage-v8": "^5.0.
|
|
19
|
-
"jsdom": "^30.1.
|
|
20
|
-
"rolldown": "^1.2.
|
|
17
|
+
"@types/node": "^26.6.5",
|
|
18
|
+
"@vitest/coverage-v8": "^5.0.5",
|
|
19
|
+
"jsdom": "^30.1.3",
|
|
20
|
+
"rolldown": "^1.2.14",
|
|
21
21
|
"typescript": "^6.0.3",
|
|
22
|
-
"vite": "8.3.
|
|
23
|
-
"vitest": "^5.0.
|
|
24
|
-
"@tomtom-org/maps-sdk": "1.0.0-rc.
|
|
22
|
+
"vite": "^8.3.4",
|
|
23
|
+
"vitest": "^5.0.5",
|
|
24
|
+
"@tomtom-org/maps-sdk": "1.0.0-rc.5"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
27
|
"dist",
|