@johnmorrisdotca/kyogijo-stadiums 0.57.0 → 0.58.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/NOTICE.md +2 -0
  3. package/README.md +1 -1
  4. package/catalogue.json +1 -1
  5. package/dist/areas.d.ts +8 -3
  6. package/dist/areas.js +14 -5
  7. package/dist/catalogue.data.js +1 -1
  8. package/dist/circuits-entry.d.ts +1 -1
  9. package/dist/circuits-entry.js +1 -1
  10. package/dist/circuits.draw.js +7 -4
  11. package/dist/circuits.held.d.ts +1 -0
  12. package/dist/circuits.held.js +13 -0
  13. package/dist/circuits.index.d.ts +15 -0
  14. package/dist/circuits.index.js +28 -0
  15. package/dist/circuits.race.dom.d.ts +25 -5
  16. package/dist/circuits.race.dom.js +22 -7
  17. package/dist/circuits.race.draw.d.ts +1 -1
  18. package/dist/circuits.race.draw.js +4 -4
  19. package/dist/circuits.turn.d.ts +8 -0
  20. package/dist/circuits.turn.js +29 -21
  21. package/dist/circuits.types.d.ts +2 -0
  22. package/dist/circuits.words.d.ts +4 -0
  23. package/dist/circuits.words.js +6 -0
  24. package/dist/cityctx-entry.d.ts +1 -1
  25. package/dist/cityctx-entry.js +1 -1
  26. package/dist/cityctx.d.ts +2 -2
  27. package/dist/cityctx.data.d.ts +2 -1
  28. package/dist/cityctx.data.js +3 -2
  29. package/dist/cityctx.js +9 -3
  30. package/dist/cityctx.las-vegas.data.d.ts +3 -0
  31. package/dist/cityctx.las-vegas.data.js +1387 -0
  32. package/dist/context.d.ts +11 -1
  33. package/dist/context.js +18 -4
  34. package/dist/district-entry.d.ts +1 -0
  35. package/dist/district-entry.js +2 -0
  36. package/dist/district.api.d.ts +9 -2
  37. package/dist/district.api.js +7 -1
  38. package/dist/district.circuit.d.ts +34 -0
  39. package/dist/district.circuit.js +13 -0
  40. package/dist/district.d.ts +27 -3
  41. package/dist/district.js +85 -15
  42. package/dist/district.race.d.ts +40 -0
  43. package/dist/district.race.js +60 -0
  44. package/dist/district.turn.js +27 -1
  45. package/dist/landmarks.d.ts +105 -0
  46. package/dist/landmarks.js +160 -0
  47. package/dist/turn.dom.js +5 -5
  48. package/dist/turn.style.js +1 -0
  49. package/dist/version.d.ts +1 -1
  50. package/dist/version.js +1 -1
  51. package/package.json +2 -1
@@ -2,7 +2,9 @@
2
2
  // (`buildTurn`: the stands, the walls and the roof, the goals and posts of its field), its planes are turned about the middle of its surface and moved to where the plan puts it, and the floor is the district's flat
3
3
  // plan drawn with each venue's building only. The model it makes is a `TurnModel`, so the camera, the controls, the pointer, the auto-turn and the steady fit of every other 3D view are this one's; its
4
4
  // buildings are stacked building by building (`Plane.group`), the far one first, so that a roof never shows through a nearer building.
5
- import { describeDistrict, districtId, drawDistrict, drawMember, nestedDrawing, planDistrict, surfaceCentre } from "./district.js";
5
+ import { circuitInDistrict, circuitPointIn, describeDistrict, districtId, drawDistrict, drawMember, nestedDrawing, planDistrict, surfaceCentre } from "./district.js";
6
+ import { circuitSupport } from "./district.circuit.js";
7
+ import { landmarkUprights } from "./landmarks.js";
6
8
  import { blockPlanes, contextUprights, shellBlock, slabPlanes, thinTrees } from "./context.js";
7
9
  import { TIER_PROFILES, tierOf } from "./quality.js";
8
10
  import { dojo } from "./budo/dojo.api.js";
@@ -193,10 +195,27 @@ export function buildDistrictTurn(options, settings = {}) {
193
195
  items.push({ id: `${prefix}plate-name`, kind: "scoreboard", at: placed.at, height: level + size * 1.7, planes: [sign(`${prefix}plate-name`, label, placed.at, level + size * 0.4, size, i)] });
194
196
  }
195
197
  });
198
+ // The circuit's pit building and its turns' numbers, standing where the district puts the circuit, apart from every venue's group.
199
+ if (plan.circuit !== undefined) {
200
+ const placed = plan.circuit;
201
+ const { circuitUprights, circuitDrawing } = circuitSupport();
202
+ for (const item of circuitUprights(circuitDrawing(circuitInDistrict(options, base, true)), Math.hypot(plan.view.w, plan.view.h) * 0.0012)) {
203
+ items.push({ ...item, id: `ci-${item.id}`, at: circuitPointIn(placed, [item.at[0], item.at[1]]), planes: item.planes.map((p) => placeInDistrict(p, placed, [0, 0], "ci-", APART)) });
204
+ }
205
+ }
196
206
  // The buildings round the venues stand apart from them: each block's planes are of no venue's group.
197
207
  if (plan.context !== undefined)
198
208
  for (const item of contextUprights(plan.context, profile.blocks))
199
209
  items.push({ ...item, planes: item.planes.map((p) => ({ ...p, group: APART })) });
210
+ // The landmarks round them, each by its own shape (a dome, a wheel, the jets of a fountain), apart from every venue too; and with names, each one's name over it.
211
+ const landmarks = plan.context?.landmarks ?? [];
212
+ for (const item of landmarkUprights(landmarks, profile.simple))
213
+ items.push({ ...item, planes: item.planes.map((p) => ({ ...p, group: APART })) });
214
+ if (labels) {
215
+ const size = Math.max(plan.view.w, plan.view.h) * (plan.mode === "city" ? 0.013 : 0.018);
216
+ for (const l of landmarks)
217
+ items.push({ id: `lm-${l.key}-name`, kind: "scoreboard", at: l.label, height: l.height + size * 2.3, planes: [sign(`lm-${l.key}-name`, l.name[language], l.label, l.height + size * 0.5, size, APART)] });
218
+ }
200
219
  if (labels) {
201
220
  const size = Math.max(plan.view.w, plan.view.h) * (plan.mode === "city" ? 0.016 : 0.022);
202
221
  plan.members.forEach((placed, i) => {
@@ -205,6 +224,13 @@ export function buildDistrictTurn(options, settings = {}) {
205
224
  items.push({ id: `m${i}-name`, kind: "scoreboard", at: middle, height: top + size * 1.7, planes: [sign(`m${i}-name`, placed.member.name[language], middle, top + size * 0.6, size, i)] });
206
225
  });
207
226
  }
227
+ if (labels && plan.circuit !== undefined) {
228
+ const size = Math.max(plan.view.w, plan.view.h) * (plan.mode === "city" ? 0.016 : 0.022);
229
+ // Over the start, where the pits are, so that it does not stand in front of what the lap goes round.
230
+ const at = circuitPointIn(plan.circuit, plan.circuit.plan.start.at);
231
+ const top = (plan.circuit.plan.building?.height ?? 0) + size * 0.6;
232
+ items.push({ id: "ci-name", kind: "scoreboard", at, height: top + size * 1.7, planes: [sign("ci-name", plan.circuit.plan.circuit.names[language], at, top, size, APART)] });
233
+ }
208
234
  const view = plan.view;
209
235
  const scale = WORLD_PX / Math.max(view.w, view.h);
210
236
  const centre = [view.x + view.w / 2, view.y + view.h / 2];
@@ -0,0 +1,105 @@
1
+ import type { Pt } from "./crowd.ts";
2
+ import { type Plane, type Upright } from "./turn.kit.ts";
3
+ import { type LonLat } from "./venues.geo.ts";
4
+ /** How a landmark is drawn. */
5
+ export type LandmarkShape = "dome" | "wheel" | "fountains" | "named";
6
+ export declare const LANDMARK_SHAPES: readonly LandmarkShape[];
7
+ /**
8
+ * A landmark of an area, as its data holds it:
9
+ * - `dome`: a sphere cut by the ground (the Sphere), `width` across at its widest and `height` tall; its outline is its footprint, and the building the area has there is the dome's own.
10
+ * - `wheel`: a big wheel standing upright along the long way of its outline (OpenStreetMap maps the High Roller's footprint), `width` across, its top `height` over the ground.
11
+ * - `fountains`: a row of jets along the long way of the water its outline is, reaching up to `height` (the most a published figure says they reach); the water is the area's land cover.
12
+ * - `named`: a name over the buildings inside its outline (they are the area's own, standing at their own heights), at `height`, the top of the tallest.
13
+ */
14
+ export interface ContextLandmark {
15
+ key: string;
16
+ /** The Wikidata item its names come from. */
17
+ wikidata: string;
18
+ name: {
19
+ en: string;
20
+ ja: string;
21
+ };
22
+ shape: LandmarkShape;
23
+ /** The OpenStreetMap element its outline is, `way/<id>`. */
24
+ source: string;
25
+ /** Its outline, metres east and north of the area's origin. */
26
+ ring: readonly (readonly [number, number])[];
27
+ /** Its height over the ground, in metres. */
28
+ height: number;
29
+ /** A dome's or a wheel's diameter, in metres. */
30
+ width?: number;
31
+ /** A named landmark's tallest building, where its name stands: metres east and north of the area's origin. */
32
+ top?: readonly [number, number];
33
+ /** Where the figures were read, in our own words. */
34
+ read: string;
35
+ }
36
+ /** A landmark placed in a district's frame: its outline in feet (x to the right, y down the page), its middle, its height and width in feet, and for a wheel or fountains the unit direction its long way runs. */
37
+ export interface PlacedLandmark {
38
+ key: string;
39
+ name: {
40
+ en: string;
41
+ ja: string;
42
+ };
43
+ shape: LandmarkShape;
44
+ ring: Pt[];
45
+ at: Pt;
46
+ /** Where its name stands: over its middle, or a named landmark's tallest building. */
47
+ label: Pt;
48
+ height: number;
49
+ width?: number;
50
+ along: Pt;
51
+ }
52
+ /** The landmarks of some areas placed in a district: turned as the plan is and kept where they stand in the frame (`frame` is the plan's box in feet). */
53
+ export declare function placeLandmarks(areas: readonly {
54
+ origin: readonly [number, number];
55
+ landmarks?: readonly ContextLandmark[];
56
+ }[], origin: LonLat, turn: number, frame: {
57
+ x: number;
58
+ y: number;
59
+ w: number;
60
+ h: number;
61
+ }): PlacedLandmark[];
62
+ /** Whether a building's outline is a landmark's own (most of it inside a dome's footprint): the dome draws it, and the block is left out. */
63
+ export declare const ownedByLandmark: (landmarks: readonly PlacedLandmark[], ring: readonly Pt[]) => boolean;
64
+ /** Whether a building stands under a named landmark (its middle inside the landmark's outline): it is raised in the 3D view whatever the count of blocks. */
65
+ export declare const underLandmark: (landmarks: readonly PlacedLandmark[], ring: readonly Pt[]) => boolean;
66
+ /** The jets of fountains, in feet: where each stands along the long way of the water (from its middle) and how high it reaches, as a share of the most. Ours: the published figure is the most they reach, the row is a pattern. */
67
+ export declare const FOUNTAIN_JETS: readonly (readonly [number, number])[];
68
+ /** What the floor has of the landmarks: a dome's footprint and a wheel's line on the ground, in the tone of the tallest buildings, and the jets' feet as dots on the water. Empty for none. */
69
+ export declare function landmarksFloor(landmarks: readonly PlacedLandmark[]): string;
70
+ /** How finely a dome is cut: sides round it and bands up it. Fewer in the lower tiers. */
71
+ export declare const DOME_CUT: Readonly<{
72
+ high: {
73
+ sides: number;
74
+ bands: number;
75
+ };
76
+ simple: {
77
+ sides: number;
78
+ bands: number;
79
+ };
80
+ }>;
81
+ /** The planes of a dome: a sphere `width` across whose top is `height` over the ground, cut by the ground, as bands of flat facets, each lit by the way it faces. */
82
+ export declare function domePlanes(prefix: string, at: Pt, height: number, width: number, cut: {
83
+ sides: number;
84
+ bands: number;
85
+ }): Plane[];
86
+ /** How a wheel is drawn: segments round its rim and spokes to its hub. */
87
+ export declare const WHEEL_CUT: Readonly<{
88
+ high: {
89
+ rim: number;
90
+ spokes: number;
91
+ };
92
+ simple: {
93
+ rim: number;
94
+ spokes: number;
95
+ };
96
+ }>;
97
+ /** The planes of a big wheel standing upright along `along`: its rim, its spokes and the two legs from its hub to the ground, each a thin bar seen from every side. */
98
+ export declare function wheelPlanes(prefix: string, at: Pt, along: Pt, height: number, width: number, cut: {
99
+ rim: number;
100
+ spokes: number;
101
+ }): Plane[];
102
+ /** The planes of fountains: a row of jets along the long way of the water, each a thin upright strip reaching to its share of `height`. */
103
+ export declare function fountainPlanes(prefix: string, l: Pick<PlacedLandmark, "ring" | "at" | "along" | "height">): Plane[];
104
+ /** The 3D view's landmarks: each one's shape standing where it is, cut more coarsely in a simple tier. A named landmark has no shape of its own: its buildings are the area's. */
105
+ export declare function landmarkUprights(landmarks: readonly PlacedLandmark[], simple: boolean): Upright[];
@@ -0,0 +1,160 @@
1
+ import { FEET_PER_METRE } from "./geometry.js";
2
+ import { bar, n, plane } from "./turn.kit.js";
3
+ import { hullOf, inside, metresFrom, smallestBox } from "./venues.geo.js";
4
+ export const LANDMARK_SHAPES = ["dome", "wheel", "fountains", "named"];
5
+ const RAD = Math.PI / 180;
6
+ const rotate = ([x, y], degrees) => [x * Math.cos(degrees * RAD) - y * Math.sin(degrees * RAD), x * Math.sin(degrees * RAD) + y * Math.cos(degrees * RAD)];
7
+ const middleOf = (ring) => [ring.reduce((s, p) => s + p[0], 0) / ring.length, ring.reduce((s, p) => s + p[1], 0) / ring.length];
8
+ /** The landmarks of some areas placed in a district: turned as the plan is and kept where they stand in the frame (`frame` is the plan's box in feet). */
9
+ export function placeLandmarks(areas, origin, turn, frame) {
10
+ const placed = [];
11
+ for (const area of areas) {
12
+ const [east, north] = metresFrom(origin, [area.origin])[0];
13
+ for (const l of area.landmarks ?? []) {
14
+ const ring = l.ring.map(([e, nn]) => rotate([(e + east) * FEET_PER_METRE, -(nn + north) * FEET_PER_METRE], turn));
15
+ const at = middleOf(ring);
16
+ if (at[0] < frame.x || at[0] > frame.x + frame.w || at[1] < frame.y || at[1] > frame.y + frame.h)
17
+ continue;
18
+ // The long way of the outline, on the page: `smallestBox` reads east and north, and the page's y runs down.
19
+ const heading = smallestBox(ring.map(([x, y]) => [x, -y])).heading;
20
+ const along = [Math.sin(heading * RAD), -Math.cos(heading * RAD)];
21
+ const label = l.top === undefined ? at : rotate([(l.top[0] + east) * FEET_PER_METRE, -(l.top[1] + north) * FEET_PER_METRE], turn);
22
+ placed.push({ key: l.key, name: l.name, shape: l.shape, ring, at, label, height: l.height * FEET_PER_METRE, ...(l.width === undefined ? {} : { width: l.width * FEET_PER_METRE }), along });
23
+ }
24
+ }
25
+ return placed;
26
+ }
27
+ /** Whether a building's outline is a landmark's own (most of it inside a dome's footprint): the dome draws it, and the block is left out. */
28
+ export const ownedByLandmark = (landmarks, ring) => landmarks.some((l) => l.shape === "dome" && (inside(hullOf([...l.ring]), middleOf(ring)) || ring.filter((p) => inside(l.ring, p)).length * 10 >= ring.length * 7));
29
+ /** Whether a building stands under a named landmark (its middle inside the landmark's outline): it is raised in the 3D view whatever the count of blocks. */
30
+ export const underLandmark = (landmarks, ring) => landmarks.some((l) => l.shape === "named" && inside(l.ring, middleOf(ring)));
31
+ const num = (value) => String(Math.round(value * 10) / 10);
32
+ /** The jets of fountains, in feet: where each stands along the long way of the water (from its middle) and how high it reaches, as a share of the most. Ours: the published figure is the most they reach, the row is a pattern. */
33
+ export const FOUNTAIN_JETS = [[-0.36, 0.3], [-0.27, 0.48], [-0.18, 0.66], [-0.09, 0.84], [0, 1], [0.09, 0.84], [0.18, 0.66], [0.27, 0.48], [0.36, 0.3]];
34
+ /** The extent of an outline along a unit direction from its middle: the least and the most. */
35
+ const extent = (ring, at, along) => {
36
+ const t = ring.map((p) => (p[0] - at[0]) * along[0] + (p[1] - at[1]) * along[1]);
37
+ return [Math.min(...t), Math.max(...t)];
38
+ };
39
+ /** What the floor has of the landmarks: a dome's footprint and a wheel's line on the ground, in the tone of the tallest buildings, and the jets' feet as dots on the water. Empty for none. */
40
+ export function landmarksFloor(landmarks) {
41
+ const parts = [];
42
+ for (const l of landmarks) {
43
+ if (l.shape === "dome")
44
+ parts.push(`<path class="ky-dist-block ky-dist-b3 ky-dist-landmark" data-landmark="${l.key}" d="M${l.ring.map(([x, y]) => `${num(x)} ${num(y)}`).join("L")}Z"/>`);
45
+ if (l.shape === "wheel") {
46
+ const r = (l.width ?? 0) / 2;
47
+ parts.push(`<path class="ky-dist-ink ky-dist-landmark" data-landmark="${l.key}" stroke-width="${num(Math.max(r * 0.06, 3))}" d="M${num(l.at[0] - l.along[0] * r)} ${num(l.at[1] - l.along[1] * r)}L${num(l.at[0] + l.along[0] * r)} ${num(l.at[1] + l.along[1] * r)}"/>`);
48
+ }
49
+ if (l.shape === "fountains") {
50
+ // The water the jets stand in, as the land cover draws water (an area's lake mapped as the fountains' own outline is not in its land cover).
51
+ parts.push(`<path class="ky-dist-land ky-dist-water" data-landmark="${l.key}" d="M${l.ring.map(([x, y]) => `${num(x)} ${num(y)}`).join("L")}Z"/>`);
52
+ const [t0, t1] = extent(l.ring, l.at, l.along);
53
+ const long = t1 - t0;
54
+ const d = FOUNTAIN_JETS.map(([s]) => `M${num(l.at[0] + l.along[0] * (t0 + long * (0.5 + s)))} ${num(l.at[1] + l.along[1] * (t0 + long * (0.5 + s)))}h0`).join("");
55
+ parts.push(`<path class="ky-dist-jet ky-dist-landmark" data-landmark="${l.key}" stroke-width="${num(Math.max(long * 0.02, 6))}" d="${d}"/>`);
56
+ }
57
+ }
58
+ return parts.length === 0 ? "" : `<g class="ky-dist-landmarks" aria-hidden="true">${parts.join("")}</g>`;
59
+ }
60
+ /** A plane through three or more points of one flat polygon in 3D, painted with `cls`, and `cull` set so that its face away from `inward` is the one drawn (it is hidden from inside). */
61
+ function facet(id, points, cls, inward) {
62
+ const sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
63
+ const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
64
+ const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
65
+ const unit = (a) => {
66
+ const l = Math.hypot(...a);
67
+ return l < 1e-9 ? [0, 0, 0] : [a[0] / l, a[1] / l, a[2] / l];
68
+ };
69
+ const [p0, p1] = points;
70
+ const u = unit(sub(p1, p0));
71
+ const normal = unit(cross(sub(p1, p0), sub(points[points.length - 1], p0)));
72
+ if (Math.hypot(...normal) < 0.5)
73
+ return undefined;
74
+ const v = unit(cross(normal, u));
75
+ const local = points.map((p) => [dot(sub(p, p0), u), dot(sub(p, p0), v)]);
76
+ const [a0, a1] = [Math.min(...local.map((p) => p[0])), Math.max(...local.map((p) => p[0]))];
77
+ const [b0, b1] = [Math.min(...local.map((p) => p[1])), Math.max(...local.map((p) => p[1]))];
78
+ const o = [p0[0] + u[0] * a0 + v[0] * b0, p0[1] + u[1] * a0 + v[1] * b0, p0[2] + u[2] * a0 + v[2] * b0];
79
+ const svg = `<polygon class="${cls}" points="${local.map(([a, b]) => `${n(a - a0)},${n(b - b0)}`).join(" ")}"/>`;
80
+ const out = sub(points[0], inward);
81
+ // `u` x `v` is the normal; the face that is drawn is the one the normal points out of.
82
+ return { ...plane(id, "block", o, [u[0] * (a1 - a0), u[1] * (a1 - a0), u[2] * (a1 - a0)], [v[0] * (b1 - b0), v[1] * (b1 - b0), v[2] * (b1 - b0)], svg), cull: dot(normal, out) >= 0 ? 1 : -1 };
83
+ }
84
+ /** The tone of a facet by the way it faces the light (from up the page, to the left, and above): three tones, as a building's walls have. */
85
+ const toneOf = (normal) => {
86
+ const lit = normal[0] * -0.4 + normal[1] * -0.5 + normal[2] * 0.75;
87
+ return lit > 0.45 ? "u-block-a" : lit > 0 ? "u-block-b" : "u-block-c";
88
+ };
89
+ /** How finely a dome is cut: sides round it and bands up it. Fewer in the lower tiers. */
90
+ export const DOME_CUT = Object.freeze({ high: { sides: 16, bands: 6 }, simple: { sides: 10, bands: 4 } });
91
+ /** The planes of a dome: a sphere `width` across whose top is `height` over the ground, cut by the ground, as bands of flat facets, each lit by the way it faces. */
92
+ export function domePlanes(prefix, at, height, width, cut) {
93
+ const r = width / 2;
94
+ const zc = height - r;
95
+ const low = Math.asin(Math.max(-1, Math.min(1, -zc / r)));
96
+ const centre = [at[0], at[1], zc];
97
+ const point = (side, band) => {
98
+ const lat = low + ((Math.PI / 2 - low) * band) / cut.bands;
99
+ const lon = (2 * Math.PI * side) / cut.sides;
100
+ return [at[0] + r * Math.cos(lat) * Math.cos(lon), at[1] + r * Math.cos(lat) * Math.sin(lon), zc + r * Math.sin(lat)];
101
+ };
102
+ const planes = [];
103
+ for (let band = 0; band < cut.bands; band += 1) {
104
+ for (let side = 0; side < cut.sides; side += 1) {
105
+ const top = band + 1 === cut.bands;
106
+ const pts = top ? [point(side, band), point(side + 1, band), point(side, band + 1)] : [point(side, band), point(side + 1, band), point(side + 1, band + 1), point(side, band + 1)];
107
+ const mid = [pts.reduce((s, p) => s + p[0], 0) / pts.length - centre[0], pts.reduce((s, p) => s + p[1], 0) / pts.length - centre[1], pts.reduce((s, p) => s + p[2], 0) / pts.length - centre[2]];
108
+ const l = Math.hypot(...mid);
109
+ const f = facet(`${prefix}d${band}-${side}`, pts, `u-block ${toneOf([mid[0] / l, mid[1] / l, mid[2] / l])}`, centre);
110
+ if (f !== undefined)
111
+ planes.push(f);
112
+ }
113
+ }
114
+ return planes;
115
+ }
116
+ /** How a wheel is drawn: segments round its rim and spokes to its hub. */
117
+ export const WHEEL_CUT = Object.freeze({ high: { rim: 24, spokes: 12 }, simple: { rim: 12, spokes: 6 } });
118
+ /** The planes of a big wheel standing upright along `along`: its rim, its spokes and the two legs from its hub to the ground, each a thin bar seen from every side. */
119
+ export function wheelPlanes(prefix, at, along, height, width, cut) {
120
+ const r = width / 2;
121
+ const hub = [at[0], at[1], height - r];
122
+ const on = (angle, radius = r) => [at[0] + along[0] * radius * Math.cos(angle), at[1] + along[1] * radius * Math.cos(angle), hub[2] + radius * Math.sin(angle)];
123
+ const thick = Math.max(width * 0.025, 2);
124
+ const planes = [];
125
+ for (let i = 0; i < cut.rim; i += 1)
126
+ planes.push(...bar(`${prefix}rim${i}`, "pole", on((2 * Math.PI * i) / cut.rim), on((2 * Math.PI * (i + 1)) / cut.rim), thick, "u-leg"));
127
+ for (let i = 0; i < cut.spokes; i += 1)
128
+ planes.push(...bar(`${prefix}spoke${i}`, "pole", hub, on((2 * Math.PI * i) / cut.spokes), thick * 0.35, "u-leg"));
129
+ // Two legs, leaning out across the wheel's plane to the ground.
130
+ const across = [-along[1], along[0]];
131
+ for (const [i, side] of [[0, 1], [1, -1]])
132
+ planes.push(...bar(`${prefix}leg${i}`, "pole", hub, [at[0] + across[0] * side * r * 0.3, at[1] + across[1] * side * r * 0.3, 0], thick * 1.4, "u-leg"));
133
+ return planes;
134
+ }
135
+ /** The planes of fountains: a row of jets along the long way of the water, each a thin upright strip reaching to its share of `height`. */
136
+ export function fountainPlanes(prefix, l) {
137
+ const [t0, t1] = extent(l.ring, l.at, l.along);
138
+ const long = t1 - t0;
139
+ const planes = [];
140
+ FOUNTAIN_JETS.forEach(([s, share], i) => {
141
+ const t = t0 + long * (0.5 + s);
142
+ const [x, y] = [l.at[0] + l.along[0] * t, l.at[1] + l.along[1] * t];
143
+ planes.push(...bar(`${prefix}jet${i}`, "pole", [x, y, 0], [x, y, l.height * share], Math.max(long * 0.012, 3), "u-jet"));
144
+ });
145
+ return planes;
146
+ }
147
+ /** The 3D view's landmarks: each one's shape standing where it is, cut more coarsely in a simple tier. A named landmark has no shape of its own: its buildings are the area's. */
148
+ export function landmarkUprights(landmarks, simple) {
149
+ const items = [];
150
+ for (const l of landmarks) {
151
+ const prefix = `lm-${l.key}-`;
152
+ if (l.shape === "dome" && l.width !== undefined)
153
+ items.push({ id: `lm-${l.key}`, kind: "blocks", at: l.at, height: l.height, planes: domePlanes(prefix, l.at, l.height, l.width, simple ? DOME_CUT.simple : DOME_CUT.high) });
154
+ if (l.shape === "wheel" && l.width !== undefined)
155
+ items.push({ id: `lm-${l.key}`, kind: "flags", at: l.at, height: l.height, planes: wheelPlanes(prefix, l.at, l.along, l.height, l.width, simple ? WHEEL_CUT.simple : WHEEL_CUT.high) });
156
+ if (l.shape === "fountains" && !simple)
157
+ items.push({ id: `lm-${l.key}`, kind: "flags", at: l.at, height: l.height, planes: fountainPlanes(prefix, l) });
158
+ }
159
+ return items;
160
+ }
package/dist/turn.dom.js CHANGED
@@ -212,6 +212,8 @@ export function mountTurn(host, options, settings, build, replayable = false, le
212
212
  root.dataset.quality = quality;
213
213
  root.dataset.tier = tier;
214
214
  root.dataset.share = String(ceiling);
215
+ // ---- One frame request a frame: the auto-turn, a replay or a race on the view, and the quality's timing of the frames ask through this, and share one animation frame when they run together. ----
216
+ const frames = frameBatch(win);
215
217
  // ---- Quality: in `auto`, the frames the view makes while it turns decide the tier. ----
216
218
  let governor = quality === "auto" && win !== null && model.tiered === true ? createGovernor(tier, settings.qualityTuning) : undefined;
217
219
  let meterFrame;
@@ -230,7 +232,7 @@ export function mountTurn(host, options, settings, build, replayable = false, le
230
232
  };
231
233
  const stopMeter = () => {
232
234
  if (meterFrame !== undefined)
233
- win?.cancelAnimationFrame(meterFrame);
235
+ frames.cancel(meterFrame);
234
236
  meterFrame = undefined;
235
237
  lastStamp = undefined;
236
238
  };
@@ -252,7 +254,7 @@ export function mountTurn(host, options, settings, build, replayable = false, le
252
254
  if (next !== undefined)
253
255
  retier(next);
254
256
  if (meterFrame === undefined)
255
- meterFrame = win.requestAnimationFrame(meterTick);
257
+ meterFrame = frames.request(meterTick);
256
258
  };
257
259
  /** The view was painted: the frames are timed from now until it is still. */
258
260
  const touched = () => {
@@ -260,7 +262,7 @@ export function mountTurn(host, options, settings, build, replayable = false, le
260
262
  return;
261
263
  lastPaint = win.performance.now();
262
264
  if (meterFrame === undefined)
263
- meterFrame = win.requestAnimationFrame(meterTick);
265
+ meterFrame = frames.request(meterTick);
264
266
  };
265
267
  /** Draw the world again, from the model of the tier and the options now: the camera, the auto-turn and the pointer are not touched (the elements are the only thing replaced). */
266
268
  const renderWorld = () => {
@@ -447,8 +449,6 @@ export function mountTurn(host, options, settings, build, replayable = false, le
447
449
  go(opening, 560, "none");
448
450
  say(turnWords(model).reset);
449
451
  };
450
- // ---- One frame request a frame: the auto-turn and a replay on the view ask through this, and share one animation frame when both run. ----
451
- const frames = frameBatch(win);
452
452
  const requestFrame = (callback) => frames.request(callback);
453
453
  const cancelFrame = (id) => frames.cancel(id);
454
454
  // ---- The auto-turn: a trophy case's slow turn. It runs only when asked, holds while the pointer holds the view, and does nothing while the page is hidden. ----
@@ -28,6 +28,7 @@ export const KYOGIJO_TURN_STYLE = `
28
28
  .ky-turn .u-orange{fill:var(--ky-up-orange)}
29
29
  .ky-turn .u-wood{fill:var(--ky-up-wood)}
30
30
  .ky-turn .u-leg{fill:var(--ky-up-leg)}
31
+ .ky-turn .u-jet{fill:var(--ky-up-jet,rgba(222,240,252,.82))}
31
32
  .ky-turn .u-pad{fill:var(--ky-up-pad)}
32
33
  .ky-turn .u-table{fill:var(--ky-up-table)}
33
34
  .ky-turn .u-foot{fill:var(--ky-up-foot)}
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.57.0";
2
+ export declare const VERSION = "0.58.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.57.0";
2
+ export const VERSION = "0.58.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnmorrisdotca/kyogijo-stadiums",
3
- "version": "0.57.0",
3
+ "version": "0.58.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",
@@ -75,6 +75,7 @@
75
75
  "./dist/grounds.js",
76
76
  "./dist/venues.all.js",
77
77
  "./dist/walk.full.js",
78
+ "./dist/circuits.held.js",
78
79
  "./dist/strings.js",
79
80
  "./dist/strings.en.js",
80
81
  "./dist/strings.ja.js"