@johnmorrisdotca/kyogijo-stadiums 0.59.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.
@@ -0,0 +1,46 @@
1
+ // Following the play on a 3D view: the shot of replay.follow.ts turned into what the view looks at (a point of the floor, and a zoom on top of the view's own), so that the auto-turn, the
2
+ // tip and every viewpoint go on as they were while the camera follows. The reader's own turn, tip or zoom holds it until Resume; a viewpoint or Reset also takes it back to the whole surface,
3
+ // in the same glide. It runs in the replay's own paint, inside the view's one animation frame, and never asks for a frame of its own.
4
+ import { outlineOf } from "./outline.js";
5
+ import { followRunner, zoomFor } from "./replay.follow.js";
6
+ import { replayPlacer, replaySnapshot } from "./replay.js";
7
+ /**
8
+ * What a 3D view looks at for a shot: the shot's middle as a point of the floor in the scene's pixels from its middle, and the zoom on top of the view's. The scene at a zoom of 1 holds more
9
+ * than the surface (the boards, the stands): the zoom is worked out against how much more, so that the closest shot frames the same share of the surface as a flat drawing's.
10
+ */
11
+ export function aimOf(shot, model, surface) {
12
+ const s = model.scale;
13
+ const room = Math.hypot(model.box.x1 - model.box.x0, model.box.y1 - model.box.y0) / Math.max(1e-9, Math.hypot(surface.w * s, surface.h * s));
14
+ return { focus: [(shot.x - model.centre[0]) * s, (shot.y - model.centre[1]) * s], zoom: zoomFor(shot.zoom, Math.min(3, room)) };
15
+ }
16
+ /** The camera of a replay on a 3D view. `floor` is the options the floor is drawn with (the replay's), or `undefined` before it is laid. */
17
+ export function turnFollow(view, controller, timeline, look, floor) {
18
+ const surface = () => outlineOf((floor() ?? view.model().options)).box;
19
+ const runner = followRunner({
20
+ timeline,
21
+ now: () => view.doc.defaultView?.performance.now() ?? 0,
22
+ at(t, stop) {
23
+ const place = replayPlacer(floor() ?? view.model().options, timeline);
24
+ const objects = replaySnapshot(timeline, t, look).objects;
25
+ const placed = place(objects.map((o) => [o.x, o.y]));
26
+ return { box: surface(), 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] };
27
+ },
28
+ show: (shot) => view.aim(shot === undefined ? undefined : aimOf(shot, view.model(), surface())),
29
+ });
30
+ const stop = view.onUser((kind) => {
31
+ if (controller().state().follow === "off")
32
+ return;
33
+ controller().holdFollow(true);
34
+ if (kind === "preset") {
35
+ runner.reset();
36
+ view.aim(undefined, true);
37
+ }
38
+ });
39
+ return {
40
+ paint: (change) => runner.paint(change),
41
+ destroy() {
42
+ stop();
43
+ view.aim(undefined);
44
+ },
45
+ };
46
+ }
@@ -33,7 +33,7 @@ export interface Layout {
33
33
  * which, so that a page need not write them at all; without it every plane is laid out, as a still picture needs. `raster` is the share of its size an element is made at (`rasterOf`): each element's matrix widens it back, so that the
34
34
  * picture is the same and the layers hold `raster` squared as many pixels.
35
35
  */
36
- export declare function layoutOf(model: TurnModel, view: TurnView, width: number, height: number, lens?: number, cull?: boolean, raster?: Raster): Layout;
36
+ export declare function layoutOf(model: TurnModel, view: TurnView, width: number, height: number, lens?: number, cull?: boolean, raster?: Raster, focus?: readonly [number, number] | readonly [number, number, number]): Layout;
37
37
  /** One plane as an element: its box in pixels and the SVG drawn on it in feet. */
38
38
  export declare function planeMarkup(p: Plane, scale: number, transform: string, stack: number, fade?: number, raster?: number): string;
39
39
  /**
@@ -96,10 +96,10 @@ const turnedAway = (m, side) => side * (m[0] * (m[5] * m[15] - m[13] * m[7]) - m
96
96
  * which, so that a page need not write them at all; without it every plane is laid out, as a still picture needs. `raster` is the share of its size an element is made at (`rasterOf`): each element's matrix widens it back, so that the
97
97
  * picture is the same and the layers hold `raster` squared as many pixels.
98
98
  */
99
- export function layoutOf(model, view, width, height, lens, cull = false, raster = 1) {
99
+ export function layoutOf(model, view, width, height, lens, cull = false, raster = 1, focus) {
100
100
  const distance = eyeDistance(width, height, lens ?? model.lens);
101
101
  const at = fit(model.box, { ...view, zoom: 1 }, width, height, distance);
102
- const camera = cameraMatrix(view, at, distance);
102
+ const camera = cameraMatrix(view, at, distance, focus);
103
103
  const s = model.scale;
104
104
  const centre = model.centre;
105
105
  const known = prepare(model);
@@ -1,4 +1,5 @@
1
1
  import type { KyogijoReplay } from "./mount.ts";
2
+ import { type Aim } from "./turn.dom.ts";
2
3
  import type { ReplaySettings, ReplayTimeline } from "./replay.types.ts";
3
4
  import type { TurnModel } from "./turn.model.ts";
4
5
  import { type TurnReplayLook } from "./turn.replay.ts";
@@ -13,6 +14,10 @@ export interface TurnInternals {
13
14
  setDecorate(decorate: ((model: TurnModel) => TurnModel) | undefined, after?: () => void): void;
14
15
  /** Paint the view as it is now: the planes whose place changed are written, the others are not touched. */
15
16
  repaint(): void;
17
+ /** Look at a point of the floor with a zoom on top of the view's (a camera following the play), at once or in a glide; `undefined` gives the view back. Painted at the next `repaint`. */
18
+ aim(next: Aim | undefined, glide?: boolean): void;
19
+ /** Be told when the reader turns, tips or zooms the view (`turn`) or presses a viewpoint or Reset (`preset`). */
20
+ onUser(listener: (kind: "preset" | "turn") => void): () => void;
16
21
  /** The view's own frame requests, which the auto-turn shares: one animation frame a frame, whatever runs. */
17
22
  requestFrame(callback: FrameRequestCallback): number;
18
23
  cancelFrame(id: number): void;
@@ -2,6 +2,7 @@ import { createReplayView, replayDriver, replayPainter } from "./replay.dom.js";
2
2
  import { useTurnReplay } from "./turn.dom.js";
3
3
  import { drawFrameOf, replayPlacer, replaySnapshot } from "./replay.js";
4
4
  import { boardFollower } from "./turn.replay.board.js";
5
+ import { turnFollow } from "./turn.follow.js";
5
6
  import { keepOnFloor, moveDisc, replayTurnModel } from "./turn.replay.js";
6
7
  /** 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. */
7
8
  export function attachTurnReplay(view, timeline, settings) {
@@ -57,6 +58,8 @@ export function attachTurnReplay(view, timeline, settings) {
57
58
  }
58
59
  }
59
60
  };
61
+ // The camera that follows the play.
62
+ const camera = turnFollow(view, () => driver.controller, timeline, look, () => laid?.floorOptions);
60
63
  const wrap = doc.createElement("div");
61
64
  wrap.className = "ky-replay ky-turn-replay";
62
65
  const driver = replayDriver(doc, timeline, settings, {
@@ -65,6 +68,8 @@ export function attachTurnReplay(view, timeline, settings) {
65
68
  place(t);
66
69
  if (look.board !== false)
67
70
  board.follow(t, view.model(), change);
71
+ if (change !== undefined)
72
+ camera.paint(change);
68
73
  view.repaint();
69
74
  },
70
75
  language,
@@ -119,12 +124,15 @@ export function attachTurnReplay(view, timeline, settings) {
119
124
  laid = replayTurnModel(model, timeline, driver.controller.state().t, look);
120
125
  return laid.model;
121
126
  }, after);
127
+ camera.paint({ state: driver.controller.state(), reason: "follow", crossed: [] });
128
+ view.repaint();
122
129
  if (settings.autoplay === true && driver.controller.state().motion === "smooth" && doc.visibilityState !== "hidden")
123
130
  driver.controller.play();
124
131
  return {
125
132
  ...driver.controller,
126
133
  destroy() {
127
134
  driver.destroy();
135
+ camera.destroy();
128
136
  root.removeEventListener("keydown", onStageKey, true);
129
137
  if (described === null)
130
138
  stage.removeAttribute("aria-describedby");
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.59.0";
2
+ export declare const VERSION = "0.60.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.59.0";
2
+ export const VERSION = "0.60.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnmorrisdotca/kyogijo-stadiums",
3
- "version": "0.59.0",
3
+ "version": "0.60.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",