@tomtom-org/maps-sdk-plugin-map-effects 0.0.3 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,10 +14,13 @@ Every effect is named by the problem it solves, and off by default:
14
14
  | Depth of field | `depthOfField.intensity`, `depthOfField.focus`, `depthOfField.band`, `depthOfField.bokeh` | Depth on a tilted map — a GPU pass whose blur grows with distance, with highlights returning as discs; a flat map has no depth, and the effect ends there |
15
15
  | Vignette | `vignette.intensity`, `vignette.reach` | Focus on the centre of the view |
16
16
 
17
+ A stylisation of your own — a watercolour, a posterise — runs inside the same GPU chain with `addPass(pass)`, so every effect above then works on your picture.
18
+
17
19
  ## Docs & examples
18
20
 
19
21
  - Developer guide: https://docs.tomtom.com/maps-sdk-js/guides/plugins/map-effects
20
22
  - 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
21
24
 
22
25
  ## Quickstart
23
26
 
@@ -27,7 +30,7 @@ Note: this plugin declares `@tomtom-org/maps-sdk` and `maplibre-gl` as peer depe
27
30
  npm install @tomtom-org/maps-sdk maplibre-gl @tomtom-org/maps-sdk-plugin-map-effects
28
31
  ```
29
32
 
30
- 1. Follow the SDK [Project setup](https://docs.tomtom.com/maps-sdk-js/guides/introduction/project-setup) or the Map [quickstart](https://docs.tomtom.com/maps-sdk-js/guides/map/quickstart) to create and initialize a `TomTomMap`. For bloom, the depth of field and capture, create it with `mapLibre: { canvasContextAttributes: { preserveDrawingBuffer: true } }`.
33
+ 1. Follow the SDK [Project setup](https://docs.tomtom.com/maps-sdk-js/guides/introduction/project-setup) or the Map [quickstart](https://docs.tomtom.com/maps-sdk-js/guides/map/quickstart) to create and initialize a `TomTomMap`. For bloom, the depth of field, your own passes and capture, create it with `mapLibre: { canvasContextAttributes: { preserveDrawingBuffer: true } }`.
31
34
 
32
35
  2. Import and use the plugin:
33
36
 
@@ -44,7 +47,6 @@ effects.set({ 'grade.saturation': 0.4, 'grade.brightness': 0.85, 'tint.opacity':
44
47
  effects.set({ 'bloom.intensity': 0.5, 'bloom.threshold': 0.6 });
45
48
 
46
49
  effects.reset(); // everything off
47
- effects.describe(); // the knob catalogue, same shape as StylingModule.describe()
48
50
  await effects.capture({ pixelRatio: 3 }); // map + effects as a canvas, for print
49
51
  ```
50
52
 
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.9.0
17
+ Version: 6.11.2
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
@@ -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.9.0
7
+ Version: 6.11.2
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,4 @@
1
- import { KnobDescriptor, KnobKind, KnobRange, StylingColorKnobId, TomTomMap } from "@tomtom-org/maps-sdk/map";
1
+ import { KnobKind, KnobRange, StylingColorKnobId, TomTomMap } from "@tomtom-org/maps-sdk/map";
2
2
  //#region src/bloomScope.d.ts
3
3
  type GroupsOf<ID extends string> = ID extends `${infer Head}.${infer Rest}` ? Head | `${Head}.${GroupsOf<Rest>}` : never;
4
4
  /**
@@ -33,8 +33,7 @@ type EffectKnobRange = KnobRange & {
33
33
  step: number;
34
34
  };
35
35
  /**
36
- * One effect knob, drawn from the SDK's shared knob vocabulary — so a UI or an agent built for
37
- * `StylingModule.describe()` reads this catalogue unchanged. Discriminated on `kind`: a range
36
+ * One effect knob, drawn from the SDK's shared knob vocabulary. Discriminated on `kind`: a range
38
37
  * belongs to a numeric knob and the accepted names to a `colors` one.
39
38
  * @ignore
40
39
  */
@@ -250,6 +249,50 @@ export declare const effectKnobIds: EffectKnobId[];
250
249
  type EffectKnobValueOf<ID extends EffectKnobId> = Extract<EffectKnobDefinition, {
251
250
  kind: (typeof effectKnobDefinitions)[ID]['kind'];
252
251
  }>['default'];
252
+ /**
253
+ * One entry of {@link effectKnobCatalogue}: what a UI or a coding agent needs to offer the knob and
254
+ * validate a value for it. The value in force is {@link MapEffects.get}.
255
+ *
256
+ * @group Map Effects
257
+ */
258
+ type EffectKnob = EffectKnobDefinition & {
259
+ /** The knob's id, as accepted by {@link MapEffects.set}. */
260
+ id: EffectKnobId;
261
+ };
262
+ /**
263
+ * Every effect knob with its kind, description, default, range or accepted names and the use case
264
+ * it serves, in compositing order. Static data: no map is needed to read it.
265
+ *
266
+ * @group Map Effects
267
+ */
268
+ export declare const effectKnobCatalogue: readonly EffectKnob[];
269
+ //#endregion
270
+ //#region src/gpu/screenPass.d.ts
271
+ /**
272
+ * The WebGL2 substrate the pixel-derived effects run on: one context, one full-screen triangle, and
273
+ * a pair of render targets a chain of passes ping-pongs between. No effect lives here — a pass
274
+ * brings its own fragment shader and its own uniforms, and this module is what stops two of them
275
+ * being two implementations of context creation, texture upload, resizing and context loss.
276
+ *
277
+ * **One context for the plugin, and that is a requirement rather than a saving.** A browser caps
278
+ * how many live WebGL contexts a page may hold, and the map itself already holds one.
279
+ *
280
+ * The two shaders it owns — the triangle's vertex half and the preamble every fragment stage is
281
+ * composed onto — are real GLSL files under `glsl/`, loaded with Vite's `?raw`, as every shader in
282
+ * this plugin is: a backtick inside a template literal's comment closes the literal, and the
283
+ * program it silently truncates still compiles to a blank picture.
284
+ *
285
+ * @module
286
+ * @ignore
287
+ */
288
+ /**
289
+ * The uniforms a {@link MapEffectsPass} reads, by the name its shader declares: a number sets a
290
+ * `float`, a pair a `vec2`. Only those two types can be set — WebGL silently refuses a float for an
291
+ * `int` or `bool` uniform, which then stays 0.
292
+ *
293
+ * @group Map Effects
294
+ */
295
+ type MapEffectsPassUniforms = Record<string, number | readonly [number, number]>;
253
296
  //#endregion
254
297
  //#region src/MapEffects.d.ts
255
298
  /**
@@ -260,31 +303,35 @@ type EffectKnobValueOf<ID extends EffectKnobId> = Extract<EffectKnobDefinition,
260
303
  */
261
304
  type MapEffectsSettings = { [ID in EffectKnobId]?: EffectKnobValueOf<ID>; };
262
305
  /**
263
- * One entry of the effects catalogue: the SDK's shared {@link KnobDescriptor}, narrowed the way
264
- * `StylingKnobDescriptor` narrows it, plus the use case the effect serves.
306
+ * Where a {@link MapEffectsPass} runs in the GPU chain, relative to the plugin's own depth of field.
307
+ * Either way it runs before every overlay effect, so bloom, grade, fog, edge blur, tint and vignette
308
+ * all read what it drew.
265
309
  *
266
310
  * @group Map Effects
267
311
  */
268
- type MapEffectsKnobDescriptor = KnobDescriptor<EffectKnobValueOf<EffectKnobId>> & {
269
- /** The knob's id, as accepted by {@link MapEffects.set}. */
270
- id: EffectKnobId;
271
- /** The range of valid values, for a numeric knob. */
272
- range?: EffectKnobRange;
273
- /** Always `true`: an effect reads the rendered pixels, so it has something to work on anywhere. */
274
- available: boolean;
275
- /** Always `any-style`, for the same reason. */
276
- appliesTo: string;
277
- /** The practical problem the effect solves. */
278
- useCase: string;
279
- };
312
+ type MapEffectsPassPlacement = 'before-depth-of-field' | 'after-depth-of-field';
280
313
  /**
281
- * The effects catalogue, in the same shape as `StylingModule.describe()` so one control panel or
282
- * agent tool can drive both.
314
+ * A full-screen GPU pass of your own, run inside the plugin's WebGL chain — a stylisation such as a
315
+ * watercolour or a posterise that the rest of the effects should then work on.
283
316
  *
284
317
  * @group Map Effects
285
318
  */
286
- type MapEffectsCatalogue = {
287
- knobs: MapEffectsKnobDescriptor[];
319
+ type MapEffectsPass = {
320
+ /** Names the pass for {@link MapEffects.removePass}; adding a pass under an id in use replaces it in place. */
321
+ id: string;
322
+ /**
323
+ * A GLSL ES 3.00 fragment shader: your own uniforms and `main`. The plugin declares the version,
324
+ * the precisions, `uniform sampler2D uSource` (the picture so far), `in vec2 vUv` and
325
+ * `out vec4 fragColor` — declaring any of them again is a compile error.
326
+ */
327
+ fragment: string;
328
+ /**
329
+ * The uniform values, or a function returning them, called on every draw. A function that throws
330
+ * skips the pass for that draw, with one warning.
331
+ */
332
+ uniforms?: MapEffectsPassUniforms | (() => MapEffectsPassUniforms);
333
+ /** Defaults to `before-depth-of-field`. */
334
+ placement?: MapEffectsPassPlacement;
288
335
  };
289
336
  /**
290
337
  * Options for {@link MapEffects.capture}.
@@ -317,15 +364,16 @@ type CaptureOptions = {
317
364
  * defocuses, worked out from the camera's pitch and field of view, so a flat map ends it;
318
365
  * - **vignette** — focus too, by darkening or lightening the edges.
319
366
  *
320
- * Effects render live as layers over the map canvas (a WebGL2 chain under `backdrop-filter`
321
- * overlays; bloom is a small canvas blended in `screen` mode) and are composited the same way into
322
- * {@link capture}, so what is captured is what was on screen. Bloom, the depth of field and capture
323
- * read the map canvas, which needs
367
+ * Effects render live as layers over the map canvas (a WebGL2 chain, which also runs your own
368
+ * passes from {@link addPass}, under `backdrop-filter` overlays; bloom is a small canvas blended in
369
+ * `screen` mode) and are composited the same way into {@link capture}, so what is captured is what was on screen.
370
+ *
371
+ * Bloom, the depth of field, your passes and capture read the map canvas, which needs
324
372
  * `mapLibre: { canvasContextAttributes: { preserveDrawingBuffer: true } }` when creating the map;
325
373
  * the plugin warns once when it is missing.
326
374
  *
327
- * {@link describe} returns the knobs in the same shape as `StylingModule.describe()`, so a control
328
- * panel or an agent tool built for the SDK's styling catalogue reads this one unchanged.
375
+ * {@link effectKnobCatalogue} lists every knob as static data, the way `stylingKnobCatalogue` lists
376
+ * the SDK's styling knobs.
329
377
  *
330
378
  * @example
331
379
  * ```typescript
@@ -345,6 +393,7 @@ type CaptureOptions = {
345
393
  export declare class MapEffects {
346
394
  private readonly map;
347
395
  private settings;
396
+ private passes;
348
397
  private readonly container;
349
398
  private readonly overlays;
350
399
  private readonly bloomCanvas;
@@ -358,6 +407,8 @@ export declare class MapEffects {
358
407
  private scopeCanvas;
359
408
  private warnedBuffer;
360
409
  private warnedScope;
410
+ private warnedNoWebGL2;
411
+ private readonly warnedUniforms;
361
412
  private readonly onStyleData;
362
413
  private readonly onRender;
363
414
  /**
@@ -398,10 +449,34 @@ export declare class MapEffects {
398
449
  */
399
450
  applyConfig(settings: MapEffectsSettings): void;
400
451
  /**
401
- * The effects catalogue: every knob with kind, range, default, current value and the practical
402
- * use case it serves. Same descriptor shape as `StylingModule.describe()`.
452
+ * Runs a GPU pass of your own in the effects chain, ahead of every overlay effect, and in
453
+ * {@link capture} as on screen. Passes run in the order they were added, within their placement.
454
+ *
455
+ * @remarks
456
+ * A pass redraws with the map. One whose uniforms change on their own — a time-driven animation —
457
+ * asks for frames with `map.mapLibreMap.triggerRepaint()`. A pass that shifts hues runs before
458
+ * bloom's colour key too, so a `bloom.only` scope may need a wider `bloom.onlyTolerance`.
459
+ *
460
+ * @throws `Error` when the pass has no id or its shader does not compile, with the compiler's log;
461
+ * `RangeError` for an unknown placement. A shader is checked only while there is a WebGL2 context:
462
+ * without one no pass runs and the plugin warns once, and after a lost one it is checked on restore.
463
+ *
464
+ * @example
465
+ * ```typescript
466
+ * effects.addPass({
467
+ * id: 'posterise',
468
+ * fragment: `uniform float uLevels;
469
+ * void main() {
470
+ * vec4 colour = texture(uSource, vUv);
471
+ * fragColor = vec4(floor(colour.rgb * uLevels) / uLevels, colour.a);
472
+ * }`,
473
+ * uniforms: { uLevels: 5 },
474
+ * });
475
+ * ```
403
476
  */
404
- describe(): MapEffectsCatalogue;
477
+ addPass(pass: MapEffectsPass): void;
478
+ /** Takes a pass added with {@link addPass} out of the chain. An id with no pass is ignored. */
479
+ removePass(id: string): void;
405
480
  /**
406
481
  * Renders the current view — map and effects — into a canvas, optionally at a higher pixel ratio
407
482
  * for print. The map is re-rendered at that ratio (never resampled), read back once every tile is
@@ -421,11 +496,15 @@ export declare class MapEffects {
421
496
  remove(): void;
422
497
  private render;
423
498
  /**
424
- * The passes the chain runs over the map's pixels, in order. One so far — the defocus — but the
425
- * substrate ping-pongs between render targets, so a second reads what the first left without
426
- * the picture leaving the GPU.
499
+ * The passes the chain runs over the map's pixels, in order: the app's placed before the depth of
500
+ * field, the depth of field, then the app's placed after it. Each reads what the one before left,
501
+ * without the picture leaving the GPU.
427
502
  */
428
503
  private chainStages;
504
+ private passStages;
505
+ private uniformsOf;
506
+ private releaseUnused;
507
+ private depthOfFieldStages;
429
508
  private drawChain;
430
509
  private scheduleBloom;
431
510
  private drawBloom;
@@ -438,8 +517,9 @@ export declare class MapEffects {
438
517
  private readBack;
439
518
  private settled;
440
519
  private warnIfBufferNotPreserved;
520
+ private warnNoWebGL2;
441
521
  private assertKnob;
442
522
  private validate;
443
523
  }
444
524
  //#endregion
445
- export type { BloomScope, BloomScopeEntry, CaptureOptions, EffectKnobId, EffectKnobRange, EffectKnobValueOf, MapEffectsCatalogue, MapEffectsKnobDescriptor, MapEffectsSettings, StylingColorKnobGroup };
525
+ export type { BloomScope, BloomScopeEntry, CaptureOptions, EffectKnob, EffectKnobId, EffectKnobRange, EffectKnobValueOf, MapEffectsPass, MapEffectsPassPlacement, MapEffectsPassUniforms, MapEffectsSettings, StylingColorKnobGroup };
package/dist/index.es.js CHANGED
@@ -1,2 +1,2 @@
1
- import{StylingModule as e,knob as t,parseCssColor as a,stylingColorKnobIds as n}from"@tomtom-org/maps-sdk/map";var s=e=>e.split(".").slice(0,-1).map((e,t,a)=>a.slice(0,t+1).join(".")),r=(()=>{const e=/* @__PURE__ */new Map;for(const t of n)for(const a of[...s(t),t])e.set(a,[...e.get(a)??[],t]);return e})(),i=[...r.keys()],o=e=>"string"!=typeof e?`${String(e)} is not a string`:r.has(e)||a(e)?void 0:`'${e}' is neither a CSS colour nor a styling colour knob or group`,h=(e,t,a)=>32*(32*(e>>3)+(t>>3))+(a>>3),l=([e,t,a])=>{const n=Math.max(e,t,a);if(0!==n)return[e/n,t/n,a/n]},c=(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[h(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)},d={min:0,max:1,step:.05},u="Emphasis on dark and driving styles.",g="Muting the base map under a data overlay.",f="Depth cue: the far context recedes as if seen through air.",p="Focus: the centre stays sharp and the edges lose their detail.",m="Depth on a tilted map: one band of distance stays sharp, the rest defocuses with distance.",b="Muting the base map under a data overlay; matching a brand.",v="Focus on the centre of the view.",w={"bloom.intensity":{kind:"number",description:"How strongly bright features (lit roads, traffic tubes, route lines) glow. 0 = off.",default:0,range:d,useCase:u},"bloom.radius":{kind:"number",description:"How far the glow spreads, in CSS pixels.",default:12,range:{min:2,max:40,step:1},useCase:u},"bloom.threshold":{kind:"number",description:"Brightness below which nothing glows. Raise it so only the brightest features light up.",default:.55,range:d,useCase:u},"bloom.only":{kind:"colors",description:"The colours bloom may light: CSS colours, styling colour knobs or groups of them (`traffic`, `traffic.incidents.majorColor`), read from the loaded style. Only the pixels wearing one of them glow; empty lights the whole frame.",default:[],options:i,useCase:u},"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:d,useCase:u},"grade.brightness":{kind:"number",description:"Brightness of the whole base map (1 = unchanged).",default:1,range:{min:.5,max:1.5,step:.05},useCase:g},"grade.contrast":{kind:"number",description:"Contrast of the whole base map (1 = unchanged).",default:1,range:{min:.5,max:1.5,step:.05},useCase:g},"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},useCase:g},"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:d,useCase:f},"fog.reach":{kind:"number",description:"How far the fog reaches in from the edges (1 = to the centre).",default:.4,range:d,useCase:f},"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},useCase:p},"edgeBlur.reach":{kind:"number",description:"How far the edge blur reaches in from the edges (1 = to the centre).",default:.5,range:d,useCase:p},"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},useCase:m},"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:d,useCase:m},"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:d,useCase:m},"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:d,useCase:m},"tint.color":{kind:"color",description:"A flat colour laid over the whole map.",default:"#1f2937",useCase:b},"tint.opacity":{kind:"number",description:"Opacity of the tint. 0 = off.",default:0,range:d,useCase:b},"vignette.intensity":{kind:"number",description:"Darkening (negative) or lightening (positive) towards the edges. 0 = off.",default:0,range:{min:-1,max:1,step:.05},useCase:v},"vignette.reach":{kind:"number",description:"How far the vignette reaches in from the edges (1 = to the centre).",default:.45,range:d,useCase:v}},y=Object.keys(w),x=(e,t,a)=>Math.min(Math.max(e,t),a),k=e=>x(e,0,1),T=(e,t,a=1)=>{const n=.92*k(t),s=(1+n)/(1-n);return`brightness(${(1/(1+n)).toFixed(4)}) contrast(${s.toFixed(3)}) blur(${e*a}px) saturate(1.35)`},E=(e,t,a)=>[1===e?"":`brightness(${e})`,1===t?"":`contrast(${t})`,1===a?"":`saturate(${a})`].filter(Boolean).join(" "),C=(e,t,a)=>`radial-gradient(ellipse farthest-corner at center, ${e} ${Math.round(100*(1-k(a)))}%, ${t} 100%)`,S=(e,t)=>{const{inner:a,outer:n}=M(e);return C(a,n,t)},M=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)})`}},R=e=>C("transparent","black",e),A=e=>e*Math.PI/180,B=(e,t,a)=>{const n=(({pitch:e,verticalFieldOfView:t})=>{const a=Math.tan(A(x(e,0,89))),n=2*Math.tan(A(x(t,1,175))/2)*a;return n/(1+n/2)})(t);if(!(n<=0||e.blurCssPx<=0))return{uDepth:n,uFocus:Math.min(k(e.focus)*n,1),uBand:.5*k(e.band),uBlur:Math.max(e.blurCssPx,0)*a.scale,uBokeh:k(e.bokeh),uTexel:[1/Math.max(a.width,1),1/Math.max(a.height,1)]}},D={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)"}},L="saturate(1.08)",_=e=>D[e].grade,F=e=>2+6*e,P=(e,t)=>`blur(${F(e)}px) ${_(t)}`,I=(e,t)=>{const{rgb:a}=D[t];return{inner:`rgba(${a}, 0)`,outer:`rgba(${a}, ${.65*e})`}},U=(e,t,a)=>{const{inner:n,outer:s}=I(e,a);return C(n,s,t)},$=(e,t,a,n)=>{const s=document.createElement("canvas");s.width=t,s.height=a;const r=s.getContext("2d");if(r)return r.filter=n,r.drawImage(e.canvas,0,0),r.filter="none",r},O=(e,t,a,n=1)=>{e.save(),e.globalCompositeOperation=a,e.globalAlpha=n,e.drawImage(t.canvas,0,0),e.restore()},N=(e,t,a,n,s,r)=>{const i=t/Math.SQRT2,o=a/Math.SQRT2,h=e.createRadialGradient(0,0,1-n,0,0,1);h.addColorStop(0,s),h.addColorStop(1,r),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()},X=(e,t,a,n,s,r,i)=>{const o=$(e,t,a,`blur(${s*n}px) ${r}`.trim());o&&(o.globalCompositeOperation="destination-in",i(o),e.drawImage(o.canvas,0,0))},G=(e,t,a,n)=>{const s=$(e,t,a,"none");return s&&c(s,n),s},j=(e,t,a,n,s,r,i)=>{s["bloom.intensity"]>0&&((e,t,a,n,s,r)=>{const i=T(s["bloom.radius"],s["bloom.threshold"],n),o=r?G(e,t,a,r):e;if(!o)return;const h=$(o,t,a,i);h&&O(e,h,"screen",s["bloom.intensity"])})(e,t,a,n,s,i);const o=E(s["grade.brightness"],s["grade.contrast"],s["grade.saturation"]);o&&((e,t,a,n)=>{const s=$(e,t,a,n);s&&O(e,s,"copy")})(e,t,a,o);const h=e=>n=>N(n,t,a,e,"rgba(0,0,0,0)","rgba(0,0,0,1)");if(s["fog.intensity"]>0){const i=s["fog.intensity"],o=s["fog.reach"],{inner:l,outer:c}=I(i,r),d=F(i);X(e,t,a,n,d,_(r),h(o)),N(e,t,a,o,l,c)}if(s["edgeBlur.intensity"]>0){const r=s["edgeBlur.intensity"];X(e,t,a,n,r,L,h(s["edgeBlur.reach"]))}if(s["tint.opacity"]>0&&(e.save(),e.globalAlpha=s["tint.opacity"],e.fillStyle=s["tint.color"],e.fillRect(0,0,t,a),e.restore()),0!==s["vignette.intensity"]){const{inner:n,outer:r}=M(s["vignette.intensity"]);N(e,t,a,s["vignette.reach"],n,r)}},W=e=>Object.fromEntries(y.map(t=>[t,e[t]??w[t].default])),q=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)},H=(e,t,a)=>{e.width===t&&e.height===a||(e.width=t,e.height=a)},K=(e,t,a)=>{const n=e.createShader(t);if(n){if(e.shaderSource(n,a),e.compileShader(n),e.getShaderParameter(n,e.COMPILE_STATUS))return n;console.warn(`[MapEffects] shader failed to compile: ${e.getShaderInfoLog(n)}`),e.deleteShader(n)}},z=class{canvas;gl;programs=/* @__PURE__ */new Map;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.sourceTexture=void 0,this.targets=[],this.targetSize={width:0,height:0},this.lost=!1}),this.gl=this.canvas.getContext("webgl2",{alpha:!0,antialias:!1,premultipliedAlpha:!1,preserveDrawingBuffer:!0})??void 0}render(e,t){const a=this.gl;if(!a||this.lost||!t.length||!e.width||!e.height)return;H(this.canvas,e.width,e.height);let n=this.uploadSource(e,t[0].mipmapped);if(!(!n||t.length>1&&!this.holdTargets(a))){for(let e=0;e<t.length;e++){const s=e===t.length-1?void 0:this.targets[e%this.targets.length];this.runStage(a,t[e],n,s),s&&(n=s.texture,t[e+1].mipmapped&&this.mipmap(a,n))}return this.canvas}}remove(){this.gl?.getExtension("WEBGL_lose_context")?.loseContext()}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 n=e.createTexture(),s=e.createFramebuffer();if(!n||!s)return!1;e.bindTexture(e.TEXTURE_2D,n),e.texImage2D(e.TEXTURE_2D,0,e.RGBA,t,a,0,e.RGBA,e.UNSIGNED_BYTE,null),q(e),e.bindFramebuffer(e.FRAMEBUFFER,s),e.framebufferTexture2D(e.FRAMEBUFFER,e.COLOR_ATTACHMENT0,e.TEXTURE_2D,n,0),this.targets.push({texture:n,framebuffer:s})}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),q(a),t&&this.mipmap(a,this.sourceTexture),this.sourceTexture}runStage(e,t,a,n){const s=this.programOf(e,t.fragment);if(s){e.bindFramebuffer(e.FRAMEBUFFER,n?.framebuffer??null),e.viewport(0,0,this.canvas.width,this.canvas.height),e.useProgram(s),e.activeTexture(e.TEXTURE0),e.bindTexture(e.TEXTURE_2D,a),e.uniform1i(e.getUniformLocation(s,"uSource"),0);for(const[a,n]of Object.entries(t.uniforms)){const t=e.getUniformLocation(s,a);t&&("number"==typeof n?e.uniform1f(t,n):e.uniform2f(t,n[0],n[1]))}e.drawArrays(e.TRIANGLES,0,3)}}programOf(e,t){const a=this.programs.get(t);if(a)return a;const n=K(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"),s=K(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}`),r=n&&s?e.createProgram():void 0;if(r&&n&&s){if(e.attachShader(r,n),e.attachShader(r,s),e.linkProgram(r),e.deleteShader(n),e.deleteShader(s),e.getProgramParameter(r,e.LINK_STATUS))return this.programs.set(t,r),r;console.warn(`[MapEffects] shader failed to link: ${e.getProgramInfoLog(r)}`)}}},V=["bloom","grade","fog","edgeBlur","tint","vignette"],Y=e=>({backdropFilter:e,webkitBackdropFilter:e}),Q=e=>({maskImage:e,webkitMaskImage:e}),J={position:"absolute",inset:"0",width:"100%",height:"100%",pointerEvents:"none",display:"none"},Z=e=>e.width/Math.max(e.clientWidth,1),ee=e=>{let t;return{schedule:()=>{void 0===t&&(t=requestAnimationFrame(()=>{t=void 0,e()}))},cancel:()=>{void 0!==t&&cancelAnimationFrame(t),t=void 0}}},te=class{map;settings={};container;overlays;bloomCanvas;artworkCanvas;chain=new z;chainDraw=ee(()=>this.drawChain());bloomDraw=ee(()=>this.drawBloom());styling;stylingRequest;scopeMask;scopeCanvas;warnedBuffer=!1;warnedScope=!1;onStyleData=()=>this.dropScopeMask();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(V.map(e=>{const t="bloom"===e?this.bloomCanvas:document.createElement("div");return t.dataset.effect=e,Object.assign(t.style,J),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,J),this.container.insertAdjacentElement("afterbegin",this.artworkCanvas),this.map.mapLibreMap.on("render",this.onRender),this.map.mapLibreMap.on("styledata",this.onStyleData),this.set(t)}cameraAngles(){const e=this.map.mapLibreMap;return{pitch:e.getPitch(),verticalFieldOfView:e.getVerticalFieldOfView()}}set(e){for(const[t,a]of Object.entries(e))this.validate(t,a);this.settings={...this.settings,...e},this.render()}get(e){return this.assertKnob(e),this.settings[e]??w[e].default}reset(e){if(void 0===e)this.settings={};else{this.assertKnob(e);const{[e]:t,...a}=this.settings;this.settings=a}this.render()}getConfig(){return{...this.settings}}applyConfig(e){this.settings={},this.set(e)}describe(){return{knobs:y.map(e=>{const a=w[e];return t(e,a.kind,a.description,a.default,this.settings[e],{..."range"in a&&{range:a.range},..."options"in a&&{options:a.options},available:!0,appliesTo:"any-style",useCase:a.useCase})})}}async capture(e={}){this.warnIfBufferNotPreserved();const t=this.map.mapLibreMap,a=t,n=t.getPixelRatio(),s=e.pixelRatio??n,r=n!==(globalThis.devicePixelRatio||1),i=Math.abs(s-n)>.01;i&&(a.setPixelRatio(s),await this.settled());try{return await this.readBack()}finally{i&&a.setPixelRatio(r?n: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()}render(){const e=W(this.settings),t=(e,t,a={})=>{const n=this.overlays[e];n.style.display=t?"block":"none",Object.assign(n.style,a)},a=E(e["grade.brightness"],e["grade.contrast"],e["grade.saturation"]);t("grade",""!==a,Y(a));const n=this.map.styleLightDarkTheme;var s;t("fog",e["fog.intensity"]>0,{...Y(P(e["fog.intensity"],n)),...Q(R(e["fog.reach"])),backgroundImage:U(e["fog.intensity"],e["fog.reach"],n)}),t("edgeBlur",e["edgeBlur.intensity"]>0,{...Y((s=e["edgeBlur.intensity"],`blur(${s}px) ${L}`)),...Q(R(e["edgeBlur.reach"]))}),e["depthOfField.intensity"]>0&&this.warnIfBufferNotPreserved(),this.drawChain(e),t("tint",e["tint.opacity"]>0,{backgroundColor:e["tint.color"],opacity:String(e["tint.opacity"])}),t("vignette",0!==e["vignette.intensity"],{backgroundImage:S(e["vignette.intensity"],e["vignette.reach"])});const r=e["bloom.intensity"]>0;t("bloom",r,{opacity:String(e["bloom.intensity"])}),r&&(this.warnIfBufferNotPreserved(),this.scheduleBloom())}chainStages(e){const t=this.map.mapLibreMap.getCanvas(),a=B({focus:e["depthOfField.focus"],band:e["depthOfField.band"],blurCssPx:e["depthOfField.intensity"],bokeh:e["depthOfField.bokeh"]},this.cameraAngles(),{width:t.width,height:t.height,scale:Z(t)});return a?[{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:a,mipmapped:!0}]:[]}drawChain(e=W(this.settings)){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(){this.get("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));H(this.bloomCanvas,t,a);const n=this.bloomCanvas.getContext("2d");if(!n)return;const s=this.scopedSource(e,t,a)??e;n.filter=T(this.get("bloom.radius"),this.get("bloom.threshold"),.5),n.clearRect(0,0,t,a),n.drawImage(s,0,0,t,a)}scopedSource(e,t,a){const n=this.currentScopeMask();if(!n)return;this.scopeCanvas??=document.createElement("canvas");const s=this.scopeCanvas;H(s,t,a);const r=s.getContext("2d",{willReadFrequently:!0});return r?(r.clearRect(0,0,t,a),r.drawImage(e,0,0,t,a),c(r,n),s):void 0}currentScopeMask(){const e=this.get("bloom.only");if(0===e.length)return;const t=e.join("\n"),a=this.get("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 n=(e=>e.some(e=>r.has(e)))(e);n&&this.requestStyling();const s=((e,t)=>t.flatMap(t=>{const n=r.get(t);return(n?n.map(t=>e?.get(t)):[t]).map(e=>"string"==typeof e?a(e):void 0).filter(e=>void 0!==e)}))(this.styling,e);return 0!==s.length||n&&!this.styling||this.warnScopeEmpty(e),((e,t)=>{const a=e.map(l).filter(e=>void 0!==e);if(0===a.length)return;const n=t*t,s=new Uint8Array(32768);for(let r=0;r<256;r+=8)for(let e=0;e<256;e+=8)for(let t=0;t<256;t+=8){const i=l([r,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<=n})&&(s[h(r,e,t)]=1)}return s})(s,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.`))}requestStyling(){this.stylingRequest??=e.get(this.map).then(e=>{this.styling=e,this.dropScopeMask()})}dropScopeMask(){this.scopeMask=void 0,this.scheduleBloom()}readBack(){const e=this.map.mapLibreMap;return new Promise(t=>{e.once("render",()=>{const a=e.getCanvas(),n=W(this.settings),s=document.createElement("canvas");s.width=a.width,s.height=a.height;const r=s.getContext("2d");if(r){const e=this.chainStages(n);r.drawImage(this.chain.render(a,e)??a,0,0),j(r,a.width,a.height,Z(a),n,this.map.styleLightDarkTheme,this.currentScopeMask())}t(s)}),e.triggerRepaint()})}settled(e=15e3){const t=this.map.mapLibreMap;return new Promise(a=>{const n=()=>{clearTimeout(s),t.off("idle",n),a()},s=setTimeout(n,e);t.once("idle",n)})}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 and capture read the map canvas, which needs `mapLibre: { canvasContextAttributes: { preserveDrawingBuffer: true } }` on the map's options."))}assertKnob(e){if(!(e in w))throw new Error(`Unknown effect knob '${e}'. Call describe() for the catalogue of knob ids.`)}validate(e,t){this.assertKnob(e);const a=w[e];if("colors"===a.kind){if(!Array.isArray(t))throw new RangeError(`Knob '${e}' expects a list of colours; got ${String(t)}.`);const a=t.map(o).find(e=>void 0!==e);if(a)throw new RangeError(`Knob '${e}': ${a}. Call describe() for the names it accepts.`);return}if("color"===a.kind){if("string"!=typeof t||!t)throw new RangeError(`Knob '${e}' expects a CSS colour; got ${String(t)}.`);return}const{range:n}=a;if("number"!=typeof t||!Number.isFinite(t))throw new RangeError(`Knob '${e}' expects a number; got ${String(t)}.`);if(t<n.min||t>n.max)throw new RangeError(`Knob '${e}' expects a number between ${n.min} and ${n.max}; got ${t}.`)}};export{te as MapEffects,y as effectKnobIds};
1
+ import{StylingModule as e,parseCssColor as t,stylingColorKnobIds as a}from"@tomtom-org/maps-sdk/map";var s=e=>e.split(".").slice(0,-1).map((e,t,a)=>a.slice(0,t+1).join(".")),n=(()=>{const e=/* @__PURE__ */new Map;for(const t of a)for(const a of[...s(t),t])e.set(a,[...e.get(a)??[],t]);return e})(),r=[...n.keys()],i=e=>"string"!=typeof e?`${String(e)} is not a string`:n.has(e)||t(e)?void 0:`'${e}' is neither a CSS colour nor a styling colour knob or group`,o=(e,t,a)=>32*(32*(e>>3)+(t>>3))+(a>>3),h=([e,t,a])=>{const s=Math.max(e,t,a);if(0!==s)return[e/s,t/s,a/s]},l=(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[o(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)},c={min:0,max:1,step:.05},d="Emphasis on dark and driving styles.",u="Muting the base map under a data overlay.",f="Depth cue: the far context recedes as if seen through air.",g="Focus: the centre stays sharp and the edges lose their detail.",p="Depth on a tilted map: one band of distance stays sharp, the rest defocuses with distance.",m="Muting the base map under a data overlay; matching a brand.",b="Focus on the centre of the view.",w={"bloom.intensity":{kind:"number",description:"How strongly bright features (lit roads, traffic tubes, route lines) glow. 0 = off.",default:0,range:c,useCase:d},"bloom.radius":{kind:"number",description:"How far the glow spreads, in CSS pixels.",default:12,range:{min:2,max:40,step:1},useCase:d},"bloom.threshold":{kind:"number",description:"Brightness below which nothing glows. Raise it so only the brightest features light up.",default:.55,range:c,useCase:d},"bloom.only":{kind:"colors",description:"The colours bloom may light: CSS colours, styling colour knobs or groups of them (`traffic`, `traffic.incidents.majorColor`), read from the loaded style. Only the pixels wearing one of them glow; empty lights the whole frame.",default:[],options:r,useCase:d},"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:c,useCase:d},"grade.brightness":{kind:"number",description:"Brightness of the whole base map (1 = unchanged).",default:1,range:{min:.5,max:1.5,step:.05},useCase:u},"grade.contrast":{kind:"number",description:"Contrast of the whole base map (1 = unchanged).",default:1,range:{min:.5,max:1.5,step:.05},useCase:u},"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},useCase:u},"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:c,useCase:f},"fog.reach":{kind:"number",description:"How far the fog reaches in from the edges (1 = to the centre).",default:.4,range:c,useCase:f},"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},useCase:g},"edgeBlur.reach":{kind:"number",description:"How far the edge blur reaches in from the edges (1 = to the centre).",default:.5,range:c,useCase:g},"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},useCase:p},"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:c,useCase:p},"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:c,useCase:p},"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:c,useCase:p},"tint.color":{kind:"color",description:"A flat colour laid over the whole map.",default:"#1f2937",useCase:m},"tint.opacity":{kind:"number",description:"Opacity of the tint. 0 = off.",default:0,range:c,useCase:m},"vignette.intensity":{kind:"number",description:"Darkening (negative) or lightening (positive) towards the edges. 0 = off.",default:0,range:{min:-1,max:1,step:.05},useCase:b},"vignette.reach":{kind:"number",description:"How far the vignette reaches in from the edges (1 = to the centre).",default:.45,range:c,useCase:b}},v=Object.keys(w),y=v.map(e=>({id:e,...w[e]})),x=(e,t,a)=>Math.min(Math.max(e,t),a),E=e=>x(e,0,1),k=(e,t,a=1)=>{const s=.92*E(t),n=(1+s)/(1-s);return`brightness(${(1/(1+s)).toFixed(4)}) contrast(${n.toFixed(3)}) blur(${e*a}px) saturate(1.35)`},T=(e,t,a)=>[1===e?"":`brightness(${e})`,1===t?"":`contrast(${t})`,1===a?"":`saturate(${a})`].filter(Boolean).join(" "),S=(e,t,a)=>`radial-gradient(ellipse farthest-corner at center, ${e} ${Math.round(100*(1-E(a)))}%, ${t} 100%)`,C=(e,t)=>{const{inner:a,outer:s}=M(e);return S(a,s,t)},M=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)})`}},R=e=>S("transparent","black",e),A=e=>e*Math.PI/180,L=(e,t,a)=>{const s=(({pitch:e,verticalFieldOfView:t})=>{const a=Math.tan(A(x(e,0,89))),s=2*Math.tan(A(x(t,1,175))/2)*a;return s/(1+s/2)})(t);if(!(s<=0||e.blurCssPx<=0))return{uDepth:s,uFocus:Math.min(E(e.focus)*s,1),uBand:.5*E(e.band),uBlur:Math.max(e.blurCssPx,0)*a.scale,uBokeh:E(e.bokeh),uTexel:[1/Math.max(a.width,1),1/Math.max(a.height,1)]}},B={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)"}},P="saturate(1.08)",D=e=>B[e].grade,_=e=>2+6*e,F=(e,t)=>`blur(${_(e)}px) ${D(t)}`,$=(e,t)=>{const{rgb:a}=B[t];return{inner:`rgba(${a}, 0)`,outer:`rgba(${a}, ${.65*e})`}},U=(e,t,a)=>{const{inner:s,outer:n}=$(e,a);return S(s,n,t)},I=(e,t,a,s)=>{const n=document.createElement("canvas");n.width=t,n.height=a;const r=n.getContext("2d");if(r)return r.filter=s,r.drawImage(e.canvas,0,0),r.filter="none",r},O=(e,t,a,s=1)=>{e.save(),e.globalCompositeOperation=a,e.globalAlpha=s,e.drawImage(t.canvas,0,0),e.restore()},N=(e,t,a,s,n,r)=>{const i=t/Math.SQRT2,o=a/Math.SQRT2,h=e.createRadialGradient(0,0,1-s,0,0,1);h.addColorStop(0,n),h.addColorStop(1,r),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()},G=(e,t,a,s,n,r,i)=>{const o=I(e,t,a,`blur(${n*s}px) ${r}`.trim());o&&(o.globalCompositeOperation="destination-in",i(o),e.drawImage(o.canvas,0,0))},W=(e,t,a,s)=>{const n=I(e,t,a,"none");return n&&l(n,s),n},X=(e,t,a,s,n,r,i)=>{n["bloom.intensity"]>0&&((e,t,a,s,n,r)=>{const i=k(n["bloom.radius"],n["bloom.threshold"],s),o=r?W(e,t,a,r):e;if(!o)return;const h=I(o,t,a,i);h&&O(e,h,"screen",n["bloom.intensity"])})(e,t,a,s,n,i);const o=T(n["grade.brightness"],n["grade.contrast"],n["grade.saturation"]);o&&((e,t,a,s)=>{const n=I(e,t,a,s);n&&O(e,n,"copy")})(e,t,a,o);const h=e=>s=>N(s,t,a,e,"rgba(0,0,0,0)","rgba(0,0,0,1)");if(n["fog.intensity"]>0){const i=n["fog.intensity"],o=n["fog.reach"],{inner:l,outer:c}=$(i,r),d=_(i);G(e,t,a,s,d,D(r),h(o)),N(e,t,a,o,l,c)}if(n["edgeBlur.intensity"]>0){const r=n["edgeBlur.intensity"];G(e,t,a,s,r,P,h(n["edgeBlur.reach"]))}if(n["tint.opacity"]>0&&(e.save(),e.globalAlpha=n["tint.opacity"],e.fillStyle=n["tint.color"],e.fillRect(0,0,t,a),e.restore()),0!==n["vignette.intensity"]){const{inner:s,outer:r}=M(n["vignette.intensity"]);N(e,t,a,n["vignette.reach"],s,r)}},j=e=>Object.fromEntries(v.map(t=>[t,e[t]??w[t].default])),K=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)},q=(e,t,a)=>{e.width===t&&e.height===a||(e.width=t,e.height=a)},H=(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 n=e.getShaderInfoLog(s)??"";return e.deleteShader(s),`failed to compile: ${n}`},z=class{canvas;gl;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}),this.gl=this.canvas.getContext("webgl2",{alpha:!0,antialias:!1,premultipliedAlpha:!1,preserveDrawingBuffer:!0})??void 0}render(e,t){const a=this.gl;if(!a||this.lost||!t.length||!e.width||!e.height)return;q(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 n=e===t.length-1?void 0:this.targets[e%this.targets.length];this.runStage(a,t[e],s,n),n&&(s=n.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.gl?.deleteProgram(t),this.programs.delete(e),this.failures.delete(e)}remove(){this.gl?.getExtension("WEBGL_lose_context")?.loseContext()}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(),n=e.createFramebuffer();if(!s||!n)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),K(e),e.bindFramebuffer(e.FRAMEBUFFER,n),e.framebufferTexture2D(e.FRAMEBUFFER,e.COLOR_ATTACHMENT0,e.TEXTURE_2D,s,0),this.targets.push({texture:s,framebuffer:n})}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),K(a),t&&this.mipmap(a,this.sourceTexture),this.sourceTexture}runStage(e,t,a,s){const n=this.programOf(e,t.fragment);if(n){e.bindFramebuffer(e.FRAMEBUFFER,s?.framebuffer??null),e.viewport(0,0,this.canvas.width,this.canvas.height),e.useProgram(n),e.activeTexture(e.TEXTURE0),e.bindTexture(e.TEXTURE_2D,a),e.uniform1i(e.getUniformLocation(n,"uSource"),0);for(const[a,s]of Object.entries(t.uniforms)){const t=e.getUniformLocation(n,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=[H(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"),H(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),n=a.filter(e=>"string"!=typeof e),r=s?null:e.createProgram();if(r){for(const t of n)e.attachShader(r,t);e.linkProgram(r)}for(const o of n)e.deleteShader(o);if(s)return s;if(!r)return"the context would not create a program";if(e.getProgramParameter(r,e.LINK_STATUS))return r;const i=e.getProgramInfoLog(r)??"";return e.deleteProgram(r),`failed to link: ${i}`}},V={"before-depth-of-field":!0,"after-depth-of-field":!0},Y=["bloom","grade","fog","edgeBlur","tint","vignette"],Q=e=>({backdropFilter:e,webkitBackdropFilter:e}),J=e=>({maskImage:e,webkitMaskImage:e}),Z={position:"absolute",inset:"0",width:"100%",height:"100%",pointerEvents:"none",display:"none"},ee=e=>e.width/Math.max(e.clientWidth,1),te=e=>{let t;return{schedule:()=>{void 0===t&&(t=requestAnimationFrame(()=>{t=void 0,e()}))},cancel:()=>{void 0!==t&&cancelAnimationFrame(t),t=void 0}}},ae=class{map;settings={};passes=[];container;overlays;bloomCanvas;artworkCanvas;chain=new z;chainDraw=te(()=>this.drawChain());bloomDraw=te(()=>this.drawBloom());styling;stylingRequest;scopeMask;scopeCanvas;warnedBuffer=!1;warnedScope=!1;warnedNoWebGL2=!1;warnedUniforms=/* @__PURE__ */new WeakSet;onStyleData=()=>this.dropScopeMask();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(Y.map(e=>{const t="bloom"===e?this.bloomCanvas:document.createElement("div");return t.dataset.effect=e,Object.assign(t.style,Z),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,Z),this.container.insertAdjacentElement("afterbegin",this.artworkCanvas),this.map.mapLibreMap.on("render",this.onRender),this.map.mapLibreMap.on("styledata",this.onStyleData),this.set(t)}cameraAngles(){const e=this.map.mapLibreMap;return{pitch:e.getPitch(),verticalFieldOfView:e.getVerticalFieldOfView()}}set(e){for(const[t,a]of Object.entries(e))this.validate(t,a);this.settings={...this.settings,...e},this.render()}get(e){return this.assertKnob(e),this.settings[e]??w[e].default}reset(e){if(void 0===e)this.settings={};else{this.assertKnob(e);const{[e]:t,...a}=this.settings;this.settings=a}this.render()}getConfig(){return{...this.settings}}applyConfig(e){this.settings={},this.set(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(V,e.placement))throw new RangeError(`Pass '${e.id}' expects a placement of ${Object.keys(V).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(),n=e.pixelRatio??s,r=s!==(globalThis.devicePixelRatio||1),i=Math.abs(n-s)>.01;i&&(a.setPixelRatio(n),await this.settled());try{return await this.readBack()}finally{i&&a.setPixelRatio(r?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()}render(){const e=j(this.settings),t=(e,t,a={})=>{const s=this.overlays[e];s.style.display=t?"block":"none",Object.assign(s.style,a)},a=T(e["grade.brightness"],e["grade.contrast"],e["grade.saturation"]);t("grade",""!==a,Q(a));const s=this.map.styleLightDarkTheme;var n;t("fog",e["fog.intensity"]>0,{...Q(F(e["fog.intensity"],s)),...J(R(e["fog.reach"])),backgroundImage:U(e["fog.intensity"],e["fog.reach"],s)}),t("edgeBlur",e["edgeBlur.intensity"]>0,{...Q((n=e["edgeBlur.intensity"],`blur(${n}px) ${P}`)),...J(R(e["edgeBlur.reach"]))}),e["depthOfField.intensity"]>0&&this.warnIfBufferNotPreserved(),this.drawChain(e),t("tint",e["tint.opacity"]>0,{backgroundColor:e["tint.color"],opacity:String(e["tint.opacity"])}),t("vignette",0!==e["vignette.intensity"],{backgroundImage:C(e["vignette.intensity"],e["vignette.reach"])});const r=e["bloom.intensity"]>0;t("bloom",r,{opacity:String(e["bloom.intensity"])}),r&&(this.warnIfBufferNotPreserved(),this.scheduleBloom())}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.map.mapLibreMap.getCanvas(),a=L({focus:e["depthOfField.focus"],band:e["depthOfField.band"],blurCssPx:e["depthOfField.intensity"],bokeh:e["depthOfField.bokeh"]},this.cameraAngles(),{width:t.width,height:t.height,scale:ee(t)});return a?[{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:a,mipmapped:!0}]:[]}drawChain(e=j(this.settings)){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(){this.get("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));q(this.bloomCanvas,t,a);const s=this.bloomCanvas.getContext("2d");if(!s)return;const n=this.scopedSource(e,t,a)??e;s.filter=k(this.get("bloom.radius"),this.get("bloom.threshold"),.5),s.clearRect(0,0,t,a),s.drawImage(n,0,0,t,a)}scopedSource(e,t,a){const s=this.currentScopeMask();if(!s)return;this.scopeCanvas??=document.createElement("canvas");const n=this.scopeCanvas;q(n,t,a);const r=n.getContext("2d",{willReadFrequently:!0});return r?(r.clearRect(0,0,t,a),r.drawImage(e,0,0,t,a),l(r,s),n):void 0}currentScopeMask(){const e=this.get("bloom.only");if(0===e.length)return;const t=e.join("\n"),a=this.get("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,a){const s=(e=>e.some(e=>n.has(e)))(e);s&&this.requestStyling();const r=((e,a)=>a.flatMap(a=>{const s=n.get(a);return(s?s.map(t=>e?.get(t)):[a]).map(e=>"string"==typeof e?t(e):void 0).filter(e=>void 0!==e)}))(this.styling,e);return 0!==r.length||s&&!this.styling||this.warnScopeEmpty(e),((e,t)=>{const a=e.map(h).filter(e=>void 0!==e);if(0===a.length)return;const s=t*t,n=new Uint8Array(32768);for(let r=0;r<256;r+=8)for(let e=0;e<256;e+=8)for(let t=0;t<256;t+=8){const i=h([r,e,t]);i&&a.some(e=>{const t=i[0]-e[0],a=i[1]-e[1],n=i[2]-e[2];return t*t+a*a+n*n<=s})&&(n[o(r,e,t)]=1)}return n})(r,a)}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.`))}requestStyling(){this.stylingRequest??=e.get(this.map).then(e=>{this.styling=e,this.dropScopeMask()})}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=j(this.settings),n=document.createElement("canvas");n.width=a.width,n.height=a.height;const r=n.getContext("2d");if(r){const e=this.chainStages(s);r.drawImage(this.chain.render(a,e)??a,0,0),X(r,a.width,a.height,ee(a),s,this.map.styleLightDarkTheme,this.currentScopeMask())}t(n)}),e.triggerRepaint()})}settled(e=15e3){const t=this.map.mapLibreMap;return new Promise(a=>{const s=()=>{clearTimeout(n),t.off("idle",s),a()},n=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."))}assertKnob(e){if(!(e in w))throw new Error(`Unknown effect knob '${e}'. effectKnobIds lists every knob id.`)}validate(e,t){this.assertKnob(e);const a=w[e];if("colors"===a.kind){if(!Array.isArray(t))throw new RangeError(`Knob '${e}' expects a list of colours; got ${String(t)}.`);const a=t.map(i).find(e=>void 0!==e);if(a)throw new RangeError(`Knob '${e}': ${a}. effectKnobCatalogue lists the names it accepts.`);return}if("color"===a.kind){if("string"!=typeof t||!t)throw new RangeError(`Knob '${e}' expects a CSS colour; got ${String(t)}.`);return}const{range:s}=a;if("number"!=typeof t||!Number.isFinite(t))throw new RangeError(`Knob '${e}' expects a number; got ${String(t)}.`);if(t<s.min||t>s.max)throw new RangeError(`Knob '${e}' expects a number between ${s.min} and ${s.max}; got ${t}.`)}};export{ae as MapEffects,y as effectKnobCatalogue,v as effectKnobIds};
2
2
  //# sourceMappingURL=index.es.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.es.js","names":[],"sources":["../src/bloomScope.ts","../src/effectsCatalogue.ts","../src/effectFilters.ts","../src/composite.ts","../src/effectValues.ts","../src/gpu/defocusShader.ts","../src/gpu/screenPass.ts","../src/MapEffects.ts"],"sourcesContent":["/**\n * Scoping bloom to part of the map, by the colours that part is drawn in.\n *\n * - **Colour is what survives flattening**: the effects read one image with no layers in it.\n * - **Names come from the SDK**: every knob and group is derived from `stylingColorKnobIds`.\n * - **Brightness is not compared**: an antialiased edge and a lit centre are one colour at two\n * levels, and `bloom.threshold` judges level.\n *\n * @module\n */\n\nimport {\n parseCssColor,\n type RGBAColor,\n type StylingColorKnobId,\n type StylingModule,\n stylingColorKnobIds,\n} from '@tomtom-org/maps-sdk/map';\n\n// Every dotted prefix of an id: `traffic` and `traffic.incidents` for `traffic.incidents.majorColor`.\ntype GroupsOf<ID extends string> = ID extends `${infer Head}.${infer Rest}`\n ? Head | `${Head}.${GroupsOf<Rest>}`\n : never;\n\n/**\n * A family of colour knobs, named by the prefix their ids share — `traffic` for every traffic\n * colour, `traffic.incidents` for the incident severities, `colors` for the base map's own.\n *\n * @group Map Effects\n */\nexport type StylingColorKnobGroup = GroupsOf<StylingColorKnobId>;\n\n/**\n * One entry of a bloom scope: a styling colour knob, a group of them, or a CSS colour — the last\n * for what the styling module does not paint, such as a route line in its `mainColor`.\n *\n * @group Map Effects\n */\n// `string & {}` accepts any CSS colour while leaving the knob names to autocomplete.\nexport type BloomScopeEntry = StylingColorKnobId | StylingColorKnobGroup | (string & {});\n\n/**\n * The colours bloom may light. Empty — the default — lights the whole frame.\n *\n * @group Map Effects\n */\nexport type BloomScope = readonly BloomScopeEntry[];\n\nconst groupsOf = (id: string): string[] =>\n id\n .split('.')\n .slice(0, -1)\n .map((_, index, parts) => parts.slice(0, index + 1).join('.'));\n\n// Every name a scope accepts besides a CSS colour, and the colour knobs it stands for: each knob\n// under its own id, and under every group its id falls in.\nconst NAMED_KNOBS: ReadonlyMap<string, readonly StylingColorKnobId[]> = (() => {\n const named = new Map<string, StylingColorKnobId[]>();\n for (const id of stylingColorKnobIds) {\n for (const name of [...groupsOf(id), id]) named.set(name, [...(named.get(name) ?? []), id]);\n }\n return named;\n})();\n\n/**\n * The names a scope accepts besides CSS colours, in catalogue order: each group before its first knob.\n * @ignore\n */\nexport const bloomScopeNames: readonly string[] = [...NAMED_KNOBS.keys()];\n\n/**\n * Why an entry is not one a scope can use, or undefined when it is: a known name, or a CSS colour.\n * @ignore\n */\nexport const invalidScopeEntry = (entry: unknown): string | undefined => {\n if (typeof entry !== 'string') return `${String(entry)} is not a string`;\n if (NAMED_KNOBS.has(entry) || parseCssColor(entry)) return undefined;\n\n return `'${entry}' is neither a CSS colour nor a styling colour knob or group`;\n};\n\n/**\n * Whether resolving a scope needs the styling module — any entry that names knobs rather than\n * giving a colour outright.\n * @ignore\n */\nexport const scopeReadsStyling = (scope: BloomScope): boolean => scope.some((entry) => NAMED_KNOBS.has(entry));\n\n/**\n * The colours a scope currently wears. Named entries are read from the styling module, so a\n * recolour or a style switch moves the scope with it; a knob the style paints with an expression no\n * single literal stands for yields nothing, and so does a named entry before the module arrives.\n * @ignore\n */\nexport const scopeColors = (styling: StylingModule | undefined, scope: BloomScope): RGBAColor[] =>\n scope.flatMap((entry) => {\n const knobs = NAMED_KNOBS.get(entry);\n const literals = knobs ? knobs.map((id) => styling?.get(id)) : [entry];\n return literals\n .map((value) => (typeof value === 'string' ? parseCssColor(value) : undefined))\n .filter((color) => color !== undefined);\n });\n\n// The cube is indexed by the top five bits of each channel: 32³ entries covering every colour a\n// pixel can be, built once per scope so the per-pixel work is one lookup rather than one distance\n// per key colour.\nconst CUBE_BITS = 3;\nconst CUBE_SIDE = 256 >> CUBE_BITS;\nconst cubeIndex = (red: number, green: number, blue: number): number =>\n ((red >> CUBE_BITS) * CUBE_SIDE + (green >> CUBE_BITS)) * CUBE_SIDE + (blue >> CUBE_BITS);\n\ntype Chromaticity = [red: number, green: number, blue: number];\n\n/**\n * Chromaticity: the colour with its brightness divided out, so a dim edge and the lit centre of the\n * same tube compare equal — and so do a 0–1 key colour and the 0–255 pixel wearing it. Black has no\n * chromaticity and is returned as such.\n */\nconst chromaticity = ([red, green, blue]: readonly number[]): Chromaticity | undefined => {\n const top = Math.max(red, green, blue);\n if (top === 0) return undefined;\n\n return [red / top, green / top, blue / top];\n};\n\n/**\n * A lookup cube saying, for every colour, whether bloom may light it: within `tolerance` of some\n * key colour's chromaticity. An empty key set has nothing to key on and returns undefined, which is\n * what lights the whole frame.\n * @ignore\n */\nexport const buildScopeMask = (colors: RGBAColor[], tolerance: number): Uint8Array | undefined => {\n const keyColors = colors.map(chromaticity).filter((color) => color !== undefined);\n if (keyColors.length === 0) return undefined;\n\n const limit = tolerance * tolerance;\n const mask = new Uint8Array(CUBE_SIDE * CUBE_SIDE * CUBE_SIDE);\n for (let red = 0; red < 256; red += 1 << CUBE_BITS) {\n for (let green = 0; green < 256; green += 1 << CUBE_BITS) {\n for (let blue = 0; blue < 256; blue += 1 << CUBE_BITS) {\n const pixel = chromaticity([red, green, blue]);\n if (!pixel) continue;\n\n const lit = keyColors.some((key) => {\n const deltaRed = pixel[0] - key[0];\n const deltaGreen = pixel[1] - key[1];\n const deltaBlue = pixel[2] - key[2];\n return deltaRed * deltaRed + deltaGreen * deltaGreen + deltaBlue * deltaBlue <= limit;\n });\n if (lit) mask[cubeIndex(red, green, blue)] = 1;\n }\n }\n }\n return mask;\n};\n\n/**\n * Blacks out every RGBA pixel the scope does not cover, in place. Black is the identity under\n * `screen`, so a pixel dropped here adds nothing to the frame — the same way a pixel below the\n * threshold contributes nothing.\n * @ignore\n */\nexport const maskPixels = (pixels: Uint8ClampedArray, mask: Uint8Array): void => {\n for (let index = 0; index < pixels.length; index += 4) {\n if (mask[cubeIndex(pixels[index], pixels[index + 1], pixels[index + 2])]) continue;\n\n pixels[index] = 0;\n pixels[index + 1] = 0;\n pixels[index + 2] = 0;\n }\n};\n\n/**\n * {@link maskPixels} over everything a 2D canvas holds — the one step the live bloom and the\n * capture both take before their threshold runs.\n * @ignore\n */\nexport const applyScopeMask = (context: CanvasRenderingContext2D, mask: Uint8Array): void => {\n const image = context.getImageData(0, 0, context.canvas.width, context.canvas.height);\n maskPixels(image.data, mask);\n context.putImageData(image, 0, 0);\n};\n","import type { KnobKind, KnobRange } from '@tomtom-org/maps-sdk/map';\nimport { type BloomScope, bloomScopeNames } from './bloomScope';\n\n/**\n * The range of a numeric effect knob. Every one ships a slider `step`; the shared {@link KnobRange}\n * leaves it optional.\n *\n * @group Map Effects\n */\nexport type EffectKnobRange = KnobRange & { step: number };\n\n/**\n * One effect knob, drawn from the SDK's shared knob vocabulary — so a UI or an agent built for\n * `StylingModule.describe()` reads this catalogue unchanged. Discriminated on `kind`: a range\n * belongs to a numeric knob and the accepted names to a `colors` one.\n * @ignore\n */\nexport type EffectKnobDefinition = {\n description: string;\n /** The practical problem the effect solves — the reason it exists in an SDK with no artistic ambitions. */\n useCase: string;\n} & (\n | { kind: Extract<KnobKind, 'number'>; default: number; range: EffectKnobRange }\n | { kind: Extract<KnobKind, 'color'>; default: string }\n | { kind: Extract<KnobKind, 'colors'>; default: BloomScope; options: readonly string[] }\n);\n\nconst FRACTION: EffectKnobRange = { min: 0, max: 1, step: 0.05 };\n\n// The practical problem each effect solves. Every knob of one effect carries the same one.\nconst USE_CASE = {\n bloom: 'Emphasis on dark and driving styles.',\n grade: 'Muting the base map under a data overlay.',\n fog: 'Depth cue: the far context recedes as if seen through air.',\n edgeBlur: 'Focus: the centre stays sharp and the edges lose their detail.',\n depthOfField: 'Depth on a tilted map: one band of distance stays sharp, the rest defocuses with distance.',\n tint: 'Muting the base map under a data overlay; matching a brand.',\n vignette: 'Focus on the centre of the view.',\n} as const;\n\n/**\n * The effects catalogue. Every effect is off at its default; each is named by the product problem it\n * solves. Ordering is the compositing order the overlays stack in — bloom → grade → fog → edge blur\n * → tint → vignette — with the depth of field listed beside the other defocus, though it is a GPU\n * pass under all of them rather than one of them.\n * @ignore\n */\nexport const effectKnobDefinitions = {\n 'bloom.intensity': {\n kind: 'number',\n description: 'How strongly bright features (lit roads, traffic tubes, route lines) glow. 0 = off.',\n default: 0,\n range: FRACTION,\n useCase: USE_CASE.bloom,\n },\n 'bloom.radius': {\n kind: 'number',\n description: 'How far the glow spreads, in CSS pixels.',\n default: 12,\n range: { min: 2, max: 40, step: 1 },\n useCase: USE_CASE.bloom,\n },\n 'bloom.threshold': {\n kind: 'number',\n description: 'Brightness below which nothing glows. Raise it so only the brightest features light up.',\n default: 0.55,\n range: FRACTION,\n useCase: USE_CASE.bloom,\n },\n 'bloom.only': {\n kind: 'colors',\n description:\n 'The colours bloom may light: CSS colours, styling colour knobs or groups of them (`traffic`, `traffic.incidents.majorColor`), read from the loaded style. Only the pixels wearing one of them glow; empty lights the whole frame.',\n default: [],\n options: bloomScopeNames,\n useCase: USE_CASE.bloom,\n },\n 'bloom.onlyTolerance': {\n kind: 'number',\n description:\n \"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.\",\n // The TomTom styles put a moderate jam 0.217 from a major one in chromaticity; from 0.22 up\n // a major-jam scope lets in every reddish jam.\n default: 0.15,\n range: FRACTION,\n useCase: USE_CASE.bloom,\n },\n 'grade.brightness': {\n kind: 'number',\n description: 'Brightness of the whole base map (1 = unchanged).',\n default: 1,\n range: { min: 0.5, max: 1.5, step: 0.05 },\n useCase: USE_CASE.grade,\n },\n 'grade.contrast': {\n kind: 'number',\n description: 'Contrast of the whole base map (1 = unchanged).',\n default: 1,\n range: { min: 0.5, max: 1.5, step: 0.05 },\n useCase: USE_CASE.grade,\n },\n 'grade.saturation': {\n kind: 'number',\n description: 'Colour saturation of the whole base map (1 = unchanged, 0 = greyscale).',\n default: 1,\n range: { min: 0, max: 1.5, step: 0.05 },\n useCase: USE_CASE.grade,\n },\n 'fog.intensity': {\n kind: 'number',\n description:\n '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.',\n default: 0,\n range: FRACTION,\n useCase: USE_CASE.fog,\n },\n 'fog.reach': {\n kind: 'number',\n description: 'How far the fog reaches in from the edges (1 = to the centre).',\n default: 0.4,\n range: FRACTION,\n useCase: USE_CASE.fog,\n },\n 'edgeBlur.intensity': {\n kind: 'number',\n description:\n '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.',\n default: 0,\n range: { min: 0, max: 30, step: 1 },\n useCase: USE_CASE.edgeBlur,\n },\n 'edgeBlur.reach': {\n kind: 'number',\n description: 'How far the edge blur reaches in from the edges (1 = to the centre).',\n default: 0.5,\n range: FRACTION,\n useCase: USE_CASE.edgeBlur,\n },\n 'depthOfField.intensity': {\n kind: 'number',\n description:\n '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.',\n default: 0,\n range: { min: 0, max: 30, step: 1 },\n useCase: USE_CASE.depthOfField,\n },\n 'depthOfField.focus': {\n kind: 'number',\n description:\n '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.',\n default: 0,\n range: FRACTION,\n useCase: USE_CASE.depthOfField,\n },\n 'depthOfField.band': {\n kind: 'number',\n description:\n '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.',\n default: 0.3,\n range: FRACTION,\n useCase: USE_CASE.depthOfField,\n },\n 'depthOfField.bokeh': {\n kind: 'number',\n description:\n '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.',\n default: 0.5,\n range: FRACTION,\n useCase: USE_CASE.depthOfField,\n },\n 'tint.color': {\n kind: 'color',\n description: 'A flat colour laid over the whole map.',\n default: '#1f2937',\n useCase: USE_CASE.tint,\n },\n 'tint.opacity': {\n kind: 'number',\n description: 'Opacity of the tint. 0 = off.',\n default: 0,\n range: FRACTION,\n useCase: USE_CASE.tint,\n },\n 'vignette.intensity': {\n kind: 'number',\n description: 'Darkening (negative) or lightening (positive) towards the edges. 0 = off.',\n default: 0,\n range: { min: -1, max: 1, step: 0.05 },\n useCase: USE_CASE.vignette,\n },\n 'vignette.reach': {\n kind: 'number',\n description: 'How far the vignette reaches in from the edges (1 = to the centre).',\n default: 0.45,\n range: FRACTION,\n useCase: USE_CASE.vignette,\n },\n} as const satisfies Record<string, EffectKnobDefinition>;\n\n/**\n * The id of an effect knob, as accepted by {@link MapEffects.set}.\n *\n * @group Map Effects\n */\nexport type EffectKnobId = keyof typeof effectKnobDefinitions;\n\n/**\n * Every effect knob id, in compositing order.\n *\n * @group Map Effects\n */\nexport const effectKnobIds = Object.keys(effectKnobDefinitions) as EffectKnobId[];\n\n/**\n * The value type an effect knob takes.\n *\n * @group Map Effects\n */\nexport type EffectKnobValueOf<ID extends EffectKnobId> = Extract<\n EffectKnobDefinition,\n { kind: (typeof effectKnobDefinitions)[ID]['kind'] }\n>['default'];\n","/**\n * The maths behind every effect, as pure functions: CSS filter strings and gradient descriptions the\n * live overlays use through `backdrop-filter`, and the same values the capture path feeds into a 2D\n * canvas. One implementation, two consumers — never a live/export pair that can drift.\n *\n * @module\n * @ignore\n */\n\nimport type { LightDark } from '@tomtom-org/maps-sdk/map';\nimport type { DefocusUniforms } from './gpu/defocusShader';\n\n// The threshold's usable ceiling: at 1 the contrast term would divide by zero.\nconst BLOOM_CUT_MAX = 0.92;\nconst BLOOM_SATURATE = 1.35;\n\nconst clamp = (value: number, min: number, max: number): number => Math.min(Math.max(value, min), max);\n\n// Knob values arrive validated against the catalogue, so this only guards the maths against a\n// caller reaching these functions directly.\nconst asFraction = (value: number): number => clamp(value, 0, 1);\n\n/**\n * The filter chain that turns the map underneath into its own glow: cut, spread, saturate.\n *\n * `brightness` and `contrast` together are the threshold, and the pair is exact: `contrast(c)`\n * scales each channel away from 0.5 and `brightness(b)` scales it from 0, so the two compose to\n * `(in·b − 0.5)·c + 0.5`. Asking that line to send `cut` to 0 and 1 to 1 solves to\n * `b = 1/(1 + cut)`, `c = (1 + cut)/(1 − cut)`. Per channel, not luminance, so a magenta road\n * blooms magenta instead of bleeding grey.\n *\n * @param scale - Device pixels per CSS pixel of the canvas the filter runs on, so the radius is in\n * CSS pixels wherever it is applied.\n * @ignore\n */\nexport const bloomFilter = (radius: number, threshold: number, scale = 1): string => {\n const cut = asFraction(threshold) * BLOOM_CUT_MAX;\n const brightness = 1 / (1 + cut);\n const contrast = (1 + cut) / (1 - cut);\n return `brightness(${brightness.toFixed(4)}) contrast(${contrast.toFixed(3)}) blur(${radius * scale}px) saturate(${BLOOM_SATURATE})`;\n};\n\n/**\n * The colour grade as a CSS filter, or an empty string when every term is at its neutral value.\n * @ignore\n */\nexport const gradeFilter = (brightness: number, contrast: number, saturation: number): string =>\n [\n brightness === 1 ? '' : `brightness(${brightness})`,\n contrast === 1 ? '' : `contrast(${contrast})`,\n saturation === 1 ? '' : `saturate(${saturation})`,\n ]\n .filter(Boolean)\n .join(' ');\n\n/**\n * Every rim effect — vignette, fog veil, mask — paints the same ellipse: the inner colour out to\n * where `reach` starts it, the outer colour at the corners. The capture path fills the same pair\n * onto a canvas gradient, so the two cannot drift.\n * @ignore\n */\nexport const rimGradient = (inner: string, outer: string, reach: number): string => {\n // Where the rim starts, as the percentage of the way from centre to corner.\n const innerStop = Math.round((1 - asFraction(reach)) * 100);\n return `radial-gradient(ellipse farthest-corner at center, ${inner} ${innerStop}%, ${outer} 100%)`;\n};\n\n/**\n * The radial gradient a vignette paints: transparent in the middle, darkening (negative intensity)\n * or lightening (positive) towards the corners.\n * @ignore\n */\nexport const vignetteGradient = (intensity: number, reach: number): string => {\n const { inner, outer } = vignetteStops(intensity);\n return rimGradient(inner, outer, reach);\n};\n\n/**\n * The two colours a vignette fades between: black towards the corners for a negative intensity,\n * white for a positive one. The capture path paints the same pair onto a canvas gradient.\n * @ignore\n */\nexport const vignetteStops = (intensity: number): { inner: string; outer: string } => {\n const rgb = intensity < 0 ? '0, 0, 0' : '255, 255, 255';\n return { inner: `rgba(${rgb}, 0)`, outer: `rgba(${rgb}, ${Math.min(Math.abs(intensity), 1)})` };\n};\n\n/**\n * A mask that keeps an effect to the rim of the view: transparent centre, opaque edges.\n * @ignore\n */\nexport const rimMask = (reach: number): string => rimGradient('transparent', 'black', reach);\n\n/** The camera a depth of field needs, and all it needs: two angles, in degrees. @ignore */\nexport type CameraAngles = { pitch: number; verticalFieldOfView: number };\n\n// A tangent runs away at 90°, and a pitch restored from a saved view is not the map's to promise.\nconst MAX_PITCH_DEGREES = 89;\n\n// The same guard on the other angle: a field of view of 0 or 180 has no half-tangent to take.\nconst MIN_FIELD_OF_VIEW_DEGREES = 1;\nconst MAX_FIELD_OF_VIEW_DEGREES = 175;\n\n// The most of the frame's depth the band may hold sharp. Half, so the knob's top end is a deep\n// field rather than a switched-off effect.\nconst MAX_BAND = 0.5;\n\nconst toRadians = (degrees: number) => (degrees * Math.PI) / 180;\n\n/**\n * **How much nearer the bottom of the frame is than the top**, in reciprocal distance normalised so\n * that 0 is the nearest ground in shot: 0 seen from straight above, 0.5 at a 45° tilt, past 1 once\n * the horizon is on screen.\n *\n * A ray through a row `y` of a plane seen by a perspective camera meets it at `1/z = 1/D − y·tanθ/D²`\n * — *affine in the row* — so one number describes the whole depth of the picture, and the viewport\n * cancels against the camera distance that defines it. Which is why this needs no depth buffer:\n * MapLibre hands out none, and for a plane none is necessary.\n *\n * A flat map is not a special case but `ramp = 0`: every part of the ground is the same distance\n * away, so there is no depth to be shallow in and the effect ends on its own.\n * @ignore\n */\nexport const depthRamp = ({ pitch, verticalFieldOfView }: CameraAngles): number => {\n const tilt = Math.tan(toRadians(clamp(pitch, 0, MAX_PITCH_DEGREES)));\n const spread = Math.tan(\n toRadians(clamp(verticalFieldOfView, MIN_FIELD_OF_VIEW_DEGREES, MAX_FIELD_OF_VIEW_DEGREES)) / 2,\n );\n const span = 2 * spread * tilt;\n return span / (1 + span / 2);\n};\n\n/**\n * The depth-of-field pass' uniforms for a camera, a set of knobs and a raster: the optics as\n * numbers, with no GPU in sight — which is what makes them testable, and they are the half most\n * likely to be quietly wrong.\n *\n * `scale` is device pixels per CSS pixel. **Only the blur takes it**, and that asymmetry is the\n * whole of how a capture matches the screen: a circle of confusion is a length, while the depth\n * ramp and the plane of focus are angles and ratios and read the same at any resolution.\n *\n * `undefined` when there is nothing to defocus — a map with no tilt has no depth to be shallow in,\n * so the geometry ends the effect rather than a check on the pitch.\n * @ignore\n */\nexport const defocusUniforms = (\n values: { focus: number; band: number; blurCssPx: number; bokeh: number },\n camera: CameraAngles,\n raster: { width: number; height: number; scale: number },\n): DefocusUniforms | undefined => {\n const ramp = depthRamp(camera);\n if (ramp <= 0 || values.blurCssPx <= 0) return undefined;\n\n return {\n uDepth: ramp,\n // Read across what the frame actually shows rather than to infinity: a gently tilted map\n // spans only the nearest part of that scale, and a knob whose top end landed beyond the top\n // of the picture would be dead at exactly the pitch a flight uses. Held at the horizon.\n uFocus: Math.min(asFraction(values.focus) * ramp, 1),\n uBand: asFraction(values.band) * MAX_BAND,\n uBlur: Math.max(values.blurCssPx, 0) * raster.scale,\n uBokeh: asFraction(values.bokeh),\n uTexel: [1 / Math.max(raster.width, 1), 1 / Math.max(raster.height, 1)],\n };\n};\n\n// How opaque the fog veil gets at the corners, per unit of intensity.\nconst FOG_VEIL_ALPHA = 0.65;\n\n// The fog's blur radius in CSS pixels: the softness the faintest fog already has, plus the span\n// intensity spends on top of it.\nconst FOG_BLUR_BASE_PX = 2;\nconst FOG_BLUR_SPAN_PX = 6;\n\n// The haze the fog veil is made of, and the grade under it, per light/dark theme of the map. Haze\n// takes the colour of the light in the air: pale over a daylit map, and the map's own darkness over\n// a dark one, where a pale veil would read as a bloom rather than as distance.\nconst FOG_BY_THEME: Record<LightDark, { rgb: string; grade: string }> = {\n light: { rgb: '238, 240, 244', grade: 'brightness(1.08) contrast(0.82) saturate(0.7)' },\n dark: { rgb: '16, 22, 33', grade: 'brightness(0.92) contrast(0.82) saturate(0.7)' },\n};\n\n/** Keeps blurred colours from greying out; used by both the live filter and the capture. @ignore */\nexport const DEFOCUS_SATURATE = 'saturate(1.08)';\n\n/**\n * The grade under the fog blur, which washes the rim out towards the colour of the air.\n * @ignore\n */\nexport const fogGradeFilter = (lightDark: LightDark): string => FOG_BY_THEME[lightDark].grade;\n\n/**\n * The fog's blur radius in CSS pixels for a given intensity.\n * @ignore\n */\nexport const fogBlurPx = (intensity: number): number => FOG_BLUR_BASE_PX + intensity * FOG_BLUR_SPAN_PX;\n\n/**\n * The fog: a blur plus a washed-out grade over the rim, under a veil that thickens outwards.\n * @ignore\n */\nexport const fogFilter = (intensity: number, lightDark: LightDark): string =>\n `blur(${fogBlurPx(intensity)}px) ${fogGradeFilter(lightDark)}`;\n\n/**\n * The two colours the fog veil fades between, for a given intensity.\n * @ignore\n */\nexport const fogVeilStops = (intensity: number, lightDark: LightDark): { inner: string; outer: string } => {\n const { rgb } = FOG_BY_THEME[lightDark];\n return { inner: `rgba(${rgb}, 0)`, outer: `rgba(${rgb}, ${intensity * FOG_VEIL_ALPHA})` };\n};\n\n/**\n * The veil over the fog blur, thickening towards the corners.\n * @ignore\n */\nexport const fogVeil = (intensity: number, reach: number, lightDark: LightDark): string => {\n const { inner, outer } = fogVeilStops(intensity, lightDark);\n return rimGradient(inner, outer, reach);\n};\n\n/**\n * The filter both defocusing effects use — edge blur and depth of field: blur alone, with a slight\n * saturation lift so blurred colours do not grey out. Unlike {@link fogFilter} it neither washes\n * what it blurs out nor lays a veil over it, so those pixels keep their colours and lose only their\n * detail. What differs between the two effects is the mask, not the filter.\n * @ignore\n */\nexport const defocusFilter = (blurCssPx: number): string => `blur(${blurCssPx}px) ${DEFOCUS_SATURATE}`;\n","/**\n * Composites the effects onto a 2D canvas that already holds the rendered map — the capture path.\n * It consumes the same filter maths as the live overlays (`effectFilters.ts`), so what is captured is\n * what was on screen.\n *\n * @module\n * @ignore\n */\n\nimport type { LightDark } from '@tomtom-org/maps-sdk/map';\nimport { applyScopeMask } from './bloomScope';\nimport {\n bloomFilter,\n DEFOCUS_SATURATE,\n fogBlurPx,\n fogGradeFilter,\n fogVeilStops,\n gradeFilter,\n vignetteStops,\n} from './effectFilters';\nimport type { EffectValues } from './effectValues';\n\ntype Context = CanvasRenderingContext2D;\n\n// A filtered copy of what `context` holds, on a canvas of its own — the capture equivalent of one\n// `backdrop-filter` overlay. Undefined when the browser gives no 2D context for the scratch canvas.\nconst filteredCopy = (context: Context, width: number, height: number, filter: string): Context | undefined => {\n const canvas = document.createElement('canvas');\n canvas.width = width;\n canvas.height = height;\n const copy = canvas.getContext('2d');\n if (!copy) return undefined;\n\n copy.filter = filter;\n copy.drawImage(context.canvas, 0, 0);\n copy.filter = 'none';\n return copy;\n};\n\n// Lays one such copy back over the capture, in the blend mode its live overlay uses.\nconst drawOver = (context: Context, layer: Context, operation: GlobalCompositeOperation, alpha = 1): void => {\n context.save();\n context.globalCompositeOperation = operation;\n context.globalAlpha = alpha;\n context.drawImage(layer.canvas, 0, 0);\n context.restore();\n};\n\n// Fills an elliptical radial gradient sized to the canvas aspect, like CSS's\n// `ellipse farthest-corner at center`.\nconst fillEllipticalRadial = (\n context: Context,\n width: number,\n height: number,\n reach: number,\n innerColor: string,\n outerColor: string,\n): void => {\n const radiusX = width / Math.SQRT2;\n const radiusY = height / Math.SQRT2;\n const gradient = context.createRadialGradient(0, 0, 1 - reach, 0, 0, 1);\n gradient.addColorStop(0, innerColor);\n gradient.addColorStop(1, outerColor);\n context.save();\n context.translate(width / 2, height / 2);\n context.scale(radiusX, radiusY);\n context.fillStyle = gradient;\n context.fillRect(-width / 2 / radiusX, -height / 2 / radiusY, width / radiusX, height / radiusY);\n context.restore();\n};\n\nconst TRANSPARENT = 'rgba(0,0,0,0)';\nconst OPAQUE = 'rgba(0,0,0,1)';\n\n// Blurs a copy of the canvas and keeps only where `paintMask` leaves it opaque — the capture\n// equivalent of a `backdrop-filter` overlay under a mask.\nconst applyMaskedBlur = (\n context: Context,\n width: number,\n height: number,\n scale: number,\n blurCssPx: number,\n extraFilter: string,\n paintMask: (target: Context) => void,\n): void => {\n const blurred = filteredCopy(context, width, height, `blur(${blurCssPx * scale}px) ${extraFilter}`.trim());\n if (!blurred) return;\n\n blurred.globalCompositeOperation = 'destination-in';\n paintMask(blurred);\n context.drawImage(blurred.canvas, 0, 0);\n};\n\n// Draws the map's own glow back over it, the way the live bloom canvas blends in `screen` mode. A\n// scope drops the pixels it does not cover first, as the live path does.\nconst applyBloom = (\n context: Context,\n width: number,\n height: number,\n scale: number,\n values: EffectValues,\n scopeMask: Uint8Array | undefined,\n): void => {\n const filter = bloomFilter(values['bloom.radius'], values['bloom.threshold'], scale);\n const source = scopeMask ? scopedCopy(context, width, height, scopeMask) : context;\n if (!source) return;\n\n const glow = filteredCopy(source, width, height, filter);\n if (!glow) return;\n\n drawOver(context, glow, 'screen', values['bloom.intensity']);\n};\n\n// The capture's pixels with everything outside the scope blacked out, on a canvas of its own —\n// `none` because this copy is the unfiltered one the threshold has yet to run on.\nconst scopedCopy = (context: Context, width: number, height: number, mask: Uint8Array): Context | undefined => {\n const copy = filteredCopy(context, width, height, 'none');\n if (copy) applyScopeMask(copy, mask);\n return copy;\n};\n\n// The colour grade replaces the pixels underneath, as a `backdrop-filter` overlay does.\nconst applyGrade = (context: Context, width: number, height: number, filter: string): void => {\n const graded = filteredCopy(context, width, height, filter);\n if (!graded) return;\n\n drawOver(context, graded, 'copy');\n};\n\n/**\n * Applies every active effect to `context`, in the same order the live overlays stack: bloom, grade,\n * fog, edge blur, tint, vignette — the overlays, that is; the depth of field is a GPU pass and has\n * already run into `context` by the time this is called. `scale` is device pixels per CSS pixel, so\n * blur radii stay in CSS pixels at any capture resolution.\n * @ignore\n */\nexport const compositeEffects = (\n context: Context,\n width: number,\n height: number,\n scale: number,\n values: EffectValues,\n lightDark: LightDark,\n scopeMask?: Uint8Array,\n): void => {\n if (values['bloom.intensity'] > 0) applyBloom(context, width, height, scale, values, scopeMask);\n\n const grade = gradeFilter(values['grade.brightness'], values['grade.contrast'], values['grade.saturation']);\n if (grade) applyGrade(context, width, height, grade);\n\n const rimMaskOf = (reach: number) => (target: Context) =>\n fillEllipticalRadial(target, width, height, reach, TRANSPARENT, OPAQUE);\n\n if (values['fog.intensity'] > 0) {\n const intensity = values['fog.intensity'];\n const reach = values['fog.reach'];\n const { inner, outer } = fogVeilStops(intensity, lightDark);\n const blur = fogBlurPx(intensity);\n applyMaskedBlur(context, width, height, scale, blur, fogGradeFilter(lightDark), rimMaskOf(reach));\n fillEllipticalRadial(context, width, height, reach, inner, outer);\n }\n if (values['edgeBlur.intensity'] > 0) {\n const blur = values['edgeBlur.intensity'];\n applyMaskedBlur(context, width, height, scale, blur, DEFOCUS_SATURATE, rimMaskOf(values['edgeBlur.reach']));\n }\n if (values['tint.opacity'] > 0) {\n context.save();\n context.globalAlpha = values['tint.opacity'];\n context.fillStyle = values['tint.color'];\n context.fillRect(0, 0, width, height);\n context.restore();\n }\n if (values['vignette.intensity'] !== 0) {\n const { inner, outer } = vignetteStops(values['vignette.intensity']);\n fillEllipticalRadial(context, width, height, values['vignette.reach'], inner, outer);\n }\n};\n","import { type EffectKnobId, type EffectKnobValueOf, effectKnobDefinitions, effectKnobIds } from './effectsCatalogue';\n\n/**\n * Every effect knob with a value: the settings with the catalogue defaults filled in.\n * @ignore\n */\nexport type EffectValues = { [ID in EffectKnobId]: EffectKnobValueOf<ID> };\n\n/**\n * @ignore\n */\nexport const withDefaults = (settings: Partial<EffectValues>): EffectValues =>\n Object.fromEntries(\n effectKnobIds.map((id) => [id, settings[id] ?? effectKnobDefinitions[id].default]),\n ) as EffectValues;\n","/**\n * The depth-of-field pass' contract with the substrate: the program, and the uniform names the\n * stage binds into it.\n *\n * The optics are worked out in `effectFilters.ts` and arrive here as uniforms; the program itself\n * is `glsl/defocus.frag.glsl`, a real GLSL file loaded with Vite's `?raw` rather than a template\n * literal — which is what keeps a backtick inside a shader comment a character rather than the end\n * of the program.\n *\n * @module\n * @ignore\n */\n\nimport defocusFragment from './glsl/defocus.frag.glsl?raw';\n\n/** The uniform names the program declares, so the stage and the tests agree on one spelling. */\nexport type DefocusUniforms = {\n /** How much nearer the bottom of the frame is than the top, in normalised reciprocal distance. */\n uDepth: number;\n /** The plane of focus, on that same scale. */\n uFocus: number;\n /** How much of the scale either side of it stays sharp. */\n uBand: number;\n /** The widest circle of confusion, in device pixels — a diameter, as a lens' is. */\n uBlur: number;\n /** How far a bright sample may outweigh a dull one: the difference between a defocus and a bokeh. */\n uBokeh: number;\n /** 1 / the resolution being rendered, so an offset in device pixels is a UV step. */\n uTexel: readonly [number, number];\n};\n\nexport const DEFOCUS_SHADER: string = defocusFragment;\n","/**\n * The WebGL2 substrate the pixel-derived effects run on: one context, one full-screen triangle, and\n * a pair of render targets a chain of passes ping-pongs between. No effect lives here — a pass\n * brings its own fragment shader and its own uniforms, and this module is what stops two of them\n * being two implementations of context creation, texture upload, resizing and context loss.\n *\n * **One context for the plugin, and that is a requirement rather than a saving.** A browser caps\n * how many live WebGL contexts a page may hold, and the map itself already holds one.\n *\n * The two shaders it owns — the triangle's vertex half and the preamble every fragment stage is\n * composed onto — are real GLSL files under `glsl/`, loaded with Vite's `?raw`, as every shader in\n * this plugin is: a backtick inside a template literal's comment closes the literal, and the\n * program it silently truncates still compiles to a blank picture.\n *\n * @module\n * @ignore\n */\n\nimport FRAGMENT_PREAMBLE from './glsl/fragmentPreamble.glsl?raw';\nimport VERTEX_SHADER from './glsl/screenPass.vert.glsl?raw';\n\n/**\n * One link of the chain: a fragment shader, the uniforms it reads, and whether it samples levels\n * other than 0 — which is what makes the source worth a mip chain.\n * @ignore\n */\nexport type PassStage = {\n fragment: string;\n uniforms: Record<string, number | readonly [number, number]>;\n mipmapped?: boolean;\n};\n\ntype RenderTarget = { texture: WebGLTexture; framebuffer: WebGLFramebuffer };\n\n// How every texture in the chain is sampled at level 0: clamped, so a tap that runs off the edge of\n// a disc reads the edge rather than wrapping, and linear, so it reads between texels.\nconst clampAndFilter = (gl: WebGL2RenderingContext): void => {\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n};\n\n/**\n * Sizes a canvas that is drawn every frame. Assigning either dimension clears the canvas and\n * reallocates its bitmap even when the value is unchanged, so this only assigns when the size moved.\n * @ignore\n */\nexport const resizeCanvas = (canvas: HTMLCanvasElement, width: number, height: number): void => {\n if (canvas.width === width && canvas.height === height) return;\n\n canvas.width = width;\n canvas.height = height;\n};\n\nconst compile = (gl: WebGL2RenderingContext, type: number, source: string): WebGLShader | undefined => {\n const shader = gl.createShader(type);\n if (!shader) return undefined;\n\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) return shader;\n\n console.warn(`[MapEffects] shader failed to compile: ${gl.getShaderInfoLog(shader)}`);\n gl.deleteShader(shader);\n return undefined;\n};\n\n/**\n * Renders a chain of full-screen passes over a source canvas, onto a canvas of its own.\n * @ignore\n */\nexport class ScreenPassRenderer {\n /** The canvas the chain draws into — the one a caller shows, or reads back. */\n readonly canvas: HTMLCanvasElement;\n private readonly gl: WebGL2RenderingContext | undefined;\n private readonly programs = new Map<string, WebGLProgram>();\n private sourceTexture: WebGLTexture | undefined;\n private targets: RenderTarget[] = [];\n private targetSize = { width: 0, height: 0 };\n private lost = false;\n\n constructor() {\n this.canvas = document.createElement('canvas');\n this.canvas.addEventListener('webglcontextlost', (event) => {\n // Without this the context never comes back, and nor does the picture.\n event.preventDefault();\n this.lost = true;\n });\n this.canvas.addEventListener('webglcontextrestored', () => {\n this.programs.clear();\n this.sourceTexture = undefined;\n this.targets = [];\n this.targetSize = { width: 0, height: 0 };\n this.lost = false;\n });\n this.gl =\n this.canvas.getContext('webgl2', {\n alpha: true,\n antialias: false,\n premultipliedAlpha: false,\n preserveDrawingBuffer: true,\n }) ?? undefined;\n }\n\n /**\n * Runs `stages` over `source`, in order, and returns the canvas holding the result — or\n * `undefined` when there is nothing to run or no context to run it on.\n */\n render(source: HTMLCanvasElement, stages: PassStage[]): HTMLCanvasElement | undefined {\n const gl = this.gl;\n if (!gl || this.lost || !stages.length || !source.width || !source.height) return undefined;\n\n resizeCanvas(this.canvas, source.width, source.height);\n let input = this.uploadSource(source, stages[0].mipmapped);\n // Without somewhere to put a pass' result, a chain would run every stage over the source\n // and show only the last — a wrong picture, where no picture at least says so.\n if (!input || (stages.length > 1 && !this.holdTargets(gl))) return undefined;\n\n for (let index = 0; index < stages.length; index++) {\n // The last pass paints the canvas the caller shows; the ones before it paint a target\n // the next pass reads, so the picture never leaves the GPU between them.\n const target = index === stages.length - 1 ? undefined : this.targets[index % this.targets.length];\n this.runStage(gl, stages[index], input, target);\n if (!target) continue;\n\n input = target.texture;\n if (stages[index + 1].mipmapped) this.mipmap(gl, input);\n }\n return this.canvas;\n }\n\n /** Releases the context's resources; the canvas is the caller's to remove. */\n remove(): void {\n this.gl?.getExtension('WEBGL_lose_context')?.loseContext();\n }\n\n /**\n * The pair of targets the chain ping-pongs between, at the current size — kept rather than\n * made per frame, since a chain runs on every map frame and a texture allocation does not\n * belong there. `false` when the context would not give us one.\n */\n private holdTargets(gl: WebGL2RenderingContext): boolean {\n const { width, height } = this.canvas;\n if (this.targets.length === 2 && this.targetSize.width === width && this.targetSize.height === height) {\n return true;\n }\n\n this.releaseTargets(gl);\n while (this.targets.length < 2) {\n const texture = gl.createTexture();\n const framebuffer = gl.createFramebuffer();\n if (!texture || !framebuffer) return false;\n\n gl.bindTexture(gl.TEXTURE_2D, texture);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);\n clampAndFilter(gl);\n gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);\n gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);\n this.targets.push({ texture, framebuffer });\n }\n this.targetSize = { width, height };\n return true;\n }\n\n private releaseTargets(gl: WebGL2RenderingContext): void {\n for (const { texture, framebuffer } of this.targets) {\n gl.deleteTexture(texture);\n gl.deleteFramebuffer(framebuffer);\n }\n this.targets = [];\n }\n\n // A gather reads the level whose texels are as wide as the gaps between its taps, so a stage\n // that samples levels needs them built on whatever it is about to read — target or source.\n private mipmap(gl: WebGL2RenderingContext, texture: WebGLTexture): void {\n gl.bindTexture(gl.TEXTURE_2D, texture);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);\n gl.generateMipmap(gl.TEXTURE_2D);\n }\n\n // The map's pixels as a texture, or `undefined` where the context would not give us one.\n private uploadSource(source: HTMLCanvasElement, mipmapped = false): WebGLTexture | undefined {\n const gl = this.gl;\n if (!gl) return undefined;\n\n this.sourceTexture ??= gl.createTexture() ?? undefined;\n if (!this.sourceTexture) return undefined;\n\n gl.bindTexture(gl.TEXTURE_2D, this.sourceTexture);\n gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);\n clampAndFilter(gl);\n if (mipmapped) this.mipmap(gl, this.sourceTexture);\n return this.sourceTexture;\n }\n\n private runStage(\n gl: WebGL2RenderingContext,\n stage: PassStage,\n input: WebGLTexture,\n target: RenderTarget | undefined,\n ): void {\n const program = this.programOf(gl, stage.fragment);\n if (!program) return;\n\n gl.bindFramebuffer(gl.FRAMEBUFFER, target?.framebuffer ?? null);\n gl.viewport(0, 0, this.canvas.width, this.canvas.height);\n gl.useProgram(program);\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, input);\n gl.uniform1i(gl.getUniformLocation(program, 'uSource'), 0);\n for (const [name, value] of Object.entries(stage.uniforms)) {\n const location = gl.getUniformLocation(program, name);\n if (!location) continue;\n\n if (typeof value === 'number') {\n gl.uniform1f(location, value);\n } else {\n gl.uniform2f(location, value[0], value[1]);\n }\n }\n gl.drawArrays(gl.TRIANGLES, 0, 3);\n }\n\n private programOf(gl: WebGL2RenderingContext, fragment: string): WebGLProgram | undefined {\n const cached = this.programs.get(fragment);\n if (cached) return cached;\n\n const vertexShader = compile(gl, gl.VERTEX_SHADER, VERTEX_SHADER);\n const fragmentShader = compile(gl, gl.FRAGMENT_SHADER, `${FRAGMENT_PREAMBLE}${fragment}`);\n const program = vertexShader && fragmentShader ? gl.createProgram() : undefined;\n if (!program || !vertexShader || !fragmentShader) return undefined;\n\n gl.attachShader(program, vertexShader);\n gl.attachShader(program, fragmentShader);\n gl.linkProgram(program);\n gl.deleteShader(vertexShader);\n gl.deleteShader(fragmentShader);\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n console.warn(`[MapEffects] shader failed to link: ${gl.getProgramInfoLog(program)}`);\n return undefined;\n }\n\n this.programs.set(fragment, program);\n return program;\n }\n}\n","import { type KnobDescriptor, knob, StylingModule, type TomTomMap } from '@tomtom-org/maps-sdk/map';\nimport {\n applyScopeMask,\n type BloomScope,\n buildScopeMask,\n invalidScopeEntry,\n scopeColors,\n scopeReadsStyling,\n} from './bloomScope';\nimport { compositeEffects } from './composite';\nimport {\n bloomFilter,\n type CameraAngles,\n defocusFilter,\n defocusUniforms,\n fogFilter,\n fogVeil,\n gradeFilter,\n rimMask,\n vignetteGradient,\n} from './effectFilters';\nimport {\n type EffectKnobDefinition,\n type EffectKnobId,\n type EffectKnobRange,\n type EffectKnobValueOf,\n effectKnobDefinitions,\n effectKnobIds,\n} from './effectsCatalogue';\nimport type { EffectValues } from './effectValues';\nimport { withDefaults } from './effectValues';\nimport { DEFOCUS_SHADER } from './gpu/defocusShader';\nimport type { PassStage } from './gpu/screenPass';\nimport { resizeCanvas, ScreenPassRenderer } from './gpu/screenPass';\n\n/**\n * The effect knob values an app sets — only the overridden ones, serializable like the SDK's\n * styling settings.\n *\n * @group Map Effects\n */\nexport type MapEffectsSettings = { [ID in EffectKnobId]?: EffectKnobValueOf<ID> };\n\n/**\n * One entry of the effects catalogue: the SDK's shared {@link KnobDescriptor}, narrowed the way\n * `StylingKnobDescriptor` narrows it, plus the use case the effect serves.\n *\n * @group Map Effects\n */\nexport type MapEffectsKnobDescriptor = KnobDescriptor<EffectKnobValueOf<EffectKnobId>> & {\n /** The knob's id, as accepted by {@link MapEffects.set}. */\n id: EffectKnobId;\n /** The range of valid values, for a numeric knob. */\n range?: EffectKnobRange;\n /** Always `true`: an effect reads the rendered pixels, so it has something to work on anywhere. */\n available: boolean;\n /** Always `any-style`, for the same reason. */\n appliesTo: string;\n /** The practical problem the effect solves. */\n useCase: string;\n};\n\n/**\n * The effects catalogue, in the same shape as `StylingModule.describe()` so one control panel or\n * agent tool can drive both.\n *\n * @group Map Effects\n */\nexport type MapEffectsCatalogue = { knobs: MapEffectsKnobDescriptor[] };\n\n/**\n * Options for {@link MapEffects.capture}.\n *\n * @group Map Effects\n */\nexport type CaptureOptions = {\n /**\n * Device pixels per CSS pixel to render at. Defaults to the map's current ratio; `2` or `3` gives\n * a print-grade capture of the same view. MapLibre clamps to its `maxCanvasSize`, so measure the\n * ratio you got from the returned canvas rather than assuming.\n */\n pixelRatio?: number;\n};\n\n// Overlay layers in compositing order; each is one absolutely positioned element over the canvas.\n// The GPU chain is not among them: it renders the picture the rest of these read, so it sits under\n// all of them — see `artworkCanvas`.\nconst OVERLAYS = ['bloom', 'grade', 'fog', 'edgeBlur', 'tint', 'vignette'] as const;\ntype OverlayName = (typeof OVERLAYS)[number];\n\n// Safari still needs `-webkit-backdrop-filter`, which the DOM typings do not carry.\ntype OverlayStyle = Partial<CSSStyleDeclaration> & { webkitBackdropFilter?: string };\n\n// A filter and a mask each go on under both the standard property and the prefixed one.\nconst backdropFilterStyle = (filter: string): OverlayStyle => ({\n backdropFilter: filter,\n webkitBackdropFilter: filter,\n});\nconst maskStyle = (mask: string): OverlayStyle => ({ maskImage: mask, webkitMaskImage: mask });\n\n// Every layer the plugin hangs over the map — the overlays and the chain's own canvas — covers the\n// container exactly, takes no pointer events, and starts hidden.\nconst LAYER_STYLE = {\n position: 'absolute',\n inset: '0',\n width: '100%',\n height: '100%',\n pointerEvents: 'none',\n display: 'none',\n} satisfies Partial<CSSStyleDeclaration>;\n\n// Device pixels per CSS pixel of a canvas — what keeps a radius in CSS pixels wherever it is used.\nconst rasterScaleOf = (canvas: HTMLCanvasElement): number => canvas.width / Math.max(canvas.clientWidth, 1);\n\n/**\n * Coalesces however many requests arrive into one `draw` per animation frame. Both the chain and\n * the bloom are made out of the map's pixels and follow its `render` event, which fires more often\n * than a frame can be drawn.\n */\nconst drawOncePerFrame = (draw: () => void) => {\n let frame: number | undefined;\n return {\n schedule: () => {\n if (frame !== undefined) return;\n\n frame = requestAnimationFrame(() => {\n frame = undefined;\n draw();\n });\n },\n cancel: () => {\n if (frame !== undefined) cancelAnimationFrame(frame);\n frame = undefined;\n },\n };\n};\n\n// The bloom canvas renders at a quarter of the pixels; its blur radius is divided to match.\nconst BLOOM_SCALE = 0.5;\n\n// Below this difference a requested capture ratio is the one the map already renders at.\nconst RATIO_EPSILON = 0.01;\n\n// `setPixelRatio(null)` is MapLibre's \"follow the device again\", which its typings spell as a number\n// only — and a number would pin an override instead.\ntype PixelRatioSetter = { setPixelRatio(pixelRatio: number | null): void };\n\n/**\n * Practical post-processing for a {@link TomTomMap}: effects computed from the rendered map's own\n * pixels, not from its style — so they work on every style, custom ones included, survive every\n * style release, and cannot be reproduced by editing the style at all.\n *\n * @remarks\n * Every effect is named by the problem it solves, and off by default:\n * - **bloom** — emphasis on dark and driving styles: lit roads, traffic tubes and route lines glow;\n * - **grade** (brightness, contrast, saturation) and **tint** — muting the base map so a data overlay\n * reads first;\n * - **fog** — depth: the rim is blurred *and* washed towards the colour of the air, which follows the\n * map's light/dark theme;\n * - **edge blur** — focus: the rim is blurred and keeps its colour, so it reads as a lens rather than\n * as distance;\n * - **depth of field** — depth on a tilted map: one band of *distance* stays sharp and the rest\n * defocuses, worked out from the camera's pitch and field of view, so a flat map ends it;\n * - **vignette** — focus too, by darkening or lightening the edges.\n *\n * Effects render live as layers over the map canvas (a WebGL2 chain under `backdrop-filter`\n * overlays; bloom is a small canvas blended in `screen` mode) and are composited the same way into\n * {@link capture}, so what is captured is what was on screen. Bloom, the depth of field and capture\n * read the map canvas, which needs\n * `mapLibre: { canvasContextAttributes: { preserveDrawingBuffer: true } }` when creating the map;\n * the plugin warns once when it is missing.\n *\n * {@link describe} returns the knobs in the same shape as `StylingModule.describe()`, so a control\n * panel or an agent tool built for the SDK's styling catalogue reads this one unchanged.\n *\n * @example\n * ```typescript\n * import { MapEffects } from '@tomtom-org/maps-sdk-plugin-map-effects';\n *\n * const effects = new MapEffects(map);\n * // The data-viz muting half: a dim, desaturated base under your own layers.\n * effects.set({ 'grade.saturation': 0.4, 'grade.brightness': 0.85, 'tint.opacity': 0.15 });\n * // Night-driving emphasis on a dark style:\n * effects.set({ 'bloom.intensity': 0.5, 'bloom.threshold': 0.6 });\n *\n * const poster = await effects.capture({ pixelRatio: 3 });\n * ```\n *\n * @group Map Effects\n */\nexport class MapEffects {\n private settings: MapEffectsSettings = {};\n private readonly container: HTMLDivElement;\n private readonly overlays: Record<OverlayName, HTMLElement>;\n private readonly bloomCanvas: HTMLCanvasElement;\n private readonly artworkCanvas: HTMLCanvasElement;\n private readonly chain = new ScreenPassRenderer();\n private readonly chainDraw = drawOncePerFrame(() => this.drawChain());\n private readonly bloomDraw = drawOncePerFrame(() => this.drawBloom());\n // What a scope's named entries resolve through, requested only once a scope names one. The SDK\n // memoizes it per map, so it is the instance the app styles the map with.\n private styling: StylingModule | undefined;\n private stylingRequest: Promise<void> | undefined;\n // Keyed on the scope's entries, since `set` replaces the array even when it is unchanged.\n private scopeMask: { key: string; tolerance: number; mask: Uint8Array | undefined } | undefined;\n private scopeCanvas: HTMLCanvasElement | undefined;\n private warnedBuffer = false;\n private warnedScope = false;\n // A style switch or a `StylingModule` recolour moves the colours a scope resolves to.\n private readonly onStyleData = () => this.dropScopeMask();\n private readonly onRender = () => {\n this.chainDraw.schedule();\n this.scheduleBloom();\n };\n\n /**\n * Attaches the effects to a map. Nothing is visible until {@link set} turns an effect on.\n *\n * @param map - The map to render effects over.\n * @param settings - Effect knob values to apply right away.\n */\n constructor(\n private readonly map: TomTomMap,\n settings: MapEffectsSettings = {},\n ) {\n const mapContainer = this.map.mapLibreMap.getContainer();\n this.container = document.createElement('div');\n this.container.className = 'tomtom-map-effects';\n Object.assign(this.container.style, {\n position: 'absolute',\n inset: '0',\n pointerEvents: 'none',\n overflow: 'hidden',\n } satisfies Partial<CSSStyleDeclaration>);\n // Right after the canvas container, so the map's own controls stay above the effects.\n const canvasContainer = mapContainer.querySelector('.maplibregl-canvas-container');\n canvasContainer?.insertAdjacentElement('afterend', this.container) ?? mapContainer.appendChild(this.container);\n\n this.bloomCanvas = document.createElement('canvas');\n this.overlays = Object.fromEntries(\n OVERLAYS.map((name) => {\n const element = name === 'bloom' ? this.bloomCanvas : document.createElement('div');\n element.dataset.effect = name;\n Object.assign(element.style, LAYER_STYLE);\n this.container.appendChild(element);\n return [name, element];\n }),\n ) as unknown as Record<OverlayName, HTMLElement>;\n this.bloomCanvas.style.mixBlendMode = 'screen';\n\n // The chain's own canvas goes under every overlay: it replaces the picture rather than\n // filtering it, so the `backdrop-filter` layers above read its result and not the map's.\n this.artworkCanvas = this.chain.canvas;\n this.artworkCanvas.dataset.effect = 'artwork';\n Object.assign(this.artworkCanvas.style, LAYER_STYLE);\n this.container.insertAdjacentElement('afterbegin', this.artworkCanvas);\n\n this.map.mapLibreMap.on('render', this.onRender);\n this.map.mapLibreMap.on('styledata', this.onStyleData);\n this.set(settings);\n }\n\n // The two angles a depth of field is a function of; nothing else about the camera changes it.\n private cameraAngles(): CameraAngles {\n const mapLibreMap = this.map.mapLibreMap;\n return { pitch: mapLibreMap.getPitch(), verticalFieldOfView: mapLibreMap.getVerticalFieldOfView() };\n }\n\n /**\n * Sets one or more effect knobs. Values are validated against the catalogue before anything is\n * touched; knobs not mentioned keep their values.\n *\n * @param settings - Knob id → value.\n * @throws `RangeError` for a value outside its range or of the wrong kind; `Error` for an unknown id.\n *\n * @example\n * ```typescript\n * effects.set({ 'vignette.intensity': -0.4 });\n * ```\n */\n set(settings: MapEffectsSettings): void {\n for (const [id, value] of Object.entries(settings)) {\n this.validate(id, value);\n }\n this.settings = { ...this.settings, ...settings };\n this.render();\n }\n\n /**\n * The current value of a knob: the override, or the catalogue default.\n */\n get<ID extends EffectKnobId>(id: ID): EffectKnobValueOf<ID> {\n this.assertKnob(id);\n return (this.settings[id] ?? effectKnobDefinitions[id].default) as EffectKnobValueOf<ID>;\n }\n\n /**\n * Turns a knob (or every knob) back to its default — for most effects, off.\n */\n reset(id?: EffectKnobId): void {\n if (id === undefined) {\n this.settings = {};\n } else {\n this.assertKnob(id);\n const { [id]: _removed, ...rest } = this.settings;\n this.settings = rest;\n }\n this.render();\n }\n\n /**\n * The overridden knobs, as a plain serializable object.\n */\n getConfig(): MapEffectsSettings {\n return { ...this.settings };\n }\n\n /**\n * Replaces every knob with the given settings (an empty object turns everything off).\n */\n applyConfig(settings: MapEffectsSettings): void {\n this.settings = {};\n this.set(settings);\n }\n\n /**\n * The effects catalogue: every knob with kind, range, default, current value and the practical\n * use case it serves. Same descriptor shape as `StylingModule.describe()`.\n */\n describe(): MapEffectsCatalogue {\n return {\n knobs: effectKnobIds.map((id): MapEffectsKnobDescriptor => {\n const definition = effectKnobDefinitions[id];\n return knob(id, definition.kind, definition.description, definition.default, this.settings[id], {\n ...('range' in definition && { range: definition.range }),\n ...('options' in definition && { options: definition.options }),\n available: true,\n appliesTo: 'any-style',\n useCase: definition.useCase,\n });\n }),\n };\n }\n\n /**\n * Renders the current view — map and effects — into a canvas, optionally at a higher pixel ratio\n * for print. The map is re-rendered at that ratio (never resampled), read back once every tile is\n * drawn, and restored to follow the device afterwards.\n *\n * @remarks\n * Needs `preserveDrawingBuffer: true` on the map's canvas context; without it the map half of the\n * capture is blank. Viewers cannot download from a sandboxed page, so hand the canvas to your own\n * upload or `toBlob` flow.\n */\n async capture(options: CaptureOptions = {}): Promise<HTMLCanvasElement> {\n this.warnIfBufferNotPreserved();\n const mapLibreMap = this.map.mapLibreMap;\n const ratioSetter: PixelRatioSetter = mapLibreMap;\n const live = mapLibreMap.getPixelRatio();\n const wanted = options.pixelRatio ?? live;\n const wasOverridden = live !== (globalThis.devicePixelRatio || 1);\n const rerenders = Math.abs(wanted - live) > RATIO_EPSILON;\n if (rerenders) {\n ratioSetter.setPixelRatio(wanted);\n await this.settled();\n }\n try {\n return await this.readBack();\n } finally {\n if (rerenders) ratioSetter.setPixelRatio(wasOverridden ? live : null);\n }\n }\n\n /**\n * Detaches the effects from the map, removes their elements and releases the WebGL context the\n * chain holds — which a page with several maps on it needs, since a browser caps how many it\n * may keep alive.\n */\n remove(): void {\n this.map.mapLibreMap.off('render', this.onRender);\n this.map.mapLibreMap.off('styledata', this.onStyleData);\n this.bloomDraw.cancel();\n this.chainDraw.cancel();\n this.chain.remove();\n this.container.remove();\n }\n\n // ── Live rendering ────────────────────────────────────────────────────────────────────────\n\n private render(): void {\n const values = withDefaults(this.settings);\n const show = (name: OverlayName, on: boolean, style: OverlayStyle = {}) => {\n const element = this.overlays[name];\n element.style.display = on ? 'block' : 'none';\n Object.assign(element.style, style);\n };\n\n const grade = gradeFilter(values['grade.brightness'], values['grade.contrast'], values['grade.saturation']);\n show('grade', grade !== '', backdropFilterStyle(grade));\n\n const lightDark = this.map.styleLightDarkTheme;\n show('fog', values['fog.intensity'] > 0, {\n ...backdropFilterStyle(fogFilter(values['fog.intensity'], lightDark)),\n ...maskStyle(rimMask(values['fog.reach'])),\n backgroundImage: fogVeil(values['fog.intensity'], values['fog.reach'], lightDark),\n });\n\n show('edgeBlur', values['edgeBlur.intensity'] > 0, {\n ...backdropFilterStyle(defocusFilter(values['edgeBlur.intensity'])),\n ...maskStyle(rimMask(values['edgeBlur.reach'])),\n });\n\n if (values['depthOfField.intensity'] > 0) this.warnIfBufferNotPreserved();\n this.drawChain(values);\n\n show('tint', values['tint.opacity'] > 0, {\n backgroundColor: values['tint.color'],\n opacity: String(values['tint.opacity']),\n });\n\n show('vignette', values['vignette.intensity'] !== 0, {\n backgroundImage: vignetteGradient(values['vignette.intensity'], values['vignette.reach']),\n });\n\n const bloomOn = values['bloom.intensity'] > 0;\n show('bloom', bloomOn, { opacity: String(values['bloom.intensity']) });\n if (bloomOn) {\n this.warnIfBufferNotPreserved();\n this.scheduleBloom();\n }\n }\n\n /**\n * The passes the chain runs over the map's pixels, in order. One so far — the defocus — but the\n * substrate ping-pongs between render targets, so a second reads what the first left without\n * the picture leaving the GPU.\n */\n private chainStages(values: EffectValues): PassStage[] {\n const source = this.map.mapLibreMap.getCanvas();\n const uniforms = defocusUniforms(\n {\n focus: values['depthOfField.focus'],\n band: values['depthOfField.band'],\n blurCssPx: values['depthOfField.intensity'],\n bokeh: values['depthOfField.bokeh'],\n },\n this.cameraAngles(),\n { width: source.width, height: source.height, scale: rasterScaleOf(source) },\n );\n return uniforms ? [{ fragment: DEFOCUS_SHADER, uniforms, mipmapped: true }] : [];\n }\n\n private drawChain(values: EffectValues = withDefaults(this.settings)): void {\n const stages = this.chainStages(values);\n const rendered = stages.length ? this.chain.render(this.map.mapLibreMap.getCanvas(), stages) : undefined;\n this.artworkCanvas.style.display = rendered ? 'block' : 'none';\n }\n\n private scheduleBloom(): void {\n if (this.get('bloom.intensity') > 0) this.bloomDraw.schedule();\n }\n\n private drawBloom(): void {\n // The finished artwork when the chain drew one, so a defocused highlight glows as the wide\n // soft disc a photograph gives it rather than as the pinpoint it was before the lens.\n const source =\n this.artworkCanvas.style.display === 'block' ? this.artworkCanvas : this.map.mapLibreMap.getCanvas();\n const width = Math.max(1, Math.round(source.width * BLOOM_SCALE));\n const height = Math.max(1, Math.round(source.height * BLOOM_SCALE));\n resizeCanvas(this.bloomCanvas, width, height);\n const context = this.bloomCanvas.getContext('2d');\n if (!context) return;\n\n // The scope drops its uncovered pixels before the threshold, so the blur spreads only its colours.\n const glowSource = this.scopedSource(source, width, height) ?? source;\n context.filter = bloomFilter(this.get('bloom.radius'), this.get('bloom.threshold'), BLOOM_SCALE);\n context.clearRect(0, 0, width, height);\n context.drawImage(glowSource, 0, 0, width, height);\n }\n\n // The map's pixels with everything outside the scope blacked out; undefined when bloom lights the whole frame.\n private scopedSource(source: HTMLCanvasElement, width: number, height: number): HTMLCanvasElement | undefined {\n const mask = this.currentScopeMask();\n if (!mask) return undefined;\n\n this.scopeCanvas ??= document.createElement('canvas');\n const canvas = this.scopeCanvas;\n resizeCanvas(canvas, width, height);\n const context = canvas.getContext('2d', { willReadFrequently: true });\n if (!context) return undefined;\n\n context.clearRect(0, 0, width, height);\n context.drawImage(source, 0, 0, width, height);\n applyScopeMask(context, mask);\n return canvas;\n }\n\n // Cached: resolving the colours reads the styling catalogue and building the cube visits 32³ colours.\n private currentScopeMask(): Uint8Array | undefined {\n const scope = this.get('bloom.only');\n if (scope.length === 0) return undefined;\n\n const key = scope.join('\\n');\n const tolerance = this.get('bloom.onlyTolerance');\n if (this.scopeMask?.key !== key || this.scopeMask.tolerance !== tolerance) {\n this.scopeMask = { key, tolerance, mask: this.buildScopeMaskFor(scope, tolerance) };\n }\n return this.scopeMask.mask;\n }\n\n private buildScopeMaskFor(scope: BloomScope, tolerance: number): Uint8Array | undefined {\n const readsStyling = scopeReadsStyling(scope);\n if (readsStyling) this.requestStyling();\n\n const colors = scopeColors(this.styling, scope);\n if (colors.length === 0 && (!readsStyling || this.styling)) this.warnScopeEmpty(scope);\n return buildScopeMask(colors, tolerance);\n }\n\n // An empty scope lights the whole frame, which passes for a working one. The caller skips the\n // warning while the styling module is still on its way.\n private warnScopeEmpty(scope: BloomScope): void {\n if (this.warnedScope) return;\n\n this.warnedScope = true;\n console.warn(\n `[MapEffects] 'bloom.only: ${scope.join(', ')}' found no colour to key on in the loaded style, so bloom is lighting the whole frame.`,\n );\n }\n\n // Until the module arrives, named entries key on nothing and bloom lights the whole frame.\n private requestStyling(): void {\n this.stylingRequest ??= StylingModule.get(this.map).then((styling) => {\n this.styling = styling;\n this.dropScopeMask();\n });\n }\n\n // The redraw is required: an idle map fires no `render`, so the stale glow would stay until the map moved.\n private dropScopeMask(): void {\n this.scopeMask = undefined;\n this.scheduleBloom();\n }\n\n // ── Capture ───────────────────────────────────────────────────────────────────────────────\n\n private readBack(): Promise<HTMLCanvasElement> {\n const mapLibreMap = this.map.mapLibreMap;\n return new Promise((resolve) => {\n mapLibreMap.once('render', () => {\n const source = mapLibreMap.getCanvas();\n const values = withDefaults(this.settings);\n const composite = document.createElement('canvas');\n composite.width = source.width;\n composite.height = source.height;\n const context = composite.getContext('2d');\n if (context) {\n // The same chain the screen is showing, re-run at the capture's own raster —\n // one implementation rather than a live half and an export half.\n const stages = this.chainStages(values);\n context.drawImage(this.chain.render(source, stages) ?? source, 0, 0);\n compositeEffects(\n context,\n source.width,\n source.height,\n rasterScaleOf(source),\n values,\n this.map.styleLightDarkTheme,\n this.currentScopeMask(),\n );\n }\n resolve(composite);\n });\n mapLibreMap.triggerRepaint();\n });\n }\n\n // Resolves once every source is loaded and every tile drawn at the new ratio, or after a timeout.\n private settled(timeoutMs = 15_000): Promise<void> {\n const mapLibreMap = this.map.mapLibreMap;\n return new Promise((resolve) => {\n const done = () => {\n clearTimeout(timer);\n mapLibreMap.off('idle', done);\n resolve();\n };\n const timer = setTimeout(done, timeoutMs);\n mapLibreMap.once('idle', done);\n });\n }\n\n private warnIfBufferNotPreserved(): void {\n if (this.warnedBuffer) return;\n\n const canvas = this.map.mapLibreMap.getCanvas();\n const context = canvas.getContext('webgl2') ?? canvas.getContext('webgl');\n const attributes = context?.getContextAttributes?.();\n if (attributes && !attributes.preserveDrawingBuffer) {\n this.warnedBuffer = true;\n console.warn(\n '[MapEffects] bloom, the depth of field and capture read the map canvas, which needs ' +\n \"`mapLibre: { canvasContextAttributes: { preserveDrawingBuffer: true } }` on the map's options.\",\n );\n }\n }\n\n // ── Validation ────────────────────────────────────────────────────────────────────────────\n\n private assertKnob(id: string): asserts id is EffectKnobId {\n if (!(id in effectKnobDefinitions)) {\n throw new Error(`Unknown effect knob '${id}'. Call describe() for the catalogue of knob ids.`);\n }\n }\n\n private validate(id: string, value: unknown): void {\n this.assertKnob(id);\n const definition: EffectKnobDefinition = effectKnobDefinitions[id];\n if (definition.kind === 'colors') {\n if (!Array.isArray(value))\n throw new RangeError(`Knob '${id}' expects a list of colours; got ${String(value)}.`);\n\n const reason = value.map(invalidScopeEntry).find((invalid) => invalid !== undefined);\n if (reason) throw new RangeError(`Knob '${id}': ${reason}. Call describe() for the names it accepts.`);\n return;\n }\n if (definition.kind === 'color') {\n if (typeof value !== 'string' || !value)\n throw new RangeError(`Knob '${id}' expects a CSS colour; got ${String(value)}.`);\n return;\n }\n const { range } = definition;\n if (typeof value !== 'number' || !Number.isFinite(value)) {\n throw new RangeError(`Knob '${id}' expects a number; got ${String(value)}.`);\n }\n if (value < range.min || value > range.max) {\n throw new RangeError(`Knob '${id}' expects a number between ${range.min} and ${range.max}; got ${value}.`);\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.es.js","names":["defocusFragment","VERTEX_SHADER","FRAGMENT_PREAMBLE","StylingModule","parseCssColor","stylingColorKnobIds","groupsOf","id","split","slice","map","_","index","parts","join","NAMED_KNOBS","named","Map","name","set","get","bloomScopeNames","keys","invalidScopeEntry","entry","String","has","cubeIndex","red","green","blue","chromaticity","top","Math","max","applyScopeMask","context","mask","image","getImageData","canvas","width","height","pixels","length","maskPixels","data","putImageData","FRACTION","min","step","USE_CASE","effectKnobDefinitions","kind","description","default","range","useCase","options","effectKnobIds","Object","effectKnobCatalogue","clamp","value","asFraction","bloomFilter","radius","threshold","scale","cut","contrast","toFixed","gradeFilter","brightness","saturation","filter","Boolean","rimGradient","inner","outer","reach","round","vignetteGradient","intensity","vignetteStops","rgb","abs","rimMask","toRadians","degrees","PI","defocusUniforms","values","camera","raster","ramp","pitch","verticalFieldOfView","tilt","tan","span","depthRamp","blurCssPx","uDepth","uFocus","focus","uBand","band","uBlur","uBokeh","bokeh","uTexel","FOG_BY_THEME","light","grade","dark","DEFOCUS_SATURATE","fogGradeFilter","lightDark","fogBlurPx","fogFilter","fogVeilStops","fogVeil","filteredCopy","document","createElement","copy","getContext","drawImage","drawOver","layer","operation","alpha","save","globalCompositeOperation","globalAlpha","restore","fillEllipticalRadial","innerColor","outerColor","radiusX","SQRT2","radiusY","gradient","createRadialGradient","addColorStop","translate","fillStyle","fillRect","applyMaskedBlur","extraFilter","paintMask","blurred","trim","scopedCopy","compositeEffects","scopeMask","source","glow","applyBloom","graded","applyGrade","rimMaskOf","target","blur","withDefaults","settings","fromEntries","clampAndFilter","gl","texParameteri","TEXTURE_2D","TEXTURE_WRAP_S","CLAMP_TO_EDGE","TEXTURE_WRAP_T","TEXTURE_MAG_FILTER","LINEAR","TEXTURE_MIN_FILTER","resizeCanvas","compile","type","shader","createShader","shaderSource","compileShader","getShaderParameter","COMPILE_STATUS","log","getShaderInfoLog","deleteShader","ScreenPassRenderer","programs","failures","Set","sourceTexture","targets","targetSize","lost","constructor","this","addEventListener","event","preventDefault","clear","antialias","premultipliedAlpha","preserveDrawingBuffer","render","stages","input","uploadSource","mipmapped","holdTargets","runStage","texture","mipmap","hasContext","compileError","fragment","built","cachedProgram","release","program","deleteProgram","delete","remove","getExtension","loseContext","releaseTargets","createTexture","framebuffer","createFramebuffer","bindTexture","texImage2D","RGBA","UNSIGNED_BYTE","bindFramebuffer","FRAMEBUFFER","framebufferTexture2D","COLOR_ATTACHMENT0","push","deleteTexture","deleteFramebuffer","LINEAR_MIPMAP_LINEAR","generateMipmap","pixelStorei","UNPACK_FLIP_Y_WEBGL","stage","programOf","viewport","useProgram","activeTexture","TEXTURE0","uniform1i","getUniformLocation","entries","uniforms","location","uniform1f","uniform2f","drawArrays","TRIANGLES","add","console","warn","cached","build","shaders","VERTEX_SHADER","FRAGMENT_SHADER","failure","find","compiled","createProgram","attachShader","linkProgram","getProgramParameter","LINK_STATUS","getProgramInfoLog","PASS_PLACEMENTS","OVERLAYS","backdropFilterStyle","backdropFilter","webkitBackdropFilter","maskStyle","maskImage","webkitMaskImage","LAYER_STYLE","position","inset","pointerEvents","display","rasterScaleOf","clientWidth","drawOncePerFrame","draw","frame","schedule","requestAnimationFrame","cancel","cancelAnimationFrame","MapEffects","passes","container","overlays","bloomCanvas","artworkCanvas","chain","chainDraw","drawChain","bloomDraw","drawBloom","styling","stylingRequest","scopeCanvas","warnedBuffer","warnedScope","warnedNoWebGL2","warnedUniforms","WeakSet","onStyleData","dropScopeMask","onRender","scheduleBloom","mapContainer","mapLibreMap","getContainer","className","assign","style","overflow","querySelector","insertAdjacentElement","appendChild","element","dataset","effect","mixBlendMode","on","cameraAngles","getPitch","getVerticalFieldOfView","validate","assertKnob","reset","_removed","rest","getConfig","applyConfig","addPass","pass","Error","placement","hasOwn","RangeError","warnNoWebGL2","error","added","replaced","existing","releaseUnused","warnIfBufferNotPreserved","removePass","removed","capture","ratioSetter","live","getPixelRatio","wanted","pixelRatio","wasOverridden","globalThis","devicePixelRatio","rerenders","setPixelRatio","settled","readBack","off","show","styleLightDarkTheme","backgroundImage","backgroundColor","opacity","bloomOn","chainStages","passStages","depthOfFieldStages","flatMap","uniformsOf","some","getCanvas","rendered","glowSource","scopedSource","clearRect","currentScopeMask","willReadFrequently","scope","key","tolerance","buildScopeMaskFor","readsStyling","scopeReadsStyling","requestStyling","colors","knobs","color","scopeColors","warnScopeEmpty","keyColors","limit","Uint8Array","pixel","deltaRed","deltaGreen","deltaBlue","buildScopeMask","then","Promise","resolve","once","composite","triggerRepaint","timeoutMs","done","clearTimeout","timer","setTimeout","attributes","getContextAttributes","definition","Array","isArray","reason","invalid","Number","isFinite"],"sources":["../src/bloomScope.ts","../src/effectsCatalogue.ts","../src/effectFilters.ts","../src/composite.ts","../src/effectValues.ts","../src/gpu/defocusShader.ts","../src/gpu/screenPass.ts","../src/MapEffects.ts"],"sourcesContent":["/**\n * Scoping bloom to part of the map, by the colours that part is drawn in.\n *\n * - **Colour is what survives flattening**: the effects read one image with no layers in it.\n * - **Names come from the SDK**: every knob and group is derived from `stylingColorKnobIds`.\n * - **Brightness is not compared**: an antialiased edge and a lit centre are one colour at two\n * levels, and `bloom.threshold` judges level.\n *\n * @module\n */\n\nimport {\n parseCssColor,\n type RGBAColor,\n type StylingColorKnobId,\n type StylingModule,\n stylingColorKnobIds,\n} from '@tomtom-org/maps-sdk/map';\n\n// Every dotted prefix of an id: `traffic` and `traffic.incidents` for `traffic.incidents.majorColor`.\ntype GroupsOf<ID extends string> = ID extends `${infer Head}.${infer Rest}`\n ? Head | `${Head}.${GroupsOf<Rest>}`\n : never;\n\n/**\n * A family of colour knobs, named by the prefix their ids share — `traffic` for every traffic\n * colour, `traffic.incidents` for the incident severities, `colors` for the base map's own.\n *\n * @group Map Effects\n */\nexport type StylingColorKnobGroup = GroupsOf<StylingColorKnobId>;\n\n/**\n * One entry of a bloom scope: a styling colour knob, a group of them, or a CSS colour — the last\n * for what the styling module does not paint, such as a route line in its `mainColor`.\n *\n * @group Map Effects\n */\n// `string & {}` accepts any CSS colour while leaving the knob names to autocomplete.\nexport type BloomScopeEntry = StylingColorKnobId | StylingColorKnobGroup | (string & {});\n\n/**\n * The colours bloom may light. Empty — the default — lights the whole frame.\n *\n * @group Map Effects\n */\nexport type BloomScope = readonly BloomScopeEntry[];\n\nconst groupsOf = (id: string): string[] =>\n id\n .split('.')\n .slice(0, -1)\n .map((_, index, parts) => parts.slice(0, index + 1).join('.'));\n\n// Every name a scope accepts besides a CSS colour, and the colour knobs it stands for: each knob\n// under its own id, and under every group its id falls in.\nconst NAMED_KNOBS: ReadonlyMap<string, readonly StylingColorKnobId[]> = (() => {\n const named = new Map<string, StylingColorKnobId[]>();\n for (const id of stylingColorKnobIds) {\n for (const name of [...groupsOf(id), id]) named.set(name, [...(named.get(name) ?? []), id]);\n }\n return named;\n})();\n\n/**\n * The names a scope accepts besides CSS colours, in catalogue order: each group before its first knob.\n * @ignore\n */\nexport const bloomScopeNames: readonly string[] = [...NAMED_KNOBS.keys()];\n\n/**\n * Why an entry is not one a scope can use, or undefined when it is: a known name, or a CSS colour.\n * @ignore\n */\nexport const invalidScopeEntry = (entry: unknown): string | undefined => {\n if (typeof entry !== 'string') return `${String(entry)} is not a string`;\n if (NAMED_KNOBS.has(entry) || parseCssColor(entry)) return undefined;\n\n return `'${entry}' is neither a CSS colour nor a styling colour knob or group`;\n};\n\n/**\n * Whether resolving a scope needs the styling module — any entry that names knobs rather than\n * giving a colour outright.\n * @ignore\n */\nexport const scopeReadsStyling = (scope: BloomScope): boolean => scope.some((entry) => NAMED_KNOBS.has(entry));\n\n/**\n * The colours a scope currently wears. Named entries are read from the styling module, so a\n * recolour or a style switch moves the scope with it; a knob the style paints with an expression no\n * single literal stands for yields nothing, and so does a named entry before the module arrives.\n * @ignore\n */\nexport const scopeColors = (styling: StylingModule | undefined, scope: BloomScope): RGBAColor[] =>\n scope.flatMap((entry) => {\n const knobs = NAMED_KNOBS.get(entry);\n const literals = knobs ? knobs.map((id) => styling?.get(id)) : [entry];\n return literals\n .map((value) => (typeof value === 'string' ? parseCssColor(value) : undefined))\n .filter((color) => color !== undefined);\n });\n\n// The cube is indexed by the top five bits of each channel: 32³ entries covering every colour a\n// pixel can be, built once per scope so the per-pixel work is one lookup rather than one distance\n// per key colour.\nconst CUBE_BITS = 3;\nconst CUBE_SIDE = 256 >> CUBE_BITS;\nconst cubeIndex = (red: number, green: number, blue: number): number =>\n ((red >> CUBE_BITS) * CUBE_SIDE + (green >> CUBE_BITS)) * CUBE_SIDE + (blue >> CUBE_BITS);\n\ntype Chromaticity = [red: number, green: number, blue: number];\n\n/**\n * Chromaticity: the colour with its brightness divided out, so a dim edge and the lit centre of the\n * same tube compare equal — and so do a 0–1 key colour and the 0–255 pixel wearing it. Black has no\n * chromaticity and is returned as such.\n */\nconst chromaticity = ([red, green, blue]: readonly number[]): Chromaticity | undefined => {\n const top = Math.max(red, green, blue);\n if (top === 0) return undefined;\n\n return [red / top, green / top, blue / top];\n};\n\n/**\n * A lookup cube saying, for every colour, whether bloom may light it: within `tolerance` of some\n * key colour's chromaticity. An empty key set has nothing to key on and returns undefined, which is\n * what lights the whole frame.\n * @ignore\n */\nexport const buildScopeMask = (colors: RGBAColor[], tolerance: number): Uint8Array | undefined => {\n const keyColors = colors.map(chromaticity).filter((color) => color !== undefined);\n if (keyColors.length === 0) return undefined;\n\n const limit = tolerance * tolerance;\n const mask = new Uint8Array(CUBE_SIDE * CUBE_SIDE * CUBE_SIDE);\n for (let red = 0; red < 256; red += 1 << CUBE_BITS) {\n for (let green = 0; green < 256; green += 1 << CUBE_BITS) {\n for (let blue = 0; blue < 256; blue += 1 << CUBE_BITS) {\n const pixel = chromaticity([red, green, blue]);\n if (!pixel) continue;\n\n const lit = keyColors.some((key) => {\n const deltaRed = pixel[0] - key[0];\n const deltaGreen = pixel[1] - key[1];\n const deltaBlue = pixel[2] - key[2];\n return deltaRed * deltaRed + deltaGreen * deltaGreen + deltaBlue * deltaBlue <= limit;\n });\n if (lit) mask[cubeIndex(red, green, blue)] = 1;\n }\n }\n }\n return mask;\n};\n\n/**\n * Blacks out every RGBA pixel the scope does not cover, in place. Black is the identity under\n * `screen`, so a pixel dropped here adds nothing to the frame — the same way a pixel below the\n * threshold contributes nothing.\n * @ignore\n */\nexport const maskPixels = (pixels: Uint8ClampedArray, mask: Uint8Array): void => {\n for (let index = 0; index < pixels.length; index += 4) {\n if (mask[cubeIndex(pixels[index], pixels[index + 1], pixels[index + 2])]) continue;\n\n pixels[index] = 0;\n pixels[index + 1] = 0;\n pixels[index + 2] = 0;\n }\n};\n\n/**\n * {@link maskPixels} over everything a 2D canvas holds — the one step the live bloom and the\n * capture both take before their threshold runs.\n * @ignore\n */\nexport const applyScopeMask = (context: CanvasRenderingContext2D, mask: Uint8Array): void => {\n const image = context.getImageData(0, 0, context.canvas.width, context.canvas.height);\n maskPixels(image.data, mask);\n context.putImageData(image, 0, 0);\n};\n","import type { KnobKind, KnobRange } from '@tomtom-org/maps-sdk/map';\nimport { type BloomScope, bloomScopeNames } from './bloomScope';\n\n/**\n * The range of a numeric effect knob. Every one ships a slider `step`; the shared {@link KnobRange}\n * leaves it optional.\n *\n * @group Map Effects\n */\nexport type EffectKnobRange = KnobRange & { step: number };\n\n/**\n * One effect knob, drawn from the SDK's shared knob vocabulary. Discriminated on `kind`: a range\n * belongs to a numeric knob and the accepted names to a `colors` one.\n * @ignore\n */\nexport type EffectKnobDefinition = {\n description: string;\n /** The practical problem the effect solves — the reason it exists in an SDK with no artistic ambitions. */\n useCase: string;\n} & (\n | { kind: Extract<KnobKind, 'number'>; default: number; range: EffectKnobRange }\n | { kind: Extract<KnobKind, 'color'>; default: string }\n | { kind: Extract<KnobKind, 'colors'>; default: BloomScope; options: readonly string[] }\n);\n\nconst FRACTION: EffectKnobRange = { min: 0, max: 1, step: 0.05 };\n\n// The practical problem each effect solves. Every knob of one effect carries the same one.\nconst USE_CASE = {\n bloom: 'Emphasis on dark and driving styles.',\n grade: 'Muting the base map under a data overlay.',\n fog: 'Depth cue: the far context recedes as if seen through air.',\n edgeBlur: 'Focus: the centre stays sharp and the edges lose their detail.',\n depthOfField: 'Depth on a tilted map: one band of distance stays sharp, the rest defocuses with distance.',\n tint: 'Muting the base map under a data overlay; matching a brand.',\n vignette: 'Focus on the centre of the view.',\n} as const;\n\n/**\n * The effects catalogue. Every effect is off at its default; each is named by the product problem it\n * solves. Ordering is the compositing order the overlays stack in — bloom → grade → fog → edge blur\n * → tint → vignette — with the depth of field listed beside the other defocus, though it is a GPU\n * pass under all of them rather than one of them.\n * @ignore\n */\nexport const effectKnobDefinitions = {\n 'bloom.intensity': {\n kind: 'number',\n description: 'How strongly bright features (lit roads, traffic tubes, route lines) glow. 0 = off.',\n default: 0,\n range: FRACTION,\n useCase: USE_CASE.bloom,\n },\n 'bloom.radius': {\n kind: 'number',\n description: 'How far the glow spreads, in CSS pixels.',\n default: 12,\n range: { min: 2, max: 40, step: 1 },\n useCase: USE_CASE.bloom,\n },\n 'bloom.threshold': {\n kind: 'number',\n description: 'Brightness below which nothing glows. Raise it so only the brightest features light up.',\n default: 0.55,\n range: FRACTION,\n useCase: USE_CASE.bloom,\n },\n 'bloom.only': {\n kind: 'colors',\n description:\n 'The colours bloom may light: CSS colours, styling colour knobs or groups of them (`traffic`, `traffic.incidents.majorColor`), read from the loaded style. Only the pixels wearing one of them glow; empty lights the whole frame.',\n default: [],\n options: bloomScopeNames,\n useCase: USE_CASE.bloom,\n },\n 'bloom.onlyTolerance': {\n kind: 'number',\n description:\n \"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.\",\n // The TomTom styles put a moderate jam 0.217 from a major one in chromaticity; from 0.22 up\n // a major-jam scope lets in every reddish jam.\n default: 0.15,\n range: FRACTION,\n useCase: USE_CASE.bloom,\n },\n 'grade.brightness': {\n kind: 'number',\n description: 'Brightness of the whole base map (1 = unchanged).',\n default: 1,\n range: { min: 0.5, max: 1.5, step: 0.05 },\n useCase: USE_CASE.grade,\n },\n 'grade.contrast': {\n kind: 'number',\n description: 'Contrast of the whole base map (1 = unchanged).',\n default: 1,\n range: { min: 0.5, max: 1.5, step: 0.05 },\n useCase: USE_CASE.grade,\n },\n 'grade.saturation': {\n kind: 'number',\n description: 'Colour saturation of the whole base map (1 = unchanged, 0 = greyscale).',\n default: 1,\n range: { min: 0, max: 1.5, step: 0.05 },\n useCase: USE_CASE.grade,\n },\n 'fog.intensity': {\n kind: 'number',\n description:\n '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.',\n default: 0,\n range: FRACTION,\n useCase: USE_CASE.fog,\n },\n 'fog.reach': {\n kind: 'number',\n description: 'How far the fog reaches in from the edges (1 = to the centre).',\n default: 0.4,\n range: FRACTION,\n useCase: USE_CASE.fog,\n },\n 'edgeBlur.intensity': {\n kind: 'number',\n description:\n '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.',\n default: 0,\n range: { min: 0, max: 30, step: 1 },\n useCase: USE_CASE.edgeBlur,\n },\n 'edgeBlur.reach': {\n kind: 'number',\n description: 'How far the edge blur reaches in from the edges (1 = to the centre).',\n default: 0.5,\n range: FRACTION,\n useCase: USE_CASE.edgeBlur,\n },\n 'depthOfField.intensity': {\n kind: 'number',\n description:\n '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.',\n default: 0,\n range: { min: 0, max: 30, step: 1 },\n useCase: USE_CASE.depthOfField,\n },\n 'depthOfField.focus': {\n kind: 'number',\n description:\n '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.',\n default: 0,\n range: FRACTION,\n useCase: USE_CASE.depthOfField,\n },\n 'depthOfField.band': {\n kind: 'number',\n description:\n '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.',\n default: 0.3,\n range: FRACTION,\n useCase: USE_CASE.depthOfField,\n },\n 'depthOfField.bokeh': {\n kind: 'number',\n description:\n '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.',\n default: 0.5,\n range: FRACTION,\n useCase: USE_CASE.depthOfField,\n },\n 'tint.color': {\n kind: 'color',\n description: 'A flat colour laid over the whole map.',\n default: '#1f2937',\n useCase: USE_CASE.tint,\n },\n 'tint.opacity': {\n kind: 'number',\n description: 'Opacity of the tint. 0 = off.',\n default: 0,\n range: FRACTION,\n useCase: USE_CASE.tint,\n },\n 'vignette.intensity': {\n kind: 'number',\n description: 'Darkening (negative) or lightening (positive) towards the edges. 0 = off.',\n default: 0,\n range: { min: -1, max: 1, step: 0.05 },\n useCase: USE_CASE.vignette,\n },\n 'vignette.reach': {\n kind: 'number',\n description: 'How far the vignette reaches in from the edges (1 = to the centre).',\n default: 0.45,\n range: FRACTION,\n useCase: USE_CASE.vignette,\n },\n} as const satisfies Record<string, EffectKnobDefinition>;\n\n/**\n * The id of an effect knob, as accepted by {@link MapEffects.set}.\n *\n * @group Map Effects\n */\nexport type EffectKnobId = keyof typeof effectKnobDefinitions;\n\n/**\n * Every effect knob id, in compositing order.\n *\n * @group Map Effects\n */\nexport const effectKnobIds = Object.keys(effectKnobDefinitions) as EffectKnobId[];\n\n/**\n * The value type an effect knob takes.\n *\n * @group Map Effects\n */\nexport type EffectKnobValueOf<ID extends EffectKnobId> = Extract<\n EffectKnobDefinition,\n { kind: (typeof effectKnobDefinitions)[ID]['kind'] }\n>['default'];\n\n/**\n * One entry of {@link effectKnobCatalogue}: what a UI or a coding agent needs to offer the knob and\n * validate a value for it. The value in force is {@link MapEffects.get}.\n *\n * @group Map Effects\n */\nexport type EffectKnob = EffectKnobDefinition & {\n /** The knob's id, as accepted by {@link MapEffects.set}. */\n id: EffectKnobId;\n};\n\n/**\n * Every effect knob with its kind, description, default, range or accepted names and the use case\n * it serves, in compositing order. Static data: no map is needed to read it.\n *\n * @group Map Effects\n */\nexport const effectKnobCatalogue: readonly EffectKnob[] = effectKnobIds.map(\n (id): EffectKnob => ({ id, ...effectKnobDefinitions[id] }),\n);\n","/**\n * The maths behind every effect, as pure functions: CSS filter strings and gradient descriptions the\n * live overlays use through `backdrop-filter`, and the same values the capture path feeds into a 2D\n * canvas. One implementation, two consumers — never a live/export pair that can drift.\n *\n * @module\n * @ignore\n */\n\nimport type { LightDark } from '@tomtom-org/maps-sdk/map';\nimport type { DefocusUniforms } from './gpu/defocusShader';\n\n// The threshold's usable ceiling: at 1 the contrast term would divide by zero.\nconst BLOOM_CUT_MAX = 0.92;\nconst BLOOM_SATURATE = 1.35;\n\nconst clamp = (value: number, min: number, max: number): number => Math.min(Math.max(value, min), max);\n\n// Knob values arrive validated against the catalogue, so this only guards the maths against a\n// caller reaching these functions directly.\nconst asFraction = (value: number): number => clamp(value, 0, 1);\n\n/**\n * The filter chain that turns the map underneath into its own glow: cut, spread, saturate.\n *\n * `brightness` and `contrast` together are the threshold, and the pair is exact: `contrast(c)`\n * scales each channel away from 0.5 and `brightness(b)` scales it from 0, so the two compose to\n * `(in·b − 0.5)·c + 0.5`. Asking that line to send `cut` to 0 and 1 to 1 solves to\n * `b = 1/(1 + cut)`, `c = (1 + cut)/(1 − cut)`. Per channel, not luminance, so a magenta road\n * blooms magenta instead of bleeding grey.\n *\n * @param scale - Device pixels per CSS pixel of the canvas the filter runs on, so the radius is in\n * CSS pixels wherever it is applied.\n * @ignore\n */\nexport const bloomFilter = (radius: number, threshold: number, scale = 1): string => {\n const cut = asFraction(threshold) * BLOOM_CUT_MAX;\n const brightness = 1 / (1 + cut);\n const contrast = (1 + cut) / (1 - cut);\n return `brightness(${brightness.toFixed(4)}) contrast(${contrast.toFixed(3)}) blur(${radius * scale}px) saturate(${BLOOM_SATURATE})`;\n};\n\n/**\n * The colour grade as a CSS filter, or an empty string when every term is at its neutral value.\n * @ignore\n */\nexport const gradeFilter = (brightness: number, contrast: number, saturation: number): string =>\n [\n brightness === 1 ? '' : `brightness(${brightness})`,\n contrast === 1 ? '' : `contrast(${contrast})`,\n saturation === 1 ? '' : `saturate(${saturation})`,\n ]\n .filter(Boolean)\n .join(' ');\n\n/**\n * Every rim effect — vignette, fog veil, mask — paints the same ellipse: the inner colour out to\n * where `reach` starts it, the outer colour at the corners. The capture path fills the same pair\n * onto a canvas gradient, so the two cannot drift.\n * @ignore\n */\nexport const rimGradient = (inner: string, outer: string, reach: number): string => {\n // Where the rim starts, as the percentage of the way from centre to corner.\n const innerStop = Math.round((1 - asFraction(reach)) * 100);\n return `radial-gradient(ellipse farthest-corner at center, ${inner} ${innerStop}%, ${outer} 100%)`;\n};\n\n/**\n * The radial gradient a vignette paints: transparent in the middle, darkening (negative intensity)\n * or lightening (positive) towards the corners.\n * @ignore\n */\nexport const vignetteGradient = (intensity: number, reach: number): string => {\n const { inner, outer } = vignetteStops(intensity);\n return rimGradient(inner, outer, reach);\n};\n\n/**\n * The two colours a vignette fades between: black towards the corners for a negative intensity,\n * white for a positive one. The capture path paints the same pair onto a canvas gradient.\n * @ignore\n */\nexport const vignetteStops = (intensity: number): { inner: string; outer: string } => {\n const rgb = intensity < 0 ? '0, 0, 0' : '255, 255, 255';\n return { inner: `rgba(${rgb}, 0)`, outer: `rgba(${rgb}, ${Math.min(Math.abs(intensity), 1)})` };\n};\n\n/**\n * A mask that keeps an effect to the rim of the view: transparent centre, opaque edges.\n * @ignore\n */\nexport const rimMask = (reach: number): string => rimGradient('transparent', 'black', reach);\n\n/** The camera a depth of field needs, and all it needs: two angles, in degrees. @ignore */\nexport type CameraAngles = { pitch: number; verticalFieldOfView: number };\n\n// A tangent runs away at 90°, and a pitch restored from a saved view is not the map's to promise.\nconst MAX_PITCH_DEGREES = 89;\n\n// The same guard on the other angle: a field of view of 0 or 180 has no half-tangent to take.\nconst MIN_FIELD_OF_VIEW_DEGREES = 1;\nconst MAX_FIELD_OF_VIEW_DEGREES = 175;\n\n// The most of the frame's depth the band may hold sharp. Half, so the knob's top end is a deep\n// field rather than a switched-off effect.\nconst MAX_BAND = 0.5;\n\nconst toRadians = (degrees: number) => (degrees * Math.PI) / 180;\n\n/**\n * **How much nearer the bottom of the frame is than the top**, in reciprocal distance normalised so\n * that 0 is the nearest ground in shot: 0 seen from straight above, 0.5 at a 45° tilt, past 1 once\n * the horizon is on screen.\n *\n * A ray through a row `y` of a plane seen by a perspective camera meets it at `1/z = 1/D − y·tanθ/D²`\n * — *affine in the row* — so one number describes the whole depth of the picture, and the viewport\n * cancels against the camera distance that defines it. Which is why this needs no depth buffer:\n * MapLibre hands out none, and for a plane none is necessary.\n *\n * A flat map is not a special case but `ramp = 0`: every part of the ground is the same distance\n * away, so there is no depth to be shallow in and the effect ends on its own.\n * @ignore\n */\nexport const depthRamp = ({ pitch, verticalFieldOfView }: CameraAngles): number => {\n const tilt = Math.tan(toRadians(clamp(pitch, 0, MAX_PITCH_DEGREES)));\n const spread = Math.tan(\n toRadians(clamp(verticalFieldOfView, MIN_FIELD_OF_VIEW_DEGREES, MAX_FIELD_OF_VIEW_DEGREES)) / 2,\n );\n const span = 2 * spread * tilt;\n return span / (1 + span / 2);\n};\n\n/**\n * The depth-of-field pass' uniforms for a camera, a set of knobs and a raster: the optics as\n * numbers, with no GPU in sight — which is what makes them testable, and they are the half most\n * likely to be quietly wrong.\n *\n * `scale` is device pixels per CSS pixel. **Only the blur takes it**, and that asymmetry is the\n * whole of how a capture matches the screen: a circle of confusion is a length, while the depth\n * ramp and the plane of focus are angles and ratios and read the same at any resolution.\n *\n * `undefined` when there is nothing to defocus — a map with no tilt has no depth to be shallow in,\n * so the geometry ends the effect rather than a check on the pitch.\n * @ignore\n */\nexport const defocusUniforms = (\n values: { focus: number; band: number; blurCssPx: number; bokeh: number },\n camera: CameraAngles,\n raster: { width: number; height: number; scale: number },\n): DefocusUniforms | undefined => {\n const ramp = depthRamp(camera);\n if (ramp <= 0 || values.blurCssPx <= 0) return undefined;\n\n return {\n uDepth: ramp,\n // Read across what the frame actually shows rather than to infinity: a gently tilted map\n // spans only the nearest part of that scale, and a knob whose top end landed beyond the top\n // of the picture would be dead at exactly the pitch a flight uses. Held at the horizon.\n uFocus: Math.min(asFraction(values.focus) * ramp, 1),\n uBand: asFraction(values.band) * MAX_BAND,\n uBlur: Math.max(values.blurCssPx, 0) * raster.scale,\n uBokeh: asFraction(values.bokeh),\n uTexel: [1 / Math.max(raster.width, 1), 1 / Math.max(raster.height, 1)],\n };\n};\n\n// How opaque the fog veil gets at the corners, per unit of intensity.\nconst FOG_VEIL_ALPHA = 0.65;\n\n// The fog's blur radius in CSS pixels: the softness the faintest fog already has, plus the span\n// intensity spends on top of it.\nconst FOG_BLUR_BASE_PX = 2;\nconst FOG_BLUR_SPAN_PX = 6;\n\n// The haze the fog veil is made of, and the grade under it, per light/dark theme of the map. Haze\n// takes the colour of the light in the air: pale over a daylit map, and the map's own darkness over\n// a dark one, where a pale veil would read as a bloom rather than as distance.\nconst FOG_BY_THEME: Record<LightDark, { rgb: string; grade: string }> = {\n light: { rgb: '238, 240, 244', grade: 'brightness(1.08) contrast(0.82) saturate(0.7)' },\n dark: { rgb: '16, 22, 33', grade: 'brightness(0.92) contrast(0.82) saturate(0.7)' },\n};\n\n/** Keeps blurred colours from greying out; used by both the live filter and the capture. @ignore */\nexport const DEFOCUS_SATURATE = 'saturate(1.08)';\n\n/**\n * The grade under the fog blur, which washes the rim out towards the colour of the air.\n * @ignore\n */\nexport const fogGradeFilter = (lightDark: LightDark): string => FOG_BY_THEME[lightDark].grade;\n\n/**\n * The fog's blur radius in CSS pixels for a given intensity.\n * @ignore\n */\nexport const fogBlurPx = (intensity: number): number => FOG_BLUR_BASE_PX + intensity * FOG_BLUR_SPAN_PX;\n\n/**\n * The fog: a blur plus a washed-out grade over the rim, under a veil that thickens outwards.\n * @ignore\n */\nexport const fogFilter = (intensity: number, lightDark: LightDark): string =>\n `blur(${fogBlurPx(intensity)}px) ${fogGradeFilter(lightDark)}`;\n\n/**\n * The two colours the fog veil fades between, for a given intensity.\n * @ignore\n */\nexport const fogVeilStops = (intensity: number, lightDark: LightDark): { inner: string; outer: string } => {\n const { rgb } = FOG_BY_THEME[lightDark];\n return { inner: `rgba(${rgb}, 0)`, outer: `rgba(${rgb}, ${intensity * FOG_VEIL_ALPHA})` };\n};\n\n/**\n * The veil over the fog blur, thickening towards the corners.\n * @ignore\n */\nexport const fogVeil = (intensity: number, reach: number, lightDark: LightDark): string => {\n const { inner, outer } = fogVeilStops(intensity, lightDark);\n return rimGradient(inner, outer, reach);\n};\n\n/**\n * The filter both defocusing effects use — edge blur and depth of field: blur alone, with a slight\n * saturation lift so blurred colours do not grey out. Unlike {@link fogFilter} it neither washes\n * what it blurs out nor lays a veil over it, so those pixels keep their colours and lose only their\n * detail. What differs between the two effects is the mask, not the filter.\n * @ignore\n */\nexport const defocusFilter = (blurCssPx: number): string => `blur(${blurCssPx}px) ${DEFOCUS_SATURATE}`;\n","/**\n * Composites the effects onto a 2D canvas that already holds the rendered map — the capture path.\n * It consumes the same filter maths as the live overlays (`effectFilters.ts`), so what is captured is\n * what was on screen.\n *\n * @module\n * @ignore\n */\n\nimport type { LightDark } from '@tomtom-org/maps-sdk/map';\nimport { applyScopeMask } from './bloomScope';\nimport {\n bloomFilter,\n DEFOCUS_SATURATE,\n fogBlurPx,\n fogGradeFilter,\n fogVeilStops,\n gradeFilter,\n vignetteStops,\n} from './effectFilters';\nimport type { EffectValues } from './effectValues';\n\ntype Context = CanvasRenderingContext2D;\n\n// A filtered copy of what `context` holds, on a canvas of its own — the capture equivalent of one\n// `backdrop-filter` overlay. Undefined when the browser gives no 2D context for the scratch canvas.\nconst filteredCopy = (context: Context, width: number, height: number, filter: string): Context | undefined => {\n const canvas = document.createElement('canvas');\n canvas.width = width;\n canvas.height = height;\n const copy = canvas.getContext('2d');\n if (!copy) return undefined;\n\n copy.filter = filter;\n copy.drawImage(context.canvas, 0, 0);\n copy.filter = 'none';\n return copy;\n};\n\n// Lays one such copy back over the capture, in the blend mode its live overlay uses.\nconst drawOver = (context: Context, layer: Context, operation: GlobalCompositeOperation, alpha = 1): void => {\n context.save();\n context.globalCompositeOperation = operation;\n context.globalAlpha = alpha;\n context.drawImage(layer.canvas, 0, 0);\n context.restore();\n};\n\n// Fills an elliptical radial gradient sized to the canvas aspect, like CSS's\n// `ellipse farthest-corner at center`.\nconst fillEllipticalRadial = (\n context: Context,\n width: number,\n height: number,\n reach: number,\n innerColor: string,\n outerColor: string,\n): void => {\n const radiusX = width / Math.SQRT2;\n const radiusY = height / Math.SQRT2;\n const gradient = context.createRadialGradient(0, 0, 1 - reach, 0, 0, 1);\n gradient.addColorStop(0, innerColor);\n gradient.addColorStop(1, outerColor);\n context.save();\n context.translate(width / 2, height / 2);\n context.scale(radiusX, radiusY);\n context.fillStyle = gradient;\n context.fillRect(-width / 2 / radiusX, -height / 2 / radiusY, width / radiusX, height / radiusY);\n context.restore();\n};\n\nconst TRANSPARENT = 'rgba(0,0,0,0)';\nconst OPAQUE = 'rgba(0,0,0,1)';\n\n// Blurs a copy of the canvas and keeps only where `paintMask` leaves it opaque — the capture\n// equivalent of a `backdrop-filter` overlay under a mask.\nconst applyMaskedBlur = (\n context: Context,\n width: number,\n height: number,\n scale: number,\n blurCssPx: number,\n extraFilter: string,\n paintMask: (target: Context) => void,\n): void => {\n const blurred = filteredCopy(context, width, height, `blur(${blurCssPx * scale}px) ${extraFilter}`.trim());\n if (!blurred) return;\n\n blurred.globalCompositeOperation = 'destination-in';\n paintMask(blurred);\n context.drawImage(blurred.canvas, 0, 0);\n};\n\n// Draws the map's own glow back over it, the way the live bloom canvas blends in `screen` mode. A\n// scope drops the pixels it does not cover first, as the live path does.\nconst applyBloom = (\n context: Context,\n width: number,\n height: number,\n scale: number,\n values: EffectValues,\n scopeMask: Uint8Array | undefined,\n): void => {\n const filter = bloomFilter(values['bloom.radius'], values['bloom.threshold'], scale);\n const source = scopeMask ? scopedCopy(context, width, height, scopeMask) : context;\n if (!source) return;\n\n const glow = filteredCopy(source, width, height, filter);\n if (!glow) return;\n\n drawOver(context, glow, 'screen', values['bloom.intensity']);\n};\n\n// The capture's pixels with everything outside the scope blacked out, on a canvas of its own —\n// `none` because this copy is the unfiltered one the threshold has yet to run on.\nconst scopedCopy = (context: Context, width: number, height: number, mask: Uint8Array): Context | undefined => {\n const copy = filteredCopy(context, width, height, 'none');\n if (copy) applyScopeMask(copy, mask);\n return copy;\n};\n\n// The colour grade replaces the pixels underneath, as a `backdrop-filter` overlay does.\nconst applyGrade = (context: Context, width: number, height: number, filter: string): void => {\n const graded = filteredCopy(context, width, height, filter);\n if (!graded) return;\n\n drawOver(context, graded, 'copy');\n};\n\n/**\n * Applies every active effect to `context`, in the same order the live overlays stack: bloom, grade,\n * fog, edge blur, tint, vignette — the overlays, that is; the depth of field is a GPU pass and has\n * already run into `context` by the time this is called. `scale` is device pixels per CSS pixel, so\n * blur radii stay in CSS pixels at any capture resolution.\n * @ignore\n */\nexport const compositeEffects = (\n context: Context,\n width: number,\n height: number,\n scale: number,\n values: EffectValues,\n lightDark: LightDark,\n scopeMask?: Uint8Array,\n): void => {\n if (values['bloom.intensity'] > 0) applyBloom(context, width, height, scale, values, scopeMask);\n\n const grade = gradeFilter(values['grade.brightness'], values['grade.contrast'], values['grade.saturation']);\n if (grade) applyGrade(context, width, height, grade);\n\n const rimMaskOf = (reach: number) => (target: Context) =>\n fillEllipticalRadial(target, width, height, reach, TRANSPARENT, OPAQUE);\n\n if (values['fog.intensity'] > 0) {\n const intensity = values['fog.intensity'];\n const reach = values['fog.reach'];\n const { inner, outer } = fogVeilStops(intensity, lightDark);\n const blur = fogBlurPx(intensity);\n applyMaskedBlur(context, width, height, scale, blur, fogGradeFilter(lightDark), rimMaskOf(reach));\n fillEllipticalRadial(context, width, height, reach, inner, outer);\n }\n if (values['edgeBlur.intensity'] > 0) {\n const blur = values['edgeBlur.intensity'];\n applyMaskedBlur(context, width, height, scale, blur, DEFOCUS_SATURATE, rimMaskOf(values['edgeBlur.reach']));\n }\n if (values['tint.opacity'] > 0) {\n context.save();\n context.globalAlpha = values['tint.opacity'];\n context.fillStyle = values['tint.color'];\n context.fillRect(0, 0, width, height);\n context.restore();\n }\n if (values['vignette.intensity'] !== 0) {\n const { inner, outer } = vignetteStops(values['vignette.intensity']);\n fillEllipticalRadial(context, width, height, values['vignette.reach'], inner, outer);\n }\n};\n","import { type EffectKnobId, type EffectKnobValueOf, effectKnobDefinitions, effectKnobIds } from './effectsCatalogue';\n\n/**\n * Every effect knob with a value: the settings with the catalogue defaults filled in.\n * @ignore\n */\nexport type EffectValues = { [ID in EffectKnobId]: EffectKnobValueOf<ID> };\n\n/**\n * @ignore\n */\nexport const withDefaults = (settings: Partial<EffectValues>): EffectValues =>\n Object.fromEntries(\n effectKnobIds.map((id) => [id, settings[id] ?? effectKnobDefinitions[id].default]),\n ) as EffectValues;\n","/**\n * The depth-of-field pass' contract with the substrate: the program, and the uniform names the\n * stage binds into it.\n *\n * The optics are worked out in `effectFilters.ts` and arrive here as uniforms; the program itself\n * is `glsl/defocus.frag.glsl`, a real GLSL file loaded with Vite's `?raw` rather than a template\n * literal — which is what keeps a backtick inside a shader comment a character rather than the end\n * of the program.\n *\n * @module\n * @ignore\n */\n\nimport defocusFragment from './glsl/defocus.frag.glsl?raw';\n\n/** The uniform names the program declares, so the stage and the tests agree on one spelling. */\nexport type DefocusUniforms = {\n /** How much nearer the bottom of the frame is than the top, in normalised reciprocal distance. */\n uDepth: number;\n /** The plane of focus, on that same scale. */\n uFocus: number;\n /** How much of the scale either side of it stays sharp. */\n uBand: number;\n /** The widest circle of confusion, in device pixels — a diameter, as a lens' is. */\n uBlur: number;\n /** How far a bright sample may outweigh a dull one: the difference between a defocus and a bokeh. */\n uBokeh: number;\n /** 1 / the resolution being rendered, so an offset in device pixels is a UV step. */\n uTexel: readonly [number, number];\n};\n\nexport const DEFOCUS_SHADER: string = defocusFragment;\n","/**\n * The WebGL2 substrate the pixel-derived effects run on: one context, one full-screen triangle, and\n * a pair of render targets a chain of passes ping-pongs between. No effect lives here — a pass\n * brings its own fragment shader and its own uniforms, and this module is what stops two of them\n * being two implementations of context creation, texture upload, resizing and context loss.\n *\n * **One context for the plugin, and that is a requirement rather than a saving.** A browser caps\n * how many live WebGL contexts a page may hold, and the map itself already holds one.\n *\n * The two shaders it owns — the triangle's vertex half and the preamble every fragment stage is\n * composed onto — are real GLSL files under `glsl/`, loaded with Vite's `?raw`, as every shader in\n * this plugin is: a backtick inside a template literal's comment closes the literal, and the\n * program it silently truncates still compiles to a blank picture.\n *\n * @module\n * @ignore\n */\n\nimport FRAGMENT_PREAMBLE from './glsl/fragmentPreamble.glsl?raw';\nimport VERTEX_SHADER from './glsl/screenPass.vert.glsl?raw';\n\n/**\n * The uniforms a {@link MapEffectsPass} reads, by the name its shader declares: a number sets a\n * `float`, a pair a `vec2`. Only those two types can be set — WebGL silently refuses a float for an\n * `int` or `bool` uniform, which then stays 0.\n *\n * @group Map Effects\n */\nexport type MapEffectsPassUniforms = Record<string, number | readonly [number, number]>;\n\n/**\n * One link of the chain: a fragment shader, the uniforms it reads, and whether it samples levels\n * other than 0 — which is what makes the source worth a mip chain.\n * @ignore\n */\nexport type PassStage = {\n fragment: string;\n uniforms: MapEffectsPassUniforms;\n mipmapped?: boolean;\n};\n\ntype RenderTarget = { texture: WebGLTexture; framebuffer: WebGLFramebuffer };\n\n// How every texture in the chain is sampled at level 0: clamped, so a tap that runs off the edge of\n// a disc reads the edge rather than wrapping, and linear, so it reads between texels.\nconst clampAndFilter = (gl: WebGL2RenderingContext): void => {\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n};\n\n/**\n * Sizes a canvas that is drawn every frame. Assigning either dimension clears the canvas and\n * reallocates its bitmap even when the value is unchanged, so this only assigns when the size moved.\n * @ignore\n */\nexport const resizeCanvas = (canvas: HTMLCanvasElement, width: number, height: number): void => {\n if (canvas.width === width && canvas.height === height) return;\n\n canvas.width = width;\n canvas.height = height;\n};\n\n// The compiled shader, or the reason it did not compile.\nconst compile = (gl: WebGL2RenderingContext, type: number, source: string): WebGLShader | string => {\n const shader = gl.createShader(type);\n if (!shader) return 'the context would not create a shader';\n\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) return shader;\n\n const log = gl.getShaderInfoLog(shader) ?? '';\n gl.deleteShader(shader);\n return `failed to compile: ${log}`;\n};\n\n/**\n * Renders a chain of full-screen passes over a source canvas, onto a canvas of its own.\n * @ignore\n */\nexport class ScreenPassRenderer {\n /** The canvas the chain draws into — the one a caller shows, or reads back. */\n readonly canvas: HTMLCanvasElement;\n private readonly gl: WebGL2RenderingContext | undefined;\n private readonly programs = new Map<string, WebGLProgram>();\n // Fragments that would not build, kept so a broken one is reported once rather than every frame.\n private readonly failures = new Set<string>();\n private sourceTexture: WebGLTexture | undefined;\n private targets: RenderTarget[] = [];\n private targetSize = { width: 0, height: 0 };\n private lost = false;\n\n constructor() {\n this.canvas = document.createElement('canvas');\n this.canvas.addEventListener('webglcontextlost', (event) => {\n // Without this the context never comes back, and nor does the picture.\n event.preventDefault();\n this.lost = true;\n });\n this.canvas.addEventListener('webglcontextrestored', () => {\n this.programs.clear();\n this.failures.clear();\n this.sourceTexture = undefined;\n this.targets = [];\n this.targetSize = { width: 0, height: 0 };\n this.lost = false;\n });\n this.gl =\n this.canvas.getContext('webgl2', {\n alpha: true,\n antialias: false,\n premultipliedAlpha: false,\n preserveDrawingBuffer: true,\n }) ?? undefined;\n }\n\n /**\n * Runs `stages` over `source`, in order, and returns the canvas holding the result — or\n * `undefined` when there is nothing to run or no context to run it on.\n */\n render(source: HTMLCanvasElement, stages: PassStage[]): HTMLCanvasElement | undefined {\n const gl = this.gl;\n if (!gl || this.lost || !stages.length || !source.width || !source.height) return undefined;\n\n resizeCanvas(this.canvas, source.width, source.height);\n let input = this.uploadSource(source, stages[0].mipmapped);\n // Without somewhere to put a pass' result, a chain would run every stage over the source\n // and show only the last — a wrong picture, where no picture at least says so.\n if (!input || (stages.length > 1 && !this.holdTargets(gl))) return undefined;\n\n for (let index = 0; index < stages.length; index++) {\n // The last pass paints the canvas the caller shows; the ones before it paint a target\n // the next pass reads, so the picture never leaves the GPU between them.\n const target = index === stages.length - 1 ? undefined : this.targets[index % this.targets.length];\n this.runStage(gl, stages[index], input, target);\n if (!target) continue;\n\n input = target.texture;\n if (stages[index + 1].mipmapped) this.mipmap(gl, input);\n }\n return this.canvas;\n }\n\n /** Whether the browser gave the chain a WebGL2 context at all; without one no stage ever runs. */\n get hasContext(): boolean {\n return this.gl !== undefined;\n }\n\n /**\n * Why `fragment` does not compile or link as a stage, or `undefined` when it does — or when there\n * is no context to ask. A stage that builds stays built; the reason one does not is the caller's\n * to report, so nothing is warned here.\n */\n compileError(fragment: string): string | undefined {\n const gl = this.gl;\n if (!gl || this.lost) return undefined;\n\n const built = this.cachedProgram(gl, fragment);\n return typeof built === 'string' ? built : undefined;\n }\n\n /** Deletes the program built for `fragment`; a stage that runs it again rebuilds it. */\n release(fragment: string): void {\n const program = this.programs.get(fragment);\n if (program) this.gl?.deleteProgram(program);\n\n this.programs.delete(fragment);\n this.failures.delete(fragment);\n }\n\n /** Releases the context's resources; the canvas is the caller's to remove. */\n remove(): void {\n this.gl?.getExtension('WEBGL_lose_context')?.loseContext();\n }\n\n /**\n * The pair of targets the chain ping-pongs between, at the current size — kept rather than\n * made per frame, since a chain runs on every map frame and a texture allocation does not\n * belong there. `false` when the context would not give us one.\n */\n private holdTargets(gl: WebGL2RenderingContext): boolean {\n const { width, height } = this.canvas;\n if (this.targets.length === 2 && this.targetSize.width === width && this.targetSize.height === height) {\n return true;\n }\n\n this.releaseTargets(gl);\n while (this.targets.length < 2) {\n const texture = gl.createTexture();\n const framebuffer = gl.createFramebuffer();\n if (!texture || !framebuffer) return false;\n\n gl.bindTexture(gl.TEXTURE_2D, texture);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);\n clampAndFilter(gl);\n gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);\n gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);\n this.targets.push({ texture, framebuffer });\n }\n this.targetSize = { width, height };\n return true;\n }\n\n private releaseTargets(gl: WebGL2RenderingContext): void {\n for (const { texture, framebuffer } of this.targets) {\n gl.deleteTexture(texture);\n gl.deleteFramebuffer(framebuffer);\n }\n this.targets = [];\n }\n\n // A gather reads the level whose texels are as wide as the gaps between its taps, so a stage\n // that samples levels needs them built on whatever it is about to read — target or source.\n private mipmap(gl: WebGL2RenderingContext, texture: WebGLTexture): void {\n gl.bindTexture(gl.TEXTURE_2D, texture);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);\n gl.generateMipmap(gl.TEXTURE_2D);\n }\n\n // The map's pixels as a texture, or `undefined` where the context would not give us one.\n private uploadSource(source: HTMLCanvasElement, mipmapped = false): WebGLTexture | undefined {\n const gl = this.gl;\n if (!gl) return undefined;\n\n this.sourceTexture ??= gl.createTexture() ?? undefined;\n if (!this.sourceTexture) return undefined;\n\n gl.bindTexture(gl.TEXTURE_2D, this.sourceTexture);\n gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);\n clampAndFilter(gl);\n if (mipmapped) this.mipmap(gl, this.sourceTexture);\n return this.sourceTexture;\n }\n\n private runStage(\n gl: WebGL2RenderingContext,\n stage: PassStage,\n input: WebGLTexture,\n target: RenderTarget | undefined,\n ): void {\n const program = this.programOf(gl, stage.fragment);\n if (!program) return;\n\n gl.bindFramebuffer(gl.FRAMEBUFFER, target?.framebuffer ?? null);\n gl.viewport(0, 0, this.canvas.width, this.canvas.height);\n gl.useProgram(program);\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, input);\n gl.uniform1i(gl.getUniformLocation(program, 'uSource'), 0);\n for (const [name, value] of Object.entries(stage.uniforms)) {\n const location = gl.getUniformLocation(program, name);\n if (!location) continue;\n\n if (typeof value === 'number') {\n gl.uniform1f(location, value);\n } else {\n gl.uniform2f(location, value[0], value[1]);\n }\n }\n gl.drawArrays(gl.TRIANGLES, 0, 3);\n }\n\n private programOf(gl: WebGL2RenderingContext, fragment: string): WebGLProgram | undefined {\n if (this.failures.has(fragment)) return undefined;\n\n const built = this.cachedProgram(gl, fragment);\n if (typeof built !== 'string') return built;\n\n this.failures.add(fragment);\n console.warn(`[MapEffects] shader ${built}`);\n return undefined;\n }\n\n // The program for `fragment`, built once and kept, or the reason it would not build.\n private cachedProgram(gl: WebGL2RenderingContext, fragment: string): WebGLProgram | string {\n const cached = this.programs.get(fragment);\n if (cached) return cached;\n\n const built = this.build(gl, fragment);\n if (typeof built !== 'string') this.programs.set(fragment, built);\n\n return built;\n }\n\n // The linked program for a stage, or the reason there is none.\n private build(gl: WebGL2RenderingContext, fragment: string): WebGLProgram | string {\n const shaders = [\n compile(gl, gl.VERTEX_SHADER, VERTEX_SHADER),\n compile(gl, gl.FRAGMENT_SHADER, `${FRAGMENT_PREAMBLE}${fragment}`),\n ];\n // `WebGLShader` is an empty interface a string satisfies, so only these guards tell the two apart.\n const failure = shaders.find((shader): shader is string => typeof shader === 'string');\n const compiled = shaders.filter((shader): shader is WebGLShader => typeof shader !== 'string');\n const program = failure ? null : gl.createProgram();\n if (program) {\n for (const shader of compiled) gl.attachShader(program, shader);\n gl.linkProgram(program);\n }\n // Attached shaders live on in the program; the rest have nothing left to be part of.\n for (const shader of compiled) gl.deleteShader(shader);\n\n if (failure) return failure;\n if (!program) return 'the context would not create a program';\n if (gl.getProgramParameter(program, gl.LINK_STATUS)) return program;\n\n const log = gl.getProgramInfoLog(program) ?? '';\n gl.deleteProgram(program);\n return `failed to link: ${log}`;\n }\n}\n","import { StylingModule, type TomTomMap } from '@tomtom-org/maps-sdk/map';\nimport {\n applyScopeMask,\n type BloomScope,\n buildScopeMask,\n invalidScopeEntry,\n scopeColors,\n scopeReadsStyling,\n} from './bloomScope';\nimport { compositeEffects } from './composite';\nimport {\n bloomFilter,\n type CameraAngles,\n defocusFilter,\n defocusUniforms,\n fogFilter,\n fogVeil,\n gradeFilter,\n rimMask,\n vignetteGradient,\n} from './effectFilters';\nimport {\n type EffectKnobDefinition,\n type EffectKnobId,\n type EffectKnobValueOf,\n effectKnobDefinitions,\n} from './effectsCatalogue';\nimport type { EffectValues } from './effectValues';\nimport { withDefaults } from './effectValues';\nimport { DEFOCUS_SHADER } from './gpu/defocusShader';\nimport type { MapEffectsPassUniforms, PassStage } from './gpu/screenPass';\nimport { resizeCanvas, ScreenPassRenderer } from './gpu/screenPass';\n\n/**\n * The effect knob values an app sets — only the overridden ones, serializable like the SDK's\n * styling settings.\n *\n * @group Map Effects\n */\nexport type MapEffectsSettings = { [ID in EffectKnobId]?: EffectKnobValueOf<ID> };\n\n/**\n * Where a {@link MapEffectsPass} runs in the GPU chain, relative to the plugin's own depth of field.\n * Either way it runs before every overlay effect, so bloom, grade, fog, edge blur, tint and vignette\n * all read what it drew.\n *\n * @group Map Effects\n */\nexport type MapEffectsPassPlacement = 'before-depth-of-field' | 'after-depth-of-field';\n\n// Every placement, so one a JavaScript caller mistyped is refused rather than never run.\nconst PASS_PLACEMENTS = {\n 'before-depth-of-field': true,\n 'after-depth-of-field': true,\n} as const satisfies Record<MapEffectsPassPlacement, true>;\n\n/**\n * A full-screen GPU pass of your own, run inside the plugin's WebGL chain — a stylisation such as a\n * watercolour or a posterise that the rest of the effects should then work on.\n *\n * @group Map Effects\n */\nexport type MapEffectsPass = {\n /** Names the pass for {@link MapEffects.removePass}; adding a pass under an id in use replaces it in place. */\n id: string;\n /**\n * A GLSL ES 3.00 fragment shader: your own uniforms and `main`. The plugin declares the version,\n * the precisions, `uniform sampler2D uSource` (the picture so far), `in vec2 vUv` and\n * `out vec4 fragColor` — declaring any of them again is a compile error.\n */\n fragment: string;\n /**\n * The uniform values, or a function returning them, called on every draw. A function that throws\n * skips the pass for that draw, with one warning.\n */\n uniforms?: MapEffectsPassUniforms | (() => MapEffectsPassUniforms);\n /** Defaults to `before-depth-of-field`. */\n placement?: MapEffectsPassPlacement;\n};\n\n/**\n * Options for {@link MapEffects.capture}.\n *\n * @group Map Effects\n */\nexport type CaptureOptions = {\n /**\n * Device pixels per CSS pixel to render at. Defaults to the map's current ratio; `2` or `3` gives\n * a print-grade capture of the same view. MapLibre clamps to its `maxCanvasSize`, so measure the\n * ratio you got from the returned canvas rather than assuming.\n */\n pixelRatio?: number;\n};\n\n// Overlay layers in compositing order; each is one absolutely positioned element over the canvas.\n// The GPU chain is not among them: it renders the picture the rest of these read, so it sits under\n// all of them — see `artworkCanvas`.\nconst OVERLAYS = ['bloom', 'grade', 'fog', 'edgeBlur', 'tint', 'vignette'] as const;\ntype OverlayName = (typeof OVERLAYS)[number];\n\n// Safari still needs `-webkit-backdrop-filter`, which the DOM typings do not carry.\ntype OverlayStyle = Partial<CSSStyleDeclaration> & { webkitBackdropFilter?: string };\n\n// A filter and a mask each go on under both the standard property and the prefixed one.\nconst backdropFilterStyle = (filter: string): OverlayStyle => ({\n backdropFilter: filter,\n webkitBackdropFilter: filter,\n});\nconst maskStyle = (mask: string): OverlayStyle => ({ maskImage: mask, webkitMaskImage: mask });\n\n// Every layer the plugin hangs over the map — the overlays and the chain's own canvas — covers the\n// container exactly, takes no pointer events, and starts hidden.\nconst LAYER_STYLE = {\n position: 'absolute',\n inset: '0',\n width: '100%',\n height: '100%',\n pointerEvents: 'none',\n display: 'none',\n} satisfies Partial<CSSStyleDeclaration>;\n\n// Device pixels per CSS pixel of a canvas — what keeps a radius in CSS pixels wherever it is used.\nconst rasterScaleOf = (canvas: HTMLCanvasElement): number => canvas.width / Math.max(canvas.clientWidth, 1);\n\n/**\n * Coalesces however many requests arrive into one `draw` per animation frame. Both the chain and\n * the bloom are made out of the map's pixels and follow its `render` event, which fires more often\n * than a frame can be drawn.\n */\nconst drawOncePerFrame = (draw: () => void) => {\n let frame: number | undefined;\n return {\n schedule: () => {\n if (frame !== undefined) return;\n\n frame = requestAnimationFrame(() => {\n frame = undefined;\n draw();\n });\n },\n cancel: () => {\n if (frame !== undefined) cancelAnimationFrame(frame);\n frame = undefined;\n },\n };\n};\n\n// The bloom canvas renders at a quarter of the pixels; its blur radius is divided to match.\nconst BLOOM_SCALE = 0.5;\n\n// Below this difference a requested capture ratio is the one the map already renders at.\nconst RATIO_EPSILON = 0.01;\n\n// `setPixelRatio(null)` is MapLibre's \"follow the device again\", which its typings spell as a number\n// only — and a number would pin an override instead.\ntype PixelRatioSetter = { setPixelRatio(pixelRatio: number | null): void };\n\n/**\n * Practical post-processing for a {@link TomTomMap}: effects computed from the rendered map's own\n * pixels, not from its style — so they work on every style, custom ones included, survive every\n * style release, and cannot be reproduced by editing the style at all.\n *\n * @remarks\n * Every effect is named by the problem it solves, and off by default:\n * - **bloom** — emphasis on dark and driving styles: lit roads, traffic tubes and route lines glow;\n * - **grade** (brightness, contrast, saturation) and **tint** — muting the base map so a data overlay\n * reads first;\n * - **fog** — depth: the rim is blurred *and* washed towards the colour of the air, which follows the\n * map's light/dark theme;\n * - **edge blur** — focus: the rim is blurred and keeps its colour, so it reads as a lens rather than\n * as distance;\n * - **depth of field** — depth on a tilted map: one band of *distance* stays sharp and the rest\n * defocuses, worked out from the camera's pitch and field of view, so a flat map ends it;\n * - **vignette** — focus too, by darkening or lightening the edges.\n *\n * Effects render live as layers over the map canvas (a WebGL2 chain, which also runs your own\n * passes from {@link addPass}, under `backdrop-filter` overlays; bloom is a small canvas blended in\n * `screen` mode) and are composited the same way into {@link capture}, so what is captured is what was on screen.\n *\n * Bloom, the depth of field, your passes and capture read the map canvas, which needs\n * `mapLibre: { canvasContextAttributes: { preserveDrawingBuffer: true } }` when creating the map;\n * the plugin warns once when it is missing.\n *\n * {@link effectKnobCatalogue} lists every knob as static data, the way `stylingKnobCatalogue` lists\n * the SDK's styling knobs.\n *\n * @example\n * ```typescript\n * import { MapEffects } from '@tomtom-org/maps-sdk-plugin-map-effects';\n *\n * const effects = new MapEffects(map);\n * // The data-viz muting half: a dim, desaturated base under your own layers.\n * effects.set({ 'grade.saturation': 0.4, 'grade.brightness': 0.85, 'tint.opacity': 0.15 });\n * // Night-driving emphasis on a dark style:\n * effects.set({ 'bloom.intensity': 0.5, 'bloom.threshold': 0.6 });\n *\n * const poster = await effects.capture({ pixelRatio: 3 });\n * ```\n *\n * @group Map Effects\n */\nexport class MapEffects {\n private settings: MapEffectsSettings = {};\n private passes: MapEffectsPass[] = [];\n private readonly container: HTMLDivElement;\n private readonly overlays: Record<OverlayName, HTMLElement>;\n private readonly bloomCanvas: HTMLCanvasElement;\n private readonly artworkCanvas: HTMLCanvasElement;\n private readonly chain = new ScreenPassRenderer();\n private readonly chainDraw = drawOncePerFrame(() => this.drawChain());\n private readonly bloomDraw = drawOncePerFrame(() => this.drawBloom());\n // What a scope's named entries resolve through, requested only once a scope names one. The SDK\n // memoizes it per map, so it is the instance the app styles the map with.\n private styling: StylingModule | undefined;\n private stylingRequest: Promise<void> | undefined;\n // Keyed on the scope's entries, since `set` replaces the array even when it is unchanged.\n private scopeMask: { key: string; tolerance: number; mask: Uint8Array | undefined } | undefined;\n private scopeCanvas: HTMLCanvasElement | undefined;\n private warnedBuffer = false;\n private warnedScope = false;\n private warnedNoWebGL2 = false;\n // Keyed on the stored copy of a pass, so a replacement that throws too is reported anew.\n private readonly warnedUniforms = new WeakSet<MapEffectsPass>();\n // A style switch or a `StylingModule` recolour moves the colours a scope resolves to.\n private readonly onStyleData = () => this.dropScopeMask();\n private readonly onRender = () => {\n this.chainDraw.schedule();\n this.scheduleBloom();\n };\n\n /**\n * Attaches the effects to a map. Nothing is visible until {@link set} turns an effect on.\n *\n * @param map - The map to render effects over.\n * @param settings - Effect knob values to apply right away.\n */\n constructor(\n private readonly map: TomTomMap,\n settings: MapEffectsSettings = {},\n ) {\n const mapContainer = this.map.mapLibreMap.getContainer();\n this.container = document.createElement('div');\n this.container.className = 'tomtom-map-effects';\n Object.assign(this.container.style, {\n position: 'absolute',\n inset: '0',\n pointerEvents: 'none',\n overflow: 'hidden',\n } satisfies Partial<CSSStyleDeclaration>);\n // Right after the canvas container, so the map's own controls stay above the effects.\n const canvasContainer = mapContainer.querySelector('.maplibregl-canvas-container');\n canvasContainer?.insertAdjacentElement('afterend', this.container) ?? mapContainer.appendChild(this.container);\n\n this.bloomCanvas = document.createElement('canvas');\n this.overlays = Object.fromEntries(\n OVERLAYS.map((name) => {\n const element = name === 'bloom' ? this.bloomCanvas : document.createElement('div');\n element.dataset.effect = name;\n Object.assign(element.style, LAYER_STYLE);\n this.container.appendChild(element);\n return [name, element];\n }),\n ) as unknown as Record<OverlayName, HTMLElement>;\n this.bloomCanvas.style.mixBlendMode = 'screen';\n\n // The chain's own canvas goes under every overlay: it replaces the picture rather than\n // filtering it, so the `backdrop-filter` layers above read its result and not the map's.\n this.artworkCanvas = this.chain.canvas;\n this.artworkCanvas.dataset.effect = 'artwork';\n Object.assign(this.artworkCanvas.style, LAYER_STYLE);\n this.container.insertAdjacentElement('afterbegin', this.artworkCanvas);\n\n this.map.mapLibreMap.on('render', this.onRender);\n this.map.mapLibreMap.on('styledata', this.onStyleData);\n this.set(settings);\n }\n\n // The two angles a depth of field is a function of; nothing else about the camera changes it.\n private cameraAngles(): CameraAngles {\n const mapLibreMap = this.map.mapLibreMap;\n return { pitch: mapLibreMap.getPitch(), verticalFieldOfView: mapLibreMap.getVerticalFieldOfView() };\n }\n\n /**\n * Sets one or more effect knobs. Values are validated against the catalogue before anything is\n * touched; knobs not mentioned keep their values.\n *\n * @param settings - Knob id → value.\n * @throws `RangeError` for a value outside its range or of the wrong kind; `Error` for an unknown id.\n *\n * @example\n * ```typescript\n * effects.set({ 'vignette.intensity': -0.4 });\n * ```\n */\n set(settings: MapEffectsSettings): void {\n for (const [id, value] of Object.entries(settings)) {\n this.validate(id, value);\n }\n this.settings = { ...this.settings, ...settings };\n this.render();\n }\n\n /**\n * The current value of a knob: the override, or the catalogue default.\n */\n get<ID extends EffectKnobId>(id: ID): EffectKnobValueOf<ID> {\n this.assertKnob(id);\n return (this.settings[id] ?? effectKnobDefinitions[id].default) as EffectKnobValueOf<ID>;\n }\n\n /**\n * Turns a knob (or every knob) back to its default — for most effects, off.\n */\n reset(id?: EffectKnobId): void {\n if (id === undefined) {\n this.settings = {};\n } else {\n this.assertKnob(id);\n const { [id]: _removed, ...rest } = this.settings;\n this.settings = rest;\n }\n this.render();\n }\n\n /**\n * The overridden knobs, as a plain serializable object.\n */\n getConfig(): MapEffectsSettings {\n return { ...this.settings };\n }\n\n /**\n * Replaces every knob with the given settings (an empty object turns everything off).\n */\n applyConfig(settings: MapEffectsSettings): void {\n this.settings = {};\n this.set(settings);\n }\n\n /**\n * Runs a GPU pass of your own in the effects chain, ahead of every overlay effect, and in\n * {@link capture} as on screen. Passes run in the order they were added, within their placement.\n *\n * @remarks\n * A pass redraws with the map. One whose uniforms change on their own — a time-driven animation —\n * asks for frames with `map.mapLibreMap.triggerRepaint()`. A pass that shifts hues runs before\n * bloom's colour key too, so a `bloom.only` scope may need a wider `bloom.onlyTolerance`.\n *\n * @throws `Error` when the pass has no id or its shader does not compile, with the compiler's log;\n * `RangeError` for an unknown placement. A shader is checked only while there is a WebGL2 context:\n * without one no pass runs and the plugin warns once, and after a lost one it is checked on restore.\n *\n * @example\n * ```typescript\n * effects.addPass({\n * id: 'posterise',\n * fragment: `uniform float uLevels;\n * void main() {\n * vec4 colour = texture(uSource, vUv);\n * fragColor = vec4(floor(colour.rgb * uLevels) / uLevels, colour.a);\n * }`,\n * uniforms: { uLevels: 5 },\n * });\n * ```\n */\n addPass(pass: MapEffectsPass): void {\n if (!pass.id) throw new Error('A pass needs an id, to be replaced or removed by.');\n\n if (pass.placement !== undefined && !Object.hasOwn(PASS_PLACEMENTS, pass.placement)) {\n throw new RangeError(\n `Pass '${pass.id}' expects a placement of ${Object.keys(PASS_PLACEMENTS).join(' or ')}; got ${String(pass.placement)}.`,\n );\n }\n if (!this.chain.hasContext) this.warnNoWebGL2();\n\n const error = this.chain.compileError(pass.fragment);\n if (error) throw new Error(`Pass '${pass.id}' ${error}`);\n\n const added = { ...pass };\n const replaced = this.passes.find((existing) => existing.id === pass.id);\n this.passes = replaced\n ? this.passes.map((existing) => (existing === replaced ? added : existing))\n : [...this.passes, added];\n if (replaced) this.releaseUnused(replaced.fragment);\n\n this.warnIfBufferNotPreserved();\n this.render();\n }\n\n /** Takes a pass added with {@link addPass} out of the chain. An id with no pass is ignored. */\n removePass(id: string): void {\n const removed = this.passes.find((pass) => pass.id === id);\n if (!removed) return;\n\n this.passes = this.passes.filter((pass) => pass !== removed);\n this.releaseUnused(removed.fragment);\n this.render();\n }\n\n /**\n * Renders the current view — map and effects — into a canvas, optionally at a higher pixel ratio\n * for print. The map is re-rendered at that ratio (never resampled), read back once every tile is\n * drawn, and restored to follow the device afterwards.\n *\n * @remarks\n * Needs `preserveDrawingBuffer: true` on the map's canvas context; without it the map half of the\n * capture is blank. Viewers cannot download from a sandboxed page, so hand the canvas to your own\n * upload or `toBlob` flow.\n */\n async capture(options: CaptureOptions = {}): Promise<HTMLCanvasElement> {\n this.warnIfBufferNotPreserved();\n const mapLibreMap = this.map.mapLibreMap;\n const ratioSetter: PixelRatioSetter = mapLibreMap;\n const live = mapLibreMap.getPixelRatio();\n const wanted = options.pixelRatio ?? live;\n const wasOverridden = live !== (globalThis.devicePixelRatio || 1);\n const rerenders = Math.abs(wanted - live) > RATIO_EPSILON;\n if (rerenders) {\n ratioSetter.setPixelRatio(wanted);\n await this.settled();\n }\n try {\n return await this.readBack();\n } finally {\n if (rerenders) ratioSetter.setPixelRatio(wasOverridden ? live : null);\n }\n }\n\n /**\n * Detaches the effects from the map, removes their elements and releases the WebGL context the\n * chain holds — which a page with several maps on it needs, since a browser caps how many it\n * may keep alive.\n */\n remove(): void {\n this.map.mapLibreMap.off('render', this.onRender);\n this.map.mapLibreMap.off('styledata', this.onStyleData);\n this.bloomDraw.cancel();\n this.chainDraw.cancel();\n this.chain.remove();\n this.container.remove();\n }\n\n // ── Live rendering ────────────────────────────────────────────────────────────────────────\n\n private render(): void {\n const values = withDefaults(this.settings);\n const show = (name: OverlayName, on: boolean, style: OverlayStyle = {}) => {\n const element = this.overlays[name];\n element.style.display = on ? 'block' : 'none';\n Object.assign(element.style, style);\n };\n\n const grade = gradeFilter(values['grade.brightness'], values['grade.contrast'], values['grade.saturation']);\n show('grade', grade !== '', backdropFilterStyle(grade));\n\n const lightDark = this.map.styleLightDarkTheme;\n show('fog', values['fog.intensity'] > 0, {\n ...backdropFilterStyle(fogFilter(values['fog.intensity'], lightDark)),\n ...maskStyle(rimMask(values['fog.reach'])),\n backgroundImage: fogVeil(values['fog.intensity'], values['fog.reach'], lightDark),\n });\n\n show('edgeBlur', values['edgeBlur.intensity'] > 0, {\n ...backdropFilterStyle(defocusFilter(values['edgeBlur.intensity'])),\n ...maskStyle(rimMask(values['edgeBlur.reach'])),\n });\n\n if (values['depthOfField.intensity'] > 0) this.warnIfBufferNotPreserved();\n this.drawChain(values);\n\n show('tint', values['tint.opacity'] > 0, {\n backgroundColor: values['tint.color'],\n opacity: String(values['tint.opacity']),\n });\n\n show('vignette', values['vignette.intensity'] !== 0, {\n backgroundImage: vignetteGradient(values['vignette.intensity'], values['vignette.reach']),\n });\n\n const bloomOn = values['bloom.intensity'] > 0;\n show('bloom', bloomOn, { opacity: String(values['bloom.intensity']) });\n if (bloomOn) {\n this.warnIfBufferNotPreserved();\n this.scheduleBloom();\n }\n }\n\n /**\n * The passes the chain runs over the map's pixels, in order: the app's placed before the depth of\n * field, the depth of field, then the app's placed after it. Each reads what the one before left,\n * without the picture leaving the GPU.\n */\n private chainStages(values: EffectValues): PassStage[] {\n return [\n ...this.passStages('before-depth-of-field'),\n ...this.depthOfFieldStages(values),\n ...this.passStages('after-depth-of-field'),\n ];\n }\n\n private passStages(placement: MapEffectsPassPlacement): PassStage[] {\n return this.passes\n .filter((pass) => (pass.placement ?? 'before-depth-of-field') === placement)\n .flatMap((pass) => {\n const uniforms = this.uniformsOf(pass);\n return uniforms ? [{ fragment: pass.fragment, uniforms }] : [];\n });\n }\n\n // A `uniforms()` that throws skips its pass for the draw, rather than breaking every frame the map renders.\n private uniformsOf(pass: MapEffectsPass): MapEffectsPassUniforms | undefined {\n const { uniforms = {} } = pass;\n if (typeof uniforms !== 'function') return uniforms;\n\n try {\n return uniforms();\n } catch (error) {\n if (!this.warnedUniforms.has(pass)) {\n this.warnedUniforms.add(pass);\n console.warn(`[MapEffects] pass '${pass.id}' is skipped while its uniforms() throws:`, error);\n }\n return undefined;\n }\n }\n\n // A program per distinct shader holds GPU memory, so it goes as soon as no pass runs that shader.\n private releaseUnused(fragment: string): void {\n if (!this.passes.some((pass) => pass.fragment === fragment)) this.chain.release(fragment);\n }\n\n private depthOfFieldStages(values: EffectValues): PassStage[] {\n const source = this.map.mapLibreMap.getCanvas();\n const uniforms = defocusUniforms(\n {\n focus: values['depthOfField.focus'],\n band: values['depthOfField.band'],\n blurCssPx: values['depthOfField.intensity'],\n bokeh: values['depthOfField.bokeh'],\n },\n this.cameraAngles(),\n { width: source.width, height: source.height, scale: rasterScaleOf(source) },\n );\n return uniforms ? [{ fragment: DEFOCUS_SHADER, uniforms, mipmapped: true }] : [];\n }\n\n private drawChain(values: EffectValues = withDefaults(this.settings)): void {\n const stages = this.chainStages(values);\n const rendered = stages.length ? this.chain.render(this.map.mapLibreMap.getCanvas(), stages) : undefined;\n this.artworkCanvas.style.display = rendered ? 'block' : 'none';\n }\n\n private scheduleBloom(): void {\n if (this.get('bloom.intensity') > 0) this.bloomDraw.schedule();\n }\n\n private drawBloom(): void {\n // The finished artwork when the chain drew one, so a defocused highlight glows as the wide\n // soft disc a photograph gives it rather than as the pinpoint it was before the lens.\n const source =\n this.artworkCanvas.style.display === 'block' ? this.artworkCanvas : this.map.mapLibreMap.getCanvas();\n const width = Math.max(1, Math.round(source.width * BLOOM_SCALE));\n const height = Math.max(1, Math.round(source.height * BLOOM_SCALE));\n resizeCanvas(this.bloomCanvas, width, height);\n const context = this.bloomCanvas.getContext('2d');\n if (!context) return;\n\n // The scope drops its uncovered pixels before the threshold, so the blur spreads only its colours.\n const glowSource = this.scopedSource(source, width, height) ?? source;\n context.filter = bloomFilter(this.get('bloom.radius'), this.get('bloom.threshold'), BLOOM_SCALE);\n context.clearRect(0, 0, width, height);\n context.drawImage(glowSource, 0, 0, width, height);\n }\n\n // The map's pixels with everything outside the scope blacked out; undefined when bloom lights the whole frame.\n private scopedSource(source: HTMLCanvasElement, width: number, height: number): HTMLCanvasElement | undefined {\n const mask = this.currentScopeMask();\n if (!mask) return undefined;\n\n this.scopeCanvas ??= document.createElement('canvas');\n const canvas = this.scopeCanvas;\n resizeCanvas(canvas, width, height);\n const context = canvas.getContext('2d', { willReadFrequently: true });\n if (!context) return undefined;\n\n context.clearRect(0, 0, width, height);\n context.drawImage(source, 0, 0, width, height);\n applyScopeMask(context, mask);\n return canvas;\n }\n\n // Cached: resolving the colours reads the styling catalogue and building the cube visits 32³ colours.\n private currentScopeMask(): Uint8Array | undefined {\n const scope = this.get('bloom.only');\n if (scope.length === 0) return undefined;\n\n const key = scope.join('\\n');\n const tolerance = this.get('bloom.onlyTolerance');\n if (this.scopeMask?.key !== key || this.scopeMask.tolerance !== tolerance) {\n this.scopeMask = { key, tolerance, mask: this.buildScopeMaskFor(scope, tolerance) };\n }\n return this.scopeMask.mask;\n }\n\n private buildScopeMaskFor(scope: BloomScope, tolerance: number): Uint8Array | undefined {\n const readsStyling = scopeReadsStyling(scope);\n if (readsStyling) this.requestStyling();\n\n const colors = scopeColors(this.styling, scope);\n if (colors.length === 0 && (!readsStyling || this.styling)) this.warnScopeEmpty(scope);\n return buildScopeMask(colors, tolerance);\n }\n\n // An empty scope lights the whole frame, which passes for a working one. The caller skips the\n // warning while the styling module is still on its way.\n private warnScopeEmpty(scope: BloomScope): void {\n if (this.warnedScope) return;\n\n this.warnedScope = true;\n console.warn(\n `[MapEffects] 'bloom.only: ${scope.join(', ')}' found no colour to key on in the loaded style, so bloom is lighting the whole frame.`,\n );\n }\n\n // Until the module arrives, named entries key on nothing and bloom lights the whole frame.\n private requestStyling(): void {\n this.stylingRequest ??= StylingModule.get(this.map).then((styling) => {\n this.styling = styling;\n this.dropScopeMask();\n });\n }\n\n // The redraw is required: an idle map fires no `render`, so the stale glow would stay until the map moved.\n private dropScopeMask(): void {\n this.scopeMask = undefined;\n this.scheduleBloom();\n }\n\n // ── Capture ───────────────────────────────────────────────────────────────────────────────\n\n private readBack(): Promise<HTMLCanvasElement> {\n const mapLibreMap = this.map.mapLibreMap;\n return new Promise((resolve) => {\n mapLibreMap.once('render', () => {\n const source = mapLibreMap.getCanvas();\n const values = withDefaults(this.settings);\n const composite = document.createElement('canvas');\n composite.width = source.width;\n composite.height = source.height;\n const context = composite.getContext('2d');\n if (context) {\n // The same chain the screen is showing, re-run at the capture's own raster —\n // one implementation rather than a live half and an export half.\n const stages = this.chainStages(values);\n context.drawImage(this.chain.render(source, stages) ?? source, 0, 0);\n compositeEffects(\n context,\n source.width,\n source.height,\n rasterScaleOf(source),\n values,\n this.map.styleLightDarkTheme,\n this.currentScopeMask(),\n );\n }\n resolve(composite);\n });\n mapLibreMap.triggerRepaint();\n });\n }\n\n // Resolves once every source is loaded and every tile drawn at the new ratio, or after a timeout.\n private settled(timeoutMs = 15_000): Promise<void> {\n const mapLibreMap = this.map.mapLibreMap;\n return new Promise((resolve) => {\n const done = () => {\n clearTimeout(timer);\n mapLibreMap.off('idle', done);\n resolve();\n };\n const timer = setTimeout(done, timeoutMs);\n mapLibreMap.once('idle', done);\n });\n }\n\n private warnIfBufferNotPreserved(): void {\n if (this.warnedBuffer) return;\n\n const canvas = this.map.mapLibreMap.getCanvas();\n const context = canvas.getContext('webgl2') ?? canvas.getContext('webgl');\n const attributes = context?.getContextAttributes?.();\n if (attributes && !attributes.preserveDrawingBuffer) {\n this.warnedBuffer = true;\n console.warn(\n '[MapEffects] bloom, the depth of field, app passes and capture read the map canvas, which needs ' +\n \"`mapLibre: { canvasContextAttributes: { preserveDrawingBuffer: true } }` on the map's options.\",\n );\n }\n }\n\n // Without a context there is no compiler to refuse a broken shader, and no chain to run a sound one.\n private warnNoWebGL2(): void {\n if (this.warnedNoWebGL2) return;\n\n this.warnedNoWebGL2 = true;\n console.warn(\n '[MapEffects] this browser gives no WebGL2 context, so passes from addPass neither compile nor run.',\n );\n }\n\n // ── Validation ────────────────────────────────────────────────────────────────────────────\n\n private assertKnob(id: string): asserts id is EffectKnobId {\n if (!(id in effectKnobDefinitions)) {\n throw new Error(`Unknown effect knob '${id}'. effectKnobIds lists every knob id.`);\n }\n }\n\n private validate(id: string, value: unknown): void {\n this.assertKnob(id);\n const definition: EffectKnobDefinition = effectKnobDefinitions[id];\n if (definition.kind === 'colors') {\n if (!Array.isArray(value))\n throw new RangeError(`Knob '${id}' expects a list of colours; got ${String(value)}.`);\n\n const reason = value.map(invalidScopeEntry).find((invalid) => invalid !== undefined);\n if (reason)\n throw new RangeError(`Knob '${id}': ${reason}. effectKnobCatalogue lists the names it accepts.`);\n return;\n }\n if (definition.kind === 'color') {\n if (typeof value !== 'string' || !value)\n throw new RangeError(`Knob '${id}' expects a CSS colour; got ${String(value)}.`);\n return;\n }\n const { range } = definition;\n if (typeof value !== 'number' || !Number.isFinite(value)) {\n throw new RangeError(`Knob '${id}' expects a number; got ${String(value)}.`);\n }\n if (value < range.min || value > range.max) {\n throw new RangeError(`Knob '${id}' expects a number between ${range.min} and ${range.max}; got ${value}.`);\n }\n }\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  [
2
2
  {
3
3
  "name": "maplibre-gl",
4
- "version": "6.9.0",
4
+ "version": "6.11.2",
5
5
  "license": "BSD-3-Clause",
6
6
  "bundled": false,
7
7
  "description": "BSD licensed community fork of mapbox-gl, a WebGL interactive maps library",
package/package.json CHANGED
@@ -2,26 +2,26 @@
2
2
  "name": "@tomtom-org/maps-sdk-plugin-map-effects",
3
3
  "author": "TomTom International B.V.",
4
4
  "license": "See in LICENSE.txt",
5
- "version": "0.0.3",
5
+ "version": "0.1.0",
6
6
  "type": "module",
7
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",
8
8
  "types": "dist/index.d.ts",
9
9
  "peerDependencies": {
10
- "@tomtom-org/maps-sdk": ">=0.56.0 <1.0.0",
11
- "maplibre-gl": "^6.9.0"
10
+ "@tomtom-org/maps-sdk": ">=0.58.0 <1.0.0",
11
+ "maplibre-gl": "^6.11.2"
12
12
  },
13
13
  "devDependencies": {
14
14
  "@playwright/test": "1.60.0",
15
15
  "@tsconfig/recommended": "^1.0.13",
16
- "@types/node": "^26.3.0",
16
+ "@types/node": "^26.6.2",
17
17
  "@vitest/coverage-v8": "^4.1.11",
18
18
  "jsdom": "^29.1.1",
19
- "rolldown": "^1.2.5",
19
+ "rolldown": "^1.2.11",
20
20
  "ts-node": "^10.9.2",
21
21
  "typescript": "^6.0.3",
22
- "vite": "^8.2.2",
22
+ "vite": "^8.3.1",
23
23
  "vitest": "^4.1.11",
24
- "@tomtom-org/maps-sdk": "0.56.0"
24
+ "@tomtom-org/maps-sdk": "0.58.0"
25
25
  },
26
26
  "files": [
27
27
  "dist",