@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
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ All notable changes to this project are written here. The format follows
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.60.0] - 2026-11-03
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **The camera follows the play (KYOGIJO-STADIUMS-46)** ([following the play](./docs/replay.md#following-the-play)): a replay can be watched with a camera that closes in where the action is and pulls back as the players spread, or moves with the puck. Off by default; the replay's bar has **Action** and **Puck** (**Ball** for a ball game) toggles, the setting `follow` (`"action"`, `"puck"`, `"off"`), the controller's `setFollow` and `holdFollow`, and the element's `follow` attribute. Action frames the puck and the five players nearest it, padded, never closer than 2.5 times the surface; Puck centres the puck at twice; from a whistle or a goal to the next `faceoff` event with a place, it goes to that faceoff dot. It eases with a critically damped spring, holds still for small moves (a dead zone), never leaves the surface, always holds the puck, and at rest writes nothing. Flat, it writes the drawing's `viewBox`; in 3D it looks at a point of the floor with a zoom over the view's, so the auto-turn and every viewpoint go on while it follows; the reader's drag, zoom, arrows or a viewpoint hold it until **Resume**. Reduced motion steps. It moves inside the replay's own animation frame, asks for none of its own, and runs nothing while paused, hidden or out of sight. The pure parts are in `/replay`: `followShot`, `followCamera`, `followStop`, `followWindow`, `clampShot`, `frameOf`, `springStep`, `zoomFor`, `FOLLOW_LIMITS`. The demo keeps the choice in its address (`follow=action`, `follow=puck`) for the hero's game, the Try it surface, the game section and the arenas page. `docs/replay.ja.md` is new: the camera in Japanese.
|
|
15
|
+
|
|
16
|
+
## [0.59.0] - 2026-11-03
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **The stands clear the whole floor, centred (KYOGIJO-STADIUMS-44)** ([the stands and the crowd](./docs/crowd.md#the-venues)): with `crowd`, the bowl's opening was laid out from the playing surface's box alone, so the floor drawn round it ran into the first rows: a court's run-off at the corners (volleyball, sepak takraw, netball), a table tennis area at its ends, a rink's boards, and an athletics track's sprint straight, jumps' runways and pits beyond the oval. The opening is now fitted to everything the surface draws round its playing area (its view with no margin, and an outline where the floor is not a rectangle: the track with its straight and runways, a rink's rounded boards), clear of it all round, corners included, by 2 feet; a floor bigger than the sport's distances (table tennis) gets an opening of its own shape. The bench side's extra room is shared by the two sides, so the opening is centred on the floor and the bowl in the drawing (a soccer ground's touchlines are 23.7 feet from the first row on both sides, where they were 19.7 and 27.7), and the margin holds the roof's shade. The tunnel, the camera platform and the media's desks stay in the seats, the media's desks no longer run round a small opening's corners, and the benches stand between the floor and the first row only where the gap holds them. An artistic swimming area with a crowd keeps the margin the stands need along its sides (it gave them four tenths of it). The flat drawing and the 3D view share the one layout (`crowdStands`), which now gives the `floor` it was fitted to. In 3D, a table tennis table's raised floor (the drawing lifted to the table's height, with the ground the stands stand on) was sorted by its middle with the stands and drawn over the far ones, so from Low corner or Broadcast the bowl looked cut open round a small tile: the bowl is now always drawn over a raised floor, and the faint near stands at a low viewpoint are the walls giving way to the eye, as before. New tests walk every sport with a crowd, at every size and orientation, light and dark: no part of the floor's outline within 2 feet of a seat, nothing of the stands on the floor, the opening centred on the floor and the bowl centred in the drawing and wholly inside it; and in 3D, at every viewpoint, zoom and box, the eye outside the whole scene, nothing near the eye's plane, and no plane of the bowl under the floor. Pictures of a court, a track, a rink, a ring and a table tennis area in their stands are in the docs.
|
|
21
|
+
|
|
10
22
|
## [0.58.0] - 2026-11-03
|
|
11
23
|
|
|
12
24
|
### Added
|
package/README.md
CHANGED
|
@@ -180,6 +180,8 @@ Real buildings drawn in the outline OpenStreetMap maps for them, each with a pla
|
|
|
180
180
|
</tr>
|
|
181
181
|
</table>
|
|
182
182
|
|
|
183
|
+
A camera can follow the play, flat or in 3D: it closes in where the action is and pulls back as the players spread, or moves with the puck, while the view goes on turning ([following the play](./docs/replay.md#following-the-play)).
|
|
184
|
+
|
|
183
185
|
The scoreboard hung over an arena's ice follows the replay: its score, period and game clock come from the same timeline and clock as the controls ([the scoreboard over the arena](./docs/replay.md#the-scoreboard-over-the-arena-follows-the-game)).
|
|
184
186
|
|
|
185
187
|
**Surfaces** ([the surfaces](./docs/surfaces.md), [score and layout](./docs/score.md))
|
package/catalogue.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"generatedAt": "2026-11-03",
|
|
4
4
|
"package": {
|
|
5
5
|
"name": "@johnmorrisdotca/kyogijo-stadiums",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.60.0"
|
|
7
7
|
},
|
|
8
8
|
"address": "https://johnmorrisdotca.github.io/kyogijo-stadiums/catalogue.json",
|
|
9
9
|
"site": "https://johnmorrisdotca.github.io/kyogijo-stadiums/",
|
package/dist/athletics/plan.d.ts
CHANGED
|
@@ -42,6 +42,8 @@ export interface AthleticsPlan {
|
|
|
42
42
|
box: MBox;
|
|
43
43
|
/** Everything drawn: the box and what lies beyond it (the jump strip, the runways' ends, the straight). */
|
|
44
44
|
extent: MBox;
|
|
45
|
+
/** What `extent` holds as points (the oval and what lies beyond it): what a crowd's stands clear. */
|
|
46
|
+
outline?: [number, number][];
|
|
45
47
|
areas: Area[];
|
|
46
48
|
shapes: Shape[];
|
|
47
49
|
digits: Digits[];
|
package/dist/athletics/plan.js
CHANGED
|
@@ -224,13 +224,28 @@ export function athleticsPlan(o) {
|
|
|
224
224
|
const box = { x0: -S / 2 - Rout, y0: -Rout, x1: S / 2 + Rout, y1: Rout };
|
|
225
225
|
let extent = grow(box, 1);
|
|
226
226
|
const edgeStraight = R + straight.lanes * straight.lane;
|
|
227
|
+
// What lies beyond the oval: the sprint straight, the jumps' strip, the javelin's runways.
|
|
228
|
+
const beyond = [];
|
|
227
229
|
if (!indoor)
|
|
228
|
-
|
|
230
|
+
beyond.push({ x0: straight.x0, y0: -Rout, x1: straight.x1, y1: edgeStraight });
|
|
229
231
|
if (events.includes("long-jump") || events.includes("triple-jump"))
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
232
|
+
beyond.push(jumpStrip());
|
|
233
|
+
extent = union([extent, ...beyond]);
|
|
234
|
+
if (events.includes("javelin-throw")) {
|
|
235
|
+
const runways = inf.facilities.filter((f) => f.event === "javelin-throw").map((f) => f.box);
|
|
236
|
+
const cut = (b) => ({ x0: Math.max(b.x0, -S / 2 - Rout - 6), y0: b.y0, x1: Math.min(b.x1, S / 2 + Rout + 6), y1: b.y1 });
|
|
237
|
+
extent = union([extent, ...runways].map(cut));
|
|
238
|
+
beyond.push(...runways.map(cut));
|
|
239
|
+
}
|
|
233
240
|
extent = grow(extent, 0);
|
|
241
|
+
const rim = Rout + 1;
|
|
242
|
+
const outline = [];
|
|
243
|
+
for (let i = 0; i <= 32; i += 1) {
|
|
244
|
+
const a = -Math.PI / 2 + (Math.PI * i) / 32;
|
|
245
|
+
outline.push([S / 2 + rim * Math.cos(a), rim * Math.sin(a)], [-S / 2 - rim * Math.cos(a), rim * Math.sin(a)]);
|
|
246
|
+
}
|
|
247
|
+
for (const b of beyond)
|
|
248
|
+
outline.push([b.x0, b.y0], [b.x1, b.y0], [b.x1, b.y1], [b.x0, b.y1]);
|
|
234
249
|
const plan = {
|
|
235
250
|
sport: "athletics",
|
|
236
251
|
league: league.id,
|
|
@@ -243,6 +258,7 @@ export function athleticsPlan(o) {
|
|
|
243
258
|
infield,
|
|
244
259
|
box,
|
|
245
260
|
extent,
|
|
261
|
+
outline,
|
|
246
262
|
areas: parts.areas,
|
|
247
263
|
shapes: parts.shapes,
|
|
248
264
|
digits: parts.digits,
|
package/dist/catalogue.data.js
CHANGED
|
@@ -3,7 +3,7 @@ export const CATALOGUE_DATA = {
|
|
|
3
3
|
"generatedAt": "2026-11-03",
|
|
4
4
|
"package": {
|
|
5
5
|
"name": "@johnmorrisdotca/kyogijo-stadiums",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.60.0"
|
|
7
7
|
},
|
|
8
8
|
"address": "https://johnmorrisdotca.github.io/kyogijo-stadiums/catalogue.json",
|
|
9
9
|
"site": "https://johnmorrisdotca.github.io/kyogijo-stadiums/",
|
package/dist/crowd.d.ts
CHANGED
|
@@ -70,6 +70,14 @@ export interface CrowdStands {
|
|
|
70
70
|
stadium?: "purpose" | "nfl" | "cfl";
|
|
71
71
|
/** The playing surface, in the wide frame. */
|
|
72
72
|
box: Box;
|
|
73
|
+
/**
|
|
74
|
+
* The floor round the playing surface, where the surface draws more than its box (a court's run-off, a track's sprint straight, runways and pits): its bounding box and its outline as points, in the wide frame.
|
|
75
|
+
* The first row of seats clears all of it by `FLOOR_CLEAR`, and the opening is centred on its box. Missing for a real venue's stands and a ballpark's, which are laid out from their own data.
|
|
76
|
+
*/
|
|
77
|
+
floor?: {
|
|
78
|
+
box: Box;
|
|
79
|
+
points: Pt[];
|
|
80
|
+
};
|
|
73
81
|
rings: StandRing[];
|
|
74
82
|
/** Boxes where the stands are left out (the open end of a horseshoe, the ends of two sides). */
|
|
75
83
|
covers: [x0: number, y0: number, x1: number, y1: number][];
|
|
@@ -310,10 +318,20 @@ export interface LayoutInput {
|
|
|
310
318
|
};
|
|
311
319
|
/** A real venue's stands in place of the sport's usual tiers (see `venueBowl`). */
|
|
312
320
|
real?: RealBowl;
|
|
321
|
+
/**
|
|
322
|
+
* Everything the surface draws round its playing area, in the wide frame: the bounding box (the surface's own view with no margin) and, where the floor is not that rectangle (an athletics track, an oval
|
|
323
|
+
* ground), its outline as points. The opening of the stands is centred on it and clears it. Without it the floor is the box.
|
|
324
|
+
*/
|
|
325
|
+
floor?: {
|
|
326
|
+
box: Box;
|
|
327
|
+
points?: readonly (readonly [number, number])[];
|
|
328
|
+
};
|
|
313
329
|
/** The ballpark's fence as points on the field, for a ballpark (see `parkLayout`). */
|
|
314
330
|
fence?: readonly Pt[];
|
|
315
331
|
/** The foul lines' length at the poles and the backstop's distance, for a ballpark. */
|
|
316
332
|
backstop?: number;
|
|
317
333
|
}
|
|
334
|
+
/** The room kept between the floor (everything the surface draws round its playing area) and the first row of seats, in feet. */
|
|
335
|
+
export declare const FLOOR_CLEAR = 2;
|
|
318
336
|
/** The layout of a sport's stands round its surface: rings, covers, sectors and gaps. For a ballpark see `parkLayout`. */
|
|
319
337
|
export declare function layoutOf(input: LayoutInput): CrowdStands;
|
package/dist/crowd.js
CHANGED
|
@@ -330,6 +330,62 @@ function offsets(p, box, stadium) {
|
|
|
330
330
|
const side = g.side + (f.width - box.h) / 2;
|
|
331
331
|
return { left: end, right: end, top: side, bottom: side + g.bench };
|
|
332
332
|
}
|
|
333
|
+
/** The room kept between the floor (everything the surface draws round its playing area) and the first row of seats, in feet. */
|
|
334
|
+
export const FLOOR_CLEAR = 2;
|
|
335
|
+
/** A point inside a rounded rectangle or an ellipse of half sizes `hx`, `hy` about the origin, with `clear` to spare. */
|
|
336
|
+
function insideOpening(ax, ay, hx, hy, r, oval, clear) {
|
|
337
|
+
const [w, h] = [hx - clear, hy - clear];
|
|
338
|
+
if (w <= 0 || h <= 0)
|
|
339
|
+
return false;
|
|
340
|
+
if (oval)
|
|
341
|
+
return sq(ax / w) + sq(ay / h) <= 1 + 1e-9;
|
|
342
|
+
const rr = Math.max(0, Math.min(r, hx, hy) - clear);
|
|
343
|
+
if (ax > w + 1e-9 || ay > h + 1e-9)
|
|
344
|
+
return false;
|
|
345
|
+
const kx = ax - (w - rr);
|
|
346
|
+
const ky = ay - (h - rr);
|
|
347
|
+
return kx <= 0 || ky <= 0 || sq(kx) + sq(ky) <= sq(rr) + 1e-9;
|
|
348
|
+
}
|
|
349
|
+
/** The first row's opening round a floor, centred on it (the bench room shared by both sides): the sport's distances, or the floor's own shape where they do not clear it, grown alike until its whole outline clears by `FLOOR_CLEAR`. */
|
|
350
|
+
function openingOf(input) {
|
|
351
|
+
const { off, corner, oval, box } = input;
|
|
352
|
+
const floor = input.floor?.box ?? box;
|
|
353
|
+
const cx = floor.x + floor.w / 2;
|
|
354
|
+
const cy = floor.y + floor.h / 2;
|
|
355
|
+
let hx = Math.max(cx - box.x, box.x + box.w - cx) + (off.left + off.right) / 2;
|
|
356
|
+
let hy = Math.max(cy - box.y, box.y + box.h - cy) + (off.top + off.bottom) / 2;
|
|
357
|
+
// A floor the sport's distances do not clear (table tennis): the floor's own shape.
|
|
358
|
+
if (Math.min(hx - floor.w / 2, hy - floor.h / 2) < FLOOR_CLEAR) {
|
|
359
|
+
hx = floor.w / 2 + FLOOR_CLEAR;
|
|
360
|
+
hy = floor.h / 2 + FLOOR_CLEAR;
|
|
361
|
+
}
|
|
362
|
+
const given = input.floor?.points;
|
|
363
|
+
const points = given !== undefined && given.length > 0
|
|
364
|
+
? given.map(([x, y]) => [x, y])
|
|
365
|
+
: oval
|
|
366
|
+
? Array.from({ length: 64 }, (_, i) => [cx + (floor.w / 2) * Math.cos((i * Math.PI) / 32), cy + (floor.h / 2) * Math.sin((i * Math.PI) / 32)])
|
|
367
|
+
: [[floor.x, floor.y], [floor.x + floor.w, floor.y], [floor.x + floor.w, floor.y + floor.h], [floor.x, floor.y + floor.h]];
|
|
368
|
+
// A sampled curve: a little to spare.
|
|
369
|
+
const clear = FLOOR_CLEAR + (given !== undefined || oval ? 0.5 : 0);
|
|
370
|
+
const fits = (d) => points.every(([x, y]) => insideOpening(Math.abs(x - cx), Math.abs(y - cy), hx + d, hy + d, corner, oval, clear));
|
|
371
|
+
if (!fits(0)) {
|
|
372
|
+
let lo = 0;
|
|
373
|
+
let hi = 4;
|
|
374
|
+
while (!fits(hi) && hi < 4096)
|
|
375
|
+
hi *= 2;
|
|
376
|
+
for (let i = 0; i < 40; i += 1) {
|
|
377
|
+
const mid = (lo + hi) / 2;
|
|
378
|
+
if (fits(mid))
|
|
379
|
+
hi = mid;
|
|
380
|
+
else
|
|
381
|
+
lo = mid;
|
|
382
|
+
}
|
|
383
|
+
hx += hi;
|
|
384
|
+
hy += hi;
|
|
385
|
+
}
|
|
386
|
+
const round = (v) => Math.round(v * 1e6) / 1e6;
|
|
387
|
+
return { off: { left: round(box.x - (cx - hx)), right: round(cx + hx - box.x - box.w), top: round(box.y - (cy - hy)), bottom: round(cy + hy - box.y - box.h) }, cx, cy, floor, points };
|
|
388
|
+
}
|
|
333
389
|
/** The layout of a sport's stands round its surface: rings, covers, sectors and gaps. For a ballpark see `parkLayout`. */
|
|
334
390
|
export function layoutOf(input) {
|
|
335
391
|
const { sport, box } = input;
|
|
@@ -342,15 +398,16 @@ export function layoutOf(input) {
|
|
|
342
398
|
const stadium = sport === "soccer" && input.real === undefined ? host ?? "purpose" : undefined;
|
|
343
399
|
const p = stadium === "nfl" || stadium === "cfl" ? { ...PROFILES[stadium], shape: base.shape, away: base.away } : base;
|
|
344
400
|
const shape = input.real !== undefined ? "bowl" : crowd.shape ?? p.shape;
|
|
345
|
-
const
|
|
346
|
-
const
|
|
347
|
-
const
|
|
401
|
+
const fit = input.real === undefined ? openingOf({ off: offsets(base, box, stadium ?? "purpose"), corner: p.corner, oval: base.oval === true, box, floor: input.floor }) : undefined;
|
|
402
|
+
const off = input.real?.off ?? fit?.off ?? offsets(base, box, stadium ?? "purpose");
|
|
403
|
+
const cx = fit?.cx ?? box.x + box.w / 2;
|
|
404
|
+
const cy = fit?.cy ?? box.y + box.h / 2;
|
|
348
405
|
const rings = input.real === undefined ? [] : input.real.rings.map((ring) => ({ ...ring }));
|
|
349
406
|
const reachOf = input.real === undefined ? { left: 0, right: 0, top: 0, bottom: 0 } : { ...input.real.reach };
|
|
350
407
|
for (const t of input.real === undefined ? p.tiers : []) {
|
|
351
408
|
const at = (k) => {
|
|
352
409
|
if (base.oval === true)
|
|
353
|
-
return { kind: "ellipse", cx, cy, rx: box.w
|
|
410
|
+
return { kind: "ellipse", cx, cy, rx: (box.w + off.left + off.right) / 2 + k, ry: (box.h + off.top + off.bottom) / 2 + k };
|
|
354
411
|
return { kind: "rrect", x0: box.x - off.left - k, y0: box.y - off.top - k, x1: box.x + box.w + off.right + k, y1: box.y + box.h + off.bottom + k, r: p.corner + k };
|
|
355
412
|
};
|
|
356
413
|
const region = at(t.to);
|
|
@@ -379,17 +436,18 @@ export function layoutOf(input) {
|
|
|
379
436
|
const covers = [];
|
|
380
437
|
const x0 = box.x - off.left;
|
|
381
438
|
const x1 = box.x + box.w + off.right;
|
|
382
|
-
|
|
383
|
-
const
|
|
439
|
+
// The covers reach past the roof's shade (9 feet beyond the upper tier) too.
|
|
440
|
+
const yTop = last === undefined ? box.y : box.y - reachOf.top - 10;
|
|
441
|
+
const yBot = last === undefined ? box.y + box.h : box.y + box.h + reachOf.bottom + 10;
|
|
384
442
|
if (shape === "horseshoe") {
|
|
385
443
|
// The open end: everything beyond the surface's end line and the gap that is kept clear.
|
|
386
444
|
if (crowd.open === "left")
|
|
387
|
-
covers.push([box.x - reachOf.left -
|
|
445
|
+
covers.push([box.x - reachOf.left - 10, yTop, x0 + p.corner * 0.5, yBot]);
|
|
388
446
|
else
|
|
389
|
-
covers.push([x1 - p.corner * 0.5, yTop, box.x + box.w + reachOf.right +
|
|
447
|
+
covers.push([x1 - p.corner * 0.5, yTop, box.x + box.w + reachOf.right + 10, yBot]);
|
|
390
448
|
}
|
|
391
449
|
else if (shape === "sides") {
|
|
392
|
-
covers.push([box.x - reachOf.left -
|
|
450
|
+
covers.push([box.x - reachOf.left - 10, yTop, x0 + p.corner * 0.5, yBot], [x1 - p.corner * 0.5, yTop, box.x + box.w + reachOf.right + 10, yBot]);
|
|
393
451
|
}
|
|
394
452
|
const concourses = [];
|
|
395
453
|
rings.forEach((ring, i) => {
|
|
@@ -398,6 +456,8 @@ export function layoutOf(input) {
|
|
|
398
456
|
concourses.push({ region: ring.hole, hole: before.region });
|
|
399
457
|
});
|
|
400
458
|
const stands = { sport, shape, box, rings, covers, concourses, sectors: [], gaps: [], reach: reachOf, seats: rings.reduce((n, r) => n + r.seats, 0) };
|
|
459
|
+
if (fit !== undefined)
|
|
460
|
+
stands.floor = { box: fit.floor, points: fit.points };
|
|
401
461
|
if (input.real !== undefined)
|
|
402
462
|
stands.real = input.real.shell;
|
|
403
463
|
if (input.real?.closedSectors !== undefined)
|
|
@@ -490,21 +550,31 @@ export function layoutOf(input) {
|
|
|
490
550
|
}
|
|
491
551
|
}
|
|
492
552
|
if (crowd.gaps && shape !== "grandstand") {
|
|
493
|
-
|
|
494
|
-
const
|
|
553
|
+
// Nothing on the floor: the benches stand between the floor and the first row.
|
|
554
|
+
const edge = box.y + box.h + off.bottom;
|
|
555
|
+
const floorEdge = Math.max(box.y + box.h, fit === undefined ? -Infinity : fit.floor.y + fit.floor.h);
|
|
556
|
+
const mid = cx;
|
|
495
557
|
// A tunnel through the lower stands at the middle of the bench side, the camera platform above them, and the benches in the gap.
|
|
496
|
-
stands.gaps.push({ kind: "tunnel", box: [mid - 6,
|
|
558
|
+
stands.gaps.push({ kind: "tunnel", box: [mid - 6, edge, mid + 6, edge + lowerRing.to + 1] });
|
|
497
559
|
const media = rings[rings.length - 1];
|
|
498
560
|
if (media !== undefined && media.tier === "upper")
|
|
499
|
-
stands.gaps.push({ kind: "camera", box: [mid - 22,
|
|
500
|
-
if (
|
|
561
|
+
stands.gaps.push({ kind: "camera", box: [mid - 22, edge + media.from + 3, mid + 22, edge + media.from + 13] });
|
|
562
|
+
if (base.bench >= 4 || sport === "soccer" || sport === "nfl" || sport === "cfl" || sport === "ifaf" || sport === "rugby" || sport === "gaelic") {
|
|
501
563
|
const benchLen = Math.min(34, box.w * 0.12);
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
564
|
+
const depth = Math.min(5, off.bottom * 0.3);
|
|
565
|
+
if (floorEdge + 2.5 + depth <= edge - 1) {
|
|
566
|
+
for (const s of [-1, 1]) {
|
|
567
|
+
const bx = box.x + box.w / 2 + s * (box.w * 0.1 + benchLen / 2 + 4);
|
|
568
|
+
stands.gaps.push({ kind: "bench", box: [bx - benchLen / 2, floorEdge + 2.5, bx + benchLen / 2, floorEdge + 2.5 + depth] });
|
|
569
|
+
}
|
|
505
570
|
}
|
|
506
571
|
}
|
|
507
|
-
|
|
572
|
+
// The media's desks: along the straight of that side only.
|
|
573
|
+
const hole = lowerRing.hole;
|
|
574
|
+
const straight = hole?.kind === "rrect" ? (hole.x1 - hole.x0) / 2 - Math.min(hole.r, (hole.x1 - hole.x0) / 2, (hole.y1 - hole.y0) / 2) : 55;
|
|
575
|
+
const half = Math.min(55, straight);
|
|
576
|
+
if (half >= 8)
|
|
577
|
+
stands.gaps.push({ kind: "media", box: [mid - half, edge + 2, mid + half, edge + 6] });
|
|
508
578
|
}
|
|
509
579
|
}
|
|
510
580
|
return stands;
|
package/dist/draw.js
CHANGED
|
@@ -436,7 +436,7 @@ function realStadiumOf(o, sport, real) {
|
|
|
436
436
|
return stadium !== undefined && stadium.configs[config] !== undefined && (stadiumKind(stadium) === "park") === (config === "mlb") ? { stadium, config } : undefined;
|
|
437
437
|
}
|
|
438
438
|
/** The stands round a surface, laid out from its box (or, for a ballpark, from its fence). */
|
|
439
|
-
function standsFor(o, surface, crowd) {
|
|
439
|
+
function standsFor(o, surface, crowd, floor) {
|
|
440
440
|
const { sport } = chooseLeague(o);
|
|
441
441
|
const awaySide = resolveHomeSide({ ...o, sport }) === "left" ? "right" : "left";
|
|
442
442
|
if (sport === "mlb") {
|
|
@@ -469,7 +469,19 @@ function standsFor(o, surface, crowd) {
|
|
|
469
469
|
// A dojo's crowd sits in its gallery, from the room's balcony data (in feet here); the arenas (the Budokan, the Kokugikan) keep their bowls.
|
|
470
470
|
const hall = sport === "dojo" ? dojoGallery(o) : undefined;
|
|
471
471
|
const gallery = hall === undefined ? undefined : { offset: hall.offset / 0.3048, depth: hall.depth / 0.3048, height: [hall.height[0] / 0.3048, hall.height[1] / 0.3048], rows: hall.rows, aisles: hall.aisles };
|
|
472
|
-
return crowdLayout({ sport, box, crowd, awaySide, ground: o.ground, arena: o.budo?.hall === "budokan", ...(gallery === undefined ? {} : { gallery }) });
|
|
472
|
+
return crowdLayout({ sport, box, crowd, awaySide, ground: o.ground, arena: o.budo?.hall === "budokan", ...(gallery === undefined ? {} : { gallery }), ...(floor === undefined ? {} : { floor }) });
|
|
473
|
+
}
|
|
474
|
+
/** A surface's floor with no margin (its view, and its outline where it has one); none for a close-up. */
|
|
475
|
+
function floorOf(probe) {
|
|
476
|
+
if (probe.whole !== undefined)
|
|
477
|
+
return undefined;
|
|
478
|
+
const view = wideView(probe);
|
|
479
|
+
if (probe.floor === undefined)
|
|
480
|
+
return { box: view };
|
|
481
|
+
const xs = [view.x, view.x + view.w, ...probe.floor.map((p) => p[0])];
|
|
482
|
+
const ys = [view.y, view.y + view.h, ...probe.floor.map((p) => p[1])];
|
|
483
|
+
const [x0, y0] = [Math.min(...xs), Math.min(...ys)];
|
|
484
|
+
return { box: { x: x0, y: y0, w: Math.max(...xs) - x0, h: Math.max(...ys) - y0 }, points: probe.floor };
|
|
473
485
|
}
|
|
474
486
|
/** True when the drawing is asked to be small: `detail: "simple"` or a `width` under 480 pixels. */
|
|
475
487
|
const smallDrawing = (o) => {
|
|
@@ -493,17 +505,20 @@ function venueFrame(o, crowd, surface) {
|
|
|
493
505
|
/** A surface with the stands and the crowd round it: the ground is made as wide as the stands need, then the stands are laid on it under the surface. */
|
|
494
506
|
function buildCrowded(o, portrait, id, score, scored, extra, crowd) {
|
|
495
507
|
let margin = extra.margin;
|
|
508
|
+
// The floor the stands' opening clears and is centred on.
|
|
509
|
+
const probe = buildWhole({ ...o, margin: 0 }, portrait, id, score, scored, { ...extra, margin: undefined });
|
|
510
|
+
const floor = floorOf(probe);
|
|
496
511
|
if (!(typeof o.margin === "number" && Number.isFinite(o.margin) && o.margin >= 0)) {
|
|
497
512
|
// How much ground the stands need beyond what the surface has with none, then the surface is made again with it.
|
|
498
|
-
const
|
|
499
|
-
const stands = standsFor(o, probe, crowd);
|
|
513
|
+
const stands = standsFor(o, probe, crowd, floor);
|
|
500
514
|
const view = wideView(probe);
|
|
501
515
|
const have = { left: stands.box.x - view.x, right: view.x + view.w - stands.box.x - stands.box.w, top: stands.box.y - view.y, bottom: view.y + view.h - stands.box.y - stands.box.h };
|
|
502
516
|
const need = Math.max(stands.reach.left - have.left, stands.reach.right - have.right, stands.reach.top - have.top, stands.reach.bottom - have.bottom);
|
|
503
|
-
|
|
517
|
+
// (and the roof's shade, 9 feet beyond the upper tier)
|
|
518
|
+
margin = Math.max(margin ?? 0, need + (crowd.roof ? 10 : 6));
|
|
504
519
|
}
|
|
505
520
|
const surface = buildWhole(o, portrait, id, score, scored, { ...extra, margin });
|
|
506
|
-
const stands = standsFor(o, surface, crowd);
|
|
521
|
+
const stands = standsFor(o, surface, crowd, floor);
|
|
507
522
|
const seats = stands.seats;
|
|
508
523
|
const ground = heldGround(o.ground)?.capacity;
|
|
509
524
|
const fine = crowd.detail === "fine" || (crowd.detail === "auto" && !smallDrawing(o));
|
|
@@ -1220,5 +1235,5 @@ export function crowdStands(o) {
|
|
|
1220
1235
|
if (crowd === undefined)
|
|
1221
1236
|
return undefined;
|
|
1222
1237
|
const probe = buildWhole({ ...o, margin: 0 }, false, drawingId(o), false, false, { features: undefined });
|
|
1223
|
-
return standsFor(o, probe, crowd);
|
|
1238
|
+
return standsFor(o, probe, crowd, floorOf(probe));
|
|
1224
1239
|
}
|
package/dist/element.d.ts
CHANGED
|
@@ -7,12 +7,12 @@ import type { ScoreOptions, SurfaceOptions } from "./types.ts";
|
|
|
7
7
|
/** The tag of the element. */
|
|
8
8
|
export declare const KYOGIJO_TAG = "kyogijo-field";
|
|
9
9
|
/** The attributes the element reads. */
|
|
10
|
-
export declare const KYOGIJO_ATTRIBUTES: readonly ["sport", "league", "size", "theme", "language", "orientation", "park", "ground", "arena", "year", "detail", "margin", "stripes", "exact-lines", "uppercase", "pitch", "pitch-type", "half", "range", "range-end", "goals", "stripe-pattern", "corner-flags", "look", "line-width", "line-dash", "line-opacity", "landmarks", "walls", "quirks", "home", "away", "home-abbr", "away-abbr", "home-score", "away-score", "home-penalties", "away-penalties", "score", "home-logo", "away-logo", "home-flag", "away-flag", "home-color", "away-color", "status", "competition", "date", "label", "winner", "home-side", "pageant", "description", "options", "loop", "speed", "autoplay", "controls", "trail", "turn", "turn-view", "turn-controls", "turn-uprights", "turn-score", "turn-touch", "turn-venue", "turn-roof", "turn-autoplay", "turn-speed", "turn-direction"];
|
|
10
|
+
export declare const KYOGIJO_ATTRIBUTES: readonly ["sport", "league", "size", "theme", "language", "orientation", "park", "ground", "arena", "year", "detail", "margin", "stripes", "exact-lines", "uppercase", "pitch", "pitch-type", "half", "range", "range-end", "goals", "stripe-pattern", "corner-flags", "look", "line-width", "line-dash", "line-opacity", "landmarks", "walls", "quirks", "home", "away", "home-abbr", "away-abbr", "home-score", "away-score", "home-penalties", "away-penalties", "score", "home-logo", "away-logo", "home-flag", "away-flag", "home-color", "away-color", "status", "competition", "date", "label", "winner", "home-side", "pageant", "description", "options", "loop", "speed", "autoplay", "controls", "trail", "follow", "turn", "turn-view", "turn-controls", "turn-uprights", "turn-score", "turn-touch", "turn-venue", "turn-roof", "turn-autoplay", "turn-speed", "turn-direction"];
|
|
11
11
|
/** The settings of a 3D view an element's attributes make: `turn-view` names the viewpoint it opens on, `turn-controls="false"` and `turn-uprights="false"` leave the buttons and what stands on the surface out, `turn-score="floor"` leaves the score panel on the floor, `turn-touch="turn"` keeps every touch for the view, `turn-venue="false"` leaves out the stands and the room (they are raised for a drawing with a crowd and for a dojo), `turn-roof` puts a roof or canopy over the stands, `turn-autoplay` starts the auto-turn (`force` even for a reader who prefers reduced motion) and `turn-speed` is its degrees a second. */
|
|
12
12
|
export declare function turnSettingsFromAttributes(read: (name: string) => string | null, base?: TurnSettings): TurnSettings;
|
|
13
13
|
/** Whether an element's `turn` attribute asks for the 3D view: present, and not `false`. */
|
|
14
14
|
export declare const turnWanted: (read: (name: string) => string | null) => boolean;
|
|
15
|
-
/** The settings of a replay an element's attributes make: `loop`, `autoplay` and `controls="false"` are switches, `speed` and `trail` numbers. */
|
|
15
|
+
/** The settings of a replay an element's attributes make: `loop`, `autoplay` and `controls="false"` are switches, `speed` and `trail` numbers, `follow` what the camera follows (`action`, `puck` or `off`). */
|
|
16
16
|
export declare function replaySettingsFromAttributes(read: (name: string) => string | null, base?: ReplaySettings): ReplaySettings;
|
|
17
17
|
/** The options an element's attributes (and its `options` attribute and property) make. */
|
|
18
18
|
export declare function optionsFromAttributes(read: (name: string) => string | null, base?: Partial<ScoreOptions>): SurfaceOptions | ScoreOptions;
|
|
@@ -25,7 +25,7 @@ export declare function optionsFromAttributes(read: (name: string) => string | n
|
|
|
25
25
|
declare const Base: typeof HTMLElement;
|
|
26
26
|
export declare class KyogijoField extends Base {
|
|
27
27
|
#private;
|
|
28
|
-
static observedAttributes: readonly ["sport", "league", "size", "theme", "language", "orientation", "park", "ground", "arena", "year", "detail", "margin", "stripes", "exact-lines", "uppercase", "pitch", "pitch-type", "half", "range", "range-end", "goals", "stripe-pattern", "corner-flags", "look", "line-width", "line-dash", "line-opacity", "landmarks", "walls", "quirks", "home", "away", "home-abbr", "away-abbr", "home-score", "away-score", "home-penalties", "away-penalties", "score", "home-logo", "away-logo", "home-flag", "away-flag", "home-color", "away-color", "status", "competition", "date", "label", "winner", "home-side", "pageant", "description", "options", "loop", "speed", "autoplay", "controls", "trail", "turn", "turn-view", "turn-controls", "turn-uprights", "turn-score", "turn-touch", "turn-venue", "turn-roof", "turn-autoplay", "turn-speed", "turn-direction"];
|
|
28
|
+
static observedAttributes: readonly ["sport", "league", "size", "theme", "language", "orientation", "park", "ground", "arena", "year", "detail", "margin", "stripes", "exact-lines", "uppercase", "pitch", "pitch-type", "half", "range", "range-end", "goals", "stripe-pattern", "corner-flags", "look", "line-width", "line-dash", "line-opacity", "landmarks", "walls", "quirks", "home", "away", "home-abbr", "away-abbr", "home-score", "away-score", "home-penalties", "away-penalties", "score", "home-logo", "away-logo", "home-flag", "away-flag", "home-color", "away-color", "status", "competition", "date", "label", "winner", "home-side", "pageant", "description", "options", "loop", "speed", "autoplay", "controls", "trail", "follow", "turn", "turn-view", "turn-controls", "turn-uprights", "turn-score", "turn-touch", "turn-venue", "turn-roof", "turn-autoplay", "turn-speed", "turn-direction"];
|
|
29
29
|
/** The options given as a property: an object, laid under the attributes. */
|
|
30
30
|
get options(): Partial<ScoreOptions> | undefined;
|
|
31
31
|
set options(value: Partial<ScoreOptions> | undefined);
|
package/dist/element.js
CHANGED
|
@@ -26,11 +26,11 @@ export const KYOGIJO_ATTRIBUTES = [
|
|
|
26
26
|
"pitch", "pitch-type", "half", "range", "range-end", "goals", "stripe-pattern", "corner-flags", "look", "line-width", "line-dash", "line-opacity", "landmarks", "walls", "quirks",
|
|
27
27
|
"home", "away", "home-abbr", "away-abbr", "home-score", "away-score", "home-penalties", "away-penalties", "score", "home-logo", "away-logo", "home-flag", "away-flag", "home-color", "away-color",
|
|
28
28
|
"status", "competition", "date", "label", "winner", "home-side", "pageant", "description", "options",
|
|
29
|
-
"loop", "speed", "autoplay", "controls", "trail",
|
|
29
|
+
"loop", "speed", "autoplay", "controls", "trail", "follow",
|
|
30
30
|
"turn", "turn-view", "turn-controls", "turn-uprights", "turn-score", "turn-touch", "turn-venue", "turn-roof", "turn-autoplay", "turn-speed", "turn-direction",
|
|
31
31
|
];
|
|
32
32
|
/** The attributes that are about a replay and not about the drawing. */
|
|
33
|
-
const REPLAY_ATTRIBUTES = ["loop", "speed", "autoplay", "controls", "trail"];
|
|
33
|
+
const REPLAY_ATTRIBUTES = ["loop", "speed", "autoplay", "controls", "trail", "follow"];
|
|
34
34
|
/** The settings of a 3D view an element's attributes make: `turn-view` names the viewpoint it opens on, `turn-controls="false"` and `turn-uprights="false"` leave the buttons and what stands on the surface out, `turn-score="floor"` leaves the score panel on the floor, `turn-touch="turn"` keeps every touch for the view, `turn-venue="false"` leaves out the stands and the room (they are raised for a drawing with a crowd and for a dojo), `turn-roof` puts a roof or canopy over the stands, `turn-autoplay` starts the auto-turn (`force` even for a reader who prefers reduced motion) and `turn-speed` is its degrees a second. */
|
|
35
35
|
export function turnSettingsFromAttributes(read, base = {}) {
|
|
36
36
|
const out = { ...base };
|
|
@@ -66,7 +66,7 @@ export function turnSettingsFromAttributes(read, base = {}) {
|
|
|
66
66
|
}
|
|
67
67
|
/** Whether an element's `turn` attribute asks for the 3D view: present, and not `false`. */
|
|
68
68
|
export const turnWanted = (read) => read("turn") !== null && read("turn") !== "false";
|
|
69
|
-
/** The settings of a replay an element's attributes make: `loop`, `autoplay` and `controls="false"` are switches, `speed` and `trail` numbers. */
|
|
69
|
+
/** The settings of a replay an element's attributes make: `loop`, `autoplay` and `controls="false"` are switches, `speed` and `trail` numbers, `follow` what the camera follows (`action`, `puck` or `off`). */
|
|
70
70
|
export function replaySettingsFromAttributes(read, base = {}) {
|
|
71
71
|
const out = { ...base };
|
|
72
72
|
const on = (name) => (read(name) === null ? undefined : read(name) !== "false");
|
|
@@ -89,6 +89,9 @@ export function replaySettingsFromAttributes(read, base = {}) {
|
|
|
89
89
|
const trail = number("trail");
|
|
90
90
|
if (trail !== undefined)
|
|
91
91
|
out.trail = trail;
|
|
92
|
+
const follow = read("follow");
|
|
93
|
+
if (follow !== null)
|
|
94
|
+
out.follow = follow === "action" || follow === "puck" ? follow : "off";
|
|
92
95
|
return out;
|
|
93
96
|
}
|
|
94
97
|
/** `3-2`, `3–2` or `3:2` as home and away. */
|
|
@@ -350,7 +353,9 @@ export class KyogijoField extends Base {
|
|
|
350
353
|
__classPrivateFieldGet(this, _KyogijoField_turn, "f").preset(view);
|
|
351
354
|
return;
|
|
352
355
|
}
|
|
353
|
-
if (
|
|
356
|
+
if (name === "follow" && __classPrivateFieldGet(this, _KyogijoField_replay, "f") !== undefined)
|
|
357
|
+
__classPrivateFieldGet(this, _KyogijoField_replay, "f").setFollow(replaySettingsFromAttributes((key) => this.getAttribute(key)).follow ?? "off");
|
|
358
|
+
else if (REPLAY_ATTRIBUTES.includes(name) && __classPrivateFieldGet(this, _KyogijoField_replay, "f") !== undefined)
|
|
354
359
|
__classPrivateFieldGet(this, _KyogijoField_instances, "m", _KyogijoField_attach).call(this, name !== "speed" && name !== "loop");
|
|
355
360
|
else
|
|
356
361
|
__classPrivateFieldGet(this, _KyogijoField_instances, "m", _KyogijoField_draw).call(this);
|
|
@@ -371,7 +376,7 @@ _KyogijoField_mount = new WeakMap(), _KyogijoField_host = new WeakMap(), _Kyogij
|
|
|
371
376
|
const from = __classPrivateFieldGet(this, _KyogijoField_replay, "f") === undefined || __classPrivateFieldGet(this, _KyogijoField_replayOf, "f") !== __classPrivateFieldGet(this, _KyogijoField_timeline, "f") ? undefined : __classPrivateFieldGet(this, _KyogijoField_replay, "f").state();
|
|
372
377
|
__classPrivateFieldGet(this, _KyogijoField_replay, "f")?.destroy();
|
|
373
378
|
__classPrivateFieldSet(this, _KyogijoField_replayOf, __classPrivateFieldGet(this, _KyogijoField_timeline, "f"), "f");
|
|
374
|
-
__classPrivateFieldSet(this, _KyogijoField_replay, on.replay(__classPrivateFieldGet(this, _KyogijoField_timeline, "f"), from === undefined ? settings : { ...settings, start: from.t - from.start }), "f");
|
|
379
|
+
__classPrivateFieldSet(this, _KyogijoField_replay, on.replay(__classPrivateFieldGet(this, _KyogijoField_timeline, "f"), from === undefined ? settings : { ...settings, start: from.t - from.start, follow: from.follow }), "f");
|
|
375
380
|
}, _KyogijoField_draw = function _KyogijoField_draw() {
|
|
376
381
|
if (__classPrivateFieldGet(this, _KyogijoField_host, "f") === undefined || !this.isConnected)
|
|
377
382
|
return;
|
package/dist/replay-entry.d.ts
CHANGED
|
@@ -8,5 +8,7 @@ export { alongPool, distanceAt, progressOf, raceTimeline } from "./pool.replay.t
|
|
|
8
8
|
export type { Progress, RaceReplayOptions } from "./pool.replay.ts";
|
|
9
9
|
export { formatTime, parseTime } from "./pool.race.ts";
|
|
10
10
|
export { REPLAY_SPEEDS, REPLAY_SPEED_RANGE, createReplay } from "./replay.control.ts";
|
|
11
|
+
export { FOLLOW_LIMITS, FOLLOW_MODES, clampShot, followCamera, followMode, followShot, followStop, followWindow, frameOf, springStep, zoomFor } from "./replay.follow.ts";
|
|
12
|
+
export type { FollowBox, FollowCamera, FollowInput, FollowLimits, FollowMode, FollowShot } from "./replay.follow.ts";
|
|
11
13
|
export type { ReplayController } from "./replay.control.ts";
|
|
12
14
|
export type { ActiveEvent, Possession, ReplayChange, ReplayEvent, ReplayEventKind, ReplayFrame, ReplayLook, ReplayObject, ReplayPeriod, ReplayReason, ReplaySettings, ReplayState, ReplayTeam, ReplayTimeline, ReplayTrail, } from "./replay.types.ts";
|
package/dist/replay-entry.js
CHANGED
|
@@ -8,3 +8,4 @@ export { boardClock, replayBoard, replayPeriods } from "./replay.board.js";
|
|
|
8
8
|
export { alongPool, distanceAt, progressOf, raceTimeline } from "./pool.replay.js";
|
|
9
9
|
export { formatTime, parseTime } from "./pool.race.js";
|
|
10
10
|
export { REPLAY_SPEEDS, REPLAY_SPEED_RANGE, createReplay } from "./replay.control.js";
|
|
11
|
+
export { FOLLOW_LIMITS, FOLLOW_MODES, clampShot, followCamera, followMode, followShot, followStop, followWindow, frameOf, springStep, zoomFor } from "./replay.follow.js";
|
package/dist/replay.control.d.ts
CHANGED
|
@@ -22,6 +22,10 @@ export interface ReplayController {
|
|
|
22
22
|
setLoop(loop: boolean): void;
|
|
23
23
|
/** `step` for a reader who prefers reduced motion: playing stops, and play steps. */
|
|
24
24
|
setMotion(motion: "smooth" | "step"): void;
|
|
25
|
+
/** What the camera follows (`off`, `action`, `puck`); a change lets go of a hold. */
|
|
26
|
+
setFollow(mode: "off" | "action" | "puck"): void;
|
|
27
|
+
/** Hold the camera where it is (the reader turned or zoomed the view), or let it follow again (`false`). */
|
|
28
|
+
holdFollow(held: boolean): void;
|
|
25
29
|
/** Tell the clock that seconds went by: while playing, the time moves by that many seconds times the speed. */
|
|
26
30
|
advance(seconds: number): void;
|
|
27
31
|
/** Be told of every change. Returns a function that stops it. */
|
package/dist/replay.control.js
CHANGED
|
@@ -39,6 +39,8 @@ export function createReplay(timeline, settings = {}) {
|
|
|
39
39
|
speed: isNum(settings.speed) ? clamp(settings.speed, REPLAY_SPEED_RANGE[0], REPLAY_SPEED_RANGE[1]) : 1,
|
|
40
40
|
loop: settings.loop === true,
|
|
41
41
|
motion: settings.motion === "step" ? "step" : "smooth",
|
|
42
|
+
follow: settings.follow === "action" || settings.follow === "puck" ? settings.follow : "off",
|
|
43
|
+
followHeld: false,
|
|
42
44
|
};
|
|
43
45
|
const listeners = new Set();
|
|
44
46
|
const tell = (reason, crossed = []) => {
|
|
@@ -148,6 +150,20 @@ export function createReplay(timeline, settings = {}) {
|
|
|
148
150
|
state.playing = false;
|
|
149
151
|
tell("motion");
|
|
150
152
|
},
|
|
153
|
+
setFollow(mode) {
|
|
154
|
+
const next = mode === "action" || mode === "puck" ? mode : "off";
|
|
155
|
+
if (next === state.follow && !state.followHeld)
|
|
156
|
+
return;
|
|
157
|
+
state.follow = next;
|
|
158
|
+
state.followHeld = false;
|
|
159
|
+
tell("follow");
|
|
160
|
+
},
|
|
161
|
+
holdFollow(held) {
|
|
162
|
+
if (state.followHeld === held || (held && state.follow === "off"))
|
|
163
|
+
return;
|
|
164
|
+
state.followHeld = held;
|
|
165
|
+
tell("follow");
|
|
166
|
+
},
|
|
151
167
|
advance(seconds) {
|
|
152
168
|
if (!state.playing || state.motion === "step" || !isNum(seconds) || seconds <= 0)
|
|
153
169
|
return;
|
package/dist/replay.dom.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { num } from "./format.js";
|
|
5
5
|
import { renderLayers } from "./layers.js";
|
|
6
6
|
import { REPLAY_SPEEDS, createReplay } from "./replay.control.js";
|
|
7
|
+
import { FOLLOW_STYLE, flatFollow, followControls } from "./replay.follow.dom.js";
|
|
7
8
|
import { describeEvent, drawFrame, replayClock, replayGroups, replaySpoken, replayInfo, replayPlacer, replaySnapshot } from "./replay.js";
|
|
8
9
|
import { KYOGIJO_STRINGS, fill } from "./strings.core.js";
|
|
9
10
|
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
@@ -175,7 +176,7 @@ export function replayDriver(doc, timeline, settings, source) {
|
|
|
175
176
|
let language = source.language;
|
|
176
177
|
const words = () => KYOGIJO_STRINGS[language].replay;
|
|
177
178
|
const style = doc.createElement("style");
|
|
178
|
-
style.textContent = REPLAY_STYLE;
|
|
179
|
+
style.textContent = REPLAY_STYLE + FOLLOW_STYLE;
|
|
179
180
|
const hint = doc.createElement("p");
|
|
180
181
|
hint.className = "ky-hint";
|
|
181
182
|
hint.id = `${id}-keys`;
|
|
@@ -236,7 +237,8 @@ export function replayDriver(doc, timeline, settings, source) {
|
|
|
236
237
|
play.hidden = stepMode;
|
|
237
238
|
speed.hidden = stepMode;
|
|
238
239
|
loop.hidden = stepMode;
|
|
239
|
-
|
|
240
|
+
const follow = followControls(doc, w, controller, timeline);
|
|
241
|
+
bar.append(back, play, forward, speed, loop, range, clock, ...follow.parts);
|
|
240
242
|
hint.textContent = source.help === "turn" ? (stepMode ? w.keysTurnStep : w.keysTurn) : stepMode ? w.keysStep : w.keys;
|
|
241
243
|
if (source.labelled !== undefined) {
|
|
242
244
|
source.labelled.setAttribute("aria-label", w.region);
|
|
@@ -267,6 +269,7 @@ export function replayDriver(doc, timeline, settings, source) {
|
|
|
267
269
|
loop.setAttribute("aria-pressed", String(s.loop));
|
|
268
270
|
source.holder.dataset.state = s.ended ? "ended" : s.playing ? "playing" : "paused";
|
|
269
271
|
source.holder.dataset.motion = s.motion;
|
|
272
|
+
follow.sync(s);
|
|
270
273
|
};
|
|
271
274
|
sync(undefined);
|
|
272
275
|
};
|
|
@@ -432,13 +435,27 @@ export function createReplayView(host, timeline, settings, initial) {
|
|
|
432
435
|
function drawStage(t) {
|
|
433
436
|
stage.innerHTML = drawFrame(options, timeline, t, settings);
|
|
434
437
|
svg = stage.querySelector("svg.kyogijo");
|
|
438
|
+
// (A drawing written again is shown at the camera's shot.)
|
|
439
|
+
camera.again();
|
|
435
440
|
}
|
|
436
|
-
const
|
|
441
|
+
const camera = flatFollow({ svg: () => svg, options: () => options, timeline, look: settings, now: () => performance.now() });
|
|
442
|
+
const driver = replayDriver(doc, timeline, settings, {
|
|
443
|
+
paint: (t, change) => {
|
|
444
|
+
painter.paint(t);
|
|
445
|
+
if (change !== undefined)
|
|
446
|
+
camera.paint(change);
|
|
447
|
+
},
|
|
448
|
+
language,
|
|
449
|
+
keysOn: root,
|
|
450
|
+
holder: root,
|
|
451
|
+
labelled: stage,
|
|
452
|
+
});
|
|
437
453
|
const { style, hint, bar, live } = driver.parts;
|
|
438
454
|
root.append(style, stage, hint, bar, live);
|
|
439
455
|
host.replaceChildren(root);
|
|
440
456
|
drawStage(driver.controller.state().t);
|
|
441
457
|
painter.bind(driver.controller.state().t);
|
|
458
|
+
camera.paint({ state: driver.controller.state(), reason: "follow", crossed: [] });
|
|
442
459
|
if (settings.autoplay === true && driver.controller.state().motion === "smooth" && doc.visibilityState !== "hidden")
|
|
443
460
|
driver.controller.play();
|
|
444
461
|
return {
|