@tomtom-org/maps-sdk-plugin-map-effects 0.0.2

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.es.js","names":[],"sources":["../src/effectsCatalogue.ts","../src/effectFilters.ts","../src/composite.ts","../src/effectValues.ts","../src/MapEffects.ts"],"sourcesContent":["import type { KnobKind, KnobRange } from '@tomtom-org/maps-sdk/map';\n\n/**\n * The range of a numeric effect knob. Every one ships a slider `step`; the shared {@link KnobRange}\n * leaves it optional.\n *\n * @group Map Effects\n */\nexport type EffectKnobRange = KnobRange & { step: number };\n\n/**\n * One effect knob, drawn from the SDK's shared knob vocabulary — so a UI or an agent built for\n * `StylingModule.describe()` reads this catalogue unchanged.\n * @ignore\n */\nexport type EffectKnobDefinition = {\n kind: KnobKind;\n description: string;\n default: number | string;\n range?: EffectKnobRange;\n /** The practical problem the effect solves — the reason it exists in an SDK with no artistic ambitions. */\n useCase: string;\n};\n\nconst FRACTION: EffectKnobRange = { min: 0, max: 1, step: 0.05 };\n\n// The practical problem each effect solves. Every knob of one effect carries the same one.\nconst USE_CASE = {\n bloom: 'Emphasis on dark and driving styles.',\n grade: 'Muting the base map under a data overlay.',\n fog: 'Depth cue: the far context recedes as if seen through air.',\n edgeBlur: 'Focus: the centre stays sharp and the edges lose their detail.',\n tint: 'Muting the base map under a data overlay; matching a brand.',\n vignette: 'Focus on the centre of the view.',\n} as const;\n\n/**\n * The effects catalogue. Every effect is off at its default; each is named by the product problem it\n * solves. Ordering is the compositing order: bloom → grade → fog → edge blur → tint → vignette.\n * @ignore\n */\nexport const effectKnobDefinitions = {\n 'bloom.intensity': {\n kind: 'number',\n description: 'How strongly bright features (lit roads, traffic tubes, route lines) glow. 0 = off.',\n default: 0,\n range: FRACTION,\n useCase: USE_CASE.bloom,\n },\n 'bloom.radius': {\n kind: 'number',\n description: 'How far the glow spreads, in CSS pixels.',\n default: 12,\n range: { min: 2, max: 40, step: 1 },\n useCase: USE_CASE.bloom,\n },\n 'bloom.threshold': {\n kind: 'number',\n description: 'Brightness below which nothing glows. Raise it so only the brightest features light up.',\n default: 0.55,\n range: FRACTION,\n useCase: USE_CASE.bloom,\n },\n 'grade.brightness': {\n kind: 'number',\n description: 'Brightness of the whole base map (1 = unchanged).',\n default: 1,\n range: { min: 0.5, max: 1.5, step: 0.05 },\n useCase: USE_CASE.grade,\n },\n 'grade.contrast': {\n kind: 'number',\n description: 'Contrast of the whole base map (1 = unchanged).',\n default: 1,\n range: { min: 0.5, max: 1.5, step: 0.05 },\n useCase: USE_CASE.grade,\n },\n 'grade.saturation': {\n kind: 'number',\n description: 'Colour saturation of the whole base map (1 = unchanged, 0 = greyscale).',\n default: 1,\n range: { min: 0, max: 1.5, step: 0.05 },\n useCase: USE_CASE.grade,\n },\n 'fog.intensity': {\n kind: 'number',\n description:\n 'Haze over the edges of the view: blur, plus a wash towards the colour of the air — pale on a light map, dark on a dark one. Reads as distance. 0 = off.',\n default: 0,\n range: FRACTION,\n useCase: USE_CASE.fog,\n },\n 'fog.reach': {\n kind: 'number',\n description: 'How far the fog reaches in from the edges (1 = to the centre).',\n default: 0.4,\n range: FRACTION,\n useCase: USE_CASE.fog,\n },\n 'edgeBlur.intensity': {\n kind: 'number',\n description:\n 'Blur over the edges of the view, in CSS pixels, leaving their colour untouched. Reads as a lens, not as distance — `fog` is the effect that also washes the rim out. 0 = off.',\n default: 0,\n range: { min: 0, max: 30, step: 1 },\n useCase: USE_CASE.edgeBlur,\n },\n 'edgeBlur.reach': {\n kind: 'number',\n description: 'How far the edge blur reaches in from the edges (1 = to the centre).',\n default: 0.5,\n range: FRACTION,\n useCase: USE_CASE.edgeBlur,\n },\n 'tint.color': {\n kind: 'color',\n description: 'A flat colour laid over the whole map.',\n default: '#1f2937',\n useCase: USE_CASE.tint,\n },\n 'tint.opacity': {\n kind: 'number',\n description: 'Opacity of the tint. 0 = off.',\n default: 0,\n range: FRACTION,\n useCase: USE_CASE.tint,\n },\n 'vignette.intensity': {\n kind: 'number',\n description: 'Darkening (negative) or lightening (positive) towards the edges. 0 = off.',\n default: 0,\n range: { min: -1, max: 1, step: 0.05 },\n useCase: USE_CASE.vignette,\n },\n 'vignette.reach': {\n kind: 'number',\n description: 'How far the vignette reaches in from the edges (1 = to the centre).',\n default: 0.45,\n range: FRACTION,\n useCase: USE_CASE.vignette,\n },\n} as const satisfies Record<string, EffectKnobDefinition>;\n\n/**\n * The id of an effect knob, as accepted by {@link MapEffects.set}.\n *\n * @group Map Effects\n */\nexport type EffectKnobId = keyof typeof effectKnobDefinitions;\n\n/**\n * Every effect knob id, in compositing order.\n *\n * @group Map Effects\n */\nexport const effectKnobIds = Object.keys(effectKnobDefinitions) as EffectKnobId[];\n\n/**\n * The value type an effect knob takes.\n *\n * @group Map Effects\n */\nexport type EffectKnobValueOf<ID extends EffectKnobId> = (typeof effectKnobDefinitions)[ID]['kind'] extends 'color'\n ? string\n : number;\n","/**\n * The maths behind every effect, as pure functions: CSS filter strings and gradient descriptions the\n * live overlays use through `backdrop-filter`, and the same values the capture path feeds into a 2D\n * canvas. One implementation, two consumers — never a live/export pair that can drift.\n *\n * @module\n * @ignore\n */\n\nimport type { LightDark } from '@tomtom-org/maps-sdk/map';\n\n// The threshold's usable ceiling: at 1 the contrast term would divide by zero.\nconst BLOOM_CUT_MAX = 0.92;\nconst BLOOM_SATURATE = 1.35;\n\n// Knob values arrive validated against the catalogue, so this only guards the maths against a\n// caller reaching these functions directly.\nconst asFraction = (value: number): number => Math.min(Math.max(value, 0), 1);\n\n/**\n * The filter chain that turns the map underneath into its own glow: cut, spread, saturate.\n *\n * `brightness` and `contrast` together are the threshold, and the pair is exact: `contrast(c)`\n * scales each channel away from 0.5 and `brightness(b)` scales it from 0, so the two compose to\n * `(in·b − 0.5)·c + 0.5`. Asking that line to send `cut` to 0 and 1 to 1 solves to\n * `b = 1/(1 + cut)`, `c = (1 + cut)/(1 − cut)`. Per channel, not luminance, so a magenta road\n * blooms magenta instead of bleeding grey.\n *\n * @param scale - Device pixels per CSS pixel of the canvas the filter runs on, so the radius is in\n * CSS pixels wherever it is applied.\n * @ignore\n */\nexport const bloomFilter = (radius: number, threshold: number, scale = 1): string => {\n const cut = asFraction(threshold) * BLOOM_CUT_MAX;\n const brightness = 1 / (1 + cut);\n const contrast = (1 + cut) / (1 - cut);\n return `brightness(${brightness.toFixed(4)}) contrast(${contrast.toFixed(3)}) blur(${radius * scale}px) saturate(${BLOOM_SATURATE})`;\n};\n\n/**\n * The colour grade as a CSS filter, or an empty string when every term is at its neutral value.\n * @ignore\n */\nexport const gradeFilter = (brightness: number, contrast: number, saturation: number): string =>\n [\n brightness === 1 ? '' : `brightness(${brightness})`,\n contrast === 1 ? '' : `contrast(${contrast})`,\n saturation === 1 ? '' : `saturate(${saturation})`,\n ]\n .filter(Boolean)\n .join(' ');\n\n/**\n * Every rim effect — vignette, fog veil, mask — paints the same ellipse: the inner colour out to\n * where `reach` starts it, the outer colour at the corners. The capture path fills the same pair\n * onto a canvas gradient, so the two cannot drift.\n * @ignore\n */\nexport const rimGradient = (inner: string, outer: string, reach: number): string => {\n // Where the rim starts, as the percentage of the way from centre to corner.\n const innerStop = Math.round((1 - asFraction(reach)) * 100);\n return `radial-gradient(ellipse farthest-corner at center, ${inner} ${innerStop}%, ${outer} 100%)`;\n};\n\n/**\n * The radial gradient a vignette paints: transparent in the middle, darkening (negative intensity)\n * or lightening (positive) towards the corners.\n * @ignore\n */\nexport const vignetteGradient = (intensity: number, reach: number): string => {\n const { inner, outer } = vignetteStops(intensity);\n return rimGradient(inner, outer, reach);\n};\n\n/**\n * The two colours a vignette fades between: black towards the corners for a negative intensity,\n * white for a positive one. The capture path paints the same pair onto a canvas gradient.\n * @ignore\n */\nexport const vignetteStops = (intensity: number): { inner: string; outer: string } => {\n const rgb = intensity < 0 ? '0, 0, 0' : '255, 255, 255';\n return { inner: `rgba(${rgb}, 0)`, outer: `rgba(${rgb}, ${Math.min(Math.abs(intensity), 1)})` };\n};\n\n/**\n * A mask that keeps an effect to the rim of the view: transparent centre, opaque edges.\n * @ignore\n */\nexport const rimMask = (reach: number): string => rimGradient('transparent', 'black', reach);\n\n// How opaque the fog veil gets at the corners, per unit of intensity.\nconst FOG_VEIL_ALPHA = 0.65;\n\n// The fog's blur radius in CSS pixels: the softness the faintest fog already has, plus the span\n// intensity spends on top of it.\nconst FOG_BLUR_BASE_PX = 2;\nconst FOG_BLUR_SPAN_PX = 6;\n\n// The haze the fog veil is made of, and the grade under it, per light/dark theme of the map. Haze\n// takes the colour of the light in the air: pale over a daylit map, and the map's own darkness over\n// a dark one, where a pale veil would read as a bloom rather than as distance.\nconst FOG_BY_THEME: Record<LightDark, { rgb: string; grade: string }> = {\n light: { rgb: '238, 240, 244', grade: 'brightness(1.08) contrast(0.82) saturate(0.7)' },\n dark: { rgb: '16, 22, 33', grade: 'brightness(0.92) contrast(0.82) saturate(0.7)' },\n};\n\n/** Keeps blurred colours from greying out; used by both the live filter and the capture. @ignore */\nexport const EDGE_BLUR_SATURATE = 'saturate(1.08)';\n\n/**\n * The grade under the fog blur, which washes the rim out towards the colour of the air.\n * @ignore\n */\nexport const fogGradeFilter = (lightDark: LightDark): string => FOG_BY_THEME[lightDark].grade;\n\n/**\n * The fog's blur radius in CSS pixels for a given intensity.\n * @ignore\n */\nexport const fogBlurPx = (intensity: number): number => FOG_BLUR_BASE_PX + intensity * FOG_BLUR_SPAN_PX;\n\n/**\n * The fog: a blur plus a washed-out grade over the rim, under a veil that thickens outwards.\n * @ignore\n */\nexport const fogFilter = (intensity: number, lightDark: LightDark): string =>\n `blur(${fogBlurPx(intensity)}px) ${fogGradeFilter(lightDark)}`;\n\n/**\n * The two colours the fog veil fades between, for a given intensity.\n * @ignore\n */\nexport const fogVeilStops = (intensity: number, lightDark: LightDark): { inner: string; outer: string } => {\n const { rgb } = FOG_BY_THEME[lightDark];\n return { inner: `rgba(${rgb}, 0)`, outer: `rgba(${rgb}, ${intensity * FOG_VEIL_ALPHA})` };\n};\n\n/**\n * The veil over the fog blur, thickening towards the corners.\n * @ignore\n */\nexport const fogVeil = (intensity: number, reach: number, lightDark: LightDark): string => {\n const { inner, outer } = fogVeilStops(intensity, lightDark);\n return rimGradient(inner, outer, reach);\n};\n\n/**\n * The edge blur's filter: blur alone, with a slight saturation lift so blurred colours do not grey\n * out. Unlike {@link fogFilter} it neither washes the rim out nor lays a veil over it, so the rim\n * keeps its own colours and only loses its detail.\n * @ignore\n */\nexport const edgeBlurFilter = (intensity: number): string => `blur(${intensity}px) ${EDGE_BLUR_SATURATE}`;\n","/**\n * Composites the effects onto a 2D canvas that already holds the rendered map — the capture path.\n * It consumes the same filter maths as the live overlays (`effectFilters.ts`), so what is captured is\n * what was on screen.\n *\n * @module\n * @ignore\n */\n\nimport type { LightDark } from '@tomtom-org/maps-sdk/map';\nimport {\n bloomFilter,\n EDGE_BLUR_SATURATE,\n fogBlurPx,\n fogGradeFilter,\n fogVeilStops,\n gradeFilter,\n vignetteStops,\n} from './effectFilters';\nimport type { EffectValues } from './effectValues';\n\ntype Context = CanvasRenderingContext2D;\n\n// A filtered copy of what `context` holds, on a canvas of its own — the capture equivalent of one\n// `backdrop-filter` overlay. Undefined when the browser gives no 2D context for the scratch canvas.\nconst filteredCopy = (context: Context, width: number, height: number, filter: string): Context | undefined => {\n const canvas = document.createElement('canvas');\n canvas.width = width;\n canvas.height = height;\n const copy = canvas.getContext('2d');\n if (!copy) return undefined;\n\n copy.filter = filter;\n copy.drawImage(context.canvas, 0, 0);\n copy.filter = 'none';\n return copy;\n};\n\n// Lays one such copy back over the capture, in the blend mode its live overlay uses.\nconst drawOver = (context: Context, layer: Context, operation: GlobalCompositeOperation, alpha = 1): void => {\n context.save();\n context.globalCompositeOperation = operation;\n context.globalAlpha = alpha;\n context.drawImage(layer.canvas, 0, 0);\n context.restore();\n};\n\n// Fills an elliptical radial gradient sized to the canvas aspect, like CSS's\n// `ellipse farthest-corner at center`.\nconst fillEllipticalRadial = (\n context: Context,\n width: number,\n height: number,\n reach: number,\n innerColor: string,\n outerColor: string,\n): void => {\n const radiusX = width / Math.SQRT2;\n const radiusY = height / Math.SQRT2;\n const gradient = context.createRadialGradient(0, 0, 1 - reach, 0, 0, 1);\n gradient.addColorStop(0, innerColor);\n gradient.addColorStop(1, outerColor);\n context.save();\n context.translate(width / 2, height / 2);\n context.scale(radiusX, radiusY);\n context.fillStyle = gradient;\n context.fillRect(-width / 2 / radiusX, -height / 2 / radiusY, width / radiusX, height / radiusY);\n context.restore();\n};\n\n// Blurs a copy of the canvas and keeps only its rim (transparent centre, opaque edges).\nconst applyRimBlur = (\n context: Context,\n width: number,\n height: number,\n scale: number,\n blurCssPx: number,\n reach: number,\n extraFilter: string,\n): void => {\n const blurred = filteredCopy(context, width, height, `blur(${blurCssPx * scale}px) ${extraFilter}`.trim());\n if (!blurred) return;\n\n blurred.globalCompositeOperation = 'destination-in';\n fillEllipticalRadial(blurred, width, height, reach, 'rgba(0,0,0,0)', 'rgba(0,0,0,1)');\n context.drawImage(blurred.canvas, 0, 0);\n};\n\n// Draws the map's own glow back over it, the way the live bloom canvas blends in `screen` mode.\nconst applyBloom = (context: Context, width: number, height: number, scale: number, values: EffectValues): void => {\n const filter = bloomFilter(values['bloom.radius'], values['bloom.threshold'], scale);\n const glow = filteredCopy(context, width, height, filter);\n if (!glow) return;\n\n drawOver(context, glow, 'screen', values['bloom.intensity']);\n};\n\n// The colour grade replaces the pixels underneath, as a `backdrop-filter` overlay does.\nconst applyGrade = (context: Context, width: number, height: number, filter: string): void => {\n const graded = filteredCopy(context, width, height, filter);\n if (!graded) return;\n\n drawOver(context, graded, 'copy');\n};\n\n/**\n * Applies every active effect to `context`, in the same order the live overlays stack: bloom, grade,\n * fog, edge blur, tint, vignette. `scale` is device pixels per CSS pixel, so blur radii stay in CSS\n * pixels at any capture resolution.\n * @ignore\n */\nexport const compositeEffects = (\n context: Context,\n width: number,\n height: number,\n scale: number,\n values: EffectValues,\n lightDark: LightDark,\n): void => {\n if (values['bloom.intensity'] > 0) applyBloom(context, width, height, scale, values);\n\n const grade = gradeFilter(values['grade.brightness'], values['grade.contrast'], values['grade.saturation']);\n if (grade) applyGrade(context, width, height, grade);\n\n if (values['fog.intensity'] > 0) {\n const intensity = values['fog.intensity'];\n const reach = values['fog.reach'];\n const { inner, outer } = fogVeilStops(intensity, lightDark);\n applyRimBlur(context, width, height, scale, fogBlurPx(intensity), reach, fogGradeFilter(lightDark));\n fillEllipticalRadial(context, width, height, reach, inner, outer);\n }\n if (values['edgeBlur.intensity'] > 0) {\n applyRimBlur(\n context,\n width,\n height,\n scale,\n values['edgeBlur.intensity'],\n values['edgeBlur.reach'],\n EDGE_BLUR_SATURATE,\n );\n }\n if (values['tint.opacity'] > 0) {\n context.save();\n context.globalAlpha = values['tint.opacity'];\n context.fillStyle = values['tint.color'];\n context.fillRect(0, 0, width, height);\n context.restore();\n }\n if (values['vignette.intensity'] !== 0) {\n const { inner, outer } = vignetteStops(values['vignette.intensity']);\n fillEllipticalRadial(context, width, height, values['vignette.reach'], inner, outer);\n }\n};\n","import { type EffectKnobId, type EffectKnobValueOf, effectKnobDefinitions, effectKnobIds } from './effectsCatalogue';\n\n/**\n * Every effect knob with a value: the settings with the catalogue defaults filled in.\n * @ignore\n */\nexport type EffectValues = { [ID in EffectKnobId]: EffectKnobValueOf<ID> };\n\n/**\n * @ignore\n */\nexport const withDefaults = (settings: Partial<EffectValues>): EffectValues =>\n Object.fromEntries(\n effectKnobIds.map((id) => [id, settings[id] ?? effectKnobDefinitions[id].default]),\n ) as EffectValues;\n","import { type KnobDescriptor, knob, type TomTomMap } from '@tomtom-org/maps-sdk/map';\nimport { compositeEffects } from './composite';\nimport {\n bloomFilter,\n edgeBlurFilter,\n fogFilter,\n fogVeil,\n gradeFilter,\n rimMask,\n vignetteGradient,\n} from './effectFilters';\nimport {\n type EffectKnobDefinition,\n type EffectKnobId,\n type EffectKnobRange,\n type EffectKnobValueOf,\n effectKnobDefinitions,\n effectKnobIds,\n} from './effectsCatalogue';\nimport { withDefaults } from './effectValues';\n\n/**\n * The effect knob values an app sets — only the overridden ones, serializable like the SDK's\n * styling settings.\n *\n * @group Map Effects\n */\nexport type MapEffectsSettings = { [ID in EffectKnobId]?: EffectKnobValueOf<ID> };\n\n/**\n * One entry of the effects catalogue: the SDK's shared {@link KnobDescriptor}, narrowed the way\n * `StylingKnobDescriptor` narrows it, plus the use case the effect serves.\n *\n * @group Map Effects\n */\nexport type MapEffectsKnobDescriptor = KnobDescriptor<number | string> & {\n /** The knob's id, as accepted by {@link MapEffects.set}. */\n id: EffectKnobId;\n /** The range of valid values, for every knob but `tint.color`. */\n range?: EffectKnobRange;\n /** Always `true`: an effect reads the rendered pixels, so it has something to work on anywhere. */\n available: boolean;\n /** Always `any-style`, for the same reason. */\n appliesTo: string;\n /** The practical problem the effect solves. */\n useCase: string;\n};\n\n/**\n * The effects catalogue, in the same shape as `StylingModule.describe()` so one control panel or\n * agent tool can drive both.\n *\n * @group Map Effects\n */\nexport type MapEffectsCatalogue = { knobs: MapEffectsKnobDescriptor[] };\n\n/**\n * Options for {@link MapEffects.capture}.\n *\n * @group Map Effects\n */\nexport type CaptureOptions = {\n /**\n * Device pixels per CSS pixel to render at. Defaults to the map's current ratio; `2` or `3` gives\n * a print-grade capture of the same view. MapLibre clamps to its `maxCanvasSize`, so measure the\n * ratio you got from the returned canvas rather than assuming.\n */\n pixelRatio?: number;\n};\n\n// Overlay layers in compositing order; each is one absolutely positioned element over the canvas.\nconst OVERLAYS = ['bloom', 'grade', 'fog', 'edgeBlur', 'tint', 'vignette'] as const;\ntype OverlayName = (typeof OVERLAYS)[number];\n\n// Safari still needs `-webkit-backdrop-filter`, which the DOM typings do not carry.\ntype OverlayStyle = Partial<CSSStyleDeclaration> & { webkitBackdropFilter?: string };\n\n// A filter and a mask each go on under both the standard property and the prefixed one.\nconst backdropFilterStyle = (filter: string): OverlayStyle => ({\n backdropFilter: filter,\n webkitBackdropFilter: filter,\n});\nconst maskStyle = (mask: string): OverlayStyle => ({ maskImage: mask, webkitMaskImage: mask });\n\n// The bloom canvas renders at a quarter of the pixels; its blur radius is divided to match.\nconst BLOOM_SCALE = 0.5;\n\n// Below this difference a requested capture ratio is the one the map already renders at.\nconst RATIO_EPSILON = 0.01;\n\n// `setPixelRatio(null)` is MapLibre's \"follow the device again\", which its typings spell as a number\n// only — and a number would pin an override instead.\ntype PixelRatioSetter = { setPixelRatio(pixelRatio: number | null): void };\n\n/**\n * Practical post-processing for a {@link TomTomMap}: effects computed from the rendered map's own\n * pixels, not from its style — so they work on every style, custom ones included, survive every\n * style release, and cannot be reproduced by editing the style at all.\n *\n * @remarks\n * Every effect is named by the problem it solves, and off by default:\n * - **bloom** — emphasis on dark and driving styles: lit roads, traffic tubes and route lines glow;\n * - **grade** (brightness, contrast, saturation) and **tint** — muting the base map so a data overlay\n * reads first;\n * - **fog** — depth: the rim is blurred *and* washed towards the colour of the air, which follows the\n * map's light/dark theme;\n * - **edge blur** — focus: the rim is blurred and keeps its colour, so it reads as a lens rather than\n * as distance;\n * - **vignette** — focus too, by darkening or lightening the edges.\n *\n * Effects render live as layers over the map canvas (`backdrop-filter` overlays; bloom is a small\n * canvas blended in `screen` mode) and are composited the same way into {@link capture}, so what is\n * captured is what was on screen. Bloom and capture read the map canvas, which needs\n * `mapLibre: { canvasContextAttributes: { preserveDrawingBuffer: true } }` when creating the map;\n * the plugin warns once when it is missing.\n *\n * {@link describe} returns the knobs in the same shape as `StylingModule.describe()`, so a control\n * panel or an agent tool built for the SDK's styling catalogue reads this one unchanged.\n *\n * @example\n * ```typescript\n * import { MapEffects } from '@tomtom-org/maps-sdk-plugin-map-effects';\n *\n * const effects = new MapEffects(map);\n * // The data-viz muting half: a dim, desaturated base under your own layers.\n * effects.set({ 'grade.saturation': 0.4, 'grade.brightness': 0.85, 'tint.opacity': 0.15 });\n * // Night-driving emphasis on a dark style:\n * effects.set({ 'bloom.intensity': 0.5, 'bloom.threshold': 0.6 });\n *\n * const poster = await effects.capture({ pixelRatio: 3 });\n * ```\n *\n * @group Map Effects\n */\nexport class MapEffects {\n private settings: MapEffectsSettings = {};\n private readonly container: HTMLDivElement;\n private readonly overlays: Record<OverlayName, HTMLElement>;\n private readonly bloomCanvas: HTMLCanvasElement;\n private bloomFrame: number | undefined;\n private warnedBuffer = false;\n private readonly onRender = () => this.scheduleBloom();\n\n /**\n * Attaches the effects to a map. Nothing is visible until {@link set} turns an effect on.\n *\n * @param map - The map to render effects over.\n * @param settings - Effect knob values to apply right away.\n */\n constructor(\n private readonly map: TomTomMap,\n settings: MapEffectsSettings = {},\n ) {\n const mapContainer = this.map.mapLibreMap.getContainer();\n this.container = document.createElement('div');\n this.container.className = 'tomtom-map-effects';\n Object.assign(this.container.style, {\n position: 'absolute',\n inset: '0',\n pointerEvents: 'none',\n overflow: 'hidden',\n } satisfies Partial<CSSStyleDeclaration>);\n // Right after the canvas container, so the map's own controls stay above the effects.\n const canvasContainer = mapContainer.querySelector('.maplibregl-canvas-container');\n canvasContainer?.insertAdjacentElement('afterend', this.container) ?? mapContainer.appendChild(this.container);\n\n this.bloomCanvas = document.createElement('canvas');\n this.overlays = Object.fromEntries(\n OVERLAYS.map((name) => {\n const element = name === 'bloom' ? this.bloomCanvas : document.createElement('div');\n element.dataset.effect = name;\n Object.assign(element.style, {\n position: 'absolute',\n inset: '0',\n width: '100%',\n height: '100%',\n pointerEvents: 'none',\n display: 'none',\n } satisfies Partial<CSSStyleDeclaration>);\n this.container.appendChild(element);\n return [name, element];\n }),\n ) as unknown as Record<OverlayName, HTMLElement>;\n this.bloomCanvas.style.mixBlendMode = 'screen';\n\n this.map.mapLibreMap.on('render', this.onRender);\n this.set(settings);\n }\n\n /**\n * Sets one or more effect knobs. Values are validated against the catalogue before anything is\n * touched; knobs not mentioned keep their values.\n *\n * @param settings - Knob id → value.\n * @throws `RangeError` for a value outside its range or of the wrong kind; `Error` for an unknown id.\n *\n * @example\n * ```typescript\n * effects.set({ 'vignette.intensity': -0.4 });\n * ```\n */\n set(settings: MapEffectsSettings): void {\n for (const [id, value] of Object.entries(settings)) {\n this.validate(id, value);\n }\n this.settings = { ...this.settings, ...settings };\n this.render();\n }\n\n /**\n * The current value of a knob: the override, or the catalogue default.\n */\n get<ID extends EffectKnobId>(id: ID): EffectKnobValueOf<ID> {\n this.assertKnob(id);\n return (this.settings[id] ?? effectKnobDefinitions[id].default) as EffectKnobValueOf<ID>;\n }\n\n /**\n * Turns a knob (or every knob) back to its default — for most effects, off.\n */\n reset(id?: EffectKnobId): void {\n if (id === undefined) {\n this.settings = {};\n } else {\n this.assertKnob(id);\n const { [id]: _removed, ...rest } = this.settings;\n this.settings = rest;\n }\n this.render();\n }\n\n /**\n * The overridden knobs, as a plain serializable object.\n */\n getConfig(): MapEffectsSettings {\n return { ...this.settings };\n }\n\n /**\n * Replaces every knob with the given settings (an empty object turns everything off).\n */\n applyConfig(settings: MapEffectsSettings): void {\n this.settings = {};\n this.set(settings);\n }\n\n /**\n * The effects catalogue: every knob with kind, range, default, current value and the practical\n * use case it serves. Same descriptor shape as `StylingModule.describe()`.\n */\n describe(): MapEffectsCatalogue {\n return {\n knobs: effectKnobIds.map((id): MapEffectsKnobDescriptor => {\n const definition = effectKnobDefinitions[id];\n return knob(id, definition.kind, definition.description, definition.default, this.settings[id], {\n ...('range' in definition && { range: definition.range }),\n available: true,\n appliesTo: 'any-style',\n useCase: definition.useCase,\n });\n }),\n };\n }\n\n /**\n * Renders the current view — map and effects — into a canvas, optionally at a higher pixel ratio\n * for print. The map is re-rendered at that ratio (never resampled), read back once every tile is\n * drawn, and restored to follow the device afterwards.\n *\n * @remarks\n * Needs `preserveDrawingBuffer: true` on the map's canvas context; without it the map half of the\n * capture is blank. Viewers cannot download from a sandboxed page, so hand the canvas to your own\n * upload or `toBlob` flow.\n */\n async capture(options: CaptureOptions = {}): Promise<HTMLCanvasElement> {\n this.warnIfBufferNotPreserved();\n const mapLibreMap = this.map.mapLibreMap;\n const ratioSetter: PixelRatioSetter = mapLibreMap;\n const live = mapLibreMap.getPixelRatio();\n const wanted = options.pixelRatio ?? live;\n const wasOverridden = live !== (globalThis.devicePixelRatio || 1);\n const rerenders = Math.abs(wanted - live) > RATIO_EPSILON;\n if (rerenders) {\n ratioSetter.setPixelRatio(wanted);\n await this.settled();\n }\n try {\n return await this.readBack();\n } finally {\n if (rerenders) ratioSetter.setPixelRatio(wasOverridden ? live : null);\n }\n }\n\n /**\n * Detaches the effects from the map and removes their elements.\n */\n remove(): void {\n this.map.mapLibreMap.off('render', this.onRender);\n if (this.bloomFrame !== undefined) cancelAnimationFrame(this.bloomFrame);\n this.container.remove();\n }\n\n // ── Live rendering ────────────────────────────────────────────────────────────────────────\n\n private render(): void {\n const values = withDefaults(this.settings);\n const show = (name: OverlayName, on: boolean, style: OverlayStyle = {}) => {\n const element = this.overlays[name];\n element.style.display = on ? 'block' : 'none';\n Object.assign(element.style, style);\n };\n\n const grade = gradeFilter(values['grade.brightness'], values['grade.contrast'], values['grade.saturation']);\n show('grade', grade !== '', backdropFilterStyle(grade));\n\n const lightDark = this.map.styleLightDarkTheme;\n show('fog', values['fog.intensity'] > 0, {\n ...backdropFilterStyle(fogFilter(values['fog.intensity'], lightDark)),\n ...maskStyle(rimMask(values['fog.reach'])),\n backgroundImage: fogVeil(values['fog.intensity'], values['fog.reach'], lightDark),\n });\n\n show('edgeBlur', values['edgeBlur.intensity'] > 0, {\n ...backdropFilterStyle(edgeBlurFilter(values['edgeBlur.intensity'])),\n ...maskStyle(rimMask(values['edgeBlur.reach'])),\n });\n\n show('tint', values['tint.opacity'] > 0, {\n backgroundColor: values['tint.color'],\n opacity: String(values['tint.opacity']),\n });\n\n show('vignette', values['vignette.intensity'] !== 0, {\n backgroundImage: vignetteGradient(values['vignette.intensity'], values['vignette.reach']),\n });\n\n const bloomOn = values['bloom.intensity'] > 0;\n show('bloom', bloomOn, { opacity: String(values['bloom.intensity']) });\n if (bloomOn) {\n this.warnIfBufferNotPreserved();\n this.scheduleBloom();\n }\n }\n\n // Bloom follows the map frame by frame; `render` events are coalesced to one draw per animation\n // frame so a pan does not draw the glow more often than the map itself.\n private scheduleBloom(): void {\n if (this.get('bloom.intensity') <= 0 || this.bloomFrame !== undefined) return;\n\n this.bloomFrame = requestAnimationFrame(() => {\n this.bloomFrame = undefined;\n this.drawBloom();\n });\n }\n\n private drawBloom(): void {\n const source = this.map.mapLibreMap.getCanvas();\n const width = Math.max(1, Math.round(source.width * BLOOM_SCALE));\n const height = Math.max(1, Math.round(source.height * BLOOM_SCALE));\n if (this.bloomCanvas.width !== width || this.bloomCanvas.height !== height) {\n this.bloomCanvas.width = width;\n this.bloomCanvas.height = height;\n }\n const context = this.bloomCanvas.getContext('2d');\n if (!context) return;\n\n context.filter = bloomFilter(this.get('bloom.radius'), this.get('bloom.threshold'), BLOOM_SCALE);\n context.clearRect(0, 0, width, height);\n context.drawImage(source, 0, 0, width, height);\n }\n\n // ── Capture ───────────────────────────────────────────────────────────────────────────────\n\n private readBack(): Promise<HTMLCanvasElement> {\n const mapLibreMap = this.map.mapLibreMap;\n return new Promise((resolve) => {\n mapLibreMap.once('render', () => {\n const source = mapLibreMap.getCanvas();\n const composite = document.createElement('canvas');\n composite.width = source.width;\n composite.height = source.height;\n const context = composite.getContext('2d');\n if (context) {\n context.drawImage(source, 0, 0);\n const scale = source.width / Math.max(1, source.clientWidth);\n compositeEffects(\n context,\n source.width,\n source.height,\n scale,\n withDefaults(this.settings),\n this.map.styleLightDarkTheme,\n );\n }\n resolve(composite);\n });\n mapLibreMap.triggerRepaint();\n });\n }\n\n // Resolves once every source is loaded and every tile drawn at the new ratio, or after a timeout.\n private settled(timeoutMs = 15_000): Promise<void> {\n const mapLibreMap = this.map.mapLibreMap;\n return new Promise((resolve) => {\n const done = () => {\n clearTimeout(timer);\n mapLibreMap.off('idle', done);\n resolve();\n };\n const timer = setTimeout(done, timeoutMs);\n mapLibreMap.once('idle', done);\n });\n }\n\n private warnIfBufferNotPreserved(): void {\n if (this.warnedBuffer) return;\n\n const canvas = this.map.mapLibreMap.getCanvas();\n const context = canvas.getContext('webgl2') ?? canvas.getContext('webgl');\n const attributes = context?.getContextAttributes?.();\n if (attributes && !attributes.preserveDrawingBuffer) {\n this.warnedBuffer = true;\n console.warn(\n '[MapEffects] bloom and capture read the map canvas, which needs ' +\n \"`mapLibre: { canvasContextAttributes: { preserveDrawingBuffer: true } }` on the map's options.\",\n );\n }\n }\n\n // ── Validation ────────────────────────────────────────────────────────────────────────────\n\n private assertKnob(id: string): asserts id is EffectKnobId {\n if (!(id in effectKnobDefinitions)) {\n throw new Error(`Unknown effect knob '${id}'. Call describe() for the catalogue of knob ids.`);\n }\n }\n\n private validate(id: string, value: unknown): void {\n this.assertKnob(id);\n const { kind, range }: EffectKnobDefinition = effectKnobDefinitions[id];\n if (kind === 'color') {\n if (typeof value !== 'string' || !value)\n throw new RangeError(`Knob '${id}' expects a CSS colour; got ${String(value)}.`);\n return;\n }\n if (typeof value !== 'number' || !Number.isFinite(value)) {\n throw new RangeError(`Knob '${id}' expects a number; got ${String(value)}.`);\n }\n if (range && (value < range.min || value > range.max)) {\n throw new RangeError(`Knob '${id}' expects a number between ${range.min} and ${range.max}; got ${value}.`);\n }\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ [
2
+ {
3
+ "name": "maplibre-gl",
4
+ "version": "6.9.0",
5
+ "license": "BSD-3-Clause",
6
+ "bundled": false,
7
+ "description": "BSD licensed community fork of mapbox-gl, a WebGL interactive maps library",
8
+ "repository": "https://github.com/maplibre/maplibre-gl-js",
9
+ "homepage": "https://maplibre.org/",
10
+ "licenseText": "Copyright (c) 2023, MapLibre contributors\n\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n * Redistributions of source code must retain the above copyright notice,\n this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n * Neither the name of MapLibre GL JS nor the names of its contributors\n may be used to endorse or promote products derived from this software\n without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR\nCONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\nEXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\nPROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\nPROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\nLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\nNEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\n-------------------------------------------------------------------------------\n\nContains code from mapbox-gl-js v1.13 and earlier\n\nVersion v1.13 of mapbox-gl-js and earlier are licensed under a BSD-3-Clause license\n\nCopyright (c) 2020, Mapbox\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice,\n this list of conditions and the following disclaimer.\n* Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n* Neither the name of Mapbox GL JS nor the names of its contributors\n may be used to endorse or promote products derived from this software\n without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR\nCONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\nEXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\nPROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\nPROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\nLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\nNEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,\nEVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\n-------------------------------------------------------------------------------\n\nContains code from glfx.js\n\nCopyright (C) 2011 by Evan Wallace\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n\n--------------------------------------------------------------------------------\n\nContains a portion of d3-color https://github.com/d3/d3-color\n\nCopyright 2010-2016 Mike Bostock\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\n* Neither the name of the author nor the names of contributors may be used to\n endorse or promote products derived from this software without specific prior\n written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
11
+ }
12
+ ]
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@tomtom-org/maps-sdk-plugin-map-effects",
3
+ "author": "TomTom International B.V.",
4
+ "license": "See in LICENSE.txt",
5
+ "version": "0.0.2",
6
+ "type": "module",
7
+ "description": "Practical post-processing for a TomTomMap: bloom for emphasis, tint/grade/vignette to mute the base map under data, fog and edge blur for focus — plus high-DPI capture",
8
+ "types": "dist/index.d.ts",
9
+ "peerDependencies": {
10
+ "maplibre-gl": "^6.9.0",
11
+ "@tomtom-org/maps-sdk": "0.55.1"
12
+ },
13
+ "devDependencies": {
14
+ "@tsconfig/recommended": "^1.0.13",
15
+ "@types/node": "^26.3.0",
16
+ "@vitest/coverage-v8": "^4.1.11",
17
+ "jsdom": "^29.1.1",
18
+ "rolldown": "^1.2.5",
19
+ "ts-node": "^10.9.2",
20
+ "typescript": "^6.0.3",
21
+ "vite": "^8.2.2",
22
+ "vitest": "^4.1.11"
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "package.json",
27
+ "README.md",
28
+ "LICENSE.txt",
29
+ "THIRD_PARTY.txt"
30
+ ],
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "default": "./dist/index.es.js"
35
+ }
36
+ },
37
+ "scripts": {
38
+ "build": "vite build --config ../plugin-vite-config.ts",
39
+ "build:watch": "vite build --watch --config ../plugin-vite-config.ts",
40
+ "test": "vitest run",
41
+ "test:coverage": "vitest run --coverage",
42
+ "type-check": "tsc --noEmit"
43
+ }
44
+ }