@tomtom-org/maps-sdk-plugin-map-effects 0.1.1 → 0.2.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/dist/index.d.ts CHANGED
@@ -1,21 +1,20 @@
1
- import { KnobKind, KnobRange, StylingColorKnobId, TomTomMap } from "@tomtom-org/maps-sdk/map";
1
+ import { KnobKind, KnobRange, StyleColorKnobId, 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
  /**
5
- * A family of colour knobs, named by the prefix their ids share — `traffic` for every traffic
6
- * colour, `traffic.incidents` for the incident severities, `colors` for the base map's own.
5
+ * A family of style colour knobs, named by the prefix their map-wide ids share — `traffic` for every
6
+ * traffic colour, `traffic.incidents` for the incident severities, `colors` for the base map's own.
7
7
  *
8
8
  * @group Map Effects
9
9
  */
10
- type StylingColorKnobGroup = GroupsOf<StylingColorKnobId>;
10
+ type StyleColorKnobGroup = GroupsOf<StyleColorKnobId>;
11
11
  /**
12
- * One entry of a bloom scope: a styling colour knob, a group of them, or a CSS colour — the last
13
- * for what the styling module does not paint, such as a route line in the `color` its
14
- * `RoutingModule` was given.
12
+ * One entry of a bloom scope: a style colour knob, a group of them, or a CSS colour — the last for
13
+ * what no style knob paints, such as a route line in the `color` its `RoutingModule` was given.
15
14
  *
16
15
  * @group Map Effects
17
16
  */
18
- type BloomScopeEntry = StylingColorKnobId | StylingColorKnobGroup | (string & {});
17
+ type BloomScopeEntry = StyleColorKnobId | StyleColorKnobGroup | (string & {});
19
18
  /**
20
19
  * The colours bloom may light. Empty — the default — lights the whole frame.
21
20
  *
@@ -77,6 +76,7 @@ declare const effectKnobDefinitions: {
77
76
  readonly min: 2;
78
77
  readonly max: 40;
79
78
  readonly step: 1;
79
+ readonly bounds: "hard";
80
80
  };
81
81
  readonly useCase: "Emphasis on dark and driving styles.";
82
82
  };
@@ -89,7 +89,7 @@ declare const effectKnobDefinitions: {
89
89
  };
90
90
  readonly 'bloom.only': {
91
91
  readonly kind: "colors";
92
- readonly 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.";
92
+ readonly description: "The colours bloom may light: CSS colours, style colour knobs or groups of them (`traffic`, `traffic.incidents.colors.major`), read from the loaded style. Only the pixels wearing one of them glow; empty lights the whole frame.";
93
93
  readonly default: readonly [];
94
94
  readonly options: readonly string[];
95
95
  readonly useCase: "Emphasis on dark and driving styles.";
@@ -109,6 +109,7 @@ declare const effectKnobDefinitions: {
109
109
  readonly min: 0.5;
110
110
  readonly max: 1.5;
111
111
  readonly step: 0.05;
112
+ readonly bounds: "hard";
112
113
  };
113
114
  readonly useCase: "Muting the base map under a data overlay.";
114
115
  };
@@ -120,6 +121,7 @@ declare const effectKnobDefinitions: {
120
121
  readonly min: 0.5;
121
122
  readonly max: 1.5;
122
123
  readonly step: 0.05;
124
+ readonly bounds: "hard";
123
125
  };
124
126
  readonly useCase: "Muting the base map under a data overlay.";
125
127
  };
@@ -131,6 +133,7 @@ declare const effectKnobDefinitions: {
131
133
  readonly min: 0;
132
134
  readonly max: 1.5;
133
135
  readonly step: 0.05;
136
+ readonly bounds: "hard";
134
137
  };
135
138
  readonly useCase: "Muting the base map under a data overlay.";
136
139
  };
@@ -156,6 +159,7 @@ declare const effectKnobDefinitions: {
156
159
  readonly min: 0;
157
160
  readonly max: 30;
158
161
  readonly step: 1;
162
+ readonly bounds: "hard";
159
163
  };
160
164
  readonly useCase: "Focus: the centre stays sharp and the edges lose their detail.";
161
165
  };
@@ -174,6 +178,7 @@ declare const effectKnobDefinitions: {
174
178
  readonly min: 0;
175
179
  readonly max: 30;
176
180
  readonly step: 1;
181
+ readonly bounds: "hard";
177
182
  };
178
183
  readonly useCase: "Depth on a tilted map: one band of distance stays sharp, the rest defocuses with distance.";
179
184
  };
@@ -219,6 +224,7 @@ declare const effectKnobDefinitions: {
219
224
  readonly min: -1;
220
225
  readonly max: 1;
221
226
  readonly step: 0.05;
227
+ readonly bounds: "hard";
222
228
  };
223
229
  readonly useCase: "Focus on the centre of the view.";
224
230
  };
@@ -373,18 +379,21 @@ type CaptureOptions = {
373
379
  * `mapLibre: { canvasContextAttributes: { preserveDrawingBuffer: true } }` when creating the map;
374
380
  * the plugin warns once when it is missing.
375
381
  *
376
- * {@link effectKnobCatalogue} lists every knob as static data, the way `stylingKnobCatalogue` lists
377
- * the SDK's styling knobs.
382
+ * {@link effectKnobCatalogue} lists every knob as static data, the way `stylingFoundationsKnobCatalogue` lists
383
+ * the SDK's styling knobs, and the SDK's `setKnob`, `getKnob` and `resetKnob` set, read and unset
384
+ * one by its id, as on a map module.
378
385
  *
379
386
  * @example
380
387
  * ```typescript
381
- * import { MapEffects } from '@tomtom-org/maps-sdk-plugin-map-effects';
388
+ * import { setKnob } from '@tomtom-org/maps-sdk/map';
389
+ * import { effectKnobCatalogue, MapEffects } from '@tomtom-org/maps-sdk-plugin-map-effects';
382
390
  *
383
391
  * const effects = new MapEffects(map);
384
392
  * // The data-viz muting half: a dim, desaturated base under your own layers.
385
- * effects.set({ 'grade.saturation': 0.4, 'grade.brightness': 0.85, 'tint.opacity': 0.15 });
393
+ * effects.updateConfig({ 'grade.saturation': 0.4, 'grade.brightness': 0.85, 'tint.opacity': 0.15 });
386
394
  * // Night-driving emphasis on a dark style:
387
- * effects.set({ 'bloom.intensity': 0.5, 'bloom.threshold': 0.6 });
395
+ * effects.updateConfig({ 'bloom.intensity': 0.5, 'bloom.threshold': 0.6 });
396
+ * setKnob(effects, effectKnobCatalogue, 'vignette.intensity', -0.4);
388
397
  *
389
398
  * const poster = await effects.capture({ pixelRatio: 3 });
390
399
  * ```
@@ -402,8 +411,6 @@ export declare class MapEffects {
402
411
  private readonly chain;
403
412
  private readonly chainDraw;
404
413
  private readonly bloomDraw;
405
- private styling;
406
- private stylingRequest;
407
414
  private scopeMask;
408
415
  private scopeCanvas;
409
416
  private warnedBuffer;
@@ -413,7 +420,7 @@ export declare class MapEffects {
413
420
  private readonly onStyleData;
414
421
  private readonly onRender;
415
422
  /**
416
- * Attaches the effects to a map. Nothing is visible until {@link set} turns an effect on.
423
+ * Attaches the effects to a map. Nothing is visible until a knob turns an effect on.
417
424
  *
418
425
  * @param map - The map to render effects over.
419
426
  * @param settings - Effect knob values to apply right away.
@@ -421,34 +428,43 @@ export declare class MapEffects {
421
428
  constructor(map: TomTomMap, settings?: MapEffectsSettings);
422
429
  private cameraAngles;
423
430
  /**
424
- * Sets one or more effect knobs. Values are validated against the catalogue before anything is
425
- * touched; knobs not mentioned keep their values.
431
+ * The knobs set, as a plain serializable object.
432
+ */
433
+ getConfig(): MapEffectsSettings;
434
+ /**
435
+ * Replaces every knob with the given settings; an empty object turns everything off. Values are
436
+ * validated against the catalogue before anything is touched.
437
+ *
438
+ * @throws `RangeError` for a value outside its range or of the wrong kind; `Error` for an unknown id.
439
+ */
440
+ applyConfig(settings: MapEffectsSettings): void;
441
+ /**
442
+ * Changes the given knobs and keeps the rest, as a map module's `updateConfig` does.
426
443
  *
427
- * @param settings - Knob id → value.
428
444
  * @throws `RangeError` for a value outside its range or of the wrong kind; `Error` for an unknown id.
429
445
  *
430
446
  * @example
431
447
  * ```typescript
432
- * effects.set({ 'vignette.intensity': -0.4 });
448
+ * effects.updateConfig({ 'bloom.intensity': 0.5, 'bloom.threshold': 0.6 });
433
449
  * ```
434
450
  */
435
- set(settings: MapEffectsSettings): void;
451
+ updateConfig(partial: MapEffectsSettings): void;
436
452
  /**
437
- * The current value of a knob: the override, or the catalogue default.
453
+ * Turns every knob back to its default — for most effects, off.
438
454
  */
439
- get<ID extends EffectKnobId>(id: ID): EffectKnobValueOf<ID>;
455
+ resetConfig(): void;
440
456
  /**
441
- * Turns a knob (or every knob) back to its default — for most effects, off.
457
+ * @ignore
442
458
  */
443
- reset(id?: EffectKnobId): void;
459
+ _knobValue(id: string): unknown;
444
460
  /**
445
- * The overridden knobs, as a plain serializable object.
461
+ * @ignore
446
462
  */
447
- getConfig(): MapEffectsSettings;
463
+ _setKnobValue(id: string, value: unknown): void;
448
464
  /**
449
- * Replaces every knob with the given settings (an empty object turns everything off).
465
+ * @ignore
450
466
  */
451
- applyConfig(settings: MapEffectsSettings): void;
467
+ _resetKnobValue(id: string): void;
452
468
  /**
453
469
  * Runs a GPU pass of your own in the effects chain, ahead of every overlay effect, and in
454
470
  * {@link capture} as on screen. Passes run in the order they were added, within their placement.
@@ -513,7 +529,6 @@ export declare class MapEffects {
513
529
  private currentScopeMask;
514
530
  private buildScopeMaskFor;
515
531
  private warnScopeEmpty;
516
- private requestStyling;
517
532
  private dropScopeMask;
518
533
  private readBack;
519
534
  private settled;
@@ -523,4 +538,4 @@ export declare class MapEffects {
523
538
  private validate;
524
539
  }
525
540
  //#endregion
526
- export type { BloomScope, BloomScopeEntry, CaptureOptions, EffectKnob, EffectKnobId, EffectKnobRange, EffectKnobValueOf, MapEffectsPass, MapEffectsPassPlacement, MapEffectsPassUniforms, MapEffectsSettings, StylingColorKnobGroup };
541
+ export type { BloomScope, BloomScopeEntry, CaptureOptions, EffectKnob, EffectKnobId, EffectKnobRange, EffectKnobValueOf, MapEffectsPass, MapEffectsPassPlacement, MapEffectsPassUniforms, MapEffectsSettings, StyleColorKnobGroup };
package/dist/index.es.js CHANGED
@@ -1,2 +1,2 @@
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};
1
+ import{getStyleColor as e,parseCssColor as t,styleColorKnobIds as a,validateKnobValue as s}from"@tomtom-org/maps-sdk/map";var n=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 a)for(const a of[...n(t),t])e.set(a,[...e.get(a)??[],t]);return e})(),i=[...r.keys()],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)},d={min:0,max:1,step:.05,bounds:"hard"},c="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.",p="Focus: the centre stays sharp and the edges lose their detail.",g="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:d,useCase:c},"bloom.radius":{kind:"number",description:"How far the glow spreads, in CSS pixels.",default:12,range:{min:2,max:40,step:1,bounds:"hard"},useCase:c},"bloom.threshold":{kind:"number",description:"Brightness below which nothing glows. Raise it so only the brightest features light up.",default:.55,range:d,useCase:c},"bloom.only":{kind:"colors",description:"The colours bloom may light: CSS colours, style colour knobs or groups of them (`traffic`, `traffic.incidents.colors.major`), read from the loaded style. Only the pixels wearing one of them glow; empty lights the whole frame.",default:[],options:i,useCase:c},"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:c},"grade.brightness":{kind:"number",description:"Brightness of the whole base map (1 = unchanged).",default:1,range:{min:.5,max:1.5,step:.05,bounds:"hard"},useCase:u},"grade.contrast":{kind:"number",description:"Contrast of the whole base map (1 = unchanged).",default:1,range:{min:.5,max:1.5,step:.05,bounds:"hard"},useCase: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,bounds:"hard"},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: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,bounds:"hard"},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,bounds:"hard"},useCase:g},"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:g},"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:g},"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:g},"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:d,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,bounds:"hard"},useCase:b},"vignette.reach":{kind:"number",description:"How far the vignette reaches in from the edges (1 = to the centre).",default:.45,range:d,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),k=e=>x(e,0,1),T=(e,t,a=1)=>{const s=.92*k(t),n=(1+s)/(1-s);return`brightness(${(1/(1+s)).toFixed(4)}) contrast(${n.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:s}=M(e);return C(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=>C("transparent","black",e),L=e=>e*Math.PI/180,B=(e,t,a)=>{const s=(({pitch:e,verticalFieldOfView:t})=>{const a=Math.tan(L(x(e,0,89))),s=2*Math.tan(L(x(t,1,175))/2)*a;return s/(1+s/2)})(t);if(!(s<=0||e.blurCssPx<=0))return{uDepth:s,uFocus:Math.min(k(e.focus)*s,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)]}},P={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)"}},A="saturate(1.08)",D=e=>P[e].grade,_=e=>2+6*e,F=(e,t)=>`blur(${_(e)}px) ${D(t)}`,U=(e,t)=>{const{rgb:a}=P[t];return{inner:`rgba(${a}, 0)`,outer:`rgba(${a}, ${.65*e})`}},I=(e,t,a)=>{const{inner:s,outer:n}=U(e,a);return C(s,n,t)},O=(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},N=(e,t,a,s=1)=>{e.save(),e.globalCompositeOperation=a,e.globalAlpha=s,e.drawImage(t.canvas,0,0),e.restore()},$=(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=O(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=O(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=T(n["bloom.radius"],n["bloom.threshold"],s),o=r?W(e,t,a,r):e;if(!o)return;const h=O(o,t,a,i);h&&N(e,h,"screen",n["bloom.intensity"])})(e,t,a,s,n,i);const o=E(n["grade.brightness"],n["grade.contrast"],n["grade.saturation"]);o&&((e,t,a,s)=>{const n=O(e,t,a,s);n&&N(e,n,"copy")})(e,t,a,o);const h=e=>s=>$(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:d}=U(i,r),c=_(i);G(e,t,a,s,c,D(r),h(o)),$(e,t,a,o,l,d)}if(n["edgeBlur.intensity"]>0){const r=n["edgeBlur.intensity"];G(e,t,a,s,r,A,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"]);$(e,t,a,n["vignette.reach"],s,r)}},j=(e,t)=>e[t]??w[t].default,V=e=>Object.fromEntries(v.map(t=>[t,j(e,t)])),H=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)},z=(e,t,a)=>{e.width===t&&e.height===a||(e.width=t,e.height=a)},K=(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}`},q=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;z(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),H(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),H(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=[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"),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}`)],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}`}},Y={"before-depth-of-field":!0,"after-depth-of-field":!0},Q=["bloom","grade","fog","edgeBlur","tint","vignette"],J=e=>({backdropFilter:e,webkitBackdropFilter:e}),Z=e=>({maskImage:e,webkitMaskImage:e}),ee={position:"absolute",inset:"0",width:"100%",height:"100%",pointerEvents:"none",display:"none"},te=e=>e.width/Math.max(e.clientWidth,1),ae=e=>{let t;return{schedule:()=>{void 0===t&&(t=requestAnimationFrame(()=>{t=void 0,e()}))},cancel:()=>{void 0!==t&&cancelAnimationFrame(t),t=void 0}}},se=class{map;settings={};passes=[];container;overlays;bloomCanvas;artworkCanvas;chain=new q;chainDraw=ae(()=>this.drawChain());bloomDraw=ae(()=>this.drawBloom());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(Q.map(e=>{const t="bloom"===e?this.bloomCanvas:document.createElement("div");return t.dataset.effect=e,Object.assign(t.style,ee),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,ee),this.container.insertAdjacentElement("afterbegin",this.artworkCanvas),this.map.mapLibreMap.on("render",this.onRender),this.map.mapLibreMap.on("styledata",this.onStyleData),this.applyConfig(t)}cameraAngles(){const e=this.map.mapLibreMap;return{pitch:e.getPitch(),verticalFieldOfView:e.getVerticalFieldOfView()}}getConfig(){return{...this.settings}}applyConfig(e){for(const[t,a]of Object.entries(e))this.validate(t,a);this.settings={...e},this.render()}updateConfig(e){this.applyConfig({...this.settings,...e})}resetConfig(){this.applyConfig({})}_knobValue(e){return this.assertKnob(e),this.settings[e]}_setKnobValue(e,t){this.updateConfig({[e]:t})}_resetKnobValue(e){this.assertKnob(e);const{[e]:t,...a}=this.settings;this.applyConfig(a)}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(Y,e.placement))throw new RangeError(`Pass '${e.id}' expects a placement of ${Object.keys(Y).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=V(this.settings),t=(e,t,a={})=>{const s=this.overlays[e];s.style.display=t?"block":"none",Object.assign(s.style,a)},a=E(e["grade.brightness"],e["grade.contrast"],e["grade.saturation"]);t("grade",""!==a,J(a));const s=this.map.styleLightDarkTheme;var n;t("fog",e["fog.intensity"]>0,{...J(F(e["fog.intensity"],s)),...Z(R(e["fog.reach"])),backgroundImage:I(e["fog.intensity"],e["fog.reach"],s)}),t("edgeBlur",e["edgeBlur.intensity"]>0,{...J((n=e["edgeBlur.intensity"],`blur(${n}px) ${A}`)),...Z(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){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=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:te(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=V(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(){j(this.settings,"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));z(this.bloomCanvas,t,a);const s=this.bloomCanvas.getContext("2d");if(!s)return;const n=this.scopedSource(e,t,a)??e;s.filter=T(j(this.settings,"bloom.radius"),j(this.settings,"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;z(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=j(this.settings,"bloom.only");if(0===e.length)return;const t=e.join("\n"),a=j(this.settings,"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(a,s){const n=((a,s)=>s.flatMap(s=>{const n=r.get(s);return(n?n.map(t=>e(a,t)):[s]).map(e=>"string"==typeof e?t(e):void 0).filter(e=>void 0!==e)}))(this.map,a);return 0===n.length&&this.warnScopeEmpty(a),((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})(n,s)}warnScopeEmpty(e){this.warnedScope||(this.warnedScope=!0,console.warn(`[MapEffects] 'bloom.only: ${e.join(", ")}' found no colour to key on in the loaded style, so bloom is lighting the whole frame.`))}dropScopeMask(){this.scopeMask=void 0,this.scheduleBloom()}readBack(){const e=this.map.mapLibreMap;return new Promise(t=>{e.once("render",()=>{const a=e.getCanvas(),s=V(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,te(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),s({id:e,...w[e]},t)}};export{se as MapEffects,y as effectKnobCatalogue,v as effectKnobIds};
2
2
  //# sourceMappingURL=index.es.js.map