@johnmorrisdotca/kyogijo-stadiums 0.59.0 → 0.60.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.
- package/CHANGELOG.md +12 -0
- package/README.md +2 -0
- package/catalogue.json +1 -1
- package/dist/catalogue.data.js +1 -1
- package/dist/element.d.ts +3 -3
- package/dist/element.js +10 -5
- package/dist/replay-entry.d.ts +2 -0
- package/dist/replay-entry.js +1 -0
- package/dist/replay.control.d.ts +4 -0
- package/dist/replay.control.js +16 -0
- package/dist/replay.dom.js +20 -3
- package/dist/replay.follow.d.ts +164 -0
- package/dist/replay.follow.dom.d.ts +30 -0
- package/dist/replay.follow.dom.js +118 -0
- package/dist/replay.follow.js +279 -0
- package/dist/replay.types.d.ts +6 -1
- package/dist/strings.core.d.ts +10 -0
- package/dist/strings.en.js +1 -0
- package/dist/strings.ja.js +1 -0
- package/dist/turn.camera.d.ts +3 -2
- package/dist/turn.camera.js +9 -4
- package/dist/turn.dom.d.ts +5 -0
- package/dist/turn.dom.js +63 -6
- package/dist/turn.follow.d.ts +22 -0
- package/dist/turn.follow.js +46 -0
- package/dist/turn.markup.d.ts +1 -1
- package/dist/turn.markup.js +2 -2
- package/dist/turn.replay.dom.d.ts +5 -0
- package/dist/turn.replay.dom.js +8 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// Following the play: where the camera looks while a replay plays, as arithmetic on numbers. `followShot` is the shot the play asks for at a moment (the skaters near the puck, padded, or the
|
|
2
|
+
// puck itself, or the faceoff dot in a stoppage); `followCamera` eases toward it with a critically damped spring, a dead zone and a leash that keeps the puck in the frame; `followRunner` is
|
|
3
|
+
// what a page's replay calls on each change of its clock. Nothing here reads the page or a clock: the caller says how many seconds went by, and a test does it with numbers.
|
|
4
|
+
import { replayEvents } from "./replay.js";
|
|
5
|
+
/** The modes, in the order the controls list them after "off". */
|
|
6
|
+
export const FOLLOW_MODES = ["off", "action", "puck"];
|
|
7
|
+
/** A mode from anything: `action` and `puck` are themselves, everything else is `off`. */
|
|
8
|
+
export const followMode = (value) => (value === "action" || value === "puck" ? value : "off");
|
|
9
|
+
/** The camera's limits by default: no closer than two and a half times, the puck followed at twice, the five nearest players, settling in about a second and a half. */
|
|
10
|
+
export const FOLLOW_LIMITS = { maxZoom: 2.5, puckZoom: 2, near: 5, pad: 0.06, dead: 0.08, omega: 3.2, leash: 0.06 };
|
|
11
|
+
const clamp = (value, least, most) => Math.min(most, Math.max(least, value));
|
|
12
|
+
const isNum = (value) => typeof value === "number" && Number.isFinite(value);
|
|
13
|
+
/** The whole surface: the shot of `off`. */
|
|
14
|
+
export const wholeShot = (box) => ({ x: box.x + box.w / 2, y: box.y + box.h / 2, zoom: 1 });
|
|
15
|
+
/** A shot held to the limits: its zoom from 1 to `maxZoom`, and its frame on the surface (the middle moved in until no side of the frame is off it). */
|
|
16
|
+
export function clampShot(shot, box, maxZoom = FOLLOW_LIMITS.maxZoom) {
|
|
17
|
+
const zoom = clamp(isNum(shot.zoom) ? shot.zoom : 1, 1, Math.max(1, maxZoom));
|
|
18
|
+
const [hw, hh] = [box.w / zoom / 2, box.h / zoom / 2];
|
|
19
|
+
const middle = wholeShot(box);
|
|
20
|
+
return { x: clamp(isNum(shot.x) ? shot.x : middle.x, box.x + hw, box.x + box.w - hw), y: clamp(isNum(shot.y) ? shot.y : middle.y, box.y + hh, box.y + box.h - hh), zoom };
|
|
21
|
+
}
|
|
22
|
+
/** The frame a shot shows: the surface's size over the zoom, round the middle. */
|
|
23
|
+
export const frameOf = (shot, box) => ({ x: shot.x - box.w / shot.zoom / 2, y: shot.y - box.h / shot.zoom / 2, w: box.w / shot.zoom, h: box.h / shot.zoom });
|
|
24
|
+
const onSurface = (p, box) => [clamp(p[0], box.x, box.x + box.w), clamp(p[1], box.y, box.y + box.h)];
|
|
25
|
+
/** The puck or the ball, on the surface, or `undefined` when there is none. */
|
|
26
|
+
function puckOf(input) {
|
|
27
|
+
const puck = input.objects.find((o) => (o.team === "puck" || o.team === "ball") && isNum(o.x) && isNum(o.y));
|
|
28
|
+
return puck === undefined ? undefined : onSurface([puck.x, puck.y], input.box);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The shot the play asks for at a moment. `action` frames the puck (or the ball) and the `near` players nearest it, with `pad` round them, never closer than `maxZoom`: when they bunch at one end
|
|
32
|
+
* it closes in, and when they spread it pulls back. `puck` puts the puck in the middle at `puckZoom`. A stopped play (`stop`) frames the faceoff dot as closely as the limits allow. The frame is
|
|
33
|
+
* always on the surface and always holds the puck. Without a puck the action is the players' middle; with nothing at all it is the whole surface.
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* import { followShot } from "@johnmorrisdotca/kyogijo-stadiums/replay";
|
|
37
|
+
* const box = { x: 0, y: 0, w: 200, h: 85 };
|
|
38
|
+
* const objects = [{ team: "puck" as const, x: 170, y: 40 }, { team: "home" as const, x: 165, y: 30 }, { team: "away" as const, x: 175, y: 50 }];
|
|
39
|
+
* const action = followShot("action", { objects, box });
|
|
40
|
+
* console.log(Math.round(action.x), action.y, Math.round(action.zoom * 100) / 100);
|
|
41
|
+
* console.log(followShot("puck", { objects, box }));
|
|
42
|
+
* // 148 40 1.93
|
|
43
|
+
* // { x: 150, y: 40, zoom: 2 }
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export function followShot(mode, input, limits = {}) {
|
|
47
|
+
const { box } = input;
|
|
48
|
+
const at = { ...FOLLOW_LIMITS, ...limits };
|
|
49
|
+
if (mode === "off" || !(box.w > 0 && box.h > 0))
|
|
50
|
+
return wholeShot(box);
|
|
51
|
+
if (input.stop !== undefined) {
|
|
52
|
+
const [x, y] = onSurface(input.stop, box);
|
|
53
|
+
return clampShot({ x, y, zoom: Math.min(at.maxZoom, mode === "puck" ? at.puckZoom : at.maxZoom) }, box, at.maxZoom);
|
|
54
|
+
}
|
|
55
|
+
const puck = puckOf(input);
|
|
56
|
+
const players = input.objects.filter((o) => (o.team === "home" || o.team === "away") && isNum(o.x) && isNum(o.y)).map((o) => onSurface([o.x, o.y], box));
|
|
57
|
+
if (mode === "puck")
|
|
58
|
+
return puck === undefined ? followShot("action", input, limits) : clampShot({ x: puck[0], y: puck[1], zoom: at.puckZoom }, box, at.maxZoom);
|
|
59
|
+
if (puck === undefined && players.length === 0)
|
|
60
|
+
return wholeShot(box);
|
|
61
|
+
const anchor = puck ?? [players.reduce((s, p) => s + p[0], 0) / players.length, players.reduce((s, p) => s + p[1], 0) / players.length];
|
|
62
|
+
const nearest = [...players].sort((a, b) => Math.hypot(a[0] - anchor[0], a[1] - anchor[1]) - Math.hypot(b[0] - anchor[0], b[1] - anchor[1])).slice(0, Math.max(0, Math.round(at.near)));
|
|
63
|
+
const held = [anchor, ...nearest];
|
|
64
|
+
const xs = held.map((p) => p[0]);
|
|
65
|
+
const ys = held.map((p) => p[1]);
|
|
66
|
+
const pad = Math.max(box.w, box.h) * at.pad;
|
|
67
|
+
const [x0, x1, y0, y1] = [Math.min(...xs) - pad, Math.max(...xs) + pad, Math.min(...ys) - pad, Math.max(...ys) + pad];
|
|
68
|
+
const zoom = Math.min(box.w / (x1 - x0), box.h / (y1 - y0), at.maxZoom);
|
|
69
|
+
return clampShot({ x: (x0 + x1) / 2, y: (y0 + y1) / 2, zoom }, box, at.maxZoom);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* One step of a critically damped spring toward `goal`: where it is and how fast it goes after `dt` seconds. It never overshoots and never rings, and a long step lands nearer, never past.
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* import { springStep } from "@johnmorrisdotca/kyogijo-stadiums/replay";
|
|
76
|
+
* let [x, v] = [0, 0];
|
|
77
|
+
* for (let i = 0; i < 60; i += 1) [x, v] = springStep(x, v, 10, 1 / 60, 4);
|
|
78
|
+
* console.log(Math.round(x * 100) / 100, x < 10);
|
|
79
|
+
* // 9.08 true
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export function springStep(at, speed, goal, dt, omega) {
|
|
83
|
+
if (!(dt > 0))
|
|
84
|
+
return [at, speed];
|
|
85
|
+
const off = at - goal;
|
|
86
|
+
const pull = speed + omega * off;
|
|
87
|
+
const fade = Math.exp(-omega * dt);
|
|
88
|
+
return [goal + (off + pull * dt) * fade, (speed - omega * pull * dt) * fade];
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The camera that follows the play: it eases toward `followShot` with a critically damped spring on the middle and on the zoom (the zoom eased as its logarithm, so a zoom in and a zoom out
|
|
92
|
+
* take the same time), holds still while the shot moves less than a dead zone, and is kept on the surface, never closer than the limit, with the puck inside the frame. When the play is still
|
|
93
|
+
* it comes to rest and stays exactly there.
|
|
94
|
+
* @example
|
|
95
|
+
* ```ts
|
|
96
|
+
* import { followCamera } from "@johnmorrisdotca/kyogijo-stadiums/replay";
|
|
97
|
+
* const camera = followCamera();
|
|
98
|
+
* const input = { box: { x: 0, y: 0, w: 200, h: 85 }, objects: [{ team: "puck" as const, x: 180, y: 40 }] };
|
|
99
|
+
* const first = camera.update("puck", input, 1 / 60);
|
|
100
|
+
* let shot = first;
|
|
101
|
+
* for (let i = 0; i < 600; i += 1) shot = camera.update("puck", input, 1 / 60);
|
|
102
|
+
* console.log(first.zoom < 1.01, shot);
|
|
103
|
+
* // true { x: 150, y: 40, zoom: 2 }
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
export function followCamera(limits = {}) {
|
|
107
|
+
const at = { ...FOLLOW_LIMITS, ...limits };
|
|
108
|
+
let now;
|
|
109
|
+
let goal;
|
|
110
|
+
let speed = [0, 0, 0];
|
|
111
|
+
const settle = (shot) => {
|
|
112
|
+
now = shot;
|
|
113
|
+
goal = shot;
|
|
114
|
+
speed = [0, 0, 0];
|
|
115
|
+
};
|
|
116
|
+
return {
|
|
117
|
+
update(mode, input, dt) {
|
|
118
|
+
const { box } = input;
|
|
119
|
+
const asked = followShot(mode, input, at);
|
|
120
|
+
const from = now ?? wholeShot(box);
|
|
121
|
+
// The dead zone: a shot that moved little from the one the camera is going to is not a new goal.
|
|
122
|
+
if (goal === undefined || mode === "off" || dt === undefined)
|
|
123
|
+
goal = asked;
|
|
124
|
+
else {
|
|
125
|
+
const frame = frameOf(goal, box);
|
|
126
|
+
const moved = Math.abs(asked.x - goal.x) > frame.w * at.dead || Math.abs(asked.y - goal.y) > frame.h * at.dead;
|
|
127
|
+
const zoomed = Math.abs(Math.log(asked.zoom / goal.zoom)) > Math.log(1 + at.dead);
|
|
128
|
+
goal = { x: moved ? asked.x : goal.x, y: moved ? asked.y : goal.y, zoom: zoomed ? asked.zoom : goal.zoom };
|
|
129
|
+
}
|
|
130
|
+
if (dt === undefined) {
|
|
131
|
+
settle(goal);
|
|
132
|
+
return { ...goal };
|
|
133
|
+
}
|
|
134
|
+
const [x, vx] = springStep(from.x, speed[0], goal.x, dt, at.omega);
|
|
135
|
+
const [y, vy] = springStep(from.y, speed[1], goal.y, dt, at.omega);
|
|
136
|
+
const [z, vz] = springStep(Math.log(from.zoom), speed[2], Math.log(goal.zoom), dt, at.omega);
|
|
137
|
+
speed = [vx, vy, vz];
|
|
138
|
+
let next = clampShot({ x, y, zoom: Math.exp(z) }, box, at.maxZoom);
|
|
139
|
+
// The leash: a puck that runs ahead of the easing carries the frame with it, so that it never leaves it; the frame moves as the puck does, never by a jump.
|
|
140
|
+
const puck = mode === "off" ? undefined : puckOf(input);
|
|
141
|
+
if (puck !== undefined) {
|
|
142
|
+
const frame = frameOf(next, box);
|
|
143
|
+
const [mx, my] = [Math.min(frame.w * at.leash, frame.w / 2), Math.min(frame.h * at.leash, frame.h / 2)];
|
|
144
|
+
const pull = (p, middle, half, margin) => (p > middle + half - margin ? p - half + margin : p < middle - half + margin ? p + half - margin : middle);
|
|
145
|
+
next = clampShot({ x: pull(puck[0], next.x, frame.w / 2, mx), y: pull(puck[1], next.y, frame.h / 2, my), zoom: next.zoom }, box, at.maxZoom);
|
|
146
|
+
}
|
|
147
|
+
// At rest: close enough to the goal and slow enough is the goal itself, so that a still play writes nothing more.
|
|
148
|
+
const near = Math.abs(next.x - goal.x) + Math.abs(next.y - goal.y) < Math.max(box.w, box.h) * 1e-4 && Math.abs(Math.log(next.zoom / goal.zoom)) < 1e-4;
|
|
149
|
+
if (near && Math.abs(speed[0]) + Math.abs(speed[1]) < Math.max(box.w, box.h) * 1e-3 && Math.abs(speed[2]) < 1e-3)
|
|
150
|
+
settle(goal);
|
|
151
|
+
else
|
|
152
|
+
now = next;
|
|
153
|
+
return { ...now };
|
|
154
|
+
},
|
|
155
|
+
shot: () => (now === undefined ? undefined : { ...now }),
|
|
156
|
+
resting: () => now === undefined || (now.zoom === 1 && goal !== undefined && goal.zoom === 1 && now.x === goal.x && now.y === goal.y && speed.every((v) => v === 0)),
|
|
157
|
+
reset() {
|
|
158
|
+
now = undefined;
|
|
159
|
+
goal = undefined;
|
|
160
|
+
speed = [0, 0, 0];
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* How much a view magnifies for a shot's zoom, when the view at 1 shows more than the surface (`room` times it: the margin round a flat drawing, the stands round a 3D one). At a zoom of 1 the
|
|
166
|
+
* view is as it was; at `maxZoom` the frame fills it; in between the two are joined smoothly, and the view never shows less than the frame.
|
|
167
|
+
*/
|
|
168
|
+
export function zoomFor(zoom, room, maxZoom = FOLLOW_LIMITS.maxZoom) {
|
|
169
|
+
if (!(zoom > 1) || !(maxZoom > 1))
|
|
170
|
+
return 1;
|
|
171
|
+
return Math.pow(Math.max(1, room) * maxZoom, Math.log(Math.min(zoom, maxZoom)) / Math.log(maxZoom));
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* The part of a flat drawing to show for a shot: a rectangle in the drawing's `viewBox` units, of the viewBox's shape, round the shot's middle, inside the viewBox, and never smaller than the
|
|
175
|
+
* shot's frame. `map` takes a point of the surface's frame to the viewBox's (a surface laid tall is turned a quarter in its drawing).
|
|
176
|
+
* @example
|
|
177
|
+
* ```ts
|
|
178
|
+
* import { followWindow } from "@johnmorrisdotca/kyogijo-stadiums/replay";
|
|
179
|
+
* const box = { x: 0, y: 0, w: 200, h: 85 };
|
|
180
|
+
* const view = { x: -8, y: -8, w: 216, h: 101 };
|
|
181
|
+
* console.log(followWindow({ x: 100, y: 42.5, zoom: 1 }, box, view));
|
|
182
|
+
* console.log(followWindow({ x: 160, y: 42.5, zoom: 2.5 }, box, view));
|
|
183
|
+
* // { x: -8, y: -8, w: 216, h: 101 }
|
|
184
|
+
* // { x: 120, y: 23.8, w: 80, h: 37.41 }
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
export function followWindow(shot, box, view, map = (p) => p, maxZoom = FOLLOW_LIMITS.maxZoom) {
|
|
188
|
+
const span = (points) => {
|
|
189
|
+
const xs = points.map((p) => p[0]);
|
|
190
|
+
const ys = points.map((p) => p[1]);
|
|
191
|
+
return { x: Math.min(...xs), y: Math.min(...ys), w: Math.max(...xs) - Math.min(...xs), h: Math.max(...ys) - Math.min(...ys) };
|
|
192
|
+
};
|
|
193
|
+
const corners = (b) => [[b.x, b.y], [b.x + b.w, b.y], [b.x, b.y + b.h], [b.x + b.w, b.y + b.h]].map((p) => map(p));
|
|
194
|
+
const surface = span(corners(box));
|
|
195
|
+
const by = zoomFor(shot.zoom, surface.w > 0 && surface.h > 0 ? Math.min(view.w / surface.w, view.h / surface.h) : 1, maxZoom);
|
|
196
|
+
const [w, h] = [view.w / by, view.h / by];
|
|
197
|
+
const [cx, cy] = map([shot.x, shot.y]);
|
|
198
|
+
const round = (n) => Math.round(n * 100) / 100;
|
|
199
|
+
return { x: round(clamp(cx - w / 2, view.x, view.x + view.w - w)), y: round(clamp(cy - h / 2, view.y, view.y + view.h - h)), w: round(w), h: round(h) };
|
|
200
|
+
}
|
|
201
|
+
/** The kinds of event that stop the play, and the one that starts it again. */
|
|
202
|
+
const STOPS = ["whistle", "goal"];
|
|
203
|
+
/**
|
|
204
|
+
* The faceoff dot, in the timeline's frame, while the play is stopped at `t`: from a whistle or a goal until the next `faceoff` event that has a place (`x` and `y`), the place of that faceoff.
|
|
205
|
+
* `undefined` while the play goes on, and always for a timeline with no `faceoff` events: the camera then follows the puck through the stoppage as at any other time.
|
|
206
|
+
* @example
|
|
207
|
+
* ```ts
|
|
208
|
+
* import { followStop } from "@johnmorrisdotca/kyogijo-stadiums/replay";
|
|
209
|
+
* const timeline = { frames: [{ t: 0, objects: [] }, { t: 30, objects: [] }], events: [{ t: 10, kind: "whistle" }, { t: 20, kind: "faceoff", x: 69, y: 22 }] };
|
|
210
|
+
* console.log(followStop(timeline, 5), followStop(timeline, 15), followStop(timeline, 25));
|
|
211
|
+
* // undefined [ 69, 22 ] undefined
|
|
212
|
+
* ```
|
|
213
|
+
*/
|
|
214
|
+
export function followStop(timeline, t) {
|
|
215
|
+
const events = replayEvents(timeline);
|
|
216
|
+
let stopped = false;
|
|
217
|
+
for (const e of events) {
|
|
218
|
+
if (e.t > t) {
|
|
219
|
+
if (stopped && e.kind === "faceoff" && isNum(e.x) && isNum(e.y))
|
|
220
|
+
return [e.x, e.y];
|
|
221
|
+
if (stopped && e.kind === "faceoff")
|
|
222
|
+
return undefined;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (STOPS.includes(e.kind))
|
|
226
|
+
stopped = true;
|
|
227
|
+
else if (e.kind === "faceoff")
|
|
228
|
+
stopped = false;
|
|
229
|
+
}
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* The camera of a replay in a page: on each tick of the clock the spring moves by the seconds that went by; a seek, a step, or a change of mode while paused puts the camera where it should be at
|
|
234
|
+
* once (a reader who prefers reduced motion gets only steps); a play, a pause, a change of speed or loop moves nothing. Nothing runs between changes, and off and at rest it does nothing at all.
|
|
235
|
+
*/
|
|
236
|
+
export function followRunner(source) {
|
|
237
|
+
const camera = followCamera(source.limits);
|
|
238
|
+
let last;
|
|
239
|
+
let shown;
|
|
240
|
+
const show = (shot) => {
|
|
241
|
+
const same = shot === undefined ? shown === undefined : shown !== undefined && shot.x === shown.x && shot.y === shown.y && shot.zoom === shown.zoom;
|
|
242
|
+
shown = shot;
|
|
243
|
+
if (!same)
|
|
244
|
+
source.show(shot);
|
|
245
|
+
};
|
|
246
|
+
return {
|
|
247
|
+
paint({ state, reason }) {
|
|
248
|
+
const mode = state.followHeld === true ? "held" : (state.follow ?? "off");
|
|
249
|
+
const tick = reason === "tick";
|
|
250
|
+
if (!tick)
|
|
251
|
+
last = undefined;
|
|
252
|
+
if (mode === "held" || (mode === "off" && camera.resting()))
|
|
253
|
+
return;
|
|
254
|
+
if (!tick && !["seek", "step", "motion", "follow"].includes(reason))
|
|
255
|
+
return;
|
|
256
|
+
// Turned on or off, or resumed, while playing: the ticks ease it there.
|
|
257
|
+
if (reason === "follow" && state.playing)
|
|
258
|
+
return;
|
|
259
|
+
const stamp = source.now();
|
|
260
|
+
const dt = tick ? (last === undefined ? 0 : Math.min(0.1, Math.max(0, (stamp - last) / 1000))) : undefined;
|
|
261
|
+
if (tick)
|
|
262
|
+
last = stamp;
|
|
263
|
+
if (dt === 0)
|
|
264
|
+
return;
|
|
265
|
+
const input = source.at(state.t, mode === "off" ? undefined : followStop(source.timeline, state.t));
|
|
266
|
+
const shot = camera.update(mode, input, dt);
|
|
267
|
+
show(mode === "off" && camera.resting() ? undefined : shot);
|
|
268
|
+
},
|
|
269
|
+
again() {
|
|
270
|
+
if (shown !== undefined)
|
|
271
|
+
source.show(shown);
|
|
272
|
+
},
|
|
273
|
+
reset() {
|
|
274
|
+
camera.reset();
|
|
275
|
+
last = undefined;
|
|
276
|
+
shown = undefined;
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
}
|
package/dist/replay.types.d.ts
CHANGED
|
@@ -117,6 +117,8 @@ export interface ReplaySettings extends ReplayLook {
|
|
|
117
117
|
stepSeconds?: number;
|
|
118
118
|
/** The time to begin at, in seconds from the first frame. 0 by default. */
|
|
119
119
|
start?: number;
|
|
120
|
+
/** The camera follows the play: `action` (the players near the puck or ball), `puck` (the puck or ball), or `off` (the default). See `followShot`. */
|
|
121
|
+
follow?: "off" | "action" | "puck";
|
|
120
122
|
}
|
|
121
123
|
/** A tracked object at a moment, and the trail behind it. */
|
|
122
124
|
export interface ReplayTrail {
|
|
@@ -154,9 +156,12 @@ export interface ReplayState {
|
|
|
154
156
|
loop: boolean;
|
|
155
157
|
/** `step` for a reader who prefers reduced motion: play moves a step, never by itself. */
|
|
156
158
|
motion: "smooth" | "step";
|
|
159
|
+
/** What the camera follows, and whether the reader's own turn or zoom of a 3D view holds it until it is resumed. */
|
|
160
|
+
follow: "off" | "action" | "puck";
|
|
161
|
+
followHeld: boolean;
|
|
157
162
|
}
|
|
158
163
|
/** Why the controller told its listeners. */
|
|
159
|
-
export type ReplayReason = "play" | "pause" | "seek" | "step" | "tick" | "speed" | "loop" | "motion";
|
|
164
|
+
export type ReplayReason = "play" | "pause" | "seek" | "step" | "tick" | "speed" | "loop" | "motion" | "follow";
|
|
160
165
|
/** What a listener of the controller is told. */
|
|
161
166
|
export interface ReplayChange {
|
|
162
167
|
state: Readonly<ReplayState>;
|
package/dist/strings.core.d.ts
CHANGED
|
@@ -1010,6 +1010,16 @@ export interface KyogijoWords {
|
|
|
1010
1010
|
keysTurn: string;
|
|
1011
1011
|
keysTurnStep: string;
|
|
1012
1012
|
region: string;
|
|
1013
|
+
/** The camera that follows the play: the name of its buttons, each button's word and what it does, and the button that lets it follow again after the reader turned the view. */
|
|
1014
|
+
follow: string;
|
|
1015
|
+
followAction: string;
|
|
1016
|
+
followActionLabel: string;
|
|
1017
|
+
followPuck: string;
|
|
1018
|
+
followPuckLabel: string;
|
|
1019
|
+
followBall: string;
|
|
1020
|
+
followBallLabel: string;
|
|
1021
|
+
followResume: string;
|
|
1022
|
+
followResumeLabel: string;
|
|
1013
1023
|
};
|
|
1014
1024
|
/** The words of the 3D view: its name, the keys, the viewpoints, the controls, what is announced, and the names of what stands on the surface. */
|
|
1015
1025
|
turn: {
|
package/dist/strings.en.js
CHANGED
|
@@ -447,6 +447,7 @@ export const WORDS_EN = {
|
|
|
447
447
|
keysTurn: "Replay of the play on the 3D view. Space plays or pauses; comma and full stop step back and on by a frame, and with Shift by 5 seconds; End goes to the end. The arrow keys still turn the view. On the replay's controls the arrow keys step, and Home and End go to the ends.",
|
|
448
448
|
keysTurnStep: "Replay of the play on the 3D view, in steps. Space or full stop moves on, comma goes back, and End goes to the end. The arrow keys still turn the view. On the replay's controls the right arrow key moves on and the left goes back.",
|
|
449
449
|
region: "Replay",
|
|
450
|
+
follow: "Follow the play", followAction: "Action", followActionLabel: "Follow the action: close in on the puck or ball and those nearest it", followPuck: "Puck", followPuckLabel: "Follow the puck", followBall: "Ball", followBallLabel: "Follow the ball", followResume: "Resume", followResumeLabel: "Follow the action again",
|
|
450
451
|
},
|
|
451
452
|
turn: {
|
|
452
453
|
region: "3D view", group: "Viewpoint",
|
package/dist/strings.ja.js
CHANGED
|
@@ -472,6 +472,7 @@ export const WORDS_JA = {
|
|
|
472
472
|
keysTurn: "3Dビューでのプレーのリプレイです。スペースキーで再生と一時停止、カンマキーで1コマ戻り、ピリオドキーで1コマ進みます。Shiftキーを押しながらカンマキーかピリオドキーで5秒ずつ移動し、Endキーで最後に移動します。矢印キーではこれまでどおりビューを回せます。リプレイの操作ボタンにフォーカスがあるときは、左右の矢印キーで1コマずつ前後に移動し、Homeキーで最初、Endキーで最後に移動します。",
|
|
473
473
|
keysTurnStep: "3Dビューでのプレーのリプレイです。コマ送りで表示しています。スペースキーまたはピリオドキーで進み、カンマキーで戻り、Endキーで最後に移動します。矢印キーではこれまでどおりビューを回せます。リプレイの操作ボタンにフォーカスがあるときは、右矢印キーで進み、左矢印キーで戻ります。",
|
|
474
474
|
region: "リプレイ",
|
|
475
|
+
follow: "プレーを追う", followAction: "プレー", followActionLabel: "プレーを追う:パックやボールと、その近くの選手に寄って映す", followPuck: "パック", followPuckLabel: "パックを追う", followBall: "ボール", followBallLabel: "ボールを追う", followResume: "再び追う", followResumeLabel: "もう一度プレーを追う",
|
|
475
476
|
},
|
|
476
477
|
turn: {
|
|
477
478
|
region: "3D表示", group: "視点",
|
package/dist/turn.camera.d.ts
CHANGED
|
@@ -129,9 +129,10 @@ export declare function multiply(a: Mat4, b: Mat4): number[];
|
|
|
129
129
|
/**
|
|
130
130
|
* The matrix that puts a point of the scene on the screen for a view in a placement: the scene (whose middle is the origin) is scaled, turned about the vertical,
|
|
131
131
|
* tipped back, shifted to where the fit puts it, and seen with a perspective `distance` pixels deep. This is `project`'s arithmetic as one matrix, so that an
|
|
132
|
-
* element can be given its whole place in one `matrix3d` and need no 3D context of the browser's: a browser that draws nested 3D layers flat still draws it right.
|
|
132
|
+
* element can be given its whole place in one `matrix3d` and need no 3D context of the browser's: a browser that draws nested 3D layers flat still draws it right. `focus`, a point of the floor
|
|
133
|
+
* in the scene's pixels from its middle, is put where the middle would be (and, by its third number from 0 to 1, that share of the way to the container's middle): the camera looks at it, and turns and tips about it.
|
|
133
134
|
*/
|
|
134
|
-
export declare function cameraMatrix(view: TurnView, at: Placement, distance: number): number[];
|
|
135
|
+
export declare function cameraMatrix(view: TurnView, at: Placement, distance: number, focus?: readonly [number, number] | readonly [number, number, number]): number[];
|
|
135
136
|
/** A matrix as the CSS `transform` that applies it. */
|
|
136
137
|
export declare const matrix3d: (m: Mat4) => string;
|
|
137
138
|
/** How far a point is from the eye's plane after the scene is turned and tipped, in pixels towards the eye: bigger is nearer. Planes are drawn far first. */
|
package/dist/turn.camera.js
CHANGED
|
@@ -208,9 +208,10 @@ const translation = (x, y, z) => [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, x, y, z, 1
|
|
|
208
208
|
/**
|
|
209
209
|
* The matrix that puts a point of the scene on the screen for a view in a placement: the scene (whose middle is the origin) is scaled, turned about the vertical,
|
|
210
210
|
* tipped back, shifted to where the fit puts it, and seen with a perspective `distance` pixels deep. This is `project`'s arithmetic as one matrix, so that an
|
|
211
|
-
* element can be given its whole place in one `matrix3d` and need no 3D context of the browser's: a browser that draws nested 3D layers flat still draws it right.
|
|
211
|
+
* element can be given its whole place in one `matrix3d` and need no 3D context of the browser's: a browser that draws nested 3D layers flat still draws it right. `focus`, a point of the floor
|
|
212
|
+
* in the scene's pixels from its middle, is put where the middle would be (and, by its third number from 0 to 1, that share of the way to the container's middle): the camera looks at it, and turns and tips about it.
|
|
212
213
|
*/
|
|
213
|
-
export function cameraMatrix(view, at, distance) {
|
|
214
|
+
export function cameraMatrix(view, at, distance, focus) {
|
|
214
215
|
const k = at.k * view.zoom;
|
|
215
216
|
const [yaw, pitch] = [view.yaw * RAD, view.pitch * RAD];
|
|
216
217
|
const [cy, sy, cp, sp] = [Math.cos(yaw), Math.sin(yaw), Math.cos(pitch), Math.sin(pitch)];
|
|
@@ -218,10 +219,14 @@ export function cameraMatrix(view, at, distance) {
|
|
|
218
219
|
const rotateZ = [cy, sy, 0, 0, -sy, cy, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
219
220
|
const rotateX = [1, 0, 0, 0, 0, cp, sp, 0, 0, -sp, cp, 0, 0, 0, 0, 1];
|
|
220
221
|
const eye = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, -1 / distance, 0, 0, 0, 1];
|
|
221
|
-
|
|
222
|
+
// A focus is brought toward the middle of the container by its share (the fit puts the scene's middle where its whole box is centred, which a tall scoreboard moves).
|
|
223
|
+
const centring = 1 - Math.min(1, Math.max(0, focus?.[2] ?? 0));
|
|
224
|
+
let m = multiply(eye, translation(at.tx * centring, at.ty * centring, 0));
|
|
222
225
|
m = multiply(m, rotateX);
|
|
223
226
|
m = multiply(m, rotateZ);
|
|
224
|
-
|
|
227
|
+
m = multiply(m, scale);
|
|
228
|
+
// A point of the floor to look at (a camera following the play), in the scene's pixels from its middle: it takes the place the middle had.
|
|
229
|
+
return focus === undefined || (focus[0] === 0 && focus[1] === 0) ? m : multiply(m, translation(-focus[0], -focus[1], 0));
|
|
225
230
|
}
|
|
226
231
|
/**
|
|
227
232
|
* A number of a matrix as CSS writes it: to ten decimals, with no exponent. The perspective terms of a camera matrix are tiny (a few ten-thousandths) and are multiplied by
|
package/dist/turn.dom.d.ts
CHANGED
|
@@ -81,6 +81,11 @@ export declare function turnSupported(win?: {
|
|
|
81
81
|
supports?: (property: string, value: string) => boolean;
|
|
82
82
|
} | null;
|
|
83
83
|
} | undefined): boolean;
|
|
84
|
+
/** Where a camera that follows the play looks: a point of the floor, in the scene's pixels from its middle, and a zoom on top of the view's own. */
|
|
85
|
+
export interface Aim {
|
|
86
|
+
focus: [number, number];
|
|
87
|
+
zoom: number;
|
|
88
|
+
}
|
|
84
89
|
/**
|
|
85
90
|
* Mount a surface as a 3D view in a container, over the static drawing it would otherwise hold: the drawing is the floor, tilted and turned by the pointer, the
|
|
86
91
|
* touch, the wheel and the keys, with what stands on the surface (a net and its posts, a basket, a goal, the posts of a football field) standing on it.
|
package/dist/turn.dom.js
CHANGED
|
@@ -35,6 +35,13 @@ export function turnSupported(win = typeof window === "undefined" ? undefined :
|
|
|
35
35
|
return css.supports("transform", "matrix3d(1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1)");
|
|
36
36
|
}
|
|
37
37
|
const KEYS = { " ": true, ArrowLeft: true, ArrowRight: true, ArrowUp: true, ArrowDown: true, "+": true, "=": true, "-": true, _: true, "0": true, Home: true, "1": true, "2": true, "3": true, "4": true };
|
|
38
|
+
/** The point an aim looks at, brought toward the middle of the view as it closes in: not at all at its zoom of 1, so that turning it on does not move the view. */
|
|
39
|
+
const focusOf = (aim) => (aim === undefined ? undefined : [aim.focus[0], aim.focus[1], 1 - 1 / Math.max(1, aim.zoom)]);
|
|
40
|
+
/** An aim part of the way from one to another (none is the middle at a zoom of 1). */
|
|
41
|
+
const aimBetween = (from, to, t) => {
|
|
42
|
+
const [a, b] = [from ?? { focus: [0, 0], zoom: 1 }, to ?? { focus: [0, 0], zoom: 1 }];
|
|
43
|
+
return { focus: [a.focus[0] + (b.focus[0] - a.focus[0]) * t, a.focus[1] + (b.focus[1] - a.focus[1]) * t], zoom: a.zoom * Math.pow(b.zoom / a.zoom, t) };
|
|
44
|
+
};
|
|
38
45
|
/** The view part of the way from one to another, `t` from 0 to 1, the turn taken the short way round. */
|
|
39
46
|
const between = (from, to, t) => ({ yaw: from.yaw + (nearest(from.yaw, to.yaw) - from.yaw) * t, pitch: from.pitch + (to.pitch - from.pitch) * t, zoom: from.zoom * Math.pow(to.zoom / from.zoom, t) });
|
|
40
47
|
/**
|
|
@@ -176,6 +183,14 @@ export function mountTurn(host, options, settings, build, replayable = false, le
|
|
|
176
183
|
let shown = view;
|
|
177
184
|
let opening = model.view;
|
|
178
185
|
let flight;
|
|
186
|
+
/** What a camera that follows the play looks at (a point of the floor, and a zoom on top of the view's), where it is going in a glide, and who is told when the reader takes the view over. */
|
|
187
|
+
let aim;
|
|
188
|
+
let aimTo;
|
|
189
|
+
const users = new Set();
|
|
190
|
+
const took = (kind) => {
|
|
191
|
+
for (const user of [...users])
|
|
192
|
+
user(kind);
|
|
193
|
+
};
|
|
179
194
|
let spoken;
|
|
180
195
|
// The auto-turn: whether it is on, the frame that will advance it, the time of the last one, and whether it has moved the view since the page was last told.
|
|
181
196
|
let playing = false;
|
|
@@ -273,7 +288,7 @@ export function mountTurn(host, options, settings, build, replayable = false, le
|
|
|
273
288
|
model = decorate === undefined ? plain : decorate(plain);
|
|
274
289
|
ceiling = ceilingFor(model, stage.clientWidth || 960, stage.clientHeight || 600, shown.zoom);
|
|
275
290
|
raster = rasterFor(model);
|
|
276
|
-
const layout = layoutOf(model, shown, stage.clientWidth || 960, stage.clientHeight || 600, settings.lens, false, raster);
|
|
291
|
+
const layout = layoutOf(model, aim === undefined ? shown : { ...shown, zoom: shown.zoom * aim.zoom }, stage.clientWidth || 960, stage.clientHeight || 600, settings.lens, false, raster, focusOf(aim));
|
|
277
292
|
const world = root.querySelector(".ky-turn-world");
|
|
278
293
|
world.innerHTML = worldMarkup(model, layout, raster);
|
|
279
294
|
// The look the floor is painted in (and the theme) are attributes of the world.
|
|
@@ -342,10 +357,11 @@ export function mountTurn(host, options, settings, build, replayable = false, le
|
|
|
342
357
|
settling = next === ceiling ? undefined : win.setTimeout(settleRaster, next > ceiling ? 160 : 1500);
|
|
343
358
|
};
|
|
344
359
|
/** Put a view on the screen. */
|
|
345
|
-
const paint = (
|
|
360
|
+
const paint = (asked) => {
|
|
346
361
|
const width = stage.clientWidth || 960;
|
|
347
362
|
const height = stage.clientHeight || 600;
|
|
348
|
-
const
|
|
363
|
+
const now = aim === undefined ? asked : { ...asked, zoom: asked.zoom * aim.zoom };
|
|
364
|
+
const layout = layoutOf(model, now, width, height, settings.lens, true, raster, focusOf(aim));
|
|
349
365
|
outside?.zoom(now.zoom);
|
|
350
366
|
const culled = layout.culled;
|
|
351
367
|
elements.forEach((one, i) => {
|
|
@@ -389,11 +405,16 @@ export function mountTurn(host, options, settings, build, replayable = false, le
|
|
|
389
405
|
};
|
|
390
406
|
/** Go to a view: by a glide of `ms` milliseconds, or at once. */
|
|
391
407
|
const show = (to, ms) => {
|
|
408
|
+
// A camera that follows the play and is asked to let go glides back with the view (and goes on doing so when another glide takes over).
|
|
409
|
+
const aiming = aimTo !== undefined ? { from: aim, to: aimTo.to } : flight?.aim === undefined ? undefined : { from: aim, to: flight.aim.to };
|
|
410
|
+
aimTo = undefined;
|
|
392
411
|
stop();
|
|
393
412
|
view = to;
|
|
394
413
|
label();
|
|
395
414
|
if (ms <= 0 || calm() || win === null) {
|
|
396
415
|
shown = to;
|
|
416
|
+
if (aiming !== undefined)
|
|
417
|
+
aim = aiming.to;
|
|
397
418
|
paint(shown);
|
|
398
419
|
return;
|
|
399
420
|
}
|
|
@@ -404,13 +425,15 @@ export function mountTurn(host, options, settings, build, replayable = false, le
|
|
|
404
425
|
const t = Math.min(1, (stamp - flight.start) / flight.ms);
|
|
405
426
|
const eased = 1 - Math.pow(1 - t, 3);
|
|
406
427
|
shown = t >= 1 ? flight.to : between(flight.from, flight.to, eased);
|
|
428
|
+
if (flight.aim !== undefined)
|
|
429
|
+
aim = t >= 1 ? flight.aim.to : aimBetween(flight.aim.from, flight.aim.to, eased);
|
|
407
430
|
paint(shown);
|
|
408
431
|
if (t < 1)
|
|
409
432
|
flight.frame = win.requestAnimationFrame(run);
|
|
410
433
|
else
|
|
411
434
|
flight = undefined;
|
|
412
435
|
};
|
|
413
|
-
flight = { from, to, start: win.performance.now(), ms, frame: 0 };
|
|
436
|
+
flight = { from, to, start: win.performance.now(), ms, frame: 0, aim: aiming };
|
|
414
437
|
flight.frame = win.requestAnimationFrame(run);
|
|
415
438
|
};
|
|
416
439
|
const say = (text, wait = 0) => {
|
|
@@ -543,6 +566,12 @@ export function mountTurn(host, options, settings, build, replayable = false, le
|
|
|
543
566
|
if (target === null || target === undefined || !root.contains(target))
|
|
544
567
|
return;
|
|
545
568
|
const name = target.dataset.preset;
|
|
569
|
+
const act = target.dataset.act ?? "";
|
|
570
|
+
if (name !== undefined || act === "reset")
|
|
571
|
+
took("preset");
|
|
572
|
+
// (The replay's own buttons are in the view too: only the view's turn it.)
|
|
573
|
+
else if (act !== "" && act !== "play" && act !== "reverse")
|
|
574
|
+
took("turn");
|
|
546
575
|
if (name !== undefined)
|
|
547
576
|
return toPreset(name);
|
|
548
577
|
const big = event.shiftKey === true;
|
|
@@ -569,6 +598,8 @@ export function mountTurn(host, options, settings, build, replayable = false, le
|
|
|
569
598
|
const yaw = big ? STEP.yawBig : STEP.yaw;
|
|
570
599
|
const pitch = big ? STEP.pitchBig : STEP.pitch;
|
|
571
600
|
event.preventDefault();
|
|
601
|
+
if (event.key !== " ")
|
|
602
|
+
took(/^[0-4]$|^Home$/.test(event.key) ? "preset" : "turn");
|
|
572
603
|
switch (event.key) {
|
|
573
604
|
// A key takes the surface toward its side as a drag toward that side does: the side nearest the eye goes to the right on ArrowRight, and ArrowUp tips it as a drag up does.
|
|
574
605
|
case "ArrowLeft": return go(nudge(view, "left", yaw), 120);
|
|
@@ -629,9 +660,13 @@ export function mountTurn(host, options, settings, build, replayable = false, le
|
|
|
629
660
|
}
|
|
630
661
|
else {
|
|
631
662
|
view = drag(view, here.x - before.x, here.y - before.y, tilts(event.pointerType));
|
|
632
|
-
if (Math.abs(here.x - before.x) + Math.abs(here.y - before.y) > 0)
|
|
663
|
+
if (Math.abs(here.x - before.x) + Math.abs(here.y - before.y) > 0 && !moved) {
|
|
633
664
|
moved = true;
|
|
665
|
+
took("turn");
|
|
666
|
+
}
|
|
634
667
|
}
|
|
668
|
+
if (pinch !== undefined)
|
|
669
|
+
took("turn");
|
|
635
670
|
shown = view;
|
|
636
671
|
paint(shown);
|
|
637
672
|
};
|
|
@@ -655,7 +690,11 @@ export function mountTurn(host, options, settings, build, replayable = false, le
|
|
|
655
690
|
stage.addEventListener("pointermove", onMove);
|
|
656
691
|
stage.addEventListener("pointerup", onUp);
|
|
657
692
|
stage.addEventListener("pointercancel", onUp);
|
|
658
|
-
|
|
693
|
+
const onDouble = () => {
|
|
694
|
+
took("preset");
|
|
695
|
+
toOpening();
|
|
696
|
+
};
|
|
697
|
+
stage.addEventListener("dblclick", onDouble);
|
|
659
698
|
// ---- The wheel over the view zooms it, as a map does; with the pointer anywhere else the page scrolls. Ctrl or Command and the wheel (a pinch on a trackpad) zoom too. Shift and the
|
|
660
699
|
// wheel, or a sideways swipe, turn it. Tipping is a drag, the keys and the buttons. A wheel that starts while the zoom is already at its end that way goes on to scroll the page, so the page is
|
|
661
700
|
// never trapped; one that reaches the end part-way through stops there rather than flinging the page. ----
|
|
@@ -667,6 +706,7 @@ export function mountTurn(host, options, settings, build, replayable = false, le
|
|
|
667
706
|
if (!pinch && (event.shiftKey || Math.abs(event.deltaX) > Math.abs(event.deltaY))) {
|
|
668
707
|
event.preventDefault();
|
|
669
708
|
wheelTaken = now;
|
|
709
|
+
took("turn");
|
|
670
710
|
grab();
|
|
671
711
|
view = clampView({ ...view, yaw: view.yaw + (event.deltaX || event.deltaY) * 0.25 });
|
|
672
712
|
}
|
|
@@ -680,6 +720,7 @@ export function mountTurn(host, options, settings, build, replayable = false, le
|
|
|
680
720
|
wheelTaken = now;
|
|
681
721
|
if (next.zoom === view.zoom)
|
|
682
722
|
return;
|
|
723
|
+
took("turn");
|
|
683
724
|
grab();
|
|
684
725
|
view = next;
|
|
685
726
|
}
|
|
@@ -721,6 +762,21 @@ export function mountTurn(host, options, settings, build, replayable = false, le
|
|
|
721
762
|
if (alive)
|
|
722
763
|
paint(shown);
|
|
723
764
|
},
|
|
765
|
+
aim(next, glide = false) {
|
|
766
|
+
if (!alive)
|
|
767
|
+
return;
|
|
768
|
+
if (glide) {
|
|
769
|
+
aimTo = { to: next };
|
|
770
|
+
// (A glide of the view that is about to start carries it; otherwise it glides alone.)
|
|
771
|
+
show(view, 560);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
aim = next;
|
|
775
|
+
},
|
|
776
|
+
onUser(listener) {
|
|
777
|
+
users.add(listener);
|
|
778
|
+
return () => void users.delete(listener);
|
|
779
|
+
},
|
|
724
780
|
requestFrame,
|
|
725
781
|
cancelFrame,
|
|
726
782
|
});
|
|
@@ -864,6 +920,7 @@ export function mountTurn(host, options, settings, build, replayable = false, le
|
|
|
864
920
|
probe?.stop();
|
|
865
921
|
frames.clear();
|
|
866
922
|
seen?.disconnect();
|
|
923
|
+
users.clear();
|
|
867
924
|
doc.removeEventListener("visibilitychange", onVisible);
|
|
868
925
|
clearTimeout(spoken);
|
|
869
926
|
observer?.disconnect();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ReplayController } from "./replay.control.ts";
|
|
2
|
+
import { type FollowShot } from "./replay.follow.ts";
|
|
3
|
+
import type { ReplayChange, ReplayLook, ReplayTimeline } from "./replay.types.ts";
|
|
4
|
+
import type { Aim } from "./turn.dom.ts";
|
|
5
|
+
import type { TurnModel } from "./turn.model.ts";
|
|
6
|
+
import type { TurnInternals } from "./turn.replay.dom.ts";
|
|
7
|
+
import type { ScoreOptions, SurfaceOptions } from "./types.ts";
|
|
8
|
+
/**
|
|
9
|
+
* What a 3D view looks at for a shot: the shot's middle as a point of the floor in the scene's pixels from its middle, and the zoom on top of the view's. The scene at a zoom of 1 holds more
|
|
10
|
+
* than the surface (the boards, the stands): the zoom is worked out against how much more, so that the closest shot frames the same share of the surface as a flat drawing's.
|
|
11
|
+
*/
|
|
12
|
+
export declare function aimOf(shot: FollowShot, model: Pick<TurnModel, "scale" | "centre" | "box">, surface: {
|
|
13
|
+
w: number;
|
|
14
|
+
h: number;
|
|
15
|
+
}): Aim;
|
|
16
|
+
/** What `turnFollow` gives back: the paint the replay calls on each change of its clock, and `destroy`. */
|
|
17
|
+
export interface TurnFollow {
|
|
18
|
+
paint(change: ReplayChange): void;
|
|
19
|
+
destroy(): void;
|
|
20
|
+
}
|
|
21
|
+
/** The camera of a replay on a 3D view. `floor` is the options the floor is drawn with (the replay's), or `undefined` before it is laid. */
|
|
22
|
+
export declare function turnFollow(view: TurnInternals, controller: () => ReplayController, timeline: ReplayTimeline, look: ReplayLook, floor: () => SurfaceOptions | ScoreOptions | undefined): TurnFollow;
|