@johnmorrisdotca/kyogijo-stadiums 0.27.0 → 0.29.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,29 @@ All notable changes to this project are written here. The format follows
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.29.0] - 2026-10-20
11
+
12
+ ### Added
13
+
14
+ - **The real venues of the Central Division**: the United Center, the Ball Arena, the American Airlines Center, the Grand Casino Arena, the Bridgestone Arena, the Enterprise Center, the Delta Center and the Canada Life Centre, drawn the way the Pacific Division's were: the outline of the building from OpenStreetMap (read on 2026-10-20), a plausible bowl of the seats, tiers and roof the sources give, each fact labelled `documented` or `plausible`. With the Pacific Division that is sixteen arenas, the Western Conference's. The Grand Casino Arena is the first of three seating tiers (its article counts four levels, one of them suites) and its building is a relation of two ways in OpenStreetMap; the Canada Life Centre's lower bowl has the 8,812 of its 15,225 seats its article gives. The rink's orientation is inferred from the main hall for twelve of the sixteen and not known for four (the Ball Arena is the new one); none is mapped. A Wikidata capacity for ice hockey that changed over the years (the Grand Casino Arena's 18,064 from 2000 and 17,954 from 2012) is read as the latest. The Eastern Conference follows.
15
+ - **Divisions and conferences in the data and the API**: every venue has a `division`, `VENUE_DIVISIONS` and `VENUE_CONFERENCES` give the league's order, and `venuesIn`, `venuesInConference` and `conferenceOf` list and place them. The raw reads are kept per division (`data-sources/venues/<date>/wikidata-<division>.json`, `wikipedia-<division>.json`, which now also keeps the infobox's capacity line, and `osm-<arena>.json`; `pnpm venues:fetch <date> <division>`), and `pnpm venues` also writes the table of `NOTICE.md`.
16
+ - **The 3D view's walls and roof apart**: `venue: { walls, roof }` (`walls` follows `roof` when it is left out, so the old `roof: true` is unchanged), and `turn.update(options, { venue })` changes them on a view that is mounted, which goes on turning from where it is.
17
+ - **The demo's venues page by division**, with a conference and division chooser, and **the controls on the view**: the stands, walls, roof, building, markings and labels are buttons beside the view (they never stop or restart the turn: the first three draw the scene again through `update`, the others are a style), flat or 3D, the view's own viewpoints, turn, tilt, zoom and Play under it (the 3D view turns by itself when it opens), a compact scrolling bar on a phone, and the choice in the address (`?stands=0&walls=1&roof=1&building=0&markings=0&labels=0&viewpoint=end&spin=off`; an old `roof=1` still means walls and roof).
18
+ - **The demo's home page leads with a real arena that turns by itself** (Climate Pledge Arena, any of the sixteen from a list), with the same buttons on it, in the address under `f` (`fv`, `fstands`, `froof`...), and links to the venues page, the tournament layouts in 3D and the judging page's broadcast view.
19
+ - **Two moving pictures at the head of the README** (a real arena and a tournament layout turning once round, animated WebP in a light and a dark file, 0.5 to 0.8 MB each) and a "Look at these first" table with one picture of the real venues, the tournament layouts, the judging broadcast view and the combat sports; the other pictures of those sections are on their docs pages. `pnpm screenshots:animation` makes the moving pictures from the demo (`scripts/readme-animation.mjs`, frames from Playwright and the few lines of RIFF in `scripts/webp-animated.mjs`, no other tool). The README lint (`scripts/readme-lint.mjs`) gives a `turning-*` picture a budget of its own and counts a picture a docs page shows as used.
20
+
21
+ ### Changed
22
+
23
+ - The fly-through demo page's arena tabs (now the sixteen venues) are one row that scrolls on a phone, so that the view stays in sight.
24
+
25
+ ## [0.28.0] - 2026-10-20
26
+
27
+ ### Added
28
+
29
+ - **The fly-through goes into the bowl** (`bowl` and `rink`, two stages after `arena`): the roof lifts away, and the camera comes in over the stands, tipping lower and nearer and through a shorter lens, to a seat's viewpoint inside the bowl, from where the rink turns. The 3D arena's own `lens` setting does it (a long lens from outside, about 1 from the seat, so that the eye is inside the building and the bowl has a real perspective), and the roof is dissolved with a CSS `filter: opacity()`, so that it composes with the 3D view's own cutting away of the walls. The seat is a place and a height of our own choosing (a pitch, a zoom and a lens, `seat` of `flightPlan`), said to be so, never a real seat; the bowl's words give the venue's seats and tiers.
30
+ - **The camera has two more channels** (`logLens`, `roof`), still one path with no jumps: the turn slows from the arena's 12 degrees a second to the rink's 8 and never stops or reverses; the lens, the roof, the zoom and the tip change in one direction only on the way in. `flightPlan` takes `seat` and `rinkTurn`; `PACE` has the seconds of each part; the path now ends on the rink turning.
31
+ - Tests for the way in (nothing reverses, the seat is reached where the rink stage starts, the stage stills show the roof lifted and the seat's view) over fourteen lists of stages, and in a real browser the four corners of the rink are followed on the screen at 30 frames a second, from outside and from the seat: each moves a little a frame and none falls behind the eye.
32
+
10
33
  ## [0.27.0] - 2026-10-20
11
34
 
12
35
  ### Added
package/NOTICE.md CHANGED
@@ -43,7 +43,7 @@ picture or code of the sources is copied; each source is named here so that the
43
43
  | `grounds`: the World Cups each ground staged matches of, and where each final was played | The venue section of the English Wikipedia article of each men's and Women's World Cup, at the revision cited below; FIFA's own pages are not used | Facts, CC BY-SA 4.0 articles as above |
44
44
  | `grounds`: the Wikidata item and the coordinates of each ground (`wikidata`, `lat`, `lon`) | The item that each cited article is linked to, its coordinates (P625) and its country (P17), read with the Wikidata API and checked against the item's labels by `scripts/check-grounds.mjs` | Wikidata's data is CC0 (https://creativecommons.org/publicdomain/zero/1.0/) |
45
45
  | `arenas`: each arena's names, years, city, seats, rink sizes, Wikidata item and coordinates | The infobox and the text of the English Wikipedia article of each arena, at the revision cited below (the names with the years the article dates them, the year it opened, closed and was pulled down, the seats for ice hockey and for basketball, a rink's length and width where the article gives them in a sentence of its own); the name in Japanese from the title of its Japanese article or written for the package; the Wikidata item (CC0) that each article is linked to and its coordinates (P625) | Facts. The articles are CC BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/); none of their text, tables or images is used |
46
- | `venues`: the outline of each NHL arena's building, how its rink lies in it and the building's height (`VENUE_OSM`, `src/venues.osm.data.ts`) | OpenStreetMap, through the Overpass API: the closed way tagged with the venue's Wikidata item (and the ice, where it is mapped), read on 2026-10-20, with the raw answers kept in `data-sources/venues/2026-10-20/`; the outline is simplified to a metre, and the way the rink lies is worked out from it (`coreOf`, `src/venues.geo.ts`) | ODbL 1.0, (c) OpenStreetMap contributors (https://www.openstreetmap.org/copyright): a derived database, kept in its own file and offered under the ODbL; a drawing made from it is a produced work and carries the credit (`VENUE_ATTRIBUTION`) |
46
+ | `venues`: the outline of each NHL arena's building, how its rink lies in it and the building's height (`VENUE_OSM`, `src/venues.osm.data.ts`) | OpenStreetMap, through the Overpass API: the closed way (or the relation of a building drawn in two ways) tagged with the venue's Wikidata item (and the ice, where it is mapped), read on 2026-10-20, with the raw answers kept in `data-sources/venues/2026-10-20/`; the outline is simplified to a metre, and the way the rink lies is worked out from it (`coreOf`, `src/venues.geo.ts`) | ODbL 1.0, (c) OpenStreetMap contributors (https://www.openstreetmap.org/copyright): a derived database, kept in its own file and offered under the ODbL; a drawing made from it is a produced work and carries the credit (`VENUE_ATTRIBUTION`) |
47
47
  | `venues`: each venue's seats for ice hockey, number of seating tiers and kind of roof (`VENUE_DATA`, `src/venues.data.ts`) | The item of Wikidata (the capacity P1083 and the sport it applies to P641) and the English Wikipedia article of each arena, at the revision cited below, in `data-sources/venues/2026-10-20/` as read; the facts are numbers and words of our own, with what was read in a few words | Wikidata's data is CC0; the facts read from the articles are facts, with the articles credited here (CC BY-SA 4.0) |
48
48
  | `stadiums`: the outline of each stadium's building, the roof OpenStreetMap maps over it (its heights and the opening in it) and how the field lies, worked out from the opening (`STADIUM_OSM`, `src/stadiums.osm.data.ts`) | OpenStreetMap, through the Overpass API: the closed way tagged with the stadium's Wikidata item and the multipolygon of its roof, read on 2026-10-20, with the raw answers kept in `data-sources/stadiums/2026-10-20/`; the outline is simplified to a metre, and the way the field lies is worked out from the opening (`smallestBox`, `src/venues.geo.ts`) | ODbL 1.0, (c) OpenStreetMap contributors (https://www.openstreetmap.org/copyright): a derived database, kept in its own file and offered under the ODbL; a drawing made from it is a produced work and carries the credit (`VENUE_ATTRIBUTION`) |
49
49
  | `stadiums`: each stadium's seats for each configuration, number of seating bowls, kind of roof and the size of its opening (`STADIUM_DATA`, `src/stadiums.data.ts`) | The item of Wikidata (the capacity P1083) and the English Wikipedia article of each stadium, at the revision cited below, in `data-sources/stadiums/2026-10-20/` as read; the facts are numbers and words of our own, with what was read in a few words | Wikidata's data is CC0; the facts read from the article are facts, with the article credited here (CC BY-SA 4.0) |
@@ -411,9 +411,10 @@ The Japanese Wikipedia articles read for the names, with their revisions, are li
411
411
 
412
412
  ## The venues' sources
413
413
 
414
- The eight arenas of the NHL's Pacific Division, read on 2026-10-20 (`pnpm venues:fetch`; the files are in `data-sources/venues/2026-10-20/`: `osm-<arena>.json` as Overpass returned them, `wikidata.json` and `wikipedia.json`). The outline of a building is OpenStreetMap's, ODbL 1.0, (c) OpenStreetMap contributors, and lives in `src/venues.osm.data.ts` alone. The seats, the tiers and the roof are in `src/venues.data.ts`, with what each cites. No seat map of a team, a ticket seller or a venue is read or copied, and no mark of a team or a league is drawn.
414
+ The arenas of the NHL's Pacific and Central Divisions, read on 2026-10-20 (`pnpm venues:fetch <date> <division>`; the files are in `data-sources/venues/2026-10-20/`: `osm-<arena>.json` as Overpass returned them, and `wikidata-<division>.json` and `wikipedia-<division>.json`). The outline of a building is OpenStreetMap's, ODbL 1.0, (c) OpenStreetMap contributors, and lives in `src/venues.osm.data.ts` alone. The seats, the tiers and the roof are in `src/venues.data.ts`, with what each cites. No seat map of a team, a ticket seller or a venue is read or copied, and no mark of a team or a league is drawn.
415
415
 
416
- | Arena | Wikidata item | OpenStreetMap way | English article and revision |
416
+ <!-- venues-notice -->
417
+ | Arena | Wikidata item | OpenStreetMap element | English article and revision |
417
418
  | --- | --- | --- | --- |
418
419
  | Honda Center | Q774138 | way/120623151 | Honda Center, 1380000927 |
419
420
  | Scotiabank Saddledome | Q1140083 | way/4397328 | Scotiabank Saddledome, 1377341466 |
@@ -423,6 +424,15 @@ The eight arenas of the NHL's Pacific Division, read on 2026-10-20 (`pnpm venues
423
424
  | Climate Pledge Arena | Q977529 | way/25123756 | Climate Pledge Arena, 1380851727 |
424
425
  | Rogers Arena | Q517318 | way/24705911 | Rogers Arena, 1379918849 |
425
426
  | T-Mobile Arena | Q3338973 | way/364489406 | T-Mobile Arena, 1379155307 |
427
+ | United Center | Q639975 | way/205221993 | United Center, 1380672233 |
428
+ | Ball Arena | Q795470 | way/25312645 | Ball Arena, 1379560431 |
429
+ | American Airlines Center | Q463345 | way/25312698 | American Airlines Center, 1379833787 |
430
+ | Grand Casino Arena | Q1421388 | relation/20658401 | Grand Casino Arena, 1379407082 |
431
+ | Bridgestone Arena | Q302869 | way/130905914 | Bridgestone Arena, 1380457078 |
432
+ | Enterprise Center | Q1067700 | way/42369176 | Enterprise Center, 1372086380 |
433
+ | Delta Center | Q82294 | way/23335190 | Delta Center, 1379043310 |
434
+ | Canada Life Centre | Q656928 | way/14869890 | Canada Life Centre, 1379717047 |
435
+ <!-- /venues-notice -->
426
436
 
427
437
  ## The stadiums' sources
428
438
 
package/README.md CHANGED
@@ -13,6 +13,25 @@ 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
+
16
35
  > **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).
17
36
 
18
37
  <table align="center">
@@ -36,6 +55,43 @@ The surfaces of forty-three sports, from football and curling to the budō and t
36
55
 
37
56
  Kyogijo Stadiums draws the places a game is played on, with the score of the game on them: the teams at their ends, the score in the middle, a label for what it was. The surfaces are drawn to scale from the rulebooks: a football field has its hash marks where the rules put them, a ballpark its fence where its published distances put it.
38
57
 
58
+ ## Look at these first
59
+
60
+ <table align="center">
61
+ <tr>
62
+ <td align="center" valign="top">
63
+ <picture>
64
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/venues-saddledome-3d-desk-dark.webp">
65
+ <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/venues-saddledome-3d-desk-light.webp" alt="The Scotiabank Saddledome in 3D: two tiers of seats under its saddle roof." width="400">
66
+ </picture>
67
+ <br><strong><a href="./docs/venues.md">Real arenas</a></strong>: the NHL's Western Conference, each in the outline of its building with a plausible bowl of the seats, tiers and roof the sources give; 2D and 3D, with the stands, walls and roof to switch.
68
+ </td>
69
+ <td align="center" valign="top">
70
+ <picture>
71
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/multi-tabletennis-3d-desk-dark.webp">
72
+ <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/multi-tabletennis-3d-desk-light.webp" alt="Four table tennis areas on one floor in 3D, with a hung scoreboard." width="400">
73
+ </picture>
74
+ <br><strong><a href="./docs/multi.md">Tournament layouts</a></strong>: judo, table tennis and other areas on one floor, to scale, in a hall or a stadium, flat or in 3D.
75
+ </td>
76
+ </tr>
77
+ <tr>
78
+ <td align="center" valign="top">
79
+ <picture>
80
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/judging-broadcast-3d-desk-dark.webp">
81
+ <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/judging-broadcast-3d-desk-light.webp" alt="A kata mat in 3D from the broadcast camera, with a board at its side listing each judge." width="400">
82
+ </picture>
83
+ <br><strong><a href="./docs/judging.md">Who judges where</a></strong>: each judo mat's officials by seat, a broadcast view with the officials on a board, and a page to print.
84
+ </td>
85
+ <td align="center" valign="top">
86
+ <picture>
87
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/mma-cage-3d-desk-dark.webp">
88
+ <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/mma-cage-3d-desk-light.webp" alt="An MMA cage in 3D: a mesh fence on a solid platform." width="400">
89
+ </picture>
90
+ <br><strong><a href="./docs/combat.md">The combat sports</a></strong>: an MMA cage, boxing and wrestling rings and mats, flat and in 3D.
91
+ </td>
92
+ </tr>
93
+ </table>
94
+
39
95
  ## In 30 seconds
40
96
 
41
97
  ```sh
@@ -505,38 +561,6 @@ console.log(layout.needs.length, layout.needs.width);
505
561
  // 47 19
506
562
  ```
507
563
 
508
- <p align="center">
509
- <picture>
510
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/multi-judo-desk-dark.webp">
511
- <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/multi-judo-desk-light.webp" alt="Three judo contest areas in a row on one red floor of mats, dressed with a sample event's name, simple emblems and boards." width="640">
512
- </picture>
513
- <br><em>Three judo contest areas on one floor of mats: 47 by 19 metres, with sample content.</em>
514
- </p>
515
-
516
- <p align="center">
517
- <picture>
518
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/multi-tabletennis-desk-dark.webp">
519
- <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/multi-tabletennis-desk-light.webp" alt="Four table tennis areas in two rows of two on one blue floor, with sample words and emblems, a hung scoreboard and boards." width="640">
520
- </picture>
521
- <br><em>Four table tennis areas under one scoreboard.</em>
522
- </p>
523
-
524
- <p align="center">
525
- <picture>
526
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/multi-tabletennis-3d-desk-dark.webp">
527
- <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/multi-tabletennis-3d-desk-light.webp" alt="Four table tennis areas in 3D: tables on legs, a surround round each, a hung scoreboard." width="640">
528
- </picture>
529
- <br><em>The same four in 3D.</em>
530
- </p>
531
-
532
- <p align="center">
533
- <picture>
534
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/multi-judo-empty-desk-dark.webp">
535
- <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/multi-judo-empty-desk-light.webp" alt="The three judo areas with the places for the event's words and marks left empty and dashed." width="640">
536
- </picture>
537
- <br><em>Without `sample`: the empty places an organiser fills.</em>
538
- </p>
539
-
540
564
  ### Boards over a surface
541
565
 
542
566
  `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)).
@@ -567,23 +591,16 @@ console.log(layout.needs.length, layout.needs.width);
567
591
 
568
592
  ### The real venues
569
593
 
570
- `crowd: { real: true }` draws an NHL arena in its building ([the real venues](./docs/venues.md)).
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. Sixteen arenas so far, the Western Conference's (Pacific and Central Divisions); the Eastern Conference follows ([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.
571
595
 
572
- <p align="center">
573
- <picture>
574
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/venues-honda-desk-dark.webp">
575
- <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/venues-honda-desk-light.webp" alt="The Honda Center's rink in its building's outline: two tiers of seats and a north arrow." width="640">
576
- </picture>
577
- <br><em>The Honda Center.</em>
578
- </p>
596
+ ```ts
597
+ import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
598
+ import { planVenue, venueById } from "@johnmorrisdotca/kyogijo-stadiums/venues";
579
599
 
580
- <p align="center">
581
- <picture>
582
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/venues-saddledome-3d-desk-dark.webp">
583
- <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/venues-saddledome-3d-desk-light.webp" alt="The Saddledome in 3D: two tiers of seats under its roof." width="640">
584
- </picture>
585
- <br><em>The Saddledome, in 3D.</em>
586
- </p>
600
+ const svg = drawKyogijo({ sport: "nhl", arena: "climate-pledge-arena", crowd: { real: true } });
601
+ console.log(svg.includes("ky-cr-shell"), planVenue(venueById("climate-pledge-arena")!).total);
602
+ // true 17151
603
+ ```
587
604
 
588
605
  ### A real stadium, and a district
589
606
 
@@ -609,32 +626,8 @@ console.log(layout.needs.length, layout.needs.width);
609
626
 
610
627
  `drawJudging` puts each judo mat's officials by seat in the picture and in a chart ([judging](./docs/judging.md)).
611
628
 
612
- <p align="center">
613
- <picture>
614
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/judging-3d-desk-dark.webp">
615
- <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/judging-3d-desk-light.webp" alt="A kata area's judges' table in 3D, numbered badges, and the chart." width="640">
616
- </picture>
617
- <br><em>Judges, in 3D.</em>
618
- </p>
619
-
620
629
  The broadcast view puts the mat large, with the officials on a board at its side; `judgingPrint` prints one mat to an A4 or US letter page.
621
630
 
622
- <p align="center">
623
- <picture>
624
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/judging-broadcast-3d-desk-dark.webp">
625
- <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/judging-broadcast-3d-desk-light.webp" alt="A kata mat in 3D from the Broadcast camera, with a dark board at its side listing each judge's number, name and ID." width="640">
626
- </picture>
627
- <br><em>The broadcast view.</em>
628
- </p>
629
-
630
- <p align="center">
631
- <picture>
632
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/judging-print-a4-desk-dark.webp">
633
- <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/judging-print-a4-desk-light.webp" alt="An A4 page with the plan of one kata mat and the chart of its five judges under it." width="640">
634
- </picture>
635
- <br><em>One mat to an A4 page.</em>
636
- </p>
637
-
638
631
  ### A table tennis area
639
632
 
640
633
  A table in the ITTF's playing area, 14 m by 7 m, with surrounds and chairs ([courts](./docs/courts.md#the-table-tennis-playing-area)).
@@ -679,46 +672,6 @@ A table in the ITTF's playing area, 14 m by 7 m, with surrounds and chairs ([cou
679
672
 
680
673
  An MMA cage, rings and wrestling mats ([the combat sports page](./docs/combat.md)).
681
674
 
682
- <p align="center">
683
- <picture>
684
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/mma-cage-desk-dark.webp">
685
- <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/mma-cage-desk-light.webp" alt="An MMA cage from above with a score" width="660">
686
- </picture>
687
- <br><em>With a score.</em>
688
- </p>
689
-
690
- <p align="center">
691
- <picture>
692
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/mma-cage-3d-desk-dark.webp">
693
- <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/mma-cage-3d-desk-light.webp" alt="The cage in 3D: a mesh fence on a solid platform" width="660">
694
- </picture>
695
- <br><em>The cage in 3D.</em>
696
- </p>
697
-
698
- <p align="center">
699
- <picture>
700
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/boxing-ring-3d-desk-dark.webp">
701
- <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/boxing-ring-3d-desk-light.webp" alt="The boxing ring in 3D: coloured corner posts, ropes and steps" width="660">
702
- </picture>
703
- <br><em>The ring in 3D.</em>
704
- </p>
705
-
706
- <p align="center">
707
- <picture>
708
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/wrestling-mat-3d-desk-dark.webp">
709
- <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/wrestling-mat-3d-desk-light.webp" alt="The UWW's wrestling mat in 3D on its platform" width="660">
710
- </picture>
711
- <br><em>The UWW's mat in 3D.</em>
712
- </p>
713
-
714
- <p align="center">
715
- <picture>
716
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/prowrestling-ring-3d-desk-dark.webp">
717
- <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/prowrestling-ring-3d-desk-light.webp" alt="A professional wrestling ring in 3D with three ropes" width="660">
718
- </picture>
719
- <br><em>A wrestling ring in 3D.</em>
720
- </p>
721
-
722
675
  ### The element in a page
723
676
 
724
677
  ```html
@@ -815,7 +768,7 @@ English and Japanese: the title, the description and the statuses of a drawing,
815
768
  Not here yet, each welcome as an [issue](https://github.com/johnmorrisdotca/kyogijo-stadiums/issues):
816
769
 
817
770
  - **More surfaces**: the FIBA and college courts, the international rink, rugby pitches, soccer's beach and street formats.
818
- - **More real venues**: the other divisions' arenas, then the NBA's, NFL's and MLB's, and more districts.
771
+ - **More real venues**: the Eastern Conference's arenas, then the NBA's, NFL's and MLB's, and more districts.
819
772
  - **More parks of the past**: the 19th-century grounds, and the parks whose articles give no distances yet.
820
773
  - **More charts on data layers**: a formation chart, a pass sonar chart and a Voronoi chart of their own (the layers exist; see [team shape](./docs/team-shape.md)).
821
774
  - **Football in a different standard**: the college and high-school hash marks.
@@ -6,6 +6,7 @@ import { arenaById, arenaNameIn } from "./arenas.js";
6
6
  import { STAGE_IDS, cameraAt, coverSide, flightPlan, foldTime, glide, glideSeconds, lengthWords, mainView, mapBox, mapFrame, nextRate, niceLength, stageAt, startOf, stagesOf, } from "./flythrough.path.js";
7
7
  import { flythroughMarkup, graticuleMarkup, gridPath, mapMarkup, stageWords } from "./flythrough.markup.js";
8
8
  import { FLYTHROUGH_WORDS, say } from "./flythrough.words.js";
9
+ import { LENS } from "./turn.camera.js";
9
10
  import { mountKyogijoTurn } from "./turn.dom.js";
10
11
  import { arenaOfVenue, planVenue, venueById, venueOrientation, venueRink } from "./venues.js";
11
12
  /**
@@ -43,7 +44,7 @@ export function mountFlythrough(host, options, settings = {}) {
43
44
  }
44
45
  })();
45
46
  const place = `${arena.city}, ${region}`;
46
- const values = { arena: name, place };
47
+ const values = { arena: name, place, seats: venue.seats.hockey.toLocaleString(language), tiers: venue.tiers.count };
47
48
  const wanted = (options.stages ?? STAGE_IDS).filter((stage, i, all) => STAGE_IDS.includes(stage) && all.indexOf(stage) === i);
48
49
  const stageList = STAGE_IDS.filter((stage) => wanted.includes(stage));
49
50
  if (stageList.length === 0)
@@ -68,7 +69,7 @@ export function mountFlythrough(host, options, settings = {}) {
68
69
  frame = { at: [0, 0], metresX: 1, metresY: 1 };
69
70
  }
70
71
  const mapFrameNow = frame;
71
- host.innerHTML = flythroughMarkup({ id, language, theme: options.theme, arena: name, place, stages: stageList, map: mapSvg, mapCredit: chizu, controls: settings.controls });
72
+ host.innerHTML = flythroughMarkup({ id, language, theme: options.theme, arena: name, place, stages: stageList, map: mapSvg, mapCredit: chizu, controls: settings.controls, seats: values.seats, tiers: values.tiers });
72
73
  const root = host.querySelector(".kf");
73
74
  const stage = root.querySelector(".kf-stage");
74
75
  const mapLayer = root.querySelector(".kf-map");
@@ -89,8 +90,10 @@ export function mountFlythrough(host, options, settings = {}) {
89
90
  // ---- The 3D arena, from above to begin with: the view the path hands over at. ----
90
91
  const drawing = { sport: "nhl", arena: venue.id, crowd: { real: true }, language, ...(options.theme === undefined ? {} : { theme: options.theme }) };
91
92
  let turn;
92
- if (stageList.includes("arena"))
93
- turn = mountKyogijoTurn(turnHost, drawing, { preset: "overhead", controls: false, venue: { roof: true }, motion: "none", id: `${id}-turn` });
93
+ // The settings object is kept: the view reads its `lens` at every paint, and the path shortens the lens as the camera comes into the bowl.
94
+ const turnSettings = { preset: "overhead", controls: false, venue: { roof: true }, motion: "none", id: `${id}-turn`, lens: LENS };
95
+ if (stageList.some((one) => one === "arena" || one === "bowl" || one === "rink"))
96
+ turn = mountKyogijoTurn(turnHost, drawing, turnSettings);
94
97
  // ---- The site plan: the building's outline and the rink, in the drawing's own feet; the sheet under it is the floor's. ----
95
98
  const plan = planVenue(venue);
96
99
  const rink = venueRink(venue);
@@ -126,6 +129,9 @@ export function mountFlythrough(host, options, settings = {}) {
126
129
  if (turn !== undefined && floor !== null) {
127
130
  // Measure the arena seen from straight above, as the 3D view lays it out in this container: where the rink stands and how big a foot is.
128
131
  const was = turn.view();
132
+ const lens = turnSettings.lens;
133
+ // The hand-over is seen from straight above through the usual long lens, whatever the lens is now.
134
+ turnSettings.lens = LENS;
129
135
  turn.set({ yaw: 0, pitch: 0, zoom: 1 }, false);
130
136
  const box = floor.getBoundingClientRect();
131
137
  // The stage's own area, inside its border: the layers are laid out in it.
@@ -141,6 +147,7 @@ export function mountFlythrough(host, options, settings = {}) {
141
147
  sheet = { x: vb.x, y: vb.y, w: vb.width, h: vb.height };
142
148
  drawPlan();
143
149
  }
150
+ turnSettings.lens = lens;
144
151
  turn.set(was, false);
145
152
  }
146
153
  const startSpan = start?.span ?? 9000000;
@@ -201,9 +208,12 @@ export function mountFlythrough(host, options, settings = {}) {
201
208
  put("3d", turnHost, "visibility", live3d ? "visible" : "hidden");
202
209
  if (live3d && turn !== undefined) {
203
210
  put("3d", turnHost, "opacity", num(cam.arena, 3));
204
- const view = `${num(cam.yaw, 3)}|${num(cam.pitch, 3)}|${num(cam.logZoom, 4)}`;
211
+ put("3d", turnHost, "--kf-roof", num(cam.roof, 3));
212
+ put("3d", turnHost, "@data-roof", cam.roof > 0.004 ? "1" : "0");
213
+ const view = `${num(cam.yaw, 3)}|${num(cam.pitch, 3)}|${num(cam.logZoom, 4)}|${num(cam.logLens, 4)}`;
205
214
  if (written["3d:view"] !== view) {
206
215
  written["3d:view"] = view;
216
+ turnSettings.lens = Math.exp(cam.logLens);
207
217
  turn.set({ yaw: cam.yaw, pitch: cam.pitch, zoom: Math.exp(cam.logZoom) }, false);
208
218
  }
209
219
  }
@@ -25,6 +25,9 @@ export interface MarkupInput {
25
25
  /** The arena's name, and where it is (`Anaheim, United States`), in the page's language. */
26
26
  arena: string;
27
27
  place: string;
28
+ /** The venue's seats for ice hockey and its seating tiers, in the words of the bowl's stage (`{seats}`, `{tiers}`). */
29
+ seats?: number | string;
30
+ tiers?: number | string;
28
31
  /** The stages flown, in order. */
29
32
  stages: readonly StageId[];
30
33
  /** The drawn map (already through `mapMarkup`), and whether it is chizu's (so that the credit names it). */
@@ -38,6 +41,8 @@ export interface MarkupInput {
38
41
  export declare const stageWords: (language: Language, stage: StageId, values: {
39
42
  arena: string;
40
43
  place: string;
44
+ seats?: number | string;
45
+ tiers?: number | string;
41
46
  }) => {
42
47
  name: string;
43
48
  text: string;
@@ -63,7 +63,7 @@ export function controlsMarkup(words, stages, current, playing) {
63
63
  /** The whole fly-through as markup. */
64
64
  export function flythroughMarkup(input) {
65
65
  const words = FLYTHROUGH_WORDS[input.language];
66
- const values = { arena: input.arena, place: input.place };
66
+ const values = { arena: input.arena, place: input.place, seats: input.seats, tiers: input.tiers };
67
67
  const first = input.stages[0];
68
68
  const intro = stageWords(input.language, first, values);
69
69
  const label = say(words.title, values);
@@ -1,14 +1,15 @@
1
1
  /** The stages of the fly-through, in order. */
2
- export type StageId = "map" | "city" | "arena";
2
+ export type StageId = "map" | "city" | "arena" | "bowl" | "rink";
3
3
  /** The stages that exist, in the order of the path. */
4
4
  export declare const STAGE_IDS: readonly StageId[];
5
5
  /** The channels of the camera. */
6
- export declare const CHANNELS: readonly ["logSpan", "ax", "ay", "north", "map", "site", "arena", "yaw", "pitch", "logZoom"];
6
+ export declare const CHANNELS: readonly ["logSpan", "ax", "ay", "north", "map", "site", "arena", "yaw", "pitch", "logZoom", "logLens", "roof"];
7
7
  export type Channel = (typeof CHANNELS)[number];
8
8
  /**
9
9
  * Where the camera is. `logSpan` is the natural logarithm of the metres the container's width shows (so that a zoom is a straight line); `ax` and `ay` are where the arena stands on the screen, as a
10
10
  * share of the width and of the height from the middle; `north` is how far the map and the site plan are turned, in degrees clockwise (the rink's long side lies along the page when it reaches the
11
- * venue's); `map`, `site` and `arena` are how much of the map, of the site plan and of the 3D arena shows (0 to 1); `yaw`, `pitch` and `logZoom` are the 3D view's turn and tip in degrees and the natural logarithm of its zoom.
11
+ * venue's); `map`, `site` and `arena` are how much of the map, of the site plan and of the 3D arena shows (0 to 1); `yaw`, `pitch` and `logZoom` are the 3D view's turn and tip in degrees and the natural logarithm of its zoom;
12
+ * `logLens` is the natural logarithm of the eye's lens, as a multiple of the stage's longer side (a long lens looks from far outside, a short one from inside the bowl) and `roof` is how much of the building's roof shows (0 to 1).
12
13
  */
13
14
  export type Camera = Record<Channel, number>;
14
15
  /** What the path needs to know of the page and the venue. */
@@ -33,6 +34,14 @@ export interface FlightInput {
33
34
  };
34
35
  /** Degrees a second the arena turns at, once it is turning: 12 by default. */
35
36
  turn?: number;
37
+ /** The seat's viewpoint the camera settles at in the bowl: the tip of the 3D view in degrees, its zoom and the eye's lens. 77, 2.8 and 1 by default. */
38
+ seat?: {
39
+ pitch: number;
40
+ zoom: number;
41
+ lens: number;
42
+ };
43
+ /** Degrees a second the rink turns at, from the seat: 8 by default. */
44
+ rinkTurn?: number;
36
45
  }
37
46
  /** A keyframe: the camera at a moment, and where it must be going where that is not what the neighbours say. */
38
47
  export interface Keyframe {
@@ -51,11 +60,11 @@ export interface Flight {
51
60
  }[];
52
61
  /** The time of the last keyframe. */
53
62
  end: number;
54
- /** After `end`, the arena goes on turning at this many degrees a second (negative: the near side goes to the right), and nothing else moves. */
63
+ /** After `end`, the view goes on turning at this many degrees a second (negative: the near side goes to the right), and nothing else moves: the arena's turn, or the rink's from the seat. */
55
64
  tail: number;
56
65
  /** The camera of each stage at rest: what a reader who prefers reduced motion is shown (one layer, nothing half-faded). */
57
66
  rest: Readonly<Record<StageId, Camera>>;
58
- input: Required<Pick<FlightInput, "north" | "siteSpan" | "startSpan" | "citySpan" | "turn">> & {
67
+ input: Required<Pick<FlightInput, "north" | "siteSpan" | "startSpan" | "citySpan" | "turn" | "rinkTurn" | "seat">> & {
59
68
  anchorStart: readonly [number, number];
60
69
  anchorEnd: readonly [number, number];
61
70
  view: {
@@ -83,7 +92,7 @@ export declare const stagesOf: (flight: Flight) => StageId[];
83
92
  export declare const turnPeriod: (flight: Flight) => number;
84
93
  /** A moment after the end brought back by whole turns of the arena to one near the end: the same view, and a time that does not grow without end. */
85
94
  export declare function foldTime(flight: Flight, time: number): number;
86
- /** How a path is paced: the seconds of each part. */
95
+ /** How a path is paced: the seconds of each part. `hold` is how long the arena turns, whole, before the camera goes in; `over` and `settle` the two parts of the way into the bowl; `stay` how long a still run of the rink's turn is kept before the end. */
87
96
  export declare const PACE: {
88
97
  readonly dwell: 1.6;
89
98
  readonly step: 1.25;
@@ -92,11 +101,15 @@ export declare const PACE: {
92
101
  readonly rise: 0.8;
93
102
  readonly hand: 0.7;
94
103
  readonly tilt: 3.2;
104
+ readonly hold: 3.5;
105
+ readonly over: 2.2;
106
+ readonly settle: 2.2;
107
+ readonly stay: 2;
95
108
  };
96
109
  /**
97
- * The path from the map to the turning arena, for the stages asked (all by default): the map holds a moment, then the camera falls to the city in four even steps of the logarithm of the distance (the
110
+ * The path from the map to the rink, for the stages asked (all by default): the map holds a moment, then the camera falls to the city in four even steps of the logarithm of the distance (the
98
111
  * map turning as it comes to the venue's own orientation and giving way to the site plan), holds there, comes in until the plan is as large as the 3D arena seen from above, the arena rises out of it,
99
- * tips to the sport's main camera and turns.
112
+ * tips to the sport's main camera and turns; then the roof lifts away and the camera comes in over the stands, the eye's lens shortening as it goes, and settles at a seat's viewpoint, from where the rink turns.
100
113
  */
101
114
  export declare function flightPlan(input: FlightInput): Flight;
102
115
  /** The 3D view the arena settles in, as the sport's main camera: its yaw and pitch. */
@@ -3,11 +3,11 @@
3
3
  // keyframes in time, and every channel runs through them as a monotone cubic (Fritsch and Carlson), so that it is smooth where it passes a keyframe (the speed does not jump),
4
4
  // never overshoots between two (a layer's opacity stays between 0 and 1), and is still at its first keyframe. Nothing here touches a page: the same path can be worked out in
5
5
  // Node, tested frame by frame, and handed to the mount, which draws it.
6
- import { LONG_PRESETS, presetView } from "./turn.camera.js";
6
+ import { LENS, LONG_PRESETS, presetView } from "./turn.camera.js";
7
7
  /** The stages that exist, in the order of the path. */
8
- export const STAGE_IDS = ["map", "city", "arena"];
8
+ export const STAGE_IDS = ["map", "city", "arena", "bowl", "rink"];
9
9
  /** The channels of the camera. */
10
- export const CHANNELS = ["logSpan", "ax", "ay", "north", "map", "site", "arena", "yaw", "pitch", "logZoom"];
10
+ export const CHANNELS = ["logSpan", "ax", "ay", "north", "map", "site", "arena", "yaw", "pitch", "logZoom", "logLens", "roof"];
11
11
  const clamp = (value, low, high) => Math.min(high, Math.max(low, value));
12
12
  const lerp = (a, b, t) => a + (b - a) * t;
13
13
  /** Zero to one with a zero slope at both ends. */
@@ -107,13 +107,13 @@ export function foldTime(flight, time) {
107
107
  return flight.end + ((time - flight.end) % period);
108
108
  }
109
109
  /** The camera's defaults: the map, from far above, with nothing of the arena showing. */
110
- const BASE = { logSpan: 0, ax: 0, ay: 0, north: 0, map: 1, site: 0, arena: 0, yaw: 0, pitch: 0, logZoom: 0 };
111
- /** How a path is paced: the seconds of each part. */
112
- export const PACE = { dwell: 1.6, step: 1.25, cityDwell: 0.9, approach: 1.6, rise: 0.8, hand: 0.7, tilt: 3.2 };
110
+ const BASE = { logSpan: 0, ax: 0, ay: 0, north: 0, map: 1, site: 0, arena: 0, yaw: 0, pitch: 0, logZoom: 0, logLens: Math.log(LENS), roof: 1 };
111
+ /** How a path is paced: the seconds of each part. `hold` is how long the arena turns, whole, before the camera goes in; `over` and `settle` the two parts of the way into the bowl; `stay` how long a still run of the rink's turn is kept before the end. */
112
+ export const PACE = { dwell: 1.6, step: 1.25, cityDwell: 0.9, approach: 1.6, rise: 0.8, hand: 0.7, tilt: 3.2, hold: 3.5, over: 2.2, settle: 2.2, stay: 2 };
113
113
  /**
114
- * The path from the map to the turning arena, for the stages asked (all by default): the map holds a moment, then the camera falls to the city in four even steps of the logarithm of the distance (the
114
+ * The path from the map to the rink, for the stages asked (all by default): the map holds a moment, then the camera falls to the city in four even steps of the logarithm of the distance (the
115
115
  * map turning as it comes to the venue's own orientation and giving way to the site plan), holds there, comes in until the plan is as large as the 3D arena seen from above, the arena rises out of it,
116
- * tips to the sport's main camera and turns.
116
+ * tips to the sport's main camera and turns; then the roof lifts away and the camera comes in over the stands, the eye's lens shortening as it goes, and settles at a seat's viewpoint, from where the rink turns.
117
117
  */
118
118
  export function flightPlan(input) {
119
119
  const startSpan = input.startSpan ?? 9000000;
@@ -122,6 +122,8 @@ export function flightPlan(input) {
122
122
  const [ax1, ay1] = input.anchorEnd ?? [0, 0];
123
123
  const north = wrap180(input.north);
124
124
  const turn = input.turn ?? 12;
125
+ const rinkTurn = input.rinkTurn ?? 8;
126
+ const seat = input.seat ?? { pitch: 77, zoom: 2.8, lens: 1 };
125
127
  const view = input.view ?? { yaw: LONG_PRESETS.broadcast.yaw, pitch: LONG_PRESETS.broadcast.pitch };
126
128
  const ln = Math.log;
127
129
  const L0 = ln(startSpan);
@@ -143,6 +145,13 @@ export function flightPlan(input) {
143
145
  const start = { logSpan: L0, ax: ax0, ay: ay0, north: 0, map: 1, site: 0, arena: 0, yaw: 0, pitch: 0, logZoom: 0 };
144
146
  const arrive = { ...fall(4), logSpan: L1, ax: ax1, ay: ay1, north, map: 0, site: 1 };
145
147
  const near = { ...arrive, logSpan: L2 };
148
+ // Where the turn has got to at each key of the arena, the bowl and the rink: the speed changes from `turn` to `rinkTurn` along the way, and each yaw is the one the two speeds either side of it come to.
149
+ const yaws = [];
150
+ let yaw = 0;
151
+ for (const [from, to, seconds] of [[0, turn, PACE.tilt], [turn, turn, PACE.hold], [turn, (turn + rinkTurn) / 2, PACE.over], [(turn + rinkTurn) / 2, rinkTurn, PACE.settle], [rinkTurn, rinkTurn, PACE.stay]]) {
152
+ yaw -= ((from + to) / 2) * seconds;
153
+ yaws.push(yaw);
154
+ }
146
155
  const blocks = [
147
156
  { id: "map", keys: [{ dt: 0, camera: start }, { dt: PACE.dwell, camera: start }, { dt: PACE.step, camera: fall(1) }, { dt: PACE.step, camera: fall(2) }, { dt: PACE.step, camera: fall(3) }] },
148
157
  { id: "city", keys: [{ dt: PACE.step, camera: arrive }, { dt: PACE.cityDwell, camera: arrive }, { dt: PACE.approach, camera: near }, { dt: PACE.rise, camera: { ...near, arena: 1 } }] },
@@ -150,7 +159,21 @@ export function flightPlan(input) {
150
159
  id: "arena",
151
160
  keys: [
152
161
  { dt: PACE.hand, camera: { ...near, arena: 1, site: 0, yaw: 0, pitch: 0 }, slope: { yaw: 0 } },
153
- { dt: PACE.tilt, camera: { ...near, arena: 1, site: 0, yaw: -(turn * PACE.tilt) / 2, pitch: view.pitch }, slope: { yaw: -turn } },
162
+ { dt: PACE.tilt, camera: { ...near, arena: 1, site: 0, yaw: yaws[0], pitch: view.pitch }, slope: { yaw: -turn } },
163
+ ],
164
+ },
165
+ {
166
+ id: "bowl",
167
+ keys: [
168
+ { dt: PACE.hold, camera: { ...near, arena: 1, site: 0, yaw: yaws[1], pitch: view.pitch }, slope: { yaw: -turn } },
169
+ { dt: PACE.over, camera: { ...near, arena: 1, site: 0, yaw: yaws[2], pitch: lerp(view.pitch, seat.pitch, 0.5), logZoom: ln(seat.zoom) * 0.55, logLens: lerp(ln(LENS), ln(seat.lens), 0.6), roof: 0.3 }, slope: { yaw: -(turn + rinkTurn) / 2 } },
170
+ ],
171
+ },
172
+ {
173
+ id: "rink",
174
+ keys: [
175
+ { dt: PACE.settle, camera: { ...near, arena: 1, site: 0, yaw: yaws[3], pitch: seat.pitch, logZoom: ln(seat.zoom), logLens: ln(seat.lens), roof: 0 }, slope: { yaw: -rinkTurn } },
176
+ { dt: PACE.stay, camera: { ...near, arena: 1, site: 0, yaw: yaws[4], pitch: seat.pitch, logZoom: ln(seat.zoom), logLens: ln(seat.lens), roof: 0 }, slope: { yaw: -rinkTurn } },
154
177
  ],
155
178
  },
156
179
  ];
@@ -184,14 +207,16 @@ export function flightPlan(input) {
184
207
  map: { ...BASE, ...start },
185
208
  city: { ...BASE, ...arrive },
186
209
  arena: { ...BASE, ...near, map: 0, site: 0, arena: 1, yaw: view.yaw, pitch: view.pitch, logZoom: 0 },
210
+ bowl: { ...BASE, ...near, map: 0, site: 0, arena: 1, yaw: view.yaw, pitch: lerp(view.pitch, seat.pitch, 0.5), logZoom: ln(seat.zoom) * 0.55, logLens: lerp(ln(LENS), ln(seat.lens), 0.6), roof: 0 },
211
+ rink: { ...BASE, ...near, map: 0, site: 0, arena: 1, yaw: view.yaw, pitch: seat.pitch, logZoom: ln(seat.zoom), logLens: ln(seat.lens), roof: 0 },
187
212
  };
188
213
  return {
189
214
  keys,
190
215
  stages,
191
216
  end: time,
192
- tail: flown.some((block) => block.id === "arena") ? -turn : 0,
217
+ tail: flown[flown.length - 1].keys[flown[flown.length - 1].keys.length - 1].slope?.yaw ?? 0,
193
218
  rest,
194
- input: { north, siteSpan: input.siteSpan, startSpan, citySpan, turn, anchorStart: [ax0, ay0], anchorEnd: [ax1, ay1], view },
219
+ input: { north, siteSpan: input.siteSpan, startSpan, citySpan, turn, rinkTurn, seat, anchorStart: [ax0, ay0], anchorEnd: [ax1, ay1], view },
195
220
  };
196
221
  }
197
222
  /** The 3D view the arena settles in, as the sport's main camera: its yaw and pitch. */
@@ -1,2 +1,2 @@
1
1
  /** The fly-through's stylesheet. */
2
- export declare const FLYTHROUGH_STYLE = ".kf{--kf-bg:#ffffff;--kf-ink:#10151b;--kf-muted:#434b54;--kf-panel:rgba(255,255,255,.9);--kf-line:#b9b3a3;--kf-accent:#1f5fa8;--kf-pin:#c0392b;--kf-grid:rgba(60,70,80,.2);--kf-sea:#cfe3ee;--kf-button:#ffffff;--kf-button-on:#10151b;--kf-button-on-ink:#ffffff;--kf-land:#e9e1c8;--kf-coast:#5d5a50;color:var(--kf-ink);font:16px/1.45 system-ui,-apple-system,\"Segoe UI\",\"Hiragino Sans\",\"Noto Sans JP\",sans-serif;color-scheme:light dark}\n@media (prefers-color-scheme:dark){.kf:not([data-theme=light]){--kf-bg:#18212a;--kf-ink:#f6f3ea;--kf-muted:#bcc3cb;--kf-panel:rgba(24,33,42,.9);--kf-line:#475463;--kf-accent:#7fb2ee;--kf-pin:#ff7a6b;--kf-grid:rgba(200,210,220,.22);--kf-sea:#14232f;--kf-button:#18212a;--kf-button-on:#f6f3ea;--kf-button-on-ink:#0d1218;--kf-land:#2f3b44;--kf-coast:#8a97a3}}\n.kf[data-theme=dark]{--kf-bg:#18212a;--kf-ink:#f6f3ea;--kf-muted:#bcc3cb;--kf-panel:rgba(24,33,42,.9);--kf-line:#475463;--kf-accent:#7fb2ee;--kf-pin:#ff7a6b;--kf-grid:rgba(200,210,220,.22);--kf-sea:#14232f;--kf-button:#18212a;--kf-button-on:#f6f3ea;--kf-button-on-ink:#0d1218;--kf-land:#2f3b44;--kf-coast:#8a97a3;color-scheme:dark}\n.kf[data-theme=light]{color-scheme:light}\n.kf *{box-sizing:border-box}\n.kf [hidden]{display:none!important}\n.kf-stage{position:relative;isolation:isolate;width:100%;aspect-ratio:16/10;min-height:260px;overflow:hidden;background:var(--kf-bg);border:1px solid var(--kf-line);border-radius:12px;outline-offset:2px;user-select:none;-webkit-user-select:none}\n.kf-stage:focus-visible{outline:3px solid var(--kf-accent)}\n@media (max-width:560px){.kf-stage{aspect-ratio:4/5}}\n.kf-layer{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none}\n.kf-map{overflow:hidden}\n.kf-map-box{position:absolute;transform-origin:50% 50%;background:var(--kf-sea)}\n.kf-map-box svg{display:block;width:100%;height:100%}\n.kf-map-box .chizu{--cz-sea:var(--kf-sea);--cz-land:var(--kf-land);--cz-line:var(--kf-coast)}\n.kf-site{overflow:hidden}\n.kf-grid{fill:none;stroke:var(--kf-grid);stroke-width:1}\n.kf-rink{fill:var(--kf-bg);fill-opacity:.9;stroke:var(--kf-muted);stroke-width:.6;vector-effect:non-scaling-stroke}\n.kf-3d{overflow:hidden}\n.kf-3d .ky-turn{height:100%}\n.kf-3d .ky-turn-view{aspect-ratio:auto;height:100%;min-height:0;border-radius:0;cursor:default;touch-action:auto}\n.kf-3d .ky-turn-help,.kf-3d .ky-turn-live{display:none}\n.kf-pin{position:absolute;left:0;top:0;width:0;height:0;pointer-events:none}\n.kf-pin::before{content:\"\";position:absolute;left:-7px;top:-7px;width:14px;height:14px;border-radius:50%;background:var(--kf-pin);border:2px solid var(--kf-bg);box-shadow:0 0 0 2px var(--kf-pin)}\n.kf-scale{position:absolute;left:12px;bottom:12px;display:flex;flex-direction:column;gap:2px;align-items:flex-start;font-size:.8rem;color:var(--kf-ink);background:var(--kf-panel);padding:4px 8px;border-radius:8px;pointer-events:none}\n.kf-scale-bar{display:block;height:6px;border:2px solid var(--kf-ink);border-top:0}\n.kf-caption{position:absolute;right:12px;top:12px;max-width:min(26rem,calc(100% - 24px));padding:8px 12px;border-radius:10px;background:var(--kf-panel);color:var(--kf-ink);pointer-events:none}\n.kf-title{display:block;font-weight:800}\n.kf-text{display:block;font-size:.92rem;color:var(--kf-muted)}\n.kf-controls{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:10px 0 0}\n.kf-group{display:flex;flex-wrap:wrap;gap:8px;margin:0;padding:0;list-style:none}\n.kf button{font:inherit;font-size:1rem;font-weight:700;min-height:44px;min-width:44px;padding:0 14px;border-radius:12px;border:2px solid var(--kf-ink);background:var(--kf-button);color:var(--kf-ink);cursor:pointer}\n.kf button[aria-pressed=true],.kf button[aria-current=step]{background:var(--kf-button-on);color:var(--kf-button-on-ink)}\n.kf button:focus-visible{outline:3px solid var(--kf-accent);outline-offset:2px}\n.kf-help,.kf-credit,.kf-still{margin:8px 0 0;font-size:.85rem;color:var(--kf-muted)}\n.kf-live{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}";
2
+ export declare const FLYTHROUGH_STYLE = ".kf{--kf-bg:#ffffff;--kf-ink:#10151b;--kf-muted:#434b54;--kf-panel:rgba(255,255,255,.9);--kf-line:#b9b3a3;--kf-accent:#1f5fa8;--kf-pin:#c0392b;--kf-grid:rgba(60,70,80,.2);--kf-sea:#cfe3ee;--kf-button:#ffffff;--kf-button-on:#10151b;--kf-button-on-ink:#ffffff;--kf-land:#e9e1c8;--kf-coast:#5d5a50;color:var(--kf-ink);font:16px/1.45 system-ui,-apple-system,\"Segoe UI\",\"Hiragino Sans\",\"Noto Sans JP\",sans-serif;color-scheme:light dark}\n@media (prefers-color-scheme:dark){.kf:not([data-theme=light]){--kf-bg:#18212a;--kf-ink:#f6f3ea;--kf-muted:#bcc3cb;--kf-panel:rgba(24,33,42,.9);--kf-line:#475463;--kf-accent:#7fb2ee;--kf-pin:#ff7a6b;--kf-grid:rgba(200,210,220,.22);--kf-sea:#14232f;--kf-button:#18212a;--kf-button-on:#f6f3ea;--kf-button-on-ink:#0d1218;--kf-land:#2f3b44;--kf-coast:#8a97a3}}\n.kf[data-theme=dark]{--kf-bg:#18212a;--kf-ink:#f6f3ea;--kf-muted:#bcc3cb;--kf-panel:rgba(24,33,42,.9);--kf-line:#475463;--kf-accent:#7fb2ee;--kf-pin:#ff7a6b;--kf-grid:rgba(200,210,220,.22);--kf-sea:#14232f;--kf-button:#18212a;--kf-button-on:#f6f3ea;--kf-button-on-ink:#0d1218;--kf-land:#2f3b44;--kf-coast:#8a97a3;color-scheme:dark}\n.kf[data-theme=light]{color-scheme:light}\n.kf *{box-sizing:border-box}\n.kf [hidden]{display:none!important}\n.kf-stage{position:relative;isolation:isolate;width:100%;aspect-ratio:16/10;min-height:260px;overflow:hidden;background:var(--kf-bg);border:1px solid var(--kf-line);border-radius:12px;outline-offset:2px;user-select:none;-webkit-user-select:none}\n.kf-stage:focus-visible{outline:3px solid var(--kf-accent)}\n@media (max-width:560px){.kf-stage{aspect-ratio:4/5}}\n.kf-layer{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none}\n.kf-map{overflow:hidden}\n.kf-map-box{position:absolute;transform-origin:50% 50%;background:var(--kf-sea)}\n.kf-map-box svg{display:block;width:100%;height:100%}\n.kf-map-box .chizu{--cz-sea:var(--kf-sea);--cz-land:var(--kf-land);--cz-line:var(--kf-coast)}\n.kf-site{overflow:hidden}\n.kf-grid{fill:none;stroke:var(--kf-grid);stroke-width:1}\n.kf-rink{fill:var(--kf-bg);fill-opacity:.9;stroke:var(--kf-muted);stroke-width:.6;vector-effect:non-scaling-stroke}\n.kf-3d{overflow:hidden}\n.kf-3d .ky-turn{height:100%}\n.kf-3d .ky-turn-view{aspect-ratio:auto;height:100%;min-height:0;border-radius:0;cursor:default;touch-action:auto}\n.kf-3d .ky-turn-help,.kf-3d .ky-turn-live{display:none}\n.kf-3d .ky-up-roof{filter:opacity(var(--kf-roof,1))}\n.kf-3d[data-roof=\"0\"] .ky-up-roof{visibility:hidden}\n.kf-pin{position:absolute;left:0;top:0;width:0;height:0;pointer-events:none}\n.kf-pin::before{content:\"\";position:absolute;left:-7px;top:-7px;width:14px;height:14px;border-radius:50%;background:var(--kf-pin);border:2px solid var(--kf-bg);box-shadow:0 0 0 2px var(--kf-pin)}\n.kf-scale{position:absolute;left:12px;bottom:12px;display:flex;flex-direction:column;gap:2px;align-items:flex-start;font-size:.8rem;color:var(--kf-ink);background:var(--kf-panel);padding:4px 8px;border-radius:8px;pointer-events:none}\n.kf-scale-bar{display:block;height:6px;border:2px solid var(--kf-ink);border-top:0}\n.kf-caption{position:absolute;right:12px;top:12px;max-width:min(26rem,calc(100% - 24px));padding:8px 12px;border-radius:10px;background:var(--kf-panel);color:var(--kf-ink);pointer-events:none}\n.kf-title{display:block;font-weight:800}\n.kf-text{display:block;font-size:.92rem;color:var(--kf-muted)}\n.kf-controls{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:10px 0 0}\n.kf-group{display:flex;flex-wrap:wrap;gap:8px;margin:0;padding:0;list-style:none}\n.kf button{font:inherit;font-size:1rem;font-weight:700;min-height:44px;min-width:44px;padding:0 14px;border-radius:12px;border:2px solid var(--kf-ink);background:var(--kf-button);color:var(--kf-ink);cursor:pointer}\n.kf button[aria-pressed=true],.kf button[aria-current=step]{background:var(--kf-button-on);color:var(--kf-button-on-ink)}\n.kf button:focus-visible{outline:3px solid var(--kf-accent);outline-offset:2px}\n.kf-help,.kf-credit,.kf-still{margin:8px 0 0;font-size:.85rem;color:var(--kf-muted)}\n.kf-live{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}";