@johnmorrisdotca/kyogijo-stadiums 0.56.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 (53) hide show
  1. package/CHANGELOG.md +15 -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.camera.d.ts +6 -3
  48. package/dist/turn.camera.js +10 -3
  49. package/dist/turn.dom.js +27 -14
  50. package/dist/turn.style.js +1 -0
  51. package/dist/version.d.ts +1 -1
  52. package/dist/version.js +1 -1
  53. 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
+ }
@@ -78,11 +78,14 @@ export declare function nudge(view: TurnView, toward: TurnToward, amount: number
78
78
  * yaw falls) and a drag down tips the top toward the eye (the pitch falls).
79
79
  */
80
80
  export declare function drag(view: TurnView, dx: number, dy: number, tilt?: boolean, degreesPerPixel?: number): TurnView;
81
+ /** Wheel steps closer together than this, in milliseconds, are one turn of the wheel or one swipe (with its glide). */
82
+ export declare const WHEEL_GESTURE_MS = 300;
81
83
  /**
82
- * The view after the wheel turns by `deltaY` (positive is down, as `WheelEvent.deltaY` says) while the view has focus: the wheel up tips the surface as a drag up does (the near edge goes up, the
83
- * front is lifted: the pitch rises) and the wheel down as a drag down does.
84
+ * The view after the wheel turns by `deltaY` (positive is down, as `WheelEvent.deltaY` says; `deltaMode` 1 is lines and 2 is pages): the wheel up zooms in and the wheel down zooms out,
85
+ * about the view's middle, as the pinch and the buttons do (the view has no pan). A trackpad's pinch (`pinch`: it arrives as Ctrl and the wheel) comes in small steps; a plain wheel's
86
+ * notch is about 100 pixels and zooms about 1.2 times, as the + key does, and one step of it never zooms more than twice either way.
84
87
  */
85
- export declare const wheelTip: (view: TurnView, deltaY: number) => TurnView;
88
+ export declare function wheelZoom(view: TurnView, deltaY: number, deltaMode?: number, pinch?: boolean): TurnView;
86
89
  /** A box in the scene, in pixels from the scene's middle, the floor at `z0` and the highest thing at `z1`. */
87
90
  export interface Box3 {
88
91
  x0: number;
@@ -100,11 +100,18 @@ export function nudge(view, toward, amount) {
100
100
  export function drag(view, dx, dy, tilt = true, degreesPerPixel = 0.4) {
101
101
  return clampView({ ...view, yaw: view.yaw - dx * degreesPerPixel, pitch: tilt ? view.pitch - dy * degreesPerPixel * 0.75 : view.pitch });
102
102
  }
103
+ /** Wheel steps closer together than this, in milliseconds, are one turn of the wheel or one swipe (with its glide). */
104
+ export const WHEEL_GESTURE_MS = 300;
103
105
  /**
104
- * The view after the wheel turns by `deltaY` (positive is down, as `WheelEvent.deltaY` says) while the view has focus: the wheel up tips the surface as a drag up does (the near edge goes up, the
105
- * front is lifted: the pitch rises) and the wheel down as a drag down does.
106
+ * The view after the wheel turns by `deltaY` (positive is down, as `WheelEvent.deltaY` says; `deltaMode` 1 is lines and 2 is pages): the wheel up zooms in and the wheel down zooms out,
107
+ * about the view's middle, as the pinch and the buttons do (the view has no pan). A trackpad's pinch (`pinch`: it arrives as Ctrl and the wheel) comes in small steps; a plain wheel's
108
+ * notch is about 100 pixels and zooms about 1.2 times, as the + key does, and one step of it never zooms more than twice either way.
106
109
  */
107
- export const wheelTip = (view, deltaY) => clampView({ ...view, pitch: view.pitch - deltaY * 0.15 });
110
+ export function wheelZoom(view, deltaY, deltaMode = 0, pinch = false) {
111
+ const pixels = deltaMode === 1 ? deltaY * (pinch ? 5 : 33) : deltaMode === 2 ? deltaY * 800 : deltaY;
112
+ const factor = pinch ? Math.exp(clamp(-pixels * 0.01, -10, 10)) : clamp(Math.exp(-pixels * 0.002), 0.5, 2);
113
+ return clampView({ ...view, zoom: view.zoom * factor });
114
+ }
108
115
  const RAD = Math.PI / 180;
109
116
  /** Where a point of the scene falls on the screen, in pixels from the middle of the container, when the scene is placed as given and the eye is `distance` pixels away. */
110
117
  export function project(point, view, at, distance) {
package/dist/turn.dom.js CHANGED
@@ -1,10 +1,10 @@
1
1
  // The 3D view in a page: `mountKyogijoTurn` writes the scene (`turn.markup.ts`) into a container and makes it turn, as the cube of kyuubu turns: a drag turns and
2
- // tips it, a pinch or Ctrl and the wheel zoom, the arrow keys step it, four viewpoints and a reset are a button or a number key away, a reader who prefers
2
+ // tips it, the wheel or a pinch zooms, the arrow keys step it, four viewpoints and a reset are a button or a number key away, a reader who prefers
3
3
  // reduced motion gets jumps for glides, and where the browser cannot draw in 3D the flat drawing stays. Nothing is polled and nothing runs while the view is
4
4
  // still: the elements' `transform`s are written when the view changes, and a glide between viewpoints is a short run of animation frames.
5
5
  import { displayProbe, frameBatch } from "./turn.frames.js";
6
6
  import { drawKyogijo } from "./draw.js";
7
- import { STEP, TURN_PRESET_NAMES, clampView, drag, nearest, nudge, presetOf, presetView, step, turnDirection, viewScale, wheelTip } from "./turn.camera.js";
7
+ import { STEP, TURN_PRESET_NAMES, clampView, drag, nearest, nudge, presetOf, presetView, step, turnDirection, viewScale, wheelZoom, WHEEL_GESTURE_MS, ZOOM_MAX, ZOOM_MIN } from "./turn.camera.js";
8
8
  import { buildTurn, floorOptions, openingView, turnWords } from "./turn.model.js";
9
9
  import { controlsMarkup, layoutOf, turnMarkupOf, worldMarkup } from "./turn.markup.js";
10
10
  import { KYOGIJO_TURN_STYLE } from "./turn.style.js";
@@ -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. ----
@@ -656,20 +656,33 @@ export function mountTurn(host, options, settings, build, replayable = false, le
656
656
  stage.addEventListener("pointerup", onUp);
657
657
  stage.addEventListener("pointercancel", onUp);
658
658
  stage.addEventListener("dblclick", toOpening);
659
- // ---- The wheel: Ctrl and the wheel (a pinch on a trackpad) zoom; with the view focused the wheel tips and turns it; otherwise the page scrolls as ever. ----
659
+ // ---- The wheel over the view zooms it, as a map does; with the pointer anywhere else the page scrolls. Ctrl or Command and the wheel (a pinch on a trackpad) zoom too. Shift and the
660
+ // wheel, or a sideways swipe, turn it. Tipping is a drag, the keys and the buttons. A wheel that starts while the zoom is already at its end that way goes on to scroll the page, so the page is
661
+ // never trapped; one that reaches the end part-way through stops there rather than flinging the page. ----
662
+ let wheelTaken = Number.NEGATIVE_INFINITY;
660
663
  const onWheel = (event) => {
661
- if (event.ctrlKey) {
664
+ const pinch = event.ctrlKey || event.metaKey;
665
+ const now = Number.isFinite(event.timeStamp) ? event.timeStamp : 0;
666
+ const continuing = now >= wheelTaken && now - wheelTaken < WHEEL_GESTURE_MS;
667
+ if (!pinch && (event.shiftKey || Math.abs(event.deltaX) > Math.abs(event.deltaY))) {
662
668
  event.preventDefault();
669
+ wheelTaken = now;
663
670
  grab();
664
- view = clampView({ ...view, zoom: view.zoom * Math.exp(-event.deltaY * 0.01) });
671
+ view = clampView({ ...view, yaw: view.yaw + (event.deltaX || event.deltaY) * 0.25 });
665
672
  }
666
- else if (doc.activeElement === stage) {
673
+ else {
674
+ const next = wheelZoom(view, event.deltaY, event.deltaMode, pinch);
675
+ const atEnd = event.deltaY <= 0 ? view.zoom >= ZOOM_MAX : view.zoom <= ZOOM_MIN;
676
+ // Ctrl and the wheel is always the view's, or the browser would zoom the whole page.
677
+ if (!pinch && atEnd && !continuing)
678
+ return;
667
679
  event.preventDefault();
680
+ wheelTaken = now;
681
+ if (next.zoom === view.zoom)
682
+ return;
668
683
  grab();
669
- view = event.shiftKey || Math.abs(event.deltaX) > Math.abs(event.deltaY) ? clampView({ ...view, yaw: view.yaw + (event.deltaX || event.deltaY) * 0.25 }) : wheelTip(view, event.deltaY);
684
+ view = next;
670
685
  }
671
- else
672
- return;
673
686
  shown = view;
674
687
  paint(shown);
675
688
  label();
@@ -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.56.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.56.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.56.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"