@johnmorrisdotca/kyogijo-stadiums 0.54.0 → 0.56.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,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,73 @@
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
+ /** A race already worked out to play instead of a simulation: one from a feed (`raceFromFeed`). */
27
+ race?: CircuitRace;
28
+ /** Told when the reader plays, pauses, seeks or changes speed, and when the race ends: not at every frame. */
29
+ onChange?: (state: RaceState) => void;
30
+ }
31
+ /** What a race's view is doing. */
32
+ export interface RaceState {
33
+ t: number;
34
+ duration: number;
35
+ playing: boolean;
36
+ ended: boolean;
37
+ speed: number;
38
+ motion: "smooth" | "step";
39
+ }
40
+ /** What `mountCircuitRace` gives back. */
41
+ export interface KyogijoRace {
42
+ /** The race it plays. */
43
+ readonly race: CircuitRace;
44
+ /** The 3D view, when the race is on one. */
45
+ readonly turn: KyogijoTurn<CircuitOptions> | undefined;
46
+ state(): RaceState;
47
+ /** The race at the clock's time: the standings, the lap, the fastest lap. */
48
+ moment(): RaceMoment;
49
+ play(): void;
50
+ pause(): void;
51
+ toggle(): void;
52
+ /** Go to a time, in seconds from the start. */
53
+ seek(t: number): void;
54
+ /** Go to the moment the leader next crosses the line. */
55
+ nextLap(): void;
56
+ setSpeed(speed: number): void;
57
+ /** 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. */
58
+ update(options: RaceViewOptions): void;
59
+ /** Play another race in the same view (a feed that came in again while a race is followed): the clock stays where it is, held to the new race's end, and stays playing or paused. */
60
+ replace(race: CircuitRace): void;
61
+ destroy(): void;
62
+ }
63
+ /**
64
+ * 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
65
+ * together. It does nothing until it is played, and nothing while it is paused, hidden or out of sight.
66
+ * @example
67
+ * ```ts no-run
68
+ * import { mountCircuitRace } from "@johnmorrisdotca/kyogijo-stadiums/circuits";
69
+ * const race = mountCircuitRace(document.querySelector("#race")!, { circuit: "las-vegas" }, { speed: 10 });
70
+ * race.play();
71
+ * ```
72
+ */
73
+ export declare function mountCircuitRace(host: HTMLElement, initial: RaceViewOptions, settings?: RaceViewSettings): KyogijoRace;