mudlet-map-renderer 2.2.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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,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
+ }
@@ -0,0 +1,70 @@
1
+ import { MapState } from '../MapState';
2
+ import { ViewportBounds } from '../types/Settings';
3
+ import { Shape } from '../scene/Shape';
4
+ import { Direction8 } from '../labelPlacement';
5
+ import { SceneOverlay, SceneOverlayContext } from './SceneOverlay';
6
+ export interface Waypoint {
7
+ roomId: number;
8
+ /** Label text. An array renders as stacked lines in one bubble. */
9
+ label: string | string[];
10
+ /** Marker / accent colour (hex). Default amber. */
11
+ color?: string;
12
+ /** Optional preferred label direction (nudge only). */
13
+ preferred?: Direction8;
14
+ /**
15
+ * Optional click handler. Fired when the waypoint's bubble is clicked.
16
+ * Wire pointer hits to it via {@link WaypointOverlay.hitTest} (see below).
17
+ * Waypoints without a handler are inert.
18
+ */
19
+ onClick?: (waypoint: Waypoint) => void;
20
+ }
21
+ /**
22
+ * Persistent named markers anchored to rooms (shops, banks, quest givers …),
23
+ * with auto-placed labels that dodge neighbouring rooms, exits, and each other
24
+ * via {@link placeLabels}. A {@link SceneOverlay}, so it renders on the
25
+ * interactive canvas and in every export.
26
+ *
27
+ * A room may carry more than one waypoint (e.g. a transport stop served by two
28
+ * routes): the list is flat, so push several entries with the same `roomId` and
29
+ * each gets its own auto-placed bubble. Alternatively give one waypoint a
30
+ * multi-line `label` to list them in a single bubble.
31
+ *
32
+ * Waypoints can be clickable. Bubbles are overlay-layer shapes, so they are not
33
+ * part of the renderer's {@link HitTester}; the overlay instead records the
34
+ * rects it places and resolves them via {@link hitTest}. Wire pointer clicks to
35
+ * it by converting the cursor to world space:
36
+ *
37
+ * ```ts
38
+ * const waypoints = new WaypointOverlay();
39
+ * waypoints.add({ roomId: 42, label: 'Bank', onClick: wp => console.log(wp.roomId) });
40
+ * renderer.addSceneOverlay('waypoints', waypoints);
41
+ *
42
+ * container.addEventListener('click', e => {
43
+ * const p = renderer.camera.clientToMapPoint(e.clientX, e.clientY, container.getBoundingClientRect());
44
+ * if (!p) return;
45
+ * const wp = waypoints.hitTest(p.x, p.y);
46
+ * wp?.onClick?.(wp);
47
+ * });
48
+ * ```
49
+ */
50
+ export declare class WaypointOverlay implements SceneOverlay {
51
+ private waypoints;
52
+ private ctx?;
53
+ /** Bubble rects from the last render, for click hit-testing (world space). */
54
+ private placed;
55
+ set(list: Waypoint[]): void;
56
+ add(waypoint: Waypoint): void;
57
+ /** Remove every waypoint anchored to this room. */
58
+ remove(roomId: number): void;
59
+ has(roomId: number): boolean;
60
+ /**
61
+ * Topmost waypoint whose bubble contains the given **world-space** point, or
62
+ * `undefined`. Hit-tests the bubbles placed by the last {@link render} (in
63
+ * reverse, so later-drawn bubbles win). Convert a pointer position to world
64
+ * space with `renderer.camera.clientToMapPoint(...)` before calling.
65
+ */
66
+ hitTest(worldX: number, worldY: number): Waypoint | undefined;
67
+ attach(ctx: SceneOverlayContext): void;
68
+ detach(): void;
69
+ render(state: MapState, _bounds: ViewportBounds): Shape[];
70
+ }
@@ -3,6 +3,7 @@ import { SketchyOptions } from './shape/SketchyStyle';
3
3
  import { IsometricOptions, IsometricRotation } from './shape/IsometricStyle';
4
4
  import { GradientRoomsOptions } from './shape/GradientRoomsStyle';
5
5
  import { WatercolorOptions } from './shape/WatercolorStyle';
6
+ import { treasureMapDecorations } from './shape/TreasureMapStyle';
6
7
  export { compose, identityStyle } from './Style';
7
8
  export type { Style, StyleContext } from './Style';
8
9
  export { applyStyleToShapes } from './applyStyle';
@@ -34,4 +35,17 @@ export declare const GraphPaper: Style;
34
35
  export declare const Topographic: Style;
35
36
  /** Hand-painted watercolour — translucent edge-bled washes that pool on overlap. */
36
37
  export declare function Watercolor(options?: WatercolorOptions): Style;
38
+ /** Flat dark "modern UI" theme — muted dark rooms with subtle elevation shadows. */
39
+ export declare const DarkModern: Style;
40
+ /**
41
+ * Aged treasure-map palette — weathered-paper rooms inked in faded brown.
42
+ * Pair with {@link treasureMapDecorations} (a scene overlay) for the compass
43
+ * rose and double border frame:
44
+ * ```ts
45
+ * renderer.setStyle(TreasureMap);
46
+ * renderer.addSceneOverlay('treasure-decor', treasureMapDecorations());
47
+ * ```
48
+ */
49
+ export declare const TreasureMap: Style;
50
+ export { treasureMapDecorations };
37
51
  export type { SketchyOptions, IsometricOptions, IsometricRotation, GradientRoomsOptions, WatercolorOptions };
@@ -0,0 +1,15 @@
1
+ import { Style } from '../Style';
2
+ /**
3
+ * Flat dark "modern UI" aesthetic as a {@link Style}.
4
+ *
5
+ * - Room fills are flattened into a muted dark palette (hue kept, saturation
6
+ * capped, lightness compressed) so the map reads as a calm dark dashboard.
7
+ * - Room outlines become a subtle translucent light border.
8
+ * - Each room body casts a soft offset drop-shadow (emitted **before** the
9
+ * body, like Neon's glow, so it renders underneath) for a sense of
10
+ * elevation.
11
+ * - Exit lines become a muted grey-blue; text is off-white.
12
+ * - Images and groups pass through unchanged (the caller walks group
13
+ * children separately).
14
+ */
15
+ export declare const darkModernShapeStyle: Style;
@@ -0,0 +1,23 @@
1
+ import { Style } from '../Style';
2
+ import { SceneOverlay } from '../../overlay/SceneOverlay';
3
+ /**
4
+ * Aged treasure-map palette as a {@link Style}.
5
+ *
6
+ * Recolours rooms onto a warm weathered-paper ramp and inks every line in
7
+ * faded brown. For the decorative compass rose and double border frame, add
8
+ * {@link treasureMapDecorations} as a scene overlay alongside this style.
9
+ *
10
+ * ```ts
11
+ * renderer.setStyle(TreasureMap);
12
+ * renderer.addSceneOverlay('treasure-decor', treasureMapDecorations());
13
+ * ```
14
+ */
15
+ export declare const treasureMapShapeStyle: Style;
16
+ /**
17
+ * Decorative scene overlay for the treasure-map look: a double-line border
18
+ * frame inset from the viewport and a compass rose in the top-right corner.
19
+ *
20
+ * Emitted in world space and pinned to the current viewport, so it tracks the
21
+ * visible region as the user pans/zooms and appears in every export path.
22
+ */
23
+ export declare function treasureMapDecorations(): SceneOverlay;
@@ -30,3 +30,5 @@ export { graphPaperShapeStyle } from './GraphPaperStyle';
30
30
  export { topographicShapeStyle } from './TopographicStyle';
31
31
  export { watercolorShapeStyle } from './WatercolorStyle';
32
32
  export type { WatercolorOptions } from './WatercolorStyle';
33
+ export { darkModernShapeStyle } from './DarkModernStyle';
34
+ export { treasureMapShapeStyle, treasureMapDecorations } from './TreasureMapStyle';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mudlet-map-renderer",
3
- "version": "2.2.0",
3
+ "version": "2.3.1",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "url": "https://github.com/Delwing/mudlet-map-renderer"