@kerusiweb/core 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.
@@ -0,0 +1,137 @@
1
+ import { RenderSeat } from '../render/render-model.js';
2
+ import { SeatBox } from '../render/seat-shapes.js';
3
+ import { PlacedSeat } from '../render/section-layout.js';
4
+ import { KerusiSeatStyle } from './kerusi-seat-style.js';
5
+ import { KerusiSeatmapColors, SeatOccupantVariant } from './kerusi-seatmap-colors.js';
6
+ /**
7
+ * The seat glyph, resolved: every path, paint and position a binding draws
8
+ * inside one seat's `<g>`.
9
+ *
10
+ * Both bindings used to call the paint and shape functions one by one, and each
11
+ * kept its own copy of the "mark box" rule below. That is exactly the code the
12
+ * architecture says belongs in core, so it lives here now and a binding only
13
+ * binds the result. It is also what a custom seat template receives, so a
14
+ * consumer drawing their own seat can reuse the library's paths and colours
15
+ * rather than re-derive them.
16
+ */
17
+ /** A filled path. */
18
+ export interface SeatGlyphShape {
19
+ /** The path's `d` attribute. */
20
+ d: string;
21
+ /** A CSS paint, usually a `var(--kerusi-*, …)` with the resolved fallback. */
22
+ fill: string;
23
+ }
24
+ export interface SeatGlyph {
25
+ /** `null` for an available or blocked seat: nothing to mark. */
26
+ variant: SeatOccupantVariant | null;
27
+ /** The classes for the seat's `<g>`: `kerusi-seat`, its status, and the modifiers. */
28
+ className: string;
29
+ /**
30
+ * The `data-*` attributes for the seat's `<g>`, so CSS can pick seats out by
31
+ * what the document says about them. An absent field is not rendered.
32
+ */
33
+ data: SeatDataAttributes;
34
+ /** The seat's own placed box. */
35
+ box: SeatBox;
36
+ /**
37
+ * The box the decorative marks sit on: the seat's own, or the selected core's
38
+ * when it is selected. See {@link resolveSeatGlyph}.
39
+ */
40
+ markBox: SeatBox;
41
+ /**
42
+ * The outline: by default square-ish at the front and tapered at the back, so
43
+ * facing reads from the shape; otherwise the seat style's `shape`.
44
+ */
45
+ body: SeatGlyphShape;
46
+ /** The dimming wash over a held or booked seat; absent otherwise. */
47
+ wash?: SeatGlyphShape;
48
+ /** The bright plate inside a selected seat's frame; absent otherwise. */
49
+ core?: SeatGlyphShape;
50
+ /** The occupant figure for a selected, held or booked seat; absent otherwise. */
51
+ occupant?: SeatGlyphShape & {
52
+ variant: SeatOccupantVariant;
53
+ /** `none` except for the hollow held figure. */
54
+ stroke: string;
55
+ strokeWidth: number;
56
+ };
57
+ /** The seat number, centred on the seat and kept upright. */
58
+ label: {
59
+ text: string;
60
+ x: number;
61
+ y: number;
62
+ fontSize: number;
63
+ fill: string;
64
+ /** A core-coloured halo behind a selected seat's number; `none` otherwise. */
65
+ halo: string;
66
+ haloWidth: number;
67
+ /** Cancels the seat's rotation; `null` when the seat does not rotate. */
68
+ transform: string | null;
69
+ };
70
+ /** The corner marker for `accessibility.wheelchairAccessible`; absent otherwise. */
71
+ wheelchair?: {
72
+ cx: number;
73
+ cy: number;
74
+ r: number;
75
+ fill: string;
76
+ };
77
+ }
78
+ /** See {@link SeatGlyph.data}. Each field is bound as the attribute named beside it. */
79
+ export interface SeatDataAttributes {
80
+ /** `data-seat-type`: `Seat.type`. */
81
+ type: string;
82
+ /** `data-seat-status`: the merged availability, same as the status class. */
83
+ status: string;
84
+ /** `data-section-id`: the section the seat belongs to. */
85
+ section: string;
86
+ /** `data-seat-row`: `Seat.row`, when the seat names one. */
87
+ row?: string;
88
+ /**
89
+ * `data-seat-attributes`: `Seat.attributes`, space-separated, when there are
90
+ * any, so `[data-seat-attributes~="aisle"]` matches one tag among several.
91
+ */
92
+ attributes?: string;
93
+ }
94
+ /**
95
+ * What a custom seat template is handed: the seat, whether it is selected, and
96
+ * the stock glyph already resolved for it. Each binding extends this with a
97
+ * framework-native way to draw the stock glyph (`defaultContent`).
98
+ */
99
+ export interface KerusiSeatContext {
100
+ /** The seat as placed in its section, in viewBox units. */
101
+ placed: PlacedSeat;
102
+ /** The resolved seat: status, type, price, accessibility, `source.metadata`. */
103
+ seat: RenderSeat;
104
+ /** Whether the seat is in the current selection. */
105
+ selected: boolean;
106
+ /** The stock glyph's paths and paints, for reuse. */
107
+ glyph: SeatGlyph;
108
+ }
109
+ /**
110
+ * Resolves the stock glyph for one placed seat.
111
+ *
112
+ * Everything over a selected seat sits on its core, so every mark is measured
113
+ * from the core's box rather than the seat's. Drawing to the seat's box leaves
114
+ * the occupant's hips crossing the frame, where a figure tinted `selectedBg`
115
+ * lands on a frame filled `selectedBg` and reads as a hard nub. Because the core
116
+ * is an exact scaled copy of the body (see `seatSelectedFrame`), measuring from
117
+ * it keeps every clearance a mark had against the body, so the frame width stays
118
+ * free to change. *
119
+ * `style` is the seat's resolved `seatStyle` / `seatTypeStyles` entry (see
120
+ * `resolveSeatStyle`). It changes the seat's own colour, label colour and
121
+ * classes; it never changes a selected or blocked seat's fill.
122
+ */
123
+ export declare function resolveSeatGlyph(placed: PlacedSeat, selected: boolean, colors: Required<KerusiSeatmapColors>, typeColors: boolean, style?: KerusiSeatStyle): SeatGlyph;
124
+ /**
125
+ * The outline for a legend type swatch drawn in its type's `shape`, on a
126
+ * `size`-square box, or `undefined` to keep the flat square swatch: a type with
127
+ * no shape, or the default `'seat'`, has nothing a square does not already say.
128
+ */
129
+ export declare function legendSwatchPath(typeStyle: KerusiSeatStyle | undefined, size: number): string | undefined;
130
+ /** The `data-*` attributes for a seat's `<g>`; see {@link SeatDataAttributes}. */
131
+ export declare function seatDataAttributes(seat: RenderSeat): SeatDataAttributes;
132
+ /**
133
+ * The class list for a seat's `<g>`: the hooks the READMEs document, then any
134
+ * `extra` classes from the seat's style.
135
+ */
136
+ export declare function seatClassName(seat: RenderSeat, selected: boolean, extra?: string | readonly string[]): string;
137
+ //# sourceMappingURL=seat-glyph.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seat-glyph.d.ts","sourceRoot":"","sources":["../../src/view/seat-glyph.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AACvD,OAAO,EACL,OAAO,EAMR,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAa,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACpE,OAAO,EACL,mBAAmB,EAGnB,mBAAmB,EAMpB,MAAM,4BAA4B,CAAC;AAEpC;;;;;;;;;;GAUG;AAEH,qBAAqB;AACrB,MAAM,WAAW,cAAc;IAC7B,gCAAgC;IAChC,CAAC,EAAE,MAAM,CAAC;IACV,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,SAAS;IACxB,gEAAgE;IAChE,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACpC,sFAAsF;IACtF,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,IAAI,EAAE,kBAAkB,CAAC;IACzB,iCAAiC;IACjC,GAAG,EAAE,OAAO,CAAC;IACb;;;OAGG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,IAAI,EAAE,cAAc,CAAC;IACrB,qEAAqE;IACrE,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,yEAAyE;IACzE,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,cAAc,GAAG;QAC1B,OAAO,EAAE,mBAAmB,CAAC;QAC7B,gDAAgD;QAChD,MAAM,EAAE,MAAM,CAAC;QACf,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,6DAA6D;IAC7D,KAAK,EAAE;QACL,IAAI,EAAE,MAAM,CAAC;QACb,CAAC,EAAE,MAAM,CAAC;QACV,CAAC,EAAE,MAAM,CAAC;QACV,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,8EAA8E;QAC9E,IAAI,EAAE,MAAM,CAAC;QACb,SAAS,EAAE,MAAM,CAAC;QAClB,yEAAyE;QACzE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;KAC1B,CAAC;IACF,oFAAoF;IACpF,UAAU,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CAClE;AAED,wFAAwF;AACxF,MAAM,WAAW,kBAAkB;IACjC,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,MAAM,EAAE,MAAM,CAAC;IACf,0DAA0D;IAC1D,OAAO,EAAE,MAAM,CAAC;IAChB,4DAA4D;IAC5D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,2DAA2D;IAC3D,MAAM,EAAE,UAAU,CAAC;IACnB,gFAAgF;IAChF,IAAI,EAAE,UAAU,CAAC;IACjB,oDAAoD;IACpD,QAAQ,EAAE,OAAO,CAAC;IAClB,qDAAqD;IACrD,KAAK,EAAE,SAAS,CAAC;CAClB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,UAAU,EAClB,QAAQ,EAAE,OAAO,EACjB,MAAM,EAAE,QAAQ,CAAC,mBAAmB,CAAC,EACrC,UAAU,EAAE,OAAO,EACnB,KAAK,CAAC,EAAE,eAAe,GACtB,SAAS,CAmEX;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,eAAe,GAAG,SAAS,EACtC,IAAI,EAAE,MAAM,GACX,MAAM,GAAG,SAAS,CAGpB;AAaD,kFAAkF;AAClF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,UAAU,GAAG,kBAAkB,CAQvE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,UAAU,EAChB,QAAQ,EAAE,OAAO,EACjB,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,GACjC,MAAM,CAYR"}
@@ -0,0 +1,130 @@
1
+ import { seatMarkerAnchor, seatOccupantPath, seatOccupantStroke, seatSelectedFrame, seatShapePath, } from '../render/seat-shapes.js';
2
+ import { classList } from './kerusi-seat-style.js';
3
+ import { occupantFillFor, occupantStrokeFor, seatCoreFill, seatFill, seatOccupantVariant, seatTextFill, seatWashFill, } from './kerusi-seatmap-colors.js';
4
+ /**
5
+ * Resolves the stock glyph for one placed seat.
6
+ *
7
+ * Everything over a selected seat sits on its core, so every mark is measured
8
+ * from the core's box rather than the seat's. Drawing to the seat's box leaves
9
+ * the occupant's hips crossing the frame, where a figure tinted `selectedBg`
10
+ * lands on a frame filled `selectedBg` and reads as a hard nub. Because the core
11
+ * is an exact scaled copy of the body (see `seatSelectedFrame`), measuring from
12
+ * it keeps every clearance a mark had against the body, so the frame width stays
13
+ * free to change. *
14
+ * `style` is the seat's resolved `seatStyle` / `seatTypeStyles` entry (see
15
+ * `resolveSeatStyle`). It changes the seat's own colour, label colour and
16
+ * classes; it never changes a selected or blocked seat's fill.
17
+ */
18
+ export function resolveSeatGlyph(placed, selected, colors, typeColors, style) {
19
+ const seat = placed.seat;
20
+ const variant = seatOccupantVariant(seat, selected);
21
+ const box = {
22
+ x: placed.x,
23
+ y: placed.y,
24
+ width: placed.width,
25
+ height: placed.height,
26
+ };
27
+ const markBox = selected ? coreBox(box) : box;
28
+ const shape = style?.shape;
29
+ const bodyD = seatShapePath(shape, box.x, box.y, box.width, box.height);
30
+ const anchor = seatMarkerAnchor(shape);
31
+ const textFill = seatTextFill(seat, selected, colors, typeColors, style);
32
+ return {
33
+ variant,
34
+ className: seatClassName(seat, selected, style?.className),
35
+ data: seatDataAttributes(seat),
36
+ box,
37
+ markBox,
38
+ body: { d: bodyD, fill: seatFill(seat, selected, colors, typeColors, style) },
39
+ wash: variant === 'held' || variant === 'booked'
40
+ ? { d: bodyD, fill: seatWashFill(variant, colors) }
41
+ : undefined,
42
+ core: selected
43
+ ? {
44
+ d: seatShapePath(shape, markBox.x, markBox.y, markBox.width, markBox.height),
45
+ fill: seatCoreFill(colors),
46
+ }
47
+ : undefined,
48
+ occupant: variant
49
+ ? {
50
+ variant,
51
+ d: seatOccupantPath(markBox.x, markBox.y, markBox.width, markBox.height),
52
+ fill: occupantFillFor(variant, colors),
53
+ stroke: occupantStrokeFor(variant, colors),
54
+ strokeWidth: seatOccupantStroke(markBox.width, markBox.height),
55
+ }
56
+ : undefined,
57
+ label: {
58
+ text: seat.label,
59
+ x: placed.centerX,
60
+ y: placed.centerY,
61
+ fontSize: placed.fontSize,
62
+ fill: textFill,
63
+ // The label is centred on the occupant's shoulder line, so without the
64
+ // halo a selected seat's number is `selectedBg` over a `selectedBg`
65
+ // silhouette. It also rescues a label too long for the core.
66
+ halo: selected ? seatCoreFill(colors) : 'none',
67
+ haloWidth: selected ? placed.fontSize * 0.22 : 0,
68
+ transform: placed.rotation
69
+ ? `rotate(${-placed.rotation} ${placed.centerX} ${placed.centerY})`
70
+ : null,
71
+ },
72
+ // The marker keeps the same relative corner on whichever box it sits on, so
73
+ // a selected seat's dot stays whole instead of being bitten off by the frame.
74
+ wheelchair: seat.accessibility?.wheelchairAccessible
75
+ ? {
76
+ cx: markBox.x + markBox.width * anchor.x,
77
+ cy: markBox.y + markBox.height * anchor.y,
78
+ r: markBox.width * 0.13,
79
+ fill: textFill,
80
+ }
81
+ : undefined,
82
+ };
83
+ }
84
+ /**
85
+ * The outline for a legend type swatch drawn in its type's `shape`, on a
86
+ * `size`-square box, or `undefined` to keep the flat square swatch: a type with
87
+ * no shape, or the default `'seat'`, has nothing a square does not already say.
88
+ */
89
+ export function legendSwatchPath(typeStyle, size) {
90
+ const shape = typeStyle?.shape;
91
+ return shape && shape !== 'seat' ? seatShapePath(shape, 0, 0, size, size) : undefined;
92
+ }
93
+ /** The selected core's box: the seat's, inset by the frame width on every side. */
94
+ function coreBox(box) {
95
+ const frame = seatSelectedFrame(box.width, box.height);
96
+ return {
97
+ x: box.x + frame,
98
+ y: box.y + frame,
99
+ width: box.width - frame * 2,
100
+ height: box.height - frame * 2,
101
+ };
102
+ }
103
+ /** The `data-*` attributes for a seat's `<g>`; see {@link SeatDataAttributes}. */
104
+ export function seatDataAttributes(seat) {
105
+ return {
106
+ type: seat.type.id,
107
+ status: seat.status,
108
+ section: seat.sectionId,
109
+ row: seat.row || undefined,
110
+ attributes: seat.attributes.length ? seat.attributes.join(' ') : undefined,
111
+ };
112
+ }
113
+ /**
114
+ * The class list for a seat's `<g>`: the hooks the READMEs document, then any
115
+ * `extra` classes from the seat's style.
116
+ */
117
+ export function seatClassName(seat, selected, extra) {
118
+ return [
119
+ 'kerusi-seat',
120
+ `kerusi-seat--${seat.status}`,
121
+ selected ? 'kerusi-seat--selected' : '',
122
+ seat.selectable ? '' : 'kerusi-seat--unselectable',
123
+ seat.accessibility?.wheelchairAccessible ? 'kerusi-seat--wheelchair' : '',
124
+ seat.companions.length > 0 ? 'kerusi-seat--companion' : '',
125
+ ...classList(extra),
126
+ ]
127
+ .filter(Boolean)
128
+ .join(' ');
129
+ }
130
+ //# sourceMappingURL=seat-glyph.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seat-glyph.js","sourceRoot":"","sources":["../../src/view/seat-glyph.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,65 @@
1
+ import { KerusiViewport, ViewportLimits, ViewportPoint, ViewportSize } from './viewport.js';
2
+ /**
3
+ * Pointer gestures on a zoomable map, as a reducer.
4
+ *
5
+ * One pointer pans, two pinch. The rule that matters most is the one that keeps
6
+ * seat selection intact: a touch that never leaves a small circle around where
7
+ * it landed is a *tap* and changes nothing, so the `click` that follows still
8
+ * reaches the seat. A binding feeds `down` / `move` / `up` / `cancel` in
9
+ * container coordinates and applies what comes back; it never has to know what
10
+ * a pinch is.
11
+ */
12
+ /** How far a pointer may travel, in CSS pixels, and still be a tap. */
13
+ export declare const TAP_SLOP_PX = 8;
14
+ export type GestureEvent = {
15
+ type: 'down' | 'move';
16
+ id: number;
17
+ point: ViewportPoint;
18
+ } | {
19
+ type: 'up' | 'cancel';
20
+ id: number;
21
+ };
22
+ /** Where the gesture stands. Opaque to callers: pass it back in. */
23
+ export interface GestureState {
24
+ /** Tracked pointer ids, oldest first. A pinch uses the first two. */
25
+ readonly ids: readonly number[];
26
+ /** Where each tracked pointer is now. */
27
+ readonly points: Readonly<Record<number, ViewportPoint>>;
28
+ /** The viewport and pointer positions the current movement is measured from. */
29
+ readonly origin: {
30
+ readonly viewport: KerusiViewport;
31
+ readonly points: Readonly<Record<number, ViewportPoint>>;
32
+ } | null;
33
+ /** True once the gesture is a pan or a pinch rather than a possible tap. */
34
+ readonly moved: boolean;
35
+ }
36
+ export declare const IDLE_GESTURE: GestureState;
37
+ export interface GestureContext {
38
+ /** The live viewport. */
39
+ viewport: KerusiViewport;
40
+ /** The map's laid-out size, unscaled. */
41
+ content: ViewportSize;
42
+ /** The visible area. */
43
+ container: ViewportSize;
44
+ limits?: ViewportLimits;
45
+ /** Overrides {@link TAP_SLOP_PX}. */
46
+ slop?: number;
47
+ }
48
+ export interface GestureResult {
49
+ state: GestureState;
50
+ /** The viewport after this event; the very same object when nothing changed. */
51
+ viewport: KerusiViewport;
52
+ /**
53
+ * True on the event that turns a possible tap into a gesture. A binding
54
+ * captures the pointers then, not earlier: capturing on `down` would
55
+ * retarget the tap's `click` away from the seat.
56
+ */
57
+ capture: boolean;
58
+ /**
59
+ * True on the `up` that ends a gesture. The browser still fires a `click`
60
+ * after a drag, and it must not select the seat under the finger.
61
+ */
62
+ suppressClick: boolean;
63
+ }
64
+ export declare function reduceGesture(state: GestureState, event: GestureEvent, context: GestureContext): GestureResult;
65
+ //# sourceMappingURL=viewport-gesture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewport-gesture.d.ts","sourceRoot":"","sources":["../../src/view/viewport-gesture.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,cAAc,EACd,cAAc,EACd,aAAa,EACb,YAAY,EAEb,MAAM,eAAe,CAAC;AAEvB;;;;;;;;;GASG;AAEH,uEAAuE;AACvE,eAAO,MAAM,WAAW,IAAI,CAAC;AAE7B,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,aAAa,CAAA;CAAE,GAC3D;IAAE,IAAI,EAAE,IAAI,GAAG,QAAQ,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1C,oEAAoE;AACpE,MAAM,WAAW,YAAY;IAC3B,qEAAqE;IACrE,QAAQ,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,yCAAyC;IACzC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC;IACzD,gFAAgF;IAChF,QAAQ,CAAC,MAAM,EAAE;QACf,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;QAClC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC;KAC1D,GAAG,IAAI,CAAC;IACT,4EAA4E;IAC5E,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAED,eAAO,MAAM,YAAY,EAAE,YAKzB,CAAC;AAEH,MAAM,WAAW,cAAc;IAC7B,yBAAyB;IACzB,QAAQ,EAAE,cAAc,CAAC;IACzB,yCAAyC;IACzC,OAAO,EAAE,YAAY,CAAC;IACtB,wBAAwB;IACxB,SAAS,EAAE,YAAY,CAAC;IACxB,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,qCAAqC;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,YAAY,CAAC;IACpB,gFAAgF;IAChF,QAAQ,EAAE,cAAc,CAAC;IACzB;;;;OAIG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,wBAAgB,aAAa,CAC3B,KAAK,EAAE,YAAY,EACnB,KAAK,EAAE,YAAY,EACnB,OAAO,EAAE,cAAc,GACtB,aAAa,CAUf"}
@@ -0,0 +1,145 @@
1
+ import { clampViewport, zoomLimits, } from './viewport.js';
2
+ /**
3
+ * Pointer gestures on a zoomable map, as a reducer.
4
+ *
5
+ * One pointer pans, two pinch. The rule that matters most is the one that keeps
6
+ * seat selection intact: a touch that never leaves a small circle around where
7
+ * it landed is a *tap* and changes nothing, so the `click` that follows still
8
+ * reaches the seat. A binding feeds `down` / `move` / `up` / `cancel` in
9
+ * container coordinates and applies what comes back; it never has to know what
10
+ * a pinch is.
11
+ */
12
+ /** How far a pointer may travel, in CSS pixels, and still be a tap. */
13
+ export const TAP_SLOP_PX = 8;
14
+ export const IDLE_GESTURE = Object.freeze({
15
+ ids: [],
16
+ points: {},
17
+ origin: null,
18
+ moved: false,
19
+ });
20
+ export function reduceGesture(state, event, context) {
21
+ switch (event.type) {
22
+ case 'down':
23
+ return onDown(state, event.id, event.point, context);
24
+ case 'move':
25
+ return onMove(state, event.id, event.point, context);
26
+ case 'up':
27
+ case 'cancel':
28
+ return onEnd(state, event.id, event.type === 'up', context);
29
+ }
30
+ }
31
+ function onDown(state, id, point, context) {
32
+ const ids = state.ids.includes(id) ? state.ids : [...state.ids, id];
33
+ const points = { ...state.points, [id]: point };
34
+ // A second finger makes it a pinch at once; there is no tap with two fingers.
35
+ const moved = state.moved || ids.length > 1;
36
+ return {
37
+ // Re-baselined on every change to the pointer set, so nothing jumps.
38
+ state: { ids, points, origin: { viewport: context.viewport, points }, moved },
39
+ viewport: context.viewport,
40
+ capture: moved && !state.moved,
41
+ suppressClick: false,
42
+ };
43
+ }
44
+ function onMove(state, id, point, context) {
45
+ if (!state.ids.includes(id) || !state.origin) {
46
+ return unchanged(state, context);
47
+ }
48
+ const points = { ...state.points, [id]: point };
49
+ let moved = state.moved;
50
+ let capture = false;
51
+ if (!moved) {
52
+ const slop = context.slop ?? TAP_SLOP_PX;
53
+ const start = state.origin.points[id] ?? point;
54
+ if (distance(start, point) <= slop) {
55
+ return {
56
+ state: { ...state, points },
57
+ viewport: context.viewport,
58
+ capture: false,
59
+ suppressClick: false,
60
+ };
61
+ }
62
+ moved = true;
63
+ capture = true;
64
+ }
65
+ const next = transformFromOrigin(state, points, context);
66
+ return {
67
+ state: { ...state, points, moved },
68
+ viewport: next,
69
+ capture,
70
+ suppressClick: false,
71
+ };
72
+ }
73
+ function onEnd(state, id, isUp, context) {
74
+ if (!state.ids.includes(id)) {
75
+ return unchanged(state, context);
76
+ }
77
+ const ids = state.ids.filter((other) => other !== id);
78
+ const points = { ...state.points };
79
+ delete points[id];
80
+ if (ids.length === 0) {
81
+ return {
82
+ state: IDLE_GESTURE,
83
+ viewport: context.viewport,
84
+ capture: false,
85
+ // A cancel means the browser took the gesture (a page scroll), and no
86
+ // click follows it.
87
+ suppressClick: isUp && state.moved,
88
+ };
89
+ }
90
+ // One finger of a pinch lifted: the other carries on as a pan from here.
91
+ return {
92
+ state: {
93
+ ids,
94
+ points,
95
+ origin: { viewport: context.viewport, points },
96
+ moved: state.moved,
97
+ },
98
+ viewport: context.viewport,
99
+ capture: false,
100
+ suppressClick: false,
101
+ };
102
+ }
103
+ /** Pan for one pointer; pan and zoom about the midpoint for two. */
104
+ function transformFromOrigin(state, points, context) {
105
+ const origin = state.origin;
106
+ const limits = context.limits ?? zoomLimits();
107
+ const [first, second] = state.ids;
108
+ const from = [origin.points[first]];
109
+ const to = [points[first]];
110
+ if (second !== undefined) {
111
+ from.push(origin.points[second]);
112
+ to.push(points[second]);
113
+ }
114
+ const fromCentre = centroid(from);
115
+ const toCentre = centroid(to);
116
+ const base = origin.viewport;
117
+ let zoom = base.zoom;
118
+ if (from.length === 2) {
119
+ const fromSpan = distance(from[0], from[1]);
120
+ if (fromSpan > 0) {
121
+ zoom = clampZoom(base.zoom * (distance(to[0], to[1]) / fromSpan), limits);
122
+ }
123
+ }
124
+ // The map point that sat under the gesture's start stays under its midpoint.
125
+ const ratio = zoom / base.zoom;
126
+ return clampViewport({
127
+ zoom,
128
+ x: toCentre.x - (fromCentre.x - base.x) * ratio,
129
+ y: toCentre.y - (fromCentre.y - base.y) * ratio,
130
+ }, context.content, context.container, limits);
131
+ }
132
+ function unchanged(state, context) {
133
+ return { state, viewport: context.viewport, capture: false, suppressClick: false };
134
+ }
135
+ function distance(a, b) {
136
+ return Math.hypot(a.x - b.x, a.y - b.y);
137
+ }
138
+ function centroid(points) {
139
+ const sum = points.reduce((acc, p) => ({ x: acc.x + p.x, y: acc.y + p.y }), { x: 0, y: 0 });
140
+ return { x: sum.x / points.length, y: sum.y / points.length };
141
+ }
142
+ function clampZoom(zoom, limits) {
143
+ return Math.min(limits.maxZoom, Math.max(limits.minZoom, zoom));
144
+ }
145
+ //# sourceMappingURL=viewport-gesture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewport-gesture.js","sourceRoot":"","sources":["../../src/view/viewport-gesture.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,144 @@
1
+ /**
2
+ * The zoom-and-pan model behind a `zoomable` seat map.
3
+ *
4
+ * A viewport is a CSS transform applied to the whole map: `translate(x, y)
5
+ * scale(zoom)` about the map's top-left corner. Everything here is arithmetic on
6
+ * numbers the caller measured, so it holds no DOM and every function is pure.
7
+ * The bindings own the measuring and the event listeners; this owns what a
8
+ * wheel tick, a key press or a pinch *means*.
9
+ *
10
+ * Zoom 1 is "fit": the map exactly as it lays out without zooming. It is also
11
+ * the minimum, so the map can never be shrunk below the container.
12
+ */
13
+ /** Zoom and pan of the map, as a CSS transform about its top-left corner. */
14
+ export interface KerusiViewport {
15
+ /** Scale relative to fit. 1 is fit-to-container. */
16
+ zoom: number;
17
+ /** Translation in CSS pixels, applied before the scale. */
18
+ x: number;
19
+ y: number;
20
+ }
21
+ export interface ViewportPoint {
22
+ x: number;
23
+ y: number;
24
+ }
25
+ export interface ViewportSize {
26
+ width: number;
27
+ height: number;
28
+ }
29
+ export interface ViewportRect extends ViewportPoint, ViewportSize {
30
+ }
31
+ export interface ViewportLimits {
32
+ minZoom: number;
33
+ maxZoom: number;
34
+ }
35
+ /** The map exactly as laid out: no zoom, no pan. */
36
+ export declare const FIT_VIEWPORT: KerusiViewport;
37
+ /** Zoom is relative to fit, so the floor is always 1. */
38
+ export declare const MIN_ZOOM = 1;
39
+ /** The default ceiling: enough to make a seat on a 375px phone comfortably tappable. */
40
+ export declare const DEFAULT_MAX_ZOOM = 4;
41
+ /** What one press of a zoom button or the `+` / `-` key multiplies the zoom by. */
42
+ export declare const ZOOM_STEP = 1.5;
43
+ /** How far an arrow key pans, in CSS pixels. */
44
+ export declare const KEYBOARD_PAN_STEP = 48;
45
+ /** The gap kept between a revealed seat and the container's edge, in CSS pixels. */
46
+ export declare const REVEAL_MARGIN = 16;
47
+ /** Wheel zoom: the exponent per pixel of `deltaY`, so a pinch's small deltas stay smooth. */
48
+ export declare const WHEEL_ZOOM_SENSITIVITY = 0.01;
49
+ /** A single wheel event never moves further than this, so a mouse notch is one step. */
50
+ export declare const WHEEL_MAX_DELTA = 50;
51
+ /** The limits for a given ceiling. A ceiling below fit still leaves fit reachable. */
52
+ export declare function zoomLimits(maxZoom?: number): ViewportLimits;
53
+ /**
54
+ * Brings a viewport back inside its limits.
55
+ *
56
+ * Zoom is clamped to the limits. Pan is clamped so the scaled map never leaves
57
+ * the container, and on an axis where the scaled map is *smaller* than the
58
+ * container (a maximized map on a large screen) it is centred instead.
59
+ */
60
+ export declare function clampViewport(viewport: KerusiViewport, content: ViewportSize, container: ViewportSize, limits?: ViewportLimits): KerusiViewport;
61
+ /** Fit, centred where the map is smaller than its container. */
62
+ export declare function fitViewport(content: ViewportSize, container: ViewportSize, limits?: ViewportLimits): KerusiViewport;
63
+ /**
64
+ * Zooms to an absolute level, keeping the map point under `anchor` where it is.
65
+ * `anchor` is in container coordinates — the pointer, the midpoint of a pinch,
66
+ * or the container's centre for a button.
67
+ */
68
+ export declare function zoomViewportTo(viewport: KerusiViewport, zoom: number, anchor: ViewportPoint, content: ViewportSize, container: ViewportSize, limits?: ViewportLimits): KerusiViewport;
69
+ /** Zooms by a factor about `anchor`. See {@link zoomViewportTo}. */
70
+ export declare function zoomViewportBy(viewport: KerusiViewport, factor: number, anchor: ViewportPoint, content: ViewportSize, container: ViewportSize, limits?: ViewportLimits): KerusiViewport;
71
+ /** One zoom-button step about the container's centre: `1` in, `-1` out. */
72
+ export declare function stepViewport(viewport: KerusiViewport, direction: 1 | -1, content: ViewportSize, container: ViewportSize, limits?: ViewportLimits): KerusiViewport;
73
+ /** Pans by a delta in CSS pixels, held inside the map's bounds. */
74
+ export declare function panViewportBy(viewport: KerusiViewport, dx: number, dy: number, content: ViewportSize, container: ViewportSize, limits?: ViewportLimits): KerusiViewport;
75
+ /**
76
+ * Pans just far enough to bring `rect` inside the container, with a margin.
77
+ *
78
+ * `rect` is where the thing is on screen *now*, in container coordinates — a
79
+ * seat's bounding box minus the container's. Measuring the result of the
80
+ * current transform, rather than computing a position from the layout, is what
81
+ * keeps this correct whatever the section widths and unit scale are. A rect
82
+ * bigger than the container is aligned by its leading edge.
83
+ */
84
+ export declare function revealRect(viewport: KerusiViewport, rect: ViewportRect, content: ViewportSize, container: ViewportSize, margin?: number, limits?: ViewportLimits): KerusiViewport;
85
+ /**
86
+ * The zoom factor a wheel event asks for, or `null` if it is not a zoom
87
+ * gesture.
88
+ *
89
+ * Only Ctrl/Cmd + wheel zooms. Browsers report a trackpad pinch as a wheel
90
+ * event with `ctrlKey` set, so one rule covers both. A plain wheel returns
91
+ * `null`, which is the cue for a binding to leave it alone so the page still
92
+ * scrolls — and only a non-null result should be `preventDefault`ed.
93
+ */
94
+ export declare function wheelZoomFactor(event: {
95
+ deltaY: number;
96
+ deltaMode?: number;
97
+ ctrlKey: boolean;
98
+ metaKey: boolean;
99
+ }): number | null;
100
+ /** The keys {@link keyboardViewport} understands, for a binding's own filtering. */
101
+ export interface ViewportKeyInput {
102
+ key: string;
103
+ ctrlKey?: boolean;
104
+ metaKey?: boolean;
105
+ altKey?: boolean;
106
+ /**
107
+ * Whether the viewport itself, rather than a seat inside it, has focus.
108
+ * Arrow keys pan only then: on a seat they keep moving seat focus.
109
+ */
110
+ viewportFocused?: boolean;
111
+ }
112
+ /**
113
+ * What a key press does to the viewport, or `null` if it does nothing.
114
+ *
115
+ * `+` / `=` zoom in, `-` zoom out, `0` resets to fit, and — only when the
116
+ * viewport itself has focus — the arrow keys pan. Ctrl, Cmd and Alt
117
+ * combinations are the browser's own zoom shortcuts and are left alone.
118
+ */
119
+ export declare function keyboardViewport(viewport: KerusiViewport, input: ViewportKeyInput, content: ViewportSize, container: ViewportSize, limits?: ViewportLimits): KerusiViewport | null;
120
+ /**
121
+ * Whether a key event came from somewhere text is typed.
122
+ *
123
+ * A host can put its own controls inside the map, and `+`, `-` and `0` are
124
+ * characters before they are shortcuts. Takes the target structurally so core
125
+ * never has to name a DOM type.
126
+ */
127
+ export declare function isTextEntryTarget(target: {
128
+ tagName?: string;
129
+ isContentEditable?: boolean;
130
+ } | null | undefined): boolean;
131
+ /** Whether two viewports are the same view, ignoring sub-pixel noise. */
132
+ export declare function viewportsEqual(a: KerusiViewport, b: KerusiViewport): boolean;
133
+ /** The CSS `transform` for a viewport. Apply with `transform-origin: 0 0`. */
134
+ export declare function viewportTransform(viewport: KerusiViewport): string;
135
+ /**
136
+ * The `touch-action` a zoomable map sets on its viewport.
137
+ *
138
+ * At fit there is nothing to pan, so a one-finger vertical swipe stays the
139
+ * page's: `pan-y` leaves vertical scrolling to the browser while still handing
140
+ * us pinch. Once zoomed in, one finger means pan, and a maximized map covers
141
+ * the page, so both take every gesture with `none`.
142
+ */
143
+ export declare function viewportTouchAction(viewport: KerusiViewport, maximized: boolean): string;
144
+ //# sourceMappingURL=viewport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewport.d.ts","sourceRoot":"","sources":["../../src/view/viewport.ts"],"names":[],"mappings":"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"}