@johnmorrisdotca/kyogijo-stadiums 0.53.0 → 0.55.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 CHANGED
@@ -7,6 +7,22 @@ All notable changes to this project are written here. The format follows
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.55.0] - 2026-11-03
11
+
12
+ ### Added
13
+
14
+ - **A simulated race on a circuit (KYOGIJO-STADIUMS-32)** ([a race](./docs/circuits.md#a-race)): twenty made-up cars as dots from the grid to the chequered flag over a full race distance (50 laps of Las Vegas, about an hour and twenty-two minutes), with gaps, passes, trains, one or two pit stops each and now and then a retirement, all from a seed. `simulateRace` works it out (the lap's speeds from the bends, `paceProfile`), `raceAt` gives any moment (the order, the gaps to the leader and the car in front, the last and best laps, the stops), `drawRace` and `raceBoard` draw it, and `mountCircuitRace` plays it in a page, flat or in 3D (the cars standing as discs), with a race clock, play, 1×, 10× and 60×, a slider over the whole race and the timing board, in English and Japanese. It is labelled a simulation: every team, driver, code and colour is invented, and no real race is in it; the model's figures were guided by public timing data read once and kept outside the package. Nothing runs while it is paused, hidden or out of sight, the 3D view's auto-turn and the race share one frame, and destroy leaves nothing. The demo's circuits page has a **Race** button, kept in the address with the speed and the moment.
15
+
16
+ ## [0.54.0] - 2026-11-03
17
+
18
+ ### Added
19
+
20
+ - **Play a game wherever the rink is (KYOGIJO-STADIUMS-42)** ([example games](./docs/example-games.md#the-index)): the demo's **Play a game** toggle is on every view that can play an example game: the real arenas' page in 3D (after Labels; `game=1` in the address), the home page's Try it surface, flat with `mount.replay` and in 3D with `view.replay` (after Dark; `game=1`), and the home page's arena as before (`fgame=1`). Where it shows is the index's to say: `examples/games/index.json` lists each game with the surface it is played on, its file, its frame and its title in English and Japanese, so a new game is its file and one line, and no code. Off until pressed; the game is fetched on the first press only; pressing again gives back the empty surface, and on Try it the reader's own teams and score exactly as they were (the game's sides and live score are laid over them while it plays, and the module code shows the replay). A change of arena, of flat and 3D, of the crowd or of the theme keeps the game at the same moment; another surface, a badge or the flat arena stops it and leaves nothing behind. On an arena with its LED boards on, the scoreboard over the ice follows the game (the replay board of 0.53.0), on the arenas page as on the home page's arena. One controller, `demo/play-game.js`, serves the three, and `e2e/game.demo.mjs` holds the replay's freeze and leak budgets on these pages (no frame while paused, hidden or out of sight; one a frame with the auto-turn; the same elements and listeners after ten times round; nothing left after it stops) and the index's say with a fake index. The 3D views of the arenas page and of Try it no longer leave two listeners on their stage each time they are made again.
21
+
22
+ ### Changed
23
+
24
+ - The demo's surface picker (KYOGIJO-STADIUMS-43): the Try it panel's Surface choice, 43 chips in one unordered block, is now a row of kinds (field, court, ice, pool, track, budō, mat and ring), then that kind's sports A to Z in the page's language (in Japanese by reading, in gojūon order), then for gridiron football its NFL, CFL and IFAF fields in a small row under it. Each row is a radio group (arrow keys, Home and End move, Space and Enter choose); the address still names the surface (`?sport=cfl`) and brings back its kind and sport; a surface with an example game in `examples/games/index.json` carries a small play mark. The logic is `demo/surface-kinds.js`, with `src/surface-kinds.test.js` and `e2e/surface.demo.mjs`.
25
+
10
26
  ## [0.53.0] - 2026-11-03
11
27
 
12
28
  - **The arena's scoreboard shows the game being played (KYOGIJO-STADIUMS-35)** ([the scoreboard over the arena](./docs/replay.md#the-scoreboard-over-the-arena-follows-the-game)): while a replay plays on a 3D view whose venue has its LED boards on, the scoreboard hung over centre ice shows the replay's score, period and game clock instead of the fixed score of its options. `replayBoard(timeline, t)` (pure, from `replay`) works the board out from the timeline's goals and its new optional `periods` (`start`, `end`, `number`, `length`) and `clock` (`down` or `up`). The board is written when it changes: at once for a goal, a period, a seek, a step or a pause; the clock at most once a second while playing; nothing runs while paused, hidden or out of sight. Without a replay, or after `destroy`, the board is as before, and `view.replay(timeline, { board: false })` leaves it so. The demo's hero passes the example game's periods (a period is twenty minutes on the board's clock). About 4 kB more source for the 3D replay (its budget in `replay.budget.test.js` is 24 kB now).
package/README.md CHANGED
@@ -114,6 +114,13 @@ Real buildings drawn in the outline OpenStreetMap maps for them, each with a pla
114
114
  </picture>
115
115
  <br><em>The same lap in 3D</em>
116
116
  </td>
117
+ <td align="center" valign="top">
118
+ <picture>
119
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/circuits-race-desk-dark.webp">
120
+ <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/circuits-race-desk-light.webp" alt="A simulated race on the same circuit half an hour in: twenty coloured dots with their positions, the race clock at Lap 18 of 50 and the timing board under it." width="250">
121
+ </picture>
122
+ <br><em>A simulated race, with its clock and timing board</em>
123
+ </td>
117
124
  </tr>
118
125
  </table>
119
126
 
@@ -247,7 +254,7 @@ console.log(svg.startsWith("<svg"), svg.includes("USA 3, Canada 2"));
247
254
  - **A catalogue of venues**: every venue drawn today, the ones being built and the ones wanted (every NBA, MLB, NFL, CFL and NHL venue of the season, world soccer, cricket, rugby, the Olympics, Formula One and more), with ids, names in English and Japanese and Wikidata items, as a [JSON file](https://johnmorrisdotca.github.io/kyogijo-stadiums/catalogue.json) and a typed entry. [The catalogue](./docs/catalogue.md).
248
255
  - **Real venues**: arenas and stadiums in the outline of their buildings, districts of several venues on one plan, the crowd in the stands and outside. [Ballparks](./docs/ballparks.md), [venues](./docs/venues.md), [districts](./docs/district.md).
249
256
  - **A 3D view** of any surface or venue that turns by mouse, touch or keys, with the roof and the walls to switch and a quality level for every device. [The 3D view](./docs/turn.md), [quality](./docs/quality.md).
250
- - **Circuits**: a famous race track as OpenStreetMap maps it, the Las Vegas Strip Circuit first, with its turns numbered and its pit building standing in 3D. [Circuits](./docs/circuits.md).
257
+ - **Circuits**: a famous race track as OpenStreetMap maps it, the Las Vegas Strip Circuit first, with its turns numbered and its pit building standing in 3D, and a simulated race on it: twenty made-up cars over a full race distance, with a race clock, a timing board and 1×, 10× and 60×. [Circuits](./docs/circuits.md#a-race).
251
258
  - **Tournaments**: several surfaces on one floor, who judges where, boards laid over a surface. [Layouts](./docs/multi.md), [judging](./docs/judging.md), [boards](./docs/boards.md).
252
259
  - **Data on a surface**: points, heat maps, shot and pass charts, team shapes and a replay, flat or in 3D with the players standing on the ice, in 48 providers' coordinate frames. [Layers](./docs/layers.md), [frames](./docs/frames.md), [replay](./docs/replay.md), [an example game](./docs/example-games.md).
253
260
  - **Looks and flags**: eight named looks, a flag or crest on each half, trophies and crests from Dougu Sports, flags from Hata, light and dark. [Theming](./docs/theming.md), [the pageant](./docs/pageant.md).
@@ -262,7 +269,7 @@ console.log(svg.startsWith("<svg"), svg.includes("USA 3, Canada 2"));
262
269
  | `drawKyogijo`, `drawSurface`, `drawScore`, `drawBadge` | The drawing, as a string of SVG. |
263
270
  | `SPORTS`, `PARKS`, `COURT_THEMES`, `SOCCER_PITCHES`, `GROUNDS`, `EVENTS` | The forty-three sports, the 94 ballparks, the court looks, the pitches, the 64 grounds, the 18 special games. |
264
271
  | `./venues`, `./stadiums`, `./district`, `./turn` | Real arenas and stadiums in their buildings' outlines; districts on one plan; a surface as a 3D view that turns. |
265
- | `./circuits` | Race tracks as OpenStreetMap maps them, in 2D and 3D ([demo](https://johnmorrisdotca.github.io/kyogijo-stadiums/circuits.html)). |
272
+ | `./circuits` | Race tracks as OpenStreetMap maps them, in 2D and 3D, and a simulated race on them with its clock and timing board ([demo](https://johnmorrisdotca.github.io/kyogijo-stadiums/circuits.html?race=1)). |
266
273
  | `./multi`, `./judging`, `./layouts` | Several surfaces for one event ([demo](https://johnmorrisdotca.github.io/kyogijo-stadiums/halls.html)); who judges where; small multiples and backgrounds. |
267
274
  | `./layers`, `./charts`, `./frames`, `./zones`, `./pointer`, `./replay` | Data layers, charts, the providers' frames, zones by the rules, a click as a place, tracking data played on a surface. |
268
275
  | `./athletics`, `./budo`, `./skating` | The athletics stadium, the budō (areas, dohyō, the dojo builder) and the skating rinks. |
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.53.0"
6
+ "version": "0.55.0"
7
7
  },
8
8
  "address": "https://johnmorrisdotca.github.io/kyogijo-stadiums/catalogue.json",
9
9
  "site": "https://johnmorrisdotca.github.io/kyogijo-stadiums/",
@@ -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.53.0"
6
+ "version": "0.55.0"
7
7
  },
8
8
  "address": "https://johnmorrisdotca.github.io/kyogijo-stadiums/catalogue.json",
9
9
  "site": "https://johnmorrisdotca.github.io/kyogijo-stadiums/",
@@ -9,3 +9,17 @@ export { CIRCUIT_WORDS } from "./circuits.words.ts";
9
9
  export type { CircuitWords } from "./circuits.words.ts";
10
10
  export { lineLength } from "./circuits.geo.ts";
11
11
  export type { Circuit, CircuitBasis, CircuitData, CircuitFact, CircuitOptions, CircuitOsm, CircuitOsmPoint } from "./circuits.types.ts";
12
+ export { RACE_PACE, cornerSpeed, paceProfile, speedAt } from "./circuits.pace.ts";
13
+ export type { PaceProfile } from "./circuits.pace.ts";
14
+ export { RACE_DISTANCE, raceLaps, simulateRace } from "./circuits.race.ts";
15
+ export type { CircuitRace, RaceCar, RaceOptions, RaceStop } from "./circuits.race.ts";
16
+ export { carAt, raceAt, racePoint } from "./circuits.race.moment.ts";
17
+ export type { RaceMoment, RaceStanding, RaceStatus } from "./circuits.race.moment.ts";
18
+ export { RACE_STYLE, drawRace, gapText, lapTime, raceBoard, raceClock, raceDrawing, raceStage } from "./circuits.race.draw.ts";
19
+ export type { RaceDrawOptions, RaceDrawing } from "./circuits.race.draw.ts";
20
+ export { RACE_SPEEDS, mountCircuitRace } from "./circuits.race.dom.ts";
21
+ export type { KyogijoRace, RaceState, RaceViewOptions, RaceViewSettings } from "./circuits.race.dom.ts";
22
+ export { RACE_CARS_MAX, RACE_DRIVERS, RACE_TEAMS } from "./circuits.race.field.ts";
23
+ export type { RaceDriver, RaceTeam } from "./circuits.race.field.ts";
24
+ export { RACE_WORDS } from "./circuits.race.words.ts";
25
+ export type { RaceWords } from "./circuits.race.words.ts";
@@ -7,3 +7,11 @@ export { CIRCUIT_MARKER, CIRCUIT_PLANES_MAX, CIRCUIT_PRESETS, buildCircuitTurn,
7
7
  export { KYOGIJO_CIRCUIT_STYLE } from "./circuits.style.js";
8
8
  export { CIRCUIT_WORDS } from "./circuits.words.js";
9
9
  export { lineLength } from "./circuits.geo.js";
10
+ // A simulated race on any circuit: made-up cars from the grid to the flag, its moments, its drawing and board, and its view in a page.
11
+ export { RACE_PACE, cornerSpeed, paceProfile, speedAt } from "./circuits.pace.js";
12
+ export { RACE_DISTANCE, raceLaps, simulateRace } from "./circuits.race.js";
13
+ export { carAt, raceAt, racePoint } from "./circuits.race.moment.js";
14
+ export { RACE_STYLE, drawRace, gapText, lapTime, raceBoard, raceClock, raceDrawing, raceStage } from "./circuits.race.draw.js";
15
+ export { RACE_SPEEDS, mountCircuitRace } from "./circuits.race.dom.js";
16
+ export { RACE_CARS_MAX, RACE_DRIVERS, RACE_TEAMS } from "./circuits.race.field.js";
17
+ export { RACE_WORDS } from "./circuits.race.words.js";
@@ -0,0 +1,96 @@
1
+ import type { Pt } from "./crowd.ts";
2
+ import type { CircuitPlan } from "./circuits.ts";
3
+ /** The figures of the model, in metres and seconds. */
4
+ export declare const RACE_PACE: Readonly<{
5
+ /** The fastest the car goes on a straight: 94 m/s, about 338 km/h. */
6
+ top: 94;
7
+ /** Grip across the car: `grip + downforce × v²` metres a second squared at a speed `v`, about 2.4 g in a hairpin and 5 g in a fast bend. */
8
+ grip: 22;
9
+ downforce: 0.0036;
10
+ /** How hard it brakes, and how hard it pulls away at rest; the pull falls to nothing at `top`. */
11
+ brake: 34;
12
+ launch: 17;
13
+ /** The line is read every `step` metres, and a bend is measured over `reach` either side: a racing car does not follow the middle of the road, so a short kink of it is not a bend. */
14
+ step: 5;
15
+ reach: 32;
16
+ /** The speed limit of the pit lane: 80 km/h. */
17
+ pitLimit: number;
18
+ }>;
19
+ /** A lap's speeds: one each `step` metres round the line from its first point, the time at each, and the lap's time and length. */
20
+ export interface PaceProfile {
21
+ /** Metres between two samples. */
22
+ step: number;
23
+ /** The length of the line, in metres. */
24
+ length: number;
25
+ /** The speed at each sample, metres a second, and the seconds from the line's first point to each (one more than the speeds: the last is the lap's time). */
26
+ speed: Float64Array;
27
+ time: Float64Array;
28
+ /** The time of a flying lap, in seconds. */
29
+ lap: number;
30
+ }
31
+ /** The cumulative length of a ring at each of its points (the first 0, one more than the points: the last is the whole length). */
32
+ export declare function ringLengths(points: readonly Pt[]): Float64Array;
33
+ /** The point of a ring `along` units round it from its first point (wrapped), from its lengths (`ringLengths`): found by halving, so a page can ask for twenty a frame. */
34
+ export declare function ringPoint(points: readonly Pt[], lengths: Float64Array, along: number): Pt;
35
+ /** The same for an open line: held to its ends. */
36
+ export declare function linePoint(points: readonly Pt[], lengths: Float64Array, along: number): Pt;
37
+ /** The fastest speed a bend of radius `r` metres allows: `v² = r × (grip + downforce × v²)`, and the top speed where the downforce holds it at any speed. */
38
+ export declare function cornerSpeed(r: number, pace?: Readonly<{
39
+ /** The fastest the car goes on a straight: 94 m/s, about 338 km/h. */
40
+ top: 94;
41
+ /** Grip across the car: `grip + downforce × v²` metres a second squared at a speed `v`, about 2.4 g in a hairpin and 5 g in a fast bend. */
42
+ grip: 22;
43
+ downforce: 0.0036;
44
+ /** How hard it brakes, and how hard it pulls away at rest; the pull falls to nothing at `top`. */
45
+ brake: 34;
46
+ launch: 17;
47
+ /** The line is read every `step` metres, and a bend is measured over `reach` either side: a racing car does not follow the middle of the road, so a short kink of it is not a bend. */
48
+ step: 5;
49
+ reach: 32;
50
+ /** The speed limit of the pit lane: 80 km/h. */
51
+ pitLimit: number;
52
+ }>): number;
53
+ /** How hard the car can pull away at a speed: `launch` at rest, nothing at the top speed. */
54
+ export declare const pullAt: (v: number, pace?: Readonly<{
55
+ /** The fastest the car goes on a straight: 94 m/s, about 338 km/h. */
56
+ top: 94;
57
+ /** Grip across the car: `grip + downforce × v²` metres a second squared at a speed `v`, about 2.4 g in a hairpin and 5 g in a fast bend. */
58
+ grip: 22;
59
+ downforce: 0.0036;
60
+ /** How hard it brakes, and how hard it pulls away at rest; the pull falls to nothing at `top`. */
61
+ brake: 34;
62
+ launch: 17;
63
+ /** The line is read every `step` metres, and a bend is measured over `reach` either side: a racing car does not follow the middle of the road, so a short kink of it is not a bend. */
64
+ step: 5;
65
+ reach: 32;
66
+ /** The speed limit of the pit lane: 80 km/h. */
67
+ pitLimit: number;
68
+ }>) => number;
69
+ /**
70
+ * A plan's speeds round its lap (`circuitPlan`), from its line alone: the bends' radii give each sample's fastest speed, and two passes round (twice, so that the lap closes on itself) hold it to the engine and the
71
+ * brakes. The same plan always gives the same profile.
72
+ * @example
73
+ * ```ts
74
+ * import { circuitById, circuitPlan, paceProfile } from "@johnmorrisdotca/kyogijo-stadiums/circuits";
75
+ * const pace = paceProfile(circuitPlan(circuitById("las-vegas")!));
76
+ * console.log(pace.lap > 90 && pace.lap < 105, Math.max(...pace.speed) * 3.6 > 300);
77
+ * // true true
78
+ * ```
79
+ */
80
+ export declare function paceProfile(plan: CircuitPlan, pace?: Readonly<{
81
+ /** The fastest the car goes on a straight: 94 m/s, about 338 km/h. */
82
+ top: 94;
83
+ /** Grip across the car: `grip + downforce × v²` metres a second squared at a speed `v`, about 2.4 g in a hairpin and 5 g in a fast bend. */
84
+ grip: 22;
85
+ downforce: 0.0036;
86
+ /** How hard it brakes, and how hard it pulls away at rest; the pull falls to nothing at `top`. */
87
+ brake: 34;
88
+ launch: 17;
89
+ /** The line is read every `step` metres, and a bend is measured over `reach` either side: a racing car does not follow the middle of the road, so a short kink of it is not a bend. */
90
+ step: 5;
91
+ reach: 32;
92
+ /** The speed limit of the pit lane: 80 km/h. */
93
+ pitLimit: number;
94
+ }>): PaceProfile;
95
+ /** The speed at `along` metres round the lap from the line's first point (wrapped), between the two samples either side. */
96
+ export declare function speedAt(profile: PaceProfile, along: number): number;
@@ -0,0 +1,131 @@
1
+ import { FEET_PER_METRE as FT } from "./geometry.js";
2
+ /** The figures of the model, in metres and seconds. */
3
+ export const RACE_PACE = Object.freeze({
4
+ /** The fastest the car goes on a straight: 94 m/s, about 338 km/h. */
5
+ top: 94,
6
+ /** Grip across the car: `grip + downforce × v²` metres a second squared at a speed `v`, about 2.4 g in a hairpin and 5 g in a fast bend. */
7
+ grip: 22,
8
+ downforce: 0.0036,
9
+ /** How hard it brakes, and how hard it pulls away at rest; the pull falls to nothing at `top`. */
10
+ brake: 34,
11
+ launch: 17,
12
+ /** The line is read every `step` metres, and a bend is measured over `reach` either side: a racing car does not follow the middle of the road, so a short kink of it is not a bend. */
13
+ step: 5,
14
+ reach: 32,
15
+ /** The speed limit of the pit lane: 80 km/h. */
16
+ pitLimit: 80 / 3.6,
17
+ });
18
+ /** The cumulative length of a ring at each of its points (the first 0, one more than the points: the last is the whole length). */
19
+ export function ringLengths(points) {
20
+ const out = new Float64Array(points.length + 1);
21
+ for (let i = 0; i < points.length; i += 1) {
22
+ const a = points[i];
23
+ const b = points[(i + 1) % points.length];
24
+ out[i + 1] = out[i] + Math.hypot(b[0] - a[0], b[1] - a[1]);
25
+ }
26
+ return out;
27
+ }
28
+ /** The point of a ring `along` units round it from its first point (wrapped), from its lengths (`ringLengths`): found by halving, so a page can ask for twenty a frame. */
29
+ export function ringPoint(points, lengths, along) {
30
+ const total = lengths[lengths.length - 1];
31
+ if (!(total > 0))
32
+ return points[0];
33
+ const d = ((along % total) + total) % total;
34
+ let lo = 0;
35
+ let hi = points.length - 1;
36
+ while (lo < hi) {
37
+ const mid = (lo + hi + 1) >> 1;
38
+ if (lengths[mid] <= d)
39
+ lo = mid;
40
+ else
41
+ hi = mid - 1;
42
+ }
43
+ const a = points[lo];
44
+ const b = points[(lo + 1) % points.length];
45
+ const long = lengths[lo + 1] - lengths[lo];
46
+ const t = long > 0 ? (d - lengths[lo]) / long : 0;
47
+ return [a[0] + t * (b[0] - a[0]), a[1] + t * (b[1] - a[1])];
48
+ }
49
+ /** The same for an open line: held to its ends. */
50
+ export function linePoint(points, lengths, along) {
51
+ const total = lengths[points.length - 1];
52
+ const d = Math.min(Math.max(along, 0), total);
53
+ let lo = 0;
54
+ let hi = points.length - 2;
55
+ while (lo < hi) {
56
+ const mid = (lo + hi + 1) >> 1;
57
+ if (lengths[mid] <= d)
58
+ lo = mid;
59
+ else
60
+ hi = mid - 1;
61
+ }
62
+ const a = points[lo];
63
+ const b = points[Math.min(lo + 1, points.length - 1)];
64
+ const long = lengths[lo + 1] - lengths[lo];
65
+ const t = long > 0 ? (d - lengths[lo]) / long : 0;
66
+ return [a[0] + t * (b[0] - a[0]), a[1] + t * (b[1] - a[1])];
67
+ }
68
+ /** The radius of the circle through three points, or Infinity when they lie in a line. */
69
+ function radius(a, b, c) {
70
+ const ab = Math.hypot(b[0] - a[0], b[1] - a[1]);
71
+ const bc = Math.hypot(c[0] - b[0], c[1] - b[1]);
72
+ const ca = Math.hypot(a[0] - c[0], a[1] - c[1]);
73
+ const cross = Math.abs((b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]));
74
+ return cross < 1e-9 ? Infinity : (ab * bc * ca) / (2 * cross);
75
+ }
76
+ /** The fastest speed a bend of radius `r` metres allows: `v² = r × (grip + downforce × v²)`, and the top speed where the downforce holds it at any speed. */
77
+ export function cornerSpeed(r, pace = RACE_PACE) {
78
+ const room = 1 - pace.downforce * r;
79
+ if (!(r < Infinity) || room <= 0)
80
+ return pace.top;
81
+ return Math.min(pace.top, Math.sqrt((pace.grip * r) / room));
82
+ }
83
+ /** How hard the car can pull away at a speed: `launch` at rest, nothing at the top speed. */
84
+ export const pullAt = (v, pace = RACE_PACE) => Math.max(0.4, pace.launch * (1 - (v / pace.top) ** 2));
85
+ /**
86
+ * A plan's speeds round its lap (`circuitPlan`), from its line alone: the bends' radii give each sample's fastest speed, and two passes round (twice, so that the lap closes on itself) hold it to the engine and the
87
+ * brakes. The same plan always gives the same profile.
88
+ * @example
89
+ * ```ts
90
+ * import { circuitById, circuitPlan, paceProfile } from "@johnmorrisdotca/kyogijo-stadiums/circuits";
91
+ * const pace = paceProfile(circuitPlan(circuitById("las-vegas")!));
92
+ * console.log(pace.lap > 90 && pace.lap < 105, Math.max(...pace.speed) * 3.6 > 300);
93
+ * // true true
94
+ * ```
95
+ */
96
+ export function paceProfile(plan, pace = RACE_PACE) {
97
+ const metres = plan.lap.map(([x, y]) => [x / FT, y / FT]);
98
+ const lengths = ringLengths(metres);
99
+ const length = lengths[lengths.length - 1];
100
+ const n = Math.max(8, Math.round(length / pace.step));
101
+ const step = length / n;
102
+ const limit = new Float64Array(n);
103
+ for (let i = 0; i < n; i += 1) {
104
+ const s = i * step;
105
+ limit[i] = cornerSpeed(radius(ringPoint(metres, lengths, s - pace.reach), ringPoint(metres, lengths, s), ringPoint(metres, lengths, s + pace.reach)), pace);
106
+ }
107
+ const speed = Float64Array.from(limit);
108
+ for (let round = 0; round < 2; round += 1) {
109
+ for (let k = 1; k <= n; k += 1) {
110
+ const i = k % n;
111
+ const before = speed[(k - 1) % n];
112
+ speed[i] = Math.min(speed[i], Math.sqrt(before * before + 2 * pullAt(before, pace) * step));
113
+ }
114
+ for (let k = n - 1; k >= 0; k -= 1) {
115
+ const after = speed[(k + 1) % n];
116
+ speed[k] = Math.min(speed[k], Math.sqrt(after * after + 2 * pace.brake * step));
117
+ }
118
+ }
119
+ const time = new Float64Array(n + 1);
120
+ for (let i = 0; i < n; i += 1)
121
+ time[i + 1] = time[i] + (2 * step) / (speed[i] + speed[(i + 1) % n]);
122
+ return { step, length, speed, time, lap: time[n] };
123
+ }
124
+ /** The speed at `along` metres round the lap from the line's first point (wrapped), between the two samples either side. */
125
+ export function speedAt(profile, along) {
126
+ const n = profile.speed.length;
127
+ const d = ((along % profile.length) + profile.length) % profile.length;
128
+ const i = Math.floor(d / profile.step) % n;
129
+ const t = d / profile.step - Math.floor(d / profile.step);
130
+ return profile.speed[i] + t * (profile.speed[(i + 1) % n] - profile.speed[i]);
131
+ }
@@ -0,0 +1,80 @@
1
+ import { type CircuitPlan } from "./circuits.ts";
2
+ import { type PaceProfile } from "./circuits.pace.ts";
3
+ import { type RaceDriver, type RaceTeam } from "./circuits.race.field.ts";
4
+ import type { Circuit } from "./circuits.types.ts";
5
+ /** What a race is made from: the circuit, and a seed, laps and cars of your own. */
6
+ export interface RaceOptions {
7
+ /** The circuit's key: `las-vegas`. */
8
+ circuit: string;
9
+ /** The seed every choice of the race is drawn from: a whole number, 1 by default. Another seed is another race. */
10
+ seed?: number;
11
+ /** How many laps: by default a full race distance, the fewest laps that make 305 km (`raceLaps`). 1 to 200. */
12
+ laps?: number;
13
+ /** How many cars: 2 to 20, 20 by default. */
14
+ cars?: number;
15
+ }
16
+ /** A car of the race: its driver and team (made up) and where it started. */
17
+ export interface RaceCar {
18
+ /** The driver's code in lower case: `hlv`. */
19
+ id: string;
20
+ driver: RaceDriver;
21
+ team: RaceTeam;
22
+ /** Its place on the grid, 1 for pole. */
23
+ grid: number;
24
+ }
25
+ /** A pit stop: the lap it ends, the time the car left the track and joined it again, and how long it stood still. */
26
+ export interface RaceStop {
27
+ lap: number;
28
+ in: number;
29
+ out: number;
30
+ stop: number;
31
+ }
32
+ /** A race worked out: the cars, and each one's distance at every second, its laps, stops and finish. */
33
+ export interface CircuitRace {
34
+ circuit: Circuit;
35
+ plan: CircuitPlan;
36
+ seed: number;
37
+ laps: number;
38
+ cars: readonly RaceCar[];
39
+ /** The lap's length along the drawn line, in metres, and how far round it from the line's first point the timing line (the finish) is. */
40
+ lapLength: number;
41
+ lineAlong: number;
42
+ /** The pit lane: where it leaves the lap and joins it again (metres round the lap from the line's first point), and its length in metres. */
43
+ pitLane: {
44
+ entry: number;
45
+ exit: number;
46
+ length: number;
47
+ };
48
+ /** The lap's speeds the cars run. */
49
+ pace: PaceProfile;
50
+ /** Seconds between two samples, the time the chequered flag fell and the time the race ends (every car home and slowed). */
51
+ sample: number;
52
+ flag: number;
53
+ duration: number;
54
+ /** Each car's race distance at every sample, in metres: 0 at the timing line, a lap is done at each `lapLength`. A car on the grid behind the line starts below 0. */
55
+ distance: readonly Float64Array[];
56
+ /** Each car's distance along the pit lane at every sample, in metres, or NaN while it is on the track. */
57
+ pit: readonly Float32Array[];
58
+ /** The time each car completed each lap, from the start. */
59
+ crossings: readonly (readonly number[])[];
60
+ stops: readonly (readonly RaceStop[])[];
61
+ /** The time a car took the chequered flag, or retired: `undefined` for neither. */
62
+ finish: readonly (number | undefined)[];
63
+ out: readonly (number | undefined)[];
64
+ }
65
+ /** The distance of a race: the fewest laps that make more than 305 km, as a Grand Prix's is set. */
66
+ export declare const RACE_DISTANCE = 305000;
67
+ /** The laps of a full race distance on a circuit: 50 at Las Vegas. */
68
+ export declare const raceLaps: (circuit: Pick<Circuit, "length">) => number;
69
+ /**
70
+ * A race on a circuit, worked out from a seed: made-up cars from the grid to the flag over a full race distance (50 laps of Las Vegas, about an hour and a half), with the gaps, overtakes, pit stops and retirements
71
+ * the model gives. The same options always give the same race; the last few asked for are kept.
72
+ * @example
73
+ * ```ts
74
+ * import { simulateRace } from "@johnmorrisdotca/kyogijo-stadiums/circuits";
75
+ * const race = simulateRace({ circuit: "las-vegas", laps: 5, cars: 6, seed: 7 });
76
+ * console.log(race.laps, race.cars.length, race.flag > 5 * 90 && race.flag < 5 * 110);
77
+ * // 5 6 true
78
+ * ```
79
+ */
80
+ export declare function simulateRace(options: RaceOptions): CircuitRace;
@@ -0,0 +1,68 @@
1
+ import { type RaceMoment } from "./circuits.race.moment.ts";
2
+ import { type CircuitRace, type RaceOptions } from "./circuits.race.ts";
3
+ import type { CircuitOptions } from "./circuits.types.ts";
4
+ import { type KyogijoTurn } from "./turn.dom.ts";
5
+ import type { TurnSettings } from "./turn.model.ts";
6
+ /** The speeds the controls offer: real time, ten times and sixty times (a minute of the race a second). */
7
+ export declare const RACE_SPEEDS: readonly number[];
8
+ /** What a race's view is drawn from: the circuit's options and the race's. */
9
+ export type RaceViewOptions = CircuitOptions & Omit<RaceOptions, "circuit">;
10
+ /** How a race is shown and played. */
11
+ export interface RaceViewSettings {
12
+ /** `flat` (the default) or `3d`: the circuit's 3D view, with the cars standing on it as discs that face the eye. */
13
+ view?: "flat" | "3d";
14
+ /** The 3D view's own settings (its viewpoint, its controls). */
15
+ turn?: TurnSettings;
16
+ /** 1 (the default), 10 or 60; any speed from 1 to 600 is allowed. */
17
+ speed?: number;
18
+ /** Seconds from the start to begin at: 0 (the grid) by default. */
19
+ start?: number;
20
+ /** Play at once. Ignored for a reader who prefers reduced motion, and while the page is hidden. */
21
+ autoplay?: boolean;
22
+ /** `auto` (the default) follows `prefers-reduced-motion`; `smooth` and `step` force one. */
23
+ motion?: "auto" | "smooth" | "step";
24
+ /** The timing board under the controls: on by default. */
25
+ board?: boolean;
26
+ /** Told when the reader plays, pauses, seeks or changes speed, and when the race ends: not at every frame. */
27
+ onChange?: (state: RaceState) => void;
28
+ }
29
+ /** What a race's view is doing. */
30
+ export interface RaceState {
31
+ t: number;
32
+ duration: number;
33
+ playing: boolean;
34
+ ended: boolean;
35
+ speed: number;
36
+ motion: "smooth" | "step";
37
+ }
38
+ /** What `mountCircuitRace` gives back. */
39
+ export interface KyogijoRace {
40
+ readonly race: CircuitRace;
41
+ /** The 3D view, when the race is on one. */
42
+ readonly turn: KyogijoTurn<CircuitOptions> | undefined;
43
+ state(): RaceState;
44
+ /** The race at the clock's time: the standings, the lap, the fastest lap. */
45
+ moment(): RaceMoment;
46
+ play(): void;
47
+ pause(): void;
48
+ toggle(): void;
49
+ /** Go to a time, in seconds from the start. */
50
+ seek(t: number): void;
51
+ /** Go to the moment the leader next crosses the line. */
52
+ nextLap(): void;
53
+ setSpeed(speed: number): void;
54
+ /** Draw again with new options (the language, the theme): the clock and the race stay where they are. A new circuit, seed, laps or cars is a new race: mount it again. */
55
+ update(options: RaceViewOptions): void;
56
+ destroy(): void;
57
+ }
58
+ /**
59
+ * Mount a simulated race on a circuit: the circuit flat (or in 3D) with a dot for each made-up car, the race clock, play and pause, the speeds 1×, 10× and 60×, a slider over the whole race and the timing board, moving
60
+ * together. It does nothing until it is played, and nothing while it is paused, hidden or out of sight.
61
+ * @example
62
+ * ```ts no-run
63
+ * import { mountCircuitRace } from "@johnmorrisdotca/kyogijo-stadiums/circuits";
64
+ * const race = mountCircuitRace(document.querySelector("#race")!, { circuit: "las-vegas" }, { speed: 10 });
65
+ * race.play();
66
+ * ```
67
+ */
68
+ export declare function mountCircuitRace(host: HTMLElement, initial: RaceViewOptions, settings?: RaceViewSettings): KyogijoRace;