@johnmorrisdotca/kyogijo-stadiums 0.0.0-stage → 0.4.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 (321) hide show
  1. package/CHANGELOG.md +129 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE.md +293 -0
  4. package/README.md +600 -2
  5. package/dist/annotate.d.ts +47 -0
  6. package/dist/annotate.js +349 -0
  7. package/dist/atlas-entry.d.ts +4 -0
  8. package/dist/atlas-entry.js +3 -0
  9. package/dist/atlas.d.ts +131 -0
  10. package/dist/atlas.js +149 -0
  11. package/dist/background.d.ts +56 -0
  12. package/dist/background.js +127 -0
  13. package/dist/ballpark.d.ts +48 -0
  14. package/dist/ballpark.js +129 -0
  15. package/dist/canvas-entry.d.ts +2 -0
  16. package/dist/canvas-entry.js +2 -0
  17. package/dist/canvas.d.ts +85 -0
  18. package/dist/canvas.js +279 -0
  19. package/dist/charts-entry.d.ts +10 -0
  20. package/dist/charts-entry.js +8 -0
  21. package/dist/charts.baseball.d.ts +30 -0
  22. package/dist/charts.baseball.js +152 -0
  23. package/dist/charts.curling.d.ts +52 -0
  24. package/dist/charts.curling.js +196 -0
  25. package/dist/charts.d.ts +172 -0
  26. package/dist/charts.js +508 -0
  27. package/dist/charts.network.d.ts +66 -0
  28. package/dist/charts.network.js +64 -0
  29. package/dist/charts.soccer.d.ts +43 -0
  30. package/dist/charts.soccer.js +191 -0
  31. package/dist/charts.types.d.ts +396 -0
  32. package/dist/charts.types.js +1 -0
  33. package/dist/colour.d.ts +29 -0
  34. package/dist/colour.js +97 -0
  35. package/dist/common.d.ts +14 -0
  36. package/dist/common.js +32 -0
  37. package/dist/court.d.ts +89 -0
  38. package/dist/court.js +248 -0
  39. package/dist/courts.style.d.ts +20 -0
  40. package/dist/courts.style.js +54 -0
  41. package/dist/cricket.board.d.ts +18 -0
  42. package/dist/cricket.board.js +67 -0
  43. package/dist/curling.board.d.ts +24 -0
  44. package/dist/curling.board.js +147 -0
  45. package/dist/d3-entry.d.ts +3 -0
  46. package/dist/d3-entry.js +2 -0
  47. package/dist/d3.d.ts +45 -0
  48. package/dist/d3.js +56 -0
  49. package/dist/draw.d.ts +52 -0
  50. package/dist/draw.js +705 -0
  51. package/dist/element-define.d.ts +2 -0
  52. package/dist/element-define.js +4 -0
  53. package/dist/element-entry.d.ts +1 -0
  54. package/dist/element-entry.js +2 -0
  55. package/dist/element.d.ts +39 -0
  56. package/dist/element.js +321 -0
  57. package/dist/eras-entry.d.ts +2 -0
  58. package/dist/eras-entry.js +2 -0
  59. package/dist/eras.d.ts +204 -0
  60. package/dist/eras.js +396 -0
  61. package/dist/events-entry.d.ts +3 -0
  62. package/dist/events-entry.js +3 -0
  63. package/dist/events.d.ts +54 -0
  64. package/dist/events.data.d.ts +59 -0
  65. package/dist/events.data.js +297 -0
  66. package/dist/events.js +127 -0
  67. package/dist/features.d.ts +31 -0
  68. package/dist/features.js +284 -0
  69. package/dist/field.score.d.ts +5 -0
  70. package/dist/field.score.js +33 -0
  71. package/dist/fields/afl.d.ts +11 -0
  72. package/dist/fields/afl.js +103 -0
  73. package/dist/fields/cricket.d.ts +9 -0
  74. package/dist/fields/cricket.js +81 -0
  75. package/dist/fields/fieldhockey.d.ts +6 -0
  76. package/dist/fields/fieldhockey.js +81 -0
  77. package/dist/fields/gaelic.d.ts +10 -0
  78. package/dist/fields/gaelic.js +86 -0
  79. package/dist/fields/handball.d.ts +6 -0
  80. package/dist/fields/handball.js +85 -0
  81. package/dist/fields/lacrosse.d.ts +17 -0
  82. package/dist/fields/lacrosse.js +169 -0
  83. package/dist/fields/rugby.d.ts +10 -0
  84. package/dist/fields/rugby.js +130 -0
  85. package/dist/fields.d.ts +12 -0
  86. package/dist/fields.js +44 -0
  87. package/dist/fields.kit.d.ts +63 -0
  88. package/dist/fields.kit.js +108 -0
  89. package/dist/fields.style.d.ts +4 -0
  90. package/dist/fields.style.js +19 -0
  91. package/dist/fields.types.d.ts +155 -0
  92. package/dist/fields.types.js +1 -0
  93. package/dist/format.d.ts +10 -0
  94. package/dist/format.js +28 -0
  95. package/dist/formations.d.ts +18 -0
  96. package/dist/formations.js +211 -0
  97. package/dist/frames-entry.d.ts +8 -0
  98. package/dist/frames-entry.js +7 -0
  99. package/dist/frames.d.ts +199 -0
  100. package/dist/frames.js +271 -0
  101. package/dist/frames.providers.d.ts +16 -0
  102. package/dist/frames.providers.js +302 -0
  103. package/dist/geojson-entry.d.ts +2 -0
  104. package/dist/geojson-entry.js +2 -0
  105. package/dist/geojson.d.ts +79 -0
  106. package/dist/geojson.js +147 -0
  107. package/dist/geometry-entry.d.ts +7 -0
  108. package/dist/geometry-entry.js +5 -0
  109. package/dist/geometry.d.ts +295 -0
  110. package/dist/geometry.js +361 -0
  111. package/dist/grounds-entry.d.ts +4 -0
  112. package/dist/grounds-entry.js +3 -0
  113. package/dist/grounds.d.ts +134 -0
  114. package/dist/grounds.data.d.ts +2 -0
  115. package/dist/grounds.data.js +1849 -0
  116. package/dist/grounds.js +74 -0
  117. package/dist/heat.colour.d.ts +37 -0
  118. package/dist/heat.colour.js +54 -0
  119. package/dist/heat.d.ts +43 -0
  120. package/dist/heat.js +614 -0
  121. package/dist/heat.stats.d.ts +123 -0
  122. package/dist/heat.stats.js +413 -0
  123. package/dist/heat.types.d.ts +145 -0
  124. package/dist/heat.types.js +1 -0
  125. package/dist/heat.zones.d.ts +54 -0
  126. package/dist/heat.zones.js +424 -0
  127. package/dist/homeside.d.ts +16 -0
  128. package/dist/homeside.js +43 -0
  129. package/dist/index.d.ts +47 -0
  130. package/dist/index.js +29 -0
  131. package/dist/layers-entry.d.ts +19 -0
  132. package/dist/layers-entry.js +11 -0
  133. package/dist/layers.d.ts +41 -0
  134. package/dist/layers.env.d.ts +72 -0
  135. package/dist/layers.env.js +79 -0
  136. package/dist/layers.js +533 -0
  137. package/dist/layers.marks.d.ts +45 -0
  138. package/dist/layers.marks.js +168 -0
  139. package/dist/layers.types.d.ts +226 -0
  140. package/dist/layers.types.js +1 -0
  141. package/dist/layout.d.ts +97 -0
  142. package/dist/layout.js +381 -0
  143. package/dist/layout.style.d.ts +6 -0
  144. package/dist/layout.style.js +17 -0
  145. package/dist/layouts-entry.d.ts +5 -0
  146. package/dist/layouts-entry.js +4 -0
  147. package/dist/leagues/aussie.d.ts +57 -0
  148. package/dist/leagues/aussie.js +36 -0
  149. package/dist/leagues/baseball.d.ts +58 -0
  150. package/dist/leagues/baseball.js +171 -0
  151. package/dist/leagues/basketball.d.ts +67 -0
  152. package/dist/leagues/basketball.js +128 -0
  153. package/dist/leagues/courts.d.ts +212 -0
  154. package/dist/leagues/courts.js +114 -0
  155. package/dist/leagues/cricket.d.ts +45 -0
  156. package/dist/leagues/cricket.js +34 -0
  157. package/dist/leagues/curling.d.ts +64 -0
  158. package/dist/leagues/curling.js +53 -0
  159. package/dist/leagues/fieldhockey.d.ts +48 -0
  160. package/dist/leagues/fieldhockey.js +29 -0
  161. package/dist/leagues/fields.d.ts +10 -0
  162. package/dist/leagues/fields.js +6 -0
  163. package/dist/leagues/football.d.ts +8 -0
  164. package/dist/leagues/football.js +82 -0
  165. package/dist/leagues/gaelic.d.ts +50 -0
  166. package/dist/leagues/gaelic.js +29 -0
  167. package/dist/leagues/handball.d.ts +45 -0
  168. package/dist/leagues/handball.js +32 -0
  169. package/dist/leagues/hockey.d.ts +63 -0
  170. package/dist/leagues/hockey.js +166 -0
  171. package/dist/leagues/lacrosse.d.ts +103 -0
  172. package/dist/leagues/lacrosse.js +175 -0
  173. package/dist/leagues/rugby.d.ts +59 -0
  174. package/dist/leagues/rugby.js +72 -0
  175. package/dist/leagues/sources.d.ts +18 -0
  176. package/dist/leagues/sources.js +95 -0
  177. package/dist/leagues/types.d.ts +43 -0
  178. package/dist/leagues/types.js +5 -0
  179. package/dist/leagues-entry.d.ts +23 -0
  180. package/dist/leagues-entry.js +15 -0
  181. package/dist/leagues.d.ts +172 -0
  182. package/dist/leagues.js +462 -0
  183. package/dist/look.d.ts +50 -0
  184. package/dist/look.js +186 -0
  185. package/dist/mark.d.ts +35 -0
  186. package/dist/mark.js +59 -0
  187. package/dist/mount-entry.d.ts +2 -0
  188. package/dist/mount-entry.js +2 -0
  189. package/dist/mount.d.ts +63 -0
  190. package/dist/mount.js +167 -0
  191. package/dist/mow.d.ts +36 -0
  192. package/dist/mow.js +111 -0
  193. package/dist/names.d.ts +77 -0
  194. package/dist/names.js +97 -0
  195. package/dist/outline.d.ts +25 -0
  196. package/dist/outline.js +112 -0
  197. package/dist/parks-entry.d.ts +4 -0
  198. package/dist/parks-entry.js +3 -0
  199. package/dist/parks.d.ts +200 -0
  200. package/dist/parks.data.d.ts +2 -0
  201. package/dist/parks.data.js +4489 -0
  202. package/dist/parks.js +92 -0
  203. package/dist/plot-entry.d.ts +2 -0
  204. package/dist/plot-entry.js +2 -0
  205. package/dist/plot.d.ts +74 -0
  206. package/dist/plot.js +68 -0
  207. package/dist/png-entry.d.ts +2 -0
  208. package/dist/png-entry.js +2 -0
  209. package/dist/png.d.ts +26 -0
  210. package/dist/png.js +46 -0
  211. package/dist/ranges.d.ts +33 -0
  212. package/dist/ranges.js +178 -0
  213. package/dist/ranges.names.d.ts +31 -0
  214. package/dist/ranges.names.js +65 -0
  215. package/dist/react-entry.d.ts +2 -0
  216. package/dist/react-entry.js +2 -0
  217. package/dist/react.d.ts +53 -0
  218. package/dist/react.js +58 -0
  219. package/dist/replay-entry.d.ts +5 -0
  220. package/dist/replay-entry.js +4 -0
  221. package/dist/replay.control.d.ts +47 -0
  222. package/dist/replay.control.js +184 -0
  223. package/dist/replay.d.ts +170 -0
  224. package/dist/replay.dom.d.ts +21 -0
  225. package/dist/replay.dom.js +388 -0
  226. package/dist/replay.js +579 -0
  227. package/dist/replay.types.d.ts +152 -0
  228. package/dist/replay.types.js +1 -0
  229. package/dist/restyle.d.ts +21 -0
  230. package/dist/restyle.js +70 -0
  231. package/dist/road.d.ts +26 -0
  232. package/dist/road.draw.d.ts +6 -0
  233. package/dist/road.draw.js +269 -0
  234. package/dist/road.js +59 -0
  235. package/dist/road.model.d.ts +63 -0
  236. package/dist/road.model.js +215 -0
  237. package/dist/road.style.d.ts +7 -0
  238. package/dist/road.style.js +26 -0
  239. package/dist/road.types.d.ts +122 -0
  240. package/dist/road.types.js +1 -0
  241. package/dist/scene-entry.d.ts +7 -0
  242. package/dist/scene-entry.js +5 -0
  243. package/dist/scene.css.d.ts +44 -0
  244. package/dist/scene.css.js +296 -0
  245. package/dist/scene.d.ts +27 -0
  246. package/dist/scene.fit.d.ts +12 -0
  247. package/dist/scene.fit.js +18 -0
  248. package/dist/scene.flat.d.ts +38 -0
  249. package/dist/scene.flat.js +167 -0
  250. package/dist/scene.js +420 -0
  251. package/dist/scene.path.d.ts +40 -0
  252. package/dist/scene.path.js +357 -0
  253. package/dist/scene.types.d.ts +172 -0
  254. package/dist/scene.types.js +1 -0
  255. package/dist/scene.xml.d.ts +31 -0
  256. package/dist/scene.xml.js +81 -0
  257. package/dist/score.d.ts +70 -0
  258. package/dist/score.js +212 -0
  259. package/dist/sets.board.d.ts +22 -0
  260. package/dist/sets.board.js +89 -0
  261. package/dist/sheet.d.ts +18 -0
  262. package/dist/sheet.js +38 -0
  263. package/dist/special.d.ts +44 -0
  264. package/dist/special.js +204 -0
  265. package/dist/special.style.d.ts +8 -0
  266. package/dist/special.style.js +36 -0
  267. package/dist/strings.d.ts +581 -0
  268. package/dist/strings.js +411 -0
  269. package/dist/style.d.ts +35 -0
  270. package/dist/style.js +177 -0
  271. package/dist/surfaces/baseball.d.ts +33 -0
  272. package/dist/surfaces/baseball.js +208 -0
  273. package/dist/surfaces/baseball.walls.d.ts +16 -0
  274. package/dist/surfaces/baseball.walls.js +150 -0
  275. package/dist/surfaces/basketball.d.ts +8 -0
  276. package/dist/surfaces/basketball.js +199 -0
  277. package/dist/surfaces/curling.d.ts +23 -0
  278. package/dist/surfaces/curling.js +128 -0
  279. package/dist/surfaces/field.d.ts +12 -0
  280. package/dist/surfaces/field.js +163 -0
  281. package/dist/surfaces/football.d.ts +12 -0
  282. package/dist/surfaces/football.js +208 -0
  283. package/dist/surfaces/hockey.d.ts +10 -0
  284. package/dist/surfaces/hockey.js +166 -0
  285. package/dist/surfaces/net.d.ts +7 -0
  286. package/dist/surfaces/net.js +151 -0
  287. package/dist/surfaces/soccer-providers.d.ts +124 -0
  288. package/dist/surfaces/soccer-providers.js +334 -0
  289. package/dist/surfaces/soccer-spec.d.ts +138 -0
  290. package/dist/surfaces/soccer-spec.js +175 -0
  291. package/dist/surfaces/soccer.d.ts +25 -0
  292. package/dist/surfaces/soccer.js +270 -0
  293. package/dist/surfaces/surface.d.ts +180 -0
  294. package/dist/surfaces/surface.js +49 -0
  295. package/dist/team.common.d.ts +75 -0
  296. package/dist/team.common.js +97 -0
  297. package/dist/team.formation.d.ts +24 -0
  298. package/dist/team.formation.js +121 -0
  299. package/dist/team.geometry.d.ts +66 -0
  300. package/dist/team.geometry.js +304 -0
  301. package/dist/team.layers.d.ts +16 -0
  302. package/dist/team.layers.js +32 -0
  303. package/dist/team.motion.d.ts +9 -0
  304. package/dist/team.motion.js +325 -0
  305. package/dist/team.network.d.ts +43 -0
  306. package/dist/team.network.js +215 -0
  307. package/dist/team.shape.d.ts +12 -0
  308. package/dist/team.shape.js +217 -0
  309. package/dist/team.types.d.ts +189 -0
  310. package/dist/team.types.js +1 -0
  311. package/dist/text.d.ts +31 -0
  312. package/dist/text.js +88 -0
  313. package/dist/themes.d.ts +14 -0
  314. package/dist/themes.js +24 -0
  315. package/dist/types.d.ts +565 -0
  316. package/dist/types.js +1 -0
  317. package/dist/units.d.ts +45 -0
  318. package/dist/units.js +90 -0
  319. package/dist/version.d.ts +2 -0
  320. package/dist/version.js +2 -0
  321. package/package.json +234 -4
package/dist/parks.js ADDED
@@ -0,0 +1,92 @@
1
+ import { FEET_PER_METRE, fenceAt, fencePoints, DEFAULT_FENCE } from "./geometry.js";
2
+ import { PARK_DATA } from "./parks.data.js";
3
+ import { officialName, placesNamed } from "./names.js";
4
+ /** The address of the revision of an article that a park's figures were read from. */
5
+ export function parkSourceUrl(source, language = "en") {
6
+ return `https://${language}.wikipedia.org/w/index.php?title=${encodeURIComponent(source.title.replace(/ /g, "_"))}&oldid=${source.revision}`;
7
+ }
8
+ const toFeet = (data) => {
9
+ const { points, unit, periods, walls, ...rest } = data;
10
+ const scale = unit === "m" ? FEET_PER_METRE : 1;
11
+ const feet = (list) => list.map(([angle, distance]) => [angle, Math.round(distance * scale * 10) / 10]);
12
+ const high = (wall) => {
13
+ const { height, ...stretch } = wall;
14
+ return { ...stretch, height_ft: Math.round(height * scale * 1000) / 1000 };
15
+ };
16
+ return {
17
+ ...rest,
18
+ points: feet(points),
19
+ ...(periods === undefined ? {} : { periods: periods.map((period) => ({ ...period, points: feet(period.points) })) }),
20
+ ...(walls === undefined ? {} : { walls: walls.map(high) }),
21
+ };
22
+ };
23
+ /** Every park by its key: the thirty current MLB ballparks, forty-three past ones, the twelve NPB home parks, and eight secondary or past NPB grounds. */
24
+ export const PARKS = Object.freeze(Object.fromEntries(PARK_DATA.map((data) => [data.id, toFeet(data)])));
25
+ /** The keys of `PARKS`: the current MLB parks, then the past ones by their first season, then the NPB's, each in the order of the sources. */
26
+ export const PARK_IDS = Object.freeze(PARK_DATA.map((data) => data.id));
27
+ /** The unit a park's figures were published in: metres for the parks of Japan (converted to feet once, in `PARKS`), feet for the rest. `undefined` for a key that is not a park. */
28
+ export const parkUnit = (id) => PARK_DATA.find((data) => data.id === id)?.unit;
29
+ /**
30
+ * The parks that answer to a name, best match first: the everyday names, then the official and cited names and the sponsors', then the former names and the nicknames. A name is
31
+ * compared without case, accents, spaces or punctuation, in full-width or half-width, and in Japanese with hiragana or katakana and with or without the long vowel mark.
32
+ * @example
33
+ * ```ts
34
+ * import { parksByName } from "@johnmorrisdotca/kyogijo-stadiums/parks";
35
+ * console.log(parksByName("Camden Yards")[0]?.id, parksByName("甲子園")[0]?.id, parksByName("ハマスタ")[0]?.id);
36
+ * // oriole-park-at-camden-yards hanshin-koshien-stadium yokohama-stadium
37
+ * ```
38
+ */
39
+ export const parksByName = (name) => placesNamed(Object.values(PARKS), name);
40
+ /** The park a name belongs to (see `parksByName`; the best match when several answer to it), or `undefined`. */
41
+ export const parkByName = (name) => parksByName(name)[0];
42
+ /** A park's name in a language: the everyday name in English, or in Japanese. */
43
+ export const parkNameIn = (park, language = "en") => (language === "ja" ? park.nameJa : park.name);
44
+ /** The name a park officially has in a language, when it is not the everyday name (`Hanshin Koshien Stadium` for `Koshien Stadium`), in the year given or today; else `undefined`. */
45
+ export const parkOfficialName = (park, language = "en", year) => officialName(park, language, undefined, Number.isFinite(year) ? year : undefined);
46
+ /** The park with a key, or `undefined`. */
47
+ export const parkById = (id) => (id === undefined ? undefined : PARKS[id]);
48
+ /** The park whose Wikidata item is `Q49136`, or `undefined`. */
49
+ export const parkByWikidata = (wikidata) => Object.values(PARKS).find((park) => park.wikidata === wikidata);
50
+ /** A park's fence in a season: the period that names the season, else the park's last fence. */
51
+ export function parkPointsIn(park, year) {
52
+ if (year === undefined || !Number.isFinite(year))
53
+ return park.points;
54
+ return park.periods?.find((period) => period.from <= year && year <= period.to)?.points ?? park.points;
55
+ }
56
+ /** True when something dated by its `first` and `last` season stands in `year`; without a year, what stands today (has no `last`). */
57
+ const standsIn = (dated, year) => {
58
+ if (year === undefined || !Number.isFinite(year))
59
+ return dated.last === undefined;
60
+ return (dated.first === undefined || dated.first <= year) && (dated.last === undefined || year <= dated.last);
61
+ };
62
+ /** The stretches of fence a park has in a season, with their heights in feet: the heights the article dates for that season, else those of today. */
63
+ export function parkWallsIn(park, year) {
64
+ return (park.walls ?? []).filter((wall) => standsIn(wall, year));
65
+ }
66
+ /** The quirks a park has in a season: those the article dates for that season, else those of today. */
67
+ export function parkQuirksIn(park, year) {
68
+ return (park.quirks ?? []).filter((quirk) => standsIn(quirk, year));
69
+ }
70
+ /**
71
+ * The fence points a drawing uses: a park's (as it stood in `year`, when the park has dated fences), with any of the
72
+ * five usual distances in `fence` laid over them, or the usual 330, 375, 400, 375 and 330 feet of a generic field (or the
73
+ * `generic` points a league's rules give).
74
+ */
75
+ export function fenceFor(park, fence, year, generic = fencePoints(DEFAULT_FENCE)) {
76
+ const base = park === undefined ? [...generic] : parkPointsIn(park, year);
77
+ if (fence === undefined)
78
+ return base;
79
+ const keys = [["left", -45], ["leftCenter", -22.5], ["center", 0], ["rightCenter", 22.5], ["right", 45]];
80
+ const given = keys.filter(([key]) => typeof fence[key] === "number" && Number.isFinite(fence[key]) && fence[key] > 0);
81
+ if (given.length === 0)
82
+ return base;
83
+ // The caller's distances stand at their angles; the park's own points that fall near one of them give way.
84
+ const mine = given.map(([key, angle]) => [angle, fence[key]]);
85
+ const kept = base.filter(([angle]) => !mine.some(([chosen]) => Math.abs(chosen - angle) < 11));
86
+ const filled = [...kept, ...mine];
87
+ if (!filled.some(([angle]) => angle === -45))
88
+ filled.push([-45, fenceAt(base, -45)]);
89
+ if (!filled.some(([angle]) => angle === 45))
90
+ filled.push([45, fenceAt(base, 45)]);
91
+ return filled.sort((a, b) => a[0] - b[0]);
92
+ }
@@ -0,0 +1,2 @@
1
+ export { surfaceMark, surfaceScales } from "./plot.ts";
2
+ export type { PlotContext, PlotDocument, PlotElement, PlotScales, SurfaceMark, SurfaceScales } from "./plot.ts";
@@ -0,0 +1,2 @@
1
+ // A drawing as a mark for Observable Plot, with Plot not imported.
2
+ export { surfaceMark, surfaceScales } from "./plot.js";
package/dist/plot.d.ts ADDED
@@ -0,0 +1,74 @@
1
+ import { type ScaleFunction } from "./scene.fit.ts";
2
+ import type { SceneOptions, SceneSource } from "./scene.types.ts";
3
+ /** The members of a document that are used. A browser's `document` has them, and so does the one Plot gives its marks in `context.document`. */
4
+ export interface PlotDocument {
5
+ createElementNS(namespace: string, name: string): PlotElement;
6
+ }
7
+ export interface PlotElement {
8
+ setAttribute(name: string, value: string): void;
9
+ appendChild(child: PlotElement): unknown;
10
+ textContent: string | null;
11
+ }
12
+ /** The scales Plot gives a mark: `x` and `y` are functions. (Plot's scales are also nested in `scales`, which is not read.) */
13
+ export interface PlotScales {
14
+ x: ScaleFunction;
15
+ y: ScaleFunction;
16
+ }
17
+ /** What Plot gives a mark when it draws it. Only `document` of the context is read. */
18
+ export interface PlotContext {
19
+ document?: PlotDocument;
20
+ }
21
+ /** The options that make a plot's x and y scales the surface's: its extent, no axes, no margin and a foot as wide as it is tall. Spread them into `Plot.plot`. */
22
+ export interface SurfaceScales {
23
+ x: {
24
+ domain: [number, number];
25
+ axis: null;
26
+ };
27
+ y: {
28
+ domain: [number, number];
29
+ axis: null;
30
+ reverse: true;
31
+ };
32
+ aspectRatio: 1;
33
+ margin: 0;
34
+ inset: 0;
35
+ }
36
+ /**
37
+ * The options that give a plot the surface's scales: the drawing's numbers (feet from the surface's left end and its top, see `surfaceDomain`) on both axes with
38
+ * y growing down as in a drawing, the same size for a foot across and down, no axis and no margin. Spread them into `Plot.plot` and put `surfaceMark` first.
39
+ * @example
40
+ * ```ts
41
+ * import { surfaceScales } from "@johnmorrisdotca/kyogijo-stadiums/plot";
42
+ * console.log(surfaceScales({ sport: "nhl", id: "rink" }));
43
+ * // {
44
+ * // x: { domain: [ -8, 208 ], axis: null },
45
+ * // y: { domain: [ -8, 93 ], axis: null, reverse: true },
46
+ * // aspectRatio: 1,
47
+ * // margin: 0,
48
+ * // inset: 0
49
+ * // }
50
+ * ```
51
+ */
52
+ export declare function surfaceScales(source: SceneSource, options?: SceneOptions): SurfaceScales;
53
+ /** A mark for `Plot.plot`: a function that Plot calls with the scales when it renders. */
54
+ export type SurfaceMark = (index: unknown, scales: PlotScales, values: unknown, dimensions: unknown, context: PlotContext) => PlotElement;
55
+ /**
56
+ * A surface as a mark for Observable Plot, drawn where the plot's `x` and `y` scales put the drawing's numbers, so that the marks after it land on the
57
+ * rink, the court or the field. Plot takes a function as a mark, so there is nothing to import; put it first in `marks`, with `surfaceScales` for the scales.
58
+ * Every colour and width is an attribute of the shape, so the page needs no stylesheet; light or dark by `theme`.
59
+ * @example
60
+ * ```ts
61
+ * import { surfaceMark, surfaceScales } from "@johnmorrisdotca/kyogijo-stadiums/plot";
62
+ * // With Plot: Plot.plot({ ...surfaceScales(options), marks: [surfaceMark(options), Plot.dot(shots, { x: "x", y: "y" })] })
63
+ * const options = { sport: "nhl", id: "rink" } as const;
64
+ * const made: string[] = [];
65
+ * const document = { createElementNS: (_: string, name: string) => { made.push(name); return { setAttribute: () => undefined, appendChild: () => undefined, textContent: null as string | null }; } };
66
+ * const scale = (v: number) => (v + 8) * 4;
67
+ * const node = surfaceMark(options)(0, { x: scale, y: scale }, {}, {}, { document });
68
+ * console.log(made[0], made.length > 30, typeof node);
69
+ * // g true object
70
+ * ```
71
+ */
72
+ export declare function surfaceMark(source: SceneSource, options?: SceneOptions & {
73
+ className?: string;
74
+ }): SurfaceMark;
package/dist/plot.js ADDED
@@ -0,0 +1,68 @@
1
+ // A drawing in an Observable Plot. Nothing is imported from Plot: it takes a function as a mark, and calls it with the scales and a document to make elements in.
2
+ import { affineOf, domainOf } from "./scene.fit.js";
3
+ import { bodyOf, defsOf, matrixAttr } from "./scene.flat.js";
4
+ import { sceneOf } from "./scene.js";
5
+ const SVG = "http://www.w3.org/2000/svg";
6
+ /**
7
+ * The options that give a plot the surface's scales: the drawing's numbers (feet from the surface's left end and its top, see `surfaceDomain`) on both axes with
8
+ * y growing down as in a drawing, the same size for a foot across and down, no axis and no margin. Spread them into `Plot.plot` and put `surfaceMark` first.
9
+ * @example
10
+ * ```ts
11
+ * import { surfaceScales } from "@johnmorrisdotca/kyogijo-stadiums/plot";
12
+ * console.log(surfaceScales({ sport: "nhl", id: "rink" }));
13
+ * // {
14
+ * // x: { domain: [ -8, 208 ], axis: null },
15
+ * // y: { domain: [ -8, 93 ], axis: null, reverse: true },
16
+ * // aspectRatio: 1,
17
+ * // margin: 0,
18
+ * // inset: 0
19
+ * // }
20
+ * ```
21
+ */
22
+ export function surfaceScales(source, options = {}) {
23
+ const { x, y } = domainOf(sceneOf(source, options));
24
+ return { x: { domain: x, axis: null }, y: { domain: y, axis: null, reverse: true }, aspectRatio: 1, margin: 0, inset: 0 };
25
+ }
26
+ function make(document, spec) {
27
+ const node = document.createElementNS(SVG, spec.tag);
28
+ for (const [name, value] of Object.entries(spec.attrs))
29
+ node.setAttribute(name, value);
30
+ if (spec.text !== undefined)
31
+ node.textContent = spec.text;
32
+ for (const child of spec.children ?? [])
33
+ node.appendChild(make(document, child));
34
+ return node;
35
+ }
36
+ /**
37
+ * A surface as a mark for Observable Plot, drawn where the plot's `x` and `y` scales put the drawing's numbers, so that the marks after it land on the
38
+ * rink, the court or the field. Plot takes a function as a mark, so there is nothing to import; put it first in `marks`, with `surfaceScales` for the scales.
39
+ * Every colour and width is an attribute of the shape, so the page needs no stylesheet; light or dark by `theme`.
40
+ * @example
41
+ * ```ts
42
+ * import { surfaceMark, surfaceScales } from "@johnmorrisdotca/kyogijo-stadiums/plot";
43
+ * // With Plot: Plot.plot({ ...surfaceScales(options), marks: [surfaceMark(options), Plot.dot(shots, { x: "x", y: "y" })] })
44
+ * const options = { sport: "nhl", id: "rink" } as const;
45
+ * const made: string[] = [];
46
+ * const document = { createElementNS: (_: string, name: string) => { made.push(name); return { setAttribute: () => undefined, appendChild: () => undefined, textContent: null as string | null }; } };
47
+ * const scale = (v: number) => (v + 8) * 4;
48
+ * const node = surfaceMark(options)(0, { x: scale, y: scale }, {}, {}, { document });
49
+ * console.log(made[0], made.length > 30, typeof node);
50
+ * // g true object
51
+ * ```
52
+ */
53
+ export function surfaceMark(source, options = {}) {
54
+ const scene = sceneOf(source, options);
55
+ return (_index, scales, _values, _dimensions, context) => {
56
+ const document = context.document ?? globalThis.document;
57
+ if (document === undefined)
58
+ throw new Error("A document is needed to make the elements: Plot passes one in context.document");
59
+ const matrix = affineOf(scales.x, scales.y);
60
+ const group = make(document, { tag: "g", attrs: { class: options.className ?? "kyogijo-surface", transform: matrixAttr(matrix) } });
61
+ const defs = defsOf(scene);
62
+ if (defs.length > 0)
63
+ group.appendChild(make(document, { tag: "defs", attrs: {}, children: defs }));
64
+ for (const spec of bodyOf(scene))
65
+ group.appendChild(make(document, spec));
66
+ return group;
67
+ };
68
+ }
@@ -0,0 +1,2 @@
1
+ export { PNG_PEER_MISSING, toPNG } from "./png.ts";
2
+ export type { PngOptions } from "./png.ts";
@@ -0,0 +1,2 @@
1
+ // A drawing as a PNG in Node, through the optional peer @resvg/resvg-js.
2
+ export { PNG_PEER_MISSING, toPNG } from "./png.js";
package/dist/png.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ import type { SceneOptions, SceneSource } from "./scene.types.ts";
2
+ /** What `toPNG` takes besides the drawing. */
3
+ export interface PngOptions extends SceneOptions {
4
+ /** The width of the picture in pixels. 1200 when neither a width nor a height is given. */
5
+ width?: number;
6
+ height?: number;
7
+ /** A colour behind the drawing; the picture is transparent where there is none. */
8
+ background?: string;
9
+ /** Options for resvg's font handling, passed as its `font` option (`{ fontFiles: [...], loadSystemFonts: false }`). */
10
+ font?: Record<string, unknown>;
11
+ }
12
+ /** The message of the error thrown when the peer is not there. */
13
+ export declare const PNG_PEER_MISSING = "toPNG needs the optional package @resvg/resvg-js: install it with `npm install @resvg/resvg-js`. It runs in Node only; in a browser draw to a canvas with drawToCanvas and call canvas.toBlob.";
14
+ /**
15
+ * A drawing as PNG bytes, by resvg-js (an optional peer dependency, loaded when this is called, in Node only). The drawing may be the options of `drawKyogijo`, an
16
+ * SVG the package wrote or a scene; light or dark by `theme`. Without the peer installed this throws an error that says what to install.
17
+ * @example
18
+ * ```ts
19
+ * import { toPNG } from "@johnmorrisdotca/kyogijo-stadiums/png";
20
+ * // Without the peer installed this stays undefined; with it, the bytes start with the PNG signature.
21
+ * const png = await toPNG({ sport: "nhl", id: "rink" }, { width: 800, theme: "dark" }).catch(() => undefined);
22
+ * console.log(png === undefined || (png[1] === 80 && png[2] === 78 && png[3] === 71));
23
+ * // true
24
+ * ```
25
+ */
26
+ export declare function toPNG(source: SceneSource, options?: PngOptions): Promise<Uint8Array>;
package/dist/png.js ADDED
@@ -0,0 +1,46 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ // A drawing as a PNG, in Node, through resvg (the optional peer `@resvg/resvg-js`). The SVG is first written without a stylesheet (see `sceneToSvg`),
10
+ // because resvg does not read CSS variables. In a browser, draw to a canvas (`drawToCanvas`) and ask it for a PNG instead.
11
+ import { sceneToSvg } from "./scene.flat.js";
12
+ /** The message of the error thrown when the peer is not there. */
13
+ export const PNG_PEER_MISSING = "toPNG needs the optional package @resvg/resvg-js: install it with `npm install @resvg/resvg-js`. It runs in Node only; in a browser draw to a canvas with drawToCanvas and call canvas.toBlob.";
14
+ /**
15
+ * A drawing as PNG bytes, by resvg-js (an optional peer dependency, loaded when this is called, in Node only). The drawing may be the options of `drawKyogijo`, an
16
+ * SVG the package wrote or a scene; light or dark by `theme`. Without the peer installed this throws an error that says what to install.
17
+ * @example
18
+ * ```ts
19
+ * import { toPNG } from "@johnmorrisdotca/kyogijo-stadiums/png";
20
+ * // Without the peer installed this stays undefined; with it, the bytes start with the PNG signature.
21
+ * const png = await toPNG({ sport: "nhl", id: "rink" }, { width: 800, theme: "dark" }).catch(() => undefined);
22
+ * console.log(png === undefined || (png[1] === 80 && png[2] === 78 && png[3] === 71));
23
+ * // true
24
+ * ```
25
+ */
26
+ export async function toPNG(source, options = {}) {
27
+ if (globalThis.process?.versions?.node === undefined)
28
+ throw new Error(PNG_PEER_MISSING);
29
+ const name = "@resvg/resvg-js";
30
+ let mod;
31
+ try {
32
+ // The name is in a variable so that a bundler does not look for the peer, and a page that never calls this does not need it.
33
+ mod = (await import(__rewriteRelativeImportExtension(/* @vite-ignore */ name)));
34
+ }
35
+ catch {
36
+ throw new Error(PNG_PEER_MISSING);
37
+ }
38
+ const wide = options.width === undefined && options.height === undefined ? 1200 : options.width;
39
+ const svg = sceneToSvg(source, { theme: options.theme, css: options.css });
40
+ const resvg = new mod.Resvg(svg, {
41
+ fitTo: wide !== undefined ? { mode: "width", value: wide } : { mode: "height", value: options.height },
42
+ ...(options.background === undefined ? {} : { background: options.background }),
43
+ ...(options.font === undefined ? { font: { loadSystemFonts: true } } : { font: options.font }),
44
+ });
45
+ return resvg.render().asPng();
46
+ }
@@ -0,0 +1,33 @@
1
+ import "./frames.providers.ts";
2
+ import { type Surface } from "./surfaces/surface.ts";
3
+ import type { Box, Language, SurfaceOptions } from "./types.ts";
4
+ /**
5
+ * The sentences a range adds to a drawing's description: what is shown, and, when the drawing has data, that data outside it is not drawn. Empty without a range.
6
+ */
7
+ export declare function describeRange(o: Pick<SurfaceOptions, "sport" | "league" | "range">, language: Language, hasData: boolean): string;
8
+ /** The part of a drawing's `range` that is a name or "custom", for the root's `data-range`. */
9
+ export declare function rangeLabel(o: Pick<SurfaceOptions, "sport" | "league" | "range">): string | undefined;
10
+ /** What the margin and the panel of a score ask of the crop. */
11
+ export interface CropOptions {
12
+ /** A prefix for ids. */
13
+ id: string;
14
+ /** The ground the annotations need all round the part that is shown, in feet; the cut's own when missing. */
15
+ margin?: number;
16
+ /** True when the drawing has its score's panel on the surface, which must then fit in what is shown. */
17
+ panel: boolean;
18
+ }
19
+ /**
20
+ * Crop a surface to the range the options ask for. The surface's view becomes the range with a small margin (the surface's own ground where the range reaches its edge, a little
21
+ * beyond the line where it is cut), `box` the part of the playing surface that is shown (`whole` the playing surface), and, when the annotations ask for room, the ground beyond the cut
22
+ * is the apron's and the surface is clipped to the range. A score's panel is moved into what is shown and made smaller if it needs to be; when it would be too small, `plateFits` says
23
+ * false and the caller draws the score as a strip. A curling sheet is cropped by its own builder (`sheet.show`), and its named ranges are that.
24
+ * @throws RangeError when the range lies wholly outside the drawing.
25
+ */
26
+ export declare function cropToRange(surface: Surface, o: Pick<SurfaceOptions, "sport" | "league" | "range" | "units">, crop: CropOptions): Surface;
27
+ /** True when a team's place (a banner's) is in the part of the drawing that is shown: always without a range. */
28
+ export declare function shows(surface: Surface, zone: {
29
+ cx: number;
30
+ cy: number;
31
+ }): boolean;
32
+ /** The part of the playing surface that is shown, in feet in the drawing's frame, when the drawing is cropped to a range; `undefined` when it is not. */
33
+ export declare function shownBox(surface: Surface): Box | undefined;
package/dist/ranges.js ADDED
@@ -0,0 +1,178 @@
1
+ // Ranges: the part of a surface a drawing is cropped to, as sportypy's `display_range` and hockey_rink's partial rinks do it, for every sport: a named part (the offensive zone
2
+ // of a rink, a half court, the red zone, the penalty area, the infield, a curling house), or a box of your own in the drawing's frame or any registered one. The viewBox is cut to the
3
+ // range with a small margin, the data and the heat of the layers are cut to it, the annotations stand against what is shown, and a score's panel is put inside it (or under it, as a strip,
4
+ // when the range is too small to hold one). The names, and where each part of a surface lies, are the builders' own (`Surface.ranges`): the code here only chooses, crops and fits.
5
+ // Written for this package; sportypy and hockey_rink were read for the idea and nothing of either is used.
6
+ import { wideView } from "./annotate.js";
7
+ import { lengthUnit } from "./common.js";
8
+ import { el, num } from "./format.js";
9
+ import { toSurface } from "./frames.js";
10
+ import "./frames.providers.js";
11
+ import { parseRange } from "./ranges.names.js";
12
+ import { frameSizeOf } from "./outline.js";
13
+ import { KYOGIJO_STRINGS, fill } from "./strings.js";
14
+ import { quarterTurn } from "./surfaces/surface.js";
15
+ import { convertLength } from "./units.js";
16
+ const round = (value) => String(Math.round(value * 100) / 100);
17
+ /**
18
+ * The sentences a range adds to a drawing's description: what is shown, and, when the drawing has data, that data outside it is not drawn. Empty without a range.
19
+ */
20
+ export function describeRange(o, language, hasData) {
21
+ const parsed = parseRange(o);
22
+ if (parsed === undefined)
23
+ return "";
24
+ const words = KYOGIJO_STRINGS[language].ranges;
25
+ const side = (end) => KYOGIJO_STRINGS[language].notes[end];
26
+ let text;
27
+ if (parsed.kind === "name") {
28
+ const name = words.names[parsed.name];
29
+ text = parsed.end === undefined ? fill(words.shows, { name }) : fill(words.showsEnd, { name, side: side(parsed.end) });
30
+ }
31
+ else {
32
+ const { x0, x1, y0, y1, frame } = parsed.box;
33
+ const numbers = { x0: round(Math.min(x0, x1)), x1: round(Math.max(x0, x1)), y0: round(Math.min(y0, y1)), y1: round(Math.max(y0, y1)) };
34
+ text = frame === undefined ? fill(words.box, numbers) : fill(words.boxFrame, { ...numbers, frame: typeof frame === "string" ? frame : frame.name });
35
+ }
36
+ const gap = KYOGIJO_STRINGS[language].said.gap;
37
+ return `${gap}${text}${hasData ? `${gap}${words.cut}` : ""}`;
38
+ }
39
+ /** The part of a drawing's `range` that is a name or "custom", for the root's `data-range`. */
40
+ export function rangeLabel(o) {
41
+ const parsed = parseRange(o);
42
+ return parsed === undefined ? undefined : parsed.kind === "name" ? parsed.name : "custom";
43
+ }
44
+ /** How much of the surface's ground the cut of a range leaves beyond a line it passes through, so that the line is whole, in feet. */
45
+ const CUT = 0.8;
46
+ /** A range's edge this close to the surface's is the surface's. */
47
+ const NEAR = 0.01;
48
+ const intersect = (a, b) => {
49
+ const x0 = Math.max(a.x, b.x);
50
+ const y0 = Math.max(a.y, b.y);
51
+ const x1 = Math.min(a.x + a.w, b.x + b.w);
52
+ const y1 = Math.min(a.y + a.h, b.y + b.h);
53
+ return { x: x0, y: y0, w: x1 - x0, h: y1 - y0 };
54
+ };
55
+ /** The box of a range in the surface's wide frame, in feet. */
56
+ function boxOf(parsed, surface, whole, o) {
57
+ if (parsed.kind === "name") {
58
+ const found = surface.ranges?.[parsed.name];
59
+ if (found === undefined)
60
+ return undefined;
61
+ return "left" in found ? found[parsed.end ?? "left"] : found;
62
+ }
63
+ const { x0, x1, y0, y1, frame, attacking } = parsed.box;
64
+ let corners;
65
+ if (frame === undefined) {
66
+ const unit = lengthUnit(o);
67
+ corners = [[x0, y0], [x1, y1]].map(([x, y]) => [convertLength(x, unit, "ft"), convertLength(y, unit, "ft")]);
68
+ }
69
+ else {
70
+ // The frame's numbers, placed on the surface as a layer's are (the frame fitted to a surface of any size), and the box round the corners, which a frame that swaps or turns the axes may have moved.
71
+ corners = toSurface([[x0, y0], [x1, y0], [x0, y1], [x1, y1]], { from: frame, units: "ft", size: frameSizeOf(o, whole), attacking });
72
+ }
73
+ const xs = corners.map(([x]) => x);
74
+ const ys = corners.map(([, y]) => y);
75
+ return { x: Math.min(...xs), y: Math.min(...ys), w: Math.max(...xs) - Math.min(...xs), h: Math.max(...ys) - Math.min(...ys) };
76
+ }
77
+ /** The least share of its size a score's panel is shrunk to before the score goes to the strip under the drawing. */
78
+ const PANEL_LEAST = 0.55;
79
+ /**
80
+ * Crop a surface to the range the options ask for. The surface's view becomes the range with a small margin (the surface's own ground where the range reaches its edge, a little
81
+ * beyond the line where it is cut), `box` the part of the playing surface that is shown (`whole` the playing surface), and, when the annotations ask for room, the ground beyond the cut
82
+ * is the apron's and the surface is clipped to the range. A score's panel is moved into what is shown and made smaller if it needs to be; when it would be too small, `plateFits` says
83
+ * false and the caller draws the score as a strip. A curling sheet is cropped by its own builder (`sheet.show`), and its named ranges are that.
84
+ * @throws RangeError when the range lies wholly outside the drawing.
85
+ */
86
+ export function cropToRange(surface, o, crop) {
87
+ const parsed = parseRange(o);
88
+ const whole = surface.box;
89
+ if (parsed === undefined || whole === undefined)
90
+ return surface;
91
+ const wide = boxOf(parsed, surface, whole, o);
92
+ // The named ranges of a curling sheet are the builder's own (`show`). A surface that lists its ranges and lacks the one asked for (a cricket ground that marks no circle) cannot be cropped to it.
93
+ if (wide === undefined) {
94
+ if (surface.ranges !== undefined && parsed.kind === "name")
95
+ throw new RangeError(`This ${surface.sport} surface has no range "${parsed.name}": use one of ${Object.keys(surface.ranges).join(", ")}`);
96
+ return surface;
97
+ }
98
+ const full = wideView(surface);
99
+ const visible = intersect(wide, full);
100
+ if (!(visible.w > 0 && visible.h > 0))
101
+ throw new RangeError("The range lies outside the surface");
102
+ const shown = (() => {
103
+ const inside = intersect(wide, whole);
104
+ return inside.w > 0 && inside.h > 0 ? inside : visible;
105
+ })();
106
+ // The cut: the surface's own ground on a side where the range reaches the surface's edge, a little beyond the line on a side where it does not.
107
+ const want = crop.margin !== undefined && crop.margin > CUT ? crop.margin : CUT;
108
+ const x0 = wide.x <= whole.x + NEAR;
109
+ const y0 = wide.y <= whole.y + NEAR;
110
+ const x1 = wide.x + wide.w >= whole.x + whole.w - NEAR;
111
+ const y1 = wide.y + wide.h >= whole.y + whole.h - NEAR;
112
+ const cutX0 = x0 ? full.x : Math.max(full.x, visible.x - CUT);
113
+ const cutY0 = y0 ? full.y : Math.max(full.y, visible.y - CUT);
114
+ const cutX1 = x1 ? full.x + full.w : Math.min(full.x + full.w, visible.x + visible.w + CUT);
115
+ const cutY1 = y1 ? full.y + full.h : Math.min(full.y + full.h, visible.y + visible.h + CUT);
116
+ const room = want - CUT;
117
+ const view = {
118
+ x: x0 ? cutX0 : Math.max(full.x, cutX0 - room),
119
+ y: y0 ? cutY0 : Math.max(full.y, cutY0 - room),
120
+ w: 0,
121
+ h: 0,
122
+ };
123
+ view.w = (x1 ? cutX1 : Math.min(full.x + full.w, cutX1 + room)) - view.x;
124
+ view.h = (y1 ? cutY1 : Math.min(full.y + full.h, cutY1 + room)) - view.y;
125
+ surface.whole = whole;
126
+ surface.ground = whole.y - full.y;
127
+ surface.room = want;
128
+ surface.cut = { x: cutX0, y: cutY0, w: cutX1 - cutX0, h: cutY1 - cutY0 };
129
+ surface.box = shown;
130
+ if (surface.turned) {
131
+ const turned = quarterTurn(view);
132
+ surface.view = turned.view;
133
+ surface.turn = turned.turn;
134
+ }
135
+ else
136
+ surface.view = view;
137
+ // Room beyond the cut for the annotations shows more of the surface than the range: paint the apron there and clip the surface to the cut.
138
+ if (room > 0.01) {
139
+ const clip = `${crop.id}-rg`;
140
+ const cut = el("clipPath", { id: clip }, el("rect", { x: num(cutX0), y: num(cutY0), width: num(cutX1 - cutX0), height: num(cutY1 - cutY0) }));
141
+ const ground = el("rect", { class: surface.sport === "mlb" ? "ky-foul" : "ky-apron", x: num(view.x), y: num(view.y), width: num(view.w), height: num(view.h) });
142
+ surface.defs += cut;
143
+ surface.base = `${ground}${el("g", { "clip-path": `url(#${clip})` }, surface.base)}`;
144
+ surface.lines = el("g", { "clip-path": `url(#${clip})` }, surface.lines);
145
+ }
146
+ if (crop.panel)
147
+ fitPanel(surface, shown);
148
+ return surface;
149
+ }
150
+ /** Put a score's panel inside the part of the surface that is shown, smaller if it needs to be; `plateFits` says false when it would have to be smaller than `PANEL_LEAST`. */
151
+ function fitPanel(surface, shown) {
152
+ const spot = surface.plate;
153
+ // The panel's width and height are the eye's; when the drawing is turned a quarter they are the surface's height and width.
154
+ const sw = surface.turned ? spot.h : spot.w;
155
+ const sh = surface.turned ? spot.w : spot.h;
156
+ const scale = Math.min(1, (0.8 * shown.w) / sw, (0.8 * shown.h) / sh, Math.sqrt((0.35 * shown.w * shown.h) / (sw * sh)));
157
+ if (!(scale >= PANEL_LEAST)) {
158
+ surface.plateFits = false;
159
+ return;
160
+ }
161
+ const w = sw * scale;
162
+ const h = sh * scale;
163
+ const pad = 0.04 * Math.min(shown.w, shown.h);
164
+ const cx = Math.min(Math.max(spot.cx, shown.x + w / 2 + pad), shown.x + shown.w - w / 2 - pad);
165
+ const cy = Math.min(Math.max(spot.cy, shown.y + h / 2 + pad), shown.y + shown.h - h / 2 - pad);
166
+ surface.plate = { cx, cy, w: spot.w * scale, h: spot.h * scale };
167
+ }
168
+ /** True when a team's place (a banner's) is in the part of the drawing that is shown: always without a range. */
169
+ export function shows(surface, zone) {
170
+ if (surface.whole === undefined)
171
+ return true;
172
+ const v = wideView(surface);
173
+ return zone.cx >= v.x && zone.cx <= v.x + v.w && zone.cy >= v.y && zone.cy <= v.y + v.h;
174
+ }
175
+ /** The part of the playing surface that is shown, in feet in the drawing's frame, when the drawing is cropped to a range; `undefined` when it is not. */
176
+ export function shownBox(surface) {
177
+ return surface.whole === undefined ? undefined : surface.box;
178
+ }
@@ -0,0 +1,31 @@
1
+ import type { RangeBox, RangeName, Side, Sport, SurfaceOptions } from "./types.ts";
2
+ /** One range of a sport: its name, and the end it is at by default when it is at an end (`left` or `right`, as the surface is drawn wide). A range with no `end` is in the middle or is the whole of something. */
3
+ export interface RangeInfo {
4
+ id: RangeName;
5
+ end?: Side;
6
+ /** The leagues the range is for, when only some of the sport's have it (rugby union's 22 and rugby league's 20-metre area; the fielding circle of the ICC's grounds). Every league when missing. */
7
+ leagues?: readonly string[];
8
+ }
9
+ /**
10
+ * The named ranges of each sport, in the order a menu lists them: `{ id, end }`, with `end` the end the range is at when `range` gives none (`left` or `right`, as the surface is
11
+ * drawn wide; a range with no `end` is in the middle or is the whole of something and ignores one). Offensive ranges are at the right by default, as a frame's numbers are for a team
12
+ * that attacks the right (see `toSurface`); defensive and own ranges at the left; the others, such as a half, at the left, as a curling sheet's house is.
13
+ */
14
+ export declare const RANGES: Readonly<Record<Sport, readonly RangeInfo[]>>;
15
+ /** What a drawing's `range` asks for, checked: a name (at an end, or at its default) or a box. */
16
+ /** A name carries its `end` (the one asked for, else the range's default; none for a range with no end) and `asked`, the end the options named, if they did. */
17
+ export type ParsedRange = {
18
+ kind: "name";
19
+ name: RangeName;
20
+ end: Side | undefined;
21
+ asked: Side | undefined;
22
+ } | {
23
+ kind: "box";
24
+ box: RangeBox;
25
+ };
26
+ /**
27
+ * The range the options ask for, or `undefined` when they ask for none.
28
+ * @throws RangeError for a name the sport does not have (the message lists the sport's), or an `end` that is not `left` or `right`.
29
+ * @throws TypeError for a box without its four numbers.
30
+ */
31
+ export declare function parseRange(o: Pick<SurfaceOptions, "sport" | "league" | "range">): ParsedRange | undefined;
@@ -0,0 +1,65 @@
1
+ // The names of the ranges of each sport (`RANGES`) and the check of a drawing's `range` option (`parseRange`): kept apart from ranges.ts, which crops, so that the curling sheet's
2
+ // own builder can read a named range without importing the code that crops every surface.
3
+ import { LEAGUES, chooseLeague } from "./leagues.js";
4
+ const range = (id, end, leagues) => Object.freeze({ id, ...(end === undefined ? {} : { end }), ...(leagues === undefined ? {} : { leagues: Object.freeze([...leagues]) }) });
5
+ const FOOTBALL = Object.freeze([range("red-zone", "right"), range("end-zone", "right"), range("between-the-20s"), range("field-of-play"), range("own-half", "left"), range("opponent-half", "right")]);
6
+ const NET = Object.freeze([range("half-court", "left")]);
7
+ /**
8
+ * The named ranges of each sport, in the order a menu lists them: `{ id, end }`, with `end` the end the range is at when `range` gives none (`left` or `right`, as the surface is
9
+ * drawn wide; a range with no `end` is in the middle or is the whole of something and ignores one). Offensive ranges are at the right by default, as a frame's numbers are for a team
10
+ * that attacks the right (see `toSurface`); defensive and own ranges at the left; the others, such as a half, at the left, as a curling sheet's house is.
11
+ */
12
+ export const RANGES = Object.freeze({
13
+ nfl: FOOTBALL,
14
+ cfl: FOOTBALL,
15
+ ifaf: FOOTBALL,
16
+ nhl: Object.freeze([range("offensive-zone", "right"), range("defensive-zone", "left"), range("neutral-zone"), range("half", "left")]),
17
+ nba: Object.freeze([range("half-court", "left"), range("paint", "left")]),
18
+ soccer: Object.freeze([range("half", "left"), range("final-third", "right"), range("defensive-third", "left"), range("middle-third"), range("penalty-area", "left")]),
19
+ mlb: Object.freeze([range("infield"), range("outfield")]),
20
+ curling: Object.freeze([range("sheet"), range("half", "left"), range("house", "left")]),
21
+ tennis: NET,
22
+ volleyball: NET,
23
+ badminton: NET,
24
+ pickleball: NET,
25
+ tabletennis: NET,
26
+ lacrosse: Object.freeze([range("half", "left"), range("crease", "right")]),
27
+ rugby: Object.freeze([range("half", "left"), range("twenty-two", "right", ["world-rugby"]), range("twenty", "right", ["nrl", "irl"]), range("in-goal", "right")]),
28
+ afl: Object.freeze([range("half", "left"), range("forward-fifty", "right"), range("centre-square")]),
29
+ gaelic: Object.freeze([range("half", "left"), range("large-rectangle", "right")]),
30
+ cricket: Object.freeze([range("pitch"), range("circle", undefined, ["icc-men", "icc-women"])]),
31
+ fieldhockey: Object.freeze([range("half", "left"), range("circle", "right"), range("twenty-three", "right")]),
32
+ handball: Object.freeze([range("half", "left"), range("goal-area", "right")]),
33
+ });
34
+ const isNumber = (value) => typeof value === "number" && Number.isFinite(value);
35
+ /**
36
+ * The range the options ask for, or `undefined` when they ask for none.
37
+ * @throws RangeError for a name the sport does not have (the message lists the sport's), or an `end` that is not `left` or `right`.
38
+ * @throws TypeError for a box without its four numbers.
39
+ */
40
+ export function parseRange(o) {
41
+ const given = o.range;
42
+ if (given === undefined || given === null)
43
+ return undefined;
44
+ const { sport, league } = chooseLeague(o);
45
+ const named = typeof given === "string" || (typeof given === "object" && "name" in given && typeof given.name === "string");
46
+ if (named) {
47
+ const name = typeof given === "string" ? given : given.name;
48
+ const end = typeof given === "object" ? given.end : undefined;
49
+ const info = (RANGES[sport] ?? []).find((one) => one.id === name);
50
+ if (info === undefined)
51
+ throw new RangeError(`The ${sport} surface has no range "${name}": use one of ${(RANGES[sport] ?? []).map((one) => one.id).join(", ")}, or a box { x0, x1, y0, y1 }`);
52
+ const leagueId = league?.id ?? LEAGUES[sport]?.[0]?.id;
53
+ if (info.leagues !== undefined && leagueId !== undefined && !info.leagues.includes(leagueId))
54
+ throw new RangeError(`The ${sport} league "${leagueId}" has no range "${name}": it is for ${info.leagues.join(" and ")}`);
55
+ if (end !== undefined && end !== "left" && end !== "right")
56
+ throw new RangeError(`A range's end is "left" or "right", not "${String(end)}"`);
57
+ return { kind: "name", name: info.id, end: info.end === undefined ? undefined : (end ?? info.end), asked: end };
58
+ }
59
+ const box = given;
60
+ if (typeof given !== "object" || !isNumber(box.x0) || !isNumber(box.x1) || !isNumber(box.y0) || !isNumber(box.y1))
61
+ throw new TypeError(`range is the name of a range of the sport, or a box { x0, x1, y0, y1 } of numbers`);
62
+ if (box.attacking !== undefined && box.attacking !== "left" && box.attacking !== "right")
63
+ throw new RangeError(`A range's attacking is "left" or "right", not "${String(box.attacking)}"`);
64
+ return { kind: "box", box: box };
65
+ }
@@ -0,0 +1,2 @@
1
+ export { createKyogijo, kyogijoHtml, kyogijoProps } from "./react.ts";
2
+ export type { KyogijoProps, ReactLike } from "./react.ts";