@johnmorrisdotca/kyogijo-stadiums 0.34.0 → 0.35.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,23 @@ All notable changes to this project are written here. The format follows
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.35.0] - 2026-10-20
11
+
12
+ ### Added
13
+
14
+ - **The auto-turn goes either way** (`TurnSettings.direction`, `KyogijoTurn.direction()` and `setDirection()`, `turnDirection()`, `TurnSettings.onDirectionChange`, the element's `turn-direction`): `"counterclockwise"` is how it has always turned, the near side swinging to the viewer's right, and stays the default; `"clockwise"` is the other way. The 3D view has a **Reverse** button (`逆回転`), a toggle pressed while it goes clockwise, that changes the way at once without stopping the turn and says so aloud; a drag, the keys and the other buttons are not reversed. Every 3D view has it: the stadiums, the districts, the tournament layouts and the judging view too. [The 3D view](./docs/turn.md#the-auto-turn).
15
+
16
+ ### Changed
17
+
18
+ - **The demo waits still and turns on the pointer** (`demo/turn-way.js`): every 3D page of the demo (the home page and its featured arena, the venues, stadiums, districts, tournament layouts, judging and boards pages) opens still, turns while the pointer is over the view, and holds when it leaves; a tap on a phone starts it and a second tap holds it; Auto-turn still starts it for good (`spin=on` on the venues page's address), and a reader who prefers reduced motion is never turned for by the pointer. The pages turn at half the old speed (6 degrees a second; the package's own default, 12, is unchanged), and Reverse is kept in the address as `dir=cw`. The replays play while the pointer is over them in the same way. The fly-through page opens still and flies on the pointer at `speed=0.5`, half the pace (`?play=1` has it fly from the start).
19
+ - **The README opens on the still pictures again**: the two turning pictures at the head are gone, and each moving picture is beside its own section (the real venues, the tournament layouts, the fly-through), at half the speed (100 milliseconds a frame for the arenas, 260 for the fly-through); the real venues' pictures are taken again with the filled bowls of 0.34.1. The pictures only a page of docs shows are in `docs/pictures`, so that `docs/images` holds the README's own: the family's README lint (`scripts/readme-lint.mjs`) is again its own file with one rule added, the moving pictures' budget.
20
+
21
+ ## [0.34.1] - 2026-10-20
22
+
23
+ ### Fixed
24
+
25
+ - **No gray empty areas in the 3D view of a real venue** (`src/venue.fill.ts`, [the venues](./docs/venues.md), [the stadiums](./docs/stadiums.md)): the 3D arenas showed flat gray nothing in three places. **The shelf between the bowls** is now the suite level: a facing wall with a band of windows, a pane to every 7 feet, and a ledge. **The apron between the surface and the first row** is a thin dark floor, with the players' benches, the penalty boxes and the timekeeper's table at the usual places on the bench side of an ice rink. **The slab between the back of the bowl and the building** is closed by a back wall with a coping (it gives way to the eye as the building's walls do, keeping a little of itself) and a darker concourse floor with its lanes and joints, out to the building's outline, the annexes included. Every arena of the NHL, every stadium and the hall, and a tournament layout's stadium (`VenueSettings.fill`) have the fill; the usual stands (`crowd: true`) are unchanged. New part kinds `apron`, `bench` and `floor`; a ballpark gets the suite wall and the back wall, and no apron or floor of its own.
26
+
10
27
  ## [0.34.0] - 2026-10-20
11
28
 
12
29
  ### Added
package/README.md CHANGED
@@ -13,25 +13,6 @@ The surfaces of forty-three sports, from football and curling to the budō and t
13
13
 
14
14
  <p align="center"><a href="https://johnmorrisdotca.github.io/kyogijo-stadiums/"><strong>Open the demo →</strong></a> · <a href="https://johnmorrisdotca.github.io/kyogijo-stadiums/docs/">Documentation</a> · <a href="https://johnmorrisdotca.github.io/kyogijo-stadiums/docs/gallery.html">Gallery</a> · <a href="./docs/compare.md">How it compares</a> · <a href="https://johnmorrisdotca.github.io/kyogijo-stadiums/api.html">API reference</a></p>
15
15
 
16
- <table align="center">
17
- <tr>
18
- <td align="center" valign="top">
19
- <picture>
20
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/turning-venue-desk-dark.webp">
21
- <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/turning-venue-desk-light.webp" alt="A real arena turning once round in 3D: Climate Pledge Arena in the outline of its building, two tiers of seats round the rink." width="410">
22
- </picture>
23
- <br><em>A real arena, turning: Climate Pledge Arena in the outline of its building (OpenStreetMap).</em>
24
- </td>
25
- <td align="center" valign="top">
26
- <picture>
27
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/turning-hall-desk-dark.webp">
28
- <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/turning-hall-desk-light.webp" alt="A tournament layout turning once round in 3D: four table tennis areas on one floor, a surround round each and a scoreboard hung over them." width="410">
29
- </picture>
30
- <br><em>A tournament layout, turning: four table tennis areas under one scoreboard.</em>
31
- </td>
32
- </tr>
33
- </table>
34
-
35
16
  > **Renamed.** Previously `@johnmorrisdotca/kyogijo`; renamed in 0.4.0 to `@johnmorrisdotca/kyogijo-stadiums`. The product is still Kyogijo Stadiums (競技場); only the package and the repository names changed. See the [changelog](./CHANGELOG.md).
36
17
 
37
18
  <table align="center">
@@ -409,6 +390,7 @@ import { mountKyogijoTurn } from "@johnmorrisdotca/kyogijo-stadiums/turn";
409
390
 
410
391
  const view = mountKyogijoTurn(document.querySelector("#court")!, { sport: "volleyball", status: "live" }, { preset: "end" });
411
392
  view.preset("corner");
393
+ view.setDirection("clockwise"); // the auto-turn goes the other way (`direction` in the settings says it at the start)
412
394
  ```
413
395
 
414
396
  ```ts
@@ -420,7 +402,7 @@ console.log(items.map((item) => item.kind).join(), html.includes("preserve-3d"))
420
402
  // net,netposts,antennae false
421
403
  ```
422
404
 
423
- The flat drawing is the floor, turned by a drag, a swipe, the keys or four viewpoints, with what the rulebook stands on it at its height; each part is one `matrix3d`. In the element: `<kyogijo-field sport="nhl" turn turn-view="broadcast">`. [The 3D view](./docs/turn.md).
405
+ The flat drawing is the floor, turned by a drag, a swipe, the keys or four viewpoints, with what the rulebook stands on it at its height; each part is one `matrix3d`. The auto-turn waits for Play, and turns either way: `counterclockwise` (the near side swinging to the viewer's right) by default, `clockwise` on the Reverse button. In the element: `<kyogijo-field sport="nhl" turn turn-view="broadcast">`. [The 3D view](./docs/turn.md).
424
406
 
425
407
  <p align="center">
426
408
  <picture>
@@ -561,6 +543,14 @@ console.log(layout.needs.length, layout.needs.width);
561
543
  // 47 19
562
544
  ```
563
545
 
546
+ <p align="center">
547
+ <picture>
548
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/turning-hall-desk-dark.webp">
549
+ <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/turning-hall-desk-light.webp" alt="A tournament layout turning once round in 3D, slowly: four table tennis areas on one floor, a surround round each and a scoreboard hung over them." width="640">
550
+ </picture>
551
+ <br><em>A tournament layout, turning: four table tennis areas under one scoreboard.</em>
552
+ </p>
553
+
564
554
  ### Boards over a surface
565
555
 
566
556
  `board` lays a results board, a bout presentation or a medal table, any SVG, over a surface in 2D and 3D, off its middle and never stretched ([boards](./docs/boards.md)).
@@ -591,7 +581,7 @@ console.log(layout.needs.length, layout.needs.width);
591
581
 
592
582
  ### The real venues
593
583
 
594
- `crowd: { real: true }` draws an NHL arena in its building: the outline from OpenStreetMap, the rink lying along the building where it says so, and a plausible bowl of the seats, tiers and roof the sources give. All thirty-two arenas of the league, division by division ([the real venues](./docs/venues.md)). In 3D the demo's [venues page](https://johnmorrisdotca.github.io/kyogijo-stadiums/venues.html) turns by itself, with the stands, walls, roof, building, markings and labels switched on the view while it turns.
584
+ `crowd: { real: true }` draws an NHL arena in its building: the outline from OpenStreetMap, the rink lying along the building where it says so, and a plausible bowl of the seats, tiers and roof the sources give. All thirty-two arenas of the league, division by division ([the real venues](./docs/venues.md)). In 3D the demo's [venues page](https://johnmorrisdotca.github.io/kyogijo-stadiums/venues.html) turns while the pointer is over it, with the stands, walls, roof, building, markings and labels switched on the view while it turns.
595
585
 
596
586
  ```ts
597
587
  import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
@@ -602,6 +592,14 @@ console.log(svg.includes("ky-cr-shell"), planVenue(venueById("climate-pledge-are
602
592
  // true 17151
603
593
  ```
604
594
 
595
+ <p align="center">
596
+ <picture>
597
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/turning-venue-desk-dark.webp">
598
+ <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/turning-venue-desk-light.webp" alt="A real arena turning once round in 3D, slowly: Climate Pledge Arena in the outline of its building, two tiers of seats with a suite level of windows between them, and a darker concourse floor round the bowl." width="640">
599
+ </picture>
600
+ <br><em>A real arena, turning: Climate Pledge Arena in the outline of its building (OpenStreetMap).</em>
601
+ </p>
602
+
605
603
  ### A real stadium, and a district
606
604
 
607
605
  `stadium: "bc-place"` draws BC Place for football or soccer under its cable roof, and Lumen Field, T-Mobile Park, Tokyo Dome, Koshien Stadium (`park`) and Korakuen Hall (`sport: "boxing"`) likewise ([stadiums](./docs/stadiums.md)); `drawDistrict({ venues: ["bc-place", "rogers-arena"] })` puts venues at their real positions, tight as neighbours or spread as a city, to turn together in 3D ([districts](./docs/district.md)).
package/dist/element.d.ts CHANGED
@@ -6,7 +6,7 @@ import type { ScoreOptions, SurfaceOptions } from "./types.ts";
6
6
  /** The tag of the element. */
7
7
  export declare const KYOGIJO_TAG = "kyogijo-field";
8
8
  /** The attributes the element reads. */
9
- export declare const KYOGIJO_ATTRIBUTES: readonly ["sport", "league", "size", "theme", "language", "orientation", "park", "ground", "arena", "year", "detail", "margin", "stripes", "exact-lines", "uppercase", "pitch", "pitch-type", "half", "range", "range-end", "goals", "stripe-pattern", "corner-flags", "look", "line-width", "line-dash", "line-opacity", "landmarks", "walls", "quirks", "home", "away", "home-abbr", "away-abbr", "home-score", "away-score", "home-penalties", "away-penalties", "score", "home-logo", "away-logo", "home-flag", "away-flag", "home-color", "away-color", "status", "competition", "date", "label", "winner", "home-side", "pageant", "description", "options", "loop", "speed", "autoplay", "controls", "trail", "turn", "turn-view", "turn-controls", "turn-uprights", "turn-score", "turn-touch", "turn-venue", "turn-roof", "turn-autoplay", "turn-speed"];
9
+ export declare const KYOGIJO_ATTRIBUTES: readonly ["sport", "league", "size", "theme", "language", "orientation", "park", "ground", "arena", "year", "detail", "margin", "stripes", "exact-lines", "uppercase", "pitch", "pitch-type", "half", "range", "range-end", "goals", "stripe-pattern", "corner-flags", "look", "line-width", "line-dash", "line-opacity", "landmarks", "walls", "quirks", "home", "away", "home-abbr", "away-abbr", "home-score", "away-score", "home-penalties", "away-penalties", "score", "home-logo", "away-logo", "home-flag", "away-flag", "home-color", "away-color", "status", "competition", "date", "label", "winner", "home-side", "pageant", "description", "options", "loop", "speed", "autoplay", "controls", "trail", "turn", "turn-view", "turn-controls", "turn-uprights", "turn-score", "turn-touch", "turn-venue", "turn-roof", "turn-autoplay", "turn-speed", "turn-direction"];
10
10
  /** The settings of a 3D view an element's attributes make: `turn-view` names the viewpoint it opens on, `turn-controls="false"` and `turn-uprights="false"` leave the buttons and what stands on the surface out, `turn-score="floor"` leaves the score panel on the floor, `turn-touch="turn"` keeps every touch for the view, `turn-venue="false"` leaves out the stands and the room (they are raised for a drawing with a crowd and for a dojo), `turn-roof` puts a roof or canopy over the stands, `turn-autoplay` starts the auto-turn (`force` even for a reader who prefers reduced motion) and `turn-speed` is its degrees a second. */
11
11
  export declare function turnSettingsFromAttributes(read: (name: string) => string | null, base?: TurnSettings): TurnSettings;
12
12
  /** Whether an element's `turn` attribute asks for the 3D view: present, and not `false`. */
@@ -24,7 +24,7 @@ export declare function optionsFromAttributes(read: (name: string) => string | n
24
24
  declare const Base: typeof HTMLElement;
25
25
  export declare class KyogijoField extends Base {
26
26
  #private;
27
- static observedAttributes: readonly ["sport", "league", "size", "theme", "language", "orientation", "park", "ground", "arena", "year", "detail", "margin", "stripes", "exact-lines", "uppercase", "pitch", "pitch-type", "half", "range", "range-end", "goals", "stripe-pattern", "corner-flags", "look", "line-width", "line-dash", "line-opacity", "landmarks", "walls", "quirks", "home", "away", "home-abbr", "away-abbr", "home-score", "away-score", "home-penalties", "away-penalties", "score", "home-logo", "away-logo", "home-flag", "away-flag", "home-color", "away-color", "status", "competition", "date", "label", "winner", "home-side", "pageant", "description", "options", "loop", "speed", "autoplay", "controls", "trail", "turn", "turn-view", "turn-controls", "turn-uprights", "turn-score", "turn-touch", "turn-venue", "turn-roof", "turn-autoplay", "turn-speed"];
27
+ static observedAttributes: readonly ["sport", "league", "size", "theme", "language", "orientation", "park", "ground", "arena", "year", "detail", "margin", "stripes", "exact-lines", "uppercase", "pitch", "pitch-type", "half", "range", "range-end", "goals", "stripe-pattern", "corner-flags", "look", "line-width", "line-dash", "line-opacity", "landmarks", "walls", "quirks", "home", "away", "home-abbr", "away-abbr", "home-score", "away-score", "home-penalties", "away-penalties", "score", "home-logo", "away-logo", "home-flag", "away-flag", "home-color", "away-color", "status", "competition", "date", "label", "winner", "home-side", "pageant", "description", "options", "loop", "speed", "autoplay", "controls", "trail", "turn", "turn-view", "turn-controls", "turn-uprights", "turn-score", "turn-touch", "turn-venue", "turn-roof", "turn-autoplay", "turn-speed", "turn-direction"];
28
28
  /** The options given as a property: an object, laid under the attributes. */
29
29
  get options(): Partial<ScoreOptions> | undefined;
30
30
  set options(value: Partial<ScoreOptions> | undefined);
package/dist/element.js CHANGED
@@ -25,7 +25,7 @@ export const KYOGIJO_ATTRIBUTES = [
25
25
  "home", "away", "home-abbr", "away-abbr", "home-score", "away-score", "home-penalties", "away-penalties", "score", "home-logo", "away-logo", "home-flag", "away-flag", "home-color", "away-color",
26
26
  "status", "competition", "date", "label", "winner", "home-side", "pageant", "description", "options",
27
27
  "loop", "speed", "autoplay", "controls", "trail",
28
- "turn", "turn-view", "turn-controls", "turn-uprights", "turn-score", "turn-touch", "turn-venue", "turn-roof", "turn-autoplay", "turn-speed",
28
+ "turn", "turn-view", "turn-controls", "turn-uprights", "turn-score", "turn-touch", "turn-venue", "turn-roof", "turn-autoplay", "turn-speed", "turn-direction",
29
29
  ];
30
30
  /** The attributes that are about a replay and not about the drawing. */
31
31
  const REPLAY_ATTRIBUTES = ["loop", "speed", "autoplay", "controls", "trail"];
@@ -57,6 +57,9 @@ export function turnSettingsFromAttributes(read, base = {}) {
57
57
  const speed = Number(read("turn-speed"));
58
58
  if (read("turn-speed") !== null && Number.isFinite(speed))
59
59
  out.speed = speed;
60
+ const direction = read("turn-direction");
61
+ if (direction === "clockwise" || direction === "counterclockwise")
62
+ out.direction = direction;
60
63
  return out;
61
64
  }
62
65
  /** Whether an element's `turn` attribute asks for the 3D view: present, and not `false`. */
@@ -59,6 +59,8 @@ export function mountJudgingBroadcast(host, options, settings = {}) {
59
59
  },
60
60
  play: () => turn.play(),
61
61
  pause: () => turn.pause(),
62
+ direction: () => turn.direction(),
63
+ setDirection: (direction) => turn.setDirection(direction),
62
64
  togglePlay: () => turn.togglePlay(),
63
65
  svg: () => turn.svg(),
64
66
  destroy: () => {
@@ -177,7 +177,7 @@ export function buildMultiTurn(options, settings = {}) {
177
177
  let crowdStyle = "";
178
178
  if (drawn.stands !== undefined && keep("stands")) {
179
179
  const roof = typeof settings.venue === "object" && settings.venue.roof !== undefined ? settings.venue.roof : typeof options.stadium === "object" && options.stadium.roof === true;
180
- const venue = standsVenueOf(drawn.stands.stands, drawn.stands.crowd, { sport: drawn.stands.standsLike }, { ...(typeof settings.venue === "object" ? settings.venue : {}), roof, id }, 0);
180
+ const venue = standsVenueOf(drawn.stands.stands, drawn.stands.crowd, { sport: drawn.stands.standsLike }, { fill: true, ...(typeof settings.venue === "object" ? settings.venue : {}), roof, id }, 0);
181
181
  items.push(...venue.items.filter((item) => item.kind !== "roof" || roof));
182
182
  defs = venue.defs;
183
183
  crowdStyle = "--ky-cr-home:#8b94a3;--ky-cr-away:#8b94a3";
package/dist/strings.d.ts CHANGED
@@ -1030,6 +1030,11 @@ export interface KyogijoWords {
1030
1030
  playOff: string;
1031
1031
  announcePlay: string;
1032
1032
  announcePause: string;
1033
+ /** The Reverse button's name (pressed while the auto-turn goes clockwise), what it is for, and what is announced when the direction changes. */
1034
+ reverse: string;
1035
+ reverseLabel: string;
1036
+ announceClockwise: string;
1037
+ announceCounterclockwise: string;
1033
1038
  /** What is announced after a key or a button: {name} for a viewpoint, and the angles: {yaw} and {pitch} in degrees. */
1034
1039
  announce: string;
1035
1040
  announceAngle: string;
package/dist/strings.js CHANGED
@@ -451,7 +451,7 @@ export const KYOGIJO_STRINGS = {
451
451
  keys: "A 3D view you can turn. Drag, or use the arrow keys, to turn and tilt it; plus and minus zoom; 1 to 4 pick a viewpoint; 0 or Home puts it back; Space starts and stops the auto-turn. Pinch, or hold Ctrl and scroll, to zoom.",
452
452
  overhead: "Overhead", broadcast: "Broadcast", end: "End-on", corner: "Low corner",
453
453
  reset: "Reset the view", turnLeft: "Turn left", turnRight: "Turn right", raise: "Tip the surface away", lower: "Tip the surface toward you", zoomIn: "Zoom in", zoomOut: "Zoom out",
454
- play: "Auto-turn", playOn: "Start the auto-turn", playOff: "Pause the auto-turn", announcePlay: "Auto-turn on", announcePause: "Auto-turn paused",
454
+ play: "Auto-turn", playOn: "Start the auto-turn", playOff: "Pause the auto-turn", announcePlay: "Auto-turn on", announcePause: "Auto-turn paused", reverse: "Reverse", reverseLabel: "Turn the other way", announceClockwise: "Turning clockwise", announceCounterclockwise: "Turning counterclockwise",
455
455
  announce: "{name} view", announceAngle: "Turned to {yaw} degrees, seen from {pitch} degrees above the floor.",
456
456
  flat: "This browser cannot draw the 3D view, so the flat drawing is shown.",
457
457
  standing: "Standing on it, in the 3D view: {list}.", item: "{name} {h}", itemSag: "{name} {h} at the posts, {low} at the middle",
@@ -1010,7 +1010,7 @@ export const KYOGIJO_STRINGS = {
1010
1010
  keys: "回して見られる3D表示です。ドラッグするか矢印キーで回転と傾きを変え、プラスとマイナスのキーで拡大と縮小、1から4のキーで視点を選び、0キーまたはHomeキーで元に戻し、スペースキーで自動回転を始めたり止めたりします。ピンチ操作、またはCtrlキーを押しながらのスクロールでも拡大・縮小できます。",
1011
1011
  overhead: "真上", broadcast: "中継カメラ", end: "エンド側から", corner: "低い角度から",
1012
1012
  reset: "元に戻す", turnLeft: "左へ回す", turnRight: "右へ回す", raise: "床面を奥へ傾ける", lower: "床面を手前へ傾ける", zoomIn: "拡大", zoomOut: "縮小",
1013
- play: "自動回転", playOn: "自動回転を始める", playOff: "自動回転を一時停止する", announcePlay: "自動回転を始めました", announcePause: "自動回転を一時停止しました",
1013
+ play: "自動回転", playOn: "自動回転を始める", playOff: "自動回転を一時停止する", announcePlay: "自動回転を始めました", announcePause: "自動回転を一時停止しました", reverse: "逆回転", reverseLabel: "逆向きに回す", announceClockwise: "時計回りに回転しています", announceCounterclockwise: "反時計回りに回転しています",
1014
1014
  announce: "{name}の視点", announceAngle: "向きは{yaw}度、見下ろす角度は{pitch}度です。",
1015
1015
  flat: "このブラウザーでは3D表示ができないため、平面の図を表示しています。",
1016
1016
  standing: "3D表示で立って見えるもの:{list}。", item: "{name} {h}", itemSag: "{name} 支柱の位置で{h}(中央は{low})",
@@ -1,6 +1,8 @@
1
1
  export { mountKyogijoTurn, mountTurn, turnSupported } from "./turn.dom.ts";
2
2
  export type { KyogijoTurn } from "./turn.dom.ts";
3
3
  export { buildTurn, floorOptions, openingView } from "./turn.model.ts";
4
+ export { turnDirection } from "./turn.camera.ts";
5
+ export type { TurnDirection } from "./turn.camera.ts";
4
6
  export type { TurnModel, TurnSettings } from "./turn.model.ts";
5
7
  export { layoutOf, planeMarkup, planeMatrix, turnMarkup, turnMarkupOf } from "./turn.markup.ts";
6
8
  export type { Layout } from "./turn.markup.ts";
@@ -1,6 +1,7 @@
1
1
  // The 3D view: a surface as a floor that turns, with a net, a basket, a goal or a pair of posts standing on it.
2
2
  export { mountKyogijoTurn, mountTurn, turnSupported } from "./turn.dom.js";
3
3
  export { buildTurn, floorOptions, openingView } from "./turn.model.js";
4
+ export { turnDirection } from "./turn.camera.js";
4
5
  export { layoutOf, planeMarkup, planeMatrix, turnMarkup, turnMarkupOf } from "./turn.markup.js";
5
6
  export { KYOGIJO_TURN_STYLE } from "./turn.style.js";
6
7
  export { CURLING_PRESETS, LONG_PRESETS, NET_PRESETS, PITCH_MAX, STEP, TURN_DEFAULT, TURN_PRESETS, TURN_PRESET_NAMES, ZOOM_MAX, ZOOM_MIN, cameraMatrix, clampView, depthOf, drag, eyeDistance, fit, matrix3d, multiply, nudge, presetOf, presetView, presetsFor, project, step } from "./turn.camera.js";
@@ -7,6 +7,13 @@ export interface TurnView {
7
7
  /** The named viewpoints. */
8
8
  export type TurnPreset = "overhead" | "broadcast" | "end" | "corner";
9
9
  /** The viewpoints, in the order the controls and the number keys list them. */
10
+ /**
11
+ * Which way the auto-turn goes, seen from above. `"counterclockwise"` is how it has always turned: the near side swings to the viewer's right. `"clockwise"` is the other way: the near side swings to the left.
12
+ * A drag, the keys and the buttons are not reversed.
13
+ */
14
+ export type TurnDirection = "clockwise" | "counterclockwise";
15
+ /** A direction from anything: `"clockwise"` is clockwise, and everything else is the default, `"counterclockwise"`. */
16
+ export declare function turnDirection(value: unknown): TurnDirection;
10
17
  export declare const TURN_PRESET_NAMES: readonly TurnPreset[];
11
18
  /**
12
19
  * The four viewpoints. `overhead` looks straight down; `broadcast` is the main camera, high on one side line and turned a little towards the near end so that the boards, the nets and the goals show their faces; `end` stands behind an end line, along the
@@ -3,7 +3,10 @@
3
3
  // worked out in Node, tested, and handed to a stylesheet as one `transform`.
4
4
  //
5
5
  // CSS has x to the right, y down and z towards the eye, and the scene is built that way: the floor lies in the x-y plane facing the eye, heights are z.
6
- /** The viewpoints, in the order the controls and the number keys list them. */
6
+ /** A direction from anything: `"clockwise"` is clockwise, and everything else is the default, `"counterclockwise"`. */
7
+ export function turnDirection(value) {
8
+ return value === "clockwise" ? "clockwise" : "counterclockwise";
9
+ }
7
10
  export const TURN_PRESET_NAMES = ["overhead", "broadcast", "end", "corner"];
8
11
  /**
9
12
  * The four viewpoints. `overhead` looks straight down; `broadcast` is the main camera, high on one side line and turned a little towards the near end so that the boards, the nets and the goals show their faces; `end` stands behind an end line, along the
@@ -1,4 +1,4 @@
1
- import { type TurnPreset, type TurnView } from "./turn.camera.ts";
1
+ import { type TurnDirection, type TurnPreset, type TurnView } from "./turn.camera.ts";
2
2
  import { type TurnModel, type TurnSettings } from "./turn.model.ts";
3
3
  import type { ScoreOptions, SurfaceOptions } from "./types.ts";
4
4
  import type { VenueSettings } from "./venue.ts";
@@ -27,6 +27,10 @@ export interface KyogijoTurn<O = SurfaceOptions | ScoreOptions> {
27
27
  play(): boolean;
28
28
  /** Stop the auto-turn. */
29
29
  pause(): void;
30
+ /** Which way the auto-turn goes. */
31
+ direction(): TurnDirection;
32
+ /** Change which way the auto-turn goes, at once and without stopping it (the Reverse button does). */
33
+ setDirection(direction: TurnDirection): void;
30
34
  /** Start the auto-turn if it is stopped and stop it if it is running; it gives whether it is running now. */
31
35
  togglePlay(): boolean;
32
36
  /** The flat drawing that is the floor, or `null` after `destroy`. */
package/dist/turn.dom.js CHANGED
@@ -3,7 +3,7 @@
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 { drawKyogijo } from "./draw.js";
6
- import { STEP, TURN_PRESET_NAMES, clampView, drag, nearest, nudge, presetOf, presetView, step } from "./turn.camera.js";
6
+ import { STEP, TURN_PRESET_NAMES, clampView, drag, nearest, nudge, presetOf, presetView, step, turnDirection } from "./turn.camera.js";
7
7
  import { buildTurn, floorOptions, openingView, turnWords } from "./turn.model.js";
8
8
  import { controlsMarkup, layoutOf, turnMarkupOf, worldMarkup } from "./turn.markup.js";
9
9
  import { KYOGIJO_TURN_STYLE } from "./turn.style.js";
@@ -62,6 +62,8 @@ export function mountTurn(host, options, settings, build) {
62
62
  playing: false,
63
63
  play: () => false,
64
64
  pause: () => undefined,
65
+ direction: () => turnDirection(settings.direction),
66
+ setDirection: () => undefined,
65
67
  togglePlay: () => false,
66
68
  update: (next, change) => {
67
69
  if (change?.venue !== undefined)
@@ -93,6 +95,7 @@ export function mountTurn(host, options, settings, build) {
93
95
  let lastTick;
94
96
  let advanced = false;
95
97
  const speed = typeof settings.speed === "number" && Number.isFinite(settings.speed) ? settings.speed : 12;
98
+ let way = turnDirection(settings.direction);
96
99
  const calm = () => settings.motion === "none" || (settings.motion !== "smooth" && media?.matches === true);
97
100
  // The elements, floor first, and what was written to each, so that only a change is written.
98
101
  let elements = [];
@@ -236,7 +239,7 @@ export function mountTurn(host, options, settings, build) {
236
239
  lastTick = stamp;
237
240
  if (dt > 0) {
238
241
  stop();
239
- view = clampView({ ...view, yaw: view.yaw - speed * dt });
242
+ view = clampView({ ...view, yaw: view.yaw - (way === "clockwise" ? -1 : 1) * speed * dt });
240
243
  shown = view;
241
244
  advanced = true;
242
245
  paint(shown);
@@ -266,6 +269,16 @@ export function mountTurn(host, options, settings, build) {
266
269
  say(next ? turnWords(model).announcePlay : turnWords(model).announcePause);
267
270
  settings.onPlayChange?.(next);
268
271
  };
272
+ /** Change the direction; the Reverse button says which it is, and the page is told. */
273
+ const setWay = (next, quiet = false) => {
274
+ if (next === way)
275
+ return;
276
+ way = next;
277
+ root.querySelector("button[data-act=reverse]")?.setAttribute("aria-pressed", String(way === "clockwise"));
278
+ if (!quiet)
279
+ say(way === "clockwise" ? turnWords(model).announceClockwise : turnWords(model).announceCounterclockwise);
280
+ settings.onDirectionChange?.(way);
281
+ };
269
282
  // A view scrolled out of sight does not turn.
270
283
  let onScreen = true;
271
284
  let seen;
@@ -304,6 +317,7 @@ export function mountTurn(host, options, settings, build) {
304
317
  case "zoom-in": return go(step(view, { zoom: STEP.zoom }), 180, "none", true);
305
318
  case "zoom-out": return go(step(view, { zoom: 1 / STEP.zoom }), 180, "none", true);
306
319
  case "play": return void setPlaying(!playing);
320
+ case "reverse": return setWay(way === "clockwise" ? "counterclockwise" : "clockwise");
307
321
  case "reset": return toOpening();
308
322
  default:
309
323
  }
@@ -476,7 +490,7 @@ export function mountTurn(host, options, settings, build) {
476
490
  root.removeAttribute("data-theme");
477
491
  const controls = root.querySelector(".ky-turn-controls");
478
492
  if (controls !== null)
479
- controls.outerHTML = controlsMarkup(model, view, playing);
493
+ controls.outerHTML = controlsMarkup(model, view, playing, way);
480
494
  paint(shown);
481
495
  label();
482
496
  },
@@ -490,6 +504,10 @@ export function mountTurn(host, options, settings, build) {
490
504
  pause() {
491
505
  setPlaying(false);
492
506
  },
507
+ direction: () => way,
508
+ setDirection(next) {
509
+ setWay(turnDirection(next));
510
+ },
493
511
  togglePlay() {
494
512
  setPlaying(!playing);
495
513
  return playing;
@@ -1,7 +1,7 @@
1
1
  /** A point or a direction: x, y, z in feet. */
2
2
  export type V3 = readonly [number, number, number];
3
3
  /** What a plane is a part of: it is the class of its element (`ky-up-<kind>`) and what a test finds it by. */
4
- export type PartKind = "post" | "net" | "tape" | "antenna" | "board" | "glass" | "rim" | "stanchion" | "frame" | "goal" | "flag" | "stump" | "leg" | "table" | "pylon" | "wall" | "screen" | "pole" | "ground" | "target" | "seats" | "concourse" | "fascia" | "roof" | "banner" | "platform" | "rail" | "matedge" | "shrine" | "surround" | "chair" | "desk" | "card" | "sign";
4
+ export type PartKind = "post" | "net" | "tape" | "antenna" | "board" | "glass" | "rim" | "stanchion" | "frame" | "goal" | "flag" | "stump" | "leg" | "table" | "pylon" | "wall" | "screen" | "pole" | "ground" | "target" | "seats" | "concourse" | "fascia" | "roof" | "banner" | "platform" | "rail" | "matedge" | "shrine" | "surround" | "chair" | "desk" | "card" | "sign" | "apron" | "bench" | "floor";
5
5
  /** One flat rectangle. Its own x runs along `u` (its whole length) and its own y along `v`; `o` is the corner both start from. `svg` is what is drawn on it, in feet along `u` and `v`. */
6
6
  export interface Plane {
7
7
  /** A name that says what it is and where: `net`, `post-left`, `goal-right-back`. */
@@ -1,4 +1,4 @@
1
- import { type Placement, type TurnView } from "./turn.camera.ts";
1
+ import { type Placement, type TurnDirection, type TurnView } from "./turn.camera.ts";
2
2
  import { type Plane } from "./turn.kit.ts";
3
3
  import { type TurnModel, type TurnSettings } from "./turn.model.ts";
4
4
  import type { ScoreOptions, SurfaceOptions } from "./types.ts";
@@ -45,7 +45,7 @@ export declare function worldAttributes(model: TurnModel): string;
45
45
  /** The floor and the planes of a model, laid out. */
46
46
  export declare function worldMarkup(model: TurnModel, layout: Layout): string;
47
47
  /** The buttons under the view. */
48
- export declare function controlsMarkup(model: TurnModel, view: TurnView, playing?: boolean): string;
48
+ export declare function controlsMarkup(model: TurnModel, view: TurnView, playing?: boolean, direction?: TurnDirection): string;
49
49
  /** The size, in pixels, the first view is worked out for. */
50
50
  export declare const sizeOf: (settings: TurnSettings) => {
51
51
  width: number;
@@ -2,7 +2,7 @@
2
2
  // `perspective` on a parent): every element carries its whole place on the screen in one `matrix3d`, worked out here from the way the scene is looked at, and
3
3
  // the elements are stacked far to near by `z-index`. A browser that cannot keep nested 3D layers (Safari on a phone can lose them) draws it as it is meant.
4
4
  // The same text is what a page is given from `turnMarkup` and what the mount writes; the mount then makes it turn.
5
- import { TURN_PRESET_NAMES, cameraMatrix, depthOf, eyeDistance, fit, matrix3d, multiply, presetOf } from "./turn.camera.js";
5
+ import { TURN_PRESET_NAMES, cameraMatrix, depthOf, eyeDistance, fit, matrix3d, multiply, presetOf, turnDirection } from "./turn.camera.js";
6
6
  import { cross, facing, fadeOf, n, size, unit } from "./turn.kit.js";
7
7
  import { buildTurn, turnWords } from "./turn.model.js";
8
8
  import { KYOGIJO_TURN_STYLE } from "./turn.style.js";
@@ -123,7 +123,7 @@ export function worldMarkup(model, layout) {
123
123
  return defsMarkup(model) + floorMarkup(model, layout.transforms[0], layout.stack[0]) + model.planes.map((p, i) => (p.crop === true ? cropMarkup(model, p, layout.transforms[i + 1], layout.stack[i + 1], layout.fade[i + 1]) : planeMarkup(p, model.scale, layout.transforms[i + 1], layout.stack[i + 1], layout.fade[i + 1]))).join("");
124
124
  }
125
125
  /** The buttons under the view. */
126
- export function controlsMarkup(model, view, playing = false) {
126
+ export function controlsMarkup(model, view, playing = false, direction = "counterclockwise") {
127
127
  const words = turnWords(model);
128
128
  const here = presetOf(view, model.presets ?? model.options.sport);
129
129
  const presets = TURN_PRESET_NAMES.map((name, i) => `<button type="button" data-preset="${name}" aria-pressed="${here === name}" aria-keyshortcuts="${i + 1}">${esc(words[name])}</button>`).join("");
@@ -138,7 +138,9 @@ export function controlsMarkup(model, view, playing = false) {
138
138
  const buttons = tools.map(([act, glyph, label]) => `<button type="button" data-act="${act}" aria-label="${esc(label)}" title="${esc(label)}">${glyph}</button>`).join("");
139
139
  // The auto-turn: a toggle named for what it is, pressed while the view turns; its glyph and tooltip say what pressing it does.
140
140
  const play = `<button type="button" data-act="play" data-wide aria-pressed="${playing}" aria-keyshortcuts="Space" title="${esc(playing ? words.playOff : words.playOn)}"><span data-glyph aria-hidden="true">${playing ? "❚❚" : "▶"}</span>${esc(words.play)}</button>`;
141
- return `<div class="ky-turn-controls"><div class="ky-turn-group" role="group" aria-label="${esc(words.group)}">${presets}</div><div class="ky-turn-group">${buttons}<button type="button" data-act="reset" aria-keyshortcuts="0 Home">${esc(words.reset)}</button>${play}</div></div>`;
141
+ // The other way round: a toggle, pressed while the auto-turn goes clockwise (the default is how it has always turned, the near side swinging to the right).
142
+ const reverse = `<button type="button" data-act="reverse" aria-pressed="${direction === "clockwise"}" aria-label="${esc(words.reverseLabel)}" title="${esc(words.reverseLabel)}">${esc(words.reverse)}</button>`;
143
+ return `<div class="ky-turn-controls"><div class="ky-turn-group" role="group" aria-label="${esc(words.group)}">${presets}</div><div class="ky-turn-group">${buttons}<button type="button" data-act="reset" aria-keyshortcuts="0 Home">${esc(words.reset)}</button>${play}${reverse}</div></div>`;
142
144
  }
143
145
  /** The size, in pixels, the first view is worked out for. */
144
146
  export const sizeOf = (settings) => settings.size ?? { width: 960, height: 600 };
@@ -156,7 +158,7 @@ export function turnMarkupOf(model, settings = {}) {
156
158
  sheet +
157
159
  `<div class="ky-turn-view" tabindex="0" role="group" aria-roledescription="${esc(words.region)}" aria-label="${esc(model.title)}" aria-describedby="${esc(model.id)}-help" aria-keyshortcuts="ArrowLeft ArrowRight ArrowUp ArrowDown + - 0 1 2 3 4 Home Space"><div ${worldAttributes(model)}>${worldMarkup(model, layout)}</div>${model.board === undefined ? "" : turnBoardMarkup(model.board)}</div>` +
158
160
  `<p class="ky-turn-help" id="${esc(model.id)}-help">${esc(keys)}</p>` +
159
- (settings.controls === false ? "" : controlsMarkup(model, view)) +
161
+ (settings.controls === false ? "" : controlsMarkup(model, view, false, turnDirection(settings.direction))) +
160
162
  `<div class="ky-turn-live" role="status" aria-live="polite"></div>` +
161
163
  `</div>`);
162
164
  }
@@ -1,5 +1,5 @@
1
1
  import { KYOGIJO_STRINGS } from "./strings.ts";
2
- import { type PresetTable, type TurnPreset, type TurnView } from "./turn.camera.ts";
2
+ import { type PresetTable, type TurnDirection, type TurnPreset, type TurnView } from "./turn.camera.ts";
3
3
  import type { Plane } from "./turn.kit.ts";
4
4
  import { type UprightOptions, type Uprights } from "./uprights.ts";
5
5
  import { type ResolvedBoard } from "./overlay.ts";
@@ -46,6 +46,10 @@ export interface TurnSettings {
46
46
  autoPlay?: boolean | "force";
47
47
  /** Degrees of yaw a second the auto-turn goes by: 12 by default, positive turning the near side to the right as a drag to the right does, negative the other way. */
48
48
  speed?: number;
49
+ /** Which way the auto-turn goes: `"counterclockwise"` (the way it has always turned, the near side swinging to the viewer's right) by default, or `"clockwise"`. The Reverse button, `setDirection` and `update` change it; a negative `speed` reverses it too. */
50
+ direction?: TurnDirection;
51
+ /** Called with the direction when the Reverse button or `setDirection` changes it. */
52
+ onDirectionChange?: (direction: TurnDirection) => void;
49
53
  /** Called with `true` when the auto-turn starts and `false` when it stops, whatever stopped it. */
50
54
  onPlayChange?: (playing: boolean) => void;
51
55
  /** Called after the view has changed by a key, a button or the pointer, with the view and the viewpoint it is (`undefined` when it has been turned by hand). */
@@ -74,6 +74,24 @@ export const KYOGIJO_TURN_STYLE = `
74
74
  .ky-turn .u-cable{fill:none;stroke:var(--ky-up-roofline);stroke-width:1}
75
75
  .ky-turn .u-fabric{fill:var(--ky-up-roof);fill-opacity:.55;stroke:none}
76
76
  .ky-turn .u-pod{fill:var(--ky-up-fascia);stroke:var(--ky-up-roofline);stroke-width:.6}
77
+ /* A real venue's empty places, filled: the suite level between two bowls, the apron round the surface, the back of each bowl and the concourse floor behind it. */
78
+ .ky-turn .u-suite{fill:color-mix(in srgb,var(--ky-up-fascia) 78%,var(--ky-cr-bed))}
79
+ .ky-turn .u-suite-win{fill:color-mix(in srgb,var(--ky-cr-gap) 55%,#6f93b3)}
80
+ .ky-turn .u-suite-bar{stroke:var(--ky-up-fascia);stroke-width:.5}
81
+ .ky-turn .u-suite-ledge{fill:color-mix(in srgb,var(--ky-up-fascia) 60%,var(--ky-cr-conc))}
82
+ .ky-turn .u-apron{fill:color-mix(in srgb,var(--ky-cr-bed) 82%,#000)}
83
+ .ky-turn .u-timeb{fill:color-mix(in srgb,var(--ky-cr-conc) 70%,#3a3f46)}
84
+ .ky-turn .u-timeb-side{fill:color-mix(in srgb,var(--ky-cr-conc) 70%,#3a3f46);filter:brightness(.7)}
85
+ .ky-turn .u-penb{fill:color-mix(in srgb,var(--ky-up-glass,#bcd2e0) 80%,#5d7a8f)}
86
+ .ky-turn .u-penb-side{fill:color-mix(in srgb,var(--ky-up-glass,#bcd2e0) 80%,#5d7a8f);filter:brightness(.72)}
87
+ .ky-turn .u-teamb{fill:color-mix(in srgb,var(--ky-up-wood,#8a6a47) 80%,#2a2f36)}
88
+ .ky-turn .u-teamb-side{fill:color-mix(in srgb,var(--ky-up-wood,#8a6a47) 80%,#2a2f36);filter:brightness(.7)}
89
+ .ky-turn .u-back{fill:color-mix(in srgb,var(--ky-up-fascia) 66%,var(--ky-cr-bed))}
90
+ .ky-turn .u-backrib{stroke:var(--ky-cr-bed);stroke-width:.7;stroke-opacity:.7}
91
+ .ky-turn .u-coping{fill:color-mix(in srgb,var(--ky-up-fascia) 70%,var(--ky-cr-conc))}
92
+ .ky-turn .u-cfloor{fill:color-mix(in srgb,var(--ky-cr-bed) 60%,var(--ky-cr-conc))}
93
+ .ky-turn .u-clane{stroke:color-mix(in srgb,var(--ky-cr-conc) 55%,#fff);stroke-opacity:.6;stroke-width:1.1}
94
+ .ky-turn .kyogijo .ky-cr-shell:not(.ky-cr-ivy){fill:color-mix(in srgb,var(--ky-cr-bed) 70%,var(--ky-cr-conc));fill-opacity:.9}
77
95
  .ky-turn .ky-up-seats svg{opacity:var(--ky-cr-dim)}
78
96
  .ky-turn .ky-cr-seat{fill:none;stroke:var(--ky-cr-empty);stroke-width:1.3;stroke-opacity:.65}
79
97
  .ky-turn .ky-cr-h{stroke:var(--ky-cr-home)}
@@ -137,7 +155,7 @@ export const KYOGIJO_TURN_STYLE = `
137
155
  .ky-turn-group{display:flex;flex-wrap:wrap;gap:6px}
138
156
  .ky-turn-controls button{font:inherit;font-size:.9rem;font-weight:600;min-width:44px;min-height:44px;padding:0 12px;border-radius:999px;border:1px solid var(--ky-turn-rule);background:var(--ky-turn-chip);color:var(--ky-turn-ink);cursor:pointer;display:inline-flex;align-items:center;justify-content:center}
139
157
  .ky-turn-controls button[aria-pressed=true]{background:var(--ky-turn-chip-on);color:var(--ky-turn-chip-on-ink);border-color:var(--ky-turn-chip-on)}
140
- .ky-turn-controls button[data-act]:not([data-act=reset]):not([data-act=play]){font-size:1.2rem}
158
+ .ky-turn-controls button[data-act]:not([data-act=reset]):not([data-act=play]):not([data-act=reverse]){font-size:1.2rem}
141
159
  .ky-turn-controls button[data-act=play]{gap:.45em;padding:0 16px}
142
160
  .ky-turn-controls button[data-act=play] [data-glyph]{font-size:.8em;letter-spacing:-.1em}
143
161
  .ky-turn-controls button:focus-visible{outline:3px solid var(--ky-turn-ink);outline-offset:2px}
package/dist/venue.d.ts CHANGED
@@ -10,6 +10,8 @@ export interface VenueSettings {
10
10
  roofOpen?: boolean;
11
11
  /** The walls of a real venue's building (`crowd: { real: true }`), raised on its outline: they follow `roof` where left out (a roof and walls together, as before), and `walls: true` with `roof` off raises the walls alone, `roof: true` with `walls: false` the roof alone. */
12
12
  walls?: boolean;
13
+ /** Fill the empty places of the stands (the apron round the surface, the back of each bowl): on for a real venue's, off for the usual stands (where it is a tournament layout's hall that asks for it). */
14
+ fill?: boolean;
13
15
  /** How finely the curves are cut: `low` a facet to each corner, `auto` (the default) three, `high` six; an oval is 12, 28 or 56 facets round. */
14
16
  detail?: Cut;
15
17
  }
@@ -0,0 +1,40 @@
1
+ import { type CrowdStands, type Outline, type Pt, type StandRing } from "./crowd.ts";
2
+ import { type Plane } from "./turn.kit.ts";
3
+ import { type Cut } from "./venue.geom.ts";
4
+ /** The suite level's windows stand between these shares of the way across the facing wall (the bottom is a parapet, the top a ledge), and a pane is about this wide, in feet (ours). */
5
+ export declare const SUITE: {
6
+ readonly from: 0.3;
7
+ readonly to: 0.82;
8
+ readonly pane: 7;
9
+ };
10
+ /** The thin floor of the apron lies this far over the surface, in feet, so that it shows over the floor's own drawing. */
11
+ export declare const APRON_RISE = 0.06;
12
+ /** The bench side's furniture, in feet along the boards from the middle of the rink: the timekeeper's table, the penalty boxes either side of it, and each team's bench beyond (ours: the usual layout of a rink, no plan of a real one). */
13
+ export declare const BENCH_SIDE: {
14
+ readonly table: 6;
15
+ readonly penalty: readonly [8, 20];
16
+ readonly bench: readonly [24, 62];
17
+ readonly depth: 5;
18
+ readonly gap: 1.5;
19
+ readonly height: 3.4;
20
+ };
21
+ /** How much of a bowl's back wall is left where it is between the eye and the surface (the walls of the building take 0.02: the back of a bowl keeps a little of its body). */
22
+ export declare const BACK_FADE = 0.08;
23
+ /** A piece of the concourse floor smaller than this, in square feet, is a sliver a corner leaves (where the hall's rim meets the building's outline): it is left to the floor drawn under it, which has the same dark tone. */
24
+ export declare const SLIVER = 80;
25
+ /** The suite level seen square on: the facing wall of a quad `[inner a, inner b, outer b, outer a]` (the plane's own corners), with a band of windows and a pane to every `SUITE.pane` feet. */
26
+ export declare function suiteWall(corners: Pt[]): string;
27
+ /** The apron: the floor between the playing surface and the first row, a thin dark floor, and where the bowl is an ice rink's the benches, the penalty boxes and the timekeeper's table on the bench side. */
28
+ export declare function apronPlanes(stands: CrowdStands, first: StandRing, cut: Cut, lift: number): Plane[];
29
+ /** The plan of a ring's back: the outer edge of every strip of a tier that no walk leads on from, as the loop of points the wall is raised on. */
30
+ export declare const backEdges: (ring: StandRing, cut: Cut) => Pt[][];
31
+ /** The back wall of one bowl: from the floor up to the top row and a coping over it, on each stretch of the edge, painted with its ribs. It gives way to the eye as the walls of the building do, but keeps a little of itself. */
32
+ export declare function backWall(id: string, edge: readonly Pt[], top: number, lift: number, centre: readonly [number, number]): Plane[];
33
+ /**
34
+ * The concourse floor behind a bowl: flat quads, painted dark with two lanes, in rays from the middle: from the bowl's outer edge out to the rim of the hall (the nearer of the hall's rounded outline and the
35
+ * building's own), and from there on out to the building's outline, where the building has annexes beyond the hall (the walls and the roof stand on the hall only; the rest is floor too, not a bare sheet).
36
+ * The two rings are cut at the rim, so that a wall standing on it is drawn between them in the right order.
37
+ */
38
+ export declare function concourseFloor(id: string, edge: Outline, middle: Pt, rim: (angle: number) => Pt, far: (angle: number) => Pt | undefined, steps: number, lift: number): Plane[];
39
+ /** The rim of a real venue's hall along an angle from `middle`: the nearer of the hall's rounded outline and the building's own outline. */
40
+ export declare const rimAt: (hall: Outline, footprint: readonly Pt[], middle: Pt) => (angle: number) => Pt;
@@ -0,0 +1,174 @@
1
+ // What fills the empty places of a real venue's 3D view, so that no big flat sheet is left in it: the suite level between two bowls (a facing wall with a band of windows, not a bare shelf), the apron between the
2
+ // playing surface and the first row (a thin dark floor, and for ice hockey the players' benches, the penalty boxes and the timekeeper's table), the back of each bowl (a wall that closes it) and, between that wall and
3
+ // the walls of the hall, a darker concourse floor with its lanes. All of it is ours, plausible and no plan of a real building: where a source gives a figure for none of it, the sizes are the usual ones.
4
+ // Pure: data in, planes out.
5
+ import { rayAt } from "./crowd.js";
6
+ import { NHL } from "./geometry.js";
7
+ import { n, shy, wall, box } from "./turn.kit.js";
8
+ import { areaOf, pointList, raked, stripsOf, tidy } from "./venue.geom.js";
9
+ import { firstHit } from "./venue.real.js";
10
+ /** The suite level's windows stand between these shares of the way across the facing wall (the bottom is a parapet, the top a ledge), and a pane is about this wide, in feet (ours). */
11
+ export const SUITE = { from: 0.3, to: 0.82, pane: 7 };
12
+ /** The thin floor of the apron lies this far over the surface, in feet, so that it shows over the floor's own drawing. */
13
+ export const APRON_RISE = 0.06;
14
+ /** The bench side's furniture, in feet along the boards from the middle of the rink: the timekeeper's table, the penalty boxes either side of it, and each team's bench beyond (ours: the usual layout of a rink, no plan of a real one). */
15
+ export const BENCH_SIDE = { table: 6, penalty: [8, 20], bench: [24, 62], depth: 5, gap: 1.5, height: 3.4 };
16
+ /** How much of a bowl's back wall is left where it is between the eye and the surface (the walls of the building take 0.02: the back of a bowl keeps a little of its body). */
17
+ export const BACK_FADE = 0.08;
18
+ /** How high the coping stands over the top row on the back wall, in feet, and the walks of the concourse floor are this many feet from its two edges (ours). */
19
+ const COPING = 3;
20
+ /** The joints of the concourse floor: one about every this many feet along it, at most so many to a quad (ours). */
21
+ const JOINT = 22;
22
+ /** A piece of the concourse floor smaller than this, in square feet, is a sliver a corner leaves (where the hall's rim meets the building's outline): it is left to the floor drawn under it, which has the same dark tone. */
23
+ export const SLIVER = 80;
24
+ const JOINTS_MOST = 8;
25
+ const lerp = (a, b, t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
26
+ const flatAt = (z) => ({ x0: 0, y0: 0, z0: z, gx: 0, gy: 0 });
27
+ /** The suite level seen square on: the facing wall of a quad `[inner a, inner b, outer b, outer a]` (the plane's own corners), with a band of windows and a pane to every `SUITE.pane` feet. */
28
+ export function suiteWall(corners) {
29
+ const base = `<polygon class="u-suite" points="${pointList(corners)}"/>`;
30
+ if (corners.length !== 4)
31
+ return base;
32
+ const [a, b, c, d] = corners;
33
+ const across = (s, t) => lerp(lerp(a, b, s), lerp(d, c, s), t);
34
+ const panes = Math.max(1, Math.round(Math.hypot(b[0] - a[0], b[1] - a[1]) / SUITE.pane));
35
+ const band = `<polygon class="u-suite-win" points="${pointList([across(0, SUITE.from), across(1, SUITE.from), across(1, SUITE.to), across(0, SUITE.to)])}"/>`;
36
+ let bars = "";
37
+ for (let j = 1; j < panes; j += 1) {
38
+ const [p, q] = [across(j / panes, SUITE.from), across(j / panes, SUITE.to)];
39
+ bars += `M${n(p[0])} ${n(p[1])}L${n(q[0])} ${n(q[1])}`;
40
+ }
41
+ const ledge = `<polygon class="u-suite-ledge" points="${pointList([across(0, SUITE.to), across(1, SUITE.to), across(1, SUITE.to + 0.07), across(0, SUITE.to + 0.07)])}"/>`;
42
+ return `${base}${band}${bars === "" ? "" : `<path class="u-suite-bar" fill="none" d="${bars}"/>`}${ledge}`;
43
+ }
44
+ /** The floor strips of a ring of quads painted as a floor: a base and the walks along it, at the shares `lanes` of the way across. */
45
+ function floorOf(cls, lanes) {
46
+ return (corners) => {
47
+ const base = `<polygon class="${cls}" points="${pointList(corners)}"/>`;
48
+ if (corners.length === 3) {
49
+ // A triangle (where two rays meet at a corner of the building): its outline drawn again, smaller, round its middle.
50
+ const mid = [(corners[0][0] + corners[1][0] + corners[2][0]) / 3, (corners[0][1] + corners[1][1] + corners[2][1]) / 3];
51
+ const inner = lanes.map((t) => `M${corners.map((p) => `${n(mid[0] + (p[0] - mid[0]) * (1 - t))} ${n(mid[1] + (p[1] - mid[1]) * (1 - t))}`).join("L")}Z`);
52
+ return `${base}<path class="u-clane" fill="none" d="${inner.join("")}"/>`;
53
+ }
54
+ if (corners.length !== 4)
55
+ return base;
56
+ const [a, b, c, d] = corners;
57
+ const lines = lanes.map((t) => {
58
+ const [p, q] = [lerp(a, d, t), lerp(b, c, t)];
59
+ return `M${n(p[0])} ${n(p[1])}L${n(q[0])} ${n(q[1])}`;
60
+ });
61
+ // Joints across the floor, a paving's worth, so that a wide floor is not one flat sheet.
62
+ const joints = Math.min(JOINTS_MOST, Math.max(0, Math.round(Math.hypot(b[0] - a[0], b[1] - a[1]) / JOINT)));
63
+ for (let j = 1; j < joints; j += 1) {
64
+ const [p, q] = [lerp(a, b, j / joints), lerp(d, c, j / joints)];
65
+ lines.push(`M${n(p[0])} ${n(p[1])}L${n(q[0])} ${n(q[1])}`);
66
+ }
67
+ return `${base}<path class="u-clane" fill="none" d="${lines.join("")}"/>`;
68
+ };
69
+ }
70
+ /** The quads of a strip, as `[inner i, inner i + 1, outer i + 1, outer i]`, those with a width. */
71
+ function* quadsOf(strip) {
72
+ for (let i = 0; i + 1 < strip.inner.length; i += 1) {
73
+ const q = [strip.inner[i], strip.inner[i + 1], strip.outer[i + 1], strip.outer[i]];
74
+ if (Math.hypot(q[1][0] - q[0][0], q[1][1] - q[0][1]) < 1e-6 && Math.hypot(q[2][0] - q[3][0], q[2][1] - q[3][1]) < 1e-6)
75
+ continue;
76
+ yield q;
77
+ }
78
+ }
79
+ /** The direction along a quad's first edge (or its outer edge where the inner has shrunk to a point). */
80
+ const alongOf = (q) => {
81
+ const [from, to] = Math.hypot(q[1][0] - q[0][0], q[1][1] - q[0][1]) >= Math.hypot(q[2][0] - q[3][0], q[2][1] - q[3][1]) ? [q[0], q[1]] : [q[3], q[2]];
82
+ const long = Math.hypot(to[0] - from[0], to[1] - from[1]);
83
+ return long < 1e-9 ? [1, 0] : [(to[0] - from[0]) / long, (to[1] - from[1]) / long];
84
+ };
85
+ /** A ring of flat quads lying at `z`, between two outlines, painted as `paint` says, with the ids `${id}-${i}`. */
86
+ function floorRing(id, kind, strips, z, paint, least = 0.05) {
87
+ const planes = [];
88
+ let count = 0;
89
+ for (const strip of strips) {
90
+ for (const q of quadsOf(strip)) {
91
+ const poly = tidy(q);
92
+ if (poly.length < 3 || Math.abs(areaOf(poly)) < least)
93
+ continue;
94
+ count += 1;
95
+ const p = raked(`${id}-${count}`, kind, poly, flatAt(z), alongOf(q), (corners) => paint(corners));
96
+ if (p !== undefined)
97
+ planes.push(p);
98
+ }
99
+ }
100
+ return planes;
101
+ }
102
+ /** The apron: the floor between the playing surface and the first row, a thin dark floor, and where the bowl is an ice rink's the benches, the penalty boxes and the timekeeper's table on the bench side. */
103
+ export function apronPlanes(stands, first, cut, lift) {
104
+ const hole = first.hole;
105
+ if (hole === null || hole.kind !== "rrect")
106
+ return [];
107
+ const b = stands.box;
108
+ const rink = { kind: "rrect", x0: b.x, y0: b.y, x1: b.x + b.w, y1: b.y + b.h, r: stands.sport === "nhl" ? NHL.cornerRadius : 0 };
109
+ const planes = floorRing("apron", "apron", stripsOf(hole, rink, [], cut), lift + APRON_RISE, (corners) => `<polygon class="u-apron" points="${pointList(corners)}"/>`);
110
+ if (stands.sport !== "nhl")
111
+ return planes;
112
+ // The bench side is the one with the deeper apron (the NHL's first row stands further from that side); the furniture stands in the clear between the boards and the first row.
113
+ const bottom = hole.y1 - (b.y + b.h) >= b.y - hole.y0;
114
+ const depth = bottom ? hole.y1 - (b.y + b.h) : b.y - hole.y0;
115
+ const need = BENCH_SIDE.gap + BENCH_SIDE.depth;
116
+ if (depth < need + 1)
117
+ return planes;
118
+ const [y0, y1] = bottom ? [b.y + b.h + BENCH_SIDE.gap, b.y + b.h + need] : [b.y - need, b.y - BENCH_SIDE.gap];
119
+ const mid = b.x + b.w / 2;
120
+ const put = (id, from, to, cls) => box(id, "bench", { x0: mid + from, y0, x1: mid + to, y1 }, BENCH_SIDE.height, cls);
121
+ const furniture = [...put("timekeeper", -BENCH_SIDE.table, BENCH_SIDE.table, "u-timeb")];
122
+ for (const sign of [-1, 1]) {
123
+ const span = (lo, hi) => (sign < 0 ? [-hi, -lo] : [lo, hi]);
124
+ furniture.push(...put(`penalty-${sign < 0 ? "a" : "b"}`, ...span(BENCH_SIDE.penalty[0], BENCH_SIDE.penalty[1]), "u-penb"));
125
+ furniture.push(...put(`bench-${sign < 0 ? "a" : "b"}`, ...span(BENCH_SIDE.bench[0], BENCH_SIDE.bench[1]), "u-teamb"));
126
+ }
127
+ return [...planes, ...furniture];
128
+ }
129
+ /** The plan of a ring's back: the outer edge of every strip of a tier that no walk leads on from, as the loop of points the wall is raised on. */
130
+ export const backEdges = (ring, cut) => stripsOf(ring.region, ring.hole, ring.more ?? [], cut).map((strip) => strip.outer);
131
+ /** The back wall of one bowl: from the floor up to the top row and a coping over it, on each stretch of the edge, painted with its ribs. It gives way to the eye as the walls of the building do, but keeps a little of itself. */
132
+ export function backWall(id, edge, top, lift, centre) {
133
+ const planes = [];
134
+ for (let i = 0; i + 1 < edge.length; i += 1) {
135
+ const [a, b] = [edge[i], edge[i + 1]];
136
+ const long = Math.hypot(b[0] - a[0], b[1] - a[1]);
137
+ if (long < 0.5)
138
+ continue;
139
+ const high = lift + top;
140
+ const ribs = Math.max(1, Math.round(long / 14));
141
+ let lines = "";
142
+ for (let j = 1; j < ribs; j += 1)
143
+ lines += `M${n((long * j) / ribs)} ${n(COPING)}V${n(high)}`;
144
+ const svg = `<rect class="u-back" width="${n(long)}" height="${n(high)}"/>${lines === "" ? "" : `<path class="u-backrib" fill="none" d="${lines}"/>`}<rect class="u-coping" width="${n(long)}" height="${n(COPING)}"/>`;
145
+ planes.push({ ...shy(wall(`${id}-${i}`, "wall", a[0], a[1], b[0], b[1], 0, high, svg), centre), fadeTo: BACK_FADE });
146
+ }
147
+ return planes;
148
+ }
149
+ /**
150
+ * The concourse floor behind a bowl: flat quads, painted dark with two lanes, in rays from the middle: from the bowl's outer edge out to the rim of the hall (the nearer of the hall's rounded outline and the
151
+ * building's own), and from there on out to the building's outline, where the building has annexes beyond the hall (the walls and the roof stand on the hall only; the rest is floor too, not a bare sheet).
152
+ * The two rings are cut at the rim, so that a wall standing on it is drawn between them in the right order.
153
+ */
154
+ export function concourseFloor(id, edge, middle, rim, far, steps, lift) {
155
+ const dist = (p) => Math.hypot(p[0] - middle[0], p[1] - middle[1]);
156
+ const angles = Array.from({ length: steps + 1 }, (_, k) => (2 * Math.PI * k) / steps);
157
+ const inner = angles.map((a) => rayAt(edge, middle[0], middle[1], a));
158
+ const rims = angles.map((a, k) => {
159
+ const r = rim(a);
160
+ return dist(r) > dist(inner[k]) + 0.5 ? r : inner[k];
161
+ });
162
+ const outs = angles.map((a, k) => {
163
+ const f = far(a);
164
+ return f !== undefined && dist(f) > dist(rims[k]) + 0.5 ? f : rims[k];
165
+ });
166
+ const paint = floorOf("u-cfloor", [0.3, 0.62]);
167
+ return [...floorRing(`${id}-in`, "floor", [{ inner, outer: rims }], lift + APRON_RISE, paint, SLIVER), ...floorRing(`${id}-out`, "floor", [{ inner: rims, outer: outs }], lift + APRON_RISE, paint, SLIVER)];
168
+ }
169
+ /** The rim of a real venue's hall along an angle from `middle`: the nearer of the hall's rounded outline and the building's own outline. */
170
+ export const rimAt = (hall, footprint, middle) => (angle) => {
171
+ const cap = rayAt(hall, middle[0], middle[1], angle);
172
+ const out = firstHit(footprint, middle, angle);
173
+ return out !== undefined && Math.hypot(out[0] - middle[0], out[1] - middle[1]) < Math.hypot(cap[0] - middle[0], cap[1] - middle[1]) ? out : cap;
174
+ };
package/dist/venue.js CHANGED
@@ -7,7 +7,8 @@ import { patterns } from "./crowd.draw.js";
7
7
  import { crowdStands } from "./draw.js";
8
8
  import { groundById } from "./grounds.js";
9
9
  import { ARC_STEPS, clipAround, clipConvex, clipSegment, areaOf, pointList, raked, slopeOf, stripsOf, tidy } from "./venue.geom.js";
10
- import { realBuilding } from "./venue.real.js";
10
+ import { apronPlanes, backEdges, backWall, concourseFloor, rimAt, suiteWall } from "./venue.fill.js";
11
+ import { firstHit, realBuilding } from "./venue.real.js";
11
12
  import { n, shy, wall } from "./turn.kit.js";
12
13
  export const NO_VENUE = { items: [], defs: "", reach: 0 };
13
14
  /** The sports played under a roof: their stands have a lid and not a canopy. */
@@ -51,6 +52,7 @@ export function standsVenueOf(stands, crowd, o, v, lift) {
51
52
  const covers = stands.covers;
52
53
  const inCover = (p) => covers.some(([x0, y0, x1, y1]) => p[0] >= x0 && p[0] <= x1 && p[1] >= y0 && p[1] <= y1);
53
54
  const pieces = (poly) => (covers.length === 0 ? [poly] : clipAround(poly, covers));
55
+ const filled = v.fill ?? stands.real !== undefined;
54
56
  const seatPlanes = [];
55
57
  const wallSeats = [];
56
58
  let count = 0;
@@ -138,12 +140,31 @@ export function standsVenueOf(stands, crowd, o, v, lift) {
138
140
  continue;
139
141
  for (const piece of pieces(tidy(q))) {
140
142
  count += 1;
141
- const p = raked(`walk-${count}`, "concourse", piece, slope.slope, slope.along, (corners) => `<polygon class="u-conc" points="${pointList(corners)}"/>`);
143
+ // Where the empty places are filled, the walk between two tiers is the suite level: a facing wall with a band of windows, not a bare shelf.
144
+ const p = raked(`walk-${count}`, "concourse", piece, slope.slope, slope.along, (corners) => (filled ? suiteWall(corners) : `<polygon class="u-conc" points="${pointList(corners)}"/>`));
142
145
  if (p !== undefined)
143
146
  seatPlanes.push({ ...p, shy: slope.out });
144
147
  }
145
148
  }
146
149
  }
150
+ // The empty places are filled (see `venue.fill.ts`): the apron round the surface and the back of each bowl, and, for a real venue's building, the concourse floor behind the bowl.
151
+ if (filled) {
152
+ const first = stands.rings.find((r) => r.tier !== "wall" && r.tier !== "bleachers") ?? stands.rings[0];
153
+ if (first !== undefined)
154
+ seatPlanes.push(...apronPlanes(stands, first, cut, lift));
155
+ const real = stands.real;
156
+ const middle = [centre[0], centre[1]];
157
+ const behind = new Set(links.map((l) => l.before));
158
+ for (const ring of stands.rings) {
159
+ if (ring.tier === "wall" || behind.has(ring))
160
+ continue;
161
+ backEdges(ring, cut).forEach((edge, k) => {
162
+ seatPlanes.push(...backWall(`back-${ring.id}${k === 0 ? "" : `-s${k}`}`, edge, ring.height[1], lift, centre));
163
+ if (real !== undefined && ring.region.kind !== "poly" && k === 0)
164
+ seatPlanes.push(...concourseFloor(`floor-${ring.id}`, ring.region, middle, rimAt(real.hall, real.footprint, middle), (a) => firstHit(real.footprint, middle, a), ARC_STEPS[cut].oval, lift));
165
+ });
166
+ }
167
+ }
147
168
  const items = [];
148
169
  if (seatPlanes.length > 0)
149
170
  items.push({ id: "stands", kind: "stands", at: centre, height: topOf(seatPlanes), planes: seatPlanes });
@@ -7,6 +7,8 @@ export declare const SADDLE_RISE = 30;
7
7
  export declare function saddleAt(real: Pick<RealShell, "footprint">, [x, y]: Pt): number;
8
8
  /** The height of the roof over a point of the building's edge, above the surface, in feet. */
9
9
  export declare const roofAt: (real: RealShell, p: Pt) => number;
10
+ /** The point where a ray from `from` first meets a polygon, or `undefined`. */
11
+ export declare function firstHit(poly: readonly Pt[], from: Pt, angle: number): Pt | undefined;
10
12
  /**
11
13
  * The walls and the roof of a real venue (`parts` says which are made). The hall is the building's outline cut to the bowl's wall and a margin (`RealShell.hall`): its rim is where a ray from the middle meets the one or the other first, in `steps`
12
14
  * rays. A wall stands on each stretch of the rim, up to the roof's height over it; the roof is a band from the rim in to the inner edge of the top tier (the middle stays open, as the 3D view's roofs are).
@@ -21,7 +21,7 @@ export function saddleAt(real, [x, y]) {
21
21
  /** The height of the roof over a point of the building's edge, above the surface, in feet. */
22
22
  export const roofAt = (real, p) => real.edge + (real.roof === "saddle" ? SADDLE_RISE * saddleAt(real, p) : 0);
23
23
  /** The point where a ray from `from` first meets a polygon, or `undefined`. */
24
- function firstHit(poly, from, angle) {
24
+ export function firstHit(poly, from, angle) {
25
25
  const [dx, dy] = [Math.cos(angle), Math.sin(angle)];
26
26
  let best;
27
27
  poly.forEach(([ax, ay], i) => {
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.34.0";
2
+ export declare const VERSION = "0.35.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.34.0";
2
+ export const VERSION = "0.35.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnmorrisdotca/kyogijo-stadiums",
3
- "version": "0.34.0",
3
+ "version": "0.35.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",