@johnmorrisdotca/kyogijo-stadiums 0.51.0 → 0.52.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 +30 -2
- package/catalogue.json +1 -1
- package/dist/catalogue.data.js +1 -1
- package/dist/element.d.ts +1 -0
- package/dist/element.js +16 -5
- package/dist/judging.mount.js +1 -0
- package/dist/replay.d.ts +4 -0
- package/dist/replay.dom.d.ts +67 -2
- package/dist/replay.dom.js +187 -114
- package/dist/replay.js +10 -1
- package/dist/strings.core.d.ts +3 -0
- package/dist/strings.en.js +2 -0
- package/dist/strings.ja.js +2 -0
- package/dist/turn-entry.d.ts +4 -0
- package/dist/turn-entry.js +2 -0
- package/dist/turn.dom.d.ts +22 -1
- package/dist/turn.dom.js +121 -8
- package/dist/turn.kit.d.ts +1 -1
- package/dist/turn.replay.d.ts +56 -0
- package/dist/turn.replay.dom.d.ts +23 -0
- package/dist/turn.replay.dom.js +139 -0
- package/dist/turn.replay.js +82 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -1
package/dist/turn.dom.js
CHANGED
|
@@ -12,6 +12,20 @@ import { turnBoardMarkup } from "./overlay.js";
|
|
|
12
12
|
import { attachOutside3d } from "./outside.dom.js";
|
|
13
13
|
import { TIER_PROFILES, createGovernor, handheld, layerBudget, qualityOf, rasterOf, rasterShares, tierOf, viewShare, FLOOR_SHARE } from "./quality.js";
|
|
14
14
|
import { size } from "./turn.kit.js";
|
|
15
|
+
/**
|
|
16
|
+
* The replay's own code, which `turn.replay.dom.ts` puts in when it is loaded (the `/turn` entry and the element load it): a page that only turns a view (a district, a tournament layout) carries none of the replay
|
|
17
|
+
* and its layers.
|
|
18
|
+
*/
|
|
19
|
+
let replayKit;
|
|
20
|
+
/** Put the replay's code in: called once by `turn.replay.dom.ts`. */
|
|
21
|
+
export function useTurnReplay(kit) {
|
|
22
|
+
replayKit = kit;
|
|
23
|
+
}
|
|
24
|
+
const kitOf = () => {
|
|
25
|
+
if (replayKit === undefined)
|
|
26
|
+
throw new Error("the replay is not loaded: import mountKyogijoTurn from @johnmorrisdotca/kyogijo-stadiums/turn");
|
|
27
|
+
return replayKit;
|
|
28
|
+
};
|
|
15
29
|
/** True where the page can draw a plane in 3D by one `matrix3d`: that is, nearly everywhere with a stylesheet. */
|
|
16
30
|
export function turnSupported(win = typeof window === "undefined" ? undefined : window) {
|
|
17
31
|
const css = win?.CSS;
|
|
@@ -34,13 +48,13 @@ const between = (from, to, t) => ({ yaw: from.yaw + (nearest(from.yaw, to.yaw) -
|
|
|
34
48
|
* ```
|
|
35
49
|
*/
|
|
36
50
|
export function mountKyogijoTurn(host, options, settings = {}) {
|
|
37
|
-
return mountTurn(host, options, settings, buildTurn);
|
|
51
|
+
return mountTurn(host, options, settings, buildTurn, true);
|
|
38
52
|
}
|
|
39
53
|
/**
|
|
40
54
|
* Mount any 3D model (`mountKyogijoTurn` is this with the model of one surface; `mountMultiTurn` with a tournament layout's): `build` makes the model from what `update` is given, and the rest, the controls, the pointer, the keys
|
|
41
55
|
* and the auto-turn, is the same.
|
|
42
56
|
*/
|
|
43
|
-
export function mountTurn(host, options, settings, build) {
|
|
57
|
+
export function mountTurn(host, options, settings, build, replayable = false) {
|
|
44
58
|
const doc = host.ownerDocument;
|
|
45
59
|
const win = doc.defaultView;
|
|
46
60
|
/** The venue's settings as they are now: the mounted ones, altered by `update`. */
|
|
@@ -71,6 +85,10 @@ export function mountTurn(host, options, settings, build) {
|
|
|
71
85
|
};
|
|
72
86
|
const sameRaster = (a, b) => (typeof a === "number" || typeof b === "number" ? a === b : a.length === b.length && a.every((x, i) => x === b[i]));
|
|
73
87
|
let current = options;
|
|
88
|
+
/** What a replay lays on a model (its floor at the replay's time, and the players standing on it), and what it does once the world is drawn again: both unset without a replay. */
|
|
89
|
+
let decorate;
|
|
90
|
+
let rendered;
|
|
91
|
+
let replaying;
|
|
74
92
|
let model = build(options, { ...settings, quality: tier });
|
|
75
93
|
if (model.tiered !== true)
|
|
76
94
|
tier = "high";
|
|
@@ -89,7 +107,12 @@ export function mountTurn(host, options, settings, build) {
|
|
|
89
107
|
let alive = true;
|
|
90
108
|
if (!supported) {
|
|
91
109
|
// The flat drawing, with a sentence that says why it does not turn when the browser is the reason.
|
|
110
|
+
let flatReplay;
|
|
92
111
|
const flat = (next) => {
|
|
112
|
+
if (flatReplay !== undefined) {
|
|
113
|
+
flatReplay.view.draw(floorOptions(next));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
93
116
|
host.innerHTML = `<div class="ky-turn" data-mode="flat"><style>${KYOGIJO_TURN_STYLE}</style>${model.flat ?? drawKyogijo({ ...floorOptions(next), board: next.board })}${model.turnable ? `<p class="ky-turn-flat">${esc(turnWords(model).flat)}</p>` : ""}</div>`;
|
|
94
117
|
};
|
|
95
118
|
flat(options);
|
|
@@ -111,12 +134,33 @@ export function mountTurn(host, options, settings, build) {
|
|
|
111
134
|
update: (next, change) => {
|
|
112
135
|
if (change?.venue !== undefined)
|
|
113
136
|
venue = { ...(typeof venue === "object" ? venue : {}), ...change.venue };
|
|
137
|
+
current = next;
|
|
114
138
|
model = build(next, { ...settings, id: settings.id ?? model.id, venue });
|
|
115
139
|
flat(next);
|
|
116
140
|
},
|
|
117
141
|
svg: () => (alive ? host.querySelector("svg.kyogijo") : null),
|
|
142
|
+
// The flat replay, where the 3D view cannot be drawn.
|
|
143
|
+
replay(timeline, replaySettings = {}) {
|
|
144
|
+
if (!replayable)
|
|
145
|
+
throw new Error("a replay plays on one surface, not on a layout");
|
|
146
|
+
flatReplay?.view.destroy();
|
|
147
|
+
const view = kitOf().flat(host, timeline, replaySettings, floorOptions(current));
|
|
148
|
+
const mine = { view, timeline, settings: replaySettings };
|
|
149
|
+
flatReplay = mine;
|
|
150
|
+
return {
|
|
151
|
+
...view.controller,
|
|
152
|
+
destroy() {
|
|
153
|
+
if (flatReplay !== mine)
|
|
154
|
+
return;
|
|
155
|
+
view.destroy();
|
|
156
|
+
flatReplay = undefined;
|
|
157
|
+
flat(current);
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
},
|
|
118
161
|
destroy: () => {
|
|
119
162
|
alive = false;
|
|
163
|
+
flatReplay?.view.destroy();
|
|
120
164
|
host.replaceChildren();
|
|
121
165
|
},
|
|
122
166
|
};
|
|
@@ -239,9 +283,11 @@ export function mountTurn(host, options, settings, build) {
|
|
|
239
283
|
};
|
|
240
284
|
/** Draw the world again, from the model of the tier and the options now: the camera, the auto-turn and the pointer are not touched (the elements are the only thing replaced). */
|
|
241
285
|
const renderWorld = () => {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
made.
|
|
286
|
+
// A model with a replay on it is made fresh each time, at the replay's time: only a plain one is kept.
|
|
287
|
+
const known = decorate === undefined ? made.get(tier) : undefined;
|
|
288
|
+
const plain = known ?? (decorate === undefined ? undefined : made.get(tier)) ?? build(current, { ...settings, quality: tier, id: settings.id ?? baseId, venue, preset: undefined, view });
|
|
289
|
+
made.set(tier, plain);
|
|
290
|
+
model = decorate === undefined ? plain : decorate(plain);
|
|
245
291
|
ceiling = ceilingFor(model, stage.clientWidth || 960, stage.clientHeight || 600, shown.zoom);
|
|
246
292
|
raster = rasterFor(model);
|
|
247
293
|
const layout = layoutOf(model, shown, stage.clientWidth || 960, stage.clientHeight || 600, settings.lens, false, raster);
|
|
@@ -263,6 +309,7 @@ export function mountTurn(host, options, settings, build) {
|
|
|
263
309
|
root.dataset.quality = quality;
|
|
264
310
|
root.dataset.tier = tier;
|
|
265
311
|
root.dataset.share = String(ceiling);
|
|
312
|
+
rendered?.();
|
|
266
313
|
};
|
|
267
314
|
/** The governor stepped: draw at the new tier. */
|
|
268
315
|
const retier = (next) => {
|
|
@@ -419,11 +466,36 @@ export function mountTurn(host, options, settings, build) {
|
|
|
419
466
|
go(opening, 560, "none");
|
|
420
467
|
say(turnWords(model).reset);
|
|
421
468
|
};
|
|
469
|
+
// ---- One frame request a frame: the auto-turn and a replay on the view ask through this, and share one animation frame when both run. ----
|
|
470
|
+
const queued = new Map();
|
|
471
|
+
let queueId = 0;
|
|
472
|
+
let pendingFrame = 0;
|
|
473
|
+
const requestFrame = (callback) => {
|
|
474
|
+
queueId += 1;
|
|
475
|
+
queued.set(queueId, callback);
|
|
476
|
+
if (pendingFrame === 0 && win !== null) {
|
|
477
|
+
pendingFrame = win.requestAnimationFrame((stamp) => {
|
|
478
|
+
pendingFrame = 0;
|
|
479
|
+
const run = [...queued.values()];
|
|
480
|
+
queued.clear();
|
|
481
|
+
for (const one of run)
|
|
482
|
+
one(stamp);
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
return queueId;
|
|
486
|
+
};
|
|
487
|
+
const cancelFrame = (id) => {
|
|
488
|
+
queued.delete(id);
|
|
489
|
+
if (queued.size === 0 && pendingFrame !== 0) {
|
|
490
|
+
win?.cancelAnimationFrame(pendingFrame);
|
|
491
|
+
pendingFrame = 0;
|
|
492
|
+
}
|
|
493
|
+
};
|
|
422
494
|
// ---- The auto-turn: a trophy case's slow turn. It runs only when asked, holds while the pointer holds the view, and does nothing while the page is hidden. ----
|
|
423
495
|
const hidden = () => doc.hidden === true;
|
|
424
496
|
const stopTick = () => {
|
|
425
497
|
if (tickFrame !== undefined)
|
|
426
|
-
|
|
498
|
+
cancelFrame(tickFrame);
|
|
427
499
|
tickFrame = undefined;
|
|
428
500
|
lastTick = undefined;
|
|
429
501
|
};
|
|
@@ -455,7 +527,7 @@ export function mountTurn(host, options, settings, build) {
|
|
|
455
527
|
};
|
|
456
528
|
const loop = () => {
|
|
457
529
|
if (tickFrame === undefined && playing && alive && win !== null)
|
|
458
|
-
tickFrame =
|
|
530
|
+
tickFrame = requestFrame(tick);
|
|
459
531
|
};
|
|
460
532
|
/** Start or stop the auto-turn. `quiet` leaves the live region alone (a key or a drag stopped it, and says its own thing). */
|
|
461
533
|
const setPlaying = (next, quiet = false) => {
|
|
@@ -531,7 +603,7 @@ export function mountTurn(host, options, settings, build) {
|
|
|
531
603
|
root.addEventListener("click", onClick);
|
|
532
604
|
// ---- Keys. ----
|
|
533
605
|
const onKey = (event) => {
|
|
534
|
-
if (event.ctrlKey || event.metaKey || event.altKey || KEYS[event.key] !== true)
|
|
606
|
+
if (event.defaultPrevented || event.ctrlKey || event.metaKey || event.altKey || KEYS[event.key] !== true)
|
|
535
607
|
return;
|
|
536
608
|
const big = event.shiftKey;
|
|
537
609
|
const yaw = big ? STEP.yawBig : STEP.yaw;
|
|
@@ -764,10 +836,51 @@ export function mountTurn(host, options, settings, build) {
|
|
|
764
836
|
return last === undefined ? undefined : { ms: last.ms, people: last.stats.people, mode: last.stats.mode, pixels: last.pixels };
|
|
765
837
|
},
|
|
766
838
|
svg: () => (alive ? root.querySelector(".ky-turn-floor svg.kyogijo") : null),
|
|
839
|
+
replay(timeline, replaySettings = {}) {
|
|
840
|
+
if (!alive)
|
|
841
|
+
throw new Error("this view was destroyed");
|
|
842
|
+
if (!replayable)
|
|
843
|
+
throw new Error("a replay plays on one surface, not on a layout");
|
|
844
|
+
replaying?.destroy();
|
|
845
|
+
const internals = {
|
|
846
|
+
doc,
|
|
847
|
+
root,
|
|
848
|
+
stage,
|
|
849
|
+
model: () => model,
|
|
850
|
+
setDecorate(next, after) {
|
|
851
|
+
decorate = next;
|
|
852
|
+
rendered = after;
|
|
853
|
+
// (A view that is going away is not drawn again.)
|
|
854
|
+
if (!alive)
|
|
855
|
+
return;
|
|
856
|
+
renderWorld();
|
|
857
|
+
paint(shown);
|
|
858
|
+
},
|
|
859
|
+
repaint: () => {
|
|
860
|
+
if (alive)
|
|
861
|
+
paint(shown);
|
|
862
|
+
},
|
|
863
|
+
requestFrame,
|
|
864
|
+
cancelFrame,
|
|
865
|
+
};
|
|
866
|
+
const mine = kitOf().attach(internals, timeline, replaySettings);
|
|
867
|
+
replaying = mine;
|
|
868
|
+
return {
|
|
869
|
+
...mine,
|
|
870
|
+
destroy() {
|
|
871
|
+
if (replaying !== mine)
|
|
872
|
+
return;
|
|
873
|
+
replaying = undefined;
|
|
874
|
+
mine.destroy();
|
|
875
|
+
},
|
|
876
|
+
};
|
|
877
|
+
},
|
|
767
878
|
destroy() {
|
|
768
879
|
if (!alive)
|
|
769
880
|
return;
|
|
770
881
|
alive = false;
|
|
882
|
+
replaying?.destroy();
|
|
883
|
+
replaying = undefined;
|
|
771
884
|
clearTimeout(settling);
|
|
772
885
|
outside?.destroy();
|
|
773
886
|
stop();
|
package/dist/turn.kit.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** A point or a direction: x, y, z in feet. */
|
|
2
2
|
export type V3 = readonly [number, number, number];
|
|
3
3
|
/** What a plane is a part of: it is the class of its element (`ky-up-<kind>`) and what a test finds it by. */
|
|
4
|
-
export type PartKind = "mast" | "post" | "net" | "tape" | "antenna" | "board" | "glass" | "rim" | "stanchion" | "frame" | "goal" | "flag" | "stump" | "leg" | "table" | "pylon" | "wall" | "screen" | "pole" | "ground" | "target" | "seats" | "concourse" | "fascia" | "roof" | "banner" | "platform" | "rail" | "matedge" | "shrine" | "surround" | "chair" | "desk" | "card" | "sign" | "apron" | "bench" | "floor" | "ribbon" | "hung-board" | "block";
|
|
4
|
+
export type PartKind = "mast" | "post" | "net" | "tape" | "antenna" | "board" | "glass" | "rim" | "stanchion" | "frame" | "goal" | "flag" | "stump" | "leg" | "table" | "pylon" | "wall" | "screen" | "pole" | "ground" | "target" | "seats" | "concourse" | "fascia" | "roof" | "banner" | "platform" | "rail" | "matedge" | "shrine" | "surround" | "chair" | "desk" | "card" | "sign" | "apron" | "bench" | "floor" | "ribbon" | "hung-board" | "block" | "player";
|
|
5
5
|
/** One flat rectangle. Its own x runs along `u` (its whole length) and its own y along `v`; `o` is the corner both start from. `svg` is what is drawn on it, in feet along `u` and `v`. */
|
|
6
6
|
export interface Plane {
|
|
7
7
|
/** A name that says what it is and where: `net`, `post-left`, `goal-right-back`. */
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type ReplayGroup } from "./replay.ts";
|
|
2
|
+
import type { ReplayLook, ReplayObject, ReplayTeam, ReplayTimeline } from "./replay.types.ts";
|
|
3
|
+
import { type Plane } from "./turn.kit.ts";
|
|
4
|
+
import type { TurnModel } from "./turn.model.ts";
|
|
5
|
+
import type { ScoreOptions, SurfaceOptions } from "./types.ts";
|
|
6
|
+
/** Who stands up on a 3D view: the players and the officials. The ball and the puck stay on the floor. */
|
|
7
|
+
export declare const STANDING: readonly ReplayTeam[];
|
|
8
|
+
/** Whether a group of the replay stays on the floor of a 3D view: the trails, the passes, the shots, the goals and the ball do, and the players, their numbers and the officials stand up. */
|
|
9
|
+
export declare const keepOnFloor: (group: Pick<ReplayGroup, "id">) => boolean;
|
|
10
|
+
/** The settings a 3D replay takes beyond the flat replay's look: `stand: false` keeps the players flat on the floor, as the flat replay draws them. */
|
|
11
|
+
export interface TurnReplayLook extends ReplayLook {
|
|
12
|
+
stand?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The size of a disc, in feet: a share of the playing surface's diagonal (the area the view is fitted to, not the ground or the building round it), so that it reads on a rink and on a pitch
|
|
16
|
+
* alike (about 6.5 feet on a hockey rink, 12 on a soccer pitch), times the look's `scale`. It is a mark, not a person: its size says nothing of a player's height.
|
|
17
|
+
*/
|
|
18
|
+
export declare function discSize(area: {
|
|
19
|
+
w: number;
|
|
20
|
+
h: number;
|
|
21
|
+
}, look?: ReplayLook): number;
|
|
22
|
+
/** The playing surface of a model, in feet: what stands on it says, else the whole floor. */
|
|
23
|
+
export declare const playingArea: (model: Pick<TurnModel, "viewBox" | "uprights">) => {
|
|
24
|
+
w: number;
|
|
25
|
+
h: number;
|
|
26
|
+
};
|
|
27
|
+
/** The custom properties a floor drawing sets on its root (the teams' colours among them): the discs stand outside that SVG and take the same. */
|
|
28
|
+
export declare function floorVars(floor: string): string;
|
|
29
|
+
/** What is drawn on a disc of `d` feet: the flat replay's mark for the side, and the number over it, in the drawing's own classes so that they take its colours, light and dark. */
|
|
30
|
+
export declare function discMarkup(o: Pick<ReplayObject, "team" | "number">, d: number, sport: string, theme: string | undefined, vars: string): string;
|
|
31
|
+
/** The plane of one disc: a billboard that leans to face the eye, its lower edge on the floor when it stands, its middle over (x, y). */
|
|
32
|
+
export declare function discPlane(o: Pick<ReplayObject, "id" | "team" | "number">, x: number, y: number, d: number, lift: number, svg: string): Plane;
|
|
33
|
+
/** Put a disc over a new place on the floor, as `discPlane` does: the plane is the caller's, changed in place, so that a page moving many does not make new ones. */
|
|
34
|
+
export declare function moveDisc(plane: Plane, x: number, y: number, d: number, lift: number): void;
|
|
35
|
+
/** A replay laid on a 3D view's model: what `replayTurnModel` gives back. */
|
|
36
|
+
export interface TurnReplayModel {
|
|
37
|
+
model: TurnModel;
|
|
38
|
+
/** The disc of each one standing, by id, and whether it is on the surface at the time. */
|
|
39
|
+
discs: Map<string, {
|
|
40
|
+
plane: Plane;
|
|
41
|
+
on: boolean;
|
|
42
|
+
}>;
|
|
43
|
+
/** The disc's size, in feet. */
|
|
44
|
+
size: number;
|
|
45
|
+
/** The options the floor was drawn with, its `id` among them. */
|
|
46
|
+
floorOptions: SurfaceOptions | ScoreOptions;
|
|
47
|
+
}
|
|
48
|
+
/** The floor's options for a replay: the model's own, and an id of its own, so that the replay's groups are found in it. */
|
|
49
|
+
export declare const replayFloorOptions: (model: Pick<TurnModel, "id" | "options">) => SurfaceOptions | ScoreOptions;
|
|
50
|
+
/**
|
|
51
|
+
* A 3D view's model with a replay on it at a time: the floor drawn as the replay's frame (without the players when they stand), and a disc for each player and official the timeline
|
|
52
|
+
* tracks, after the model's own planes. One that is not on the surface at the time is still a plane, and says so (`on`), so that the elements stay the same while it plays. Pure.
|
|
53
|
+
*/
|
|
54
|
+
export declare function replayTurnModel(model: TurnModel, timeline: ReplayTimeline, t: number, look?: TurnReplayLook): TurnReplayModel;
|
|
55
|
+
/** A number for a test or a page that wants to know where a disc stands: its middle on the floor, in feet. */
|
|
56
|
+
export declare const discAt: (plane: Plane) => string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { KyogijoReplay } from "./mount.ts";
|
|
2
|
+
import type { ReplaySettings, ReplayTimeline } from "./replay.types.ts";
|
|
3
|
+
import type { TurnModel } from "./turn.model.ts";
|
|
4
|
+
import { type TurnReplayLook } from "./turn.replay.ts";
|
|
5
|
+
/** What a 3D view lends a replay: its elements, its model, a way to lay the replay on the model, and a way to paint the view again. */
|
|
6
|
+
export interface TurnInternals {
|
|
7
|
+
doc: Document;
|
|
8
|
+
/** The view's root (`.ky-turn`) and the element that turns (`.ky-turn-view`). */
|
|
9
|
+
root: HTMLElement;
|
|
10
|
+
stage: HTMLElement;
|
|
11
|
+
model(): TurnModel;
|
|
12
|
+
/** Lay a replay on every model the view makes from now on (`undefined` takes it off), draw the world again with it, and call `after` once it is drawn, then and each time after. */
|
|
13
|
+
setDecorate(decorate: ((model: TurnModel) => TurnModel) | undefined, after?: () => void): void;
|
|
14
|
+
/** Paint the view as it is now: the planes whose place changed are written, the others are not touched. */
|
|
15
|
+
repaint(): void;
|
|
16
|
+
/** The view's own frame requests, which the auto-turn shares: one animation frame a frame, whatever runs. */
|
|
17
|
+
requestFrame(callback: FrameRequestCallback): number;
|
|
18
|
+
cancelFrame(id: number): void;
|
|
19
|
+
}
|
|
20
|
+
/** Settings of a replay on a 3D view: the flat replay's, and `stand`. */
|
|
21
|
+
export type TurnReplaySettings = ReplaySettings & TurnReplayLook;
|
|
22
|
+
/** Attach a replay to a 3D view. The controller it gives back has `destroy`, which takes the replay away and leaves the view as it was. */
|
|
23
|
+
export declare function attachTurnReplay(view: TurnInternals, timeline: ReplayTimeline, settings: TurnReplaySettings): KyogijoReplay;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { createReplayView, replayDriver, replayPainter } from "./replay.dom.js";
|
|
2
|
+
import { useTurnReplay } from "./turn.dom.js";
|
|
3
|
+
import { drawFrameOf, replayPlacer, replaySnapshot } from "./replay.js";
|
|
4
|
+
import { keepOnFloor, moveDisc, replayTurnModel } from "./turn.replay.js";
|
|
5
|
+
/** Attach a replay to a 3D view. The controller it gives back has `destroy`, which takes the replay away and leaves the view as it was. */
|
|
6
|
+
export function attachTurnReplay(view, timeline, settings) {
|
|
7
|
+
const { doc, root, stage } = view;
|
|
8
|
+
const look = settings;
|
|
9
|
+
let language = view.model().language;
|
|
10
|
+
let laid;
|
|
11
|
+
let floorSvg = null;
|
|
12
|
+
/** The element of each disc, and whether it was last shown. */
|
|
13
|
+
let discs = new Map();
|
|
14
|
+
let placedAt = Number.NaN;
|
|
15
|
+
const painter = replayPainter({
|
|
16
|
+
doc,
|
|
17
|
+
timeline,
|
|
18
|
+
look,
|
|
19
|
+
svg: () => floorSvg,
|
|
20
|
+
options: () => laid?.floorOptions ?? view.model().options,
|
|
21
|
+
language: () => language,
|
|
22
|
+
keep: look.stand === false ? undefined : keepOnFloor,
|
|
23
|
+
redraw: (t) => {
|
|
24
|
+
// The floor alone is drawn again (a mark that cannot be matched): the planes round it stay.
|
|
25
|
+
if (floorSvg === null || laid === undefined)
|
|
26
|
+
return;
|
|
27
|
+
const scratch = doc.createElement("div");
|
|
28
|
+
scratch.innerHTML = drawFrameOf(laid.floorOptions, timeline, t, look, look.stand === false ? () => true : keepOnFloor);
|
|
29
|
+
const fresh = scratch.querySelector("svg.kyogijo");
|
|
30
|
+
if (fresh === null)
|
|
31
|
+
return;
|
|
32
|
+
floorSvg.replaceWith(fresh);
|
|
33
|
+
floorSvg = fresh;
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
/** Move the discs to a time: a plane's place changes, and an element is shown or hidden only when that changes. */
|
|
37
|
+
const place = (t) => {
|
|
38
|
+
if (laid === undefined || t === placedAt)
|
|
39
|
+
return;
|
|
40
|
+
placedAt = t;
|
|
41
|
+
const snap = replaySnapshot(timeline, t, look);
|
|
42
|
+
const placed = replayPlacer(laid.floorOptions, timeline)(snap.objects.map((o) => [o.x, o.y]));
|
|
43
|
+
const here = new Map();
|
|
44
|
+
snap.objects.forEach((o, i) => here.set(o.id, placed[i]));
|
|
45
|
+
const lift = view.model().uprights.lift;
|
|
46
|
+
for (const [id, disc] of laid.discs) {
|
|
47
|
+
const at = here.get(id);
|
|
48
|
+
if (at !== undefined)
|
|
49
|
+
moveDisc(disc.plane, at[0], at[1], laid.size, lift);
|
|
50
|
+
const node = discs.get(id);
|
|
51
|
+
if (node?.el !== null && node !== undefined && node.shown !== (at !== undefined)) {
|
|
52
|
+
node.shown = at !== undefined;
|
|
53
|
+
node.el.style.visibility = node.shown ? "" : "hidden";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const wrap = doc.createElement("div");
|
|
58
|
+
wrap.className = "ky-replay ky-turn-replay";
|
|
59
|
+
const driver = replayDriver(doc, timeline, settings, {
|
|
60
|
+
paint: (t) => {
|
|
61
|
+
painter.paint(t);
|
|
62
|
+
place(t);
|
|
63
|
+
view.repaint();
|
|
64
|
+
},
|
|
65
|
+
language,
|
|
66
|
+
keysOn: wrap,
|
|
67
|
+
holder: wrap,
|
|
68
|
+
seen: stage,
|
|
69
|
+
help: "turn",
|
|
70
|
+
frames: { request: view.requestFrame, cancel: view.cancelFrame },
|
|
71
|
+
});
|
|
72
|
+
const { style, hint, bar, live } = driver.parts;
|
|
73
|
+
wrap.append(style, hint, bar, live);
|
|
74
|
+
const controls = root.querySelector(".ky-turn-controls");
|
|
75
|
+
if (controls !== null)
|
|
76
|
+
controls.after(wrap);
|
|
77
|
+
else
|
|
78
|
+
root.append(wrap);
|
|
79
|
+
// The view's own description, and the replay's keys after it.
|
|
80
|
+
const described = stage.getAttribute("aria-describedby");
|
|
81
|
+
stage.setAttribute("aria-describedby", `${described ?? ""} ${hint.id}`.trim());
|
|
82
|
+
const shortcuts = stage.getAttribute("aria-keyshortcuts");
|
|
83
|
+
stage.setAttribute("aria-keyshortcuts", `${shortcuts ?? ""} , . Shift+, Shift+. End`.trim());
|
|
84
|
+
// The keys of the view itself: Space plays and pauses the replay (the Auto-turn button still turns the view), comma and full stop step, End goes to the end. The arrows stay the view's.
|
|
85
|
+
const onStageKey = (event) => {
|
|
86
|
+
const target = event.target;
|
|
87
|
+
if (target === null || (target !== stage && !stage.contains(target)))
|
|
88
|
+
return;
|
|
89
|
+
driver.key(event, "stage");
|
|
90
|
+
};
|
|
91
|
+
root.addEventListener("keydown", onStageKey, true);
|
|
92
|
+
// Each time the view draws its world (now, and again after `update` or a change of quality), the model has the replay at the clock's time: the elements are found again.
|
|
93
|
+
const after = () => {
|
|
94
|
+
language = view.model().language;
|
|
95
|
+
driver.setLanguage(language);
|
|
96
|
+
floorSvg = root.querySelector(".ky-turn-floor > svg.kyogijo");
|
|
97
|
+
discs = new Map();
|
|
98
|
+
if (laid !== undefined) {
|
|
99
|
+
const byPart = new Map([...root.querySelectorAll(".ky-up-player")].map((el) => [el.dataset.part ?? "", el]));
|
|
100
|
+
for (const [id, disc] of laid.discs) {
|
|
101
|
+
const el = byPart.get(disc.plane.id) ?? null;
|
|
102
|
+
if (el !== null && !disc.on)
|
|
103
|
+
el.style.visibility = "hidden";
|
|
104
|
+
discs.set(id, { el, shown: disc.on });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const t = driver.controller.state().t;
|
|
108
|
+
placedAt = t;
|
|
109
|
+
painter.bind(t);
|
|
110
|
+
};
|
|
111
|
+
view.setDecorate((model) => {
|
|
112
|
+
laid = replayTurnModel(model, timeline, driver.controller.state().t, look);
|
|
113
|
+
return laid.model;
|
|
114
|
+
}, after);
|
|
115
|
+
if (settings.autoplay === true && driver.controller.state().motion === "smooth" && doc.visibilityState !== "hidden")
|
|
116
|
+
driver.controller.play();
|
|
117
|
+
return {
|
|
118
|
+
...driver.controller,
|
|
119
|
+
destroy() {
|
|
120
|
+
driver.destroy();
|
|
121
|
+
root.removeEventListener("keydown", onStageKey, true);
|
|
122
|
+
if (described === null)
|
|
123
|
+
stage.removeAttribute("aria-describedby");
|
|
124
|
+
else
|
|
125
|
+
stage.setAttribute("aria-describedby", described);
|
|
126
|
+
if (shortcuts === null)
|
|
127
|
+
stage.removeAttribute("aria-keyshortcuts");
|
|
128
|
+
else
|
|
129
|
+
stage.setAttribute("aria-keyshortcuts", shortcuts);
|
|
130
|
+
wrap.remove();
|
|
131
|
+
laid = undefined;
|
|
132
|
+
discs = new Map();
|
|
133
|
+
floorSvg = null;
|
|
134
|
+
view.setDecorate(undefined);
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
// The view's `replay` plays with this file's code once it is loaded.
|
|
139
|
+
useTurnReplay({ attach: attachTurnReplay, flat: createReplayView });
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// A replay on the 3D view, as data: the floor is the replay's frame with the ball or the puck, its trail and the events on it, and every player and official stands up on it as a
|
|
2
|
+
// disc that faces the eye (a circle for the home side, a square for the away side, a triangle for an official, each with its number), the marks of the flat replay stood up. Pure:
|
|
3
|
+
// the same model, timeline, time and look give the same model. `turn.replay.dom.ts` moves it in a page.
|
|
4
|
+
import { esc, num } from "./format.js";
|
|
5
|
+
import { drawFrameOf, replayCast, replayPlacer, replaySnapshot } from "./replay.js";
|
|
6
|
+
import { n } from "./turn.kit.js";
|
|
7
|
+
/** Who stands up on a 3D view: the players and the officials. The ball and the puck stay on the floor. */
|
|
8
|
+
export const STANDING = ["home", "away", "official"];
|
|
9
|
+
/** What stands up is in these groups of the flat replay; the 3D view's floor draws the rest (`keepOnFloor`). */
|
|
10
|
+
const STOOD = new Set(["replay-players", "replay-numbers", "replay-officials"]);
|
|
11
|
+
/** Whether a group of the replay stays on the floor of a 3D view: the trails, the passes, the shots, the goals and the ball do, and the players, their numbers and the officials stand up. */
|
|
12
|
+
export const keepOnFloor = (group) => !STOOD.has(group.id);
|
|
13
|
+
/**
|
|
14
|
+
* The size of a disc, in feet: a share of the playing surface's diagonal (the area the view is fitted to, not the ground or the building round it), so that it reads on a rink and on a pitch
|
|
15
|
+
* alike (about 6.5 feet on a hockey rink, 12 on a soccer pitch), times the look's `scale`. It is a mark, not a person: its size says nothing of a player's height.
|
|
16
|
+
*/
|
|
17
|
+
export function discSize(area, look = {}) {
|
|
18
|
+
const scale = typeof look.scale === "number" && Number.isFinite(look.scale) && look.scale > 0 ? look.scale : 1;
|
|
19
|
+
return Math.hypot(area.w, area.h) * 0.03 * scale;
|
|
20
|
+
}
|
|
21
|
+
/** The playing surface of a model, in feet: what stands on it says, else the whole floor. */
|
|
22
|
+
export const playingArea = (model) => {
|
|
23
|
+
const area = model.uprights.area;
|
|
24
|
+
return area === undefined ? model.viewBox : { w: area.x1 - area.x0, h: area.y1 - area.y0 };
|
|
25
|
+
};
|
|
26
|
+
/** The custom properties a floor drawing sets on its root (the teams' colours among them): the discs stand outside that SVG and take the same. */
|
|
27
|
+
export function floorVars(floor) {
|
|
28
|
+
const style = /^<svg[^>]*\sstyle="([^"]*)"/.exec(floor)?.[1];
|
|
29
|
+
return style === undefined ? "" : style.split(";").filter((rule) => /^--ky-[\w-]+:/.test(rule)).join(";");
|
|
30
|
+
}
|
|
31
|
+
/** What is drawn on a disc of `d` feet: the flat replay's mark for the side, and the number over it, in the drawing's own classes so that they take its colours, light and dark. */
|
|
32
|
+
export function discMarkup(o, d, sport, theme, vars) {
|
|
33
|
+
const r = d / 2;
|
|
34
|
+
const edge = d * 0.06;
|
|
35
|
+
const shape = o.team === "home"
|
|
36
|
+
? `<circle class="ky-h" cx="${n(r)}" cy="${n(r)}" r="${n(r - edge)}"/>`
|
|
37
|
+
: o.team === "away"
|
|
38
|
+
? `<rect class="ky-a" x="${n(edge)}" y="${n(edge)}" width="${n(d - 2 * edge)}" height="${n(d - 2 * edge)}" rx="${n(d * 0.08)}"/>`
|
|
39
|
+
: `<path d="M${n(r)} ${n(edge)}L${n(d - edge)} ${n(d - edge)}L${n(edge)} ${n(d - edge)}Z"/>`;
|
|
40
|
+
const number = o.number === undefined || o.number === "" || o.team === "official" ? "" : `<text class="ky-lt" x="${n(r)}" y="${n(r)}" text-anchor="middle" dominant-baseline="central" font-size="${n(d * 0.46)}">${esc(String(o.number))}</text>`;
|
|
41
|
+
const themed = theme === "light" || theme === "dark" ? ` data-theme="${theme}"` : "";
|
|
42
|
+
return `<g class="kyogijo" data-sport="${esc(sport)}"${themed}${vars === "" ? "" : ` style="${esc(vars)}"`}><g class="ky-pts" style="--w:${n(d * 0.05)}">${shape}</g>${number}</g>`;
|
|
43
|
+
}
|
|
44
|
+
/** The plane of one disc: a billboard that leans to face the eye, its lower edge on the floor when it stands, its middle over (x, y). */
|
|
45
|
+
export function discPlane(o, x, y, d, lift, svg) {
|
|
46
|
+
return { id: `replay-${o.id}`, kind: "player", o: [x - d / 2, y, lift + d], u: [d, 0, 0], v: [0, 0, -d], svg, billboard: true, lean: true };
|
|
47
|
+
}
|
|
48
|
+
/** Put a disc over a new place on the floor, as `discPlane` does: the plane is the caller's, changed in place, so that a page moving many does not make new ones. */
|
|
49
|
+
export function moveDisc(plane, x, y, d, lift) {
|
|
50
|
+
plane.o = [x - d / 2, y, lift + d];
|
|
51
|
+
}
|
|
52
|
+
/** The floor's options for a replay: the model's own, and an id of its own, so that the replay's groups are found in it. */
|
|
53
|
+
export const replayFloorOptions = (model) => ({ ...model.options, id: `${model.id}-replay` });
|
|
54
|
+
/**
|
|
55
|
+
* A 3D view's model with a replay on it at a time: the floor drawn as the replay's frame (without the players when they stand), and a disc for each player and official the timeline
|
|
56
|
+
* tracks, after the model's own planes. One that is not on the surface at the time is still a plane, and says so (`on`), so that the elements stay the same while it plays. Pure.
|
|
57
|
+
*/
|
|
58
|
+
export function replayTurnModel(model, timeline, t, look = {}) {
|
|
59
|
+
const stand = look.stand !== false;
|
|
60
|
+
const floorOptions = replayFloorOptions(model);
|
|
61
|
+
const floor = drawFrameOf(floorOptions, timeline, t, look, stand ? keepOnFloor : () => true);
|
|
62
|
+
const size = discSize(playingArea(model), look);
|
|
63
|
+
const discs = new Map();
|
|
64
|
+
if (!stand)
|
|
65
|
+
return { model: { ...model, floor }, discs, size, floorOptions };
|
|
66
|
+
const sport = String(model.options.sport ?? "");
|
|
67
|
+
const theme = model.options.theme;
|
|
68
|
+
const vars = floorVars(floor);
|
|
69
|
+
const lift = model.uprights.lift;
|
|
70
|
+
const snap = replaySnapshot(timeline, t, look);
|
|
71
|
+
const placed = replayPlacer(floorOptions, timeline)(snap.objects.map((o) => [o.x, o.y]));
|
|
72
|
+
const here = new Map(snap.objects.map((o, i) => [o.id, placed[i]]));
|
|
73
|
+
for (const o of replayCast(timeline)) {
|
|
74
|
+
if (!STANDING.includes(o.team))
|
|
75
|
+
continue;
|
|
76
|
+
const at = here.get(o.id);
|
|
77
|
+
discs.set(o.id, { plane: discPlane(o, at?.[0] ?? 0, at?.[1] ?? 0, size, lift, discMarkup(o, size, sport, theme, vars)), on: at !== undefined });
|
|
78
|
+
}
|
|
79
|
+
return { model: { ...model, floor, planes: [...model.planes, ...[...discs.values()].map((disc) => disc.plane)] }, discs, size, floorOptions };
|
|
80
|
+
}
|
|
81
|
+
/** A number for a test or a page that wants to know where a disc stands: its middle on the floor, in feet. */
|
|
82
|
+
export const discAt = (plane) => `${num(plane.o[0] + plane.u[0] / 2)} ${num(plane.o[1])}`;
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The package's version: the same as package.json's, and a test holds them to each other. */
|
|
2
|
-
export declare const VERSION = "0.
|
|
2
|
+
export declare const VERSION = "0.52.0";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The package's version: the same as package.json's, and a test holds them to each other. */
|
|
2
|
-
export const VERSION = "0.
|
|
2
|
+
export const VERSION = "0.52.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnmorrisdotca/kyogijo-stadiums",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.52.0",
|
|
4
4
|
"description": "Kyogijo Stadiums 競技場, 40 sports' surfaces as SVG, drawn to scale from the rulebooks: NFL, CFL and IFAF football fields, an NHL rink, an NBA court, MLB ballparks, soccer pitches, curling sheets, athletics stadiums, speed skating, short track and figure skating rinks, swimming pools, water polo fields, diving wells, artistic swimming areas, and the courts and fields of tennis, volleyball (indoor, beach and sitting), badminton, pickleball, table tennis, padel, squash, racquetball, sepak takraw, teqball, roundnet, netball, lacrosse, rugby, Australian football, Gaelic games, cricket, field hockey and handball, and the budo of judo, karate, kendo, sumo and kyudo with a dojo of tatami laid as real units, the MMA cage, with each team's name and logo on its side and the score in the middle, stands and a crowd, a pageant of flags, and a 3D view. Light and dark, accessible, a compact badge for lists, a helper for React and a custom element. Zero dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sports",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"type": "module",
|
|
68
68
|
"sideEffects": [
|
|
69
69
|
"./dist/element-define.js",
|
|
70
|
+
"./dist/turn.replay.dom.js",
|
|
70
71
|
"./dist/frames.providers.js",
|
|
71
72
|
"./dist/full.js",
|
|
72
73
|
"./dist/arenas.js",
|