mudlet-map-renderer 0.32.0-konva → 0.34.0-konva

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.
Files changed (36) hide show
  1. package/README.md +141 -21
  2. package/dist/ExitRenderer.d.ts +44 -0
  3. package/dist/ScenePipeline.d.ts +15 -2
  4. package/dist/Viewport.d.ts +12 -0
  5. package/dist/backend/CanvasBackend.d.ts +129 -0
  6. package/dist/backend/DrawingBackend.d.ts +55 -6
  7. package/dist/backend/KonvaBackend.d.ts +1 -0
  8. package/dist/export/CanvasExporter.d.ts +67 -0
  9. package/dist/export/Exporter.d.ts +57 -0
  10. package/dist/export/PngExporter.d.ts +23 -0
  11. package/dist/export/SvgExporter.d.ts +15 -0
  12. package/dist/export/canvasToBytes.d.ts +19 -0
  13. package/dist/index.cjs +4 -4
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.ts +20 -17
  16. package/dist/index.mjs +1354 -1042
  17. package/dist/index.mjs.map +1 -1
  18. package/dist/overlay/AmbientLightOverlay.d.ts +32 -0
  19. package/dist/overlay/LiveEffect.d.ts +33 -0
  20. package/dist/overlay/SceneOverlay.d.ts +63 -0
  21. package/dist/rendering/KonvaRenderBackend.d.ts +31 -9
  22. package/dist/rendering/MapRenderer.d.ts +64 -36
  23. package/dist/scene/AmbientLightStyle.d.ts +7 -2
  24. package/dist/{backend/BlueprintBackend.d.ts → style/BlueprintStyle.d.ts} +2 -12
  25. package/dist/{backend/IsometricBackend.d.ts → style/IsometricStyle.d.ts} +5 -6
  26. package/dist/{backend/NeonBackend.d.ts → style/NeonStyle.d.ts} +2 -12
  27. package/dist/{backend/ParchmentBackend.d.ts → style/ParchmentStyle.d.ts} +6 -16
  28. package/dist/{backend/SketchyBackend.d.ts → style/SketchyStyle.d.ts} +4 -13
  29. package/dist/style/index.d.ts +32 -0
  30. package/dist/types/Settings.d.ts +3 -17
  31. package/package.json +1 -1
  32. package/dist/HeadlessRenderer.d.ts +0 -64
  33. package/dist/MapRenderer.d.ts +0 -23
  34. package/dist/Renderer.d.ts +0 -56
  35. package/dist/rendering/SvgRenderBackend.d.ts +0 -21
  36. package/dist/types/OverlayPlugin.d.ts +0 -44
package/README.md CHANGED
@@ -186,37 +186,97 @@ For large maps, spatial culling hides off-screen rooms for better performance:
186
186
  renderer.setCullingMode('indexed');
187
187
  ```
188
188
 
189
- ### Export
189
+ ### Styles
190
+
191
+ A `Style` is a target-agnostic visual transformer. One style drives the
192
+ interactive canvas *and* every exporter — set it once and it applies to SVG,
193
+ PNG, and anything else you export.
190
194
 
191
195
  ```ts
192
- // SVG export
193
- const svg = renderer.exportSvg();
194
- const svgCentered = renderer.exportSvg({ roomId: 1234, padding: 5 });
196
+ import {
197
+ compose, identityStyle,
198
+ Parchment, Blueprint, Neon, Sketchy, Isometric,
199
+ } from 'mudlet-map-renderer';
200
+
201
+ // Single style
202
+ renderer.setStyle(Parchment);
203
+
204
+ // Compose a chain (left → right = inner → outer)
205
+ renderer.setStyle(compose(
206
+ Parchment,
207
+ Sketchy({ jitter: 0.012, color: '#4a3728' }),
208
+ Isometric({ rotation: 30, depth: 0.18 }),
209
+ ));
210
+
211
+ // Clear the current style
212
+ renderer.setStyle(identityStyle);
213
+ // or
214
+ renderer.clearStyle();
215
+ ```
216
+
217
+ Built-in styles:
218
+
219
+ | Style | Effect |
220
+ |---|---|
221
+ | `Parchment` | Warm sepia / aged-paper palette |
222
+ | `Blueprint` | White lines on deep blue |
223
+ | `Neon` | Glowing neon outlines on dark background |
224
+ | `Sketchy({ jitter, color })` | Hand-drawn pencil wobble |
225
+ | `Isometric({ rotation?, depth? })` | 2:1 iso projection with optional cubes |
226
+
227
+ Custom styles extend `BaseStyle<Inner>` and override only the draw calls they
228
+ transform — see the built-ins for examples.
229
+
230
+ ### Export
195
231
 
196
- // SVG with explicit overlays
197
- const svgWithOverlays = renderer.exportSvg({
232
+ Exporters are plug-ins: a new output format is a new `Exporter<T>` class,
233
+ not a new `MapRenderer` method.
234
+
235
+ ```ts
236
+ import {
237
+ SvgExporter, PngExporter, PngBlobExporter, CanvasExporter,
238
+ } from 'mudlet-map-renderer';
239
+
240
+ // SVG (string)
241
+ const svg = renderer.export(new SvgExporter());
242
+ const svgCentered = renderer.export(new SvgExporter({ roomId: 1234, padding: 5 }));
243
+ const svgTyped = renderer.export(new SvgExporter({
198
244
  overlays: {
199
245
  position: { roomId: 1234 },
200
246
  highlights: [{ roomId: 100, color: '#ff0000' }],
201
247
  paths: [{ locations: [101, 102, 103], color: '#00ff00' }],
202
248
  },
203
- });
249
+ }));
204
250
 
205
- // PNG export (data URL)
206
- const pngDataUrl = renderer.exportPng({ pixelRatio: 2 });
251
+ // PNG data URL
252
+ const pngUrl = renderer.export(new PngExporter({ pixelRatio: 2 }));
207
253
 
208
- // PNG export (Blob)
209
- const blob = await renderer.exportPngBlob({ pixelRatio: 2 });
254
+ // PNG as Blob
255
+ const blob = await renderer.export(new PngBlobExporter({ pixelRatio: 2 }));
210
256
 
211
- // Canvas export (specific region)
212
- const canvas = renderer.renderToCanvas({
257
+ // Headless PNG bytes at a specific size (portable — works in browser + Node)
258
+ const png = renderer.export(new PngBytesExporter({
213
259
  width: 1920,
214
260
  height: 1080,
215
261
  roomId: 1234,
216
262
  padding: 5,
217
- });
263
+ }));
264
+ // fs.writeFileSync('out.png', png!); // Node
265
+ // new Blob([png!], { type: 'image/png' }); // Browser
266
+
267
+ // Canvas handle (if you need to draw more on it, attach to DOM, etc.)
268
+ const canvas = renderer.export(new CanvasExporter({
269
+ width: 1920,
270
+ height: 1080,
271
+ }));
218
272
  ```
219
273
 
274
+ Style + export compose: the style currently applied with `setStyle` is passed
275
+ to every exporter, so the SVG, the PNG, and the on-screen canvas stay in sync.
276
+
277
+ Writing a new exporter — e.g. PDF — means shipping a class that implements
278
+ `Exporter<Uint8Array>`. No changes to `MapRenderer`.
279
+
220
280
  ### Headless rendering (no DOM)
221
281
 
222
282
  For server-side or offscreen rendering, omit the container argument:
@@ -225,10 +285,45 @@ For server-side or offscreen rendering, omit the container argument:
225
285
  const renderer = new MapRenderer(mapReader, createSettings());
226
286
 
227
287
  renderer.drawArea(42, 0);
228
- renderer.setPosition(1234);
288
+ renderer.state.positionRoomId = 1234; // mark player position without auto-centering
229
289
 
230
- const svg = renderer.exportSvg({ padding: 5 });
231
- const canvas = renderer.renderToCanvas({ width: 1920, height: 1080 });
290
+ const svg = renderer.export(new SvgExporter({ padding: 5 }));
291
+ const png = renderer.export(new PngBytesExporter({ width: 1920, height: 1080 }));
292
+ ```
293
+
294
+ ### Overlays
295
+
296
+ Two kinds, picked by what you need:
297
+
298
+ - **`SceneOverlay`** — target-agnostic, appears in every output (interactive
299
+ canvas + every exporter). Use for static scene content (badges, annotations).
300
+ - **`LiveEffect`** — interactive canvas only, receives a Konva layer and
301
+ viewport updates for animation. Skipped by exporters.
302
+
303
+ ```ts
304
+ import type { SceneOverlay, LiveEffect } from 'mudlet-map-renderer';
305
+
306
+ // Scene overlay — uses target-agnostic draw primitives
307
+ class BadgeOverlay implements SceneOverlay {
308
+ render(target, state, bounds) {
309
+ const g = target.createGroup(0, 0);
310
+ target.addCircle(g, { cx: 5, cy: 5, radius: 0.4, fill: '#ff0' });
311
+ return g;
312
+ }
313
+ }
314
+
315
+ renderer.addSceneOverlay('badge', new BadgeOverlay());
316
+ renderer.removeSceneOverlay('badge');
317
+
318
+ // Live effect — gets a Konva layer, interactive only
319
+ class Pulse implements LiveEffect {
320
+ attach(layer) { /* add Konva shapes */ }
321
+ updateViewport(bounds, scale) { /* react to pan/zoom */ }
322
+ destroy() { /* cleanup */ }
323
+ }
324
+
325
+ renderer.addLiveEffect('pulse', new Pulse());
326
+ renderer.removeLiveEffect('pulse');
232
327
  ```
233
328
 
234
329
  ### Cleanup
@@ -360,16 +455,41 @@ MudletMapReader.exportJson(map, 'map.json');
360
455
  | `setZoom(zoom)` | Set zoom level |
361
456
  | `zoomToCenter(zoom)` | Zoom keeping center fixed |
362
457
  | `fitArea()` | Fit the full area in view |
363
- | `exportSvg(options?)` | Export as SVG string |
364
- | `exportPng(options?)` | Export as PNG data URL |
365
- | `exportPngBlob(options?)` | Export as PNG Blob |
366
- | `renderToCanvas(options)` | Export a region to canvas |
458
+ | `setStyle(style)` | Apply a visual style (interactive + exporters) |
459
+ | `clearStyle()` | Remove the current style |
460
+ | `export(exporter)` | Run an `Exporter<T>` and return its output |
461
+ | `addSceneOverlay(id, overlay)` | Add a target-agnostic overlay |
462
+ | `removeSceneOverlay(id)` | Remove a scene overlay |
463
+ | `addLiveEffect(id, effect)` | Add an interactive-only animated effect |
464
+ | `removeLiveEffect(id)` | Remove a live effect |
367
465
  | `refresh()` | Force a full re-render |
368
466
  | `on(event, handler)` | Subscribe to an event |
369
467
  | `off(event, handler)` | Unsubscribe from an event |
370
468
  | `setCullingMode(mode)` | Set culling strategy |
371
469
  | `destroy()` | Release all resources |
372
470
 
471
+ ### Exporters
472
+
473
+ | Exporter | Output |
474
+ |---|---|
475
+ | `SvgExporter({ roomId?, padding?, overlays? })` | `string` — SVG document |
476
+ | `PngExporter({ pixelRatio? })` | `string` — PNG data URL (current viewport) |
477
+ | `PngBlobExporter({ pixelRatio? })` | `Promise<Blob>` — PNG Blob (browser only) |
478
+ | `PngBytesExporter({ width, height, roomId?, padding?, overlays?, mimeType?, quality? })` | `Uint8Array` — headless PNG/JPEG bytes; portable (browser + Node) |
479
+ | `CanvasExporter({ width, height, roomId?, padding?, overlays? })` | `ExportCanvas` — canvas handle; reframes to fit |
480
+
481
+ ### Styles
482
+
483
+ | Style | Constructor / Usage |
484
+ |---|---|
485
+ | `Parchment` | `setStyle(Parchment)` |
486
+ | `Blueprint` | `setStyle(Blueprint)` |
487
+ | `Neon` | `setStyle(Neon)` |
488
+ | `Sketchy(opts)` | `setStyle(Sketchy({ jitter, color }))` |
489
+ | `Isometric(opts)` | `setStyle(Isometric({ rotation?, depth? }))` |
490
+ | `compose(...)` | Chain multiple styles into one |
491
+ | `identityStyle` | Pass-through; equivalent to `clearStyle()` |
492
+
373
493
  ### `PathFinder`
374
494
 
375
495
  | Method | Description |
@@ -36,6 +36,18 @@ export type ExitDrawData = {
36
36
  };
37
37
  /** Set when this exit leads to a room in a different area (cross-area exit). */
38
38
  targetRoomId?: number;
39
+ /** Source room center in map coords — used to compute label direction. */
40
+ from?: {
41
+ x: number;
42
+ y: number;
43
+ };
44
+ /** Arrow tip / far endpoint in map coords — anchor for placed labels. */
45
+ tip?: {
46
+ x: number;
47
+ y: number;
48
+ };
49
+ /** Stroke colour of the rendered arrow — used to colour area-exit labels. */
50
+ arrowColor?: string;
39
51
  };
40
52
  export default class ExitRenderer {
41
53
  private mapReader;
@@ -53,6 +65,29 @@ export default class ExitRenderer {
53
65
  /**
54
66
  * Returns hit-zone bounds for special exits (custom lines) that lead to rooms in another area.
55
67
  */
68
+ /**
69
+ * Hit zones for cross-area inner exits (up/down/in/out). These are drawn as
70
+ * triangles inside the room, not through the exit pipeline, so they need
71
+ * their own plumbing for area-exit labelling and clickability.
72
+ */
73
+ getInnerExitAreaTargets(room: MapData.Room): {
74
+ bounds: {
75
+ x: number;
76
+ y: number;
77
+ width: number;
78
+ height: number;
79
+ };
80
+ targetRoomId: number;
81
+ from: {
82
+ x: number;
83
+ y: number;
84
+ };
85
+ tip: {
86
+ x: number;
87
+ y: number;
88
+ };
89
+ arrowColor: string;
90
+ }[];
56
91
  getSpecialExitAreaTargets(room: MapData.Room): {
57
92
  bounds: {
58
93
  x: number;
@@ -61,5 +96,14 @@ export default class ExitRenderer {
61
96
  height: number;
62
97
  };
63
98
  targetRoomId: number;
99
+ from: {
100
+ x: number;
101
+ y: number;
102
+ };
103
+ tip: {
104
+ x: number;
105
+ y: number;
106
+ };
107
+ arrowColor: string;
64
108
  }[];
65
109
  }
@@ -24,6 +24,18 @@ export type StandaloneExitEntry = {
24
24
  export type AreaExitHitZone = {
25
25
  bounds: Bounds;
26
26
  targetRoomId: number;
27
+ /** Source room center — used to compute arrow direction for labels. Optional for back-compat. */
28
+ from?: {
29
+ x: number;
30
+ y: number;
31
+ };
32
+ /** Arrow tip / far endpoint — anchor for placed labels. Optional for back-compat. */
33
+ tip?: {
34
+ x: number;
35
+ y: number;
36
+ };
37
+ /** Stroke colour of the rendered arrow — used to colour area-exit labels. */
38
+ arrowColor?: string;
27
39
  };
28
40
  export type SceneBuildResult = {
29
41
  roomNodes: Map<number, RoomNodeEntry>;
@@ -34,8 +46,8 @@ export type SceneBuildResult = {
34
46
  * Backend-agnostic scene composition pipeline.
35
47
  * Drives a DrawingBackend + LayerNode to render the full map scene.
36
48
  *
37
- * Both KonvaRenderBackend and SvgRenderBackend use this same pipeline
38
- * with their respective DrawingBackend implementations.
49
+ * Both the interactive KonvaRenderBackend and exporters (SvgExporter,
50
+ * CanvasExporter, …) drive this pipeline with their respective DrawingBackend.
39
51
  */
40
52
  export declare class ScenePipeline {
41
53
  private readonly mapReader;
@@ -70,6 +82,7 @@ export declare class ScenePipeline {
70
82
  renderExitData(data: ExitDrawData): GroupNode;
71
83
  private renderArrow;
72
84
  private renderLabels;
85
+ private renderAreaExitLabels;
73
86
  private renderAreaName;
74
87
  }
75
88
  export {};
@@ -52,6 +52,18 @@ export declare class Viewport {
52
52
  * Center on a map coordinate, with optional animation.
53
53
  */
54
54
  panToMapPointAnimated(x: number, y: number, instant: boolean): void;
55
+ /**
56
+ * Compute the zoom level that would fit the given map bounds in the current
57
+ * viewport (with the same padding/insets as {@link fitToMapBounds}). Useful
58
+ * for updating `minZoom` to lock zoom-out to an area without changing the
59
+ * current zoom or position.
60
+ */
61
+ computeFitZoom(minX: number, maxX: number, minY: number, maxY: number, insets?: {
62
+ top?: number;
63
+ right?: number;
64
+ bottom?: number;
65
+ left?: number;
66
+ }): number;
55
67
  /**
56
68
  * Fit the viewport to show the given map bounds with padding.
57
69
  * Optional `insets` (screen pixels) reserve space at each edge — content
@@ -0,0 +1,129 @@
1
+ import { default as Konva } from 'konva';
2
+ import { DrawingBackend, GroupNode, LayerNode, CoordFn, RectConfig, CircleConfig, LineConfig, PolygonConfig, TextConfig, ImageConfig } from './DrawingBackend';
3
+ type RectCommand = {
4
+ type: 'rect';
5
+ x: number;
6
+ y: number;
7
+ w: number;
8
+ h: number;
9
+ fill?: string;
10
+ stroke?: string;
11
+ sw: number;
12
+ cr: number;
13
+ dash?: number[];
14
+ };
15
+ type CircleCommand = {
16
+ type: 'circle';
17
+ cx: number;
18
+ cy: number;
19
+ r: number;
20
+ fill?: string;
21
+ stroke?: string;
22
+ sw: number;
23
+ dash?: number[];
24
+ };
25
+ type LineCommand = {
26
+ type: 'line';
27
+ points: number[];
28
+ stroke?: string;
29
+ sw: number;
30
+ dash?: number[];
31
+ lineCap?: string;
32
+ lineJoin?: string;
33
+ alpha?: number;
34
+ };
35
+ type PolygonCommand = {
36
+ type: 'polygon';
37
+ vertices: number[];
38
+ fill?: string;
39
+ stroke?: string;
40
+ sw: number;
41
+ };
42
+ type TextCommand = {
43
+ type: 'text';
44
+ x: number;
45
+ y: number;
46
+ text: string;
47
+ fontSize: number;
48
+ fontFamily: string;
49
+ fontStyle: string;
50
+ fill: string;
51
+ align: string;
52
+ vAlign: string;
53
+ w: number;
54
+ h: number;
55
+ baselineRatio?: number;
56
+ transform?: [number, number, number, number, number, number];
57
+ };
58
+ type ImageCommand = {
59
+ type: 'image';
60
+ x: number;
61
+ y: number;
62
+ w: number;
63
+ h: number;
64
+ image: HTMLImageElement | any;
65
+ transform?: [number, number, number, number, number, number];
66
+ };
67
+ type DrawCommand = RectCommand | CircleCommand | LineCommand | PolygonCommand | TextCommand | ImageCommand;
68
+ export declare class RecordingGroupNode implements GroupNode {
69
+ x: number;
70
+ y: number;
71
+ _visible: boolean;
72
+ readonly commands: DrawCommand[];
73
+ /** Lazily created when this group is materialized for a KonvaLayerNode. */
74
+ _konvaGroup?: Konva.Group;
75
+ constructor(x: number, y: number);
76
+ setVisible(visible: boolean): void;
77
+ isVisible(): boolean;
78
+ destroy(): void;
79
+ setPosition(x: number, y: number): void;
80
+ getPosition(): {
81
+ x: number;
82
+ y: number;
83
+ };
84
+ moveToTop(): void;
85
+ /**
86
+ * Materialize this recording as a Konva.Group + Konva.Shape.
87
+ * Used when the group is added to a KonvaLayerNode (overlay/position layers).
88
+ */
89
+ materialize(): Konva.Group;
90
+ }
91
+ /**
92
+ * A LayerNode backed by a single Konva.Shape whose sceneFunc replays
93
+ * all recorded groups. Much faster than individual Konva nodes.
94
+ */
95
+ export declare class RecordingLayerNode implements LayerNode {
96
+ private groups;
97
+ private readonly konvaLayer;
98
+ private konvaShape;
99
+ constructor(konvaLayer: Konva.Layer);
100
+ private ensureShape;
101
+ addNode(node: GroupNode): void;
102
+ destroyChildren(): void;
103
+ batchDraw(): void;
104
+ }
105
+ /**
106
+ * DrawingBackend that records draw commands into RecordingGroupNodes.
107
+ * Commands are replayed via Canvas2D in a single Konva.Shape sceneFunc
108
+ * per layer, eliminating per-node Konva overhead.
109
+ *
110
+ * Drop-in replacement for KonvaBackend. Decorator backends wrap this
111
+ * the same way they wrap KonvaBackend.
112
+ */
113
+ export declare class CanvasBackend implements DrawingBackend {
114
+ createGroup(x: number, y: number): RecordingGroupNode;
115
+ addRect(parent: GroupNode, config: RectConfig): void;
116
+ addCircle(parent: GroupNode, config: CircleConfig): void;
117
+ addLine(parent: GroupNode, config: LineConfig): void;
118
+ addPolygon(parent: GroupNode, config: PolygonConfig): void;
119
+ addText(parent: GroupNode, config: TextConfig): void;
120
+ addImage(parent: GroupNode, config: ImageConfig): void;
121
+ supportsBatchExitRendering(): boolean;
122
+ getExitDepthOffset(): {
123
+ x: number;
124
+ y: number;
125
+ };
126
+ getTransform(): CoordFn;
127
+ getInverseTransform(): CoordFn;
128
+ }
129
+ export {};
@@ -1,9 +1,12 @@
1
1
  /**
2
- * Abstract interface for creating visual nodes.
3
- * Implement this to swap the rendering engine (Konva, PixiJS, raw Canvas2D, etc.).
2
+ * Low-level drawing primitives. A {@link DrawingBackend} is a thin abstraction
3
+ * over per-node draw calls. Leaf implementations target a specific rendering
4
+ * engine (Konva, raw Canvas2D, SVG strings, …); decorator implementations
5
+ * ({@link BaseStyle}) wrap another backend and transform the calls.
4
6
  *
5
- * GroupNode is the opaque handle returned by createGroup(). The culling system
6
- * and renderer use it for visibility toggling, positioning, and cleanup.
7
+ * End users should rarely touch this directly prefer {@link Style} factories
8
+ * (Parchment, Sketchy(…), …) with {@link MapRenderer.setStyle}, and
9
+ * {@link Exporter} implementations with {@link MapRenderer.export}.
7
10
  */
8
11
  export interface GroupNode {
9
12
  setVisible(visible: boolean): void;
@@ -103,6 +106,12 @@ export interface DrawingBackend {
103
106
  addPolygon(parent: GroupNode, config: PolygonConfig): void;
104
107
  addText(parent: GroupNode, config: TextConfig): void;
105
108
  addImage(parent: GroupNode, config: ImageConfig): void;
109
+ /**
110
+ * Whether this backend supports batch exit rendering via a single Canvas2D shape.
111
+ * When true, link exits are collected as ExitDrawData and drawn in one batched
112
+ * Konva.Shape sceneFunc instead of creating individual nodes per exit.
113
+ */
114
+ supportsBatchExitRendering?(): boolean;
106
115
  /**
107
116
  * Cartesian offset for exit line groups so they connect at the cube base
108
117
  * instead of the top face. Returns {x:0, y:0} for flat backends.
@@ -113,11 +122,51 @@ export interface DrawingBackend {
113
122
  };
114
123
  /**
115
124
  * Map-space → render-space transform. Identity for flat backends; non-identity
116
- * for backends that warp coordinates (e.g. {@link IsometricBackend}).
125
+ * for styles that warp coordinates (e.g. `IsometricStyle`).
117
126
  * Decorators delegate to their inner backend.
118
- * MapRenderer auto-applies this to culling and grid rendering when the backend is set.
119
127
  */
120
128
  getTransform(): CoordFn;
121
129
  /** Inverse of {@link getTransform}. */
122
130
  getInverseTransform(): CoordFn;
123
131
  }
132
+ /**
133
+ * Abstract base for style (decorator) backends. Forwards every
134
+ * {@link DrawingBackend} method to `this.inner` by default; subclasses override
135
+ * only the methods they transform. Generic over the wrapped inner type so
136
+ * tooling preserves specific types through chains where useful.
137
+ */
138
+ export declare abstract class BaseStyle<Inner extends DrawingBackend = DrawingBackend> implements DrawingBackend {
139
+ protected readonly inner: Inner;
140
+ constructor(inner: Inner);
141
+ createGroup(x: number, y: number): GroupNode;
142
+ addRect(parent: GroupNode, config: RectConfig): void;
143
+ addCircle(parent: GroupNode, config: CircleConfig): void;
144
+ addLine(parent: GroupNode, config: LineConfig): void;
145
+ addPolygon(parent: GroupNode, config: PolygonConfig): void;
146
+ addText(parent: GroupNode, config: TextConfig): void;
147
+ addImage(parent: GroupNode, config: ImageConfig): void;
148
+ supportsBatchExitRendering(): boolean;
149
+ getExitDepthOffset(): {
150
+ x: number;
151
+ y: number;
152
+ };
153
+ getTransform(): CoordFn;
154
+ getInverseTransform(): CoordFn;
155
+ }
156
+ /**
157
+ * A {@link Style} is a target-agnostic transformer: given a {@link DrawingBackend}
158
+ * it returns a decorated one. The same style drives interactive canvas, SVG
159
+ * export, and any future target.
160
+ *
161
+ * Compose via {@link compose}; built-in styles live in `src/style`.
162
+ */
163
+ export type Style = (target: DrawingBackend) => DrawingBackend;
164
+ /** Identity style — passes the target through unchanged. Useful as a default. */
165
+ export declare const identityStyle: Style;
166
+ /**
167
+ * Compose a chain of {@link Style}s into a single Style.
168
+ *
169
+ * `compose(Parchment, Sketchy)` wraps with Parchment first, then Sketchy —
170
+ * Sketchy is the outermost decorator, i.e. its methods run first during rendering.
171
+ */
172
+ export declare function compose(...styles: Style[]): Style;
@@ -34,6 +34,7 @@ export declare class KonvaBackend implements DrawingBackend {
34
34
  addPolygon(parent: GroupNode, config: PolygonConfig): void;
35
35
  addText(parent: GroupNode, config: TextConfig): void;
36
36
  addImage(parent: GroupNode, config: ImageConfig): void;
37
+ supportsBatchExitRendering(): boolean;
37
38
  getExitDepthOffset(): {
38
39
  x: number;
39
40
  y: number;
@@ -0,0 +1,67 @@
1
+ import { Exporter, ExportContext, ExportCanvas } from './Exporter';
2
+ export interface CanvasExportOptions {
3
+ /** Width of the output image in pixels. */
4
+ width: number;
5
+ /** Height of the output image in pixels. */
6
+ height: number;
7
+ /** Room ID to center the export on. If omitted, exports the full area. */
8
+ roomId?: number;
9
+ /** Padding in map units around the exported region. Default: 3 */
10
+ padding?: number;
11
+ /** Overlays to render over the scene (position marker, highlights, paths). */
12
+ overlays?: {
13
+ position?: {
14
+ roomId: number;
15
+ };
16
+ highlights?: Array<{
17
+ roomId: number;
18
+ color: string;
19
+ }>;
20
+ paths?: Array<{
21
+ locations: number[];
22
+ color: string;
23
+ }>;
24
+ };
25
+ }
26
+ /**
27
+ * Renders the current scene into a canvas at the requested width/height,
28
+ * reframing the viewport to fit the area (or a specific room) with padding.
29
+ *
30
+ * The returned {@link ExportCanvas} is the node-canvas-compatible object
31
+ * produced by Konva. Serialize with `.toBuffer('image/png')` in Node or
32
+ * `.toDataURL('image/png')` / `.toBlob(cb)` in the browser.
33
+ *
34
+ * Unlike {@link PngExporter} (which rasterizes the current on-screen viewport),
35
+ * `CanvasExporter` is the headless/programmatic path.
36
+ */
37
+ export declare class CanvasExporter implements Exporter<ExportCanvas | undefined> {
38
+ private readonly options;
39
+ constructor(options: CanvasExportOptions);
40
+ render({ backend }: ExportContext): ExportCanvas | undefined;
41
+ }
42
+ export interface PngBytesExportOptions extends CanvasExportOptions {
43
+ /** MIME type to encode. Defaults to `'image/png'`. */
44
+ mimeType?: string;
45
+ /** Encoder quality (0..1). Only used for lossy formats like `'image/jpeg'`. */
46
+ quality?: number;
47
+ }
48
+ /**
49
+ * Headless PNG/JPEG bytes at a specific width × height.
50
+ *
51
+ * Composes {@link CanvasExporter} with a portable `toDataURL` → `Uint8Array`
52
+ * decode, so callers get bytes directly without touching a canvas or casting
53
+ * to platform-specific types:
54
+ *
55
+ * ```ts
56
+ * const png = renderer.export(new PngBytesExporter({ width: 1920, height: 1080 }));
57
+ * fs.writeFileSync('out.png', png!); // Node
58
+ * new Blob([png!], { type: 'image/png' }); // Browser
59
+ * ```
60
+ *
61
+ * For JPEG: `new PngBytesExporter({ width, height, mimeType: 'image/jpeg', quality: 0.9 })`.
62
+ */
63
+ export declare class PngBytesExporter implements Exporter<Uint8Array | undefined> {
64
+ private readonly options;
65
+ constructor(options: PngBytesExportOptions);
66
+ render(context: ExportContext): Uint8Array | undefined;
67
+ }
@@ -0,0 +1,57 @@
1
+ import { MapState } from '../MapState';
2
+ import { Style } from '../backend/DrawingBackend';
3
+ import { SceneOverlay } from '../overlay/SceneOverlay';
4
+ import { InteractiveBackend } from '../rendering/MapRenderer';
5
+ /**
6
+ * Context handed to every {@link Exporter} by {@link MapRenderer.export}.
7
+ * Exporters pull whatever they need — state + style for SVG, backend for
8
+ * canvas/PNG rasterization — so callers never have to wire `renderer.backend`
9
+ * (or any other internal) into an exporter themselves.
10
+ */
11
+ export interface ExportContext {
12
+ readonly state: MapState;
13
+ readonly backend: InteractiveBackend;
14
+ readonly style: Style;
15
+ readonly sceneOverlays: Iterable<SceneOverlay>;
16
+ }
17
+ /**
18
+ * Pluggable output format. An {@link Exporter} consumes an {@link ExportContext}
19
+ * and returns an output of type `T` (a string, a Blob promise, a canvas, bytes…).
20
+ *
21
+ * Adding a new output format means implementing `Exporter<T>`. `MapRenderer`'s
22
+ * surface does not change — users just pass the new exporter to `renderer.export`.
23
+ *
24
+ * ```ts
25
+ * renderer.export(new SvgExporter({ padding: 5 })); // string
26
+ * renderer.export(new PngExporter({ pixelRatio: 2 })); // string (data URL)
27
+ * renderer.export(new CanvasExporter({ width, height })); // ExportCanvas
28
+ * ```
29
+ */
30
+ export interface Exporter<T> {
31
+ render(context: ExportContext): T;
32
+ }
33
+ /**
34
+ * Canvas returned by {@link InteractiveBackend.toCanvas} and
35
+ * {@link CanvasExporter}. Describes the portable surface common to the
36
+ * browser `HTMLCanvasElement` and the `canvas` package's Node-side Canvas:
37
+ *
38
+ * - Portable: `width`, `height`, `getContext`, `toDataURL`.
39
+ * - Browser: `toBlob(cb)` (optional here; use when serializing to a Blob).
40
+ *
41
+ * Platform-specific serializers (e.g. node-canvas's `toBuffer`) are intentionally
42
+ * not part of this interface. Cast when you need them:
43
+ *
44
+ * ```ts
45
+ * import type { Canvas } from 'canvas';
46
+ * const canvas = renderer.export(new CanvasExporter({width, height})) as unknown as Canvas;
47
+ * const png = canvas.toBuffer('image/png');
48
+ * ```
49
+ */
50
+ export interface ExportCanvas {
51
+ readonly width: number;
52
+ readonly height: number;
53
+ getContext(contextId: '2d', options?: any): CanvasRenderingContext2D | null;
54
+ toDataURL(type?: string, quality?: any): string;
55
+ /** Browser only; undefined in Node. */
56
+ toBlob?(callback: (blob: Blob | null) => void, type?: string, quality?: any): void;
57
+ }