@johnmorrisdotca/kyogijo-stadiums 0.40.1 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/NOTICE.md +17 -0
  3. package/README.md +1 -3
  4. package/dist/context.d.ts +141 -0
  5. package/dist/context.js +290 -0
  6. package/dist/crowd.d.ts +33 -1
  7. package/dist/crowd.draw.d.ts +1 -1
  8. package/dist/crowd.draw.js +16 -2
  9. package/dist/crowd.js +9 -1
  10. package/dist/district-entry.d.ts +14 -0
  11. package/dist/district-entry.js +9 -0
  12. package/dist/district.d.ts +54 -1
  13. package/dist/district.dom.d.ts +31 -0
  14. package/dist/district.dom.js +52 -0
  15. package/dist/district.js +71 -11
  16. package/dist/district.turn.js +30 -6
  17. package/dist/draw.js +37 -13
  18. package/dist/gates.d.ts +56 -0
  19. package/dist/gates.js +95 -0
  20. package/dist/halls.d.ts +2 -0
  21. package/dist/halls.js +1 -1
  22. package/dist/kodokan.d.ts +28 -0
  23. package/dist/kodokan.data.d.ts +18 -0
  24. package/dist/kodokan.data.js +18 -0
  25. package/dist/kodokan.js +34 -0
  26. package/dist/osm-entry.d.ts +2 -0
  27. package/dist/osm-entry.js +2 -0
  28. package/dist/osm.context.data.d.ts +10 -0
  29. package/dist/osm.context.data.js +1107 -0
  30. package/dist/outside.d.ts +43 -0
  31. package/dist/outside.dom.d.ts +44 -0
  32. package/dist/outside.dom.js +203 -0
  33. package/dist/outside.js +227 -0
  34. package/dist/outside.paint.d.ts +57 -0
  35. package/dist/outside.paint.js +88 -0
  36. package/dist/plateau-entry.d.ts +3 -0
  37. package/dist/plateau-entry.js +3 -0
  38. package/dist/plateau.credit.d.ts +5 -0
  39. package/dist/plateau.credit.js +7 -0
  40. package/dist/plateau.data.d.ts +7 -0
  41. package/dist/plateau.data.js +757 -0
  42. package/dist/stadiums.d.ts +15 -1
  43. package/dist/stadiums.data.js +2 -1
  44. package/dist/stadiums.js +48 -12
  45. package/dist/strings.d.ts +3 -1
  46. package/dist/strings.js +4 -4
  47. package/dist/surfaces/surface.d.ts +5 -0
  48. package/dist/turn.dom.d.ts +10 -0
  49. package/dist/turn.dom.js +21 -0
  50. package/dist/turn.kit.d.ts +10 -3
  51. package/dist/turn.kit.js +7 -0
  52. package/dist/turn.markup.js +7 -2
  53. package/dist/turn.model.d.ts +3 -0
  54. package/dist/turn.style.js +15 -3
  55. package/dist/uprights.js +9 -1
  56. package/dist/venue.js +9 -3
  57. package/dist/venue.real.d.ts +22 -1
  58. package/dist/venue.real.js +76 -7
  59. package/dist/venues.d.ts +9 -1
  60. package/dist/venues.data.js +2 -1
  61. package/dist/venues.js +1 -1
  62. package/dist/version.d.ts +1 -1
  63. package/dist/version.js +1 -1
  64. package/dist/walk.d.ts +56 -0
  65. package/dist/walk.data.d.ts +4 -0
  66. package/dist/walk.data.js +1295 -0
  67. package/dist/walk.js +120 -0
  68. package/package.json +17 -1
@@ -80,6 +80,13 @@ export interface RealStadiumData {
80
80
  };
81
81
  read: string;
82
82
  };
83
+ /** The building's lower band, a podium under the concourse levels: its height in feet and how it is known (ours where the sources do not say). */
84
+ facade?: {
85
+ podium: number;
86
+ unit: "ft";
87
+ basis: StadiumBasis;
88
+ read: string;
89
+ };
83
90
  roof: {
84
91
  kind: StadiumRoof;
85
92
  basis: StadiumBasis;
@@ -90,6 +97,13 @@ export interface RealStadiumData {
90
97
  width: number;
91
98
  unit: "m";
92
99
  };
100
+ /** A cable roof's masts round its rim, as the engineering article gives them: how many, and how high they rise over the ring beam, in metres (the source is named, apart from Wikipedia's, in `source`). */
101
+ masts?: {
102
+ count: number;
103
+ height: number;
104
+ unit: "m";
105
+ source: string;
106
+ };
93
107
  /** A canopy's share of the seats it covers, as the article gives it. */
94
108
  cover?: number;
95
109
  /** A retractable roof's bottom and top above the field, in feet, as the article gives them (an air-supported roof's height is the building's, from OpenStreetMap). */
@@ -183,7 +197,7 @@ export declare const stadiumConfigOf: (sport: string | undefined) => StadiumConf
183
197
  export declare const stadiumSeats: (stadium: RealStadium, config: StadiumConfigId) => number;
184
198
  /**
185
199
  * The stylesheet of a stadium that stages more than one field (`data-stadium` on the drawing's root): the ground round the building is its own, the football field's, whichever field lies in it, so that a soccer pitch does not tint it
186
- * green. Light only: the dark ground is one for every sport.
200
+ * green (light only: the dark ground is one for every sport), and the crowd's dimming in the dark kept on a layer of its own.
187
201
  */
188
202
  export declare const KYOGIJO_STADIUM_STYLE: string;
189
203
  /** How far a hall's first row is from the box of the ring, on every side, in feet (ours: the seats at the ringside are a few feet from the ropes). */
@@ -7,7 +7,8 @@ export const STADIUM_DATA = [
7
7
  "configs": { "cfl": { "sport": "cfl", "seats": 54500, "source": "wikipedia", "read": "the infobox's first capacity, 54,500, which Wikidata's capacity (P1083) also gives; the article's stand for football of the CFL's club" }, "soccer": { "sport": "soccer", "seats": 52497, "source": "wikipedia", "read": "the infobox's capacity for the 2026 world cup of soccer, 52,497, 2,003 fewer than its first" } },
8
8
  "wikidataSeats": [54500],
9
9
  "tiers": { "count": 2, "basis": "documented", "lowerSeats": 27695, "read": "the infobox's lower bowl of 27,695 seats, and the article's lower and upper bowls (the club that plays soccer there sold only the lower bowl)" },
10
- "roof": { "kind": "cable", "basis": "documented", "read": "the article: a retractable roof supported by cables, the largest of its kind, its opening 100 by 85 metres (the size of the field), the fabric retracting into a pod in the middle of the opening above the suspended video board", "opening": { "length": 100, "width": 85, "unit": "m" } },
10
+ "roof": { "kind": "cable", "basis": "documented", "read": "the article: a retractable roof supported by cables, the largest of its kind, its opening 100 by 85 metres (the size of the field), the fabric retracting into a pod in the middle of the opening above the suspended video board", "opening": { "length": 100, "width": 85, "unit": "m" }, "masts": { "count": 36, "height": 47.5, "unit": "m", "source": "https://www.structuremag.org/article/bc-place-revitalization/ (STRUCTURE magazine, 'BC Place Revitalization': thirty-six steel perimeter masts rise 47.5 metres above the original roof's concrete ring beam, carrying a radial cable truss post-tensioned in a new steel compression ring; the roof is about 40,000 square metres, clear spans of 227 by 186 metres, its centre 7,500 square metres retractable into the centre gondola above the video scoreboard); read on 2026-10-22, facts only" } },
11
+ "facade": { "podium": 40, "unit": "ft", "basis": "plausible", "read": "the brief: a lower podium ring under the concourse levels, in the building's own outline; its height is ours (the sources do not say)" },
11
12
  },
12
13
  {
13
14
  "id": "lumen-field",
package/dist/stadiums.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // field and not a rink: level one is the building, level two a plausible bowl, and it stops there (no team's, ticket seller's or federation's seat map is read or copied, no section or row is named, and no mark of a club, a
4
4
  // league or a tournament is drawn). The drawing is `drawKyogijo({ sport: "cfl", stadium, crowd: { real: true } })` (or `sport: "soccer"` with a `ground` or a `stadium`, `sport: "mlb"` with a `park`) and, in 3D, the same options
5
5
  // in `buildTurn`.
6
- import { NORTH_ROOM, aislesOf, areaOfOutline, outlinePoints } from "./crowd.js";
6
+ import { NORTH_ROOM, aisleParameters, aislesOf, areaOfOutline, outlinePoints, sectorPolygon } from "./crowd.js";
7
7
  import { parkLayout } from "./crowd.park.js";
8
8
  import { FEET_PER_METRE, FOOTBALL } from "./geometry.js";
9
9
  import { groundById } from "./grounds.js";
@@ -61,9 +61,12 @@ export const stadiumConfigOf = (sport) => (sport === "cfl" || sport === "nfl" ||
61
61
  export const stadiumSeats = (stadium, config) => stadium.configs[config]?.seats ?? 0;
62
62
  /**
63
63
  * The stylesheet of a stadium that stages more than one field (`data-stadium` on the drawing's root): the ground round the building is its own, the football field's, whichever field lies in it, so that a soccer pitch does not tint it
64
- * green. Light only: the dark ground is one for every sport.
64
+ * green (light only: the dark ground is one for every sport), and the crowd's dimming in the dark kept on a layer of its own.
65
65
  */
66
- export const KYOGIJO_STADIUM_STYLE = "@media (prefers-color-scheme:light){:where(html:not([data-theme=dark]) .kyogijo[data-sport=soccer][data-stadium]:not([data-theme=dark])){--ky-apron:#dcd8c9}}\n" +
66
+ export const KYOGIJO_STADIUM_STYLE =
67
+ // The crowd dims in the dark as a group (`opacity`): on a layer of its own it is drawn the same whatever is drawn over it (the field), so that two configurations of a stadium are the same crowd to the last level of a pixel.
68
+ ".kyogijo[data-stadium] .ky-crowd{will-change:opacity}\n" +
69
+ "@media (prefers-color-scheme:light){:where(html:not([data-theme=dark]) .kyogijo[data-sport=soccer][data-stadium]:not([data-theme=dark])){--ky-apron:#dcd8c9}}\n" +
67
70
  ":where(html[data-theme=light] .kyogijo[data-sport=soccer][data-stadium],.kyogijo[data-theme=light][data-sport=soccer][data-stadium]){--ky-apron:#dcd8c9}";
68
71
  // ---- The bowl. ----
69
72
  /** How far the first row is from a football field's footprint (the league's field and its end zones) along a side and at an end, and the corner's radius: the package's own gridiron profile (`crowd.ts`), so that a real stadium's first row is the usual one. */
@@ -100,6 +103,42 @@ function solveFor(f, lo, hi, target) {
100
103
  }
101
104
  return (a + b) / 2;
102
105
  }
106
+ /**
107
+ * The bowl with `closedSeats` of its seats closed, the ones a configuration does not open: whole bands from the last (outermost) inward, marked `closed` and drawn as empty seats, and where the seats left to close are part of a band, a block of
108
+ * that band at the middle of its left side (an end of the field, where the long axis ends), as many seats as comes nearest, drawn as empty seats over it. The same place every time: no random number. `area` is
109
+ * the floor a seat takes in the plan.
110
+ */
111
+ function shut(rings, closedSeats, area) {
112
+ const out = rings.map((ring) => ({ ...ring }));
113
+ const sectors = [];
114
+ let left = closedSeats;
115
+ for (let k = out.length - 1; k >= 0 && left > 0; k -= 1) {
116
+ const ring = out[k];
117
+ if (left >= ring.seats - 2) {
118
+ ring.fill = 0;
119
+ ring.closed = true;
120
+ left -= ring.seats;
121
+ continue;
122
+ }
123
+ if (ring.region.kind === "rrect" && ring.hole?.kind === "rrect") {
124
+ const [region, hole] = [ring.region, ring.hole];
125
+ // A block centred on the middle of the left side (6.5), widened by a tenth of a side's length (about 30 feet) at a time, as wide as comes nearest to the seats to close.
126
+ const step = 1 / Math.max(aisleParameters(hole).nv, 2);
127
+ const seatsOf = (w) => Math.round(areaOfOutline({ kind: "poly", points: sectorPolygon(region, hole, 6.5 - w / 2, 6.5 + w / 2) }) / area);
128
+ let width = step;
129
+ let made = seatsOf(width);
130
+ for (let i = 0; made < left && width + step < 8; i += 1) {
131
+ const next = seatsOf(width + step);
132
+ if (Math.abs(next - left) > Math.abs(made - left))
133
+ break;
134
+ [width, made] = [width + step, next];
135
+ }
136
+ sectors.push({ ring: ring.id, kind: "closed", points: sectorPolygon(region, hole, 6.5 - width / 2, 6.5 + width / 2) });
137
+ }
138
+ left = 0;
139
+ }
140
+ return { rings: out, sectors };
141
+ }
103
142
  /** Plans already made, by stadium, configuration and field: a plan is a pure function of them, and a drawing asks for it more than once. */
104
143
  const PLANS = new WeakMap();
105
144
  /**
@@ -244,14 +283,8 @@ function plan(stadium, config, rink) {
244
283
  }
245
284
  }
246
285
  const total = rings.reduce((sum, ring) => sum + ring.seats, 0);
247
- // A configuration that opens fewer seats than the bowl holds opens them from the lowest bowl up: a bowl's people are its open share of them, and a closed one is empty.
248
- if (open < seatsTotal) {
249
- let before = 0;
250
- for (const ring of rings) {
251
- ring.fill = round(ring.fill * Math.min(1, Math.max(0, (open - before) / Math.max(1, ring.seats))), 3);
252
- before += ring.seats;
253
- }
254
- }
286
+ // A configuration that opens fewer seats than the bowl holds opens them from the lowest bowl up: the seats it does not open are the top tier's, closed, drawn empty and in the same place every time.
287
+ const closing = open < seatsTotal ? shut(rings, seatsTotal - open, seatArea) : { rings, sectors: [] };
255
288
  const top = rings[rings.length - 1].height[1];
256
289
  const roof = stadium.osm.roof;
257
290
  const rise = roof?.rise === undefined ? 0 : round(roof.rise * FEET_PER_METRE);
@@ -263,7 +296,8 @@ function plan(stadium, config, rink) {
263
296
  const north = orientation.basis === "none" ? undefined : orientation.north;
264
297
  const room = north === undefined ? 0 : NORTH_ROOM;
265
298
  const bowl = {
266
- rings,
299
+ rings: closing.rings,
300
+ ...(closing.sectors.length === 0 ? {} : { closedSectors: closing.sectors }),
267
301
  off: { left: rink.x - first.x0, right: first.x1 - rink.x - rink.w, top: rink.y - first.y0, bottom: first.y1 - rink.y - rink.h },
268
302
  reach: { left: Math.max(0, -rink.w / 2 - around.x0), right: Math.max(0, around.x1 - rink.w / 2), top: Math.max(0, -rink.h / 2 - around.y0) + room, bottom: Math.max(0, around.y1 - rink.h / 2) },
269
303
  shell: {
@@ -273,6 +307,8 @@ function plan(stadium, config, rink) {
273
307
  roof: cable ? "cable" : canopies === undefined ? "fixed" : "canopy",
274
308
  edge,
275
309
  ...(cable ? { crown: { rise, opening } } : {}),
310
+ ...(stadium.facade === undefined ? {} : { podium: stadium.facade.podium }),
311
+ ...(cable && stadium.roof.masts !== undefined ? { masts: { count: stadium.roof.masts.count, height: round(stadium.roof.masts.height * FEET_PER_METRE) } } : {}),
276
312
  ...(canopies === undefined ? {} : { canopies }),
277
313
  },
278
314
  };
package/dist/strings.d.ts CHANGED
@@ -529,6 +529,8 @@ export interface KyogijoWords {
529
529
  districtNeighbours: string;
530
530
  districtCity: string;
531
531
  districtCredit: string;
532
+ /** The people outside a district's venues: {percent} of the venues' seats, {people} people, on ground people may stand on. */
533
+ districtOutside: string;
532
534
  /** A game decided on penalties: {name}, and the shoot-out score {a}-{b}, winner first. */
533
535
  wonPens: string;
534
536
  /** The shoot-out, as it is written beside a status: {a} and {b}, in the order of the sides on the drawing. */
@@ -1046,7 +1048,7 @@ export interface KyogijoWords {
1046
1048
  item: string;
1047
1049
  itemSag: string;
1048
1050
  /** The names of what stands on the surface, by kind. */
1049
- parts: Record<"scoreboard" | "net" | "antennae" | "goalposts" | "netposts" | "hoop" | "goal" | "boards" | "stumps" | "pylons" | "flags" | "fence" | "backstop" | "foul-poles" | "table-legs" | "walls" | "starting-blocks" | "backstroke-flags" | "false-start-rope" | "springboards" | "platforms" | "judges-stand" | "hurdles" | "steeplechase" | "high-jump" | "pole-vault" | "cage" | "padding" | "rink-boards" | "mound" | "azuchi" | "targets" | "stands" | "wall-seats" | "roof" | "gallery" | "dais" | "banner" | "shrine" | "mat-edge" | "surrounds" | "officials" | "judges" | "deck" | "cage-fence" | "cage-posts" | "ring-ropes" | "ring-posts" | "ribbon-boards" | "hung-board", string>;
1051
+ parts: Record<"scoreboard" | "net" | "antennae" | "goalposts" | "netposts" | "hoop" | "goal" | "boards" | "stumps" | "pylons" | "flags" | "fence" | "backstop" | "foul-poles" | "table-legs" | "walls" | "starting-blocks" | "backstroke-flags" | "false-start-rope" | "springboards" | "platforms" | "judges-stand" | "hurdles" | "steeplechase" | "high-jump" | "pole-vault" | "cage" | "padding" | "rink-boards" | "mound" | "azuchi" | "targets" | "stands" | "wall-seats" | "roof" | "gallery" | "dais" | "banner" | "shrine" | "mat-edge" | "surrounds" | "officials" | "judges" | "deck" | "cage-fence" | "cage-posts" | "ring-ropes" | "ring-posts" | "ribbon-boards" | "hung-board" | "blocks", string>;
1050
1052
  };
1051
1053
  /** The names of the zones that `zoneAt` answers with (rules.ts), by id. */
1052
1054
  rules: Record<RuleId, string>;
package/dist/strings.js CHANGED
@@ -199,7 +199,7 @@ export const KYOGIJO_STRINGS = {
199
199
  },
200
200
  },
201
201
  scoreTitle: "{away} {awayScore}, {home} {homeScore}",
202
- said: { home: "home", away: "away", won: "{name} won.", tied: "The game is level.", park: "The outfield fence follows the figures published for {park}.", officially: "Its official name is {name}.", ground: "The pitch follows the size published for {ground}.", groundDefault: "No pitch size is published for {ground}, so the pitch has the international size of 105 by 68 metres.", arenaRink: "{arena}, hockey rink", arenaCourt: "{arena}, basketball court", arenaRinkAbout: "an ice hockey rink, {length} by {width} feet ({lengthM} by {widthM} metres) with rounded corners, with its red and blue lines, face-off circles, creases and goal nets", arenaCourtAbout: "a basketball court, {length} by {width} feet ({lengthM} by {widthM} metres), with its keys, three-point lines, free-throw circles and the centre circle", arenaSize: "The size is the one published for {arena}.", arenaUnknown: "The article says the rink of {arena} was shorter than the NHL's but gives no size, so the NHL's is drawn.", real: "The stands are a plausible bowl of {seats} seats in {tiers} tiers, fitted inside the outline of the building of {arena} from OpenStreetMap ((c) OpenStreetMap contributors); it is not the arena's plan.", realBuilding: "The rink lies along the building's long axis, which is an inference, and the arrow points north.", realMapped: "The rink lies as OpenStreetMap maps the ice, and the arrow points north.", realNone: "The rink's orientation is not known, so it is drawn east-west with north at the top.", stadium: "The stands are a plausible bowl of {seats} seats in {tiers} bowls, fitted inside the outline of the building of {stadium} from OpenStreetMap ((c) OpenStreetMap contributors); it is not the stadium's plan.", stadiumOpening: "The field lies along the opening in the roof, which is the size of the field, and the arrow points north.", stadiumBuilding: "The field lies along the building's long axis, which is an inference, and the arrow points north.", stadiumNone: "The field's orientation is not known, so it is drawn east-west with north at the top.", stadiumHall: "The stands are a plausible bowl of {seats} seats in {tiers} levels, fitted inside the outline of the building of {stadium} from OpenStreetMap ((c) OpenStreetMap contributors); it is not the hall's plan.", stadiumRingBuilding: "The ring is set along the building's long axis, which is an inference, and the arrow points north.", stadiumPitchPark: "The way home plate faces is worked out from the grass of the field OpenStreetMap maps, fitted to the published fence, and the arrow points north.", stadiumNonePark: "The way home plate faces is not known, so the field is drawn the way that fits the building, and no arrow points north.", stadiumPark: "The stands are the usual stands of a ballpark, each band as deep as its seats need, for {seats} seats, fitted inside the outline of the building of {stadium} from OpenStreetMap ((c) OpenStreetMap contributors); it is not the park's plan.", stadiumPitch: "The field lies as OpenStreetMap maps the pitch, and the arrow points north.", stadiumInfield: "The way home plate faces is worked out from the infield OpenStreetMap maps and the building's outline, and the arrow points north.", district: "A plan of {count} real venues at their real positions and orientations, from the outlines OpenStreetMap maps; {mode}.", districtNeighbours: "it is fitted close round the buildings", districtCity: "it keeps the real distances, with a grid of kilometres and a scale", districtCredit: "Credit:", era: "The markings are those of the {season} season.", hammer: "{name} has the hammer.", ends: "By end, {name}: {list}.", total: "Total", wonPens: "{name} won {a}-{b} on penalties.", pens: "{a}-{b} pens", on: "Drawn as", sep: ", ", stop: ".", colon: ": ", gap: " " },
202
+ said: { home: "home", away: "away", won: "{name} won.", tied: "The game is level.", park: "The outfield fence follows the figures published for {park}.", officially: "Its official name is {name}.", ground: "The pitch follows the size published for {ground}.", groundDefault: "No pitch size is published for {ground}, so the pitch has the international size of 105 by 68 metres.", arenaRink: "{arena}, hockey rink", arenaCourt: "{arena}, basketball court", arenaRinkAbout: "an ice hockey rink, {length} by {width} feet ({lengthM} by {widthM} metres) with rounded corners, with its red and blue lines, face-off circles, creases and goal nets", arenaCourtAbout: "a basketball court, {length} by {width} feet ({lengthM} by {widthM} metres), with its keys, three-point lines, free-throw circles and the centre circle", arenaSize: "The size is the one published for {arena}.", arenaUnknown: "The article says the rink of {arena} was shorter than the NHL's but gives no size, so the NHL's is drawn.", real: "The stands are a plausible bowl of {seats} seats in {tiers} tiers, fitted inside the outline of the building of {arena} from OpenStreetMap ((c) OpenStreetMap contributors); it is not the arena's plan.", realBuilding: "The rink lies along the building's long axis, which is an inference, and the arrow points north.", realMapped: "The rink lies as OpenStreetMap maps the ice, and the arrow points north.", realNone: "The rink's orientation is not known, so it is drawn east-west with north at the top.", stadium: "The stands are a plausible bowl of {seats} seats in {tiers} bowls, fitted inside the outline of the building of {stadium} from OpenStreetMap ((c) OpenStreetMap contributors); it is not the stadium's plan.", stadiumOpening: "The field lies along the opening in the roof, which is the size of the field, and the arrow points north.", stadiumBuilding: "The field lies along the building's long axis, which is an inference, and the arrow points north.", stadiumNone: "The field's orientation is not known, so it is drawn east-west with north at the top.", stadiumHall: "The stands are a plausible bowl of {seats} seats in {tiers} levels, fitted inside the outline of the building of {stadium} from OpenStreetMap ((c) OpenStreetMap contributors); it is not the hall's plan.", stadiumRingBuilding: "The ring is set along the building's long axis, which is an inference, and the arrow points north.", stadiumPitchPark: "The way home plate faces is worked out from the grass of the field OpenStreetMap maps, fitted to the published fence, and the arrow points north.", stadiumNonePark: "The way home plate faces is not known, so the field is drawn the way that fits the building, and no arrow points north.", stadiumPark: "The stands are the usual stands of a ballpark, each band as deep as its seats need, for {seats} seats, fitted inside the outline of the building of {stadium} from OpenStreetMap ((c) OpenStreetMap contributors); it is not the park's plan.", stadiumPitch: "The field lies as OpenStreetMap maps the pitch, and the arrow points north.", stadiumInfield: "The way home plate faces is worked out from the infield OpenStreetMap maps and the building's outline, and the arrow points north.", district: "A plan of {count} real venues at their real positions and orientations, from the outlines OpenStreetMap maps; {mode}.", districtNeighbours: "it is fitted close round the buildings", districtCity: "it keeps the real distances, with a grid of kilometres and a scale", districtCredit: "Credit:", districtOutside: "People outside: {percent} percent of the seats of the venues, {people} people, stand on the sidewalks, plazas and grounds round them, and none on a road.", era: "The markings are those of the {season} season.", hammer: "{name} has the hammer.", ends: "By end, {name}: {list}.", total: "Total", wonPens: "{name} won {a}-{b} on penalties.", pens: "{a}-{b} pens", on: "Drawn as", sep: ", ", stop: ".", colon: ": ", gap: " " },
203
203
  budo: {
204
204
  sides: { north: "north", south: "south", east: "east", west: "west" },
205
205
  judo: "a judo contest area of {contest} by {contest} metres in a safety area {safety} metres wide, laid with tatami of {tatami}",
@@ -455,7 +455,7 @@ export const KYOGIJO_STRINGS = {
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",
458
- parts: { scoreboard: "scoreboard (lower edge)", net: "net", antennae: "antennae", goalposts: "goal posts", netposts: "net posts", hoop: "baskets", goal: "goals", boards: "boards and glass", stumps: "stumps", pylons: "pylons", flags: "corner flags", fence: "fence", backstop: "backstop", "foul-poles": "foul poles", "table-legs": "table top", walls: "walls", "starting-blocks": "starting blocks", "backstroke-flags": "backstroke flags", "false-start-rope": "false start rope", springboards: "springboards", platforms: "platforms", "judges-stand": "judges' stand", hurdles: "hurdles", steeplechase: "steeplechase barriers", "high-jump": "high jump uprights and bar", "pole-vault": "pole vault uprights and bar", cage: "discus and hammer cage", padding: "padding", "rink-boards": "boards", mound: "clay mound", azuchi: "azuchi", targets: "targets", stands: "stands", "wall-seats": "seats on top of the wall", roof: "roof", gallery: "gallery", dais: "dais", banner: "banner", shrine: "shrine", "mat-edge": "mat thickness", surrounds: "surrounds", officials: "umpires' chairs and score tables", judges: "name cards on the judges' table", deck: "platform", "cage-fence": "fence", "cage-posts": "padded posts", "ring-ropes": "ropes", "ring-posts": "corner posts", "ribbon-boards": "LED ribbon boards", "hung-board": "centre-hung scoreboard" },
458
+ parts: { scoreboard: "scoreboard (lower edge)", net: "net", antennae: "antennae", goalposts: "goal posts", netposts: "net posts", hoop: "baskets", goal: "goals", boards: "boards and glass", stumps: "stumps", pylons: "pylons", flags: "corner flags", fence: "fence", backstop: "backstop", "foul-poles": "foul poles", "table-legs": "table top", walls: "walls", "starting-blocks": "starting blocks", "backstroke-flags": "backstroke flags", "false-start-rope": "false start rope", springboards: "springboards", platforms: "platforms", "judges-stand": "judges' stand", hurdles: "hurdles", steeplechase: "steeplechase barriers", "high-jump": "high jump uprights and bar", "pole-vault": "pole vault uprights and bar", cage: "discus and hammer cage", padding: "padding", "rink-boards": "boards", mound: "clay mound", azuchi: "azuchi", targets: "targets", stands: "stands", "wall-seats": "seats on top of the wall", roof: "roof", gallery: "gallery", dais: "dais", banner: "banner", shrine: "shrine", "mat-edge": "mat thickness", surrounds: "surrounds", officials: "umpires' chairs and score tables", judges: "name cards on the judges' table", deck: "platform", "cage-fence": "fence", "cage-posts": "padded posts", "ring-ropes": "ropes", "ring-posts": "corner posts", "ribbon-boards": "LED ribbon boards", "hung-board": "centre-hung scoreboard", blocks: "surrounding buildings" },
459
459
  },
460
460
  rules: {
461
461
  "defensive-zone": "Defensive zone",
@@ -758,7 +758,7 @@ export const KYOGIJO_STRINGS = {
758
758
  },
759
759
  },
760
760
  scoreTitle: "{away} {awayScore}、{home} {homeScore}",
761
- said: { home: "ホーム", away: "アウェイ", won: "{name}の勝ちです。", tied: "同点です。", park: "外野フェンスは、{park}の公表されている距離をもとに描かれています。", officially: "正式名称は{name}です。", ground: "ピッチは、{ground}の公表されている広さをもとに描かれています。", groundDefault: "{ground}のピッチの広さは公表されていないため、国際標準の105×68mで描かれています。", arenaRink: "{arena}のアイスホッケーリンク", arenaCourt: "{arena}のバスケットボールコート", arenaRinkAbout: "アイスホッケーのリンク。{length}フィート(約{lengthM}m)×{width}フィート(約{widthM}m)で角が丸く、レッドライン、ブルーライン、フェイスオフサークル、クリース、ゴールがあります", arenaCourtAbout: "バスケットボールのコート。{length}フィート(約{lengthM}m)×{width}フィート(約{widthM}m)で、ペイントエリア、スリーポイントライン、フリースローサークル、センターサークルがあります", arenaSize: "広さは、{arena}の公表されている数値をもとに描かれています。", arenaUnknown: "{arena}のリンクはNHLの規格より短かったとされていますが、広さが示されていないため、NHLの規格で描かれています。", real: "スタンドは、OpenStreetMapの建物の輪郭((c) OpenStreetMap contributors)の内側に収まるように、{arena}の{seats}席・{tiers}層を推定して描いたもので、アリーナの設計図ではありません。", realBuilding: "リンクは建物の長い軸に沿って置いており(推定)、矢印は北を指します。", realMapped: "リンクの向きはOpenStreetMapに記録された氷面のとおりで、矢印は北を指します。", realNone: "リンクの向きは分かっていないため、東西に置き、北を上にしています。", stadium: "{stadium}の建物の輪郭(OpenStreetMap、(c) OpenStreetMap contributors)の内側に、{seats}席・{tiers}層のスタンドを推定して描いています。スタジアムの設計図ではありません。", stadiumOpening: "フィールドは屋根の開口部(フィールドと同じ大きさ)に沿って置いており、矢印は北を指します。", stadiumBuilding: "フィールドは建物の長い軸に沿って置いており(推定)、矢印は北を指します。", stadiumNone: "フィールドの向きは分かっていないため、東西に置き、北を上にしています。", stadiumHall: "{stadium}の建物の輪郭(OpenStreetMap、(c) OpenStreetMap contributors)の内側に、{seats}席・{tiers}層の客席を推定して描いています。ホールの設計図ではありません。", stadiumRingBuilding: "リングは建物の長い軸に沿って置いており(推定)、矢印は北を指します。", stadiumPitchPark: "ホームプレートの向きは、OpenStreetMapに記録されたグラウンドの芝生を、公表されているフェンスの形に合わせて求めており、矢印は北を指します。", stadiumNonePark: "ホームプレートの向きは分かっていないため、建物に収まる向きでグラウンドを描いており、北を示す矢印はありません。", stadiumPark: "{stadium}の建物の輪郭(OpenStreetMap、(c) OpenStreetMap contributors)の内側に、{seats}席のスタンドを推定して描いています。球場の設計図ではありません。", stadiumPitch: "フィールドの向きはOpenStreetMapに記録されたピッチのとおりで、矢印は北を指します。", stadiumInfield: "ホームプレートの向きは、OpenStreetMapに記録された内野と建物の輪郭から推定しており、矢印は北を指します。", district: "OpenStreetMapの建物の輪郭をもとに、{count}か所の実在の会場を実際の位置と向きのとおりに並べた配置図です。{mode}。", districtNeighbours: "建物のすぐ近くまで寄せて描いています", districtCity: "実際の距離を保ち、1km方眼と縮尺を添えています", districtCredit: "出典:", era: "ラインやマークは{season}シーズンのものです。", hammer: "{name}がハンマー(最後の1投)を持っています。", ends: "{name}のエンドごとの得点:{list}。", total: "合計", wonPens: "{name}がPK戦を{a}-{b}で制しました。", pens: "PK戦 {a}-{b}", on: "図の内容", sep: "、", stop: "。", colon: ":", gap: "" },
761
+ said: { home: "ホーム", away: "アウェイ", won: "{name}の勝ちです。", tied: "同点です。", park: "外野フェンスは、{park}の公表されている距離をもとに描かれています。", officially: "正式名称は{name}です。", ground: "ピッチは、{ground}の公表されている広さをもとに描かれています。", groundDefault: "{ground}のピッチの広さは公表されていないため、国際標準の105×68mで描かれています。", arenaRink: "{arena}のアイスホッケーリンク", arenaCourt: "{arena}のバスケットボールコート", arenaRinkAbout: "アイスホッケーのリンク。{length}フィート(約{lengthM}m)×{width}フィート(約{widthM}m)で角が丸く、レッドライン、ブルーライン、フェイスオフサークル、クリース、ゴールがあります", arenaCourtAbout: "バスケットボールのコート。{length}フィート(約{lengthM}m)×{width}フィート(約{widthM}m)で、ペイントエリア、スリーポイントライン、フリースローサークル、センターサークルがあります", arenaSize: "広さは、{arena}の公表されている数値をもとに描かれています。", arenaUnknown: "{arena}のリンクはNHLの規格より短かったとされていますが、広さが示されていないため、NHLの規格で描かれています。", real: "スタンドは、OpenStreetMapの建物の輪郭((c) OpenStreetMap contributors)の内側に収まるように、{arena}の{seats}席・{tiers}層を推定して描いたもので、アリーナの設計図ではありません。", realBuilding: "リンクは建物の長い軸に沿って置いており(推定)、矢印は北を指します。", realMapped: "リンクの向きはOpenStreetMapに記録された氷面のとおりで、矢印は北を指します。", realNone: "リンクの向きは分かっていないため、東西に置き、北を上にしています。", stadium: "{stadium}の建物の輪郭(OpenStreetMap、(c) OpenStreetMap contributors)の内側に、{seats}席・{tiers}層のスタンドを推定して描いています。スタジアムの設計図ではありません。", stadiumOpening: "フィールドは屋根の開口部(フィールドと同じ大きさ)に沿って置いており、矢印は北を指します。", stadiumBuilding: "フィールドは建物の長い軸に沿って置いており(推定)、矢印は北を指します。", stadiumNone: "フィールドの向きは分かっていないため、東西に置き、北を上にしています。", stadiumHall: "{stadium}の建物の輪郭(OpenStreetMap、(c) OpenStreetMap contributors)の内側に、{seats}席・{tiers}層の客席を推定して描いています。ホールの設計図ではありません。", stadiumRingBuilding: "リングは建物の長い軸に沿って置いており(推定)、矢印は北を指します。", stadiumPitchPark: "ホームプレートの向きは、OpenStreetMapに記録されたグラウンドの芝生を、公表されているフェンスの形に合わせて求めており、矢印は北を指します。", stadiumNonePark: "ホームプレートの向きは分かっていないため、建物に収まる向きでグラウンドを描いており、北を示す矢印はありません。", stadiumPark: "{stadium}の建物の輪郭(OpenStreetMap、(c) OpenStreetMap contributors)の内側に、{seats}席のスタンドを推定して描いています。球場の設計図ではありません。", stadiumPitch: "フィールドの向きはOpenStreetMapに記録されたピッチのとおりで、矢印は北を指します。", stadiumInfield: "ホームプレートの向きは、OpenStreetMapに記録された内野と建物の輪郭から推定しており、矢印は北を指します。", district: "OpenStreetMapの建物の輪郭をもとに、{count}か所の実在の会場を実際の位置と向きのとおりに並べた配置図です。{mode}。", districtNeighbours: "建物のすぐ近くまで寄せて描いています", districtCity: "実際の距離を保ち、1km方眼と縮尺を添えています", districtCredit: "出典:", districtOutside: "会場の外の人出:会場の全座席数の{percent}%にあたる{people}人が、会場の周りの歩道、広場、敷地に立っています。車道には立っていません。", era: "ラインやマークは{season}シーズンのものです。", hammer: "{name}がハンマー(最後の1投)を持っています。", ends: "{name}のエンドごとの得点:{list}。", total: "合計", wonPens: "{name}がPK戦を{a}-{b}で制しました。", pens: "PK戦 {a}-{b}", on: "図の内容", sep: "、", stop: "。", colon: ":", gap: "" },
762
762
  budo: {
763
763
  sides: { north: "北", south: "南", east: "東", west: "西" },
764
764
  judo: "柔道の試合場。試合場内は{contest}m四方、幅{safety}mの安全地帯があり、{tatami}の畳を敷いています",
@@ -1014,7 +1014,7 @@ export const KYOGIJO_STRINGS = {
1014
1014
  announce: "{name}の視点", announceAngle: "向きは{yaw}度、見下ろす角度は{pitch}度です。",
1015
1015
  flat: "このブラウザーでは3D表示ができないため、平面の図を表示しています。",
1016
1016
  standing: "3D表示で立って見えるもの:{list}。", item: "{name} {h}", itemSag: "{name} 支柱の位置で{h}(中央は{low})",
1017
- parts: { scoreboard: "スコアボードの下端", net: "ネット", antennae: "アンテナ", goalposts: "ゴールポスト", netposts: "支柱", hoop: "バスケットゴール", goal: "ゴール", boards: "ボードとガラス", stumps: "ウィケットのスタンプ", pylons: "パイロン", flags: "コーナーフラッグ", fence: "外野フェンス", backstop: "バックネット", "foul-poles": "ファウルポール", "table-legs": "天板", walls: "壁", "starting-blocks": "スタート台", "backstroke-flags": "背泳ぎ用フラッグ", "false-start-rope": "フォルススタートロープ", springboards: "飛板", platforms: "固定台", "judges-stand": "ジャッジ台", hurdles: "ハードル", steeplechase: "障害物", "high-jump": "走高跳の支柱とバー", "pole-vault": "棒高跳の支柱とバー", cage: "円盤投・ハンマー投の囲い", padding: "防護マット", "rink-boards": "フェンス", mound: "土の台", azuchi: "安土", targets: "的", stands: "スタンド", "wall-seats": "壁の上の観客席", roof: "屋根", gallery: "見学席", dais: "上座の台", banner: "垂れ幕", shrine: "神棚", "mat-edge": "畳の厚み", surrounds: "競技エリアのフェンス", officials: "審判席と記録台", judges: "審判員席の名札", deck: "台", "cage-fence": "金網", "cage-posts": "パッド付き支柱", "ring-ropes": "ロープ", "ring-posts": "コーナーポスト", "ribbon-boards": "LEDリボンビジョン", "hung-board": "センターハングビジョン(吊り下げ式スコアボード)" },
1017
+ parts: { scoreboard: "スコアボードの下端", net: "ネット", antennae: "アンテナ", goalposts: "ゴールポスト", netposts: "支柱", hoop: "バスケットゴール", goal: "ゴール", boards: "ボードとガラス", stumps: "ウィケットのスタンプ", pylons: "パイロン", flags: "コーナーフラッグ", fence: "外野フェンス", backstop: "バックネット", "foul-poles": "ファウルポール", "table-legs": "天板", walls: "壁", "starting-blocks": "スタート台", "backstroke-flags": "背泳ぎ用フラッグ", "false-start-rope": "フォルススタートロープ", springboards: "飛板", platforms: "固定台", "judges-stand": "ジャッジ台", hurdles: "ハードル", steeplechase: "障害物", "high-jump": "走高跳の支柱とバー", "pole-vault": "棒高跳の支柱とバー", cage: "円盤投・ハンマー投の囲い", padding: "防護マット", "rink-boards": "フェンス", mound: "土の台", azuchi: "安土", targets: "的", stands: "スタンド", "wall-seats": "壁の上の観客席", roof: "屋根", gallery: "見学席", dais: "上座の台", banner: "垂れ幕", shrine: "神棚", "mat-edge": "畳の厚み", surrounds: "競技エリアのフェンス", officials: "審判席と記録台", judges: "審判員席の名札", deck: "台", "cage-fence": "金網", "cage-posts": "パッド付き支柱", "ring-ropes": "ロープ", "ring-posts": "コーナーポスト", "ribbon-boards": "LEDリボンビジョン", "hung-board": "センターハングビジョン(吊り下げ式スコアボード)", blocks: "周辺の建物" },
1018
1018
  },
1019
1019
  rules: {
1020
1020
  "defensive-zone": "ディフェンディングゾーン",
@@ -155,6 +155,11 @@ export interface Surface {
155
155
  };
156
156
  /** For a surface with a crowd: the margin it was drawn with, which holds the stands. */
157
157
  crowdMargin?: number;
158
+ /** A real stadium of more than one configuration laid out in the venue's own frame: where the middle of its field is in the surface's frame (the surface is shifted to lie there), and the ground, building, stands and crowd under it, in the venue's frame. */
159
+ venue?: {
160
+ shift: [number, number];
161
+ under: string;
162
+ };
158
163
  /** Where the parts a situation is drawn against are: a rink's lines and nets, a diamond's bases, a gridiron's goal lines. */
159
164
  anchors?: SituationAnchors;
160
165
  }
@@ -2,6 +2,7 @@ import { type TurnDirection, type TurnPreset, type TurnView } from "./turn.camer
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";
5
+ import type { OutsideLevel } from "./outside.paint.ts";
5
6
  /** What `mountKyogijoTurn` gives back. `O` is what `update` takes: the options of a drawing, or (for `mountMultiTurn`) of a tournament layout. */
6
7
  export interface KyogijoTurn<O = SurfaceOptions | ScoreOptions> {
7
8
  /** `3d`, or `flat` where the browser cannot draw the 3D view (or there is nothing to turn): the flat drawing is shown and the rest of this does nothing. */
@@ -33,6 +34,15 @@ export interface KyogijoTurn<O = SurfaceOptions | ScoreOptions> {
33
34
  setDirection(direction: TurnDirection): void;
34
35
  /** Start the auto-turn if it is stopped and stop it if it is running; it gives whether it is running now. */
35
36
  togglePlay(): boolean;
37
+ /** For a view with a crowd outside (a district asked for one): show another level at once, without drawing the view again, so that the turn goes on. */
38
+ setOutside?(level: OutsideLevel): void;
39
+ /** What the crowd outside's last paint cost, in milliseconds, how many people it drew and how (`dots` or `density`), or `undefined` where there is no crowd or it has not been drawn. */
40
+ outsideCost?(): {
41
+ ms: number;
42
+ people: number;
43
+ mode: string;
44
+ pixels: number;
45
+ } | undefined;
36
46
  /** The flat drawing that is the floor, or `null` after `destroy`. */
37
47
  svg(): SVGSVGElement | null;
38
48
  /** Stop listening and empty the container. */
package/dist/turn.dom.js CHANGED
@@ -9,6 +9,7 @@ import { controlsMarkup, layoutOf, turnMarkupOf, worldMarkup } from "./turn.mark
9
9
  import { KYOGIJO_TURN_STYLE } from "./turn.style.js";
10
10
  import { esc } from "./format.js";
11
11
  import { turnBoardMarkup } from "./overlay.js";
12
+ import { attachOutside3d } from "./outside.dom.js";
12
13
  /** True where the page can draw a plane in 3D by one `matrix3d`: that is, nearly everywhere with a stylesheet. */
13
14
  export function turnSupported(win = typeof window === "undefined" ? undefined : window) {
14
15
  const css = win?.CSS;
@@ -109,6 +110,14 @@ export function mountTurn(host, options, settings, build) {
109
110
  faded = elements.map((one) => one.style.opacity);
110
111
  };
111
112
  gather();
113
+ // The crowd outside, a canvas on the floor (the floor's matrix carries it), painted again only when its level, its detail or its colour changes.
114
+ let outside;
115
+ const crowd = () => {
116
+ outside?.destroy();
117
+ const floor = stage.querySelector(".ky-turn-floor");
118
+ outside = model.outside === undefined || floor === null ? undefined : attachOutside3d(floor, model.outside);
119
+ };
120
+ crowd();
112
121
  const buttons = () => root.querySelectorAll("button[data-preset]");
113
122
  /** The Play button says what it is and what it does: pressed while the view turns. */
114
123
  const syncPlay = () => {
@@ -127,6 +136,7 @@ export function mountTurn(host, options, settings, build) {
127
136
  const width = stage.clientWidth || 960;
128
137
  const height = stage.clientHeight || 600;
129
138
  const layout = layoutOf(model, now, width, height, settings.lens);
139
+ outside?.zoom(now.zoom);
130
140
  elements.forEach((one, i) => {
131
141
  if (written[i] !== layout.transforms[i])
132
142
  one.style.transform = written[i] = layout.transforms[i];
@@ -477,6 +487,7 @@ export function mountTurn(host, options, settings, build) {
477
487
  world.setAttribute("data-theme", forced);
478
488
  }
479
489
  gather();
490
+ crowd();
480
491
  // The board is a layer of its own over the view, after the scene: it is written again, and the view's turn is not touched.
481
492
  stage.querySelector(".ky-turn-board")?.remove();
482
493
  if (model.board !== undefined)
@@ -517,11 +528,21 @@ export function mountTurn(host, options, settings, build) {
517
528
  setPlaying(!playing);
518
529
  return playing;
519
530
  },
531
+ setOutside(level) {
532
+ if (!alive)
533
+ return;
534
+ outside?.setLevel(level);
535
+ },
536
+ outsideCost() {
537
+ const last = outside?.last;
538
+ return last === undefined ? undefined : { ms: last.ms, people: last.stats.people, mode: last.stats.mode, pixels: last.pixels };
539
+ },
520
540
  svg: () => (alive ? root.querySelector(".ky-turn-floor svg.kyogijo") : null),
521
541
  destroy() {
522
542
  if (!alive)
523
543
  return;
524
544
  alive = false;
545
+ outside?.destroy();
525
546
  stop();
526
547
  stopTick();
527
548
  seen?.disconnect();
@@ -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" | "apron" | "bench" | "floor" | "ribbon" | "hung-board";
4
+ export type PartKind = "mast" | "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" | "ribbon" | "hung-board" | "block";
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`. */
@@ -35,14 +35,21 @@ export interface Plane {
35
35
  fadeTo?: number;
36
36
  /** How low the eye must be for a `shy` plane to begin to give way: the share (of the plane's side being towards the eye, times the sine of the pitch) at which the fade starts; 0.1 when missing. A low barrier takes a high one, so that it stays whole until the eye is nearly level with it. */
37
37
  fadeAt?: number;
38
+ /**
39
+ * A `shy` plane that gives way by the side the eye is on and not by how low it is: every wall on the eye's side of what it surrounds fades the same, at any tilt, and one on the far side stays whole. A see-through building's walls
40
+ * (a raised room is seen through every wall between it and the eye, at a low angle and from straight above alike).
41
+ */
42
+ fadeSide?: boolean;
38
43
  /**
39
44
  * The building (a `TurnModel.groups` index) a plane belongs to, in a scene of several that stand apart (a district's): the view draws one building's planes all before another's, by which side of the line between
40
45
  * them the eye is on, and the planes of one building among themselves by depth.
41
46
  */
42
47
  group?: number;
43
48
  }
49
+ /** The `group` of a plane that belongs to no building of its scene but stands apart from all of them: a district's surrounding blocks. The view stacks these among themselves by depth and then among the buildings' planes by depth, where the buildings' are stacked building by building. */
50
+ export declare const APART = -1;
44
51
  /** What stands on the surface as a thing a reader would name: a net, a basket, a goal, a pair of posts. */
45
- export type UprightKind = "scoreboard" | "net" | "antennae" | "goalposts" | "netposts" | "hoop" | "goal" | "boards" | "stumps" | "pylons" | "flags" | "fence" | "backstop" | "foul-poles" | "table-legs" | "walls" | "starting-blocks" | "backstroke-flags" | "false-start-rope" | "springboards" | "platforms" | "judges-stand" | "hurdles" | "steeplechase" | "high-jump" | "pole-vault" | "cage" | "padding" | "rink-boards" | "mound" | "azuchi" | "targets" | "stands" | "wall-seats" | "roof" | "gallery" | "dais" | "banner" | "shrine" | "mat-edge" | "surrounds" | "officials" | "judges" | "deck" | "cage-fence" | "cage-posts" | "ring-ropes" | "ring-posts" | "ribbon-boards" | "hung-board";
52
+ export type UprightKind = "scoreboard" | "net" | "antennae" | "goalposts" | "netposts" | "hoop" | "goal" | "boards" | "stumps" | "pylons" | "flags" | "fence" | "backstop" | "foul-poles" | "table-legs" | "walls" | "starting-blocks" | "backstroke-flags" | "false-start-rope" | "springboards" | "platforms" | "judges-stand" | "hurdles" | "steeplechase" | "high-jump" | "pole-vault" | "cage" | "padding" | "rink-boards" | "mound" | "azuchi" | "targets" | "stands" | "wall-seats" | "roof" | "gallery" | "dais" | "banner" | "shrine" | "mat-edge" | "surrounds" | "officials" | "judges" | "deck" | "cage-fence" | "cage-posts" | "ring-ropes" | "ring-posts" | "ribbon-boards" | "hung-board" | "blocks";
46
53
  /** A thing that stands on the surface: where, how high, and the planes it is made of. */
47
54
  export interface Upright {
48
55
  id: string;
@@ -102,7 +109,7 @@ export declare function shy(p: Plane, centre: readonly [number, number]): Plane;
102
109
  * How much of a wall is left (1 whole, down to 0.08) at a view: none is cut away unless the wall is on the eye's side of the surface, and then more the lower the eye is, because a wall seen
103
110
  * from straight above hides nothing. `yaw` and `pitch` are the view's, in degrees.
104
111
  */
105
- export declare function fadeOf(p: Pick<Plane, "shy" | "fadeTo" | "fadeAt">, yaw: number, pitch: number): number;
112
+ export declare function fadeOf(p: Pick<Plane, "shy" | "fadeTo" | "fadeAt" | "fadeSide">, yaw: number, pitch: number): number;
106
113
  /** A rectangle on the surface, in feet: its corners. */
107
114
  export type Rect = {
108
115
  x0: number;
package/dist/turn.kit.js CHANGED
@@ -4,6 +4,8 @@
4
4
  //
5
5
  // The frame is the drawing's own: x along the surface and y across it, both in feet, as the SVG has them (y grows downwards on the page), and z the height
6
6
  // above the surface, in feet, growing towards the eye of someone looking straight down.
7
+ /** The `group` of a plane that belongs to no building of its scene but stands apart from all of them: a district's surrounding blocks. The view stacks these among themselves by depth and then among the buildings' planes by depth, where the buildings' are stacked building by building. */
8
+ export const APART = -1;
7
9
  /** The four numbers a plane's SVG needs: its length along `u` and along `v`, in feet. */
8
10
  export const size = (p) => [Math.hypot(...p.u), Math.hypot(...p.v)];
9
11
  export const add = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
@@ -108,6 +110,11 @@ export function fadeOf(p, yaw, pitch) {
108
110
  const a = (yaw * Math.PI) / 180;
109
111
  // The horizontal direction from the surface to the eye (the same sum as the depth of a point).
110
112
  const toward = p.shy[0] * Math.sin(a) + p.shy[1] * Math.cos(a);
113
+ // By side: whole on the far side, a short ramp across the walls edge-on to the eye, and the same fade on every wall of the near side, whatever the tilt.
114
+ if (p.fadeSide === true) {
115
+ const t = Math.min(1, Math.max(0, (toward + 0.05) / 0.15));
116
+ return Math.round((1 - (1 - (p.fadeTo ?? 0.08)) * t) * 1000) / 1000;
117
+ }
111
118
  const hides = toward * Math.sin((pitch * Math.PI) / 180);
112
119
  const from = p.fadeAt ?? 0.1;
113
120
  const t = Math.min(1, Math.max(0, (hides - from) / 0.25));
@@ -3,7 +3,7 @@
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
5
  import { TURN_PRESET_NAMES, cameraMatrix, depthOf, eyeDistance, fit, matrix3d, multiply, presetOf, turnDirection } from "./turn.camera.js";
6
- import { cross, facing, fadeOf, n, size, unit } from "./turn.kit.js";
6
+ import { APART, 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";
9
9
  import { esc } from "./format.js";
@@ -71,7 +71,12 @@ export function layoutOf(model, view, width, height, lens) {
71
71
  const order = [...planes.map((p, i) => ({ i: i + 1, ground: p.kind === "ground", depth: depth(p), group: p.group })), ...(raised ? [{ i: 0, ground: false, depth: depth(floor), group: undefined }] : [])].filter((one) => !one.ground);
72
72
  // Buildings that stand apart are drawn one after another, the far one first; a plane of none (the floor) goes by its depth alone.
73
73
  const rank = model.groups === undefined ? undefined : groupRanks(model.groups, view.yaw);
74
- order.sort((a, b) => (rank === undefined || a.group === undefined || b.group === undefined ? 0 : rank[a.group] - rank[b.group]) || a.depth - b.depth);
74
+ // The planes of the blocks that stand round the buildings (`APART`) are stacked by depth among themselves and then woven into the buildings' by depth, each list keeping its own order.
75
+ const apart = order.filter((one) => one.group === APART).sort((a, b) => a.depth - b.depth);
76
+ const own = order.filter((one) => one.group !== APART).sort((a, b) => (rank === undefined || a.group === undefined || b.group === undefined ? 0 : rank[a.group] - rank[b.group]) || a.depth - b.depth);
77
+ order.length = 0;
78
+ for (let i = 0, j = 0; i < own.length || j < apart.length;)
79
+ order.push(j >= apart.length || (i < own.length && own[i].depth <= apart[j].depth) ? own[i++] : apart[j++]);
75
80
  const stack = new Array(model.planes.length + 1).fill(0);
76
81
  stack[0] = 1;
77
82
  order.forEach((one, rank) => {
@@ -5,6 +5,7 @@ import { type UprightOptions, type Uprights } from "./uprights.ts";
5
5
  import { type ResolvedBoard } from "./overlay.ts";
6
6
  import type { VenueSettings } from "./venue.ts";
7
7
  import type { Box3 } from "./turn.camera.ts";
8
+ import type { OutsideSpec } from "./outside.dom.ts";
8
9
  import type { Language, LightingOptions, LightingPreset, ScoreOptions, SurfaceOptions } from "./types.ts";
9
10
  /** What can be asked of a 3D view. */
10
11
  export interface TurnSettings {
@@ -112,6 +113,8 @@ export interface TurnModel {
112
113
  }[];
113
114
  /** The board the options ask for (`board`), laid over the view: a layer of its own, never a plane of the scene. */
114
115
  board?: ResolvedBoard;
116
+ /** The crowd outside the venues of a district, asked for (`DistrictOptions.outside`): a canvas on the floor that the mount paints (`outside.dom.ts`), never a plane of the scene. */
117
+ outside?: OutsideSpec;
115
118
  }
116
119
  /** The lighting a venue is built with, as a model's field: the venue's own setting, else the crowd's, and none where there is no venue. */
117
120
  export declare const lightingOf: (venue: {
@@ -5,9 +5,9 @@ import { KYOGIJO_TURN_BOARD_STYLE } from "./overlay.js";
5
5
  * defaults are in `:where()`, which has no weight, so a page's own rule always wins; the colours are custom properties (`--ky-up-*`) a page can set.
6
6
  */
7
7
  export const KYOGIJO_TURN_STYLE = `
8
- :where(.ky-turn){--ky-left:#2f6db0;--ky-right:#b5452c;--ky-cr-home:#b5452c;--ky-cr-away:#2f6db0;--ky-cr-bed:#4a525d;--ky-cr-empty:#7d8794;--ky-cr-gap:#14181d;--ky-cr-conc:#8c929b;--ky-cr-dim:1;--ky-up-roof:rgba(214,218,212,.6);--ky-up-roofline:rgba(40,46,42,.55);--ky-up-fascia:#9aa09a;--ky-up-ivy:#5b8a45;--ky-up-door:#8a6a46;--ky-up-window:rgba(150,200,240,.55);--ky-up-sash:#f2f0e6;--ky-up-banner:#26211d;--ky-up-bannerline:#c8b88a;--ky-up-room:#efe8d6;--ky-up-roomback:#d6cdb9;--ky-up-ceiling:#dedcd5;--ky-up-railw:#fbfaf6;--ky-up-wainline:#6b4a2a;--ky-up-riser:#3f342c;--ky-up-slab:#d8cfba;--ky-up-slabedge:#c2b69c;--ky-up-seatc:#46342a;--ky-up-seatb:#2d211b;--ky-up-dais:#a0683a;--ky-up-dais-side:#6a4326;--ky-up-shrine:#c9b48f;--ky-up-shrine-side:#8f7a55;--ky-up-gal:#4d4037;--ky-up-aisle:#dcc196;--ky-up-panel:rgba(95,58,22,.3);--ky-up-alcove:#b58a52;--ky-up-purple:#5b2a6e;--ky-up-clock:#ffffff;--ky-up-rail:#3a2c1f;--ky-up-matedge:#2a2a26;--ky-up-sbframe:#1a1f1c;--ky-up-sbedge:#5b645e;--ky-up-sbline:#59615b;--ky-up-pole:#e9e7de;--ky-up-white:#f6f4ec;--ky-up-yellow:#f2c230;--ky-up-red:#c8372d;--ky-up-orange:#f26a1b;--ky-up-wood:#e3d4b4;--ky-up-leg:#474e49;--ky-up-pad:#2f6db0;--ky-up-cagepad:#2a2d32;--ky-up-table:#18243f;--ky-up-foot:rgba(0,0,0,.25);--ky-up-ground:#d9d5c7;--ky-up-wall:#2f4a36;--ky-up-clay:#a8703f;--ky-up-cap:#1d2a22;--ky-up-board:#ece9df;--ky-up-glass:rgba(138,190,232,.26);--ky-up-backboard:rgba(255,255,255,.88);--ky-up-outline:#f2f0e6;--ky-up-rim:#e8591a;--ky-up-mesh:rgba(24,30,27,.7);--ky-up-netfill:rgba(255,255,255,.2);--ky-up-tape:#fbfaf4;--ky-turn-bg:transparent;--ky-turn-ink:#1f2320;--ky-turn-rule:rgba(31,35,32,.22);--ky-turn-chip:rgba(255,255,255,.86);--ky-turn-chip-on:#1f2320;--ky-turn-chip-on-ink:#fff;${JUDGING_LIGHT}}
9
- @media (prefers-color-scheme:dark){:where(html:not([data-theme=light]) .ky-turn:not([data-theme=light])){--ky-cr-bed:#242a32;--ky-cr-empty:#4a535f;--ky-cr-gap:#050709;--ky-cr-conc:#2a3038;--ky-cr-dim:.82;--ky-up-roof:rgba(60,66,62,.6);--ky-up-roofline:rgba(200,206,200,.5);--ky-up-fascia:#505650;--ky-up-ivy:#3d6633;--ky-up-door:#6a4f33;--ky-up-gal:#2d2621;--ky-up-aisle:#8f7752;--ky-up-alcove:#7d5c36;--ky-up-purple:#43204f;--ky-up-clock:#cfcbc0;--ky-up-room:#6d675a;--ky-up-roomback:#58534a;--ky-up-ceiling:#8c8a82;--ky-up-railw:#d9d6cc;--ky-up-riser:#251f1b;--ky-up-slab:#6b6455;--ky-up-slabedge:#585143;--ky-up-seatc:#2f231c;--ky-up-seatb:#1c1411;--ky-up-dais:#7d5230;--ky-up-dais-side:#52361f;--ky-up-sbframe:#0c0f0d;--ky-up-sbedge:#7a847d;--ky-up-sbline:#8a928c;--ky-up-pole:#d6d4ca;--ky-up-white:#e8e6dc;--ky-up-wood:#a89873;--ky-up-leg:#8a928c;--ky-up-ground:#242823;--ky-up-cagepad:#8f969f;--ky-up-wall:#4d6b55;--ky-up-clay:#7d5231;--ky-up-cap:#c9d2cc;--ky-up-board:#8d928b;--ky-up-glass:rgba(150,200,240,.2);--ky-up-backboard:rgba(255,255,255,.34);--ky-up-outline:#e7e4d8;--ky-up-mesh:rgba(236,236,226,.62);--ky-up-netfill:rgba(255,255,255,.08);--ky-up-tape:#efede2;--ky-turn-ink:#ece8dc;--ky-turn-rule:rgba(236,232,220,.28);--ky-turn-chip:rgba(24,28,25,.82);--ky-turn-chip-on:#ece8dc;--ky-turn-chip-on-ink:#1f2320;${JUDGING_DARK}}}
10
- :where(html[data-theme=dark] .ky-turn,.ky-turn[data-theme=dark]){--ky-cr-bed:#242a32;--ky-cr-empty:#4a535f;--ky-cr-gap:#050709;--ky-cr-conc:#2a3038;--ky-cr-dim:.82;--ky-up-roof:rgba(60,66,62,.6);--ky-up-roofline:rgba(200,206,200,.5);--ky-up-fascia:#505650;--ky-up-ivy:#3d6633;--ky-up-door:#6a4f33;--ky-up-gal:#2d2621;--ky-up-aisle:#8f7752;--ky-up-alcove:#7d5c36;--ky-up-purple:#43204f;--ky-up-clock:#cfcbc0;--ky-up-room:#6d675a;--ky-up-roomback:#58534a;--ky-up-ceiling:#8c8a82;--ky-up-railw:#d9d6cc;--ky-up-riser:#251f1b;--ky-up-slab:#6b6455;--ky-up-slabedge:#585143;--ky-up-seatc:#2f231c;--ky-up-seatb:#1c1411;--ky-up-dais:#7d5230;--ky-up-dais-side:#52361f;--ky-up-sbframe:#0c0f0d;--ky-up-sbedge:#7a847d;--ky-up-sbline:#8a928c;--ky-up-pole:#d6d4ca;--ky-up-white:#e8e6dc;--ky-up-wood:#a89873;--ky-up-leg:#8a928c;--ky-up-ground:#242823;--ky-up-cagepad:#8f969f;--ky-up-wall:#4d6b55;--ky-up-clay:#7d5231;--ky-up-cap:#c9d2cc;--ky-up-board:#8d928b;--ky-up-glass:rgba(150,200,240,.2);--ky-up-backboard:rgba(255,255,255,.34);--ky-up-outline:#e7e4d8;--ky-up-mesh:rgba(236,236,226,.62);--ky-up-netfill:rgba(255,255,255,.08);--ky-up-tape:#efede2;--ky-turn-ink:#ece8dc;--ky-turn-rule:rgba(236,232,220,.28);--ky-turn-chip:rgba(24,28,25,.82);--ky-turn-chip-on:#ece8dc;--ky-turn-chip-on-ink:#1f2320;${JUDGING_DARK}}
8
+ :where(.ky-turn){--ky-left:#2f6db0;--ky-right:#b5452c;--ky-cr-home:#b5452c;--ky-cr-away:#2f6db0;--ky-cr-bed:#4a525d;--ky-cr-empty:#7d8794;--ky-cr-gap:#14181d;--ky-cr-conc:#8c929b;--ky-cr-dim:1;--ky-up-roof:rgba(214,218,212,.6);--ky-up-roofline:rgba(40,46,42,.55);--ky-up-fascia:#9aa09a;--ky-up-ivy:#5b8a45;--ky-up-door:#8a6a46;--ky-up-window:rgba(150,200,240,.55);--ky-up-sash:#f2f0e6;--ky-up-banner:#26211d;--ky-up-bannerline:#c8b88a;--ky-up-room:#efe8d6;--ky-up-roomback:#d6cdb9;--ky-up-ceiling:#dedcd5;--ky-up-railw:#fbfaf6;--ky-up-wainline:#6b4a2a;--ky-up-riser:#3f342c;--ky-up-slab:#d8cfba;--ky-up-slabedge:#c2b69c;--ky-up-seatc:#46342a;--ky-up-seatb:#2d211b;--ky-up-dais:#a0683a;--ky-up-dais-side:#6a4326;--ky-up-shrine:#c9b48f;--ky-up-shrine-side:#8f7a55;--ky-up-gal:#4d4037;--ky-up-aisle:#dcc196;--ky-up-panel:rgba(95,58,22,.3);--ky-up-alcove:#b58a52;--ky-up-purple:#5b2a6e;--ky-up-clock:#ffffff;--ky-up-rail:#3a2c1f;--ky-up-matedge:#2a2a26;--ky-up-sbframe:#1a1f1c;--ky-up-sbedge:#5b645e;--ky-up-sbline:#59615b;--ky-up-pole:#e9e7de;--ky-up-white:#f6f4ec;--ky-up-yellow:#f2c230;--ky-up-red:#c8372d;--ky-up-orange:#f26a1b;--ky-up-wood:#e3d4b4;--ky-up-leg:#474e49;--ky-up-pad:#2f6db0;--ky-up-cagepad:#2a2d32;--ky-up-table:#18243f;--ky-up-foot:rgba(0,0,0,.25);--ky-up-ground:#d9d5c7;--ky-up-wall:#2f4a36;--ky-up-clay:#a8703f;--ky-up-cap:#1d2a22;--ky-up-board:#ece9df;--ky-up-glass:rgba(138,190,232,.26);--ky-up-backboard:rgba(255,255,255,.88);--ky-up-outline:#f2f0e6;--ky-up-rim:#e8591a;--ky-up-mesh:rgba(24,30,27,.7);--ky-up-netfill:rgba(255,255,255,.2);--ky-up-tape:#fbfaf4;--ky-turn-bg:transparent;--ky-turn-ink:#1f2320;--ky-turn-rule:rgba(31,35,32,.22);--ky-turn-chip:rgba(255,255,255,.86);--ky-turn-chip-on:#1f2320;--ky-turn-chip-on-ink:#fff;${JUDGING_LIGHT};--ky-up-blk-a:#e9ece4;--ky-up-blk-b:#c9cdc3;--ky-up-blk-c:#a4aa9f;--ky-up-blk-roof:#f4f5f0;--ky-up-blk-line:rgba(40,46,42,.38)}
9
+ @media (prefers-color-scheme:dark){:where(html:not([data-theme=light]) .ky-turn:not([data-theme=light])){--ky-cr-bed:#242a32;--ky-cr-empty:#4a535f;--ky-cr-gap:#050709;--ky-cr-conc:#2a3038;--ky-cr-dim:.82;--ky-up-roof:rgba(60,66,62,.6);--ky-up-roofline:rgba(200,206,200,.5);--ky-up-fascia:#505650;--ky-up-ivy:#3d6633;--ky-up-door:#6a4f33;--ky-up-gal:#2d2621;--ky-up-aisle:#8f7752;--ky-up-alcove:#7d5c36;--ky-up-purple:#43204f;--ky-up-clock:#cfcbc0;--ky-up-room:#6d675a;--ky-up-roomback:#58534a;--ky-up-ceiling:#8c8a82;--ky-up-railw:#d9d6cc;--ky-up-riser:#251f1b;--ky-up-slab:#6b6455;--ky-up-slabedge:#585143;--ky-up-seatc:#2f231c;--ky-up-seatb:#1c1411;--ky-up-dais:#7d5230;--ky-up-dais-side:#52361f;--ky-up-sbframe:#0c0f0d;--ky-up-sbedge:#7a847d;--ky-up-sbline:#8a928c;--ky-up-pole:#d6d4ca;--ky-up-white:#e8e6dc;--ky-up-wood:#a89873;--ky-up-leg:#8a928c;--ky-up-ground:#242823;--ky-up-cagepad:#8f969f;--ky-up-wall:#4d6b55;--ky-up-clay:#7d5231;--ky-up-cap:#c9d2cc;--ky-up-board:#8d928b;--ky-up-glass:rgba(150,200,240,.2);--ky-up-backboard:rgba(255,255,255,.34);--ky-up-outline:#e7e4d8;--ky-up-mesh:rgba(236,236,226,.62);--ky-up-netfill:rgba(255,255,255,.08);--ky-up-tape:#efede2;--ky-turn-ink:#ece8dc;--ky-turn-rule:rgba(236,232,220,.28);--ky-turn-chip:rgba(24,28,25,.82);--ky-turn-chip-on:#ece8dc;--ky-turn-chip-on-ink:#1f2320;${JUDGING_DARK};--ky-up-blk-a:#59626b;--ky-up-blk-b:#444b53;--ky-up-blk-c:#30363c;--ky-up-blk-roof:#69727b;--ky-up-blk-line:rgba(220,226,220,.3)}}
10
+ :where(html[data-theme=dark] .ky-turn,.ky-turn[data-theme=dark]){--ky-cr-bed:#242a32;--ky-cr-empty:#4a535f;--ky-cr-gap:#050709;--ky-cr-conc:#2a3038;--ky-cr-dim:.82;--ky-up-roof:rgba(60,66,62,.6);--ky-up-roofline:rgba(200,206,200,.5);--ky-up-fascia:#505650;--ky-up-ivy:#3d6633;--ky-up-door:#6a4f33;--ky-up-gal:#2d2621;--ky-up-aisle:#8f7752;--ky-up-alcove:#7d5c36;--ky-up-purple:#43204f;--ky-up-clock:#cfcbc0;--ky-up-room:#6d675a;--ky-up-roomback:#58534a;--ky-up-ceiling:#8c8a82;--ky-up-railw:#d9d6cc;--ky-up-riser:#251f1b;--ky-up-slab:#6b6455;--ky-up-slabedge:#585143;--ky-up-seatc:#2f231c;--ky-up-seatb:#1c1411;--ky-up-dais:#7d5230;--ky-up-dais-side:#52361f;--ky-up-sbframe:#0c0f0d;--ky-up-sbedge:#7a847d;--ky-up-sbline:#8a928c;--ky-up-pole:#d6d4ca;--ky-up-white:#e8e6dc;--ky-up-wood:#a89873;--ky-up-leg:#8a928c;--ky-up-ground:#242823;--ky-up-cagepad:#8f969f;--ky-up-wall:#4d6b55;--ky-up-clay:#7d5231;--ky-up-cap:#c9d2cc;--ky-up-board:#8d928b;--ky-up-glass:rgba(150,200,240,.2);--ky-up-backboard:rgba(255,255,255,.34);--ky-up-outline:#e7e4d8;--ky-up-mesh:rgba(236,236,226,.62);--ky-up-netfill:rgba(255,255,255,.08);--ky-up-tape:#efede2;--ky-turn-ink:#ece8dc;--ky-turn-rule:rgba(236,232,220,.28);--ky-turn-chip:rgba(24,28,25,.82);--ky-turn-chip-on:#ece8dc;--ky-turn-chip-on-ink:#1f2320;${JUDGING_DARK};--ky-up-blk-a:#59626b;--ky-up-blk-b:#444b53;--ky-up-blk-c:#30363c;--ky-up-blk-roof:#69727b;--ky-up-blk-line:rgba(220,226,220,.3)}
11
11
  .ky-turn{position:relative;display:block;width:100%;color:var(--ky-turn-ink);font-family:var(--ky-font,system-ui,-apple-system,"Segoe UI","Hiragino Sans","Noto Sans JP",sans-serif);background:var(--ky-turn-bg)}
12
12
  .ky-turn-view{position:relative;isolation:isolate;width:100%;aspect-ratio:var(--ky-turn-ratio,16/10);min-height:260px;overflow:hidden;touch-action:pan-y;cursor:grab;user-select:none;-webkit-user-select:none;-webkit-tap-highlight-color:transparent;border-radius:12px;outline-offset:2px}
13
13
  .ky-turn[data-touch=turn] .ky-turn-view{touch-action:none}
@@ -68,9 +68,20 @@ export const KYOGIJO_TURN_STYLE = `
68
68
  .ky-turn .u-fascia{fill:var(--ky-up-fascia)}
69
69
  .ky-turn .u-shell{fill:var(--ky-up-fascia);stroke:var(--ky-up-roofline);stroke-width:.6}
70
70
  .ky-turn .u-ivy{fill:var(--ky-up-ivy)}
71
+ /* The buildings round a district's venues (context.ts): three tones of wall by the way each faces, and a lighter roof. */
72
+ .ky-turn .u-block{stroke:var(--ky-up-blk-line);stroke-width:.5}
73
+ .ky-turn .u-block-a{fill:var(--ky-up-blk-a)}
74
+ .ky-turn .u-block-b{fill:var(--ky-up-blk-b)}
75
+ .ky-turn .u-block-c{fill:var(--ky-up-blk-c)}
76
+ .ky-turn .u-block-roof{fill:var(--ky-up-blk-roof);stroke:var(--ky-up-blk-line);stroke-width:.5;stroke-linejoin:round}
71
77
  .ky-turn .u-roof{fill:var(--ky-up-roof);stroke:var(--ky-up-roofline);stroke-width:.8;stroke-linejoin:round}
72
78
  .ky-turn .u-signplate{fill:var(--ky-turn-bg);fill-opacity:.9;stroke:var(--ky-turn-rule);stroke-width:.5}
73
79
  .ky-turn .u-sign{fill:var(--ky-turn-ink);font-family:var(--ky-font,system-ui,-apple-system,"Segoe UI","Hiragino Sans","Noto Sans JP",sans-serif);font-weight:600}
80
+ .ky-turn .u-mast{fill:var(--ky-up-fascia);stroke:var(--ky-up-roofline);stroke-width:.4}
81
+ .ky-turn .u-skylight{fill:var(--ky-up-skylight,#a9c8da);fill-opacity:.9;stroke:var(--ky-up-roofline);stroke-width:.5}
82
+ .ky-turn .u-podium{fill:#000;fill-opacity:.16}
83
+ .ky-turn .u-ledge{fill:var(--ky-up-roofline)}
84
+ .ky-turn .u-glazing{fill:#0b1a24;fill-opacity:.32}
74
85
  .ky-turn .u-cable{fill:none;stroke:var(--ky-up-roofline);stroke-width:1}
75
86
  .ky-turn .u-fabric{fill:var(--ky-up-roof);fill-opacity:.55;stroke:none}
76
87
  .ky-turn .u-pod{fill:var(--ky-up-fascia);stroke:var(--ky-up-roofline);stroke-width:.6}
@@ -120,6 +131,7 @@ export const KYOGIJO_TURN_STYLE = `
120
131
  .ky-turn .ky-cr-ta{fill:var(--ky-cr-away)}
121
132
  .ky-turn .ky-cr-tw{fill:#f2f0ea}
122
133
  .ky-turn .ky-cr-aisle{fill:none;stroke:var(--ky-cr-gap);stroke-opacity:.8}
134
+ .ky-turn .ky-cr-shut-veil{fill:var(--ky-cr-empty);fill-opacity:.42}
123
135
  .ky-turn .ky-cr-row{fill:none;stroke:var(--ky-cr-gap);stroke-opacity:.5;stroke-width:.5}
124
136
  .ky-turn .ky-bud-gseat{stroke:var(--ky-up-rail);stroke-opacity:.3}
125
137
  .ky-turn .u-door{fill:var(--ky-up-door)}
package/dist/uprights.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // stylesheet. `describeUprights` says the same in words, for a reader who cannot see it.
4
4
  import { courtFor, courtGeometry, isNetSport } from "./court.js";
5
5
  import { drawingId, feetFence, lengthUnit } from "./common.js";
6
- import { drawKyogijo, pitchOf } from "./draw.js";
6
+ import { crowdStands, drawKyogijo, pitchOf } from "./draw.js";
7
7
  import { rulesFor, footballIn } from "./eras.js";
8
8
  import { fieldPlan, isFieldSport } from "./fields.js";
9
9
  import { footballLength } from "./geometry.js";
@@ -126,6 +126,14 @@ export function uprightsFor(o, settings = {}) {
126
126
  thin = thinnest(sheet.length, sheet.width);
127
127
  area = { x0: 0, y0: 0, x1: sheet.length, y1: sheet.width };
128
128
  }
129
+ // A real stadium of more than one configuration is drawn in the venue's own frame (the middle of its field at the origin): what stands on the field is moved there with it, and the stands are already in it.
130
+ const shift = crowdStands(o)?.shift;
131
+ if (shift !== undefined) {
132
+ const [dx, dy] = shift;
133
+ items = items.map((item) => ({ ...item, at: [item.at[0] - dx, item.at[1] - dy], planes: item.planes.map((p) => ({ ...p, o: [p.o[0] - dx, p.o[1] - dy, p.o[2]] })) }));
134
+ if (area !== undefined)
135
+ area = { x0: area.x0 - dx, y0: area.y0 - dy, x1: area.x1 - dx, y1: area.y1 - dy };
136
+ }
129
137
  // The venue: the stands and the room, raised. A dojo's own walls are the room's, with their doors and windows.
130
138
  let defs = "";
131
139
  let reach = 0;
package/dist/venue.js CHANGED
@@ -50,7 +50,7 @@ export function standsVenueOf(stands, crowd, o, v, lift) {
50
50
  const density = densityOf(crowd, stands.seats, groundById(o.ground)?.capacity);
51
51
  const tifo = { home: stands.sectors.some((s) => s.kind === "tifo-home"), away: stands.sectors.some((s) => s.kind === "tifo-away") };
52
52
  const pid = `${v.id}-v`;
53
- const defs = fine ? patterns(pid, crowd, density, tifo.home || tifo.away ? tifo : undefined, stands.real !== undefined, true) : "";
53
+ const defs = fine ? patterns(pid, crowd, density, tifo.home || tifo.away ? tifo : undefined, stands.real !== undefined, true, stands.rings.some((r) => r.closed === true) || stands.sectors.some((s) => s.kind === "closed")) : "";
54
54
  const centre = [stands.box.x + stands.box.w / 2, stands.box.y + stands.box.h / 2];
55
55
  const covers = stands.covers;
56
56
  const inCover = (p) => covers.some(([x0, y0, x1, y1]) => p[0] >= x0 && p[0] <= x1 && p[1] >= y0 && p[1] <= y1);
@@ -62,7 +62,11 @@ export function standsVenueOf(stands, crowd, o, v, lift) {
62
62
  const paint = (ring, piece) => (corners, local) => {
63
63
  let svg = `<polygon class="u-bed" points="${pointList(corners)}"/>`;
64
64
  const people = ring.fill < 0.95 ? ` fill-opacity="${round(1 - Math.min(0.5, (1 - ring.fill) * 1.6))}"` : "";
65
- svg += fine ? `<polygon fill="url(#${pid}-cr-h)"${people} points="${pointList(corners)}"/>` : `<polygon class="u-blk" fill-opacity="${round(0.28 + 0.6 * density * ring.fill)}" points="${pointList(corners)}"/>`;
65
+ // Closed seats are the same seats, every one empty.
66
+ if (ring.closed === true)
67
+ svg += fine ? `<polygon fill="url(#${pid}-cr-e)" points="${pointList(corners)}"/><polygon class="ky-cr-shut-veil" points="${pointList(corners)}"/>` : `<polygon class="u-blk" fill-opacity="0.28" points="${pointList(corners)}"/>`;
68
+ else
69
+ svg += fine ? `<polygon fill="url(#${pid}-cr-h)"${people} points="${pointList(corners)}"/>` : `<polygon class="u-blk" fill-opacity="${round(0.28 + 0.6 * density * ring.fill)}" points="${pointList(corners)}"/>`;
66
70
  for (const s of stands.sectors) {
67
71
  if (s.ring !== ring.id)
68
72
  continue;
@@ -70,7 +74,9 @@ export function standsVenueOf(stands, crowd, o, v, lift) {
70
74
  if (part.length < 3 || Math.abs(areaOf(part)) < 0.05)
71
75
  continue;
72
76
  const points = pointList(part.map(local));
73
- if (s.kind === "away")
77
+ if (s.kind === "closed")
78
+ svg += fine ? `<polygon class="u-bed" points="${points}"/><polygon fill="url(#${pid}-cr-e)" points="${points}"/><polygon class="ky-cr-shut-veil" points="${points}"/>` : `<polygon class="u-blk" fill-opacity="0.28" points="${points}"/>`;
79
+ else if (s.kind === "away")
74
80
  svg += fine ? `<polygon fill="url(#${pid}-cr-a)" points="${points}"/>` : `<polygon class="u-blk-a" fill-opacity="${round(0.3 + 0.6 * density)}" points="${points}"/>`;
75
81
  else
76
82
  svg += fine ? `<polygon fill="url(#${pid}-cr-${s.kind === "tifo-home" ? "th" : "ta"})" points="${points}"/>` : `<polygon class="${s.kind === "tifo-home" ? "u-th" : "u-ta"}" points="${points}"/>`;