@johnmorrisdotca/kyogijo-stadiums 0.4.0 → 0.5.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 (244) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/NOTICE.md +4 -0
  3. package/README.md +71 -16
  4. package/dist/annotate.js +1 -1
  5. package/dist/athletics/describe.d.ts +8 -0
  6. package/dist/athletics/describe.js +41 -0
  7. package/dist/athletics/events.d.ts +17 -0
  8. package/dist/athletics/events.js +48 -0
  9. package/dist/athletics/facilities.d.ts +80 -0
  10. package/dist/athletics/facilities.js +176 -0
  11. package/dist/athletics/infield.d.ts +79 -0
  12. package/dist/athletics/infield.js +166 -0
  13. package/dist/athletics/layout.d.ts +13 -0
  14. package/dist/athletics/layout.js +98 -0
  15. package/dist/athletics/oval.d.ts +57 -0
  16. package/dist/athletics/oval.js +116 -0
  17. package/dist/athletics/plan.d.ts +97 -0
  18. package/dist/athletics/plan.js +355 -0
  19. package/dist/athletics/races.d.ts +102 -0
  20. package/dist/athletics/races.js +355 -0
  21. package/dist/athletics/steeplechase.d.ts +72 -0
  22. package/dist/athletics/steeplechase.js +196 -0
  23. package/dist/athletics/stencil.d.ts +13 -0
  24. package/dist/athletics/stencil.js +85 -0
  25. package/dist/athletics/track.d.ts +54 -0
  26. package/dist/athletics/track.js +105 -0
  27. package/dist/athletics/types.d.ts +77 -0
  28. package/dist/athletics/types.js +1 -0
  29. package/dist/athletics-entry.d.ts +17 -0
  30. package/dist/athletics-entry.js +11 -0
  31. package/dist/athletics.style.d.ts +16 -0
  32. package/dist/athletics.style.js +51 -0
  33. package/dist/atlas-entry.d.ts +4 -2
  34. package/dist/atlas-entry.js +3 -2
  35. package/dist/atlas.d.ts +83 -7
  36. package/dist/atlas.js +108 -8
  37. package/dist/charts-entry.d.ts +6 -1
  38. package/dist/charts-entry.js +3 -0
  39. package/dist/charts.athletics.d.ts +183 -0
  40. package/dist/charts.athletics.js +455 -0
  41. package/dist/charts.curling.heat.d.ts +24 -0
  42. package/dist/charts.curling.heat.js +116 -0
  43. package/dist/charts.curling.js +14 -3
  44. package/dist/charts.pool.d.ts +67 -0
  45. package/dist/charts.pool.js +76 -0
  46. package/dist/charts.skating.d.ts +204 -0
  47. package/dist/charts.skating.js +426 -0
  48. package/dist/charts.types.d.ts +53 -1
  49. package/dist/court.d.ts +85 -2
  50. package/dist/court.js +102 -7
  51. package/dist/court.more.d.ts +46 -0
  52. package/dist/court.more.js +219 -0
  53. package/dist/courts.style.d.ts +14 -2
  54. package/dist/courts.style.js +51 -4
  55. package/dist/crowd.d.ts +154 -0
  56. package/dist/crowd.draw.d.ts +17 -0
  57. package/dist/crowd.draw.js +198 -0
  58. package/dist/crowd.js +465 -0
  59. package/dist/crowd.park.d.ts +19 -0
  60. package/dist/crowd.park.js +153 -0
  61. package/dist/curling.stats.d.ts +271 -0
  62. package/dist/curling.stats.js +301 -0
  63. package/dist/draw.d.ts +16 -0
  64. package/dist/draw.js +318 -35
  65. package/dist/element-entry.d.ts +1 -1
  66. package/dist/element-entry.js +1 -1
  67. package/dist/element.d.ts +13 -2
  68. package/dist/element.js +77 -3
  69. package/dist/features.js +131 -0
  70. package/dist/fields.kit.d.ts +2 -2
  71. package/dist/fields.types.d.ts +18 -4
  72. package/dist/frames.providers.js +63 -8
  73. package/dist/geojson.js +3 -0
  74. package/dist/geometry-entry.d.ts +2 -1
  75. package/dist/geometry-entry.js +2 -1
  76. package/dist/geometry.d.ts +20 -0
  77. package/dist/geometry.js +78 -1
  78. package/dist/grounds.d.ts +4 -0
  79. package/dist/grounds.data.js +12 -0
  80. package/dist/hardwood.d.ts +31 -0
  81. package/dist/hardwood.js +157 -0
  82. package/dist/heat.js +4 -3
  83. package/dist/heat.types.d.ts +5 -0
  84. package/dist/heat.zones.d.ts +2 -2
  85. package/dist/heat.zones.js +154 -6
  86. package/dist/homeside.js +17 -0
  87. package/dist/index.d.ts +16 -2
  88. package/dist/index.js +10 -1
  89. package/dist/layers.d.ts +1 -1
  90. package/dist/layers.env.d.ts +3 -1
  91. package/dist/layers.env.js +4 -2
  92. package/dist/layers.js +4 -4
  93. package/dist/leagues/athletics.d.ts +63 -0
  94. package/dist/leagues/athletics.js +87 -0
  95. package/dist/leagues/courts.d.ts +269 -4
  96. package/dist/leagues/courts.js +155 -1
  97. package/dist/leagues/skating.d.ts +145 -0
  98. package/dist/leagues/skating.js +73 -0
  99. package/dist/leagues/sources.js +20 -0
  100. package/dist/leagues/types.d.ts +1 -1
  101. package/dist/leagues-entry.d.ts +4 -2
  102. package/dist/leagues-entry.js +2 -1
  103. package/dist/leagues.d.ts +14 -4
  104. package/dist/leagues.js +146 -5
  105. package/dist/look.d.ts +3 -3
  106. package/dist/look.js +39 -6
  107. package/dist/outline.d.ts +1 -1
  108. package/dist/outline.js +30 -2
  109. package/dist/pageant.d.ts +120 -0
  110. package/dist/pageant.draw.d.ts +21 -0
  111. package/dist/pageant.draw.js +171 -0
  112. package/dist/pageant.js +207 -0
  113. package/dist/pageant.style.d.ts +8 -0
  114. package/dist/pageant.style.js +42 -0
  115. package/dist/png.d.ts +8 -1
  116. package/dist/png.js +21 -1
  117. package/dist/pool.d.ts +176 -0
  118. package/dist/pool.figures.d.ts +304 -0
  119. package/dist/pool.figures.js +144 -0
  120. package/dist/pool.js +304 -0
  121. package/dist/pool.kit.d.ts +48 -0
  122. package/dist/pool.kit.js +141 -0
  123. package/dist/pool.labels.d.ts +20 -0
  124. package/dist/pool.labels.js +257 -0
  125. package/dist/pool.race.d.ts +65 -0
  126. package/dist/pool.race.js +140 -0
  127. package/dist/pool.replay.d.ts +46 -0
  128. package/dist/pool.replay.js +127 -0
  129. package/dist/pool.section.d.ts +13 -0
  130. package/dist/pool.section.js +83 -0
  131. package/dist/pool.style.d.ts +20 -0
  132. package/dist/pool.style.js +169 -0
  133. package/dist/pool.types.d.ts +145 -0
  134. package/dist/pool.types.js +3 -0
  135. package/dist/pool.words.d.ts +13 -0
  136. package/dist/pool.words.js +101 -0
  137. package/dist/ranges.names.js +47 -3
  138. package/dist/replay-entry.d.ts +3 -0
  139. package/dist/replay-entry.js +2 -0
  140. package/dist/restyle.js +1 -1
  141. package/dist/score.js +70 -5
  142. package/dist/sheet.d.ts +3 -0
  143. package/dist/sheet.js +7 -3
  144. package/dist/skating/describe.d.ts +7 -0
  145. package/dist/skating/describe.js +44 -0
  146. package/dist/skating/events.d.ts +24 -0
  147. package/dist/skating/events.js +47 -0
  148. package/dist/skating/figure.d.ts +24 -0
  149. package/dist/skating/figure.js +37 -0
  150. package/dist/skating/long.d.ts +54 -0
  151. package/dist/skating/long.js +111 -0
  152. package/dist/skating/parts.d.ts +43 -0
  153. package/dist/skating/parts.js +40 -0
  154. package/dist/skating/plan.d.ts +55 -0
  155. package/dist/skating/plan.js +197 -0
  156. package/dist/skating/short.d.ts +47 -0
  157. package/dist/skating/short.js +89 -0
  158. package/dist/skating/track.d.ts +66 -0
  159. package/dist/skating/track.js +94 -0
  160. package/dist/skating/types.d.ts +57 -0
  161. package/dist/skating/types.js +3 -0
  162. package/dist/skating-entry.d.ts +17 -0
  163. package/dist/skating-entry.js +11 -0
  164. package/dist/skating.style.d.ts +4 -0
  165. package/dist/skating.style.js +24 -0
  166. package/dist/strings.d.ts +296 -0
  167. package/dist/strings.js +328 -11
  168. package/dist/style.d.ts +2 -0
  169. package/dist/style.js +11 -1
  170. package/dist/surfaces/artistic.d.ts +9 -0
  171. package/dist/surfaces/artistic.js +94 -0
  172. package/dist/surfaces/athletics.d.ts +16 -0
  173. package/dist/surfaces/athletics.js +132 -0
  174. package/dist/surfaces/baseball.js +15 -0
  175. package/dist/surfaces/basketball.floor.d.ts +27 -0
  176. package/dist/surfaces/basketball.floor.js +83 -0
  177. package/dist/surfaces/basketball.gear.d.ts +27 -0
  178. package/dist/surfaces/basketball.gear.js +90 -0
  179. package/dist/surfaces/basketball.js +43 -21
  180. package/dist/surfaces/curling.d.ts +16 -0
  181. package/dist/surfaces/curling.fold.d.ts +67 -0
  182. package/dist/surfaces/curling.fold.js +107 -0
  183. package/dist/surfaces/curling.js +37 -3
  184. package/dist/surfaces/dive.d.ts +9 -0
  185. package/dist/surfaces/dive.js +128 -0
  186. package/dist/surfaces/field.d.ts +7 -2
  187. package/dist/surfaces/field.js +15 -9
  188. package/dist/surfaces/hockey.js +1 -0
  189. package/dist/surfaces/net.d.ts +2 -1
  190. package/dist/surfaces/net.js +160 -15
  191. package/dist/surfaces/polo.d.ts +12 -0
  192. package/dist/surfaces/polo.js +159 -0
  193. package/dist/surfaces/pool.d.ts +27 -0
  194. package/dist/surfaces/pool.js +198 -0
  195. package/dist/surfaces/skating.d.ts +6 -0
  196. package/dist/surfaces/skating.js +62 -0
  197. package/dist/surfaces/soccer.js +2 -1
  198. package/dist/surfaces/surface.d.ts +44 -0
  199. package/dist/surfaces/venue.d.ts +15 -0
  200. package/dist/surfaces/venue.js +95 -0
  201. package/dist/tenants.d.ts +77 -0
  202. package/dist/tenants.data.d.ts +8 -0
  203. package/dist/tenants.data.js +945 -0
  204. package/dist/tenants.js +46 -0
  205. package/dist/turn-entry.d.ts +12 -0
  206. package/dist/turn-entry.js +7 -0
  207. package/dist/turn.camera.d.ts +105 -0
  208. package/dist/turn.camera.js +168 -0
  209. package/dist/turn.dom.d.ts +40 -0
  210. package/dist/turn.dom.js +351 -0
  211. package/dist/turn.kit.d.ts +86 -0
  212. package/dist/turn.kit.js +110 -0
  213. package/dist/turn.markup.d.ts +48 -0
  214. package/dist/turn.markup.js +108 -0
  215. package/dist/turn.model.d.ts +78 -0
  216. package/dist/turn.model.js +91 -0
  217. package/dist/turn.style.d.ts +5 -0
  218. package/dist/turn.style.js +55 -0
  219. package/dist/types.d.ts +183 -18
  220. package/dist/units.d.ts +1 -1
  221. package/dist/units.js +15 -1
  222. package/dist/uprights.board.d.ts +17 -0
  223. package/dist/uprights.board.js +59 -0
  224. package/dist/uprights.d.ts +37 -0
  225. package/dist/uprights.fields.d.ts +32 -0
  226. package/dist/uprights.fields.js +172 -0
  227. package/dist/uprights.goal.d.ts +49 -0
  228. package/dist/uprights.goal.js +69 -0
  229. package/dist/uprights.js +160 -0
  230. package/dist/uprights.more.d.ts +7 -0
  231. package/dist/uprights.more.js +183 -0
  232. package/dist/uprights.net.d.ts +12 -0
  233. package/dist/uprights.net.js +109 -0
  234. package/dist/uprights.netkit.d.ts +28 -0
  235. package/dist/uprights.netkit.js +67 -0
  236. package/dist/uprights.pools.d.ts +8 -0
  237. package/dist/uprights.pools.js +190 -0
  238. package/dist/uprights.rinks.d.ts +27 -0
  239. package/dist/uprights.rinks.js +106 -0
  240. package/dist/uprights.track.d.ts +50 -0
  241. package/dist/uprights.track.js +246 -0
  242. package/dist/version.d.ts +1 -1
  243. package/dist/version.js +1 -1
  244. package/package.json +19 -2
package/dist/draw.js CHANGED
@@ -6,17 +6,33 @@ import { ERA_SPORTS, footballIn, rulesFor, seasonLabel } from "./eras.js";
6
6
  import { FOOTBALL } from "./geometry.js";
7
7
  import { renderLayers } from "./layers.js";
8
8
  import { groundById, groundNameIn, groundOfficialName } from "./grounds.js";
9
- import { courtFor, isNetSport } from "./court.js";
9
+ import { BOARD, courtFor, hasSetsBoard, isNetLeague, isNetSport } from "./court.js";
10
10
  import { cricketBoardMarkup } from "./cricket.board.js";
11
+ import { athleticsPlan } from "./athletics/plan.js";
12
+ import { skatingPlan } from "./skating/plan.js";
13
+ import { skatingAbout, skatingSentences } from "./skating/describe.js";
14
+ import { skatingStyle } from "./skating.style.js";
15
+ import { skatingSurface } from "./surfaces/skating.js";
16
+ import { athleticsAbout, athleticsSentences } from "./athletics/describe.js";
17
+ import { athleticsStyle, athleticsTrackRules } from "./athletics.style.js";
18
+ import { athleticsSurface, pitchGroup } from "./surfaces/athletics.js";
11
19
  import { fieldPlan, isFieldSport } from "./fields.js";
12
20
  import { fieldScoreText, fieldTotal } from "./field.score.js";
13
21
  import { fieldsStyle } from "./fields.style.js";
14
22
  import { resolveHomeSide } from "./homeside.js";
15
23
  import { LEAGUES, chooseLeague, courtOf, diamondOf, fieldOf, hasHistory, leagueSeason, leagueSize, netOf, rinkOf, sheetOf } from "./leagues.js";
16
- import { diamondFence } from "./leagues/baseball.js";
17
- import { fenceFor, parkById, parkNameIn, parkOfficialName } from "./parks.js";
24
+ import { MLB_DIAMOND, diamondFence } from "./leagues/baseball.js";
25
+ import { fenceFor, parkById, parkNameIn, parkOfficialName, parkWallsIn } from "./parks.js";
18
26
  import { boardMarkup, endText } from "./curling.board.js";
19
27
  import { sheetFor } from "./sheet.js";
28
+ import { commonOf, isPoolSport, swimPlan, poloPlan, divePlan, artisticPlan } from "./pool.js";
29
+ import { natatorium } from "./surfaces/venue.js";
30
+ import { poolDescription } from "./pool.words.js";
31
+ import { KYOGIJO_POOL_STYLE, poolStyle } from "./pool.style.js";
32
+ import { swimming } from "./surfaces/pool.js";
33
+ import { waterPolo } from "./surfaces/polo.js";
34
+ import { diving } from "./surfaces/dive.js";
35
+ import { artistic } from "./surfaces/artistic.js";
20
36
  import { cropToRange, describeRange, rangeLabel, shows } from "./ranges.js";
21
37
  import { badgeMarkup, bannerMarkup, plateMarkup, stateOf } from "./score.js";
22
38
  import { setsBoardMarkup } from "./sets.board.js";
@@ -24,13 +40,21 @@ import { KYOGIJO_STRINGS, fill } from "./strings.js";
24
40
  import { courtStyle } from "./courts.style.js";
25
41
  import { annotate, annotated, wideView } from "./annotate.js";
26
42
  import { KYOGIJO_BACKGROUND_STYLE, laidOn } from "./background.js";
43
+ import { KYOGIJO_HARDWOOD_STYLE, resolveHardwood } from "./hardwood.js";
44
+ import { WALL_SEATS, densityOf, layoutOf as crowdLayout, resolveCrowd } from "./crowd.js";
45
+ import { drawCrowd, KYOGIJO_CROWD_STYLE } from "./crowd.draw.js";
46
+ import { parkLayout } from "./crowd.park.js";
27
47
  import { featureColours, featureStyle } from "./features.js";
28
48
  import { isLook, lookStyle } from "./look.js";
29
49
  import { lineLook, restyleLines } from "./restyle.js";
30
50
  import { KYOGIJO_MARK_STYLE } from "./mark.js";
31
- import { KYOGIJO_BASE_STYLE, KYOGIJO_CURLING_STYLE, KYOGIJO_NOTE_STYLE, KYOGIJO_WALLS_STYLE } from "./style.js";
51
+ import { outlineOf } from "./outline.js";
52
+ import { autoScore, pageantLayout, pageantOf, ratioOf } from "./pageant.js";
53
+ import { emblemMarkup, picturesMarkup } from "./pageant.draw.js";
54
+ import { KYOGIJO_PAGEANT_MOTION, KYOGIJO_PAGEANT_STYLE } from "./pageant.style.js";
55
+ import { KYOGIJO_BASE_STYLE, KYOGIJO_CURLING_STYLE, KYOGIJO_FOLD_STYLE, KYOGIJO_NOTE_STYLE, KYOGIJO_WALLS_STYLE } from "./style.js";
32
56
  import { convertLength } from "./units.js";
33
- import { baseball } from "./surfaces/baseball.js";
57
+ import { baseball, fenceOutline } from "./surfaces/baseball.js";
34
58
  import { curling } from "./surfaces/curling.js";
35
59
  import { netCourt } from "./surfaces/net.js";
36
60
  import { fieldSurface } from "./surfaces/field.js";
@@ -47,7 +71,7 @@ import { abbreviate, capitals } from "./text.js";
47
71
  import { VERSION } from "./version.js";
48
72
  export { isPortrait };
49
73
  /** The sports that can be drawn, in the order a menu lists them. */
50
- export const SPORTS = ["nfl", "cfl", "ifaf", "nhl", "nba", "mlb", "soccer", "curling", "tennis", "volleyball", "badminton", "pickleball", "tabletennis", "lacrosse", "rugby", "afl", "gaelic", "cricket", "fieldhockey", "handball"];
74
+ export const SPORTS = ["nfl", "cfl", "ifaf", "nhl", "nba", "mlb", "soccer", "curling", "tennis", "volleyball", "badminton", "pickleball", "tabletennis", "padel", "squash", "racquetball", "sepaktakraw", "teqball", "roundnet", "netball", "swimming", "waterpolo", "diving", "artisticswimming", "athletics", "skating", "lacrosse", "rugby", "afl", "gaelic", "cricket", "fieldhockey", "handball"];
51
75
  /** The CSS custom property each colour of `Colors` sets. */
52
76
  export const COLOR_PROPERTIES = {
53
77
  apron: "--ky-apron",
@@ -90,7 +114,8 @@ const isScore = (o) => o.home !== undefined && o.away !== undefined;
90
114
  function minSets(sport, league) {
91
115
  if (sport === "volleyball")
92
116
  return league === "fivb-beach" ? 3 : 5;
93
- return sport === "tabletennis" ? 5 : 3;
117
+ // Table tennis, squash (World Squash Singles Rules 2.3) and racquetball (IRF Rule 1.5) are played to the best of five games.
118
+ return sport === "tabletennis" || sport === "squash" || sport === "racquetball" ? 5 : 3;
94
119
  }
95
120
  /** The standard statuses, spelled out. */
96
121
  const FINAL = /^(?:final|f\b|ft\b|試合終了|終了)/i;
@@ -177,7 +202,7 @@ const safeLength = (value) => {
177
202
  const PATTERNS = ["bands", "wide", "narrow", "lengthwise", "checks", "diagonal", "circles", "rings", "fan", "none"];
178
203
  const GOALS = ["box", "line", "strip", "none"];
179
204
  /** The pitch the options ask for: a `pitch` of the caller's, else the size a `ground` publishes, else none (the international pitch). */
180
- function pitchOf(o) {
205
+ export function pitchOf(o) {
181
206
  if (o.pitch !== undefined)
182
207
  return o.pitch;
183
208
  const ground = groundById(o.ground);
@@ -199,10 +224,30 @@ function buildSoccer(o, common, scored, unit) {
199
224
  frame: frame === undefined ? undefined : { id: frame.id, matrix: frameMatrix(frame, spec) },
200
225
  }, common);
201
226
  }
227
+ /** The athletics stadium: its plan, with the soccer pitch laid inside the track as the soccer builder draws it. */
228
+ function buildAthletics(o, common) {
229
+ const plan = athleticsPlan({ ...o, detail: common.detail });
230
+ let embedded;
231
+ if (plan.pitch !== undefined) {
232
+ const spec = plan.pitch.spec;
233
+ const goals = common.goals ?? "box";
234
+ const pitch = soccer({ spec, layout: layoutOf(spec), half: undefined, pattern: "bands", goals, flags: common.detail === "full", frame: undefined }, { ...common, portrait: false, margin: 0, score: false, landmarks: false, id: `${common.id}-pitch` });
235
+ embedded = { base: pitchGroup(pitch.base), lines: pitch.lines, defs: pitch.defs };
236
+ }
237
+ // A cropped view has less ground round it than the stadium whole.
238
+ return athleticsSurface(common, plan, embedded, common.margin ?? (o.range === undefined ? 20 : 5));
239
+ }
240
+ /** The skating rink: its plan, drawn as it lies. */
241
+ function buildSkating(o, common) {
242
+ const plan = skatingPlan({ ...o, detail: common.detail });
243
+ return skatingSurface(common, plan, common.margin ?? (o.range === undefined ? 20 : 5));
244
+ }
245
+ /** One text for both ends or sides, or a pair of them. */
246
+ const pair = (text) => (text === undefined ? undefined : typeof text === "string" ? [text, text] : text);
202
247
  /** The surface the options ask for, whole: before it is cropped to a `range`. */
203
248
  function buildWhole(o, portrait, id, score, scored, extra) {
204
249
  const court = o.court;
205
- const baselineText = court?.baselineText === undefined ? undefined : typeof court.baselineText === "string" ? [court.baselineText, court.baselineText] : court.baselineText;
250
+ const baselineText = pair(court?.baselineText);
206
251
  const unit = lengthUnit(o);
207
252
  const common = {
208
253
  portrait,
@@ -212,12 +257,13 @@ function buildWhole(o, portrait, id, score, scored, extra) {
212
257
  stripes: o.stripes !== false,
213
258
  id,
214
259
  score,
215
- court: { baselineText, centreLogo: safeLogo(court?.centreLogo), paintThreePoint: court?.paintThreePoint },
260
+ court: { baselineText, centreLogo: safeLogo(court?.centreLogo), paintThreePoint: court?.paintThreePoint, hardwood: resolveHardwood(court, o.detail === "simple", o.width), sidelineText: pair(court?.sidelineText) },
216
261
  features: extra.features,
217
262
  mow: PATTERNS.includes(o.stripePattern) ? o.stripePattern : undefined,
218
263
  goals: GOALS.includes(o.goals) ? o.goals : undefined,
219
264
  goalMarkup: typeof o.goalMarkup === "function" ? o.goalMarkup : undefined,
220
265
  landmarks: o.landmarks === true,
266
+ pageant: extra.pageant === true ? true : undefined,
221
267
  };
222
268
  // A league's rulebook (`league`), else the markings of the season asked for (`year`), today's without one.
223
269
  const { sport, league } = chooseLeague(o);
@@ -248,14 +294,33 @@ function buildWhole(o, portrait, id, score, scored, extra) {
248
294
  case "curling": {
249
295
  const plan = sheetFor(o);
250
296
  // A score always shows the whole sheet, for the board's sake.
251
- return curling(common, { pebble: plan.pebble, guardZone: plan.guardZone, show: scored ? "sheet" : plan.show, end: plan.end, figures: plan.spec });
297
+ const words = KYOGIJO_STRINGS[o.language === "ja" ? "ja" : "en"].ranges.folded;
298
+ const labels = portrait ? [words.top, words.bottom] : [words.left, words.right];
299
+ return curling(common, { pebble: plan.pebble, guardZone: plan.guardZone, show: scored ? "sheet" : plan.show, end: plan.end, figures: plan.spec, fold: { panels: plan.panels, fit: plan.fit, labels } });
252
300
  }
253
301
  case "tennis":
254
302
  case "volleyball":
255
303
  case "badminton":
256
304
  case "pickleball":
257
305
  case "tabletennis":
306
+ case "padel":
307
+ case "squash":
308
+ case "racquetball":
309
+ case "sepaktakraw":
310
+ case "teqball":
311
+ case "roundnet":
312
+ case "netball":
258
313
  return netCourt(common, courtFor({ ...o, sport }));
314
+ case "swimming":
315
+ if (o.pool?.venue !== undefined)
316
+ return natatorium(common, o.pool, commonOf(o.pool), o.language === "ja" ? "ja" : "en");
317
+ return swimming(common, swimPlan({ ...o, sport }));
318
+ case "waterpolo":
319
+ return waterPolo(common, poloPlan(o));
320
+ case "diving":
321
+ return diving(common, divePlan(o));
322
+ case "artisticswimming":
323
+ return artistic(common, artisticPlan(o));
259
324
  case "lacrosse":
260
325
  case "rugby":
261
326
  case "afl":
@@ -264,18 +329,91 @@ function buildWhole(o, portrait, id, score, scored, extra) {
264
329
  case "fieldhockey":
265
330
  case "handball":
266
331
  return fieldSurface(common, fieldPlan({ ...o, sport, detail: common.detail }), { goals: common.goals });
332
+ case "athletics":
333
+ return buildAthletics({ ...o, sport }, common);
334
+ case "skating":
335
+ return buildSkating({ ...o, sport }, common);
267
336
  default:
268
337
  throw new Error(`Unknown sport "${String(o.sport)}": use one of ${SPORTS.join(", ")}`);
269
338
  }
270
339
  }
340
+ /**
341
+ * The crowd the options ask for, or `undefined` for none: a special game has none, and nor has a folded curling sheet (its houses are enlarged over where the stands would be, so the
342
+ * stands would show only as strips behind the panels). A score on a curling sheet shows the whole sheet, which has its stands.
343
+ */
344
+ function crowdOf(o) {
345
+ if (o.host !== undefined)
346
+ return undefined;
347
+ if (chooseLeague(o).sport === "curling" && !isScore(o) && sheetFor(o).show === "folded")
348
+ return undefined;
349
+ return resolveCrowd(o.crowd);
350
+ }
271
351
  /** The surface the options ask for, cropped to their `range` when they have one (a curling sheet's named ranges are its builder's own: `sheet.show`). */
272
352
  function buildSurface(o, portrait, id, score, scored, extra) {
273
- const surface = buildWhole(o, portrait, id, score, scored, extra);
353
+ const crowd = crowdOf(o);
354
+ const surface = crowd === undefined ? buildWhole(o, portrait, id, score, scored, extra) : buildCrowded(o, portrait, id, score, scored, extra, crowd);
274
355
  // A score on a curling sheet shows all of it, for the board's sake.
275
356
  if (o.range === undefined || (scored && chooseLeague(o).sport === "curling"))
276
357
  return surface;
358
+ // A close-up of a start is the athletics builder's own view, turned to the start's line.
359
+ if (chooseLeague(o).sport === "athletics" && rangeLabel(o) === "start")
360
+ return surface;
277
361
  const given = typeof o.margin === "number" && Number.isFinite(o.margin) && o.margin >= 0 ? convertLength(o.margin, lengthUnit(o), "ft") : undefined;
278
- return cropToRange(surface, o, { id, margin: extra.room ?? extra.margin ?? given, panel: score });
362
+ return cropToRange(surface, o, { id, margin: extra.room ?? extra.margin ?? given ?? surface.crowdMargin, panel: score });
363
+ }
364
+ /** The fence of the ballpark the options ask for, as the drawing has it, and how far behind home plate the backstop stands. */
365
+ function parkFence(o) {
366
+ const unit = lengthUnit(o);
367
+ const { league } = chooseLeague(o);
368
+ if (league?.family === "baseball") {
369
+ const diamond = diamondOf(league, o.year);
370
+ return { points: fenceFor(parkById(o.park), feetFence(o.fence, unit), o.year, diamondFence(diamond)), backstop: diamond.backstop };
371
+ }
372
+ return { points: fenceFor(parkById(o.park), feetFence(o.fence, unit), o.year), backstop: MLB_DIAMOND.backstop };
373
+ }
374
+ /** The stands round a surface, laid out from its box (or, for a ballpark, from its fence). */
375
+ function standsFor(o, surface, crowd) {
376
+ const { sport } = chooseLeague(o);
377
+ const awaySide = resolveHomeSide({ ...o, sport }) === "left" ? "right" : "left";
378
+ if (sport === "mlb") {
379
+ const { points, backstop } = parkFence(o);
380
+ // The walls that have seats on top of them: the ones the package knows (Fenway Park's Green Monster), or, asked for, every wall of 25 feet or more.
381
+ const park = parkById(o.park);
382
+ const walls = park === undefined || crowd.wallSeats === false ? [] : parkWallsIn(park, o.year).filter((w) => (crowd.wallSeats === true ? w.height_ft >= 25 : w.label !== undefined && WALL_SEATS[park.id] === w.label));
383
+ return parkLayout({ fence: fenceOutline(points), backstop, crowd, seatsOnWalls: walls.map((w) => ({ from: w.from, to: w.to, height: w.height_ft })) });
384
+ }
385
+ return crowdLayout({ sport, box: surface.whole ?? surface.box ?? { x: 0, y: 0, w: 100, h: 100 }, crowd, awaySide, ground: o.ground });
386
+ }
387
+ /** True when the drawing is asked to be small: `detail: "simple"` or a `width` under 480 pixels. */
388
+ const smallDrawing = (o) => {
389
+ const w = typeof o.width === "number" ? o.width : typeof o.width === "string" && /^\s*\d+(?:\.\d+)?(?:px)?\s*$/.test(o.width) ? parseFloat(o.width) : undefined;
390
+ return o.detail === "simple" || (w !== undefined && w < 480);
391
+ };
392
+ /** A surface with the stands and the crowd round it: the ground is made as wide as the stands need, then the stands are laid on it under the surface. */
393
+ function buildCrowded(o, portrait, id, score, scored, extra, crowd) {
394
+ let margin = extra.margin;
395
+ if (!(typeof o.margin === "number" && Number.isFinite(o.margin) && o.margin >= 0)) {
396
+ // How much ground the stands need beyond what the surface has with none, then the surface is made again with it.
397
+ const probe = buildWhole({ ...o, margin: 0 }, portrait, id, score, scored, { ...extra, margin: undefined });
398
+ const stands = standsFor(o, probe, crowd);
399
+ const view = wideView(probe);
400
+ const have = { left: stands.box.x - view.x, right: view.x + view.w - stands.box.x - stands.box.w, top: stands.box.y - view.y, bottom: view.y + view.h - stands.box.y - stands.box.h };
401
+ const need = Math.max(stands.reach.left - have.left, stands.reach.right - have.right, stands.reach.top - have.top, stands.reach.bottom - have.bottom);
402
+ margin = Math.max(margin ?? 0, need + 6);
403
+ }
404
+ const surface = buildWhole(o, portrait, id, score, scored, { ...extra, margin });
405
+ const stands = standsFor(o, surface, crowd);
406
+ const seats = stands.seats;
407
+ const ground = groundById(o.ground)?.capacity;
408
+ const fine = crowd.detail === "fine" || (crowd.detail === "auto" && !smallDrawing(o));
409
+ const first = /^<[^>]*\/>/.exec(surface.base)?.[0] ?? "";
410
+ const bg = /class="([^"]*)"/.exec(first)?.[1] ?? "ky-apron";
411
+ const drawn = drawCrowd(stands, { id, crowd, density: densityOf(crowd, seats, ground), fine, ground: bg });
412
+ surface.base = first + drawn.markup + surface.base.slice(first.length);
413
+ surface.defs += drawn.defs;
414
+ surface.lines += drawn.over;
415
+ surface.crowdMargin = margin;
416
+ return surface;
279
417
  }
280
418
  /** The custom properties a drawing sets on its root, from the options' colours. */
281
419
  function colourStyle(o, teams, features) {
@@ -315,6 +453,23 @@ function colourStyle(o, teams, features) {
315
453
  props.push(`--ky-left-ink:${leftInk}`);
316
454
  if (rightInk !== undefined)
317
455
  props.push(`--ky-right-ink:${rightInk}`);
456
+ // A hardwood floor's border behind each baseline: the court's colour for it, else the team's, else the border's.
457
+ if (o.court?.hardwood !== undefined && o.court.hardwood !== false) {
458
+ const ends = pair(o.court.baselineColor);
459
+ const endLeft = safeColour(ends?.[0]) ?? left;
460
+ const endRight = safeColour(ends?.[1]) ?? right;
461
+ if (endLeft !== undefined)
462
+ props.push(`--ky-hw-end-left:${endLeft}`);
463
+ if (endRight !== undefined)
464
+ props.push(`--ky-hw-end-right:${endRight}`);
465
+ }
466
+ // The crowd's colours: the home fans in the home team's colour and the visitors in the other's, or the colours the crowd names.
467
+ const crowd = crowdOf(o);
468
+ if (crowd !== undefined) {
469
+ const homeLeft = resolveHomeSide({ ...o, sport: chooseLeague(o).sport }) === "left";
470
+ props.push(`--ky-cr-home:${safeColour(crowd.home) ?? (homeLeft ? left : right) ?? `var(--ky-${homeLeft ? "left" : "right"})`}`);
471
+ props.push(`--ky-cr-away:${safeColour(crowd.away) ?? (homeLeft ? right : left) ?? `var(--ky-${homeLeft ? "right" : "left"})`}`);
472
+ }
318
473
  return props.join(";");
319
474
  }
320
475
  /** The round mark of a team, when its own `mark` or the score's `markShape` asks for one: the fit, the colour of its disc and the ink of its initials. */
@@ -348,10 +503,46 @@ function teamView(team, side, role, winner, uppercase, sport, markShape) {
348
503
  innings: sport === "cricket" && Array.isArray(team.innings) ? team.innings.map((value) => (typeof value === "number" ? (Number.isFinite(value) ? String(value) : "") : typeof value === "string" ? value.trim().slice(0, 7) : "")) : undefined,
349
504
  };
350
505
  }
506
+ /** A team as the pageant lays it out. */
507
+ function pageantTeam(view, team) {
508
+ return { side: view.side, role: view.role, name: view.name, abbr: view.abbr, state: view.state, href: view.logo, fit: team.markFit === "contain" ? "contain" : "cover", ratio: ratioOf(team), color: safeColour(team.color), ink: safeColour(team.textColor) };
509
+ }
510
+ /** True when the drawing is asked to be small: its `width` is under 320 pixels. The pageant then leaves out its fabric, hairlines and name pills. */
511
+ function tiny(width) {
512
+ const px = typeof width === "number" ? width : typeof width === "string" && /^\d+(?:\.\d+)?(?:px)?$/.test(width.trim()) ? Number.parseFloat(width) : undefined;
513
+ return px !== undefined && Number.isFinite(px) && px < 320;
514
+ }
515
+ /** The teams that fill the first and the second place of a surface: a diamond's outfield is the visitors' and its infield the home team's, whichever side each is on. */
516
+ function pageantPair(surface, t) {
517
+ const l = pageantTeam(t.left, t.leftTeam);
518
+ const r = pageantTeam(t.right, t.rightTeam);
519
+ return surface.pageant?.byRole === true && l.role === "home" ? [r, l] : [l, r];
520
+ }
521
+ function pageantParts(o, surface, p, t) {
522
+ const [first, second] = pageantPair(surface, t);
523
+ const layout = pageantLayout({ pageant: p, surface, outline: outlineOf(o), left: first, right: second, turned: surface.turned, sport: t.sport, plate: t.plate });
524
+ if (layout === undefined)
525
+ return undefined;
526
+ // Cropped to a range, an emblem that does not lie wholly in what is shown is left out.
527
+ const shown = surface.whole === undefined ? undefined : surface.box;
528
+ if (layout.emblem !== undefined && shown !== undefined) {
529
+ const e = layout.emblem;
530
+ if (e.cx - e.r < shown.x || e.cx + e.r > shown.x + shown.w || e.cy - e.r < shown.y || e.cy + e.r > shown.y + shown.h)
531
+ delete layout.emblem;
532
+ }
533
+ const draw = { id: t.id, turned: surface.turned, simple: o.detail === "simple" || tiny(o.width), parts: t.parts };
534
+ const pictures = picturesMarkup(layout, p, draw);
535
+ const emblem = emblemMarkup(layout, p, draw);
536
+ return { pictures: pictures.markup, names: pictures.names, emblem: emblem.markup, defs: pictures.defs + emblem.defs, plateScale: layout.plateScale };
537
+ }
351
538
  const trimmed = (value) => String(Math.round(value * 100) / 100);
352
539
  /** What a league's surface is, in a sentence fragment: its size or its bases, from the league's own figures in the unit its rulebook uses. */
353
540
  function leagueAbout(league, year, language, sheet) {
354
541
  const words = KYOGIJO_STRINGS[language].league;
542
+ if (league.family === "skating")
543
+ return skatingAbout({ sport: "skating", league: league.id }, language);
544
+ if (league.family === "athletics")
545
+ return athleticsAbout({ sport: "athletics", league: league.id, athletics: sheet === undefined ? undefined : undefined }, language);
355
546
  // The field sports: their size is the surface drawn (the one the options ask for), in the unit their rulebook prints first.
356
547
  if (league.family === "lacrosse" || league.family === "rugby" || league.family === "afl" || league.family === "gaelic" || league.family === "cricket" || league.family === "fieldhockey" || league.family === "handball") {
357
548
  const sized = sheet ?? leagueSize(league.family, league.id);
@@ -370,7 +561,7 @@ function leagueAbout(league, year, language, sheet) {
370
561
  ? rinkOf(league, year)
371
562
  : league.family === "curling"
372
563
  ? (sheet ?? sheetOf(league))
373
- : league.family === "tennis" || league.family === "volleyball" || league.family === "badminton" || league.family === "pickleball" || league.family === "tabletennis"
564
+ : isNetLeague(league)
374
565
  ? (() => {
375
566
  const f = netOf(league);
376
567
  return { length: f.length, width: f.widthDoubles ?? f.width };
@@ -381,7 +572,7 @@ function leagueAbout(league, year, language, sheet) {
381
572
  })();
382
573
  const to = league.unit;
383
574
  const text = fill(words.size, { length: trimmed(convertLength(size.length, "ft", to)), width: trimmed(convertLength(size.width, "ft", to)), unit: words.units[to === "m" || to === "yd" ? to : "ft"] });
384
- return fill(league.family === "volleyball" && league.id === "fivb-beach" ? words.beachVolleyball : words[league.family], { size: text });
575
+ return fill(league.family === "volleyball" && league.id === "fivb-beach" ? words.beachVolleyball : league.family === "volleyball" && league.id === "wpv-sitting" ? words.sittingVolleyball : words[league.family], { size: text });
385
576
  }
386
577
  /** A drawing's title and description, in a language. */
387
578
  export function describe(o) {
@@ -398,15 +589,19 @@ export function describe(o) {
398
589
  const ground = pitch?.format === "11" ? groundById(o.ground) : undefined;
399
590
  const groundName = ground === undefined ? undefined : groundNameIn(ground, o.year, language);
400
591
  // A league other than the sport's own is named by its rulebook's, and its description is made from its figures.
401
- const surfaceName = groundName !== undefined ? (language === "ja" ? `${groundName}のピッチ` : `${groundName}, soccer pitch`) : league !== undefined && parkName === undefined ? league.name[language] : parkName === undefined ? (pitch?.format === "futsal" ? words.soccer.futsal : words.surface[sport]) : language === "ja" ? `${parkName}のグラウンド` : `${parkName}, baseball field`;
592
+ const trackGround = sport === "athletics" ? groundById(o.ground) : undefined;
593
+ const trackGroundName = trackGround === undefined ? undefined : groundNameIn(trackGround, o.year, language);
594
+ const surfaceName = trackGroundName !== undefined ? fill(words.athletics.ground, { ground: trackGroundName }) : groundName !== undefined ? (language === "ja" ? `${groundName}のピッチ` : `${groundName}, soccer pitch`) : league !== undefined && parkName === undefined ? league.name[language] : parkName === undefined ? (pitch?.format === "futsal" ? words.soccer.futsal : words.surface[sport]) : language === "ja" ? `${parkName}のグラウンド` : `${parkName}, baseball field`;
402
595
  const { stop, colon, gap } = words.said;
403
596
  const sentence = (text) => (/[.。]$/.test(text) ? text : `${text}${stop}`);
404
597
  const metres = (value) => String(Math.round(value * 10) / 10);
405
598
  const plan = sport === "curling" ? sheetFor(o) : undefined;
406
599
  // A field sport: the surface as drawn, at the size the options ask for.
407
600
  const field = isFieldSport(sport) ? fieldPlan({ ...o, sport }) : undefined;
408
- const aboutText = field !== undefined ? leagueAbout(league ?? LEAGUES[sport][0], o.year, language, { length: field.length, width: field.width }) : plan !== undefined ? leagueAbout(plan.league, o.year, language, plan.spec) : league !== undefined ? leagueAbout(league, o.year, language) : pitch === undefined ? words.about[sport] : fill(pitch.format === "futsal" ? words.soccer.aboutFutsal : words.about.soccer, { length: metres(pitch.length), width: metres(pitch.width) });
409
- const about = `${words.said.on}${colon}${aboutText}${stop}`;
601
+ const pools = isPoolSport(sport) ? poolDescription({ sport, pool: o.pool }, language) : undefined;
602
+ const poolLine = pools === undefined ? "" : pools.after.map((line) => `${gap}${line}`).join("");
603
+ const aboutText = pools !== undefined ? pools.about : sport === "skating" ? skatingAbout(o, language) : sport === "athletics" ? athleticsAbout(o, language) : field !== undefined ? leagueAbout(league ?? LEAGUES[sport][0], o.year, language, { length: field.length, width: field.width }) : plan !== undefined ? leagueAbout(plan.league, o.year, language, plan.spec) : league !== undefined ? leagueAbout(league, o.year, language) : pitch === undefined ? words.about[sport] : fill(pitch.format === "futsal" ? words.soccer.aboutFutsal : words.about.soccer, { length: metres(pitch.length), width: metres(pitch.width) });
604
+ const about = `${words.said.on}${colon}${aboutText}${stop}${sport === "athletics" ? athleticsSentences(o, language) : sport === "skating" ? skatingSentences(o, language) : ""}`;
410
605
  const halfLine = pitch !== undefined && !isScore(o) && (o.half === "left" || o.half === "right") ? `${gap}${words.soccer.half}` : "";
411
606
  // What the ground says about the size: that it is the published one, or that the ground has none and the international pitch is drawn.
412
607
  const year = typeof o.year === "number" && Number.isFinite(o.year) ? o.year : undefined;
@@ -428,10 +623,21 @@ export function describe(o) {
428
623
  ? `${gap}${fill(words.said.era, { season: seasonLabel(sport, o.year) })}`
429
624
  : "";
430
625
  // A range says what is shown; a curling score shows all of the sheet, whatever the range.
431
- const rangeLine = isScore(o) && sport === "curling" ? "" : describeRange(o, language, o.layers !== undefined && o.layers.length > 0);
626
+ const hasData = o.layers !== undefined && o.layers.length > 0;
627
+ // A folded sheet says what is enlarged and where, in place of the range's own sentence.
628
+ const foldedWords = words.ranges.folded;
629
+ const rangeLine = isScore(o) && sport === "curling"
630
+ ? ""
631
+ : plan?.show === "folded"
632
+ ? `${gap}${[foldedWords[isPortrait(o) ? "tall" : "wide"], foldedWords[plan.panels], hasData ? foldedWords.data : ""].filter((part) => part !== "").join(gap)}`
633
+ : describeRange(o, language, hasData);
432
634
  if (!isScore(o))
433
- return { title: o.title ?? special?.title ?? surfaceName, description: o.description ?? `${about}${parkLine}${eraLine}${rangeLine}` };
635
+ return { title: o.title ?? special?.title ?? surfaceName, description: o.description ?? `${about}${parkLine}${eraLine}${rangeLine}${poolLine}` };
434
636
  const winner = winnerOf({ ...o, sport });
637
+ // A pageant says what lies on each half, and the emblem in the middle.
638
+ const pageant = pageantOf(o);
639
+ const pg = KYOGIJO_STRINGS[language].pageant;
640
+ const pageantLine = pageant === undefined ? "" : `${gap}${pageant.style === "stage" ? pg.stage : pg.carpet}${pageant.emblem === undefined ? "" : `${gap}${pageant.emblem.name === undefined ? pg.emblem : fill(pg.emblemNamed, { name: pageant.emblem.name })}`}`;
435
641
  const s = (t) => fieldScoreText(sport, t) ?? (t.score === undefined || String(t.score) === "" ? "–" : String(t.score));
436
642
  // The sentence follows the drawing: the team on the left is first. The words' {away} and {home} are the first and the second team named.
437
643
  const order = resolveHomeSide({ ...o, sport }) === "left" ? [o.home, o.away] : [o.away, o.home];
@@ -463,12 +669,12 @@ export function describe(o) {
463
669
  const tb = endText(team.tiebreaks?.[i]);
464
670
  return `${endText(value) || "–"}${tb === "" ? "" : ` (${tb})`}`;
465
671
  }).join(words.layers.listSep);
466
- const netLine = !isNetSport(sport)
672
+ const netLine = !hasSetsBoard(sport)
467
673
  ? ""
468
674
  : order
469
675
  .flatMap((team) => [
470
676
  team.serving === true ? fill(net.serving, { name: team.name }) : "",
471
- Array.isArray(team.sets) && team.sets.length > 0 ? fill(sport === "badminton" || sport === "pickleball" || sport === "tabletennis" ? net.byGame : net.bySet, { name: team.name, list: setList(team) }) : "",
677
+ Array.isArray(team.sets) && team.sets.length > 0 ? fill(BOARD[sport] === "games" ? net.byGame : net.bySet, { name: team.name, list: setList(team) }) : "",
472
678
  team.points !== undefined && team.points !== null && String(team.points).trim() !== "" ? fill(net.gamePoints, { name: team.name, points: String(team.points).trim() }) : "",
473
679
  ])
474
680
  .filter((part) => part !== "")
@@ -493,7 +699,7 @@ export function describe(o) {
493
699
  : "";
494
700
  return {
495
701
  title: o.title ?? (status === undefined ? line : `${line}${words.said.sep}${status}`),
496
- description: o.description ?? `${extra === "" ? "" : `${sentence(extra)}${gap}`}${sentence(line)}${result}${tied}${curlingLine}${netLine}${fieldLine}${gap}${about}${parkLine}${eraLine}${rangeLine}`.trim(),
702
+ description: o.description ?? `${extra === "" ? "" : `${sentence(extra)}${gap}`}${sentence(line)}${result}${tied}${curlingLine}${netLine}${fieldLine}${gap}${about}${parkLine}${eraLine}${rangeLine}${pageantLine}${poolLine}`.trim(),
497
703
  };
498
704
  }
499
705
  /**
@@ -515,6 +721,9 @@ export function describe(o) {
515
721
  export function drawKyogijo(o) {
516
722
  if (o.host !== undefined && o.range !== undefined)
517
723
  return drawKyogijo({ ...o, range: undefined });
724
+ // A special game is a surface set inside another, drawn to scale with no stands.
725
+ if (o.host !== undefined && o.crowd !== undefined)
726
+ return drawKyogijo({ ...o, crowd: undefined });
518
727
  const language = o.language === "ja" ? "ja" : "en";
519
728
  // The sport as drawn: a league that is a sport of its own (`league: "cfl"` on an `nfl`) is that sport.
520
729
  const { sport } = chooseLeague(o);
@@ -526,6 +735,14 @@ export function drawKyogijo(o) {
526
735
  // A tall curling sheet is 30 feet wide on the page and a strip under it would be unreadable: its compact size is the board on the ice.
527
736
  const asked = isScore(o) ? (o.size === "compact" || o.size === "badge" ? o.size : "full") : "full";
528
737
  let sized = asked === "compact" && sport === "curling" && portrait ? "full" : asked;
738
+ // A pageant that sends the score to the strip makes the drawing compact; a tall curling sheet cannot have a strip, so its board stays in the middle and the emblem is left out.
739
+ let pageant = isScore(o) ? pageantOf(o) : undefined;
740
+ if (pageant !== undefined && pageant.score === "strip") {
741
+ if (sport === "curling" && portrait)
742
+ pageant = { ...pageant, score: "centre", emblem: undefined };
743
+ else if (sized === "full")
744
+ sized = "compact";
745
+ }
529
746
  const id = drawingId(o);
530
747
  const parts = o.parts === true;
531
748
  const homeSide = isScore(o) ? resolveHomeSide({ ...o, sport }) : "right";
@@ -555,6 +772,8 @@ export function drawKyogijo(o) {
555
772
  const featured = featureColours(sport, o.featureColors);
556
773
  const features = featured.length === 0 ? undefined : new Set(featured.map(([name]) => name));
557
774
  const walled = sport === "mlb" && ballparkDrawn(ballparkFor(o));
775
+ const hardwood = sport === "nba" && o.court?.hardwood !== undefined && o.court.hardwood !== false;
776
+ const crowded = crowdOf(o) !== undefined;
558
777
  const style = colourStyle(o, { left: leftTeam, right: rightTeam, home: isScore(o) ? o.home : undefined, away: isScore(o) ? o.away : undefined }, featured);
559
778
  const palette = isLook(o.look) ? o.look : undefined;
560
779
  let notes;
@@ -563,6 +782,8 @@ export function drawKyogijo(o) {
563
782
  let body;
564
783
  let defs = "";
565
784
  let grounded = false;
785
+ let foldedSheet = false;
786
+ let pg;
566
787
  if (isScore(o) && left !== undefined && right !== undefined && sized === "badge") {
567
788
  const badge = badgeMarkup(left, right, { sport, status, parts, id });
568
789
  viewBox = `0 0 ${badge.w} ${badge.h}`;
@@ -576,13 +797,28 @@ export function drawKyogijo(o) {
576
797
  const guest = buildSurface({ ...o, margin: 0 }, false, id, isScore(o) && sized === "full", isScore(o), more);
577
798
  return composeSpecial(o, guest, (host, hostId) => buildSurface(host, false, hostId, false, false, { features: undefined }), id);
578
799
  };
579
- let surface = make({ features });
800
+ let surface = make({ features, pageant: pageant !== undefined });
801
+ // The score of a pageant with no emblem and no board: the plate stays in the middle if the pictures lose little to it, else it goes to the strip and the surface is built again.
802
+ if (pageant !== undefined && pageant.score === "auto") {
803
+ let score = "centre";
804
+ if (isScore(o) && left !== undefined && right !== undefined && leftTeam !== undefined && rightTeam !== undefined && sized === "full") {
805
+ const [first, second] = pageantPair(surface, { left, right, leftTeam, rightTeam });
806
+ score = autoScore({ pageant, surface, outline: outlineOf(o), left: first, right: second, turned: surface.turned, sport, plate: true });
807
+ }
808
+ pageant = { ...pageant, score };
809
+ if (score === "strip") {
810
+ sized = "compact";
811
+ surface = make({ features, pageant: true });
812
+ }
813
+ }
814
+ foldedSheet = surface.folded === true;
580
815
  // A range too small to hold the score's panel: the score goes to the strip under the drawing, as a compact card does.
581
816
  if (surface.plateFits === false) {
582
817
  sized = "compact";
583
- surface = buildSurface(o, portrait, id, false, true, { features });
818
+ surface = buildSurface(o, portrait, id, false, true, { features, pageant: pageant !== undefined });
584
819
  }
585
- if (annotated(o) && o.host === undefined) {
820
+ // A folded sheet has no margin for the annotations to stand in: they are not drawn on it.
821
+ if (annotated(o) && o.host === undefined && surface.folded !== true) {
586
822
  const teams = { home: isScore(o) ? o.home.name : undefined, away: isScore(o) ? o.away.name : undefined };
587
823
  notes = annotate({ o, surface, turned: surface.turned, words: KYOGIJO_STRINGS[language], teams });
588
824
  // The annotations need room in the margin: unless the options fix it, the surface is drawn again with a margin that holds them.
@@ -590,7 +826,7 @@ export function drawKyogijo(o) {
590
826
  const have = surface.ground ?? (surface.box === undefined ? 0 : surface.box.y - wideView(surface).y);
591
827
  const roomed = surface.room ?? have;
592
828
  if (o.margin === undefined && (notes.need > have + 0.01 || notes.need > roomed + 0.01)) {
593
- surface = make({ features, margin: notes.need > have + 0.01 ? notes.need : undefined, room: notes.need });
829
+ surface = make({ features, pageant: pageant !== undefined, margin: notes.need > have + 0.01 ? notes.need : undefined, room: notes.need });
594
830
  notes = annotate({ o, surface, turned: surface.turned, words: KYOGIJO_STRINGS[language], teams });
595
831
  }
596
832
  }
@@ -604,10 +840,19 @@ export function drawKyogijo(o) {
604
840
  defs = surface.defs + ground.defs;
605
841
  let inner = ground.base;
606
842
  let teams = "";
607
- if (isScore(o) && left !== undefined && right !== undefined && sized === "full") {
843
+ // A pageant lays each team's picture over its half, in place of the names painted at the ends.
844
+ if (pageant !== undefined && isScore(o) && left !== undefined && right !== undefined && leftTeam !== undefined && rightTeam !== undefined && (sized === "full" || sized === "compact")) {
845
+ pg = pageantParts(o, surface, pageant, { left, right, leftTeam, rightTeam, sport, id, plate: sized === "full", parts });
846
+ if (pg !== undefined) {
847
+ defs += pg.defs;
848
+ if (sized === "full" && pg.plateScale !== 1)
849
+ surface.plate = { ...surface.plate, w: surface.plate.w * pg.plateScale, h: surface.plate.h * pg.plateScale };
850
+ }
851
+ }
852
+ if (isScore(o) && left !== undefined && right !== undefined && sized === "full" && pg === undefined) {
608
853
  const onColour = surface.onColour ?? (sport === "nfl" || sport === "cfl" || sport === "ifaf" || sport === "soccer");
609
854
  // A curling sheet, the net sports' courts and a cricket ground have no place for the names: the board names the teams.
610
- if (sport !== "curling" && !isNetSport(sport) && sport !== "cricket") {
855
+ if (sport !== "curling" && !hasSetsBoard(sport) && sport !== "cricket") {
611
856
  // Cropped to a range, a team's name is written only where its place is in what is shown.
612
857
  if (shows(surface, surface.left))
613
858
  teams += bannerMarkup(surface.left, left, { turned: surface.turned, onColour, parts, id });
@@ -616,20 +861,38 @@ export function drawKyogijo(o) {
616
861
  }
617
862
  }
618
863
  // The teams are painted under the lines, unless the surface asks for them over (a panel that must hide them).
619
- inner += surface.bannersOver === true ? surface.lines + teams : teams + surface.lines;
864
+ if (pg !== undefined && pageant !== undefined) {
865
+ // A surface cropped with room for its annotations is clipped to the cut; the pictures are cut with it.
866
+ const cut = surface.defs.includes(`id="${id}-rg"`);
867
+ const laid = cut ? el("g", { "clip-path": `url(#${id}-rg)` }, pg.pictures) : pg.pictures;
868
+ inner += pageant.lines !== "over" ? surface.lines + laid + pg.names + pg.emblem : laid + surface.lines + pg.names + pg.emblem;
869
+ }
870
+ else
871
+ inner += surface.bannersOver === true ? surface.lines + teams : teams + surface.lines;
620
872
  // Data goes over the markings and under the score's panel.
621
- const layered = renderLayers(o, o.layers, surface.turned, surface.whole === undefined ? undefined : surface.box);
873
+ const layered = renderLayers(o, o.layers, surface.turned, surface.whole === undefined ? undefined : surface.box, surface.upright);
622
874
  inner += surface.frame === undefined || layered.svg === "" ? layered.svg : el("g", { transform: surface.frame }, layered.svg);
875
+ // A surface drawn in parts shows the data again in each enlarged part, with its own ids, hidden from a reader of the drawing (the strip says it). A colour bar sized
876
+ // against the whole sheet falls outside the panels' part of it and is cut away there; one given a `region` inside a panel's part is drawn there, large.
877
+ if (surface.panels !== undefined && layered.svg !== "" && o.layers !== undefined) {
878
+ surface.panels.forEach((panel, i) => {
879
+ const copy = renderLayers({ ...o, id: `${id}-f${i}` }, o.layers, surface.turned);
880
+ if (copy.svg !== "")
881
+ inner += el("g", { class: "ky-fold-data", "aria-hidden": "true", transform: panel.transform, "clip-path": `url(#${panel.clip})` }, copy.svg);
882
+ });
883
+ }
623
884
  if (layered.summaries.length > 0 && o.description === undefined) {
624
885
  const { colon, gap } = KYOGIJO_STRINGS[language].said;
625
886
  description = `${said}${gap}${KYOGIJO_STRINGS[language].layers.data}${colon}${layered.summaries.join(gap)}`;
626
887
  }
888
+ if (surface.over !== undefined)
889
+ inner += surface.over;
627
890
  if (notes !== undefined && notes.svg !== "") {
628
891
  inner += notes.svg;
629
892
  if (notes.summaries.length > 0 && o.description === undefined)
630
893
  description = `${description}${KYOGIJO_STRINGS[language].said.gap}${notes.summaries.join(KYOGIJO_STRINGS[language].said.gap)}`;
631
894
  }
632
- if (isScore(o) && left !== undefined && right !== undefined && sized === "full") {
895
+ if (isScore(o) && left !== undefined && right !== undefined && sized === "full" && o.plate !== false) {
633
896
  const foot = o.label !== undefined && o.label !== "" ? o.label : [status, o.date].filter((part) => part !== undefined && part !== "").join(" · ");
634
897
  if (sport === "curling") {
635
898
  const board = boardMarkup(surface.plate, left, right, { competition: o.competition, foot: foot === "" ? undefined : foot, total: KYOGIJO_STRINGS[language].said.total }, { turned: surface.turned, parts });
@@ -640,8 +903,8 @@ export function drawKyogijo(o) {
640
903
  const board = cricketBoardMarkup(surface.plate, left, right, { competition: o.competition, foot: foot === "" ? undefined : foot, innings: words.innings, total: words.total, overs: words.overs }, { parts });
641
904
  inner += el("g", { transform: `translate(${num(surface.plate.cx)} ${num(surface.plate.cy)})${surface.turned ? " rotate(-90)" : ""}` }, board);
642
905
  }
643
- else if (isNetSport(sport)) {
644
- const board = setsBoardMarkup(surface.plate, left, right, { competition: o.competition, foot: foot === "" ? undefined : foot, head: KYOGIJO_STRINGS[language].net[sport === "badminton" || sport === "pickleball" || sport === "tabletennis" ? "games" : "sets"], points: KYOGIJO_STRINGS[language].net.points }, { parts, least: minSets(sport, chooseLeague(o).league?.id) });
906
+ else if (hasSetsBoard(sport)) {
907
+ const board = setsBoardMarkup(surface.plate, left, right, { competition: o.competition, foot: foot === "" ? undefined : foot, head: KYOGIJO_STRINGS[language].net[BOARD[sport] === "games" ? "games" : "sets"], points: KYOGIJO_STRINGS[language].net.points }, { parts, least: minSets(sport, chooseLeague(o).league?.id) });
645
908
  inner += el("g", { transform: `translate(${num(surface.plate.cx)} ${num(surface.plate.cy)})${surface.turned ? " rotate(-90)" : ""}` }, board);
646
909
  }
647
910
  else
@@ -676,14 +939,16 @@ export function drawKyogijo(o) {
676
939
  "data-orientation": portrait ? "portrait" : "landscape",
677
940
  "data-theme": theme,
678
941
  "data-palette": palette,
942
+ "data-track": sport === "athletics" ? o.athletics?.track : undefined,
679
943
  "data-winner": isScore(o) ? winner : undefined,
944
+ "data-pageant": pg === undefined ? undefined : pageant?.style,
680
945
  "data-version": VERSION,
681
946
  width: safeLength(o.width),
682
947
  height: safeLength(o.height),
683
948
  part: parts ? "svg" : undefined,
684
949
  style: style === "" ? undefined : style,
685
950
  }, `<title id="${titleId}">${esc(title)}</title><desc id="${descId}">${esc(description)}</desc>` +
686
- (o.style === "none" ? "" : `<style>${KYOGIJO_BASE_STYLE}${left?.round === true || right?.round === true ? `\n${KYOGIJO_MARK_STYLE}` : ""}${sport === "curling" ? `\n${KYOGIJO_CURLING_STYLE}` : ""}${plan === undefined ? "" : `\n${courtStyle(plan.sport, plan.look)}`}${fieldsStyle(sport) === "" ? "" : `\n${fieldsStyle(sport)}`}${palette === undefined ? "" : `\n${lookStyle(palette)}`}${walled ? `\n${KYOGIJO_WALLS_STYLE}` : ""}${features === undefined ? "" : `\n${featureStyle(sport, features)}`}${notes === undefined || notes.svg === "" ? "" : `\n${KYOGIJO_NOTE_STYLE}`}${grounded ? `\n${KYOGIJO_BACKGROUND_STYLE}` : ""}${o.host === undefined ? "" : `\n${specialStyle(hostSport(o.host))}`}</style>`) +
951
+ (o.style === "none" ? "" : `<style>${KYOGIJO_BASE_STYLE}${left?.round === true || right?.round === true || (pg !== undefined && pageant?.style === "stage") || (pg !== undefined && pageant?.emblem?.shape !== "plain" && pageant?.emblem !== undefined) ? `\n${KYOGIJO_MARK_STYLE}` : ""}${pg === undefined ? "" : `\n${KYOGIJO_PAGEANT_STYLE}${pageant?.animate === undefined || pageant.animate === "none" ? "" : `\n${KYOGIJO_PAGEANT_MOTION}`}`}${sport === "curling" ? `\n${KYOGIJO_CURLING_STYLE}` : ""}${foldedSheet ? `\n${KYOGIJO_FOLD_STYLE}` : ""}${isPoolSport(sport) ? `\n${o.pool?.venue !== undefined ? KYOGIJO_POOL_STYLE : poolStyle({ sport, ...commonOf(o.pool ?? {}), race: o.pool?.race !== undefined, section: o.pool?.section !== undefined && o.pool.section !== false })}` : ""}${plan === undefined ? "" : `\n${courtStyle(plan.sport, plan.look)}`}${fieldsStyle(sport) === "" ? "" : `\n${fieldsStyle(sport)}`}${athleticsStyle(sport) === "" ? "" : `\n${athleticsStyle(sport)}`}${skatingStyle(sport) === "" ? "" : `\n${skatingStyle(sport)}`}${palette === undefined ? "" : `\n${lookStyle(palette)}`}${athleticsTrackRules(sport, o.athletics?.track) === "" ? "" : `\n${athleticsTrackRules(sport, o.athletics?.track)}`}${walled ? `\n${KYOGIJO_WALLS_STYLE}` : ""}${hardwood ? `\n${KYOGIJO_HARDWOOD_STYLE}` : ""}${features === undefined ? "" : `\n${featureStyle(sport, features)}`}${notes === undefined || notes.svg === "" ? "" : `\n${KYOGIJO_NOTE_STYLE}`}${grounded ? `\n${KYOGIJO_BACKGROUND_STYLE}` : ""}${crowded ? `\n${KYOGIJO_CROWD_STYLE}` : ""}${o.host === undefined ? "" : `\n${specialStyle(hostSport(o.host))}`}</style>`) +
687
952
  (defs === "" ? "" : `<defs>${defs}</defs>`) +
688
953
  body);
689
954
  return root;
@@ -703,3 +968,21 @@ export function drawScore(options) {
703
968
  export function drawBadge(options) {
704
969
  return drawKyogijo({ ...options, size: "badge" });
705
970
  }
971
+ /**
972
+ * The stands round a surface as data: the rings of seats with their outlines, heights and rake, the blocks of visitors and the places left open, in feet in the surface's
973
+ * wide frame, so that another view (a 3D one) can raise them. `undefined` when the options ask for no crowd (or for a special game, which has none).
974
+ * @example
975
+ * ```ts
976
+ * import { crowdStands } from "@johnmorrisdotca/kyogijo-stadiums";
977
+ * const stands = crowdStands({ sport: "soccer", crowd: true });
978
+ * console.log(stands?.rings.map((ring) => ring.id).join(" "), stands?.shape, stands?.stadium);
979
+ * // lower upper bowl purpose
980
+ * ```
981
+ */
982
+ export function crowdStands(o) {
983
+ const crowd = crowdOf(o);
984
+ if (crowd === undefined)
985
+ return undefined;
986
+ const probe = buildWhole({ ...o, margin: 0 }, false, drawingId(o), false, false, { features: undefined });
987
+ return standsFor(o, probe, crowd);
988
+ }