mudlet-map-renderer 2.1.0 → 2.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/README.md +42 -0
- package/dist/{MapReader-mU_4JWv_.js → MapReader-Brzg-X7T.js} +1 -1
- package/dist/{MapReader-mU_4JWv_.js.map → MapReader-Brzg-X7T.js.map} +1 -1
- package/dist/assets/worker-DtQws85m.js.map +1 -0
- package/dist/binary.mjs +1 -1
- package/dist/export/flushSceneShapes.d.ts +1 -1
- package/dist/flushSceneShapes-Duhi0OQv.js +2937 -0
- package/dist/flushSceneShapes-Duhi0OQv.js.map +1 -0
- package/dist/hit/HitTester.d.ts +19 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.mjs +2534 -4696
- package/dist/index.mjs.map +1 -1
- package/dist/labelPlacement.d.ts +132 -0
- package/dist/offscreen.mjs +355 -0
- package/dist/offscreen.mjs.map +1 -0
- package/dist/overlay/RippleEffect.d.ts +48 -0
- package/dist/overlay/SceneOverlay.d.ts +16 -3
- package/dist/rendering/KonvaRenderBackend.d.ts +4 -0
- package/dist/rendering/MapRenderer.d.ts +12 -3
- package/dist/rendering/offscreen/OffscreenCanvasBackend.d.ts +109 -0
- package/dist/rendering/offscreen/index.d.ts +11 -0
- package/dist/rendering/offscreen/protocol.d.ts +100 -0
- package/dist/rendering/offscreen/renderFrame.d.ts +28 -0
- package/dist/rendering/offscreen/serializeTransform.d.ts +20 -0
- package/dist/rendering/offscreen/worker.d.ts +15 -0
- package/dist/style/Style.d.ts +12 -0
- package/dist/style/index.d.ts +24 -1
- package/dist/style/shape/DarkModernStyle.d.ts +15 -0
- package/dist/style/shape/GraphPaperStyle.d.ts +16 -0
- package/dist/style/shape/StainedGlassStyle.d.ts +15 -0
- package/dist/style/shape/TopographicStyle.d.ts +14 -0
- package/dist/style/shape/TreasureMapStyle.d.ts +23 -0
- package/dist/style/shape/WatercolorStyle.d.ts +32 -0
- package/dist/style/shape/index.d.ts +7 -0
- package/dist/style/shape/paintMap.d.ts +8 -0
- package/package.json +6 -1
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Point-feature label placement.
|
|
3
|
+
*
|
|
4
|
+
* Given a set of anchored labels (e.g. waypoints on rooms) and obstacles
|
|
5
|
+
* (rooms, exits, already-placed labels), choose for each label the candidate
|
|
6
|
+
* slot around its anchor that best avoids collisions. Pure and deterministic —
|
|
7
|
+
* the same inputs always produce the same layout — so it's reusable beyond
|
|
8
|
+
* waypoints and easy to test.
|
|
9
|
+
*
|
|
10
|
+
* For each slot the label is scanned outward (up to {@link extend}); the spot
|
|
11
|
+
* with the most CLEARANCE — distance to the nearest room, exit line, or other
|
|
12
|
+
* placed label — wins, lightly penalised by how far it had to push. This puts
|
|
13
|
+
* labels in open space, off every line, and only as far out as openness needs.
|
|
14
|
+
* Positions overlapping a room/label are skipped; sitting on an exit just reads
|
|
15
|
+
* as zero clearance. Cardinal-over-diagonal and an optional preferred direction
|
|
16
|
+
* break ties between equally-open slots.
|
|
17
|
+
*
|
|
18
|
+
* Placement is greedy: items are processed in input order and each placed label
|
|
19
|
+
* becomes an obstacle for the ones after it.
|
|
20
|
+
*/
|
|
21
|
+
export type Direction8 = "n" | "s" | "e" | "w" | "ne" | "nw" | "se" | "sw";
|
|
22
|
+
export interface Rect {
|
|
23
|
+
x: number;
|
|
24
|
+
y: number;
|
|
25
|
+
width: number;
|
|
26
|
+
height: number;
|
|
27
|
+
}
|
|
28
|
+
/** An obstacle the placement should avoid. `kind` weights how hard to avoid it. */
|
|
29
|
+
export interface Obstacle extends Rect {
|
|
30
|
+
kind?: "room" | "label" | "exit" | string;
|
|
31
|
+
}
|
|
32
|
+
export interface LabelPlacementItem {
|
|
33
|
+
/** Anchor point (e.g. room centre), world coords. */
|
|
34
|
+
x: number;
|
|
35
|
+
y: number;
|
|
36
|
+
/** Label box size, world units. */
|
|
37
|
+
width: number;
|
|
38
|
+
height: number;
|
|
39
|
+
/** Optional preferred direction — nudges the choice, never forces overlap. */
|
|
40
|
+
preferred?: Direction8;
|
|
41
|
+
/** Optional stable id, echoed back on the result. */
|
|
42
|
+
id?: string | number;
|
|
43
|
+
}
|
|
44
|
+
export interface PlacedLabel {
|
|
45
|
+
id?: string | number;
|
|
46
|
+
/** Top-left of the placed label box, world coords. */
|
|
47
|
+
x: number;
|
|
48
|
+
y: number;
|
|
49
|
+
width: number;
|
|
50
|
+
height: number;
|
|
51
|
+
/** Chosen slot. */
|
|
52
|
+
direction: Direction8;
|
|
53
|
+
/** Leader endpoint on the box edge nearest the anchor (for drawing a connector). */
|
|
54
|
+
leader: {
|
|
55
|
+
x: number;
|
|
56
|
+
y: number;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** Per-slot diagnostic from {@link PlaceLabelsOptions.onScored}. */
|
|
60
|
+
export interface SlotScore {
|
|
61
|
+
dir: Direction8;
|
|
62
|
+
/** False when no non-overlapping position exists in this slot. */
|
|
63
|
+
valid: boolean;
|
|
64
|
+
/** Clearance (distance to nearest obstacle) at the best position found. */
|
|
65
|
+
clearance: number;
|
|
66
|
+
/** Outward offset of the best position. */
|
|
67
|
+
off: number;
|
|
68
|
+
/** Final cost (lower = better; Infinity when invalid). */
|
|
69
|
+
cost: number;
|
|
70
|
+
/** The label box at the best position. */
|
|
71
|
+
box: Rect;
|
|
72
|
+
}
|
|
73
|
+
export interface PlaceLabelsOptions {
|
|
74
|
+
/** Gap between the anchor and the near edge of the label box, world units. Default 0.6. */
|
|
75
|
+
offset?: number;
|
|
76
|
+
/** Use only N/S/E/W (4) or all 8 slots. Default 8. */
|
|
77
|
+
slots?: 4 | 8;
|
|
78
|
+
/**
|
|
79
|
+
* How far (world units) a label may be pushed outward along a slot, beyond
|
|
80
|
+
* the base {@link offset}, while searching for the most open spot. 0 keeps
|
|
81
|
+
* labels at the base offset; larger lets them float into open pockets away
|
|
82
|
+
* from rooms/exit lines.
|
|
83
|
+
*/
|
|
84
|
+
extend?: number;
|
|
85
|
+
/**
|
|
86
|
+
* Clearance is only rewarded up to this many world units — past it a spot is
|
|
87
|
+
* "open enough" and extra distance isn't worth pushing further for. Defaults
|
|
88
|
+
* to the label's larger side.
|
|
89
|
+
*/
|
|
90
|
+
clearCap?: number;
|
|
91
|
+
/**
|
|
92
|
+
* When the best slot's clearance is below this (world units), the room is
|
|
93
|
+
* treated as "surrounded": instead of floating out for a marginal gain, the
|
|
94
|
+
* label sits close at the base offset on an exit, preferring north. Default 0
|
|
95
|
+
* (disabled — always go for max clearance).
|
|
96
|
+
*/
|
|
97
|
+
minClearance?: number;
|
|
98
|
+
/**
|
|
99
|
+
* Debug hook — called once per item with the score of every candidate slot
|
|
100
|
+
* and the chosen direction. For visualising/diagnosing placement decisions.
|
|
101
|
+
*/
|
|
102
|
+
onScored?: (item: LabelPlacementItem, slots: SlotScore[], chosen: Direction8) => void;
|
|
103
|
+
/** Cost weights (sane defaults; overlap dominates, then openness, then direction). */
|
|
104
|
+
weights?: Partial<typeof DEFAULT_WEIGHTS>;
|
|
105
|
+
}
|
|
106
|
+
declare const DEFAULT_WEIGHTS: {
|
|
107
|
+
/**
|
|
108
|
+
* Reward per world unit of clearance (distance from the label box to the
|
|
109
|
+
* nearest room/exit/label), capped at {@link PlaceLabelsOptions.clearCap}.
|
|
110
|
+
* The dominant term — pushes labels into open space, off every line.
|
|
111
|
+
*/
|
|
112
|
+
clear: number;
|
|
113
|
+
/**
|
|
114
|
+
* Cardinal-over-diagonal preference — a TRUE tie-break only. Kept tiny so
|
|
115
|
+
* any real clearance advantage (≳0.03 units) overrides it: a clearly more
|
|
116
|
+
* open diagonal always beats an on-a-line cardinal.
|
|
117
|
+
*/
|
|
118
|
+
diagonal: number;
|
|
119
|
+
/** Bonus (subtracted from cost) when a slot matches the item's preferred direction. */
|
|
120
|
+
preferred: number;
|
|
121
|
+
/**
|
|
122
|
+
* Penalty per world unit the label is pushed out — keeps it close, but small
|
|
123
|
+
* enough not to cancel the clearance gained by pushing into open space.
|
|
124
|
+
*/
|
|
125
|
+
distance: number;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Place each item at its best-scoring slot. Returns one {@link PlacedLabel} per
|
|
129
|
+
* input item, in the same order.
|
|
130
|
+
*/
|
|
131
|
+
export declare function placeLabels(items: LabelPlacementItem[], obstacles: Obstacle[], options?: PlaceLabelsOptions): PlacedLabel[];
|
|
132
|
+
export {};
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { E as e, T as t, _ as n, a as r, b as i, g as a, h as o, k as s, m as c, o as l, r as u, t as d, v as f, w as p, x as m, y as h } from "./flushSceneShapes-Duhi0OQv.js";
|
|
2
|
+
import g from "konva";
|
|
3
|
+
//#region src/rendering/offscreen/serializeTransform.ts
|
|
4
|
+
function _(e) {
|
|
5
|
+
let t = e(0, 0), n = e(1, 0), r = e(0, 1);
|
|
6
|
+
return [
|
|
7
|
+
n.x - t.x,
|
|
8
|
+
n.y - t.y,
|
|
9
|
+
r.x - t.x,
|
|
10
|
+
r.y - t.y,
|
|
11
|
+
t.x,
|
|
12
|
+
t.y
|
|
13
|
+
];
|
|
14
|
+
}
|
|
15
|
+
function v(e) {
|
|
16
|
+
return !e.worldToScene || !e.sceneToWorld ? { kind: "identity" } : {
|
|
17
|
+
kind: "affine",
|
|
18
|
+
forward: _((t, n) => e.worldToScene(t, n)),
|
|
19
|
+
inverse: _((t, n) => e.sceneToWorld(t, n))
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/rendering/offscreen/OffscreenCanvasBackend.ts
|
|
24
|
+
var y = "rgb(120, 72, 0)";
|
|
25
|
+
function b() {
|
|
26
|
+
return typeof document < "u" && typeof HTMLCanvasElement < "u" && typeof HTMLCanvasElement.prototype.transferControlToOffscreen == "function";
|
|
27
|
+
}
|
|
28
|
+
var x = class {
|
|
29
|
+
constructor(n, i, a = {}) {
|
|
30
|
+
this.hitTester = new l(), this.currentStyle = r, this._coordinateTransform = t, this.coordinateInverse = t, this.lastHitShapes = [], this.boundsByShape = /* @__PURE__ */ new Map(), this.sceneOverlays = /* @__PURE__ */ new Map(), this.viewportSubscribers = /* @__PURE__ */ new Set(), this.liveEffects = /* @__PURE__ */ new Map(), this.pendingExports = /* @__PURE__ */ new Map(), this.exportSeq = 0, this.destroyed = !1, this.state = n, this.container = i, this.pipeline = new o(n.mapReader, n.settings), this.dpr = a.devicePixelRatio ?? (typeof devicePixelRatio < "u" ? devicePixelRatio : 1), this.transport = a.transport ?? a.createTransport?.();
|
|
31
|
+
let u = i?.clientWidth ?? 1, d = i?.clientHeight ?? 1;
|
|
32
|
+
if (this.camera = new e(u, d), this.events = new s(i), this.culling = new p(n.settings, () => {
|
|
33
|
+
this.postSettings(), this.postCamera();
|
|
34
|
+
}), this.transport) {
|
|
35
|
+
let e = (e) => this.handleWorkerMessage(e.data);
|
|
36
|
+
this.transport.addEventListener ? this.transport.addEventListener("message", e) : this.transport.onmessage = e;
|
|
37
|
+
}
|
|
38
|
+
if (i && this.transport && b()) {
|
|
39
|
+
getComputedStyle(i).position === "static" && (i.style.position = "relative");
|
|
40
|
+
let e = document.createElement("canvas");
|
|
41
|
+
e.style.position = "absolute", e.style.inset = "0", e.style.width = "100%", e.style.height = "100%", e.style.display = "block", i.appendChild(e), i.style.backgroundColor = n.settings.backgroundColor;
|
|
42
|
+
let t = e.transferControlToOffscreen();
|
|
43
|
+
this.transport.postMessage({
|
|
44
|
+
type: "init",
|
|
45
|
+
canvas: t,
|
|
46
|
+
width: u,
|
|
47
|
+
height: d,
|
|
48
|
+
dpr: this.dpr,
|
|
49
|
+
settings: n.settings
|
|
50
|
+
}, [t]), this.interactionHandler = new c(i, this.camera, n, {
|
|
51
|
+
clientToMapPoint: (e, t) => this.camera.clientToMapPoint(e, t, i.getBoundingClientRect()),
|
|
52
|
+
pickAtPoint: (e, t) => this.hitTester.pick(e, t)
|
|
53
|
+
}, this.events);
|
|
54
|
+
}
|
|
55
|
+
this.cameraChangeHandler = () => this.onCameraChange(), this.camera.on("change", this.cameraChangeHandler), this.applyStyleTransforms(), this.subscribeToState(n), this.postCamera();
|
|
56
|
+
}
|
|
57
|
+
get coordinateTransform() {
|
|
58
|
+
return this._coordinateTransform;
|
|
59
|
+
}
|
|
60
|
+
post(e, t) {
|
|
61
|
+
this.destroyed || !this.transport || this.transport.postMessage(e, t);
|
|
62
|
+
}
|
|
63
|
+
postSettings() {
|
|
64
|
+
this.post({
|
|
65
|
+
type: "settings",
|
|
66
|
+
settings: this.state.settings
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
postCamera() {
|
|
70
|
+
this.post({
|
|
71
|
+
type: "camera",
|
|
72
|
+
camera: {
|
|
73
|
+
scale: this.camera.getScale(),
|
|
74
|
+
offsetX: this.camera.position.x,
|
|
75
|
+
offsetY: this.camera.position.y
|
|
76
|
+
},
|
|
77
|
+
viewport: this.camera.getViewportBounds(),
|
|
78
|
+
width: this.camera.width,
|
|
79
|
+
height: this.camera.height
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
postScene() {
|
|
83
|
+
let e = this.lastResult;
|
|
84
|
+
if (!e) {
|
|
85
|
+
this.post({
|
|
86
|
+
type: "scene",
|
|
87
|
+
link: {
|
|
88
|
+
shapes: [],
|
|
89
|
+
bounds: []
|
|
90
|
+
},
|
|
91
|
+
room: {
|
|
92
|
+
shapes: [],
|
|
93
|
+
bounds: []
|
|
94
|
+
},
|
|
95
|
+
topLabel: [],
|
|
96
|
+
transform: v(this.currentStyle)
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
let t = this.styleContext();
|
|
101
|
+
this.post({
|
|
102
|
+
type: "scene",
|
|
103
|
+
link: this.styleLayer(e.sceneShapes.link, t),
|
|
104
|
+
room: this.styleLayer(e.sceneShapes.room, t),
|
|
105
|
+
topLabel: this.styleShapes(e.sceneShapes.topLabel, t),
|
|
106
|
+
transform: v(this.currentStyle)
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
postOverlays() {
|
|
110
|
+
let e = this.styleContext(), t = [];
|
|
111
|
+
for (let n of this.buildCurrentRoomOverlayShapes()) t.push(...this.styleOne(n, e, !1));
|
|
112
|
+
for (let n of d(this.state)) t.push(...this.styleOne(n, e, !1));
|
|
113
|
+
for (let n of this.sceneOverlays.values()) {
|
|
114
|
+
let r = n.render(this.state, this.camera.getViewportBounds());
|
|
115
|
+
if (!r) continue;
|
|
116
|
+
let i = Array.isArray(r) ? r : [r];
|
|
117
|
+
for (let r of i) t.push(...this.styleOne(r, e, n.sceneSpace ?? !1));
|
|
118
|
+
}
|
|
119
|
+
this.post({
|
|
120
|
+
type: "overlays",
|
|
121
|
+
shapes: t
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
styleContext() {
|
|
125
|
+
return {
|
|
126
|
+
scale: this.camera.getScale(),
|
|
127
|
+
roomSize: this.state.settings.roomSize
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
styleOne(e, t, n) {
|
|
131
|
+
return n || this.currentStyle === r ? [e] : u([e], this.currentStyle, t);
|
|
132
|
+
}
|
|
133
|
+
styleShapes(e, t) {
|
|
134
|
+
return this.currentStyle === r ? e : u(e, this.currentStyle, t);
|
|
135
|
+
}
|
|
136
|
+
styleLayer(e, t) {
|
|
137
|
+
let n = [], i = [];
|
|
138
|
+
for (let a of e) {
|
|
139
|
+
let e = this.boundsByShape.get(a) ?? null, o = this.currentStyle === r ? [a] : u([a], this.currentStyle, t);
|
|
140
|
+
for (let t of o) n.push(t), i.push(e);
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
shapes: n,
|
|
144
|
+
bounds: i
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
refresh() {
|
|
148
|
+
let { currentAreaInstance: e, currentZIndex: t, positionRoomId: n } = this.state;
|
|
149
|
+
if (!e || t === void 0) return;
|
|
150
|
+
let r = e.getPlane(t);
|
|
151
|
+
if (!r) {
|
|
152
|
+
this.lastResult = void 0, this.lastHitShapes = [], this.boundsByShape.clear(), this.hitTester.clear(), this.postSettings(), this.postScene(), this.postOverlays(), this.postCamera();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
let i = this.pipeline.buildScene(e, r, t, this.state.lens);
|
|
156
|
+
this.lastResult = i, this.lastHitShapes = i.hitShapes, this.boundsByShape = this.buildBoundsMap(i), this.hitTester.build(this.lastHitShapes, this.state.settings.roomSize, this._coordinateTransform, this.coordinateLayerOffset), this.container && (this.container.style.backgroundColor = this.state.settings.backgroundColor), this.postSettings(), this.postScene(), this.postOverlays(), this.postCamera();
|
|
157
|
+
}
|
|
158
|
+
buildBoundsMap(e) {
|
|
159
|
+
let t = /* @__PURE__ */ new Map(), n = this.state.settings.roomSize / 2;
|
|
160
|
+
for (let { room: r, shape: i } of e.roomShapeRefs.values()) t.set(i, {
|
|
161
|
+
x: r.x - n,
|
|
162
|
+
y: r.y - n,
|
|
163
|
+
width: this.state.settings.roomSize,
|
|
164
|
+
height: this.state.settings.roomSize
|
|
165
|
+
});
|
|
166
|
+
for (let { shape: n, bounds: r } of e.standaloneExitShapeRefs) t.set(n, r);
|
|
167
|
+
for (let { shape: n, bounds: r } of e.labelShapeRefs) t.set(n, r);
|
|
168
|
+
for (let { shape: n, bounds: r } of e.specialExitShapeRefs) t.set(n, r);
|
|
169
|
+
for (let { shape: n, bounds: r } of e.stubShapeRefs) t.set(n, r);
|
|
170
|
+
for (let { shape: n, bounds: r } of e.areaExitLabelShapeRefs) t.set(n, r);
|
|
171
|
+
return t;
|
|
172
|
+
}
|
|
173
|
+
buildCurrentRoomOverlayShapes() {
|
|
174
|
+
let e = this.state, t = e.positionRoomId;
|
|
175
|
+
if (t === void 0) return [];
|
|
176
|
+
let r = e.mapReader.getRoom(t);
|
|
177
|
+
if (!r || r.area !== e.currentArea || r.z !== e.currentZIndex) return [];
|
|
178
|
+
let o = e.settings;
|
|
179
|
+
if (!o.highlightCurrentRoom) return [];
|
|
180
|
+
let s = [], c = this.pipeline.exitRenderer, l = e.lens, u = /* @__PURE__ */ new Map();
|
|
181
|
+
if (u.set(r.id, r), e.currentAreaInstance && e.currentZIndex !== void 0) {
|
|
182
|
+
let t = e.currentAreaInstance.getLinkExits(e.currentZIndex).filter((e) => e.a === r.id || e.b === r.id);
|
|
183
|
+
for (let n of t) {
|
|
184
|
+
let t = c.renderDataWithColor(n, y, e.currentZIndex);
|
|
185
|
+
t && s.push(this.pipeline.buildExitShape(t));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
for (let e of i(r, o, y)) s.push(n(e, r.id));
|
|
189
|
+
for (let e of m(r, o, y)) s.push(a(e));
|
|
190
|
+
return [...Object.values(r.exits), ...Object.values(r.specialExits)].forEach((t) => {
|
|
191
|
+
let n = e.mapReader.getRoom(t);
|
|
192
|
+
n && n.area === e.currentArea && n.z === e.currentZIndex && l.isVisible(n) && u.set(n.id, n);
|
|
193
|
+
}), u.forEach((t, n) => {
|
|
194
|
+
let i = n === r.id, a = h(t, e.mapReader, o, {
|
|
195
|
+
strokeOverride: i ? y : o.lineColor,
|
|
196
|
+
flatPipeline: !0
|
|
197
|
+
});
|
|
198
|
+
a.children.push(...f(t, e.mapReader, o)), s.push(a);
|
|
199
|
+
}), s;
|
|
200
|
+
}
|
|
201
|
+
setStyle(e) {
|
|
202
|
+
this.currentStyle = e, this.lastHitShapes = [], this.hitTester.clear(), this.applyStyleTransforms(), this.refresh();
|
|
203
|
+
}
|
|
204
|
+
applyStyleTransforms() {
|
|
205
|
+
let e = this.currentStyle, n = e.worldToScene ? (t, n) => e.worldToScene(t, n) : t, r = e.sceneToWorld ? (t, n) => e.sceneToWorld(t, n) : t, i = this.coordinateInverse, a = e.sceneLayerOffset ? (t) => e.sceneLayerOffset(t) : void 0;
|
|
206
|
+
this._coordinateTransform = n, this.coordinateInverse = r, this.coordinateLayerOffset = a, this.culling.setCoordinateTransform(n), this.lastHitShapes.length > 0 && this.hitTester.build(this.lastHitShapes, this.state.settings.roomSize, n, a);
|
|
207
|
+
let o = this.camera.getScale(), s = this.camera.width / 2, c = this.camera.height / 2, l = i((s - this.camera.position.x) / o, (c - this.camera.position.y) / o), u = n(l.x, l.y);
|
|
208
|
+
this.camera.position = {
|
|
209
|
+
x: s - u.x * o,
|
|
210
|
+
y: c - u.y * o
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
onCameraChange() {
|
|
214
|
+
this.postCamera();
|
|
215
|
+
let e = this.camera.getViewportBounds();
|
|
216
|
+
this.events.emit("pan", e);
|
|
217
|
+
for (let e of this.viewportSubscribers) e();
|
|
218
|
+
if (this.liveEffectStage) {
|
|
219
|
+
this.syncEffectStage();
|
|
220
|
+
let t = this.camera.getScale();
|
|
221
|
+
for (let n of this.liveEffects.values()) n.updateViewport(e, t, this._coordinateTransform);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
subscribeToState(e) {
|
|
225
|
+
e.events.on("area", () => this.refresh()), e.events.on("position", ({ roomId: e, center: t, areaChanged: n }) => this.onPositionChanged(e, t, n)), e.events.on("center", ({ roomId: t, instant: n }) => {
|
|
226
|
+
let r = e.mapReader.getRoom(t);
|
|
227
|
+
if (r) {
|
|
228
|
+
let e = this._coordinateTransform(r.x, r.y);
|
|
229
|
+
this.camera.panToMapPointAnimated(e.x, e.y, n || this.state.settings.instantMapMove);
|
|
230
|
+
}
|
|
231
|
+
}), e.events.on("highlight", () => this.postOverlays()), e.events.on("path", () => this.postOverlays()), e.events.on("clear", () => this.postOverlays()), e.events.on("lens", () => this.refresh());
|
|
232
|
+
}
|
|
233
|
+
onPositionChanged(e, t, n) {
|
|
234
|
+
if (e !== void 0 && t) {
|
|
235
|
+
let t = this.state.mapReader.getRoom(e);
|
|
236
|
+
if (t) {
|
|
237
|
+
let e = this._coordinateTransform(t.x, t.y);
|
|
238
|
+
this.camera.panToMapPointAnimated(e.x, e.y, n || this.state.settings.instantMapMove);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
this.postOverlays();
|
|
242
|
+
}
|
|
243
|
+
updateBackground() {
|
|
244
|
+
this.container && (this.container.style.backgroundColor = this.state.settings.backgroundColor), this.postSettings(), this.postCamera();
|
|
245
|
+
}
|
|
246
|
+
addSceneOverlay(e, t) {
|
|
247
|
+
let n = this.sceneOverlays.get(e);
|
|
248
|
+
n && n.detach?.(), this.sceneOverlays.set(e, t), t.attach?.(this.createOverlayContext(e, t)), this.postOverlays();
|
|
249
|
+
}
|
|
250
|
+
removeSceneOverlay(e) {
|
|
251
|
+
let t = this.sceneOverlays.get(e);
|
|
252
|
+
t && (t.detach?.(), this.sceneOverlays.delete(e), this.postOverlays());
|
|
253
|
+
}
|
|
254
|
+
getSceneOverlays() {
|
|
255
|
+
return this.sceneOverlays.values();
|
|
256
|
+
}
|
|
257
|
+
createOverlayContext(e, t) {
|
|
258
|
+
return {
|
|
259
|
+
state: this.state,
|
|
260
|
+
onViewportChange: (e) => (this.viewportSubscribers.add(e), () => this.viewportSubscribers.delete(e)),
|
|
261
|
+
invalidate: () => {
|
|
262
|
+
this.sceneOverlays.get(e) === t && this.postOverlays();
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
ensureEffectStage() {
|
|
267
|
+
if (this.liveEffectStage || !this.container) return;
|
|
268
|
+
let e = document.createElement("div");
|
|
269
|
+
e.style.position = "absolute", e.style.inset = "0", e.style.pointerEvents = "none", this.container.appendChild(e);
|
|
270
|
+
let t = new g.Stage({
|
|
271
|
+
container: e,
|
|
272
|
+
width: this.camera.width,
|
|
273
|
+
height: this.camera.height,
|
|
274
|
+
listening: !1
|
|
275
|
+
}), n = new g.Layer({ listening: !1 });
|
|
276
|
+
t.add(n), this.liveEffectHost = e, this.liveEffectStage = t, this.liveEffectLayer = n, this.syncEffectStage();
|
|
277
|
+
}
|
|
278
|
+
syncEffectStage() {
|
|
279
|
+
let e = this.liveEffectStage;
|
|
280
|
+
if (!e) return;
|
|
281
|
+
(e.width() !== this.camera.width || e.height() !== this.camera.height) && (e.width(this.camera.width), e.height(this.camera.height));
|
|
282
|
+
let t = this.camera.getScale();
|
|
283
|
+
e.scale({
|
|
284
|
+
x: t,
|
|
285
|
+
y: t
|
|
286
|
+
}), e.position(this.camera.position), e.batchDraw();
|
|
287
|
+
}
|
|
288
|
+
addLiveEffect(e, t) {
|
|
289
|
+
this.removeLiveEffect(e), this.ensureEffectStage(), this.liveEffectLayer && (t.attach(this.liveEffectLayer), this.liveEffects.set(e, t), this.syncEffectStage(), t.updateViewport(this.camera.getViewportBounds(), this.camera.getScale(), this._coordinateTransform));
|
|
290
|
+
}
|
|
291
|
+
removeLiveEffect(e) {
|
|
292
|
+
let t = this.liveEffects.get(e);
|
|
293
|
+
t && (t.destroy(), this.liveEffects.delete(e), this.liveEffectLayer?.batchDraw());
|
|
294
|
+
}
|
|
295
|
+
getDrawnExits() {
|
|
296
|
+
return this.lastResult?.drawnExits ?? [];
|
|
297
|
+
}
|
|
298
|
+
getDrawnSpecialExits() {
|
|
299
|
+
return this.lastResult?.drawnSpecialExits ?? [];
|
|
300
|
+
}
|
|
301
|
+
getDrawnStubs() {
|
|
302
|
+
return this.lastResult?.drawnStubs ?? [];
|
|
303
|
+
}
|
|
304
|
+
exportCanvas() {}
|
|
305
|
+
captureViewport(e) {
|
|
306
|
+
if (!this.transport) return Promise.reject(/* @__PURE__ */ Error("No worker transport"));
|
|
307
|
+
let t = ++this.exportSeq;
|
|
308
|
+
return new Promise((n) => {
|
|
309
|
+
this.pendingExports.set(t, n), this.post({
|
|
310
|
+
type: "export",
|
|
311
|
+
requestId: t,
|
|
312
|
+
pixelRatio: e?.pixelRatio ?? this.dpr
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
handleWorkerMessage(e) {
|
|
317
|
+
if (e.type === "export") {
|
|
318
|
+
let t = this.pendingExports.get(e.requestId);
|
|
319
|
+
t && (this.pendingExports.delete(e.requestId), t(e.bitmap));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
destroy() {
|
|
323
|
+
if (!this.destroyed) {
|
|
324
|
+
this.destroyed = !0, this.state.events.removeAllListeners(), this.interactionHandler?.destroy();
|
|
325
|
+
for (let e of this.liveEffects.values()) e.destroy();
|
|
326
|
+
this.liveEffects.clear(), this.liveEffectStage?.destroy(), this.liveEffectStage = void 0, this.liveEffectLayer = void 0, this.liveEffectHost?.remove(), this.liveEffectHost = void 0;
|
|
327
|
+
for (let e of this.sceneOverlays.values()) e.detach?.();
|
|
328
|
+
this.sceneOverlays.clear(), this.viewportSubscribers.clear(), this.pendingExports.clear(), this.camera.cancelAnimation(), this.cameraChangeHandler &&= (this.camera.off("change", this.cameraChangeHandler), void 0), this.post({ type: "destroy" }), this.transport?.terminate?.(), this.events.removeAllListeners();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}, S = "(function(){function e(e,t,n={}){if(!t.gridEnabled)return[];let r=n.inverseTransform??((e,t)=>({x:e,y:t})),{minX:i,maxX:a,minY:o,maxY:s}=e,c=r(i,o),l=r(a,o),u=r(a,s),d=r(i,s),f=Math.min(c.x,l.x,u.x,d.x),p=Math.max(c.x,l.x,u.x,d.x),m=Math.min(c.y,l.y,u.y,d.y),h=Math.max(c.y,l.y,u.y,d.y),g=t.gridSize*2,_=Math.floor((f-g)/t.gridSize)*t.gridSize,v=Math.ceil((p+g)/t.gridSize)*t.gridSize,y=Math.floor((m-g)/t.gridSize)*t.gridSize,b=Math.ceil((h+g)/t.gridSize)*t.gridSize,x=[],S={stroke:t.gridColor,strokeWidth:t.gridLineWidth};for(let e=_;e<=v;e+=t.gridSize)x.push({type:`line`,points:[e,y,e,b],paint:S,grid:!0,layer:`grid`});for(let e=y;e<=b;e+=t.gridSize)x.push({type:`line`,points:[_,e,v,e],paint:S,grid:!0,layer:`grid`});return x}function t(e){return typeof e==`object`&&!!e}function n(e,n,r,i,a,o){return t(e)?e.type===`linear`?{type:`linear`,x0:(n+e.x0)*i+a,y0:(r+e.y0)*i+o,x1:(n+e.x1)*i+a,y1:(r+e.y1)*i+o,stops:e.stops}:{type:`radial`,cx:(n+e.cx)*i+a,cy:(r+e.cy)*i+o,r:e.r*i,fx:e.fx===void 0?void 0:(n+e.fx)*i+a,fy:e.fy===void 0?void 0:(r+e.fy)*i+o,fr:e.fr===void 0?void 0:e.fr*i,stops:e.stops}:e}let r=`room`;function i(e,t){let i=new Map;function c(e){let t=i.get(e);return t||(t=[],i.set(e,t)),t}function l(e,t,i,l,d,f,p){let m=e.layer??p??r;switch(e.type){case`rect`:{let r={type:`rect`,x:(t+e.x)*l+d,y:(i+e.y)*l+f,w:e.width*l,h:e.height*l,fill:n(e.paint.fill,t,i,l,d,f),stroke:e.paint.stroke,sw:(e.paint.strokeWidth??0)*l,cr:(e.cornerRadius??0)*l,dash:o(e.paint.dash,l,e.paint.dashEnabled)};c(m).push(r);return}case`circle`:c(m).push({type:`circle`,cx:(t+e.cx)*l+d,cy:(i+e.cy)*l+f,r:e.radius*l,fill:n(e.paint.fill,t,i,l,d,f),stroke:e.paint.stroke,sw:(e.paint.strokeWidth??0)*l,dash:o(e.paint.dash,l,e.paint.dashEnabled)});return;case`line`:{let n={type:`line`,points:a(e.points,t,i,l,d,f),stroke:e.paint.stroke,sw:(e.paint.strokeWidth??0)*l,dash:o(e.paint.dash,l,e.paint.dashEnabled),lineCap:e.lineCap,lineJoin:e.lineJoin,alpha:e.paint.alpha};c(m).push(n);return}case`polygon`:{let r={type:`polygon`,vertices:a(e.vertices,t,i,l,d,f),fill:n(e.paint.fill,t,i,l,d,f),stroke:e.paint.stroke,sw:(e.paint.strokeWidth??0)*l};c(m).push(r);return}case`text`:{let n=s(e.transform,t,i,l,d,f),r={type:`text`,x:n?0:(t+e.x)*l+d,y:n?0:(i+e.y)*l+f,text:e.text,fontSize:e.fontSize*l,fontFamily:e.fontFamily??`sans-serif`,fontStyle:e.fontStyle??`normal`,fill:e.fill??`black`,stroke:e.stroke,sw:(e.strokeWidth??0)*l,align:e.align??`left`,vAlign:e.verticalAlign??`top`,w:(e.width??0)*l,h:(e.height??0)*l,baselineRatio:e.baselineRatio,konvaCorrectionRatio:e.konvaCorrectionRatio,transform:n};c(m).push(r);return}case`image`:{let n=s(e.transform,t,i,l,d,f),r=n?0:(t+e.x)*l+d,a=n?0:(i+e.y)*l+f;c(m).push({type:`image`,x:r,y:a,w:e.width*l,h:e.height*l,src:e.src,transform:n});return}case`group`:u(e,t,i,l,d,f,m);return}}function u(e,t,n,i,a,o,s){let u=e.layer??s;if(e.noScale){let s=(t+e.x)*i+a,d=(n+e.y)*i+o,f=c(u??r);f.push({type:`pushTransform`,matrix:[1,0,0,1,s,d]});for(let t of e.children)l(t,0,0,1,0,0,u);f.push({type:`popTransform`});return}let d=t+e.x,f=n+e.y;for(let t of e.children)l(t,d,f,i,a,o,u)}for(let n of e)l(n,0,0,t.scale,t.offsetX,t.offsetY,void 0);let d=[];for(let[e,t]of i)d.push({layer:e,commands:t});return d}function a(e,t,n,r,i,a){let o=Array(e.length);for(let s=0;s<e.length;s+=2)o[s]=(t+e[s])*r+i,o[s+1]=(n+e[s+1])*r+a;return o}function o(e,t,n){if(n===!1)return;if(!e||e.length===0)return e;let r=Array(e.length);for(let n=0;n<e.length;n++)r[n]=e[n]*t;return r}function s(e,t,n,r,i,a){if(!e)return;let[o,s,c,l,u,d]=e;return[o*r,s*r,c*r,l*r,u*r+t*r+i,d*r+n*r+a]}function c(e,t){if(typeof t==`string`)return t;if(t.type===`linear`){let n=e.createLinearGradient(t.x0,t.y0,t.x1,t.y1);for(let e of t.stops)n.addColorStop(e.offset,e.color);return n}let n=t.fx??t.cx,r=t.fy??t.cy,i=t.fr??0,a=e.createRadialGradient(n,r,i,t.cx,t.cy,t.r);for(let e of t.stops)a.addColorStop(e.offset,e.color);return a}let l=e=>{if(typeof Image<`u`){let t=new Image;return t.src=e,t}return null};function u(e,t,n={}){let r=n.imageFactory??l;for(let n of t)d(e,n.commands,r)}function d(e,t,n){let r=0;for(let i of t)switch(i.type){case`pushTransform`:e.save(),r++,e.transform(...i.matrix);break;case`pushClip`:e.save(),r++,e.beginPath(),e.rect(i.x,i.y,i.w,i.h),e.clip();break;case`popTransform`:case`popClip`:r>0&&(e.restore(),r--);break;default:f(e,i,n);break}for(;r>0;)e.restore(),r--}function f(e,t,n){switch(t.type){case`rect`:e.beginPath(),t.cr>0&&typeof e.roundRect==`function`?e.roundRect(t.x,t.y,t.w,t.h,t.cr):e.rect(t.x,t.y,t.w,t.h),t.fill&&(e.fillStyle=c(e,t.fill),e.fill()),t.stroke&&t.sw>0&&(e.strokeStyle=t.stroke,e.lineWidth=t.sw,e.setLineDash(t.dash??[]),e.stroke());break;case`circle`:e.beginPath(),e.arc(t.cx,t.cy,t.r,0,Math.PI*2),t.fill&&(e.fillStyle=c(e,t.fill),e.fill()),t.stroke&&t.sw>0&&(e.strokeStyle=t.stroke,e.lineWidth=t.sw,e.setLineDash(t.dash??[]),e.stroke());break;case`line`:{if(t.points.length<4)break;let n=e.globalAlpha;t.alpha!==void 0&&(e.globalAlpha=t.alpha),e.beginPath(),e.moveTo(t.points[0],t.points[1]);for(let n=2;n<t.points.length;n+=2)e.lineTo(t.points[n],t.points[n+1]);t.stroke&&(e.strokeStyle=t.stroke),e.lineWidth=t.sw,e.setLineDash(t.dash??[]),t.lineCap&&(e.lineCap=t.lineCap),t.lineJoin&&(e.lineJoin=t.lineJoin),e.stroke(),t.alpha!==void 0&&(e.globalAlpha=n);break}case`polygon`:if(t.vertices.length<4)break;e.beginPath(),e.moveTo(t.vertices[0],t.vertices[1]);for(let n=2;n<t.vertices.length;n+=2)e.lineTo(t.vertices[n],t.vertices[n+1]);e.closePath(),t.fill&&(e.fillStyle=c(e,t.fill),e.fill()),t.stroke&&t.sw>0&&(e.strokeStyle=t.stroke,e.lineWidth=t.sw,e.setLineDash([]),e.stroke());break;case`text`:{let n=t.fontSize*100,r=`${t.fontStyle} ${n}px ${t.fontFamily}`;e.save(),e.font=r,e.fillStyle=t.fill,t.stroke&&t.sw>0&&(e.strokeStyle=t.stroke,e.lineWidth=t.sw*100,e.lineJoin=`round`);let i=t.baselineRatio!==void 0;if(t.transform)if(e.transform(...t.transform),e.scale(1/100,1/100),e.textAlign=`center`,i){e.textBaseline=`alphabetic`;let n=(t.h/2+t.baselineRatio*t.fontSize)*100;t.stroke&&t.sw>0&&e.strokeText(t.text,t.w*100/2,n),e.fillText(t.text,t.w*100/2,n)}else{e.textBaseline=`middle`;let n=t.w*100/2,r=t.h*100/2;t.stroke&&t.sw>0&&e.strokeText(t.text,n,r),e.fillText(t.text,n,r)}else if(t.w>0&&t.h>0){e.textAlign=t.align||`left`;let n=t.align===`center`?t.x+t.w/2:t.align===`right`?t.x+t.w:t.x;if(e.scale(1/100,1/100),t.vAlign===`middle`&&i){e.textBaseline=`alphabetic`;let r=t.y+t.h/2+t.baselineRatio*t.fontSize;t.stroke&&t.sw>0&&e.strokeText(t.text,n*100,r*100),e.fillText(t.text,n*100,r*100)}else{e.textBaseline=t.vAlign===`middle`?`middle`:`top`;let r=t.vAlign===`middle`?t.y+t.h/2:t.y;t.stroke&&t.sw>0&&e.strokeText(t.text,n*100,r*100),e.fillText(t.text,n*100,r*100)}}else e.textAlign=`left`,e.textBaseline=`top`,e.scale(1/100,1/100),t.stroke&&t.sw>0&&e.strokeText(t.text,t.x*100,t.y*100),e.fillText(t.text,t.x*100,t.y*100);e.restore();break}case`image`:{let r=n(t.src);if(!r)break;t.transform?(e.save(),e.transform(...t.transform),e.drawImage(r,0,0,t.w,t.h),e.restore()):e.drawImage(r,t.x,t.y,t.w,t.h);break}}}function p(e,t,n){return{x:e[0]*t+e[2]*n+e[4],y:e[1]*t+e[3]*n+e[5]}}function m(e){if(e.kind===`identity`)return;let t=e.forward;return(e,n)=>p(t,e,n)}function h(e){if(e.kind===`identity`)return;let t=e.inverse;return(e,n)=>p(t,e,n)}function g(e,t,n,r,i){let a=i(e,t),o=i(n,t),s=i(n,r),c=i(e,r);return{minX:Math.min(a.x,o.x,s.x,c.x),minY:Math.min(a.y,o.y,s.y,c.y),maxX:Math.max(a.x,o.x,s.x,c.x),maxY:Math.max(a.y,o.y,s.y,c.y)}}function _(e,t,n){let{minX:r,maxX:i,minY:a,maxY:o}=t,s=[];for(let t=0;t<e.shapes.length;t++){let c=e.bounds[t];if(!c){s.push(e.shapes[t]);continue}let l=c.x,u=c.y,d=c.x+c.width,f=c.y+c.height;if(n){let e=g(l,u,d,f,n);l=e.minX,u=e.minY,d=e.maxX,f=e.maxY}d>=r&&l<=i&&f>=a&&u<=o&&s.push(e.shapes[t])}return s}function v(t,n){let{scene:r,overlays:a,camera:o,viewport:s,settings:c,width:l,height:d,imageFactory:f}=n,p=f?{imageFactory:f}:void 0;if(t.fillStyle=c.backgroundColor,t.fillRect(0,0,l,d),!r)return;let g=m(r.transform),v=e(s,c,{inverseTransform:h(r.transform)});v.length>0&&u(t,i(v,o),p);let y=c.cullingEnabled?_(r.link,s,g):r.link.shapes;y.length>0&&u(t,i(y,o),p);let b=c.cullingEnabled?_(r.room,s,g):r.room.shapes;b.length>0&&u(t,i(b,o),p),a.length>0&&u(t,i(a,o),p),r.topLabel.length>0&&u(t,i(r.topLabel,o),p)}let y=globalThis,b=null,x=null,S=1,C=1,w=1,T=null,E=null,D=[],O={scale:1,offsetX:0,offsetY:0},k={minX:0,maxX:0,minY:0,maxY:0},A=!1,j=!1,M=new Map;function N(e){let t=M.get(e);return t===void 0?(M.set(e,null),fetch(e).then(e=>e.blob()).then(e=>createImageBitmap(e)).then(t=>{if(M.size>64){let t=M.keys().next().value;if(t!==void 0&&t!==e){let e=M.get(t);e&&e.close(),M.delete(t)}}M.set(e,t),F()}).catch(()=>{}),null):t}let P=typeof requestAnimationFrame<`u`?e=>{requestAnimationFrame(e)}:e=>{setTimeout(()=>e(0),16)};function F(){A=!0,!j&&(j=!0,P(()=>{j=!1,A&&L()}))}function I(){b&&(b.width=Math.max(1,Math.round(C*S)),b.height=Math.max(1,Math.round(w*S)),x=b.getContext(`2d`))}function L(){A=!1,!(!x||!T)&&(x.setTransform(S,0,0,S,0,0),v(x,{scene:E,overlays:D,camera:O,viewport:k,settings:T,width:C,height:w,imageFactory:N}))}async function R(e){if(!b)return;A&&L();let t=await createImageBitmap(b);y.postMessage({type:`export`,requestId:e,bitmap:t},[t])}y.onmessage=e=>{let t=e.data;switch(t.type){case`init`:b=t.canvas,S=t.dpr,C=t.width,w=t.height,T=t.settings,I(),y.postMessage({type:`ready`},[]),F();break;case`settings`:T=t.settings,F();break;case`scene`:E={link:t.link,room:t.room,topLabel:t.topLabel,transform:t.transform},F();break;case`overlays`:D=t.shapes,F();break;case`camera`:O=t.camera,k=t.viewport,(t.width!==C||t.height!==w)&&(C=t.width,w=t.height,I()),F();break;case`resize`:C=t.width,w=t.height,S=t.dpr,I(),F();break;case`export`:R(t.requestId);break;case`destroy`:b=null,x=null,E=null,D=[],T=null;for(let e of M.values())e?.close();M.clear();break}}})();\n//# sourceMappingURL=worker-DtQws85m.js.map", C = typeof self < "u" && self.Blob && new Blob(["(self.URL || self.webkitURL).revokeObjectURL(self.location.href);", S], { type: "text/javascript;charset=utf-8" });
|
|
332
|
+
function w(e) {
|
|
333
|
+
let t;
|
|
334
|
+
try {
|
|
335
|
+
if (t = C && (self.URL || self.webkitURL).createObjectURL(C), !t) throw "";
|
|
336
|
+
let n = new Worker(t, { name: e?.name });
|
|
337
|
+
return n.addEventListener("error", () => {
|
|
338
|
+
(self.URL || self.webkitURL).revokeObjectURL(t);
|
|
339
|
+
}), n;
|
|
340
|
+
} catch {
|
|
341
|
+
return new Worker("data:text/javascript;charset=utf-8," + encodeURIComponent(S), { name: e?.name });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
//#endregion
|
|
345
|
+
//#region src/rendering/offscreen/index.ts
|
|
346
|
+
function T(e, t = {}) {
|
|
347
|
+
return (n) => new x(n, e, {
|
|
348
|
+
...t,
|
|
349
|
+
createTransport: t.createTransport ?? (() => new w())
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
//#endregion
|
|
353
|
+
export { x as OffscreenCanvasBackend, T as createOffscreenBackend };
|
|
354
|
+
|
|
355
|
+
//# sourceMappingURL=offscreen.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"offscreen.mjs","names":[],"sources":["../src/rendering/offscreen/serializeTransform.ts","../src/rendering/offscreen/OffscreenCanvasBackend.ts","../src/rendering/offscreen/index.ts"],"sourcesContent":["/**\n * Flatten a {@link Style}'s coordinate warp into a {@link SerializableTransform}\n * the worker can use without the (non-serialisable) style closures.\n *\n * All current built-in styles are either flat (`worldToScene` absent → identity)\n * or affine (Isometric). We reconstruct the affine matrix by sampling the\n * closure at three basis points — exact for affine maps. A future non-affine\n * style would be mis-sampled here; callers should fall back to main-thread\n * culling for such styles (see OffscreenCanvasBackend).\n */\n\nimport type {Style} from \"../../style/Style\";\nimport type {AffineMatrix, SerializableTransform} from \"./protocol\";\n\ntype TransformFn = (x: number, y: number) => {x: number; y: number};\n\n/** Recover the affine matrix `[a,b,c,d,e,f]` of a (presumed affine) map. */\nfunction sampleAffine(fn: TransformFn): AffineMatrix {\n const o = fn(0, 0);\n const x = fn(1, 0);\n const y = fn(0, 1);\n return [x.x - o.x, x.y - o.y, y.x - o.x, y.y - o.y, o.x, o.y];\n}\n\n/** Build the flattened transform for a style. Identity styles short-circuit. */\nexport function serializeTransform(style: Style): SerializableTransform {\n if (!style.worldToScene || !style.sceneToWorld) {\n return {kind: \"identity\"};\n }\n return {\n kind: \"affine\",\n forward: sampleAffine((x, y) => style.worldToScene!(x, y)),\n inverse: sampleAffine((x, y) => style.sceneToWorld!(x, y)),\n };\n}\n\nfunction applyMatrix(m: AffineMatrix, x: number, y: number): {x: number; y: number} {\n return {x: m[0] * x + m[2] * y + m[4], y: m[1] * x + m[3] * y + m[5]};\n}\n\n/**\n * World → scene projector for the worker's cull-bounds test. Returns\n * `undefined` for the identity transform so callers can skip projection.\n */\nexport function forwardFn(transform: SerializableTransform): TransformFn | undefined {\n if (transform.kind === \"identity\") return undefined;\n const m = transform.forward;\n return (x, y) => applyMatrix(m, x, y);\n}\n\n/**\n * Scene → world projector for the worker's grid Cartesian-bounds computation.\n * Returns `undefined` for the identity transform (grid layout treats absent\n * inverse as identity).\n */\nexport function inverseFn(transform: SerializableTransform): TransformFn | undefined {\n if (transform.kind === \"identity\") return undefined;\n const m = transform.inverse;\n return (x, y) => applyMatrix(m, x, y);\n}\n","/**\n * OffscreenCanvasBackend — opt-in {@link InteractiveBackend} that renders in a\n * Web Worker via `OffscreenCanvas`.\n *\n * The main thread keeps everything that must stay synchronous or that touches\n * the DOM: the {@link Camera}, {@link CullingManager}, {@link HitTester},\n * interaction handling, and the (text-measuring, therefore DOM-bound) scene\n * build. It styles the resulting shapes, then ships plain {@link Shape} lists to\n * the worker. The worker owns the per-frame hot path (cull → draw commands →\n * rasterise). See {@link renderFrame} and {@link worker}.\n *\n * Differences from {@link KonvaRenderBackend}:\n * - `exportCanvas` returns `undefined` (the live canvas lives in the worker and\n * isn't readable here) — use the headless `CanvasExporter`/`PngBytesExporter`,\n * which rebuild from state. An async {@link captureViewport} is offered for\n * callers that genuinely need a live snapshot.\n * - Image shapes (image labels, ambient-light vignette) ARE supported: the\n * worker decodes their `src` via `createImageBitmap` (see worker.ts).\n * - Live effects ARE supported, but run on a main-thread Konva overlay stage\n * composited above the worker canvas (Konva animations can't run in a\n * worker); the map itself still rasterises off-thread.\n */\n\nimport Konva from \"konva\";\nimport type {InteractiveBackend} from \"../MapRenderer\";\nimport type {MapState} from \"../../MapState\";\nimport type {RendererEventMap} from \"../../types/Settings\";\nimport {Camera} from \"../../camera/Camera\";\nimport {CullingManager} from \"../../CullingManager\";\nimport {HitTester, type LayerOffset} from \"../../hit/HitTester\";\nimport {TypedEventEmitter} from \"../../TypedEventEmitter\";\nimport {InteractionHandler} from \"../../InteractionHandler\";\nimport {ScenePipeline, type SceneBuildResult, type DrawnExitEntry, type DrawnSpecialExitEntry, type DrawnStubEntry} from \"../../ScenePipeline\";\nimport {buildBuiltInOverlayShapes} from \"../../export/flushSceneShapes\";\nimport {layoutRoom} from \"../../scene/elements/RoomLayout\";\nimport {layoutInnerExits} from \"../../scene/elements/ExitLayout\";\nimport {computeSpecialExits} from \"../../scene/SpecialExitStyle\";\nimport {specialExitToShape} from \"../../scene/elements/SpecialExitLayout\";\nimport {computeStubs} from \"../../scene/StubStyle\";\nimport {stubToShape} from \"../../scene/elements/StubLayout\";\nimport type {Shape, GroupShape} from \"../../scene/Shape\";\nimport type {Style, StyleContext} from \"../../style/Style\";\nimport {identityStyle} from \"../../style/Style\";\nimport {applyStyleToShapes} from \"../../style/applyStyle\";\nimport type {CoordFn} from \"../../coord/CoordFn\";\nimport {IDENTITY_TRANSFORM} from \"../../coord/CoordFn\";\nimport type {SceneOverlay, SceneOverlayContext} from \"../../overlay/SceneOverlay\";\nimport type {LiveEffect} from \"../../overlay/LiveEffect\";\nimport type {ExportCanvas} from \"../../export/Exporter\";\nimport {serializeTransform} from \"./serializeTransform\";\nimport type {Bounds, LayerPayload, MainToWorkerMessage, WorkerToMainMessage} from \"./protocol\";\n\nconst currentRoomColor = \"rgb(120, 72, 0)\";\n\n/** Transport the backend talks to. A real `Worker` satisfies this directly. */\nexport interface WorkerTransport {\n postMessage(message: MainToWorkerMessage, transfer?: Transferable[]): void;\n addEventListener?(type: \"message\", handler: (ev: {data: WorkerToMainMessage}) => void): void;\n onmessage?: ((ev: {data: WorkerToMainMessage}) => void) | null;\n terminate?(): void;\n}\n\nexport interface OffscreenBackendOptions {\n /** Pre-created transport (used by the inline-worker factory and by tests). */\n transport?: WorkerTransport;\n /** Lazily create the transport. Ignored when {@link transport} is provided. */\n createTransport?: () => WorkerTransport;\n /** Override the device pixel ratio (tests / forcing 1x). */\n devicePixelRatio?: number;\n}\n\nfunction supportsOffscreen(): boolean {\n return (\n typeof document !== \"undefined\" &&\n typeof HTMLCanvasElement !== \"undefined\" &&\n typeof HTMLCanvasElement.prototype.transferControlToOffscreen === \"function\"\n );\n}\n\nexport class OffscreenCanvasBackend implements InteractiveBackend {\n readonly camera: Camera;\n readonly culling: CullingManager;\n readonly events: TypedEventEmitter<RendererEventMap>;\n readonly hitTester: HitTester = new HitTester();\n\n private readonly state: MapState;\n private readonly container?: HTMLDivElement;\n private readonly transport?: WorkerTransport;\n private readonly pipeline: ScenePipeline;\n private readonly dpr: number;\n\n private currentStyle: Style = identityStyle;\n private _coordinateTransform: CoordFn = IDENTITY_TRANSFORM;\n private coordinateInverse: CoordFn = IDENTITY_TRANSFORM;\n private coordinateLayerOffset: LayerOffset | undefined;\n\n private lastResult?: SceneBuildResult;\n private lastHitShapes: Shape[] = [];\n private boundsByShape: Map<Shape, Bounds> = new Map();\n\n private interactionHandler?: InteractionHandler;\n private cameraChangeHandler?: () => void;\n private readonly sceneOverlays: Map<string, SceneOverlay> = new Map();\n private readonly viewportSubscribers: Set<() => void> = new Set();\n private readonly liveEffects: Map<string, LiveEffect> = new Map();\n // Live effects can't run in the worker (Konva animations), so they render on\n // a main-thread Konva stage composited above the worker's canvas. The map\n // still rasterises off-thread; only the (light) effect animation is local.\n private liveEffectStage?: Konva.Stage;\n private liveEffectLayer?: Konva.Layer;\n private liveEffectHost?: HTMLDivElement;\n private readonly pendingExports: Map<number, (bitmap: ImageBitmap) => void> = new Map();\n private exportSeq = 0;\n private destroyed = false;\n\n constructor(state: MapState, container?: HTMLDivElement, options: OffscreenBackendOptions = {}) {\n this.state = state;\n this.container = container;\n this.pipeline = new ScenePipeline(state.mapReader, state.settings);\n this.dpr = options.devicePixelRatio ?? (typeof devicePixelRatio !== \"undefined\" ? devicePixelRatio : 1);\n this.transport = options.transport ?? options.createTransport?.();\n\n const width = container?.clientWidth ?? 1;\n const height = container?.clientHeight ?? 1;\n this.camera = new Camera(width, height);\n this.events = new TypedEventEmitter<RendererEventMap>(container);\n this.culling = new CullingManager(state.settings, () => {\n // Culling runs in the worker per camera frame; a mode change just\n // needs the fresh settings + a redraw.\n this.postSettings();\n this.postCamera();\n });\n\n if (this.transport) {\n const onMessage = (ev: {data: WorkerToMainMessage}) => this.handleWorkerMessage(ev.data);\n if (this.transport.addEventListener) this.transport.addEventListener(\"message\", onMessage);\n else this.transport.onmessage = onMessage;\n }\n\n if (container && this.transport && supportsOffscreen()) {\n // The map canvas and any live-effect overlay stack absolutely inside\n // the container, so it must establish a positioning context.\n if (getComputedStyle(container).position === \"static\") {\n container.style.position = \"relative\";\n }\n const canvas = document.createElement(\"canvas\");\n canvas.style.position = \"absolute\";\n canvas.style.inset = \"0\";\n canvas.style.width = \"100%\";\n canvas.style.height = \"100%\";\n canvas.style.display = \"block\";\n container.appendChild(canvas);\n container.style.backgroundColor = state.settings.backgroundColor;\n const offscreen = canvas.transferControlToOffscreen();\n this.transport.postMessage(\n {type: \"init\", canvas: offscreen, width, height, dpr: this.dpr, settings: state.settings},\n [offscreen],\n );\n\n this.interactionHandler = new InteractionHandler(container, this.camera, state, {\n clientToMapPoint: (cx, cy) => this.camera.clientToMapPoint(cx, cy, container.getBoundingClientRect()),\n pickAtPoint: (x, y) => this.hitTester.pick(x, y),\n }, this.events);\n }\n\n this.cameraChangeHandler = () => this.onCameraChange();\n this.camera.on(\"change\", this.cameraChangeHandler);\n\n this.applyStyleTransforms();\n this.subscribeToState(state);\n\n // Seed the worker with the current camera so it paints the background\n // before the first scene arrives.\n this.postCamera();\n }\n\n get coordinateTransform(): CoordFn {\n return this._coordinateTransform;\n }\n\n // --- Messaging ---\n\n private post(message: MainToWorkerMessage, transfer?: Transferable[]): void {\n if (this.destroyed || !this.transport) return;\n this.transport.postMessage(message, transfer);\n }\n\n private postSettings(): void {\n this.post({type: \"settings\", settings: this.state.settings});\n }\n\n private postCamera(): void {\n this.post({\n type: \"camera\",\n camera: {scale: this.camera.getScale(), offsetX: this.camera.position.x, offsetY: this.camera.position.y},\n viewport: this.camera.getViewportBounds(),\n width: this.camera.width,\n height: this.camera.height,\n });\n }\n\n private postScene(): void {\n const result = this.lastResult;\n if (!result) {\n this.post({\n type: \"scene\",\n link: {shapes: [], bounds: []},\n room: {shapes: [], bounds: []},\n topLabel: [],\n transform: serializeTransform(this.currentStyle),\n });\n return;\n }\n const ctx = this.styleContext();\n this.post({\n type: \"scene\",\n link: this.styleLayer(result.sceneShapes.link, ctx),\n room: this.styleLayer(result.sceneShapes.room, ctx),\n topLabel: this.styleShapes(result.sceneShapes.topLabel, ctx),\n transform: serializeTransform(this.currentStyle),\n });\n }\n\n private postOverlays(): void {\n const ctx = this.styleContext();\n const out: Shape[] = [];\n for (const s of this.buildCurrentRoomOverlayShapes()) out.push(...this.styleOne(s, ctx, false));\n for (const s of buildBuiltInOverlayShapes(this.state)) out.push(...this.styleOne(s, ctx, false));\n for (const overlay of this.sceneOverlays.values()) {\n const rendered = overlay.render(this.state, this.camera.getViewportBounds());\n if (!rendered) continue;\n const shapes = Array.isArray(rendered) ? rendered : [rendered];\n for (const s of shapes) out.push(...this.styleOne(s, ctx, overlay.sceneSpace ?? false));\n }\n this.post({type: \"overlays\", shapes: out});\n }\n\n // --- Styling helpers ---\n\n private styleContext(): StyleContext {\n return {scale: this.camera.getScale(), roomSize: this.state.settings.roomSize};\n }\n\n private styleOne(shape: Shape, ctx: StyleContext, bypass: boolean): Shape[] {\n if (bypass || this.currentStyle === identityStyle) return [shape];\n return applyStyleToShapes([shape], this.currentStyle, ctx);\n }\n\n private styleShapes(shapes: Shape[], ctx: StyleContext): Shape[] {\n if (this.currentStyle === identityStyle) return shapes;\n return applyStyleToShapes(shapes, this.currentStyle, ctx);\n }\n\n /** Style a scene layer, carrying each shape's world-space cull bounds through. */\n private styleLayer(shapes: Shape[], ctx: StyleContext): LayerPayload {\n const outShapes: Shape[] = [];\n const outBounds: Array<Bounds | null> = [];\n for (const shape of shapes) {\n const b = this.boundsByShape.get(shape) ?? null;\n const styled = this.currentStyle === identityStyle ? [shape] : applyStyleToShapes([shape], this.currentStyle, ctx);\n for (const s of styled) {\n outShapes.push(s);\n outBounds.push(b);\n }\n }\n return {shapes: outShapes, bounds: outBounds};\n }\n\n // --- Scene build ---\n\n refresh(): void {\n const {currentAreaInstance, currentZIndex, positionRoomId} = this.state;\n if (!currentAreaInstance || currentZIndex === undefined) return;\n const plane = currentAreaInstance.getPlane(currentZIndex);\n if (!plane) {\n this.lastResult = undefined;\n this.lastHitShapes = [];\n this.boundsByShape.clear();\n this.hitTester.clear();\n this.postSettings();\n this.postScene();\n this.postOverlays();\n this.postCamera();\n return;\n }\n\n const result = this.pipeline.buildScene(currentAreaInstance, plane, currentZIndex, this.state.lens);\n this.lastResult = result;\n this.lastHitShapes = result.hitShapes;\n this.boundsByShape = this.buildBoundsMap(result);\n this.hitTester.build(this.lastHitShapes, this.state.settings.roomSize, this._coordinateTransform, this.coordinateLayerOffset);\n\n if (this.container) this.container.style.backgroundColor = this.state.settings.backgroundColor;\n\n this.postSettings();\n this.postScene();\n this.postOverlays();\n this.postCamera();\n\n // Position marker handled inside postOverlays; nothing else needed.\n void positionRoomId;\n }\n\n private buildBoundsMap(result: SceneBuildResult): Map<Shape, Bounds> {\n const map = new Map<Shape, Bounds>();\n const half = this.state.settings.roomSize / 2;\n for (const {room, shape} of result.roomShapeRefs.values()) {\n map.set(shape, {x: room.x - half, y: room.y - half, width: this.state.settings.roomSize, height: this.state.settings.roomSize});\n }\n for (const {shape, bounds} of result.standaloneExitShapeRefs) map.set(shape, bounds);\n for (const {shape, bounds} of result.labelShapeRefs) map.set(shape, bounds);\n for (const {shape, bounds} of result.specialExitShapeRefs) map.set(shape, bounds);\n for (const {shape, bounds} of result.stubShapeRefs) map.set(shape, bounds);\n for (const {shape, bounds} of result.areaExitLabelShapeRefs) map.set(shape, bounds);\n return map;\n }\n\n /** Port of {@link KonvaRenderBackend.updateCurrentRoomOverlay} returning shapes. */\n private buildCurrentRoomOverlayShapes(): Shape[] {\n const state = this.state;\n const roomId = state.positionRoomId;\n if (roomId === undefined) return [];\n const room = state.mapReader.getRoom(roomId);\n if (!room) return [];\n if (room.area !== state.currentArea || room.z !== state.currentZIndex) return [];\n const settings = state.settings;\n if (!settings.highlightCurrentRoom) return [];\n\n const out: Shape[] = [];\n const exitRenderer = this.pipeline.exitRenderer;\n const lens = state.lens;\n const roomsToRedraw = new Map<number, MapData.Room>();\n roomsToRedraw.set(room.id, room);\n\n if (state.currentAreaInstance && state.currentZIndex !== undefined) {\n const exits = state.currentAreaInstance\n .getLinkExits(state.currentZIndex)\n .filter(exit => exit.a === room.id || exit.b === room.id);\n for (const exit of exits) {\n const data = exitRenderer.renderDataWithColor(exit, currentRoomColor, state.currentZIndex);\n if (data) out.push(this.pipeline.buildExitShape(data));\n }\n }\n for (const se of computeSpecialExits(room, settings, currentRoomColor)) out.push(specialExitToShape(se, room.id));\n for (const stub of computeStubs(room, settings, currentRoomColor)) out.push(stubToShape(stub));\n\n [...Object.values(room.exits), ...Object.values(room.specialExits)].forEach(id => {\n const other = state.mapReader.getRoom(id as number);\n if (other && other.area === state.currentArea && other.z === state.currentZIndex && lens.isVisible(other)) {\n roomsToRedraw.set(other.id, other);\n }\n });\n\n roomsToRedraw.forEach((roomToRedraw, id) => {\n const isCurrent = id === room.id;\n const overlayShape: GroupShape = layoutRoom(roomToRedraw, state.mapReader, settings, {\n strokeOverride: isCurrent ? currentRoomColor : settings.lineColor,\n flatPipeline: true,\n });\n overlayShape.children.push(...layoutInnerExits(roomToRedraw, state.mapReader, settings));\n out.push(overlayShape);\n });\n\n return out;\n }\n\n // --- Style transform (mirrors KonvaRenderBackend.applyStyleTransforms) ---\n\n setStyle(style: Style): void {\n this.currentStyle = style;\n this.lastHitShapes = [];\n this.hitTester.clear();\n this.applyStyleTransforms();\n this.refresh();\n }\n\n private applyStyleTransforms(): void {\n const style = this.currentStyle;\n const forward: CoordFn = style.worldToScene ? (x, y) => style.worldToScene!(x, y) : IDENTITY_TRANSFORM;\n const newInverse: CoordFn = style.sceneToWorld ? (x, y) => style.sceneToWorld!(x, y) : IDENTITY_TRANSFORM;\n const oldInverse = this.coordinateInverse;\n const layerOffset: LayerOffset | undefined = style.sceneLayerOffset ? (layer) => style.sceneLayerOffset!(layer) : undefined;\n\n this._coordinateTransform = forward;\n this.coordinateInverse = newInverse;\n this.coordinateLayerOffset = layerOffset;\n this.culling.setCoordinateTransform(forward);\n if (this.lastHitShapes.length > 0) {\n this.hitTester.build(this.lastHitShapes, this.state.settings.roomSize, forward, layerOffset);\n }\n\n // Keep the same map point centred when the projection changes.\n const scale = this.camera.getScale();\n const screenCX = this.camera.width / 2;\n const screenCY = this.camera.height / 2;\n const oldRX = (screenCX - this.camera.position.x) / scale;\n const oldRY = (screenCY - this.camera.position.y) / scale;\n const map = oldInverse(oldRX, oldRY);\n const nr = forward(map.x, map.y);\n this.camera.position = {x: screenCX - nr.x * scale, y: screenCY - nr.y * scale};\n // position assignment does not notify; the upcoming refresh posts camera.\n }\n\n // --- Camera ---\n\n private onCameraChange(): void {\n this.postCamera();\n const vpBounds = this.camera.getViewportBounds();\n this.events.emit(\"pan\", vpBounds);\n for (const cb of this.viewportSubscribers) cb();\n if (this.liveEffectStage) {\n this.syncEffectStage();\n const scale = this.camera.getScale();\n for (const effect of this.liveEffects.values()) {\n effect.updateViewport(vpBounds, scale, this._coordinateTransform);\n }\n }\n }\n\n // --- State subscriptions ---\n\n private subscribeToState(state: MapState): void {\n state.events.on(\"area\", () => this.refresh());\n state.events.on(\"position\", ({roomId, center, areaChanged}) => this.onPositionChanged(roomId, center, areaChanged));\n state.events.on(\"center\", ({roomId, instant}) => {\n const room = state.mapReader.getRoom(roomId);\n if (room) {\n const p = this._coordinateTransform(room.x, room.y);\n this.camera.panToMapPointAnimated(p.x, p.y, instant || this.state.settings.instantMapMove);\n }\n });\n state.events.on(\"highlight\", () => this.postOverlays());\n state.events.on(\"path\", () => this.postOverlays());\n state.events.on(\"clear\", () => this.postOverlays());\n state.events.on(\"lens\", () => this.refresh());\n }\n\n private onPositionChanged(roomId: number | undefined, center: boolean, instant: boolean): void {\n if (roomId !== undefined && center) {\n const room = this.state.mapReader.getRoom(roomId);\n if (room) {\n const p = this._coordinateTransform(room.x, room.y);\n this.camera.panToMapPointAnimated(p.x, p.y, instant || this.state.settings.instantMapMove);\n }\n }\n this.postOverlays();\n }\n\n // --- Background ---\n\n updateBackground(): void {\n if (this.container) this.container.style.backgroundColor = this.state.settings.backgroundColor;\n this.postSettings();\n this.postCamera();\n }\n\n // --- Scene overlays ---\n\n addSceneOverlay(id: string, overlay: SceneOverlay): void {\n const existing = this.sceneOverlays.get(id);\n if (existing) existing.detach?.();\n this.sceneOverlays.set(id, overlay);\n overlay.attach?.(this.createOverlayContext(id, overlay));\n this.postOverlays();\n }\n\n removeSceneOverlay(id: string): void {\n const overlay = this.sceneOverlays.get(id);\n if (!overlay) return;\n overlay.detach?.();\n this.sceneOverlays.delete(id);\n this.postOverlays();\n }\n\n getSceneOverlays(): Iterable<SceneOverlay> {\n return this.sceneOverlays.values();\n }\n\n private createOverlayContext(id: string, overlay: SceneOverlay): SceneOverlayContext {\n return {\n state: this.state,\n onViewportChange: (cb) => {\n this.viewportSubscribers.add(cb);\n return () => this.viewportSubscribers.delete(cb);\n },\n invalidate: () => {\n if (this.sceneOverlays.get(id) !== overlay) return;\n this.postOverlays();\n },\n };\n }\n\n // --- Live effects (main-thread Konva overlay above the worker canvas) ---\n\n private ensureEffectStage(): void {\n if (this.liveEffectStage || !this.container) return;\n const host = document.createElement(\"div\");\n host.style.position = \"absolute\";\n host.style.inset = \"0\";\n host.style.pointerEvents = \"none\"; // never intercept interaction\n this.container.appendChild(host);\n const stage = new Konva.Stage({\n container: host,\n width: this.camera.width,\n height: this.camera.height,\n listening: false,\n });\n const layer = new Konva.Layer({listening: false});\n stage.add(layer);\n this.liveEffectHost = host;\n this.liveEffectStage = stage;\n this.liveEffectLayer = layer;\n this.syncEffectStage();\n }\n\n /** Mirror the camera transform onto the effect stage so effects align with the map. */\n private syncEffectStage(): void {\n const stage = this.liveEffectStage;\n if (!stage) return;\n if (stage.width() !== this.camera.width || stage.height() !== this.camera.height) {\n stage.width(this.camera.width);\n stage.height(this.camera.height);\n }\n const scale = this.camera.getScale();\n stage.scale({x: scale, y: scale});\n stage.position(this.camera.position);\n stage.batchDraw();\n }\n\n addLiveEffect(id: string, effect: LiveEffect): void {\n this.removeLiveEffect(id);\n this.ensureEffectStage();\n if (!this.liveEffectLayer) return; // headless / no container\n effect.attach(this.liveEffectLayer);\n this.liveEffects.set(id, effect);\n this.syncEffectStage();\n effect.updateViewport(this.camera.getViewportBounds(), this.camera.getScale(), this._coordinateTransform);\n }\n\n removeLiveEffect(id: string): void {\n const effect = this.liveEffects.get(id);\n if (!effect) return;\n effect.destroy();\n this.liveEffects.delete(id);\n this.liveEffectLayer?.batchDraw();\n }\n\n // --- Drawn-geometry snapshots (synchronous, from the last main-thread build) ---\n\n getDrawnExits(): readonly DrawnExitEntry[] {\n return this.lastResult?.drawnExits ?? [];\n }\n\n getDrawnSpecialExits(): readonly DrawnSpecialExitEntry[] {\n return this.lastResult?.drawnSpecialExits ?? [];\n }\n\n getDrawnStubs(): readonly DrawnStubEntry[] {\n return this.lastResult?.drawnStubs ?? [];\n }\n\n // --- Export ---\n\n /**\n * Not supported with the worker backend — the live canvas is owned by the\n * worker and cannot be read synchronously here. Use the headless\n * `CanvasExporter` / `PngBytesExporter` (they rebuild from state), or the\n * async {@link captureViewport} for a live snapshot.\n */\n exportCanvas(): ExportCanvas | undefined {\n return undefined;\n }\n\n /** Async live-viewport snapshot via a worker round-trip. */\n captureViewport(options?: {pixelRatio?: number}): Promise<ImageBitmap> {\n if (!this.transport) return Promise.reject(new Error(\"No worker transport\"));\n const requestId = ++this.exportSeq;\n return new Promise<ImageBitmap>((resolve) => {\n this.pendingExports.set(requestId, resolve);\n this.post({type: \"export\", requestId, pixelRatio: options?.pixelRatio ?? this.dpr});\n });\n }\n\n private handleWorkerMessage(message: WorkerToMainMessage): void {\n if (message.type === \"export\") {\n const resolve = this.pendingExports.get(message.requestId);\n if (resolve) {\n this.pendingExports.delete(message.requestId);\n resolve(message.bitmap);\n }\n }\n // 'ready' is informational; the worker buffers state until init.\n }\n\n // --- Teardown ---\n\n destroy(): void {\n if (this.destroyed) return;\n this.destroyed = true;\n\n this.state.events.removeAllListeners();\n this.interactionHandler?.destroy();\n\n for (const effect of this.liveEffects.values()) effect.destroy();\n this.liveEffects.clear();\n this.liveEffectStage?.destroy();\n this.liveEffectStage = undefined;\n this.liveEffectLayer = undefined;\n this.liveEffectHost?.remove();\n this.liveEffectHost = undefined;\n\n for (const overlay of this.sceneOverlays.values()) overlay.detach?.();\n this.sceneOverlays.clear();\n this.viewportSubscribers.clear();\n this.pendingExports.clear();\n\n this.camera.cancelAnimation();\n if (this.cameraChangeHandler) {\n this.camera.off(\"change\", this.cameraChangeHandler);\n this.cameraChangeHandler = undefined;\n }\n\n this.post({type: \"destroy\"});\n this.transport?.terminate?.();\n\n this.events.removeAllListeners();\n }\n}\n","/**\n * Opt-in OffscreenCanvas backend entry point.\n *\n * Renders the map in a Web Worker via `OffscreenCanvas`, moving the per-frame\n * rasterisation off the main thread. The default Konva backend is untouched —\n * you only get this when you wire it in explicitly:\n *\n * ```ts\n * import { MapRenderer } from \"mudlet-map-renderer\";\n * import { createOffscreenBackend } from \"mudlet-map-renderer/offscreen\";\n *\n * const renderer = new MapRenderer(reader, settings, container, createOffscreenBackend(container));\n * ```\n *\n * Note the `container` is passed twice: `MapRenderer`'s `backendFactory` hook\n * receives only the `MapState`, so the container is closed over by the factory.\n *\n * Supports image labels, the ambient-light overlay, and live effects (the last\n * via a main-thread Konva overlay stage; the map still rasterises off-thread).\n * The one limitation: `renderer.exportCanvas()` returns `undefined` (the live\n * canvas lives in the worker) — use the headless `CanvasExporter` /\n * `PngBytesExporter` exporters, or `backend.captureViewport()`.\n */\n\nimport type {MapState} from \"../../MapState\";\nimport type {InteractiveBackend} from \"../MapRenderer\";\nimport {OffscreenCanvasBackend, type OffscreenBackendOptions, type WorkerTransport} from \"./OffscreenCanvasBackend\";\nimport OffscreenWorker from \"./worker?worker&inline\";\n\nexport {OffscreenCanvasBackend} from \"./OffscreenCanvasBackend\";\nexport type {OffscreenBackendOptions, WorkerTransport} from \"./OffscreenCanvasBackend\";\nexport type {MainToWorkerMessage, WorkerToMainMessage, SerializableTransform} from \"./protocol\";\n\n/**\n * Build a backend factory for {@link MapRenderer}'s `backendFactory` parameter.\n * Pass the same `container` you give `MapRenderer`.\n */\nexport function createOffscreenBackend(\n container?: HTMLDivElement,\n options: OffscreenBackendOptions = {},\n): (state: MapState) => InteractiveBackend {\n return (state: MapState) =>\n new OffscreenCanvasBackend(state, container, {\n ...options,\n createTransport: options.createTransport ?? (() => new OffscreenWorker() as unknown as WorkerTransport),\n });\n}\n"],"mappings":";;;AAiBA,SAAS,EAAa,GAA+B;CACjD,IAAM,IAAI,EAAG,GAAG,EAAE,EACZ,IAAI,EAAG,GAAG,EAAE,EACZ,IAAI,EAAG,GAAG,EAAE;AAClB,QAAO;EAAC,EAAE,IAAI,EAAE;EAAG,EAAE,IAAI,EAAE;EAAG,EAAE,IAAI,EAAE;EAAG,EAAE,IAAI,EAAE;EAAG,EAAE;EAAG,EAAE;EAAE;;AAIjE,SAAgB,EAAmB,GAAqC;AAIpE,QAHI,CAAC,EAAM,gBAAgB,CAAC,EAAM,eACvB,EAAC,MAAM,YAAW,GAEtB;EACH,MAAM;EACN,SAAS,GAAc,GAAG,MAAM,EAAM,aAAc,GAAG,EAAE,CAAC;EAC1D,SAAS,GAAc,GAAG,MAAM,EAAM,aAAc,GAAG,EAAE,CAAC;EAC7D;;;;ACmBL,IAAM,IAAmB;AAmBzB,SAAS,IAA6B;AAClC,QACI,OAAO,WAAa,OACpB,OAAO,oBAAsB,OAC7B,OAAO,kBAAkB,UAAU,8BAA+B;;AAI1E,IAAa,IAAb,MAAkE;CAoC9D,YAAY,GAAiB,GAA4B,IAAmC,EAAE,EAAE;AAK5F,mBArC4B,IAAI,GAAW,sBAQjB,+BACU,4BACH,wBAIJ,EAAE,uCACS,IAAI,KAAK,uCAIO,IAAI,KAAK,6CACb,IAAI,KAAK,qCACT,IAAI,KAAK,wCAOa,IAAI,KAAK,mBACnE,oBACA,IAGhB,KAAK,QAAQ,GACb,KAAK,YAAY,GACjB,KAAK,WAAW,IAAI,EAAc,EAAM,WAAW,EAAM,SAAS,EAClE,KAAK,MAAM,EAAQ,qBAAqB,OAAO,mBAAqB,MAAc,mBAAmB,IACrG,KAAK,YAAY,EAAQ,aAAa,EAAQ,mBAAmB;EAEjE,IAAM,IAAQ,GAAW,eAAe,GAClC,IAAS,GAAW,gBAAgB;AAU1C,MATA,KAAK,SAAS,IAAI,EAAO,GAAO,EAAO,EACvC,KAAK,SAAS,IAAI,EAAoC,EAAU,EAChE,KAAK,UAAU,IAAI,EAAe,EAAM,gBAAgB;AAIpD,GADA,KAAK,cAAc,EACnB,KAAK,YAAY;IACnB,EAEE,KAAK,WAAW;GAChB,IAAM,KAAa,MAAoC,KAAK,oBAAoB,EAAG,KAAK;AACxF,GAAI,KAAK,UAAU,mBAAkB,KAAK,UAAU,iBAAiB,WAAW,EAAU,GACrF,KAAK,UAAU,YAAY;;AAGpC,MAAI,KAAa,KAAK,aAAa,GAAmB,EAAE;AAGpD,GAAI,iBAAiB,EAAU,CAAC,aAAa,aACzC,EAAU,MAAM,WAAW;GAE/B,IAAM,IAAS,SAAS,cAAc,SAAS;AAO/C,GANA,EAAO,MAAM,WAAW,YACxB,EAAO,MAAM,QAAQ,KACrB,EAAO,MAAM,QAAQ,QACrB,EAAO,MAAM,SAAS,QACtB,EAAO,MAAM,UAAU,SACvB,EAAU,YAAY,EAAO,EAC7B,EAAU,MAAM,kBAAkB,EAAM,SAAS;GACjD,IAAM,IAAY,EAAO,4BAA4B;AAMrD,GALA,KAAK,UAAU,YACX;IAAC,MAAM;IAAQ,QAAQ;IAAW;IAAO;IAAQ,KAAK,KAAK;IAAK,UAAU,EAAM;IAAS,EACzF,CAAC,EAAU,CACd,EAED,KAAK,qBAAqB,IAAI,EAAmB,GAAW,KAAK,QAAQ,GAAO;IAC5E,mBAAmB,GAAI,MAAO,KAAK,OAAO,iBAAiB,GAAI,GAAI,EAAU,uBAAuB,CAAC;IACrG,cAAc,GAAG,MAAM,KAAK,UAAU,KAAK,GAAG,EAAE;IACnD,EAAE,KAAK,OAAO;;AAWnB,EARA,KAAK,4BAA4B,KAAK,gBAAgB,EACtD,KAAK,OAAO,GAAG,UAAU,KAAK,oBAAoB,EAElD,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,EAAM,EAI5B,KAAK,YAAY;;CAGrB,IAAI,sBAA+B;AAC/B,SAAO,KAAK;;CAKhB,KAAa,GAA8B,GAAiC;AACpE,OAAK,aAAa,CAAC,KAAK,aAC5B,KAAK,UAAU,YAAY,GAAS,EAAS;;CAGjD,eAA6B;AACzB,OAAK,KAAK;GAAC,MAAM;GAAY,UAAU,KAAK,MAAM;GAAS,CAAC;;CAGhE,aAA2B;AACvB,OAAK,KAAK;GACN,MAAM;GACN,QAAQ;IAAC,OAAO,KAAK,OAAO,UAAU;IAAE,SAAS,KAAK,OAAO,SAAS;IAAG,SAAS,KAAK,OAAO,SAAS;IAAE;GACzG,UAAU,KAAK,OAAO,mBAAmB;GACzC,OAAO,KAAK,OAAO;GACnB,QAAQ,KAAK,OAAO;GACvB,CAAC;;CAGN,YAA0B;EACtB,IAAM,IAAS,KAAK;AACpB,MAAI,CAAC,GAAQ;AACT,QAAK,KAAK;IACN,MAAM;IACN,MAAM;KAAC,QAAQ,EAAE;KAAE,QAAQ,EAAE;KAAC;IAC9B,MAAM;KAAC,QAAQ,EAAE;KAAE,QAAQ,EAAE;KAAC;IAC9B,UAAU,EAAE;IACZ,WAAW,EAAmB,KAAK,aAAa;IACnD,CAAC;AACF;;EAEJ,IAAM,IAAM,KAAK,cAAc;AAC/B,OAAK,KAAK;GACN,MAAM;GACN,MAAM,KAAK,WAAW,EAAO,YAAY,MAAM,EAAI;GACnD,MAAM,KAAK,WAAW,EAAO,YAAY,MAAM,EAAI;GACnD,UAAU,KAAK,YAAY,EAAO,YAAY,UAAU,EAAI;GAC5D,WAAW,EAAmB,KAAK,aAAa;GACnD,CAAC;;CAGN,eAA6B;EACzB,IAAM,IAAM,KAAK,cAAc,EACzB,IAAe,EAAE;AACvB,OAAK,IAAM,KAAK,KAAK,+BAA+B,CAAE,GAAI,KAAK,GAAG,KAAK,SAAS,GAAG,GAAK,GAAM,CAAC;AAC/F,OAAK,IAAM,KAAK,EAA0B,KAAK,MAAM,CAAE,GAAI,KAAK,GAAG,KAAK,SAAS,GAAG,GAAK,GAAM,CAAC;AAChG,OAAK,IAAM,KAAW,KAAK,cAAc,QAAQ,EAAE;GAC/C,IAAM,IAAW,EAAQ,OAAO,KAAK,OAAO,KAAK,OAAO,mBAAmB,CAAC;AAC5E,OAAI,CAAC,EAAU;GACf,IAAM,IAAS,MAAM,QAAQ,EAAS,GAAG,IAAW,CAAC,EAAS;AAC9D,QAAK,IAAM,KAAK,EAAQ,GAAI,KAAK,GAAG,KAAK,SAAS,GAAG,GAAK,EAAQ,cAAc,GAAM,CAAC;;AAE3F,OAAK,KAAK;GAAC,MAAM;GAAY,QAAQ;GAAI,CAAC;;CAK9C,eAAqC;AACjC,SAAO;GAAC,OAAO,KAAK,OAAO,UAAU;GAAE,UAAU,KAAK,MAAM,SAAS;GAAS;;CAGlF,SAAiB,GAAc,GAAmB,GAA0B;AAExE,SADI,KAAU,KAAK,iBAAiB,IAAsB,CAAC,EAAM,GAC1D,EAAmB,CAAC,EAAM,EAAE,KAAK,cAAc,EAAI;;CAG9D,YAAoB,GAAiB,GAA4B;AAE7D,SADI,KAAK,iBAAiB,IAAsB,IACzC,EAAmB,GAAQ,KAAK,cAAc,EAAI;;CAI7D,WAAmB,GAAiB,GAAiC;EACjE,IAAM,IAAqB,EAAE,EACvB,IAAkC,EAAE;AAC1C,OAAK,IAAM,KAAS,GAAQ;GACxB,IAAM,IAAI,KAAK,cAAc,IAAI,EAAM,IAAI,MACrC,IAAS,KAAK,iBAAiB,IAAgB,CAAC,EAAM,GAAG,EAAmB,CAAC,EAAM,EAAE,KAAK,cAAc,EAAI;AAClH,QAAK,IAAM,KAAK,EAEZ,CADA,EAAU,KAAK,EAAE,EACjB,EAAU,KAAK,EAAE;;AAGzB,SAAO;GAAC,QAAQ;GAAW,QAAQ;GAAU;;CAKjD,UAAgB;EACZ,IAAM,EAAC,wBAAqB,kBAAe,sBAAkB,KAAK;AAClE,MAAI,CAAC,KAAuB,MAAkB,KAAA,EAAW;EACzD,IAAM,IAAQ,EAAoB,SAAS,EAAc;AACzD,MAAI,CAAC,GAAO;AAQR,GAPA,KAAK,aAAa,KAAA,GAClB,KAAK,gBAAgB,EAAE,EACvB,KAAK,cAAc,OAAO,EAC1B,KAAK,UAAU,OAAO,EACtB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,YAAY;AACjB;;EAGJ,IAAM,IAAS,KAAK,SAAS,WAAW,GAAqB,GAAO,GAAe,KAAK,MAAM,KAAK;AAWnG,EAVA,KAAK,aAAa,GAClB,KAAK,gBAAgB,EAAO,WAC5B,KAAK,gBAAgB,KAAK,eAAe,EAAO,EAChD,KAAK,UAAU,MAAM,KAAK,eAAe,KAAK,MAAM,SAAS,UAAU,KAAK,sBAAsB,KAAK,sBAAsB,EAEzH,KAAK,cAAW,KAAK,UAAU,MAAM,kBAAkB,KAAK,MAAM,SAAS,kBAE/E,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,YAAY;;CAMrB,eAAuB,GAA8C;EACjE,IAAM,oBAAM,IAAI,KAAoB,EAC9B,IAAO,KAAK,MAAM,SAAS,WAAW;AAC5C,OAAK,IAAM,EAAC,SAAM,cAAU,EAAO,cAAc,QAAQ,CACrD,GAAI,IAAI,GAAO;GAAC,GAAG,EAAK,IAAI;GAAM,GAAG,EAAK,IAAI;GAAM,OAAO,KAAK,MAAM,SAAS;GAAU,QAAQ,KAAK,MAAM,SAAS;GAAS,CAAC;AAEnI,OAAK,IAAM,EAAC,UAAO,eAAW,EAAO,wBAAyB,GAAI,IAAI,GAAO,EAAO;AACpF,OAAK,IAAM,EAAC,UAAO,eAAW,EAAO,eAAgB,GAAI,IAAI,GAAO,EAAO;AAC3E,OAAK,IAAM,EAAC,UAAO,eAAW,EAAO,qBAAsB,GAAI,IAAI,GAAO,EAAO;AACjF,OAAK,IAAM,EAAC,UAAO,eAAW,EAAO,cAAe,GAAI,IAAI,GAAO,EAAO;AAC1E,OAAK,IAAM,EAAC,UAAO,eAAW,EAAO,uBAAwB,GAAI,IAAI,GAAO,EAAO;AACnF,SAAO;;CAIX,gCAAiD;EAC7C,IAAM,IAAQ,KAAK,OACb,IAAS,EAAM;AACrB,MAAI,MAAW,KAAA,EAAW,QAAO,EAAE;EACnC,IAAM,IAAO,EAAM,UAAU,QAAQ,EAAO;AAE5C,MADI,CAAC,KACD,EAAK,SAAS,EAAM,eAAe,EAAK,MAAM,EAAM,cAAe,QAAO,EAAE;EAChF,IAAM,IAAW,EAAM;AACvB,MAAI,CAAC,EAAS,qBAAsB,QAAO,EAAE;EAE7C,IAAM,IAAe,EAAE,EACjB,IAAe,KAAK,SAAS,cAC7B,IAAO,EAAM,MACb,oBAAgB,IAAI,KAA2B;AAGrD,MAFA,EAAc,IAAI,EAAK,IAAI,EAAK,EAE5B,EAAM,uBAAuB,EAAM,kBAAkB,KAAA,GAAW;GAChE,IAAM,IAAQ,EAAM,oBACf,aAAa,EAAM,cAAc,CACjC,QAAO,MAAQ,EAAK,MAAM,EAAK,MAAM,EAAK,MAAM,EAAK,GAAG;AAC7D,QAAK,IAAM,KAAQ,GAAO;IACtB,IAAM,IAAO,EAAa,oBAAoB,GAAM,GAAkB,EAAM,cAAc;AAC1F,IAAI,KAAM,EAAI,KAAK,KAAK,SAAS,eAAe,EAAK,CAAC;;;AAG9D,OAAK,IAAM,KAAM,EAAoB,GAAM,GAAU,EAAiB,CAAE,GAAI,KAAK,EAAmB,GAAI,EAAK,GAAG,CAAC;AACjH,OAAK,IAAM,KAAQ,EAAa,GAAM,GAAU,EAAiB,CAAE,GAAI,KAAK,EAAY,EAAK,CAAC;AAmB9F,SAjBA,CAAC,GAAG,OAAO,OAAO,EAAK,MAAM,EAAE,GAAG,OAAO,OAAO,EAAK,aAAa,CAAC,CAAC,SAAQ,MAAM;GAC9E,IAAM,IAAQ,EAAM,UAAU,QAAQ,EAAa;AACnD,GAAI,KAAS,EAAM,SAAS,EAAM,eAAe,EAAM,MAAM,EAAM,iBAAiB,EAAK,UAAU,EAAM,IACrG,EAAc,IAAI,EAAM,IAAI,EAAM;IAExC,EAEF,EAAc,SAAS,GAAc,MAAO;GACxC,IAAM,IAAY,MAAO,EAAK,IACxB,IAA2B,EAAW,GAAc,EAAM,WAAW,GAAU;IACjF,gBAAgB,IAAY,IAAmB,EAAS;IACxD,cAAc;IACjB,CAAC;AAEF,GADA,EAAa,SAAS,KAAK,GAAG,EAAiB,GAAc,EAAM,WAAW,EAAS,CAAC,EACxF,EAAI,KAAK,EAAa;IACxB,EAEK;;CAKX,SAAS,GAAoB;AAKzB,EAJA,KAAK,eAAe,GACpB,KAAK,gBAAgB,EAAE,EACvB,KAAK,UAAU,OAAO,EACtB,KAAK,sBAAsB,EAC3B,KAAK,SAAS;;CAGlB,uBAAqC;EACjC,IAAM,IAAQ,KAAK,cACb,IAAmB,EAAM,gBAAgB,GAAG,MAAM,EAAM,aAAc,GAAG,EAAE,GAAG,GAC9E,IAAsB,EAAM,gBAAgB,GAAG,MAAM,EAAM,aAAc,GAAG,EAAE,GAAG,GACjF,IAAa,KAAK,mBAClB,IAAuC,EAAM,oBAAoB,MAAU,EAAM,iBAAkB,EAAM,GAAG,KAAA;AAMlH,EAJA,KAAK,uBAAuB,GAC5B,KAAK,oBAAoB,GACzB,KAAK,wBAAwB,GAC7B,KAAK,QAAQ,uBAAuB,EAAQ,EACxC,KAAK,cAAc,SAAS,KAC5B,KAAK,UAAU,MAAM,KAAK,eAAe,KAAK,MAAM,SAAS,UAAU,GAAS,EAAY;EAIhG,IAAM,IAAQ,KAAK,OAAO,UAAU,EAC9B,IAAW,KAAK,OAAO,QAAQ,GAC/B,IAAW,KAAK,OAAO,SAAS,GAGhC,IAAM,GAFG,IAAW,KAAK,OAAO,SAAS,KAAK,IACrC,IAAW,KAAK,OAAO,SAAS,KAAK,EAChB,EAC9B,IAAK,EAAQ,EAAI,GAAG,EAAI,EAAE;AAChC,OAAK,OAAO,WAAW;GAAC,GAAG,IAAW,EAAG,IAAI;GAAO,GAAG,IAAW,EAAG,IAAI;GAAM;;CAMnF,iBAA+B;AAC3B,OAAK,YAAY;EACjB,IAAM,IAAW,KAAK,OAAO,mBAAmB;AAChD,OAAK,OAAO,KAAK,OAAO,EAAS;AACjC,OAAK,IAAM,KAAM,KAAK,oBAAqB,IAAI;AAC/C,MAAI,KAAK,iBAAiB;AACtB,QAAK,iBAAiB;GACtB,IAAM,IAAQ,KAAK,OAAO,UAAU;AACpC,QAAK,IAAM,KAAU,KAAK,YAAY,QAAQ,CAC1C,GAAO,eAAe,GAAU,GAAO,KAAK,qBAAqB;;;CAO7E,iBAAyB,GAAuB;AAa5C,EAZA,EAAM,OAAO,GAAG,cAAc,KAAK,SAAS,CAAC,EAC7C,EAAM,OAAO,GAAG,aAAa,EAAC,WAAQ,WAAQ,qBAAiB,KAAK,kBAAkB,GAAQ,GAAQ,EAAY,CAAC,EACnH,EAAM,OAAO,GAAG,WAAW,EAAC,WAAQ,iBAAa;GAC7C,IAAM,IAAO,EAAM,UAAU,QAAQ,EAAO;AAC5C,OAAI,GAAM;IACN,IAAM,IAAI,KAAK,qBAAqB,EAAK,GAAG,EAAK,EAAE;AACnD,SAAK,OAAO,sBAAsB,EAAE,GAAG,EAAE,GAAG,KAAW,KAAK,MAAM,SAAS,eAAe;;IAEhG,EACF,EAAM,OAAO,GAAG,mBAAmB,KAAK,cAAc,CAAC,EACvD,EAAM,OAAO,GAAG,cAAc,KAAK,cAAc,CAAC,EAClD,EAAM,OAAO,GAAG,eAAe,KAAK,cAAc,CAAC,EACnD,EAAM,OAAO,GAAG,cAAc,KAAK,SAAS,CAAC;;CAGjD,kBAA0B,GAA4B,GAAiB,GAAwB;AAC3F,MAAI,MAAW,KAAA,KAAa,GAAQ;GAChC,IAAM,IAAO,KAAK,MAAM,UAAU,QAAQ,EAAO;AACjD,OAAI,GAAM;IACN,IAAM,IAAI,KAAK,qBAAqB,EAAK,GAAG,EAAK,EAAE;AACnD,SAAK,OAAO,sBAAsB,EAAE,GAAG,EAAE,GAAG,KAAW,KAAK,MAAM,SAAS,eAAe;;;AAGlG,OAAK,cAAc;;CAKvB,mBAAyB;AAGrB,EAFI,KAAK,cAAW,KAAK,UAAU,MAAM,kBAAkB,KAAK,MAAM,SAAS,kBAC/E,KAAK,cAAc,EACnB,KAAK,YAAY;;CAKrB,gBAAgB,GAAY,GAA6B;EACrD,IAAM,IAAW,KAAK,cAAc,IAAI,EAAG;AAI3C,EAHI,KAAU,EAAS,UAAU,EACjC,KAAK,cAAc,IAAI,GAAI,EAAQ,EACnC,EAAQ,SAAS,KAAK,qBAAqB,GAAI,EAAQ,CAAC,EACxD,KAAK,cAAc;;CAGvB,mBAAmB,GAAkB;EACjC,IAAM,IAAU,KAAK,cAAc,IAAI,EAAG;AACrC,QACL,EAAQ,UAAU,EAClB,KAAK,cAAc,OAAO,EAAG,EAC7B,KAAK,cAAc;;CAGvB,mBAA2C;AACvC,SAAO,KAAK,cAAc,QAAQ;;CAGtC,qBAA6B,GAAY,GAA4C;AACjF,SAAO;GACH,OAAO,KAAK;GACZ,mBAAmB,OACf,KAAK,oBAAoB,IAAI,EAAG,QACnB,KAAK,oBAAoB,OAAO,EAAG;GAEpD,kBAAkB;AACV,SAAK,cAAc,IAAI,EAAG,KAAK,KACnC,KAAK,cAAc;;GAE1B;;CAKL,oBAAkC;AAC9B,MAAI,KAAK,mBAAmB,CAAC,KAAK,UAAW;EAC7C,IAAM,IAAO,SAAS,cAAc,MAAM;AAI1C,EAHA,EAAK,MAAM,WAAW,YACtB,EAAK,MAAM,QAAQ,KACnB,EAAK,MAAM,gBAAgB,QAC3B,KAAK,UAAU,YAAY,EAAK;EAChC,IAAM,IAAQ,IAAI,EAAM,MAAM;GAC1B,WAAW;GACX,OAAO,KAAK,OAAO;GACnB,QAAQ,KAAK,OAAO;GACpB,WAAW;GACd,CAAC,EACI,IAAQ,IAAI,EAAM,MAAM,EAAC,WAAW,IAAM,CAAC;AAKjD,EAJA,EAAM,IAAI,EAAM,EAChB,KAAK,iBAAiB,GACtB,KAAK,kBAAkB,GACvB,KAAK,kBAAkB,GACvB,KAAK,iBAAiB;;CAI1B,kBAAgC;EAC5B,IAAM,IAAQ,KAAK;AACnB,MAAI,CAAC,EAAO;AACZ,GAAI,EAAM,OAAO,KAAK,KAAK,OAAO,SAAS,EAAM,QAAQ,KAAK,KAAK,OAAO,YACtE,EAAM,MAAM,KAAK,OAAO,MAAM,EAC9B,EAAM,OAAO,KAAK,OAAO,OAAO;EAEpC,IAAM,IAAQ,KAAK,OAAO,UAAU;AAGpC,EAFA,EAAM,MAAM;GAAC,GAAG;GAAO,GAAG;GAAM,CAAC,EACjC,EAAM,SAAS,KAAK,OAAO,SAAS,EACpC,EAAM,WAAW;;CAGrB,cAAc,GAAY,GAA0B;AAChD,OAAK,iBAAiB,EAAG,EACzB,KAAK,mBAAmB,EACnB,KAAK,oBACV,EAAO,OAAO,KAAK,gBAAgB,EACnC,KAAK,YAAY,IAAI,GAAI,EAAO,EAChC,KAAK,iBAAiB,EACtB,EAAO,eAAe,KAAK,OAAO,mBAAmB,EAAE,KAAK,OAAO,UAAU,EAAE,KAAK,qBAAqB;;CAG7G,iBAAiB,GAAkB;EAC/B,IAAM,IAAS,KAAK,YAAY,IAAI,EAAG;AAClC,QACL,EAAO,SAAS,EAChB,KAAK,YAAY,OAAO,EAAG,EAC3B,KAAK,iBAAiB,WAAW;;CAKrC,gBAA2C;AACvC,SAAO,KAAK,YAAY,cAAc,EAAE;;CAG5C,uBAAyD;AACrD,SAAO,KAAK,YAAY,qBAAqB,EAAE;;CAGnD,gBAA2C;AACvC,SAAO,KAAK,YAAY,cAAc,EAAE;;CAW5C,eAAyC;CAKzC,gBAAgB,GAAuD;AACnE,MAAI,CAAC,KAAK,UAAW,QAAO,QAAQ,OAAO,gBAAI,MAAM,sBAAsB,CAAC;EAC5E,IAAM,IAAY,EAAE,KAAK;AACzB,SAAO,IAAI,SAAsB,MAAY;AAEzC,GADA,KAAK,eAAe,IAAI,GAAW,EAAQ,EAC3C,KAAK,KAAK;IAAC,MAAM;IAAU;IAAW,YAAY,GAAS,cAAc,KAAK;IAAI,CAAC;IACrF;;CAGN,oBAA4B,GAAoC;AAC5D,MAAI,EAAQ,SAAS,UAAU;GAC3B,IAAM,IAAU,KAAK,eAAe,IAAI,EAAQ,UAAU;AAC1D,GAAI,MACA,KAAK,eAAe,OAAO,EAAQ,UAAU,EAC7C,EAAQ,EAAQ,OAAO;;;CAQnC,UAAgB;AACR,YAAK,WAIT;GAHA,KAAK,YAAY,IAEjB,KAAK,MAAM,OAAO,oBAAoB,EACtC,KAAK,oBAAoB,SAAS;AAElC,QAAK,IAAM,KAAU,KAAK,YAAY,QAAQ,CAAE,GAAO,SAAS;AAMhE,GALA,KAAK,YAAY,OAAO,EACxB,KAAK,iBAAiB,SAAS,EAC/B,KAAK,kBAAkB,KAAA,GACvB,KAAK,kBAAkB,KAAA,GACvB,KAAK,gBAAgB,QAAQ,EAC7B,KAAK,iBAAiB,KAAA;AAEtB,QAAK,IAAM,KAAW,KAAK,cAAc,QAAQ,CAAE,GAAQ,UAAU;AAcrE,GAbA,KAAK,cAAc,OAAO,EAC1B,KAAK,oBAAoB,OAAO,EAChC,KAAK,eAAe,OAAO,EAE3B,KAAK,OAAO,iBAAiB,EAC7B,AAEI,KAAK,yBADL,KAAK,OAAO,IAAI,UAAU,KAAK,oBAAoB,EACxB,KAAA,IAG/B,KAAK,KAAK,EAAC,MAAM,WAAU,CAAC,EAC5B,KAAK,WAAW,aAAa,EAE7B,KAAK,OAAO,oBAAoB;;;;;;;;;;;;;;;;;;AC5kBxC,SAAgB,EACZ,GACA,IAAmC,EAAE,EACE;AACvC,SAAQ,MACJ,IAAI,EAAuB,GAAO,GAAW;EACzC,GAAG;EACH,iBAAiB,EAAQ,0BAA0B,IAAI,GAAiB;EAC3E,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { default as Konva } from 'konva';
|
|
2
|
+
import { CoordinateTransform, LiveEffect } from './LiveEffect';
|
|
3
|
+
import { ViewportBounds } from '../types/Settings';
|
|
4
|
+
export interface RippleOptions {
|
|
5
|
+
/** Animation length in milliseconds. Default 400. */
|
|
6
|
+
duration?: number;
|
|
7
|
+
/** Ring colour (any CSS colour). Default "#00e5b2". */
|
|
8
|
+
color?: string;
|
|
9
|
+
/** Starting ring radius in world units. Default 0.3. */
|
|
10
|
+
startRadius?: number;
|
|
11
|
+
/** Final ring radius in world units. Default 1.2. */
|
|
12
|
+
endRadius?: number;
|
|
13
|
+
/** Ring stroke width in world units. Default 0.08. */
|
|
14
|
+
strokeWidth?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Invoked once when the animation finishes. The host should use this to
|
|
17
|
+
* remove the (now spent) effect, e.g. `removeLiveEffect(id)`.
|
|
18
|
+
*/
|
|
19
|
+
onComplete?: () => void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* One-shot expanding, fading ring centred on a world-space point — used to
|
|
23
|
+
* pulse the player marker when it moves. Implements {@link LiveEffect}, so it
|
|
24
|
+
* works on both the Konva and OffscreenCanvas backends (the latter composites
|
|
25
|
+
* it on its main-thread effect stage).
|
|
26
|
+
*
|
|
27
|
+
* The ring lives in scene space (the host layer carries the camera transform),
|
|
28
|
+
* so it tracks the map as the user pans/zooms. It self-animates and calls
|
|
29
|
+
* {@link RippleOptions.onComplete} when done.
|
|
30
|
+
*/
|
|
31
|
+
export declare class RippleEffect implements LiveEffect {
|
|
32
|
+
private readonly worldX;
|
|
33
|
+
private readonly worldY;
|
|
34
|
+
private ring?;
|
|
35
|
+
private rafId?;
|
|
36
|
+
private cancelled;
|
|
37
|
+
private transform;
|
|
38
|
+
private readonly duration;
|
|
39
|
+
private readonly color;
|
|
40
|
+
private readonly startRadius;
|
|
41
|
+
private readonly endRadius;
|
|
42
|
+
private readonly strokeWidth;
|
|
43
|
+
private readonly onComplete?;
|
|
44
|
+
constructor(worldX: number, worldY: number, options?: RippleOptions);
|
|
45
|
+
attach(layer: Konva.Layer): void;
|
|
46
|
+
updateViewport(_bounds: ViewportBounds, _scale: number, coordinateTransform: CoordinateTransform): void;
|
|
47
|
+
destroy(): void;
|
|
48
|
+
}
|