@tomtom-org/maps-sdk-plugin-map-effects 0.2.4 → 0.3.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/LICENSE.txt +2 -2
- package/README.md +14 -10
- package/THIRD_PARTY.txt +1 -1
- package/dist/THIRD_PARTY.txt +1 -1
- package/dist/index.d.ts +43 -27
- package/dist/index.es.js +1 -1
- package/dist/third-party.json +1 -1
- package/package.json +9 -9
package/LICENSE.txt
CHANGED
|
@@ -14,7 +14,7 @@ You may not use the SDK or accept this Agreement if you are barred from receivin
|
|
|
14
14
|
|
|
15
15
|
3.1. You agree to use the SDK only for purposes permitted by (a) this Agreement and (b) any applicable law, regulation or generally accepted practice or guideline in the relevant jurisdictions (including any laws regarding the export of data or software to and from the United States or other relevant countries).
|
|
16
16
|
|
|
17
|
-
3.2.
|
|
17
|
+
3.2. Except as permitted by applicable law or this Clause 3.2, you may not copy (except for backup purposes), modify, adapt, distribute, rent, lend, publicly present, decompile, reverse engineer, disassemble or create derivative works of the SDK. Solely for your Permitted Applications and subject to Clauses 3.3 to 3.5, you may: (a) inspect the SDK, including source code in its source maps, for debugging; (b) copy the SDK as reasonably necessary to develop, build, test and deploy them, including in internal repositories, mirrors and offline build environments used only by you, your Affiliates and your contractors; (c) modify the SDK to fix errors or security vulnerabilities, to maintain compatibility, or to prepare a Contribution under Clause 11.5, but you may not alter, circumvent or disable any functionality for authentication, metering, billing, the notices in Clause 5.2 or the data collection in Clause 4; and (d) distribute the SDK only as a bundled part of a Permitted Application, and not on a standalone basis or as a library. Modified versions remain part of the SDK, and TomTom need not support them. On termination, you must delete all copies held under (b), except copies already distributed under (d). The readability of the SDK's source code does not make it open source and grants no rights beyond those in Clause 1 and this Clause 3.2.
|
|
18
18
|
|
|
19
19
|
3.3. You may not use the SDK in connection with any illegal, fraudulent, dishonest or unethical activity or behaviour, and in particular not to create or develop, or help others create or develop, a product, service or library that competes, directly or indirectly, with the SDK or the TomTom APIs.
|
|
20
20
|
|
|
@@ -22,7 +22,7 @@ You may not use the SDK or accept this Agreement if you are barred from receivin
|
|
|
22
22
|
|
|
23
23
|
3.5. The SDK may include data or software that TomTom licenses from third parties, including open-source software. Such components are made available to you under the terms and conditions of their applicable licenses, and you shall comply with all requirements and restrictions that those third parties may require TomTom to impose on you (as may be updated and amended by TomTom from time to time). It is your responsibility to check, review and comply with those terms as they apply to the SDK licensed to you under this Agreement. The applicable third party and open-source notices are set out in the accompanying THIRD_PARTY.txt file. In the event of a conflict between this Agreement and an open-source license governing a specific dependency, that open-source license shall prevail solely with respect to that dependency.
|
|
24
24
|
|
|
25
|
-
4. PRIVACY: In order to improve the TomTom Developer Portal and the SDK, TomTom may collect certain usage related information from the software, including but not limited to general SDK and product identifiers, version numbers, and information on which tools and/or services in the SDK are used and how you use them. In addition, TomTom may collect your unique identifier and associated IP address for legitimate business reasons (such as, without limitation, the processing of invoices). This data is retained for a maximum period of 120 days and is maintained in accordance with our Privacy Policy.
|
|
25
|
+
4. PRIVACY: In order to improve the TomTom Developer Portal and the SDK, TomTom may collect certain usage related information from the software, including but not limited to general SDK and product identifiers, version numbers, and information on which tools and/or services in the SDK are used and how you use them. In addition, TomTom may collect your unique identifier and associated IP address for legitimate business reasons (such as, without limitation, the processing of invoices). This data is retained for a maximum period of 120 days and is maintained in accordance with our Privacy Policy (https://www.tomtom.com/privacy/).
|
|
26
26
|
|
|
27
27
|
5. INTELLECTUAL PROPERTY RIGHTS AND COPYRIGHT NOTICES:
|
|
28
28
|
|
package/README.md
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
# Map Effects Plugin
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This plugin for the [TomTom Maps SDK for JavaScript](https://docs.tomtom.com/maps-sdk-js/introduction/overview) adds practical post-processing to a `TomTomMap`. It computes its effects from the rendered map's own pixels, so they work on every style and survive every style release. Editing the style can't reproduce them.
|
|
4
4
|
|
|
5
|
-
Every effect is
|
|
5
|
+
Every effect is off by default. Here is each one, with the problem it solves:
|
|
6
6
|
|
|
7
7
|
| Effect | Knobs | Solves |
|
|
8
8
|
| --- | --- | --- |
|
|
9
|
-
| Bloom | `bloom.intensity`, `bloom.radius`, `bloom.threshold`, `bloom.only`, `bloom.onlyTolerance` | Emphasis on dark and driving styles
|
|
9
|
+
| Bloom | `bloom.intensity`, `bloom.radius`, `bloom.threshold`, `bloom.only`, `bloom.onlyTolerance` | Emphasis on dark and driving styles. Lit roads, traffic tubes and route lines glow, optionally only in the colours `bloom.only` lists |
|
|
10
10
|
| Grade | `grade.brightness`, `grade.contrast`, `grade.saturation` | Muting the base map under a data overlay |
|
|
11
|
-
| Tint | `tint.color`, `tint.opacity` | Muting the base map
|
|
11
|
+
| Tint | `tint.color`, `tint.opacity` | Muting the base map, or matching a brand |
|
|
12
12
|
| Fog | `fog.intensity`, `fog.reach` | Depth cue and de-emphasis of far context |
|
|
13
13
|
| Edge blur | `edgeBlur.intensity`, `edgeBlur.reach` | Focus on the centre of the view |
|
|
14
|
-
| Depth of field | `depthOfField.intensity`, `depthOfField.focus`, `depthOfField.band`, `depthOfField.bokeh` | Depth on a tilted map
|
|
14
|
+
| Depth of field | `depthOfField.intensity`, `depthOfField.focus`, `depthOfField.band`, `depthOfField.bokeh` | Depth on a tilted map. A GPU pass blurs more with distance and returns highlights as discs. A flat map has no depth, so the effect ends there |
|
|
15
15
|
| Vignette | `vignette.intensity`, `vignette.reach` | Focus on the centre of the view |
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Your own stylisation, such as a watercolour or a posterise, runs inside the same GPU chain with `addPass(pass)`. Every effect above then works on your picture.
|
|
18
18
|
|
|
19
19
|
## Docs & examples
|
|
20
20
|
|
|
@@ -24,7 +24,7 @@ A stylisation of your own — a watercolour, a posterise — runs inside the sam
|
|
|
24
24
|
|
|
25
25
|
## Quickstart
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
This plugin declares `@tomtom-org/maps-sdk` and `maplibre-gl` as peer dependencies, so install both in your project too.
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
30
|
npm install @tomtom-org/maps-sdk maplibre-gl @tomtom-org/maps-sdk-plugin-map-effects
|
|
@@ -35,16 +35,20 @@ npm install @tomtom-org/maps-sdk maplibre-gl @tomtom-org/maps-sdk-plugin-map-eff
|
|
|
35
35
|
2. Import and use the plugin:
|
|
36
36
|
|
|
37
37
|
```ts
|
|
38
|
-
import {
|
|
38
|
+
import { setKnob } from '@tomtom-org/maps-sdk/map';
|
|
39
|
+
import { effectKnobCatalogue, MapEffects } from '@tomtom-org/maps-sdk-plugin-map-effects';
|
|
39
40
|
|
|
40
41
|
// assume `map` is your initialized TomTomMap instance
|
|
41
42
|
const effects = new MapEffects(map);
|
|
42
43
|
|
|
43
44
|
// A quiet base under your own data layers:
|
|
44
|
-
effects.updateConfig({
|
|
45
|
+
effects.updateConfig({ grade: { saturation: 0.4, brightness: 0.85 }, tint: { opacity: 0.15 } });
|
|
45
46
|
|
|
46
47
|
// Emphasis on a dark style:
|
|
47
|
-
effects.updateConfig({
|
|
48
|
+
effects.updateConfig({ bloom: { intensity: 0.5, threshold: 0.6 } });
|
|
49
|
+
|
|
50
|
+
// One knob by its id, from a control or a language model, the others kept:
|
|
51
|
+
setKnob(effects, effectKnobCatalogue, 'bloom.threshold', 0.4);
|
|
48
52
|
|
|
49
53
|
effects.resetConfig(); // everything off
|
|
50
54
|
await effects.capture({ pixelRatio: 3 }); // map + effects as a canvas, for print
|
package/THIRD_PARTY.txt
CHANGED
|
@@ -14,7 +14,7 @@ by the licenses reproduced here.
|
|
|
14
14
|
--------------------------------------------------------------------------------
|
|
15
15
|
|
|
16
16
|
Name: maplibre-gl
|
|
17
|
-
Version: 6.
|
|
17
|
+
Version: 6.13.0
|
|
18
18
|
License: BSD-3-Clause
|
|
19
19
|
Bundled: false (peer dependency)
|
|
20
20
|
Description: BSD licensed community fork of mapbox-gl, a WebGL interactive maps library
|
package/dist/THIRD_PARTY.txt
CHANGED
|
@@ -4,7 +4,7 @@ your application bundles them. The licenses below apply to those copies, and eac
|
|
|
4
4
|
these components may in turn install its own dependencies under their own licenses.
|
|
5
5
|
|
|
6
6
|
Name: maplibre-gl
|
|
7
|
-
Version: 6.
|
|
7
|
+
Version: 6.13.0
|
|
8
8
|
License: BSD-3-Clause
|
|
9
9
|
Bundled: false (peer dependency)
|
|
10
10
|
Description: BSD licensed community fork of mapbox-gl, a WebGL interactive maps library
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { KnobKind, KnobRange, StyleColorKnobId, TomTomMap } from "@tomtom-org/maps-sdk/map";
|
|
1
|
+
import { KnobConfigOf, 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
|
/**
|
|
@@ -237,7 +237,7 @@ declare const effectKnobDefinitions: {
|
|
|
237
237
|
};
|
|
238
238
|
};
|
|
239
239
|
/**
|
|
240
|
-
* The id of an effect knob, as accepted by the SDK's `setKnob` and as a key of {@link
|
|
240
|
+
* The id of an effect knob, as accepted by the SDK's `setKnob` and as a key of {@link MapEffectsConfig}.
|
|
241
241
|
*
|
|
242
242
|
* @group Map Effects
|
|
243
243
|
*/
|
|
@@ -303,11 +303,17 @@ type MapEffectsPassUniforms = Record<string, number | readonly [number, number]>
|
|
|
303
303
|
//#endregion
|
|
304
304
|
//#region src/MapEffects.d.ts
|
|
305
305
|
/**
|
|
306
|
-
* The effect
|
|
306
|
+
* The effect values an app overrides, as {@link MapEffects.getConfig} returns them. Each knob id is
|
|
307
|
+
* its path in here, so the `bloom.intensity` knob is `config.bloom.intensity`. It's plain JSON.
|
|
308
|
+
*
|
|
309
|
+
* @example
|
|
310
|
+
* ```typescript
|
|
311
|
+
* const config: MapEffectsConfig = { grade: { saturation: 0.4, brightness: 0.85 }, tint: { opacity: 0.15 } };
|
|
312
|
+
* ```
|
|
307
313
|
*
|
|
308
314
|
* @group Map Effects
|
|
309
315
|
*/
|
|
310
|
-
type
|
|
316
|
+
type MapEffectsConfig = KnobConfigOf<{ [ID in EffectKnobId]: EffectKnobValueOf<ID>; }>;
|
|
311
317
|
/**
|
|
312
318
|
* Where a {@link MapEffectsPass} runs in the GPU chain, relative to the plugin's own depth of field.
|
|
313
319
|
* Either way it runs before every overlay effect, so bloom, grade, fog, edge blur, tint and vignette
|
|
@@ -380,19 +386,19 @@ type CaptureOptions = {
|
|
|
380
386
|
* The plugin warns once when it is missing.
|
|
381
387
|
*
|
|
382
388
|
* {@link effectKnobCatalogue} lists every knob as static data, the way `stylingFoundationsKnobCatalogue` lists
|
|
383
|
-
* the SDK's styling knobs. The SDK's `setKnob`,
|
|
384
|
-
*
|
|
389
|
+
* the SDK's styling knobs. Each id is its path into {@link MapEffectsConfig}. The SDK's `setKnob`,
|
|
390
|
+
* `setKnobs`, `getKnob` and `resetKnob` set, read and unset knobs by id, as on a map module.
|
|
385
391
|
*
|
|
386
392
|
* @example
|
|
387
393
|
* ```typescript
|
|
388
|
-
* import { setKnob } from '@tomtom-org/maps-sdk/map';
|
|
394
|
+
* import { setKnob, setKnobs } from '@tomtom-org/maps-sdk/map';
|
|
389
395
|
* import { effectKnobCatalogue, MapEffects } from '@tomtom-org/maps-sdk-plugin-map-effects';
|
|
390
396
|
*
|
|
391
397
|
* const effects = new MapEffects(map);
|
|
392
398
|
* // The data-viz muting half: a dim, desaturated base under your own layers.
|
|
393
|
-
* effects.
|
|
394
|
-
* // Night-driving emphasis on a dark style:
|
|
395
|
-
* effects
|
|
399
|
+
* effects.applyConfig({ grade: { saturation: 0.4, brightness: 0.85 }, tint: { opacity: 0.15 } });
|
|
400
|
+
* // Night-driving emphasis on a dark style, keeping the rest:
|
|
401
|
+
* setKnobs(effects, effectKnobCatalogue, { 'bloom.intensity': 0.5, 'bloom.threshold': 0.6 });
|
|
396
402
|
* setKnob(effects, effectKnobCatalogue, 'vignette.intensity', -0.4);
|
|
397
403
|
*
|
|
398
404
|
* const poster = await effects.capture({ pixelRatio: 3 });
|
|
@@ -402,7 +408,7 @@ type CaptureOptions = {
|
|
|
402
408
|
*/
|
|
403
409
|
export declare class MapEffects {
|
|
404
410
|
private readonly map;
|
|
405
|
-
private
|
|
411
|
+
private config;
|
|
406
412
|
private passes;
|
|
407
413
|
private readonly container;
|
|
408
414
|
private readonly overlays;
|
|
@@ -423,32 +429,42 @@ export declare class MapEffects {
|
|
|
423
429
|
* Attaches the effects to a map. Nothing is visible until a knob turns an effect on.
|
|
424
430
|
*
|
|
425
431
|
* @param map - The map to render effects over.
|
|
426
|
-
* @param
|
|
432
|
+
* @param config - Effect knob values to apply right away.
|
|
427
433
|
*/
|
|
428
|
-
constructor(map: TomTomMap,
|
|
434
|
+
constructor(map: TomTomMap, config?: MapEffectsConfig);
|
|
429
435
|
private cameraAngles;
|
|
430
436
|
/**
|
|
431
|
-
* Returns the
|
|
437
|
+
* Returns the configuration, as a plain serializable copy.
|
|
432
438
|
*/
|
|
433
|
-
getConfig():
|
|
439
|
+
getConfig(): MapEffectsConfig;
|
|
434
440
|
/**
|
|
435
|
-
* Replaces
|
|
436
|
-
*
|
|
441
|
+
* Replaces the whole configuration. An empty object turns everything off.
|
|
442
|
+
*
|
|
443
|
+
* @remarks
|
|
444
|
+
* Use it, and {@link updateConfig}, for a look your code writes, such as a use case swapped in
|
|
445
|
+
* whole: as on a map module, the type is the check, and the values are not checked again. For
|
|
446
|
+
* values held as data, the SDK's `setKnobs` checks each against the catalogue and sets them by id.
|
|
437
447
|
*
|
|
438
|
-
* @
|
|
448
|
+
* @example
|
|
449
|
+
* ```typescript
|
|
450
|
+
* effects.applyConfig({ grade: { saturation: 0.4, brightness: 0.85 }, tint: { opacity: 0.15 } });
|
|
451
|
+
* ```
|
|
439
452
|
*/
|
|
440
|
-
applyConfig(
|
|
453
|
+
applyConfig(config: MapEffectsConfig): void;
|
|
441
454
|
/**
|
|
442
|
-
* Changes the given
|
|
455
|
+
* Changes the given effects and keeps the others, one level deep as a map module's
|
|
456
|
+
* `updateConfig` does: an effect you pass replaces that effect's values.
|
|
443
457
|
*
|
|
444
|
-
* @
|
|
458
|
+
* @remarks
|
|
459
|
+
* To change some values of an effect and keep its others, use the SDK's `setKnobs`:
|
|
460
|
+
* `setKnobs(effects, effectKnobCatalogue, { 'bloom.intensity': 0.5 })`.
|
|
445
461
|
*
|
|
446
462
|
* @example
|
|
447
463
|
* ```typescript
|
|
448
|
-
* effects.updateConfig({
|
|
464
|
+
* effects.updateConfig({ bloom: { intensity: 0.5, threshold: 0.6 } }); // the bloom's radius goes back to its default
|
|
449
465
|
* ```
|
|
450
466
|
*/
|
|
451
|
-
updateConfig(partial:
|
|
467
|
+
updateConfig(partial: MapEffectsConfig): void;
|
|
452
468
|
/**
|
|
453
469
|
* Resets every knob to its default, off for most effects.
|
|
454
470
|
*/
|
|
@@ -460,7 +476,7 @@ export declare class MapEffects {
|
|
|
460
476
|
/**
|
|
461
477
|
* @ignore
|
|
462
478
|
*/
|
|
463
|
-
|
|
479
|
+
_setKnobValues(values: Readonly<Record<string, unknown>>): void;
|
|
464
480
|
/**
|
|
465
481
|
* @ignore
|
|
466
482
|
*/
|
|
@@ -511,7 +527,9 @@ export declare class MapEffects {
|
|
|
511
527
|
* may keep alive.
|
|
512
528
|
*/
|
|
513
529
|
remove(): void;
|
|
530
|
+
private show;
|
|
514
531
|
private render;
|
|
532
|
+
private showFog;
|
|
515
533
|
/**
|
|
516
534
|
* The passes the chain runs over the map's pixels, in order: the app's placed before the depth of
|
|
517
535
|
* field, the depth of field, then the app's placed after it. Each reads what the one before left,
|
|
@@ -534,8 +552,6 @@ export declare class MapEffects {
|
|
|
534
552
|
private settled;
|
|
535
553
|
private warnIfBufferNotPreserved;
|
|
536
554
|
private warnNoWebGL2;
|
|
537
|
-
private assertKnob;
|
|
538
|
-
private validate;
|
|
539
555
|
}
|
|
540
556
|
//#endregion
|
|
541
|
-
export type { BloomScope, BloomScopeEntry, CaptureOptions, EffectKnob, EffectKnobId, EffectKnobRange, EffectKnobValueOf, MapEffectsPass, MapEffectsPassPlacement, MapEffectsPassUniforms,
|
|
557
|
+
export type { BloomScope, BloomScopeEntry, CaptureOptions, EffectKnob, EffectKnobId, EffectKnobRange, EffectKnobValueOf, MapEffectsConfig, MapEffectsPass, MapEffectsPassPlacement, MapEffectsPassUniforms, StyleColorKnobGroup };
|
package/dist/index.es.js
CHANGED
|
@@ -1 +1 @@
|
|
|
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};
|
|
1
|
+
import{configWithKnobs as e,configWithoutKnob as t,getStyleColor as a,knobValueIn as s,parseCssColor as n,styleColorKnobIds as r}from"@tomtom-org/maps-sdk/map";var i=e=>e.split(".").slice(0,-1).map((e,t,a)=>a.slice(0,t+1).join(".")),o=(()=>{const e=/* @__PURE__ */new Map;for(const t of r)for(const a of[...i(t),t])e.set(a,[...e.get(a)??[],t]);return e})(),h=[...o.keys()],l=(e,t,a)=>32*(32*(e>>3)+(t>>3))+(a>>3),c=([e,t,a])=>{const s=Math.max(e,t,a);if(0!==s)return[e/s,t/s,a/s]},d=(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[l(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)},u={min:0,max:1,step:.05,bounds:"hard"},f="Emphasis on dark and driving styles.",p="Muting the base map under a data overlay.",g="Depth cue: the far context recedes as if seen through air.",m="Focus: the centre stays sharp and the edges lose their detail.",b="Depth on a tilted map: one band of distance stays sharp, the rest defocuses with distance.",w="Muting the base map under a data overlay; matching a brand.",v="Focus on the centre of the view.",y={"bloom.intensity":{kind:"number",description:"How strongly bright features (lit roads, traffic tubes, route lines) glow. 0 = off.",default:0,range:u,useCase:f},"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:f},"bloom.threshold":{kind:"number",description:"Brightness below which nothing glows. Raise it so only the brightest features light up.",default:.55,range:u,useCase:f},"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:h,useCase:f},"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:u,useCase:f},"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:p},"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:p},"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:p},"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:u,useCase:g},"fog.reach":{kind:"number",description:"How far the fog reaches in from the edges (1 = to the centre).",default:.4,range:u,useCase:g},"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:m},"edgeBlur.reach":{kind:"number",description:"How far the edge blur reaches in from the edges (1 = to the centre).",default:.5,range:u,useCase:m},"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:b},"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:u,useCase:b},"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:u,useCase:b},"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:u,useCase:b},"tint.color":{kind:"color",description:"A flat colour laid over the whole map.",default:"#1f2937",useCase:w},"tint.opacity":{kind:"number",description:"Opacity of the tint. 0 = off.",default:0,range:u,useCase:w},"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:v},"vignette.reach":{kind:"number",description:"How far the vignette reaches in from the edges (1 = to the centre).",default:.45,range:u,useCase:v}},x=Object.keys(y),T=x.map(e=>({id:e,...y[e]})),E=(e,t,a)=>Math.min(Math.max(e,t),a),k=e=>E(e,0,1),C=(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)`},S=(e,t,a)=>[1===e?"":`brightness(${e})`,1===t?"":`contrast(${t})`,1===a?"":`saturate(${a})`].filter(Boolean).join(" "),M=(e,t,a)=>`radial-gradient(ellipse farthest-corner at center, ${e} ${Math.round(100*(1-k(a)))}%, ${t} 100%)`,R=(e,t)=>{const{inner:a,outer:s}=L(e);return M(a,s,t)},L=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)})`}},B=e=>M("transparent","black",e),P=e=>e*Math.PI/180,A=(e,t,a)=>{const s=(({pitch:e,verticalFieldOfView:t})=>{const a=Math.tan(P(E(e,0,89))),s=2*Math.tan(P(E(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)]}},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)"}},_="saturate(1.08)",F=e=>D[e].grade,U=e=>2+6*e,I=(e,t)=>`blur(${U(e)}px) ${F(t)}`,O=(e,t)=>{const{rgb:a}=D[t];return{inner:`rgba(${a}, 0)`,outer:`rgba(${a}, ${.65*e})`}},N=(e,t,a)=>{const{inner:s,outer:n}=O(e,a);return M(s,n,t)},$=(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},G=(e,t,a,s=1)=>{e.save(),e.globalCompositeOperation=a,e.globalAlpha=s,e.drawImage(t.canvas,0,0),e.restore()},W=(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()},X=(e,t,a,s,n,r,i)=>{const o=$(e,t,a,`blur(${n*s}px) ${r}`.trim());o&&(o.globalCompositeOperation="destination-in",i(o),e.drawImage(o.canvas,0,0))},j=(e,t,a,s)=>{const n=$(e,t,a,"none");return n&&d(n,s),n},V=(e,t,a,s,n,r,i)=>{n["bloom.intensity"]>0&&((e,t,a,s,n,r)=>{const i=C(n["bloom.radius"],n["bloom.threshold"],s),o=r?j(e,t,a,r):e;if(!o)return;const h=$(o,t,a,i);h&&G(e,h,"screen",n["bloom.intensity"])})(e,t,a,s,n,i);const o=S(n["grade.brightness"],n["grade.contrast"],n["grade.saturation"]);o&&((e,t,a,s)=>{const n=$(e,t,a,s);n&&G(e,n,"copy")})(e,t,a,o);const h=e=>s=>W(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}=O(i,r),d=U(i);X(e,t,a,s,d,F(r),h(o)),W(e,t,a,o,l,c)}if(n["edgeBlur.intensity"]>0){const r=n["edgeBlur.intensity"];X(e,t,a,s,r,_,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}=L(n["vignette.intensity"]);W(e,t,a,n["vignette.reach"],s,r)}},H=(e,t)=>s(e,t)??y[t].default,z=e=>Object.fromEntries(x.map(t=>[t,H(e,t)])),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)},K=(e,t,a)=>{e.width===t&&e.height===a||(e.width=t,e.height=a)},Y=(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;K(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),q(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),q(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=[Y(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"),Y(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}`}},J={"before-depth-of-field":!0,"after-depth-of-field":!0},Z=["bloom","grade","fog","edgeBlur","tint","vignette"],ee=e=>({backdropFilter:e,webkitBackdropFilter:e}),te=e=>({maskImage:e,webkitMaskImage:e}),ae={position:"absolute",inset:"0",width:"100%",height:"100%",pointerEvents:"none",display:"none"},se=e=>e.width/Math.max(e.clientWidth,1),ne=e=>{let t;return{schedule:()=>{void 0===t&&(t=requestAnimationFrame(()=>{t=void 0,e()}))},cancel:()=>{void 0!==t&&cancelAnimationFrame(t),t=void 0}}},re=class{map;config={};passes=[];container;overlays;bloomCanvas;artworkCanvas;chain=new Q;chainDraw=ne(()=>this.drawChain());bloomDraw=ne(()=>this.drawBloom());scopeMask;scopeCanvas;warnedBuffer=!1;warnedScope=!1;warnedNoWebGL2=!1;warnedUniforms=/* @__PURE__ */new WeakSet;onStyleData=()=>{this.dropScopeMask(),this.showFog(z(this.config))};onRender=()=>{this.chainDraw.schedule(),this.scheduleBloom()};constructor(e,t={}){this.map=e;const a=this.map.mapLibreMap.getContainer();this.container=document.createElement("div"),this.container.className="tomtom-map-effects",Object.assign(this.container.style,{position:"absolute",inset:"0",pointerEvents:"none",overflow:"hidden"}),a.querySelector(".maplibregl-canvas-container")?.insertAdjacentElement("afterend",this.container)??a.appendChild(this.container),this.bloomCanvas=document.createElement("canvas"),this.overlays=Object.fromEntries(Z.map(e=>{const t="bloom"===e?this.bloomCanvas:document.createElement("div");return t.dataset.effect=e,Object.assign(t.style,ae),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,ae),this.container.insertAdjacentElement("afterbegin",this.artworkCanvas),this.map.mapLibreMap.on("render",this.onRender),this.map.mapLibreMap.on("styledata",this.onStyleData),this.applyConfig(t)}cameraAngles(){const e=this.map.mapLibreMap;return{pitch:e.getPitch(),verticalFieldOfView:e.getVerticalFieldOfView()}}getConfig(){return structuredClone(this.config)}applyConfig(e){this.config=e,this.render()}updateConfig(e){this.applyConfig({...this.config,...e})}resetConfig(){this.applyConfig({})}_knobValue(e){return s(this.config,e)}_setKnobValues(t){this.applyConfig(e(this.config,t))}_resetKnobValue(e){this.applyConfig(t(this.config,e))}addPass(e){if(!e.id)throw new Error("A pass needs an id, to be replaced or removed by.");if(void 0!==e.placement&&!Object.hasOwn(J,e.placement))throw new RangeError(`Pass '${e.id}' expects a placement of ${Object.keys(J).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()}show(e,t,a={}){const s=this.overlays[e];s.style.display=t?"block":"none",Object.assign(s.style,a)}render(){const e=z(this.config),t=S(e["grade.brightness"],e["grade.contrast"],e["grade.saturation"]);var a;this.show("grade",""!==t,ee(t)),this.showFog(e),this.show("edgeBlur",e["edgeBlur.intensity"]>0,{...ee((a=e["edgeBlur.intensity"],`blur(${a}px) ${_}`)),...te(B(e["edgeBlur.reach"]))}),e["depthOfField.intensity"]>0&&this.warnIfBufferNotPreserved(),this.drawChain(e),this.show("tint",e["tint.opacity"]>0,{backgroundColor:e["tint.color"],opacity:String(e["tint.opacity"])}),this.show("vignette",0!==e["vignette.intensity"],{backgroundImage:R(e["vignette.intensity"],e["vignette.reach"])});const s=e["bloom.intensity"]>0;this.show("bloom",s,{opacity:String(e["bloom.intensity"])}),s&&(this.warnIfBufferNotPreserved(),this.scheduleBloom())}showFog(e){const t=this.map.styleLightDarkTheme;this.show("fog",e["fog.intensity"]>0,{...ee(I(e["fog.intensity"],t)),...te(B(e["fog.reach"])),backgroundImage:N(e["fog.intensity"],e["fog.reach"],t)})}chainStages(e){return[...this.passStages("before-depth-of-field"),...this.depthOfFieldStages(e),...this.passStages("after-depth-of-field")]}passStages(e){return this.passes.filter(t=>(t.placement??"before-depth-of-field")===e).flatMap(e=>{const t=this.uniformsOf(e);return t?[{fragment:e.fragment,uniforms:t}]:[]})}uniformsOf(e){const{uniforms:t={}}=e;if("function"!=typeof t)return t;try{return t()}catch(a){return void(this.warnedUniforms.has(e)||(this.warnedUniforms.add(e),console.warn(`[MapEffects] pass '${e.id}' is skipped while its uniforms() throws:`,a)))}}releaseUnused(e){this.passes.some(t=>t.fragment===e)||this.chain.release(e)}depthOfFieldStages(e){const t=this.map.mapLibreMap.getCanvas(),a=A({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:se(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=z(this.config)){const t=this.chainStages(e),a=t.length?this.chain.render(this.map.mapLibreMap.getCanvas(),t):void 0;this.artworkCanvas.style.display=a?"block":"none"}scheduleBloom(){H(this.config,"bloom.intensity")>0&&this.bloomDraw.schedule()}drawBloom(){const e="block"===this.artworkCanvas.style.display?this.artworkCanvas:this.map.mapLibreMap.getCanvas(),t=Math.max(1,Math.round(.5*e.width)),a=Math.max(1,Math.round(.5*e.height));K(this.bloomCanvas,t,a);const s=this.bloomCanvas.getContext("2d");if(!s)return;const n=this.scopedSource(e,t,a)??e;s.filter=C(H(this.config,"bloom.radius"),H(this.config,"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;K(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),d(r,s),n):void 0}currentScopeMask(){const e=H(this.config,"bloom.only");if(0===e.length)return;const t=e.join("\n"),a=H(this.config,"bloom.onlyTolerance");return this.scopeMask?.key===t&&this.scopeMask.tolerance===a||(this.scopeMask={key:t,tolerance:a,mask:this.buildScopeMaskFor(e,a)}),this.scopeMask.mask}buildScopeMaskFor(e,t){const s=((e,t)=>t.flatMap(t=>{const s=o.get(t);return(s?s.map(t=>a(e,t)):[t]).map(e=>"string"==typeof e?n(e):void 0).filter(e=>void 0!==e)}))(this.map,e);return 0===s.length&&this.warnScopeEmpty(e),((e,t)=>{const a=e.map(c).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=c([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[l(r,e,t)]=1)}return n})(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.`))}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=z(this.config),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),V(r,a.width,a.height,se(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."))}};export{re as MapEffects,T as effectKnobCatalogue,x as effectKnobIds};
|
package/dist/third-party.json
CHANGED
package/package.json
CHANGED
|
@@ -2,25 +2,25 @@
|
|
|
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.
|
|
5
|
+
"version": "0.3.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": ">=
|
|
11
|
-
"maplibre-gl": "^6.
|
|
10
|
+
"@tomtom-org/maps-sdk": ">=1.0.0-rc.2 <1.0.0",
|
|
11
|
+
"maplibre-gl": "^6.13.0"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
14
|
"@playwright/test": "1.60.0",
|
|
15
15
|
"@tsconfig/recommended": "^1.0.13",
|
|
16
16
|
"@types/node": "^26.6.4",
|
|
17
|
-
"@vitest/coverage-v8": "^
|
|
18
|
-
"jsdom": "^
|
|
19
|
-
"rolldown": "^1.2.
|
|
17
|
+
"@vitest/coverage-v8": "^5.0.3",
|
|
18
|
+
"jsdom": "^30.1.2",
|
|
19
|
+
"rolldown": "^1.2.13",
|
|
20
20
|
"typescript": "^6.0.3",
|
|
21
|
-
"vite": "
|
|
22
|
-
"vitest": "^
|
|
23
|
-
"@tomtom-org/maps-sdk": "1.0.0-rc.
|
|
21
|
+
"vite": "8.3.3",
|
|
22
|
+
"vitest": "^5.0.3",
|
|
23
|
+
"@tomtom-org/maps-sdk": "1.0.0-rc.2"
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
26
|
"dist",
|