@johnmorrisdotca/kyogijo-stadiums 0.29.0 → 0.31.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,145 @@
1
+ // The game of the fly-through: a short play on the rink, made up for the picture and said to be: five skaters and a goalie a side, a breakout, two passes, a rush and a goal, as a replay's timeline
2
+ // (`ReplayTimeline` of `replay.types.ts`, the format the package already animates), so that the dots, their numbers, the puck, its trail and the events are drawn by the replay's own layers. It is NOT a real game's tracking data
3
+ // and nothing in it is a real player, a real team or a real play: the shirt numbers are only numbers. Pure and deterministic: the same moment always gives the same frame.
4
+ import { renderLayers } from "./layers.js";
5
+ import { replayLayers } from "./replay.js";
6
+ /** How long the play runs, and how long it is held at the end (the goal's marks still on the ice) before it starts again. */
7
+ export const PLAY_SECONDS = 10;
8
+ export const PLAY_HOLD = 1.5;
9
+ /** The seconds of the whole run, the play and the hold: what the loop's period is. */
10
+ export const PLAY_PERIOD = PLAY_SECONDS + PLAY_HOLD;
11
+ /** The moment a still of the play (for a reader who prefers reduced motion) is taken at: the shot has just been taken. */
12
+ export const PLAY_STILL = 6.4;
13
+ /** The seconds the dots fade in and out at the ends of a run, so that the play does not jump back to its start in view. */
14
+ export const PLAY_FADE = 0.5;
15
+ /** The home skaters, as waypoints `[seconds, x, y]`: the home team attacks to the right. */
16
+ const HOME = {
17
+ h4: { number: 4, way: [[0, 24, 26], [1.2, 30, 30], [3.5, 60, 24], [6, 110, 30], [8, 130, 35], [10, 135, 38]] },
18
+ h6: { number: 6, way: [[0, 24, 58], [1.5, 40, 60], [3.5, 75, 58], [6, 120, 55], [8, 140, 50], [10, 145, 48]] },
19
+ h17: { number: 17, way: [[0, 45, 12], [1.2, 48, 12], [2.6, 85, 14], [4, 130, 12], [5.4, 165, 18], [6.8, 176, 28], [10, 170, 30]] },
20
+ h9: { number: 9, way: [[0, 60, 40], [2.4, 80, 40], [3.6, 105, 36], [5, 150, 42], [6.2, 166, 42], [7, 180, 44], [10, 178, 46]] },
21
+ h22: { number: 22, way: [[0, 55, 70], [2.6, 90, 70], [4.2, 135, 64], [5.6, 165, 60], [6.4, 178, 50], [10, 176, 52]] },
22
+ };
23
+ /** The away skaters, who fall back into their own end as the rush comes. */
24
+ const AWAY = {
25
+ a11: { number: 11, way: [[0, 95, 40], [2, 110, 42], [4, 150, 44], [6, 170, 43], [10, 180, 44]] },
26
+ a14: { number: 14, way: [[0, 100, 20], [3, 125, 22], [5, 160, 24], [6.5, 175, 26], [10, 178, 30]] },
27
+ a27: { number: 27, way: [[0, 98, 65], [3, 128, 62], [5, 158, 62], [6.5, 176, 62], [10, 178, 60]] },
28
+ a5: { number: 5, way: [[0, 150, 30], [4, 160, 30], [6, 172, 34], [10, 180, 32]] },
29
+ a8: { number: 8, way: [[0, 150, 55], [4, 162, 54], [6, 172, 52], [10, 180, 54]] },
30
+ };
31
+ /** The goalies: the home one stays on his line, the away one moves across his crease as the shot comes. */
32
+ const HOME_GOALIE = [11, 42.5];
33
+ const AWAY_GOAL = [189, 42.5];
34
+ /** A point of a way at a time: a cubic through the waypoints with the slope between the neighbours at each, so that a skater never stops or turns on a corner. */
35
+ export function at(way, t) {
36
+ const last = way.length - 1;
37
+ if (t <= way[0][0])
38
+ return [way[0][1], way[0][2]];
39
+ if (t >= way[last][0])
40
+ return [way[last][1], way[last][2]];
41
+ let i = 0;
42
+ while (i < last - 1 && t >= way[i + 1][0])
43
+ i += 1;
44
+ const [t0, x0, y0] = way[i];
45
+ const [t1, x1, y1] = way[i + 1];
46
+ const slope = (k) => {
47
+ const a = way[Math.max(0, k - 1)];
48
+ const b = way[Math.min(last, k + 1)];
49
+ const span = b[0] - a[0];
50
+ return span === 0 ? [0, 0] : [(b[1] - a[1]) / span, (b[2] - a[2]) / span];
51
+ };
52
+ const [m0, m1] = [slope(i), slope(i + 1)];
53
+ const h = t1 - t0;
54
+ const s = (t - t0) / h;
55
+ const [s2, s3] = [s * s, s * s * s];
56
+ const [h00, h10, h01, h11] = [2 * s3 - 3 * s2 + 1, s3 - 2 * s2 + s, -2 * s3 + 3 * s2, s3 - s2];
57
+ return [h00 * x0 + h10 * h * m0[0] + h01 * x1 + h11 * h * m1[0], h00 * y0 + h10 * h * m0[1] + h01 * y1 + h11 * h * m1[1]];
58
+ }
59
+ const lerp = (a, b, u) => [a[0] + (b[0] - a[0]) * u, a[1] + (b[1] - a[1]) * u];
60
+ const round = (n) => Math.round(n * 100) / 100;
61
+ const LEGS = [
62
+ { from: 0, to: 1.2, carrier: "h4" },
63
+ { from: 1.2, to: 1.8, carrier: null },
64
+ { from: 1.8, to: 2.6, carrier: "h17" },
65
+ { from: 2.6, to: 3.6, carrier: null },
66
+ { from: 3.6, to: 6.2, carrier: "h9" },
67
+ { from: 6.2, to: 6.55, carrier: null },
68
+ { from: 6.55, to: PLAY_SECONDS, carrier: "net" },
69
+ ];
70
+ const WAYS = Object.fromEntries([...Object.entries(HOME), ...Object.entries(AWAY)].map(([id, one]) => [id, one.way]));
71
+ const where = (id, t) => (id === "net" ? AWAY_GOAL : at(WAYS[id], t));
72
+ /** The puck at a time. */
73
+ export function puckAt(t) {
74
+ const time = Math.min(Math.max(t, 0), PLAY_SECONDS);
75
+ const index = LEGS.findIndex((leg) => time <= leg.to);
76
+ const leg = LEGS[index < 0 ? LEGS.length - 1 : index];
77
+ const carried = (id, when) => {
78
+ if (id === "net")
79
+ return AWAY_GOAL;
80
+ const [x, y] = where(id, when);
81
+ return [x + 2.5, y];
82
+ };
83
+ if (leg.carrier !== null)
84
+ return carried(leg.carrier, time);
85
+ // A pass or a shot goes from the carrier before it, at its start, to the carrier after it, at its end.
86
+ const [before, after] = [LEGS[LEGS.indexOf(leg) - 1], LEGS[LEGS.indexOf(leg) + 1]];
87
+ return lerp(carried(before.carrier, leg.from), after.carrier === "net" ? AWAY_GOAL : carried(after.carrier, leg.to), (time - leg.from) / (leg.to - leg.from));
88
+ }
89
+ /** The away goalie's place: across his crease a little as the shot is taken, and back. */
90
+ function awayGoalie(t) {
91
+ const bump = Math.max(0, 1 - Math.abs(t - 6.45) / 0.6);
92
+ return [AWAY_GOAL[0] - 1, AWAY_GOAL[1] - 3 * bump];
93
+ }
94
+ /** Who has the puck at a time, as a replay says it: a skater's id, or `null` while it is passed or shot. */
95
+ const possessionAt = (t) => {
96
+ const leg = LEGS.find((one) => t >= one.from && t < one.to);
97
+ return leg === undefined || leg.carrier === null || leg.carrier === "net" ? null : leg.carrier;
98
+ };
99
+ /** The frames of the play, a tenth of a second apart. */
100
+ function frames() {
101
+ const out = [];
102
+ for (let step = 0; step <= PLAY_SECONDS * 10; step += 1) {
103
+ const t = step / 10;
104
+ const objects = [];
105
+ for (const [id, one] of Object.entries(HOME))
106
+ objects.push({ id, team: "home", number: one.number, x: round(at(one.way, t)[0]), y: round(at(one.way, t)[1]) });
107
+ objects.push({ id: "h30", team: "home", number: 30, x: HOME_GOALIE[0], y: HOME_GOALIE[1] });
108
+ for (const [id, one] of Object.entries(AWAY))
109
+ objects.push({ id, team: "away", number: one.number, x: round(at(one.way, t)[0]), y: round(at(one.way, t)[1]) });
110
+ const goalie = awayGoalie(t);
111
+ objects.push({ id: "a35", team: "away", number: 35, x: round(goalie[0]), y: round(goalie[1]) });
112
+ const puck = puckAt(t);
113
+ objects.push({ id: "puck", team: "puck", x: round(puck[0]), y: round(puck[1]) });
114
+ const holder = possessionAt(t);
115
+ out.push({ t, objects, possession: holder });
116
+ }
117
+ return out;
118
+ }
119
+ const EVENTS = [
120
+ { t: 1.2, kind: "pass", team: "home", by: "h4", to: "h17", until: 1.8 },
121
+ { t: 2.6, kind: "pass", team: "home", by: "h17", to: "h9", until: 3.6 },
122
+ { t: 6.2, kind: "shot", team: "home", by: "h9", x2: AWAY_GOAL[0], y2: AWAY_GOAL[1], until: 6.55 },
123
+ { t: 6.55, kind: "goal", team: "home", by: "h9", x: AWAY_GOAL[0], y: AWAY_GOAL[1] },
124
+ ];
125
+ /** The play, as a replay's timeline in the surface's own feet. It is made once. */
126
+ export const ILLUSTRATIVE_PLAY = Object.freeze({ frames: frames(), events: EVENTS });
127
+ /** How the play is drawn: a short trail behind the puck, the dots large enough to see from the stands, events that stay a moment. */
128
+ export const PLAY_LOOK = Object.freeze({ trail: 1.2, hold: 1.5, scale: 1.7, numbers: true });
129
+ /** Where a moment of a run is in the play, looping: `seconds` after the run began, brought into one period. */
130
+ export const playMoment = (seconds) => ((seconds % PLAY_PERIOD) + PLAY_PERIOD) % PLAY_PERIOD;
131
+ /** How much of the play shows at a moment of a run: it fades in at the start and out at the end of each run. 0 to 1. */
132
+ export function playAlpha(moment) {
133
+ const m = Math.min(Math.max(moment, 0), PLAY_PERIOD);
134
+ return Math.min(1, m / PLAY_FADE, (PLAY_PERIOD - m) / PLAY_FADE);
135
+ }
136
+ /**
137
+ * The play at a moment of a run, as the layers' SVG (a `g.ky-layers` in the drawing's own feet, to go inside an `svg.kyogijo` with the floor's viewBox): the dots with their numbers, the puck and its trail, and the events that are on
138
+ * the ice at that time, drawn by the replay's own layers. The same moment always gives the same string.
139
+ */
140
+ export function playMarkup(moment, options) {
141
+ const t = Math.min(playMoment(moment), PLAY_SECONDS);
142
+ const language = options.language === "ja" ? "ja" : "en";
143
+ const surface = { sport: "nhl", id: options.id, language };
144
+ return renderLayers(surface, replayLayers(ILLUSTRATIVE_PLAY, t, PLAY_LOOK, language)).svg;
145
+ }
@@ -75,6 +75,7 @@ export function flythroughMarkup(input) {
75
75
  `<div class="kf-layer kf-3d" inert></div>` +
76
76
  `<div class="kf-pin" aria-hidden="true"></div>` +
77
77
  `<div class="kf-scale" aria-hidden="true"><span class="kf-scale-bar"></span><span class="kf-scale-text"></span></div>` +
78
+ `<div class="kf-badge" aria-hidden="true">${esc(words.illustrative)}</div>` +
78
79
  `<div class="kf-caption" aria-hidden="true"><span class="kf-title">${esc(intro.name)}</span><span class="kf-text">${esc(intro.text)}</span></div>` +
79
80
  `</div>` +
80
81
  `<p class="kf-help" id="${esc(input.id)}-help">${esc(words.keys)}</p>` +
@@ -1,15 +1,16 @@
1
1
  /** The stages of the fly-through, in order. */
2
- export type StageId = "map" | "city" | "arena" | "bowl" | "rink";
2
+ export type StageId = "map" | "city" | "arena" | "bowl" | "rink" | "game";
3
3
  /** The stages that exist, in the order of the path. */
4
4
  export declare const STAGE_IDS: readonly StageId[];
5
5
  /** The channels of the camera. */
6
- export declare const CHANNELS: readonly ["logSpan", "ax", "ay", "north", "map", "site", "arena", "yaw", "pitch", "logZoom", "logLens", "roof"];
6
+ export declare const CHANNELS: readonly ["logSpan", "ax", "ay", "north", "map", "site", "arena", "yaw", "pitch", "logZoom", "logLens", "roof", "game"];
7
7
  export type Channel = (typeof CHANNELS)[number];
8
8
  /**
9
9
  * Where the camera is. `logSpan` is the natural logarithm of the metres the container's width shows (so that a zoom is a straight line); `ax` and `ay` are where the arena stands on the screen, as a
10
10
  * share of the width and of the height from the middle; `north` is how far the map and the site plan are turned, in degrees clockwise (the rink's long side lies along the page when it reaches the
11
11
  * venue's); `map`, `site` and `arena` are how much of the map, of the site plan and of the 3D arena shows (0 to 1); `yaw`, `pitch` and `logZoom` are the 3D view's turn and tip in degrees and the natural logarithm of its zoom;
12
- * `logLens` is the natural logarithm of the eye's lens, as a multiple of the stage's longer side (a long lens looks from far outside, a short one from inside the bowl) and `roof` is how much of the building's roof shows (0 to 1).
12
+ * `logLens` is the natural logarithm of the eye's lens, as a multiple of the stage's longer side (a long lens looks from far outside, a short one from inside the bowl), `roof` is how much of the building's roof shows and `game` how much of the
13
+ * play on the ice shows (each 0 to 1).
13
14
  */
14
15
  export type Camera = Record<Channel, number>;
15
16
  /** What the path needs to know of the page and the venue. */
@@ -92,7 +93,7 @@ export declare const stagesOf: (flight: Flight) => StageId[];
92
93
  export declare const turnPeriod: (flight: Flight) => number;
93
94
  /** A moment after the end brought back by whole turns of the arena to one near the end: the same view, and a time that does not grow without end. */
94
95
  export declare function foldTime(flight: Flight, time: number): number;
95
- /** How a path is paced: the seconds of each part. `hold` is how long the arena turns, whole, before the camera goes in; `over` and `settle` the two parts of the way into the bowl; `stay` how long a still run of the rink's turn is kept before the end. */
96
+ /** How a path is paced: the seconds of each part. `hold` is how long the arena turns, whole, before the camera goes in; `over` and `settle` the two parts of the way into the bowl; `stay` how long the rink turns bare before the play comes onto it, and `fade` how long the play takes to show. */
96
97
  export declare const PACE: {
97
98
  readonly dwell: 1.6;
98
99
  readonly step: 1.25;
@@ -105,6 +106,7 @@ export declare const PACE: {
105
106
  readonly over: 2.2;
106
107
  readonly settle: 2.2;
107
108
  readonly stay: 2;
109
+ readonly fade: 0.6;
108
110
  };
109
111
  /**
110
112
  * The path from the map to the rink, for the stages asked (all by default): the map holds a moment, then the camera falls to the city in four even steps of the logarithm of the distance (the
@@ -5,9 +5,9 @@
5
5
  // Node, tested frame by frame, and handed to the mount, which draws it.
6
6
  import { LENS, LONG_PRESETS, presetView } from "./turn.camera.js";
7
7
  /** The stages that exist, in the order of the path. */
8
- export const STAGE_IDS = ["map", "city", "arena", "bowl", "rink"];
8
+ export const STAGE_IDS = ["map", "city", "arena", "bowl", "rink", "game"];
9
9
  /** The channels of the camera. */
10
- export const CHANNELS = ["logSpan", "ax", "ay", "north", "map", "site", "arena", "yaw", "pitch", "logZoom", "logLens", "roof"];
10
+ export const CHANNELS = ["logSpan", "ax", "ay", "north", "map", "site", "arena", "yaw", "pitch", "logZoom", "logLens", "roof", "game"];
11
11
  const clamp = (value, low, high) => Math.min(high, Math.max(low, value));
12
12
  const lerp = (a, b, t) => a + (b - a) * t;
13
13
  /** Zero to one with a zero slope at both ends. */
@@ -53,34 +53,33 @@ function hermite(times, values, slopes, time) {
53
53
  const s3 = s2 * s;
54
54
  return (2 * s3 - 3 * s2 + 1) * values[i] + (s3 - 2 * s2 + s) * h * slopes[i] + (-2 * s3 + 3 * s2) * values[i + 1] + (s3 - s2) * h * slopes[i + 1];
55
55
  }
56
- /** The speeds of every channel at every keyframe, worked out once for a path. */
57
- const SLOPES = new WeakMap();
58
- function slopesOf(keys) {
59
- const known = SLOPES.get(keys);
56
+ const TABLES = new WeakMap();
57
+ function tableOf(keys) {
58
+ const known = TABLES.get(keys);
60
59
  if (known !== undefined)
61
60
  return known;
62
61
  const times = keys.map((key) => key.t);
63
- const made = {};
62
+ const values = {};
63
+ const slopes = {};
64
64
  for (const channel of CHANNELS) {
65
- const slopes = monotoneSlopes(times, keys.map((key) => key.camera[channel]));
65
+ values[channel] = keys.map((key) => key.camera[channel]);
66
+ slopes[channel] = monotoneSlopes(times, values[channel]);
66
67
  keys.forEach((key, i) => {
67
68
  const own = key.slope?.[channel];
68
69
  if (own !== undefined)
69
- slopes[i] = own;
70
+ slopes[channel][i] = own;
70
71
  });
71
- made[channel] = slopes;
72
72
  }
73
- SLOPES.set(keys, made);
73
+ const made = { times, values, slopes };
74
+ TABLES.set(keys, made);
74
75
  return made;
75
76
  }
76
77
  /** The camera at a moment of a path. After the last keyframe the arena goes on turning and nothing else moves. Before the first, the first. */
77
78
  export function cameraAt(flight, time) {
78
- const { keys } = flight;
79
- const slopes = slopesOf(keys);
80
- const times = keys.map((key) => key.t);
79
+ const { times, values, slopes } = tableOf(flight.keys);
81
80
  const out = {};
82
81
  for (const channel of CHANNELS)
83
- out[channel] = hermite(times, keys.map((key) => key.camera[channel]), slopes[channel], time);
82
+ out[channel] = hermite(times, values[channel], slopes[channel], time);
84
83
  if (time > flight.end)
85
84
  out.yaw += flight.tail * (time - flight.end);
86
85
  return out;
@@ -107,9 +106,9 @@ export function foldTime(flight, time) {
107
106
  return flight.end + ((time - flight.end) % period);
108
107
  }
109
108
  /** The camera's defaults: the map, from far above, with nothing of the arena showing. */
110
- const BASE = { logSpan: 0, ax: 0, ay: 0, north: 0, map: 1, site: 0, arena: 0, yaw: 0, pitch: 0, logZoom: 0, logLens: Math.log(LENS), roof: 1 };
111
- /** How a path is paced: the seconds of each part. `hold` is how long the arena turns, whole, before the camera goes in; `over` and `settle` the two parts of the way into the bowl; `stay` how long a still run of the rink's turn is kept before the end. */
112
- export const PACE = { dwell: 1.6, step: 1.25, cityDwell: 0.9, approach: 1.6, rise: 0.8, hand: 0.7, tilt: 3.2, hold: 3.5, over: 2.2, settle: 2.2, stay: 2 };
109
+ const BASE = { logSpan: 0, ax: 0, ay: 0, north: 0, map: 1, site: 0, arena: 0, yaw: 0, pitch: 0, logZoom: 0, logLens: Math.log(LENS), roof: 1, game: 0 };
110
+ /** How a path is paced: the seconds of each part. `hold` is how long the arena turns, whole, before the camera goes in; `over` and `settle` the two parts of the way into the bowl; `stay` how long the rink turns bare before the play comes onto it, and `fade` how long the play takes to show. */
111
+ export const PACE = { dwell: 1.6, step: 1.25, cityDwell: 0.9, approach: 1.6, rise: 0.8, hand: 0.7, tilt: 3.2, hold: 3.5, over: 2.2, settle: 2.2, stay: 2, fade: 0.6 };
113
112
  /**
114
113
  * The path from the map to the rink, for the stages asked (all by default): the map holds a moment, then the camera falls to the city in four even steps of the logarithm of the distance (the
115
114
  * map turning as it comes to the venue's own orientation and giving way to the site plan), holds there, comes in until the plan is as large as the 3D arena seen from above, the arena rises out of it,
@@ -148,7 +147,7 @@ export function flightPlan(input) {
148
147
  // Where the turn has got to at each key of the arena, the bowl and the rink: the speed changes from `turn` to `rinkTurn` along the way, and each yaw is the one the two speeds either side of it come to.
149
148
  const yaws = [];
150
149
  let yaw = 0;
151
- for (const [from, to, seconds] of [[0, turn, PACE.tilt], [turn, turn, PACE.hold], [turn, (turn + rinkTurn) / 2, PACE.over], [(turn + rinkTurn) / 2, rinkTurn, PACE.settle], [rinkTurn, rinkTurn, PACE.stay]]) {
150
+ for (const [from, to, seconds] of [[0, turn, PACE.tilt], [turn, turn, PACE.hold], [turn, (turn + rinkTurn) / 2, PACE.over], [(turn + rinkTurn) / 2, rinkTurn, PACE.settle], [rinkTurn, rinkTurn, PACE.stay], [rinkTurn, rinkTurn, PACE.fade]]) {
152
151
  yaw -= ((from + to) / 2) * seconds;
153
152
  yaws.push(yaw);
154
153
  }
@@ -171,9 +170,13 @@ export function flightPlan(input) {
171
170
  },
172
171
  {
173
172
  id: "rink",
173
+ keys: [{ dt: PACE.settle, camera: { ...near, arena: 1, site: 0, yaw: yaws[3], pitch: seat.pitch, logZoom: ln(seat.zoom), logLens: ln(seat.lens), roof: 0, game: 0 }, slope: { yaw: -rinkTurn } }],
174
+ },
175
+ {
176
+ id: "game",
174
177
  keys: [
175
- { dt: PACE.settle, camera: { ...near, arena: 1, site: 0, yaw: yaws[3], pitch: seat.pitch, logZoom: ln(seat.zoom), logLens: ln(seat.lens), roof: 0 }, slope: { yaw: -rinkTurn } },
176
- { dt: PACE.stay, camera: { ...near, arena: 1, site: 0, yaw: yaws[4], pitch: seat.pitch, logZoom: ln(seat.zoom), logLens: ln(seat.lens), roof: 0 }, slope: { yaw: -rinkTurn } },
178
+ { dt: PACE.stay, camera: { ...near, arena: 1, site: 0, yaw: yaws[4], pitch: seat.pitch, logZoom: ln(seat.zoom), logLens: ln(seat.lens), roof: 0, game: 0 }, slope: { yaw: -rinkTurn } },
179
+ { dt: PACE.fade, camera: { ...near, arena: 1, site: 0, yaw: yaws[5], pitch: seat.pitch, logZoom: ln(seat.zoom), logLens: ln(seat.lens), roof: 0, game: 1 }, slope: { yaw: -rinkTurn } },
177
180
  ],
178
181
  },
179
182
  ];
@@ -208,7 +211,8 @@ export function flightPlan(input) {
208
211
  city: { ...BASE, ...arrive },
209
212
  arena: { ...BASE, ...near, map: 0, site: 0, arena: 1, yaw: view.yaw, pitch: view.pitch, logZoom: 0 },
210
213
  bowl: { ...BASE, ...near, map: 0, site: 0, arena: 1, yaw: view.yaw, pitch: lerp(view.pitch, seat.pitch, 0.5), logZoom: ln(seat.zoom) * 0.55, logLens: lerp(ln(LENS), ln(seat.lens), 0.6), roof: 0 },
211
- rink: { ...BASE, ...near, map: 0, site: 0, arena: 1, yaw: view.yaw, pitch: seat.pitch, logZoom: ln(seat.zoom), logLens: ln(seat.lens), roof: 0 },
214
+ rink: { ...BASE, ...near, map: 0, site: 0, arena: 1, yaw: view.yaw, pitch: seat.pitch, logZoom: ln(seat.zoom), logLens: ln(seat.lens), roof: 0, game: 0 },
215
+ game: { ...BASE, ...near, map: 0, site: 0, arena: 1, yaw: view.yaw, pitch: seat.pitch, logZoom: ln(seat.zoom), logLens: ln(seat.lens), roof: 0, game: 1 },
212
216
  };
213
217
  return {
214
218
  keys,
@@ -1,2 +1,2 @@
1
1
  /** The fly-through's stylesheet. */
2
- export declare const FLYTHROUGH_STYLE = ".kf{--kf-bg:#ffffff;--kf-ink:#10151b;--kf-muted:#434b54;--kf-panel:rgba(255,255,255,.9);--kf-line:#b9b3a3;--kf-accent:#1f5fa8;--kf-pin:#c0392b;--kf-grid:rgba(60,70,80,.2);--kf-sea:#cfe3ee;--kf-button:#ffffff;--kf-button-on:#10151b;--kf-button-on-ink:#ffffff;--kf-land:#e9e1c8;--kf-coast:#5d5a50;color:var(--kf-ink);font:16px/1.45 system-ui,-apple-system,\"Segoe UI\",\"Hiragino Sans\",\"Noto Sans JP\",sans-serif;color-scheme:light dark}\n@media (prefers-color-scheme:dark){.kf:not([data-theme=light]){--kf-bg:#18212a;--kf-ink:#f6f3ea;--kf-muted:#bcc3cb;--kf-panel:rgba(24,33,42,.9);--kf-line:#475463;--kf-accent:#7fb2ee;--kf-pin:#ff7a6b;--kf-grid:rgba(200,210,220,.22);--kf-sea:#14232f;--kf-button:#18212a;--kf-button-on:#f6f3ea;--kf-button-on-ink:#0d1218;--kf-land:#2f3b44;--kf-coast:#8a97a3}}\n.kf[data-theme=dark]{--kf-bg:#18212a;--kf-ink:#f6f3ea;--kf-muted:#bcc3cb;--kf-panel:rgba(24,33,42,.9);--kf-line:#475463;--kf-accent:#7fb2ee;--kf-pin:#ff7a6b;--kf-grid:rgba(200,210,220,.22);--kf-sea:#14232f;--kf-button:#18212a;--kf-button-on:#f6f3ea;--kf-button-on-ink:#0d1218;--kf-land:#2f3b44;--kf-coast:#8a97a3;color-scheme:dark}\n.kf[data-theme=light]{color-scheme:light}\n.kf *{box-sizing:border-box}\n.kf [hidden]{display:none!important}\n.kf-stage{position:relative;isolation:isolate;width:100%;aspect-ratio:16/10;min-height:260px;overflow:hidden;background:var(--kf-bg);border:1px solid var(--kf-line);border-radius:12px;outline-offset:2px;user-select:none;-webkit-user-select:none}\n.kf-stage:focus-visible{outline:3px solid var(--kf-accent)}\n@media (max-width:560px){.kf-stage{aspect-ratio:4/5}}\n.kf-layer{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none}\n.kf-map{overflow:hidden}\n.kf-map-box{position:absolute;transform-origin:50% 50%;background:var(--kf-sea)}\n.kf-map-box svg{display:block;width:100%;height:100%}\n.kf-map-box .chizu{--cz-sea:var(--kf-sea);--cz-land:var(--kf-land);--cz-line:var(--kf-coast)}\n.kf-site{overflow:hidden}\n.kf-grid{fill:none;stroke:var(--kf-grid);stroke-width:1}\n.kf-rink{fill:var(--kf-bg);fill-opacity:.9;stroke:var(--kf-muted);stroke-width:.6;vector-effect:non-scaling-stroke}\n.kf-3d{overflow:hidden}\n.kf-3d .ky-turn{height:100%}\n.kf-3d .ky-turn-view{aspect-ratio:auto;height:100%;min-height:0;border-radius:0;cursor:default;touch-action:auto}\n.kf-3d .ky-turn-help,.kf-3d .ky-turn-live{display:none}\n.kf-3d .ky-up-roof{filter:opacity(var(--kf-roof,1))}\n.kf-3d[data-roof=\"0\"] .ky-up-roof{visibility:hidden}\n.kf-pin{position:absolute;left:0;top:0;width:0;height:0;pointer-events:none}\n.kf-pin::before{content:\"\";position:absolute;left:-7px;top:-7px;width:14px;height:14px;border-radius:50%;background:var(--kf-pin);border:2px solid var(--kf-bg);box-shadow:0 0 0 2px var(--kf-pin)}\n.kf-scale{position:absolute;left:12px;bottom:12px;display:flex;flex-direction:column;gap:2px;align-items:flex-start;font-size:.8rem;color:var(--kf-ink);background:var(--kf-panel);padding:4px 8px;border-radius:8px;pointer-events:none}\n.kf-scale-bar{display:block;height:6px;border:2px solid var(--kf-ink);border-top:0}\n.kf-caption{position:absolute;right:12px;top:12px;max-width:min(26rem,calc(100% - 24px));padding:8px 12px;border-radius:10px;background:var(--kf-panel);color:var(--kf-ink);pointer-events:none}\n.kf-title{display:block;font-weight:800}\n.kf-text{display:block;font-size:.92rem;color:var(--kf-muted)}\n.kf-controls{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:10px 0 0}\n.kf-group{display:flex;flex-wrap:wrap;gap:8px;margin:0;padding:0;list-style:none}\n.kf button{font:inherit;font-size:1rem;font-weight:700;min-height:44px;min-width:44px;padding:0 14px;border-radius:12px;border:2px solid var(--kf-ink);background:var(--kf-button);color:var(--kf-ink);cursor:pointer}\n.kf button[aria-pressed=true],.kf button[aria-current=step]{background:var(--kf-button-on);color:var(--kf-button-on-ink)}\n.kf button:focus-visible{outline:3px solid var(--kf-accent);outline-offset:2px}\n.kf-help,.kf-credit,.kf-still{margin:8px 0 0;font-size:.85rem;color:var(--kf-muted)}\n.kf-live{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}";
2
+ export declare const FLYTHROUGH_STYLE = ".kf{--kf-bg:#ffffff;--kf-ink:#10151b;--kf-muted:#434b54;--kf-panel:rgba(255,255,255,.9);--kf-line:#b9b3a3;--kf-accent:#1f5fa8;--kf-pin:#c0392b;--kf-grid:rgba(60,70,80,.2);--kf-sea:#cfe3ee;--kf-button:#ffffff;--kf-button-on:#10151b;--kf-button-on-ink:#ffffff;--kf-land:#e9e1c8;--kf-coast:#5d5a50;color:var(--kf-ink);font:16px/1.45 system-ui,-apple-system,\"Segoe UI\",\"Hiragino Sans\",\"Noto Sans JP\",sans-serif;color-scheme:light dark}\n@media (prefers-color-scheme:dark){.kf:not([data-theme=light]){--kf-bg:#18212a;--kf-ink:#f6f3ea;--kf-muted:#bcc3cb;--kf-panel:rgba(24,33,42,.9);--kf-line:#475463;--kf-accent:#7fb2ee;--kf-pin:#ff7a6b;--kf-grid:rgba(200,210,220,.22);--kf-sea:#14232f;--kf-button:#18212a;--kf-button-on:#f6f3ea;--kf-button-on-ink:#0d1218;--kf-land:#2f3b44;--kf-coast:#8a97a3}}\n.kf[data-theme=dark]{--kf-bg:#18212a;--kf-ink:#f6f3ea;--kf-muted:#bcc3cb;--kf-panel:rgba(24,33,42,.9);--kf-line:#475463;--kf-accent:#7fb2ee;--kf-pin:#ff7a6b;--kf-grid:rgba(200,210,220,.22);--kf-sea:#14232f;--kf-button:#18212a;--kf-button-on:#f6f3ea;--kf-button-on-ink:#0d1218;--kf-land:#2f3b44;--kf-coast:#8a97a3;color-scheme:dark}\n.kf[data-theme=light]{color-scheme:light}\n.kf *{box-sizing:border-box}\n.kf [hidden]{display:none!important}\n.kf-stage{position:relative;isolation:isolate;width:100%;aspect-ratio:16/10;min-height:260px;overflow:hidden;background:var(--kf-bg);border:1px solid var(--kf-line);border-radius:12px;outline-offset:2px;user-select:none;-webkit-user-select:none}\n.kf-stage:focus-visible{outline:3px solid var(--kf-accent)}\n@media (max-width:560px){.kf-stage{aspect-ratio:4/5}}\n.kf-layer{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none}\n.kf-map{overflow:hidden}\n.kf-map-box{position:absolute;transform-origin:50% 50%;background:var(--kf-sea)}\n.kf-map-box svg{display:block;width:100%;height:100%}\n.kf-map-box .chizu{--cz-sea:var(--kf-sea);--cz-land:var(--kf-land);--cz-line:var(--kf-coast)}\n.kf-site{overflow:hidden}\n.kf-grid{fill:none;stroke:var(--kf-grid);stroke-width:1}\n.kf-rink{fill:var(--kf-bg);fill-opacity:.9;stroke:var(--kf-muted);stroke-width:.6;vector-effect:non-scaling-stroke}\n.kf-3d{overflow:hidden}\n.kf-3d .ky-turn{height:100%}\n.kf-3d .ky-turn-view{aspect-ratio:auto;height:100%;min-height:0;border-radius:0;cursor:default;touch-action:auto}\n.kf-3d .ky-turn-help,.kf-3d .ky-turn-live{display:none}\n.kf-3d .ky-up-roof{filter:opacity(var(--kf-roof,1))}\n.kf-3d[data-roof=\"0\"] .ky-up-roof{visibility:hidden}\n.kf-pin{position:absolute;left:0;top:0;width:0;height:0;pointer-events:none}\n.kf-pin::before{content:\"\";position:absolute;left:-7px;top:-7px;width:14px;height:14px;border-radius:50%;background:var(--kf-pin);border:2px solid var(--kf-bg);box-shadow:0 0 0 2px var(--kf-pin)}\n.kf-scale{position:absolute;left:12px;bottom:12px;display:flex;flex-direction:column;gap:2px;align-items:flex-start;font-size:.8rem;color:var(--kf-ink);background:var(--kf-panel);padding:4px 8px;border-radius:8px;pointer-events:none}\n.kf-scale-bar{display:block;height:6px;border:2px solid var(--kf-ink);border-top:0}\n.kf-caption{position:absolute;right:12px;top:12px;max-width:min(26rem,calc(100% - 24px));padding:8px 12px;border-radius:10px;background:var(--kf-panel);color:var(--kf-ink);pointer-events:none}\n.kf-title{display:block;font-weight:800}\n.kf-text{display:block;font-size:.92rem;color:var(--kf-muted)}\n.kf-badge{position:absolute;left:12px;top:12px;padding:3px 10px;border-radius:999px;background:var(--kf-panel);border:1px solid var(--kf-line);font-size:.8rem;font-weight:700;color:var(--kf-ink);pointer-events:none;opacity:0;visibility:hidden}\n.kf-game{position:absolute;left:0;top:0;transform-origin:0 0;pointer-events:none;z-index:1}\n.kf-game svg{display:block;width:100%;height:100%;overflow:visible}\n.kf-controls{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:10px 0 0}\n.kf-group{display:flex;flex-wrap:wrap;gap:8px;margin:0;padding:0;list-style:none}\n.kf button{font:inherit;font-size:1rem;font-weight:700;min-height:44px;min-width:44px;padding:0 14px;border-radius:12px;border:2px solid var(--kf-ink);background:var(--kf-button);color:var(--kf-ink);cursor:pointer}\n.kf button[aria-pressed=true],.kf button[aria-current=step]{background:var(--kf-button-on);color:var(--kf-button-on-ink)}\n.kf button:focus-visible{outline:3px solid var(--kf-accent);outline-offset:2px}\n.kf-help,.kf-credit,.kf-still{margin:8px 0 0;font-size:.85rem;color:var(--kf-muted)}\n.kf-live{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}";
@@ -33,6 +33,9 @@ export const FLYTHROUGH_STYLE = `.kf{${LIGHT};color:var(--kf-ink);font:16px/1.45
33
33
  .kf-caption{position:absolute;right:12px;top:12px;max-width:min(26rem,calc(100% - 24px));padding:8px 12px;border-radius:10px;background:var(--kf-panel);color:var(--kf-ink);pointer-events:none}
34
34
  .kf-title{display:block;font-weight:800}
35
35
  .kf-text{display:block;font-size:.92rem;color:var(--kf-muted)}
36
+ .kf-badge{position:absolute;left:12px;top:12px;padding:3px 10px;border-radius:999px;background:var(--kf-panel);border:1px solid var(--kf-line);font-size:.8rem;font-weight:700;color:var(--kf-ink);pointer-events:none;opacity:0;visibility:hidden}
37
+ .kf-game{position:absolute;left:0;top:0;transform-origin:0 0;pointer-events:none;z-index:1}
38
+ .kf-game svg{display:block;width:100%;height:100%;overflow:visible}
36
39
  .kf-controls{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:10px 0 0}
37
40
  .kf-group{display:flex;flex-wrap:wrap;gap:8px;margin:0;padding:0;list-style:none}
38
41
  .kf button{font:inherit;font-size:1rem;font-weight:700;min-height:44px;min-width:44px;padding:0 14px;border-radius:12px;border:2px solid var(--kf-ink);background:var(--kf-button);color:var(--kf-ink);cursor:pointer}
@@ -25,6 +25,8 @@ export interface FlythroughWords {
25
25
  /** The credit of the map and of the building's outline, drawn with the picture. */
26
26
  creditMap: string;
27
27
  creditPlain: string;
28
+ /** The label on the picture while the play is on the ice: it is made up for the picture. */
29
+ illustrative: string;
28
30
  /** The sentence on a still stage, for a reader who prefers reduced motion. */
29
31
  still: string;
30
32
  }
@@ -2,13 +2,14 @@ export const FLYTHROUGH_WORDS = {
2
2
  en: {
3
3
  title: "Fly-through: {arena}",
4
4
  region: "fly-through",
5
- about: "A fly-through from a map to {arena} in {place}: the map, the city, the arena in 3D, the way in over the stands and the rink seen from a seat. Play, pause, step back or forward and choose a stage with the buttons or the keys.",
5
+ about: "A fly-through from a map to {arena} in {place}: the map, the city, the arena in 3D, the way in over the stands, the rink seen from a seat and a short play on the ice. Play, pause, step back or forward and choose a stage with the buttons or the keys.",
6
6
  stage: {
7
7
  map: { name: "Map", text: "{arena} stands in {place}." },
8
8
  city: { name: "City", text: "The building of {arena} from above, as OpenStreetMap maps it." },
9
9
  arena: { name: "Arena", text: "{arena} in 3D, turning." },
10
10
  bowl: { name: "Bowl", text: "In over the stands, with the roof lifted away: a plausible bowl of {seats} seats in {tiers} tiers, not a seat map." },
11
11
  rink: { name: "Rink", text: "A view across the ice from the stands, turning; the place and the height of the seat are ours, not a real seat's." },
12
+ game: { name: "Game", text: "A short play made up for the picture, with five skaters and a goalie a side and a puck, moving on the ice. It is illustrative, not a real game's data." },
12
13
  },
13
14
  announce: "Stage {n} of {total}: {name}. {text}",
14
15
  play: "Play",
@@ -19,18 +20,20 @@ export const FLYTHROUGH_WORDS = {
19
20
  keys: "Space plays or pauses. Left and right arrows go back and forward a stage; the number keys choose a stage; Home starts again.",
20
21
  creditMap: "Map: Natural Earth (public domain), drawn with chizu. Building outline: (c) OpenStreetMap contributors, ODbL 1.0, openstreetmap.org/copyright.",
21
22
  creditPlain: "Building outline: (c) OpenStreetMap contributors, ODbL 1.0, openstreetmap.org/copyright.",
23
+ illustrative: "Illustrative play",
22
24
  still: "Each stage is shown as a still picture, because this device is set to reduce motion.",
23
25
  },
24
26
  ja: {
25
27
  title: "フライスルー:{arena}",
26
28
  region: "フライスルー",
27
- about: "地図から{place}の{arena}まで進むフライスルーです。地図、都市、3Dのアリーナ、スタンドの上空から中へ入る場面、客席から見たリンクの順に見られます。ボタンまたはキーで、再生・一時停止、前後の場面への移動、場面の選択ができます。",
29
+ about: "地図から{place}の{arena}まで進むフライスルーです。地図、都市、3Dのアリーナ、スタンドの上空から中へ入る場面、客席から見たリンク、氷上の短い模擬プレーの順に見られます。ボタンまたはキーで、再生・一時停止、前後の場面への移動、場面の選択ができます。",
28
30
  stage: {
29
31
  map: { name: "地図", text: "{arena}は{place}にあります。" },
30
32
  city: { name: "都市", text: "{arena}の建物を真上から見たところです(OpenStreetMapの記録による)。" },
31
33
  arena: { name: "アリーナ", text: "{arena}を3Dで回転させて表示しています。" },
32
34
  bowl: { name: "客席", text: "屋根を取り払い、スタンドの上空から中へ入ります。{seats}席・{tiers}層の推定のスタンドで、実際の座席表ではありません。" },
33
35
  rink: { name: "リンク", text: "スタンドからリンクを見渡した眺めで、ゆっくり回転します。視点の位置と高さは仮のもので、実在の座席ではありません。" },
36
+ game: { name: "試合", text: "絵として作った短い模擬プレーです。両チーム各5人のスケーターとゴーリー、そしてパックが氷上を動きます。実際の試合のデータではありません。" },
34
37
  },
35
38
  announce: "場面{n}/{total}:{name}。{text}",
36
39
  play: "再生",
@@ -41,6 +44,7 @@ export const FLYTHROUGH_WORDS = {
41
44
  keys: "スペースキーで再生・一時停止。左右の矢印キーで前後の場面へ、数字キーで場面を選び、Homeキーで最初に戻ります。",
42
45
  creditMap: "地図:Natural Earth(パブリックドメイン)、chizuで描画。建物の輪郭:(c) OpenStreetMap contributors、ODbL 1.0、openstreetmap.org/copyright。",
43
46
  creditPlain: "建物の輪郭:(c) OpenStreetMap contributors、ODbL 1.0、openstreetmap.org/copyright。",
47
+ illustrative: "模擬プレー(イメージ)",
44
48
  still: "この端末は動きを減らす設定のため、各場面を静止画で表示しています。",
45
49
  },
46
50
  };
@@ -1,2 +1,2 @@
1
- export { VENUE_ATTRIBUTION, STADIUMS, STADIUM_IDS, groundOfStadium, planStadium, stadiumBowl, stadiumBowlInside, stadiumById, stadiumByWikidata, stadiumConfigOf, stadiumOrientation, stadiumSeats } from "./stadiums.ts";
2
- export type { RealStadium, RealStadiumData, StadiumBasis, StadiumConfigData, StadiumConfigId, StadiumOrientation, StadiumOsm, StadiumPlan, StadiumRoof } from "./stadiums.ts";
1
+ export { VENUE_ATTRIBUTION, STADIUMS, STADIUM_IDS, groundOfStadium, planPark, planStadium, stadiumBowl, stadiumBowlInside, stadiumById, stadiumByWikidata, stadiumConfigOf, stadiumConfigs, stadiumFrameHeading, stadiumKind, stadiumNames, stadiumOrientation, stadiumSeats } from "./stadiums.ts";
2
+ export type { ParkPlan, RealStadium, RealStadiumData, StadiumBasis, StadiumConfigData, StadiumConfigId, StadiumKind, StadiumOrientation, StadiumOsm, StadiumPlan, StadiumRoof } from "./stadiums.ts";
@@ -1,2 +1,2 @@
1
- // The real stadiums: the outline of each stadium's building and the way its field lies (OpenStreetMap, ODbL 1.0) and a plausible bowl from the seats of each configuration, the bowls and the roof the sources give (Wikidata, Wikipedia).
2
- export { VENUE_ATTRIBUTION, STADIUMS, STADIUM_IDS, groundOfStadium, planStadium, stadiumBowl, stadiumBowlInside, stadiumById, stadiumByWikidata, stadiumConfigOf, stadiumOrientation, stadiumSeats } from "./stadiums.js";
1
+ // The real stadiums and ballparks: the outline of each building and the way its field lies (OpenStreetMap, ODbL 1.0) and a plausible bowl from the seats of each configuration, the bowls and the roof the sources give (Wikidata, Wikipedia).
2
+ export { VENUE_ATTRIBUTION, STADIUMS, STADIUM_IDS, groundOfStadium, planPark, planStadium, stadiumBowl, stadiumBowlInside, stadiumById, stadiumByWikidata, stadiumConfigOf, stadiumConfigs, stadiumFrameHeading, stadiumKind, stadiumNames, stadiumOrientation, stadiumSeats } from "./stadiums.js";