@johnmorrisdotca/kyogijo-stadiums 0.58.0 → 0.60.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/CHANGELOG.md +12 -0
- package/README.md +2 -0
- package/catalogue.json +1 -1
- package/dist/athletics/plan.d.ts +2 -0
- package/dist/athletics/plan.js +20 -4
- package/dist/catalogue.data.js +1 -1
- package/dist/crowd.d.ts +18 -0
- package/dist/crowd.js +88 -18
- package/dist/draw.js +22 -7
- 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/surfaces/artistic.js +3 -1
- package/dist/surfaces/athletics.js +2 -0
- package/dist/surfaces/hockey.js +3 -1
- package/dist/surfaces/surface.d.ts +7 -0
- package/dist/surfaces/surface.js +9 -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 +14 -3
- 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,164 @@
|
|
|
1
|
+
import type { ReplayChange, ReplayObject, ReplayTimeline } from "./replay.types.ts";
|
|
2
|
+
/** What the camera follows: nothing (the whole surface), the action (the players near the puck or ball, and it), or the puck or ball alone. */
|
|
3
|
+
export type FollowMode = "off" | "action" | "puck";
|
|
4
|
+
/** The modes, in the order the controls list them after "off". */
|
|
5
|
+
export declare const FOLLOW_MODES: readonly FollowMode[];
|
|
6
|
+
/** A mode from anything: `action` and `puck` are themselves, everything else is `off`. */
|
|
7
|
+
export declare const followMode: (value: unknown) => FollowMode;
|
|
8
|
+
/** A rectangle in the drawing's own feet. */
|
|
9
|
+
export interface FollowBox {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
w: number;
|
|
13
|
+
h: number;
|
|
14
|
+
}
|
|
15
|
+
/** Where the camera looks: the middle of the frame, in the drawing's feet, and how much it magnifies the surface (1 is the whole of it: the frame is the surface's size over `zoom`). */
|
|
16
|
+
export interface FollowShot {
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
zoom: number;
|
|
20
|
+
}
|
|
21
|
+
/** How the camera frames and moves. Shares are of the surface's length (its longer side). */
|
|
22
|
+
export interface FollowLimits {
|
|
23
|
+
/** The closest the camera comes: the frame is never smaller than the surface over this. */
|
|
24
|
+
maxZoom: number;
|
|
25
|
+
/** The zoom of `puck`. */
|
|
26
|
+
puckZoom: number;
|
|
27
|
+
/** How many of the players nearest the puck the action is framed round. */
|
|
28
|
+
near: number;
|
|
29
|
+
/** The room left round the action, a share of the surface's length on each side. */
|
|
30
|
+
pad: number;
|
|
31
|
+
/** A move of the shot smaller than this share of the frame does not move the camera, and a zoom that changes by less than this share does not zoom it. */
|
|
32
|
+
dead: number;
|
|
33
|
+
/** How quickly the spring follows, in radians a second: about 4.7 over this many seconds settles it. */
|
|
34
|
+
omega: number;
|
|
35
|
+
/** The room kept between the puck and the edge of the frame, a share of the frame. */
|
|
36
|
+
leash: number;
|
|
37
|
+
}
|
|
38
|
+
/** 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. */
|
|
39
|
+
export declare const FOLLOW_LIMITS: Readonly<FollowLimits>;
|
|
40
|
+
/** What the shot is worked out from at a moment: the objects in the drawing's feet, the surface, and the faceoff dot when the play is stopped. */
|
|
41
|
+
export interface FollowInput {
|
|
42
|
+
objects: readonly Pick<ReplayObject, "team" | "x" | "y">[];
|
|
43
|
+
box: FollowBox;
|
|
44
|
+
/** The faceoff dot while the play is stopped (see `followStop`): the camera goes there. */
|
|
45
|
+
stop?: readonly [number, number];
|
|
46
|
+
}
|
|
47
|
+
/** The whole surface: the shot of `off`. */
|
|
48
|
+
export declare const wholeShot: (box: FollowBox) => FollowShot;
|
|
49
|
+
/** 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). */
|
|
50
|
+
export declare function clampShot(shot: FollowShot, box: FollowBox, maxZoom?: number): FollowShot;
|
|
51
|
+
/** The frame a shot shows: the surface's size over the zoom, round the middle. */
|
|
52
|
+
export declare const frameOf: (shot: FollowShot, box: FollowBox) => FollowBox;
|
|
53
|
+
/**
|
|
54
|
+
* 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
|
|
55
|
+
* 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
|
|
56
|
+
* 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.
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* import { followShot } from "@johnmorrisdotca/kyogijo-stadiums/replay";
|
|
60
|
+
* const box = { x: 0, y: 0, w: 200, h: 85 };
|
|
61
|
+
* 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 }];
|
|
62
|
+
* const action = followShot("action", { objects, box });
|
|
63
|
+
* console.log(Math.round(action.x), action.y, Math.round(action.zoom * 100) / 100);
|
|
64
|
+
* console.log(followShot("puck", { objects, box }));
|
|
65
|
+
* // 148 40 1.93
|
|
66
|
+
* // { x: 150, y: 40, zoom: 2 }
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export declare function followShot(mode: FollowMode, input: FollowInput, limits?: Partial<FollowLimits>): FollowShot;
|
|
70
|
+
/**
|
|
71
|
+
* 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.
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* import { springStep } from "@johnmorrisdotca/kyogijo-stadiums/replay";
|
|
75
|
+
* let [x, v] = [0, 0];
|
|
76
|
+
* for (let i = 0; i < 60; i += 1) [x, v] = springStep(x, v, 10, 1 / 60, 4);
|
|
77
|
+
* console.log(Math.round(x * 100) / 100, x < 10);
|
|
78
|
+
* // 9.08 true
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export declare function springStep(at: number, speed: number, goal: number, dt: number, omega: number): [number, number];
|
|
82
|
+
/** What `followCamera` gives back. */
|
|
83
|
+
export interface FollowCamera {
|
|
84
|
+
/** Move toward the shot the play asks for: by `dt` seconds of the spring, or at once (`dt` undefined). Gives where it is now. */
|
|
85
|
+
update(mode: FollowMode, input: FollowInput, dt?: number): FollowShot;
|
|
86
|
+
/** Where it is now. */
|
|
87
|
+
shot(): FollowShot | undefined;
|
|
88
|
+
/** Whether it is at rest on the whole surface: `off`, and nothing left to ease back. */
|
|
89
|
+
resting(): boolean;
|
|
90
|
+
/** Forget where it was: the next update starts from the whole surface. */
|
|
91
|
+
reset(): void;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* 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
|
|
95
|
+
* 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
|
|
96
|
+
* it comes to rest and stays exactly there.
|
|
97
|
+
* @example
|
|
98
|
+
* ```ts
|
|
99
|
+
* import { followCamera } from "@johnmorrisdotca/kyogijo-stadiums/replay";
|
|
100
|
+
* const camera = followCamera();
|
|
101
|
+
* const input = { box: { x: 0, y: 0, w: 200, h: 85 }, objects: [{ team: "puck" as const, x: 180, y: 40 }] };
|
|
102
|
+
* const first = camera.update("puck", input, 1 / 60);
|
|
103
|
+
* let shot = first;
|
|
104
|
+
* for (let i = 0; i < 600; i += 1) shot = camera.update("puck", input, 1 / 60);
|
|
105
|
+
* console.log(first.zoom < 1.01, shot);
|
|
106
|
+
* // true { x: 150, y: 40, zoom: 2 }
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export declare function followCamera(limits?: Partial<FollowLimits>): FollowCamera;
|
|
110
|
+
/**
|
|
111
|
+
* 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
|
|
112
|
+
* 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.
|
|
113
|
+
*/
|
|
114
|
+
export declare function zoomFor(zoom: number, room: number, maxZoom?: number): number;
|
|
115
|
+
/**
|
|
116
|
+
* 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
|
|
117
|
+
* 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).
|
|
118
|
+
* @example
|
|
119
|
+
* ```ts
|
|
120
|
+
* import { followWindow } from "@johnmorrisdotca/kyogijo-stadiums/replay";
|
|
121
|
+
* const box = { x: 0, y: 0, w: 200, h: 85 };
|
|
122
|
+
* const view = { x: -8, y: -8, w: 216, h: 101 };
|
|
123
|
+
* console.log(followWindow({ x: 100, y: 42.5, zoom: 1 }, box, view));
|
|
124
|
+
* console.log(followWindow({ x: 160, y: 42.5, zoom: 2.5 }, box, view));
|
|
125
|
+
* // { x: -8, y: -8, w: 216, h: 101 }
|
|
126
|
+
* // { x: 120, y: 23.8, w: 80, h: 37.41 }
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
export declare function followWindow(shot: FollowShot, box: FollowBox, view: FollowBox, map?: (p: [number, number]) => [number, number], maxZoom?: number): FollowBox;
|
|
130
|
+
/**
|
|
131
|
+
* 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.
|
|
132
|
+
* `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.
|
|
133
|
+
* @example
|
|
134
|
+
* ```ts
|
|
135
|
+
* import { followStop } from "@johnmorrisdotca/kyogijo-stadiums/replay";
|
|
136
|
+
* const timeline = { frames: [{ t: 0, objects: [] }, { t: 30, objects: [] }], events: [{ t: 10, kind: "whistle" }, { t: 20, kind: "faceoff", x: 69, y: 22 }] };
|
|
137
|
+
* console.log(followStop(timeline, 5), followStop(timeline, 15), followStop(timeline, 25));
|
|
138
|
+
* // undefined [ 69, 22 ] undefined
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
export declare function followStop(timeline: ReplayTimeline, t: number): [number, number] | undefined;
|
|
142
|
+
/** What a runner is given: the timeline, where its objects are in the drawing at a time, the surface, a clock in milliseconds, and how to show a shot (`undefined` gives back the whole drawing). */
|
|
143
|
+
export interface FollowSource {
|
|
144
|
+
timeline: ReplayTimeline;
|
|
145
|
+
/** The objects at a time and the faceoff dot of a stoppage, in the drawing's feet. */
|
|
146
|
+
at(t: number, stop: readonly [number, number] | undefined): FollowInput;
|
|
147
|
+
now(): number;
|
|
148
|
+
show(shot: FollowShot | undefined): void;
|
|
149
|
+
limits?: Partial<FollowLimits>;
|
|
150
|
+
}
|
|
151
|
+
/** What `followRunner` gives back. */
|
|
152
|
+
export interface FollowRunner {
|
|
153
|
+
/** The replay's clock changed: move the camera for it. */
|
|
154
|
+
paint(change: ReplayChange): void;
|
|
155
|
+
/** Show the shot again (the drawing was written again). */
|
|
156
|
+
again(): void;
|
|
157
|
+
/** Forget the shot: the camera starts from the whole surface. */
|
|
158
|
+
reset(): void;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* 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
|
|
162
|
+
* 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.
|
|
163
|
+
*/
|
|
164
|
+
export declare function followRunner(source: FollowSource): FollowRunner;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ReplayController } from "./replay.control.ts";
|
|
2
|
+
import { type FollowRunner } from "./replay.follow.ts";
|
|
3
|
+
import type { ReplayLook, ReplayState, ReplayTimeline } from "./replay.types.ts";
|
|
4
|
+
import type { KyogijoWords } from "./strings.core.ts";
|
|
5
|
+
import type { ScoreOptions, SurfaceOptions } from "./types.ts";
|
|
6
|
+
/** The buttons' own rules, added to the replay's stylesheet: a word beside each icon. */
|
|
7
|
+
export declare const FOLLOW_STYLE: string;
|
|
8
|
+
/** The buttons of the follow camera, for the replay's bar, and how to make them say the state. */
|
|
9
|
+
export interface FollowControls {
|
|
10
|
+
parts: HTMLElement[];
|
|
11
|
+
sync(state: Readonly<ReplayState>): void;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The buttons: Action and Puck (Ball for a game with a ball) press in and out, one at a time, and pressing the one that is in turns the camera off (held or not); Resume shows only while the
|
|
15
|
+
* reader's own turn or zoom holds the camera.
|
|
16
|
+
*/
|
|
17
|
+
export declare function followControls(doc: Document, words: KyogijoWords["replay"], controller: ReplayController, timeline: ReplayTimeline): FollowControls;
|
|
18
|
+
/** What the flat camera is given: the drawing now, what it was drawn from, the timeline and how its frames are drawn. */
|
|
19
|
+
export interface FlatFollowSource {
|
|
20
|
+
svg(): SVGSVGElement | null;
|
|
21
|
+
options(): SurfaceOptions | ScoreOptions;
|
|
22
|
+
timeline: ReplayTimeline;
|
|
23
|
+
look: ReplayLook;
|
|
24
|
+
now(): number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The camera of a flat replay: it shows the part of the drawing the shot frames by writing the drawing's `viewBox`, one attribute, and puts the whole drawing back when it is off. A drawing
|
|
28
|
+
* written again (a theme, a score) is shown at the same shot by `again`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function flatFollow(source: FlatFollowSource): FollowRunner;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Following the play in a page: the buttons in the replay's bar (Action, Puck or Ball, and Resume), and the camera of a flat replay, which shows part of the drawing by its `viewBox`. The
|
|
2
|
+
// arithmetic is replay.follow.ts's; the 3D view's camera is turn.follow.ts's.
|
|
3
|
+
import { outlineOf } from "./outline.js";
|
|
4
|
+
import { FOLLOW_LIMITS, followRunner, followWindow } from "./replay.follow.js";
|
|
5
|
+
import { replayPlacer, replaySnapshot } from "./replay.js";
|
|
6
|
+
/** The buttons' own rules, added to the replay's stylesheet: a word beside each icon. */
|
|
7
|
+
export const FOLLOW_STYLE = `
|
|
8
|
+
.ky-replay .ky-follow{display:inline-flex;flex-wrap:wrap;gap:.5rem}
|
|
9
|
+
.ky-replay .ky-follow button{gap:.35rem}
|
|
10
|
+
.ky-replay .ky-follow svg{fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
|
11
|
+
`.trim();
|
|
12
|
+
const SHAPES = {
|
|
13
|
+
action: "M3 8V3h5M16 3h5v5M21 16v5h-5M8 21H3v-5M9 12h6M12 9v6",
|
|
14
|
+
puck: "M12 3v3M12 18v3M3 12h3M18 12h3M12 8a4 4 0 1 0 0.01 0",
|
|
15
|
+
resume: "M4 12a8 8 0 1 0 2.3-5.7M4 4v5h5",
|
|
16
|
+
};
|
|
17
|
+
/** What the timeline follows: a puck, a ball, or neither (then there is no Puck button). */
|
|
18
|
+
const followed = (timeline) => {
|
|
19
|
+
for (const frame of timeline.frames)
|
|
20
|
+
for (const o of frame.objects)
|
|
21
|
+
if (o.team === "puck" || o.team === "ball")
|
|
22
|
+
return o.team;
|
|
23
|
+
return undefined;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* The buttons: Action and Puck (Ball for a game with a ball) press in and out, one at a time, and pressing the one that is in turns the camera off (held or not); Resume shows only while the
|
|
27
|
+
* reader's own turn or zoom holds the camera.
|
|
28
|
+
*/
|
|
29
|
+
export function followControls(doc, words, controller, timeline) {
|
|
30
|
+
const group = doc.createElement("span");
|
|
31
|
+
group.className = "ky-follow";
|
|
32
|
+
group.setAttribute("role", "group");
|
|
33
|
+
group.setAttribute("aria-label", words.follow);
|
|
34
|
+
const button = (name, word, label, onClick) => {
|
|
35
|
+
const b = doc.createElement("button");
|
|
36
|
+
b.type = "button";
|
|
37
|
+
b.className = `ky-follow-${name}`;
|
|
38
|
+
b.setAttribute("aria-label", label);
|
|
39
|
+
b.title = label;
|
|
40
|
+
b.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="${SHAPES[name]}"/></svg><span>${word}</span>`;
|
|
41
|
+
b.addEventListener("click", onClick);
|
|
42
|
+
return b;
|
|
43
|
+
};
|
|
44
|
+
const press = (mode) => () => controller.setFollow(controller.state().follow === mode ? "off" : mode);
|
|
45
|
+
const ball = followed(timeline);
|
|
46
|
+
const action = button("action", words.followAction, words.followActionLabel, press("action"));
|
|
47
|
+
const puck = button("puck", ball === "ball" ? words.followBall : words.followPuck, ball === "ball" ? words.followBallLabel : words.followPuckLabel, press("puck"));
|
|
48
|
+
puck.hidden = ball === undefined;
|
|
49
|
+
const resume = button("resume", words.followResume, words.followResumeLabel, () => controller.holdFollow(false));
|
|
50
|
+
group.append(action, puck, resume);
|
|
51
|
+
return {
|
|
52
|
+
parts: [group],
|
|
53
|
+
sync(state) {
|
|
54
|
+
action.setAttribute("aria-pressed", String(state.follow === "action"));
|
|
55
|
+
puck.setAttribute("aria-pressed", String(state.follow === "puck"));
|
|
56
|
+
resume.hidden = !state.followHeld;
|
|
57
|
+
group.dataset.follow = state.followHeld ? "held" : state.follow;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** The matrix that takes the replay's layers to the drawing's own units: a surface laid tall is turned a quarter inside its drawing. */
|
|
62
|
+
function layersToDrawing(svg) {
|
|
63
|
+
const layers = svg.querySelector("g.ky-layers");
|
|
64
|
+
let m = { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 };
|
|
65
|
+
for (let el = layers?.parentElement ?? null; el !== null && el !== svg; el = el.parentElement) {
|
|
66
|
+
const t = el.transform?.baseVal?.consolidate?.()?.matrix;
|
|
67
|
+
if (t === undefined || t === null)
|
|
68
|
+
continue;
|
|
69
|
+
// The outer transform after the inner: t times m.
|
|
70
|
+
m = { a: t.a * m.a + t.c * m.b, b: t.b * m.a + t.d * m.b, c: t.a * m.c + t.c * m.d, d: t.b * m.c + t.d * m.d, e: t.a * m.e + t.c * m.f + t.e, f: t.b * m.e + t.d * m.f + t.f };
|
|
71
|
+
}
|
|
72
|
+
return ([x, y]) => [m.a * x + m.c * y + m.e, m.b * x + m.d * y + m.f];
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The camera of a flat replay: it shows the part of the drawing the shot frames by writing the drawing's `viewBox`, one attribute, and puts the whole drawing back when it is off. A drawing
|
|
76
|
+
* written again (a theme, a score) is shown at the same shot by `again`.
|
|
77
|
+
*/
|
|
78
|
+
export function flatFollow(source) {
|
|
79
|
+
const whole = new WeakMap();
|
|
80
|
+
const own = (svg) => {
|
|
81
|
+
let known = whole.get(svg);
|
|
82
|
+
if (known === undefined) {
|
|
83
|
+
const text = svg.getAttribute("viewBox");
|
|
84
|
+
const [x, y, w, h] = (text ?? "").split(/[\s,]+/).map(Number);
|
|
85
|
+
known = { view: [x, y, w, h].every(Number.isFinite) ? { x, y, w, h } : { x: 0, y: 0, w: 0, h: 0 }, text, map: layersToDrawing(svg) };
|
|
86
|
+
whole.set(svg, known);
|
|
87
|
+
}
|
|
88
|
+
return known;
|
|
89
|
+
};
|
|
90
|
+
const box = () => outlineOf(source.options()).box;
|
|
91
|
+
return followRunner({
|
|
92
|
+
timeline: source.timeline,
|
|
93
|
+
now: source.now,
|
|
94
|
+
at(t, stop) {
|
|
95
|
+
const place = replayPlacer(source.options(), source.timeline);
|
|
96
|
+
const objects = replaySnapshot(source.timeline, t, source.look).objects;
|
|
97
|
+
const placed = place(objects.map((o) => [o.x, o.y]));
|
|
98
|
+
return { box: box(), objects: objects.map((o, i) => ({ team: o.team, x: placed[i][0], y: placed[i][1] })), stop: stop === undefined ? undefined : place([[stop[0], stop[1]]])[0] };
|
|
99
|
+
},
|
|
100
|
+
show(shot) {
|
|
101
|
+
const svg = source.svg();
|
|
102
|
+
if (svg === null)
|
|
103
|
+
return;
|
|
104
|
+
const known = own(svg);
|
|
105
|
+
if (shot === undefined || !(known.view.w > 0)) {
|
|
106
|
+
if (known.text === null)
|
|
107
|
+
svg.removeAttribute("viewBox");
|
|
108
|
+
else
|
|
109
|
+
svg.setAttribute("viewBox", known.text);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const w = followWindow(shot, box(), known.view, known.map, FOLLOW_LIMITS.maxZoom);
|
|
113
|
+
const text = `${w.x} ${w.y} ${w.w} ${w.h}`;
|
|
114
|
+
if (svg.getAttribute("viewBox") !== text)
|
|
115
|
+
svg.setAttribute("viewBox", text);
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
@@ -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: "視点",
|