@johnmorrisdotca/kyogijo-stadiums 0.5.0 → 0.6.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 (193) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/NOTICE.md +179 -3
  3. package/README.md +43 -11
  4. package/dist/arenas-entry.d.ts +4 -0
  5. package/dist/arenas-entry.js +3 -0
  6. package/dist/arenas.d.ts +125 -0
  7. package/dist/arenas.data.d.ts +2 -0
  8. package/dist/arenas.data.js +3763 -0
  9. package/dist/arenas.js +72 -0
  10. package/dist/atlas-entry.d.ts +3 -1
  11. package/dist/atlas-entry.js +3 -2
  12. package/dist/atlas.d.ts +37 -19
  13. package/dist/atlas.js +92 -7
  14. package/dist/background.js +2 -1
  15. package/dist/budo/describe.d.ts +13 -0
  16. package/dist/budo/describe.js +120 -0
  17. package/dist/budo/dojo.api.d.ts +64 -0
  18. package/dist/budo/dojo.api.js +46 -0
  19. package/dist/budo/dojo.d.ts +72 -0
  20. package/dist/budo/dojo.js +300 -0
  21. package/dist/budo/judo.d.ts +61 -0
  22. package/dist/budo/judo.js +109 -0
  23. package/dist/budo/karate.d.ts +29 -0
  24. package/dist/budo/karate.js +80 -0
  25. package/dist/budo/kendo.d.ts +18 -0
  26. package/dist/budo/kendo.js +44 -0
  27. package/dist/budo/kimarite.d.ts +19 -0
  28. package/dist/budo/kimarite.js +33 -0
  29. package/dist/budo/kit.d.ts +16 -0
  30. package/dist/budo/kit.js +24 -0
  31. package/dist/budo/kyudo.d.ts +28 -0
  32. package/dist/budo/kyudo.js +66 -0
  33. package/dist/budo/looks.d.ts +28 -0
  34. package/dist/budo/looks.js +58 -0
  35. package/dist/budo/parts.d.ts +32 -0
  36. package/dist/budo/parts.js +10 -0
  37. package/dist/budo/plan.d.ts +64 -0
  38. package/dist/budo/plan.js +139 -0
  39. package/dist/budo/room.d.ts +133 -0
  40. package/dist/budo/room.js +111 -0
  41. package/dist/budo/sumo.d.ts +31 -0
  42. package/dist/budo/sumo.js +71 -0
  43. package/dist/budo/tatami.d.ts +55 -0
  44. package/dist/budo/tatami.js +156 -0
  45. package/dist/budo/types.d.ts +192 -0
  46. package/dist/budo/types.js +2 -0
  47. package/dist/budo-entry.d.ts +22 -0
  48. package/dist/budo-entry.js +13 -0
  49. package/dist/budo.board.d.ts +28 -0
  50. package/dist/budo.board.js +81 -0
  51. package/dist/budo.score.d.ts +18 -0
  52. package/dist/budo.score.js +52 -0
  53. package/dist/budo.style.d.ts +6 -0
  54. package/dist/budo.style.js +67 -0
  55. package/dist/charts.baseball.js +1 -1
  56. package/dist/charts.js +4 -4
  57. package/dist/charts.soccer.js +3 -3
  58. package/dist/charts.types.d.ts +10 -0
  59. package/dist/colour.d.ts +4 -4
  60. package/dist/colour.js +4 -4
  61. package/dist/crowd.d.ts +11 -1
  62. package/dist/crowd.draw.js +15 -9
  63. package/dist/crowd.js +17 -2
  64. package/dist/draw.js +156 -31
  65. package/dist/element-entry.d.ts +2 -0
  66. package/dist/element-entry.js +1 -0
  67. package/dist/element.d.ts +2 -2
  68. package/dist/element.js +2 -1
  69. package/dist/emblems-source.d.ts +50 -0
  70. package/dist/emblems-source.js +56 -0
  71. package/dist/eras.d.ts +3 -1
  72. package/dist/eras.js +8 -3
  73. package/dist/features.js +73 -0
  74. package/dist/field.data.d.ts +16 -0
  75. package/dist/field.data.js +50 -0
  76. package/dist/fields.kit.d.ts +2 -2
  77. package/dist/fields.types.d.ts +9 -3
  78. package/dist/formations.js +18 -7
  79. package/dist/formations.sports.d.ts +8 -0
  80. package/dist/formations.sports.js +251 -0
  81. package/dist/frames.providers.js +9 -1
  82. package/dist/halls.d.ts +65 -0
  83. package/dist/halls.js +122 -0
  84. package/dist/heat.js +17 -10
  85. package/dist/heat.zones.d.ts +4 -2
  86. package/dist/heat.zones.js +55 -3
  87. package/dist/homeside.js +7 -0
  88. package/dist/index.d.ts +15 -3
  89. package/dist/index.js +8 -1
  90. package/dist/layers-entry.d.ts +2 -1
  91. package/dist/layers-entry.js +1 -0
  92. package/dist/layers.d.ts +2 -1
  93. package/dist/layers.env.d.ts +10 -1
  94. package/dist/layers.env.js +33 -3
  95. package/dist/layers.js +30 -11
  96. package/dist/layers.types.d.ts +9 -2
  97. package/dist/leagues/budo.d.ts +196 -0
  98. package/dist/leagues/budo.js +105 -0
  99. package/dist/leagues/hockey.js +41 -19
  100. package/dist/leagues/sources.js +21 -0
  101. package/dist/leagues/types.d.ts +1 -1
  102. package/dist/leagues.d.ts +4 -1
  103. package/dist/leagues.js +102 -2
  104. package/dist/lineup.codes.d.ts +23 -0
  105. package/dist/lineup.codes.js +122 -0
  106. package/dist/logo.d.ts +25 -0
  107. package/dist/logo.js +47 -0
  108. package/dist/outline.d.ts +1 -1
  109. package/dist/outline.js +17 -3
  110. package/dist/pageant.js +7 -3
  111. package/dist/pointer-entry.d.ts +2 -0
  112. package/dist/pointer-entry.js +2 -0
  113. package/dist/pointer.d.ts +113 -0
  114. package/dist/pointer.js +151 -0
  115. package/dist/pool.d.ts +7 -7
  116. package/dist/pool.figures.d.ts +57 -55
  117. package/dist/pool.figures.js +59 -55
  118. package/dist/pool.js +7 -7
  119. package/dist/pool.labels.js +2 -1
  120. package/dist/ranges.names.js +6 -0
  121. package/dist/road.draw.js +3 -2
  122. package/dist/road.js +2 -1
  123. package/dist/road.types.d.ts +3 -0
  124. package/dist/rules.d.ts +138 -0
  125. package/dist/rules.js +511 -0
  126. package/dist/rules.types.d.ts +4 -0
  127. package/dist/rules.types.js +64 -0
  128. package/dist/score.d.ts +3 -0
  129. package/dist/score.js +23 -1
  130. package/dist/situation.d.ts +135 -0
  131. package/dist/situation.draw.d.ts +34 -0
  132. package/dist/situation.draw.js +202 -0
  133. package/dist/situation.js +337 -0
  134. package/dist/situation.style.d.ts +2 -0
  135. package/dist/situation.style.js +45 -0
  136. package/dist/situation.types.d.ts +77 -0
  137. package/dist/situation.types.js +3 -0
  138. package/dist/situation.widgets.d.ts +39 -0
  139. package/dist/situation.widgets.js +129 -0
  140. package/dist/strings.d.ts +175 -1
  141. package/dist/strings.js +364 -20
  142. package/dist/style.js +9 -2
  143. package/dist/surfaces/artistic.d.ts +3 -3
  144. package/dist/surfaces/artistic.js +3 -3
  145. package/dist/surfaces/baseball.js +2 -0
  146. package/dist/surfaces/basketball.js +1 -1
  147. package/dist/surfaces/budo.d.ts +6 -0
  148. package/dist/surfaces/budo.js +104 -0
  149. package/dist/surfaces/dive.d.ts +3 -3
  150. package/dist/surfaces/dive.js +3 -3
  151. package/dist/surfaces/football.js +7 -0
  152. package/dist/surfaces/hockey.js +3 -2
  153. package/dist/surfaces/polo.d.ts +4 -4
  154. package/dist/surfaces/polo.js +5 -5
  155. package/dist/surfaces/pool.d.ts +1 -1
  156. package/dist/surfaces/pool.js +3 -3
  157. package/dist/surfaces/surface.d.ts +32 -0
  158. package/dist/surfaces/venue.d.ts +1 -1
  159. package/dist/surfaces/venue.js +1 -1
  160. package/dist/target-entry.d.ts +2 -0
  161. package/dist/target-entry.js +2 -0
  162. package/dist/target.d.ts +200 -0
  163. package/dist/target.js +525 -0
  164. package/dist/team.common.d.ts +1 -1
  165. package/dist/team.common.js +3 -3
  166. package/dist/team.formation.d.ts +3 -1
  167. package/dist/team.formation.js +3 -3
  168. package/dist/team.layers.d.ts +2 -2
  169. package/dist/team.layers.js +4 -1
  170. package/dist/team.lineup.d.ts +27 -0
  171. package/dist/team.lineup.js +207 -0
  172. package/dist/team.network.js +6 -5
  173. package/dist/team.shape.js +5 -5
  174. package/dist/team.types.d.ts +40 -1
  175. package/dist/tenants.data.js +848 -0
  176. package/dist/turn-entry.d.ts +2 -2
  177. package/dist/turn-entry.js +1 -1
  178. package/dist/turn.camera.d.ts +12 -1
  179. package/dist/turn.camera.js +18 -2
  180. package/dist/turn.dom.js +11 -9
  181. package/dist/turn.kit.d.ts +2 -2
  182. package/dist/turn.style.js +4 -3
  183. package/dist/types.d.ts +43 -7
  184. package/dist/units.js +7 -0
  185. package/dist/uprights.budo.d.ts +17 -0
  186. package/dist/uprights.budo.js +87 -0
  187. package/dist/uprights.js +10 -1
  188. package/dist/uprights.pools.js +4 -4
  189. package/dist/version.d.ts +1 -1
  190. package/dist/version.js +1 -1
  191. package/dist/zones-entry.d.ts +2 -0
  192. package/dist/zones-entry.js +2 -0
  193. package/package.json +39 -3
package/dist/target.js ADDED
@@ -0,0 +1,525 @@
1
+ // The targets seen end-on: the mouth of a goal from the shooter's side (hockey, soccer, handball, water polo, lacrosse, field hockey), the stumps from the bowler's end, and the strike
2
+ // zone from the catcher's. Each is a flat elevation, x across (positive to the viewer's right) and z up, drawn to scale from the rulebook's figure, with zones, and the shots, deliveries
3
+ // or pitches laid over it as marks that carry `data-mark-id`, `data-index` and `data-zone`. The marks are the data layers' (layers.ts), so they are described, tabled, upright and
4
+ // light or dark like any other, and the drawing is one SVG string that every renderer reads. Nothing here is drawn from another package's code; `soccerGoalMouth` of the charts is the
5
+ // soccer goal of this, with its own words.
6
+ import { drawingId, safeClass } from "./common.js";
7
+ import { safeColour } from "./colour.js";
8
+ import { esc, el, num } from "./format.js";
9
+ import { layerTable, renderLayers } from "./layers.js";
10
+ import { chooseLeague, lacrosseOf } from "./leagues.js";
11
+ import { KYOGIJO_BASE_STYLE } from "./style.js";
12
+ import { KYOGIJO_STRINGS, fill } from "./strings.js";
13
+ import { soccerSpec } from "./surfaces/soccer-spec.js";
14
+ import { NHL, FEET_PER_METRE } from "./geometry.js";
15
+ import { WATER_POLO } from "./pool.figures.js";
16
+ import { IHF_COURT } from "./leagues/handball.js";
17
+ import { FIH_PITCH } from "./leagues/fieldhockey.js";
18
+ import { VERSION } from "./version.js";
19
+ /** The things that have a target seen end-on: the sports with a goal, cricket's stumps and baseball's strike zone. */
20
+ export const TARGET_SPORTS = ["nhl", "soccer", "handball", "waterpolo", "lacrosse", "fieldhockey", "cricket", "mlb"];
21
+ /** The sports whose target is a goal. */
22
+ export const GOAL_SPORTS = ["nhl", "soccer", "handball", "waterpolo", "lacrosse", "fieldhockey"];
23
+ const METRES = { m: 1, ft: 0.3048, in: 0.0254, cm: 0.01, yd: 0.9144 };
24
+ // ----- the targets' sizes
25
+ /** The width of a home plate across, the same as the strike zone's: 17 inches (OBR 2.02). */
26
+ const PLATE = 17 / 12;
27
+ /** The size of a goal in its rulebook unit. Hockey: the NHL's net (Rule 2.1) 6 by 4 feet. Soccer: the Laws' 7.32 by 2.44 m, or the format's. Handball: the IHF's 3 by 2 m. Water polo: World Aquatics' 3 m, the crossbar 0.9 m over the water. Lacrosse: 6 by 6 feet (World Lacrosse 1.83 m, NCAA, PLL), the box floor's 4 ft 9 in by 4 ft. Field hockey: the FIH's 3.66 by 2.14 m. */
28
+ export function goalSize(sport, options = {}) {
29
+ switch (sport) {
30
+ case "nhl":
31
+ return { width: NHL.net.width, height: 4, unit: "ft", source: "NHL Official Rules, Rule 2.1: the goal posts six feet apart and four feet high" };
32
+ case "soccer": {
33
+ const spec = soccerSpec(options.pitch);
34
+ return { width: spec.goalWidth, height: spec.goalHeight, unit: "m", source: "IFAB Laws of the Game, Law 1 (Law 1 for small-sided formats: the format's own goal)" };
35
+ }
36
+ case "handball":
37
+ return { width: IHF_COURT.goalWidth, height: IHF_COURT.goalHeight, unit: "m", source: "IHF Rules of the Game for Indoor Handball, Rule 1:2: 3 m wide and 2 m high" };
38
+ case "waterpolo":
39
+ return { width: WATER_POLO.goal.width, height: WATER_POLO.goal.crossbarHeight, unit: "m", source: "World Aquatics Competition Regulations (February 2026), Part Six Article 21.6 (21.6.3.2 posts, 21.6.4.2 crossbar): 3.0 m between the posts, the crossbar's underside 0.9 m over the water" };
40
+ case "lacrosse": {
41
+ const { league } = chooseLeague({ sport: "lacrosse", league: options.league });
42
+ if (league?.family === "lacrosse") {
43
+ const spec = lacrosseOf(league);
44
+ return { width: spec.goalWidth, height: spec.goalHeight, unit: "ft", source: `${league.id}: the goal ${spec.goalWidth.toFixed(2)} ft by ${spec.goalHeight.toFixed(2)} ft` };
45
+ }
46
+ return { width: 6, height: 6, unit: "ft", source: "World Lacrosse Men's Field Lacrosse Rules 2.1 (1.83 m by 1.83 m), the NCAA's and the PLL's six feet by six feet" };
47
+ }
48
+ case "fieldhockey":
49
+ return { width: FIH_PITCH.goalWidth, height: FIH_PITCH.goalHeight, unit: "m", source: "FIH Rules of Hockey 2026, Field and Equipment Specifications 1.5 (Goals): 3.66 m between the posts, 2.14 m under the crossbar" };
50
+ }
51
+ }
52
+ const frameOf = (sport, options) => {
53
+ if (sport === "mlb") {
54
+ const bottom = options.bottom ?? 1.5;
55
+ const top = options.top ?? 3.5;
56
+ if (!(top > bottom))
57
+ throw new RangeError("the strike zone's top is above its bottom");
58
+ const h = top - bottom;
59
+ return { sport, unit: "ft", width: PLATE, height: h, floor: bottom, view: { x0: -2.1, x1: 2.1, z0: -0.3, z1: top + h * 0.7 }, source: "Official Baseball Rules, definition of the strike zone, and Rule 2.02 (home plate 17 inches wide)" };
60
+ }
61
+ if (sport === "cricket") {
62
+ return { sport, unit: "m", width: 0.2286, height: 0.7112, floor: 0, view: { x0: -0.7, x1: 0.7, z0: -0.1, z1: 1.35 }, source: "MCC Laws of Cricket, Law 8.1 and 8.2: three stumps across 9 in (22.86 cm), their tops 28 in (71.12 cm) over the ground" };
63
+ }
64
+ const base = goalSize(sport, options);
65
+ const width = options.goal?.width ?? base.width;
66
+ const height = options.goal?.height ?? base.height;
67
+ return { sport, unit: base.unit, width, height, floor: 0, view: { x0: (-width * 1.7) / 2, x1: (width * 1.7) / 2, z0: -height * 0.22, z1: height * 1.88 }, source: base.source };
68
+ };
69
+ // ----- zones
70
+ /** The ids of the 3 by 3 grid across a goal, left to right and top to bottom: the shooter's left first. */
71
+ export function goalZoneIds(sport) {
72
+ const ids = ["top-left-corner", "top-centre", "top-right-corner", "mid-left", "centre", "mid-right", "bottom-left-corner", "bottom-centre", "bottom-right-corner"];
73
+ // Hockey's own words for the two middle cells of the top and the bottom row: the top shelf under the crossbar and the five-hole between the pads.
74
+ if (sport === "nhl")
75
+ return ids.map((id) => (id === "top-centre" ? "top-shelf" : id === "bottom-centre" ? "five-hole" : id));
76
+ return ids;
77
+ }
78
+ function goalCells(f) {
79
+ const ids = goalZoneIds(f.sport);
80
+ const hw = f.width / 2;
81
+ const out = [];
82
+ for (let r = 0; r < 3; r += 1) {
83
+ for (let c = 0; c < 3; c += 1) {
84
+ out.push({ id: ids[r * 3 + c], x0: -hw + (f.width * c) / 3, x1: -hw + (f.width * (c + 1)) / 3, z0: f.height - (f.height * (r + 1)) / 3, z1: f.height - (f.height * r) / 3 });
85
+ }
86
+ }
87
+ return out;
88
+ }
89
+ function strikeCells(f, chase) {
90
+ const hw = f.width / 2;
91
+ const top = f.floor + f.height;
92
+ const out = [];
93
+ for (let r = 0; r < 3; r += 1) {
94
+ for (let c = 0; c < 3; c += 1)
95
+ out.push({ id: `zone-${r * 3 + c + 1}`, x0: -hw + (f.width * c) / 3, x1: -hw + (f.width * (c + 1)) / 3, z0: top - (f.height * (r + 1)) / 3, z1: top - (f.height * r) / 3 });
96
+ }
97
+ if (chase) {
98
+ const ox = f.width / 2;
99
+ const oz = f.height / 2;
100
+ const mid = f.floor + f.height / 2;
101
+ out.push({ id: "zone-11", x0: -hw - ox, x1: 0, z0: mid, z1: top + oz }, { id: "zone-12", x0: 0, x1: hw + ox, z0: mid, z1: top + oz }, { id: "zone-13", x0: -hw - ox, x1: 0, z0: f.floor - oz, z1: mid }, { id: "zone-14", x0: 0, x1: hw + ox, z0: f.floor - oz, z1: mid });
102
+ }
103
+ return out;
104
+ }
105
+ /** The line zones across the stumps: the stumps' own width, then the same width again either side, and wide of that. They are on the screen's left and right of the stumps as the bowler sees them: which is the off side and which the leg depends on the batter's hand, so the ids do not say. */
106
+ function stumpCells(f) {
107
+ const hw = f.width / 2;
108
+ const w = f.width;
109
+ const big = 100;
110
+ const top = f.height;
111
+ return [
112
+ { id: "wide-left", x0: -big, x1: -hw - w, z0: -big, z1: big },
113
+ { id: "outside-left", x0: -hw - w, x1: -hw, z0: -big, z1: big },
114
+ { id: "on-stumps", x0: -hw, x1: hw, z0: -big, z1: top },
115
+ { id: "over-stumps", x0: -hw, x1: hw, z0: top, z1: big },
116
+ { id: "outside-right", x0: hw, x1: hw + w, z0: -big, z1: big },
117
+ { id: "wide-right", x0: hw + w, x1: big, z0: -big, z1: big },
118
+ ];
119
+ }
120
+ function cellsOf(f, options) {
121
+ return f.sport === "mlb" ? strikeCells(f, options.chase !== false) : f.sport === "cricket" ? stumpCells(f) : goalCells(f);
122
+ }
123
+ /** The length zones of a delivery by where it pitched, in metres from the batter's stumps (the convention here is ours: broadcasters' bands differ with the bowler's pace). */
124
+ export const LENGTH_ZONES = Object.freeze([
125
+ { id: "yorker", to: 2 },
126
+ { id: "full", to: 4 },
127
+ { id: "good-length", to: 6 },
128
+ { id: "short-of-a-length", to: 8 },
129
+ { id: "short", to: Infinity },
130
+ ]);
131
+ /** The length zone of a delivery that pitched this many metres from the batter's stumps. */
132
+ export function lengthZoneOf(metres) {
133
+ if (!Number.isFinite(metres) || metres < 0)
134
+ throw new RangeError("a length is a distance from the stumps, 0 or more");
135
+ return (LENGTH_ZONES.find((zone) => metres < zone.to) ?? LENGTH_ZONES[LENGTH_ZONES.length - 1]).id;
136
+ }
137
+ /**
138
+ * The zone of a place across a goal, the stumps or the strike zone, in the options' units. A goal: its nine cells (the top shelf and the five-hole in hockey), or `wide-left`, `wide-right` and `over-the-bar`
139
+ * outside the opening; the stumps: the line zones; the strike zone: `zone-1` to `zone-9` and the chase zones `zone-11` to `zone-14`, else `outside`.
140
+ * @example
141
+ * ```ts
142
+ * import { targetZoneAt } from "@johnmorrisdotca/kyogijo-stadiums/target";
143
+ * // A hockey net is 6 by 4 feet: a shot 2.5 feet to the right and 3.7 feet up is under the crossbar in the top right corner.
144
+ * console.log(targetZoneAt({ sport: "nhl" }, 2.5, 3.7), targetZoneAt({ sport: "nhl" }, 0, 0.3).zone);
145
+ * // { zone: 'top-right-corner', placement: 'in' } five-hole
146
+ * ```
147
+ */
148
+ export function targetZoneAt(options, x, z) {
149
+ const f = frameOf(options.sport, options);
150
+ const k = unitScale(options, f);
151
+ return placeIn(f, options, x * k, z * k);
152
+ }
153
+ const unitScale = (options, f) => METRES[options.units ?? f.unit] / METRES[f.unit];
154
+ function placeIn(f, options, x, z) {
155
+ const cells = cellsOf(f, options);
156
+ const hw = f.width / 2;
157
+ if (f.sport === "cricket") {
158
+ const cell = cells.find((c) => x >= c.x0 && x < c.x1 && z >= c.z0 && z < c.z1) ?? cells[0];
159
+ return { zone: cell.id, placement: cell.id === "on-stumps" ? "in" : cell.id.startsWith("wide") ? "wide" : cell.id === "over-stumps" ? "high" : "wide" };
160
+ }
161
+ if (f.sport === "mlb") {
162
+ // The nine cells of the zone first, then the four chase zones; the edge of the zone is inside it (any part of the ball on it is a strike).
163
+ const inner = cells.slice(0, 9).find((c) => x >= c.x0 && x <= c.x1 && z >= c.z0 && z <= c.z1);
164
+ if (inner !== undefined)
165
+ return { zone: inner.id, placement: "in" };
166
+ const outer = cells.slice(9).find((c) => x >= c.x0 && x <= c.x1 && z >= c.z0 && z <= c.z1);
167
+ return outer === undefined ? { zone: "outside", placement: Math.abs(x) > hw ? "wide" : "high" } : { zone: outer.id, placement: Math.abs(x) > hw ? "wide" : "high" };
168
+ }
169
+ const wide = Math.abs(x) > hw;
170
+ const high = z > f.height;
171
+ if (wide)
172
+ return { zone: x < 0 ? "wide-left" : "wide-right", placement: high ? "wide-high" : "wide" };
173
+ if (high)
174
+ return { zone: "over-the-bar", placement: "high" };
175
+ const cell = cells.find((c) => x >= c.x0 && x <= c.x1 && z >= c.z0 && z <= c.z1) ?? cells[4];
176
+ return { zone: cell.id, placement: "in" };
177
+ }
178
+ // ----- looks
179
+ const GOAL_LOOKS = {
180
+ goal: { shape: "star", scale: 1.35 },
181
+ saved: { hollow: true },
182
+ post: { shape: "diamond", hollow: true },
183
+ crossbar: { shape: "square", hollow: true },
184
+ wide: { shape: "x", hollow: true },
185
+ high: { shape: "triangle", hollow: true },
186
+ missed: { shape: "x", hollow: true },
187
+ blocked: { shape: "triangle", hollow: true },
188
+ };
189
+ const PITCH_LOOKS = {
190
+ ball: { shape: "circle", hollow: true },
191
+ "called-strike": { shape: "square" },
192
+ "swinging-strike": { shape: "x" },
193
+ foul: { shape: "triangle", hollow: true },
194
+ "in-play": { shape: "star", scale: 1.2 },
195
+ "hit-by-pitch": { shape: "diamond" },
196
+ };
197
+ const CRICKET_LOOKS = { wicket: { shape: "star", scale: 1.35 }, runs: { shape: "circle" }, dot: { shape: "circle", hollow: true } };
198
+ /** Colours for the results of a pitch and, in order, the types, readable on both grounds. */
199
+ const RESULT_COLOURS = { ball: "#4e79a7", "called-strike": "#e15759", "swinging-strike": "#b6222a", foul: "#f28e2b", "in-play": "#59a14f", "hit-by-pitch": "#9c755f" };
200
+ const TYPE_COLOURS = ["#e15759", "#4e79a7", "#59a14f", "#f28e2b", "#b07aa1", "#76b7b2", "#edc948", "#9c755f"];
201
+ const ORDER = {
202
+ goal: ["goal", "saved", "post", "crossbar", "wide", "high", "blocked", "shot"],
203
+ mlb: ["ball", "called-strike", "swinging-strike", "foul", "in-play", "hit-by-pitch"],
204
+ cricket: ["wicket", "runs", "dot"],
205
+ };
206
+ export const KYOGIJO_TARGET_STYLE = `
207
+ .kyogijo .ky-tg-bg{fill:var(--ky-surface)}
208
+ .kyogijo.ky-target-field .ky-tg-bg{fill:var(--ky-apron)}
209
+ .kyogijo .ky-tg-open{fill:var(--ky-zone);opacity:.55}
210
+ .kyogijo.ky-target-field .ky-tg-open{fill:var(--ky-surface);opacity:.5}
211
+ .kyogijo .ky-tg-cell{fill:none;stroke:var(--ky-line);opacity:.6}
212
+ .kyogijo .ky-tg-out{fill:none;stroke:var(--ky-line);opacity:.4}
213
+ .kyogijo .ky-tg-cell:hover,.kyogijo .ky-tg-out:hover{opacity:1}
214
+ .kyogijo .ky-tg-label{fill:var(--ky-line);font-family:var(--ky-font);font-weight:700;opacity:.85;text-anchor:middle;pointer-events:none}
215
+ .kyogijo .ky-tg-frame{fill:none;stroke:var(--ky-line);stroke-linejoin:round;stroke-linecap:round}
216
+ .kyogijo .ky-tg-solid{fill:var(--ky-line);stroke:none}
217
+ .kyogijo .ky-tg-net{stroke:var(--ky-line);opacity:.3}
218
+ .kyogijo .ky-tg-mark{fill:var(--ky-line);font-family:var(--ky-font);font-weight:800;text-anchor:middle;opacity:.9}
219
+ .kyogijo .ky-tg-ink.ky-tg-cell,.kyogijo .ky-tg-ink.ky-tg-out,.kyogijo .ky-tg-ink.ky-tg-frame{stroke:var(--ky-ink)}
220
+ .kyogijo .ky-tg-ink.ky-tg-solid,.kyogijo .ky-tg-ink.ky-tg-mark{fill:var(--ky-ink)}
221
+ `;
222
+ // ----- drawing
223
+ const words = (language) => KYOGIJO_STRINGS[language];
224
+ function shortNumber(n) {
225
+ return String(Math.round(n * 100) / 100);
226
+ }
227
+ /** Draw a goal, the stumps or a strike zone with shots laid on it. */
228
+ export function drawTarget(shots, options) {
229
+ if (!Array.isArray(shots))
230
+ throw new TypeError("shots is a list of shots");
231
+ if (!TARGET_SPORTS.includes(options.sport))
232
+ throw new RangeError(`Unknown target "${String(options.sport)}": use one of ${TARGET_SPORTS.join(", ")}`);
233
+ const language = options.language === "ja" ? "ja" : "en";
234
+ const all = words(language);
235
+ const t = all.target;
236
+ const f = frameOf(options.sport, options);
237
+ const U = f.unit;
238
+ const k = unitScale(options, f);
239
+ const inputUnit = options.units ?? U;
240
+ const cells = cellsOf(f, options);
241
+ const pitches = f.sport === "mlb";
242
+ const colorBy = options.colorBy === "type" ? "type" : "result";
243
+ const kind = pitches ? "mlb" : f.sport === "cricket" ? "cricket" : "goal";
244
+ const looks = kind === "mlb" ? PITCH_LOOKS : kind === "cricket" ? CRICKET_LOOKS : GOAL_LOOKS;
245
+ // The view and its frame, in the target's unit; the drawing is in feet, as every drawing is.
246
+ const F = U === "ft" ? 1 : FEET_PER_METRE;
247
+ const { x0, x1, z0, z1 } = f.view;
248
+ const viewW = x1 - x0;
249
+ const viewH = z1 - z0;
250
+ const id = options.id !== undefined ? options.id : undefined;
251
+ const surface = { sport: "soccer", pitch: { length: viewW, width: viewH, unit: U }, units: U, language, ...(id === undefined ? {} : { id }), margin: 0 };
252
+ const frame = {
253
+ name: "target",
254
+ title: "A target seen end-on: across from the middle (positive to the viewer's right) and the height",
255
+ sport: "soccer",
256
+ unit: U,
257
+ x: [x0, x1],
258
+ y: [z0, z1],
259
+ origin: "the middle of the target on the ground",
260
+ yDirection: "up",
261
+ size: "variable",
262
+ ends: "none",
263
+ landmarks: ({ length, width }) => ({ x: [[x0, 0], [x1, length]], y: [[z0, width], [z1, 0]] }),
264
+ source: "Kyogijo",
265
+ };
266
+ // The zone of a mark, from its place in the drawing.
267
+ const zoneFeet = (xf, yf) => {
268
+ const x = x0 + xf / F;
269
+ const z = z1 - yf / F;
270
+ return placeIn(f, options, x, z).zone;
271
+ };
272
+ const resultOf = (s) => {
273
+ if (typeof s.outcome === "string" && s.outcome !== "")
274
+ return s.outcome.toLowerCase();
275
+ if (kind === "mlb")
276
+ return "ball";
277
+ if (kind === "cricket")
278
+ return "dot";
279
+ const x = isFiniteNumber(s.x) ? s.x * k : 0;
280
+ const z = isFiniteNumber(s.z) ? s.z * k : 0;
281
+ const p = placeIn(f, options, x, z).placement;
282
+ return p === "in" ? "shot" : p === "high" ? "high" : "wide";
283
+ };
284
+ const types = [...new Set(shots.map((s) => s.type).filter((v) => typeof v === "string" && v !== ""))].sort();
285
+ const rows = shots.map((s, i) => {
286
+ const outcome = resultOf(s);
287
+ const length = isFiniteNumber(s.length) ? s.length : undefined;
288
+ const lengthZone = kind === "cricket" && length !== undefined ? lengthZoneOf(length * METRES[inputUnit]) : undefined;
289
+ const color = pitches ? (colorBy === "type" && s.type !== undefined ? TYPE_COLOURS[types.indexOf(s.type) % TYPE_COLOURS.length] : colorBy === "result" ? RESULT_COLOURS[outcome] : undefined) : undefined;
290
+ const title = s.title ?? (lengthZone === undefined ? undefined : t.lengths[lengthZone]);
291
+ return {
292
+ id: s.id,
293
+ x: isFiniteNumber(s.x) ? round(s.x * k, 4) : null,
294
+ y: isFiniteNumber(s.z) ? round(s.z * k, 4) : null,
295
+ outcome,
296
+ value: s.value,
297
+ team: s.team,
298
+ title,
299
+ color: color === undefined ? undefined : safeColour(color),
300
+ __order: isFiniteNumber(s.order) ? s.order : i + 1,
301
+ };
302
+ });
303
+ const placedRows = rows.filter((r) => r.x !== null && r.y !== null);
304
+ const counts = new Map();
305
+ for (const r of placedRows)
306
+ counts.set(r.outcome, (counts.get(r.outcome) ?? 0) + 1);
307
+ const zones = {};
308
+ for (const r of placedRows) {
309
+ const z = placeIn(f, options, r.x, r.y).zone;
310
+ zones[z] = (zones[z] ?? 0) + 1;
311
+ }
312
+ const outcomeLabels = t.outcomes;
313
+ const present = [...counts.keys()];
314
+ const order = ORDER[kind];
315
+ const entries = [...order.filter((o) => present.includes(o)), ...present.filter((o) => !order.includes(o))];
316
+ const markSize = Math.max(viewW * 0.045, (f.width / (pitches ? 6 : 1)) * (pitches ? 0.55 : 0.09));
317
+ const sized = options.sizeBy === "value" && rows.some((r) => isFiniteNumber(r.value));
318
+ const maxValue = Math.max(0.0001, ...rows.map((r) => (isFiniteNumber(r.value) ? r.value : 0)));
319
+ const style = { outcomes: looks, opacity: 0.95, ...(pitches ? { stroke: "none" } : {}), ...(sized ? { size: { domain: [0, maxValue], range: [markSize * 0.5, markSize * 1.6] } } : { size: markSize }) };
320
+ const data = rows;
321
+ const layers = [];
322
+ const listing = entries.length === 0 ? undefined : fill(all.layers.outcomes, { list: entries.map((o) => fill(all.layers.item, { name: outcomeLabels[o] ?? o, n: counts.get(o) ?? 0 })).join(all.layers.listSep) });
323
+ const pointsLayer = {
324
+ kind: "points",
325
+ id: kind === "mlb" ? "pitches" : kind === "cricket" ? "deliveries" : "goal-shots",
326
+ name: kind === "mlb" ? t.pitches : kind === "cricket" ? t.deliveries : all.charts.shots,
327
+ ...(listing === undefined ? {} : { description: listing }),
328
+ from: frame,
329
+ units: U,
330
+ clip: "none",
331
+ marks: true,
332
+ data: data.map((d) => Object.fromEntries(Object.entries(d).filter(([key, v]) => key !== "__order" && v !== undefined))),
333
+ style,
334
+ };
335
+ layers.push(pointsLayer);
336
+ if (pitches && options.numbers !== false) {
337
+ const labelled = {
338
+ kind: "text",
339
+ id: "pitch-numbers",
340
+ name: t.numbers,
341
+ from: frame,
342
+ units: U,
343
+ clip: "none",
344
+ marks: true,
345
+ data: data.map((r) => ({ x: r.x, y: r.y, text: String(r.__order), id: r.id })).filter((d) => d.x !== null),
346
+ style: { size: markSize * 0.62, color: "var(--ky-layer-ink)" },
347
+ };
348
+ layers.push(labelled);
349
+ }
350
+ const rendered = renderLayers(surface, layers, false, undefined, undefined, zoneFeet);
351
+ // The art, in feet.
352
+ const X = (x) => (x - x0) * F;
353
+ const Y = (z) => (z1 - z) * F;
354
+ const art = [];
355
+ const lw = viewW * F * 0.0035;
356
+ art.push(el("rect", { class: "ky-tg-bg", x: 0, y: 0, width: viewW * F, height: viewH * F }));
357
+ const hw = f.width / 2;
358
+ const top = f.floor + f.height;
359
+ if (kind === "goal") {
360
+ art.push(el("rect", { class: "ky-tg-open", x: X(-hw), y: Y(f.height), width: f.width * F, height: f.height * F }));
361
+ const net = [];
362
+ const step = f.width / 16;
363
+ for (let x = -hw + step; x < hw - 1e-6; x += step)
364
+ net.push(`M${num(X(x))} ${num(Y(f.height))}V${num(Y(0))}`);
365
+ for (let z = step; z < f.height - 1e-6; z += step)
366
+ net.push(`M${num(X(-hw))} ${num(Y(z))}H${num(X(hw))}`);
367
+ art.push(el("path", { class: "ky-tg-net", d: net.join(""), "stroke-width": lw * 0.5 }));
368
+ }
369
+ else if (pitches) {
370
+ art.push(el("rect", { class: "ky-tg-open", x: X(-hw), y: Y(top), width: f.width * F, height: f.height * F }));
371
+ }
372
+ if (options.zones !== false || pitches) {
373
+ const grid = options.zones !== false || pitches;
374
+ if (grid) {
375
+ const labels = options.zones === "labels" || (pitches && options.zones !== false);
376
+ cells.forEach((c, i) => {
377
+ if (kind === "cricket" && Math.abs(c.x0) > 50)
378
+ return;
379
+ const cx0 = Math.max(c.x0, x0);
380
+ const cx1 = Math.min(c.x1, x1);
381
+ const cz0 = Math.max(c.z0, z0);
382
+ const cz1 = Math.min(c.z1, z1);
383
+ if (!(cx1 > cx0) || !(cz1 > cz0))
384
+ return;
385
+ const outer = pitches && i >= 9;
386
+ const name = t.zones[c.id] ?? c.id;
387
+ art.push(el("rect", { class: outer ? "ky-tg-out ky-tg-ink" : kind === "cricket" ? "ky-tg-cell ky-tg-ink" : "ky-tg-cell", "stroke-width": outer ? lw * 0.8 : lw * 0.9, "stroke-dasharray": `${num(lw * 4)} ${num(lw * 3)}`, x: X(cx0), y: Y(cz1), width: (cx1 - cx0) * F, height: (cz1 - cz0) * F, "data-mark-id": c.id, "data-index": i, "data-zone": c.id }, `<title>${esc(name)}</title>`));
388
+ if (labels && !outer && !(kind === "cricket")) {
389
+ const size = Math.min(((cx1 - cx0) * F) / Math.max(4, name.length * 0.62), ((cz1 - cz0) * F) * 0.3);
390
+ art.push(`<text class="ky-tg-label" x="${num(X((cx0 + cx1) / 2))}" y="${num(Y((cz0 + cz1) / 2) + size * 0.35)}" font-size="${num(size)}">${esc(pitches ? c.id.replace("zone-", "") : name)}</text>`);
391
+ }
392
+ });
393
+ }
394
+ }
395
+ if (kind === "goal") {
396
+ art.push(el("path", { class: "ky-tg-frame", d: `M${num(X(-hw))} ${num(Y(0))}V${num(Y(f.height))}H${num(X(hw))}V${num(Y(0))}`, "stroke-width": lw * 2.4 }));
397
+ art.push(el("path", { class: "ky-tg-frame", d: `M0 ${num(Y(0))}H${num(viewW * F)}`, "stroke-width": lw * 1.4 }));
398
+ }
399
+ else if (pitches) {
400
+ art.push(el("rect", { class: "ky-tg-frame", x: X(-hw), y: Y(top), width: f.width * F, height: f.height * F, "stroke-width": lw * 2.2 }));
401
+ // Home plate seen from the catcher's side: its 17 inches across at the ground, a pentagon's front edge and the two sloping sides.
402
+ const py = Y(0);
403
+ const p = (PLATE / 2) * F;
404
+ art.push(el("path", { class: "ky-tg-solid ky-tg-ink", d: `M${num(X(0) - p)} ${num(py)}H${num(X(0) + p)}L${num(X(0) + p * 0.7)} ${num(py + 0.18 * F)}H${num(X(0) - p * 0.7)}Z`, opacity: 0.8 }));
405
+ const side = options.bats === "L" ? 1 : -1;
406
+ const bx = X(side * (hw + 0.85));
407
+ const by = Y(top - f.height * 0.2);
408
+ art.push(el("path", { class: "ky-tg-frame ky-tg-ink", d: `M${num(bx)} ${num(Y(f.floor - 0.1))}V${num(Y(top + 0.25))}`, "stroke-width": lw * 3, opacity: 0.6 }));
409
+ art.push(`<text class="ky-tg-mark ky-tg-ink" x="${num(bx)}" y="${num(by)}" font-size="${num(0.4 * F)}">${options.bats === "L" ? "L" : "R"}</text>`);
410
+ }
411
+ else {
412
+ const d = 1.5 / 39.37;
413
+ const gap = (f.width - 3 * d) / 2;
414
+ for (let i = 0; i < 3; i += 1) {
415
+ const cx = -hw + d / 2 + i * (d + gap);
416
+ art.push(el("rect", { class: "ky-tg-solid ky-tg-ink", x: X(cx - d / 2), y: Y(f.height), width: d * F, height: f.height * F }));
417
+ }
418
+ // Two bails, each 4.31 in long and standing 0.5 in over the stumps, across the gaps: the Laws allow no more than that above the stumps.
419
+ const bail = 0.0125;
420
+ for (let i = 0; i < 2; i += 1) {
421
+ const a = -hw + d + i * (d + gap) - d * 0.1;
422
+ art.push(el("rect", { class: "ky-tg-solid ky-tg-ink", x: X(a), y: Y(f.height + bail), width: (gap + d * 0.2) * F, height: bail * F }));
423
+ }
424
+ art.push(el("path", { class: "ky-tg-frame ky-tg-ink", d: `M0 ${num(Y(0))}H${num(viewW * F)}`, "stroke-width": lw * 1.4 }));
425
+ }
426
+ const title = options.title ?? (kind === "mlb" ? t.titleStrike : kind === "cricket" ? t.titleStumps : fill(t.titleGoal, { name: t.sports[f.sport] }));
427
+ const unitWord = t.units[U];
428
+ const description = [
429
+ kind === "mlb"
430
+ ? fill(t.descStrike, { w: shortNumber(f.width * 12), bottom: shortNumber(f.floor), top: shortNumber(top), side: options.bats === "L" ? t.right : t.left })
431
+ : kind === "cricket"
432
+ ? t.descStumps
433
+ : fill(t.descGoal, { name: t.sports[f.sport], w: shortNumber(f.width), h: shortNumber(f.height), unit: unitWord }),
434
+ rendered.summaries.length === 0 ? "" : `${all.layers.data}${all.said.colon}${rendered.summaries.join(all.said.gap)}`,
435
+ ]
436
+ .filter(Boolean)
437
+ .join(all.said.gap);
438
+ const gid = drawingId(surface);
439
+ const theme = options.theme === "light" || options.theme === "dark" ? options.theme : undefined;
440
+ const cls = safeClass(options.className);
441
+ const svg = el("svg", {
442
+ xmlns: "http://www.w3.org/2000/svg",
443
+ class: `kyogijo ky-${f.sport === "mlb" ? "mlb" : f.sport} ky-target ky-target-${kind === "goal" ? "goal" : "field"} ky-${kind === "mlb" ? "strike-zone" : kind === "cricket" ? "stumps" : "goal-mouth"}${cls === "" ? "" : ` ${cls}`}`,
444
+ viewBox: `0 0 ${num(viewW * F)} ${num(viewH * F)}`,
445
+ role: "img",
446
+ "aria-labelledby": `${gid}-t ${gid}-d`,
447
+ "data-sport": f.sport,
448
+ "data-target": kind === "mlb" ? "strike-zone" : kind === "cricket" ? "stumps" : "goal",
449
+ "data-orientation": "landscape",
450
+ "data-theme": theme,
451
+ "data-version": VERSION,
452
+ }, `<title id="${gid}-t">${esc(title)}</title><desc id="${gid}-d">${esc(description)}</desc>${options.style === "none" ? "" : `<style>${KYOGIJO_BASE_STYLE}${KYOGIJO_TARGET_STYLE}</style>`}<g class="ky-surface-group">${art.join("")}${rendered.svg}</g>`);
453
+ const key = entries.map((o) => ({ id: o, label: outcomeLabels[o] ?? o, shape: looks[o]?.shape ?? "circle", hollow: looks[o]?.hollow === true, ...(pitches && colorBy === "result" && RESULT_COLOURS[o] !== undefined ? { color: RESULT_COLOURS[o] } : {}) }));
454
+ return {
455
+ svg,
456
+ layers,
457
+ placed: placedRows.length,
458
+ skipped: rows.length - placedRows.length,
459
+ zones,
460
+ key,
461
+ target: f,
462
+ table: () => layerTable(layers, { language, surface: surface }),
463
+ };
464
+ }
465
+ /**
466
+ * The mouth of a goal seen from the shooter's side, with the shots laid on it: where each crossed the goal line (`x` across from the middle, positive to the shooter's right, and `z` up). A shot wide of the posts or over the bar is drawn
467
+ * outside the frame, a post or crossbar on it; the nine zones (the top shelf and the five-hole in hockey, the top corners and the bottom corners everywhere) are drawn when asked and counted in `zones`.
468
+ * @example
469
+ * ```ts
470
+ * import { drawGoalMouth } from "@johnmorrisdotca/kyogijo-stadiums/target";
471
+ * const view = drawGoalMouth(
472
+ * [
473
+ * { x: -2.4, z: 3.6, outcome: "goal" },
474
+ * { x: 0, z: 0.4, outcome: "saved" },
475
+ * { x: 3.4, z: 2, outcome: "wide" },
476
+ * ],
477
+ * { sport: "nhl", zones: true },
478
+ * );
479
+ * console.log(view.placed, view.zones["top-left-corner"], view.zones["wide-right"], view.target.width, view.target.height);
480
+ * // 3 1 1 6 4
481
+ * ```
482
+ */
483
+ export function drawGoalMouth(shots, options) {
484
+ if (!GOAL_SPORTS.includes(options.sport))
485
+ throw new RangeError(`A goal mouth is for ${GOAL_SPORTS.join(", ")}, not "${String(options.sport)}"`);
486
+ return drawTarget(shots, options);
487
+ }
488
+ /**
489
+ * The strike zone seen from the catcher's position: home plate's 17 inches across, the zone from the batter's knees to his belt (`bottom` and `top`, in feet), the nine cells of the zone (1 to 9, top left to bottom right as the catcher sees them) and
490
+ * the four chase zones round them (11 to 14), the batter's side, and the pitches numbered in order, coloured by result or type and shaped by what became of them. The place is Statcast's `plate_x` and `plate_z`.
491
+ * @example
492
+ * ```ts
493
+ * import { drawStrikeZone } from "@johnmorrisdotca/kyogijo-stadiums/target";
494
+ * const view = drawStrikeZone([
495
+ * { x: 0.1, z: 2.6, outcome: "called-strike", type: "FF" },
496
+ * { x: 1.3, z: 1.1, outcome: "ball", type: "SL" },
497
+ * { x: -0.4, z: 2.0, outcome: "swinging-strike", type: "SL" },
498
+ * ], { bats: "R" });
499
+ * console.log(view.placed, view.zones["zone-5"], view.zones["zone-13"] ?? 0, view.key.map((k) => k.id).join(" "));
500
+ * // 3 1 0 ball called-strike swinging-strike
501
+ * ```
502
+ */
503
+ export function drawStrikeZone(pitches, options = {}) {
504
+ return drawTarget(pitches, { ...options, sport: "mlb" });
505
+ }
506
+ /**
507
+ * The stumps seen from the bowler's end: three stumps across 9 inches and 28 inches high with the bails, the line zones either side of them, and the deliveries where they reached the stumps' line (`x` across, `z` up), each with the length it
508
+ * pitched at if you give one (`length`): `lengthZoneOf` names its length zone.
509
+ * @example
510
+ * ```ts
511
+ * import { drawStumps, lengthZoneOf } from "@johnmorrisdotca/kyogijo-stadiums/target";
512
+ * const view = drawStumps([{ x: 0.02, z: 0.4, outcome: "wicket", length: 5.1 }, { x: 0.5, z: 0.9, outcome: "dot", length: 7.4 }]);
513
+ * console.log(view.zones["on-stumps"], view.zones["wide-right"], lengthZoneOf(5.1), lengthZoneOf(7.4));
514
+ * // 1 1 good-length short-of-a-length
515
+ * ```
516
+ */
517
+ export function drawStumps(deliveries, options = {}) {
518
+ return drawTarget(deliveries, { ...options, sport: "cricket" });
519
+ }
520
+ function isFiniteNumber(v) {
521
+ return typeof v === "number" && Number.isFinite(v);
522
+ }
523
+ function round(value, places) {
524
+ return Math.round(value * 10 ** places) / 10 ** places;
525
+ }
@@ -66,7 +66,7 @@ export declare function dots(items: readonly Placed<{
66
66
  opacity?: number;
67
67
  }>[], radius: number, titles: (item: Placed<{
68
68
  team?: unknown;
69
- }>) => string | undefined): string;
69
+ }>) => string | undefined, env?: Env): string;
70
70
  /** The caption's note on a table's coordinates: the frame and its unit, or nothing when the numbers are the surface's own. */
71
71
  export declare function frameNote(layer: LayerBase, env: Env): string;
72
72
  /** A unit of area as words in the drawing's language: square feet, square yards and square metres by name, any other unit as "square" and its symbol. */
@@ -2,7 +2,7 @@
2
2
  // put on the surface, and the shape of a table. Kept apart from the layers themselves so that each file of them is about one kind of layer.
3
3
  import { safeColour } from "./colour.js";
4
4
  import { esc, num } from "./format.js";
5
- import { isNum, place } from "./layers.env.js";
5
+ import { isNum, markAttrs, place } from "./layers.env.js";
6
6
  import { FRAMES } from "./frames.js";
7
7
  import { fill } from "./strings.js";
8
8
  /** The class and the custom property that colour a mark: its own colour, else its team's, else the layer's. */
@@ -70,10 +70,10 @@ export function groupByTeam(items, by) {
70
70
  return groups;
71
71
  }
72
72
  /** The dots that mark players, as a group that the stylesheet's `ky-pts` rule draws. */
73
- export function dots(items, radius, titles) {
73
+ export function dots(items, radius, titles, env) {
74
74
  const circles = items.map((item) => {
75
75
  const { cls, css } = paintOf(item.d);
76
- return titled("circle", `${attrs(cls, css, undefined)} cx="${num(item.x)}" cy="${num(item.y)}" r="${num(radius)}"`, titles(item));
76
+ return titled("circle", `${attrs(cls, css, undefined)} cx="${num(item.x)}" cy="${num(item.y)}" r="${num(radius)}"${env === undefined ? "" : markAttrs(env, item.d.id, item.index, [item.x, item.y])}`, titles(item));
77
77
  });
78
78
  return circles.length === 0 ? "" : `<g class="ky-pts" style="--w:${num(radius * 0.25)}">${circles.join("")}</g>`;
79
79
  }
@@ -1,7 +1,9 @@
1
1
  import { type Env, type Made } from "./layers.env.ts";
2
2
  import type { P } from "./layers.marks.ts";
3
3
  import { type TeamTable } from "./team.common.ts";
4
- import type { Formation, FormationLayer, FormationPlayer } from "./team.types.ts";
4
+ import type { Formation, FormationLayer, FormationPlayer, FormationSport } from "./team.types.ts";
5
+ /** The sports whose surface a formation of a sport is drawn on. */
6
+ export declare const SURFACES: Readonly<Record<FormationSport, readonly string[]>>;
5
7
  /** The formation a layer names, checked. */
6
8
  export declare function formationOf(layer: FormationLayer): Formation;
7
9
  /** One position, placed. */
@@ -3,12 +3,12 @@
3
3
  import { esc, num } from "./format.js";
4
4
  import { FRAMES } from "./frames.js";
5
5
  import { formationById, positionName } from "./formations.js";
6
- import { isNum, newMade, opacityOf, place, unitOfLayer } from "./layers.env.js";
6
+ import { isNum, markAttrs, newMade, opacityOf, place, unitOfLayer } from "./layers.env.js";
7
7
  import { fill } from "./strings.js";
8
8
  import { attrs, labelOf, paintOf, round, titled } from "./team.common.js";
9
9
  import { FEET_PER } from "./units.js";
10
10
  /** The sports whose surface a formation of a sport is drawn on. */
11
- const SURFACES = { soccer: ["soccer"], nhl: ["nhl"], nba: ["nba"], nfl: ["nfl", "ifaf"], mlb: ["mlb"] };
11
+ export const SURFACES = { soccer: ["soccer"], nhl: ["nhl"], nba: ["nba"], nfl: ["nfl", "ifaf"], mlb: ["mlb"], rugby: ["rugby"], afl: ["afl"], cricket: ["cricket"], volleyball: ["volleyball"], handball: ["handball"], waterpolo: ["waterpolo"], fieldhockey: ["fieldhockey"], netball: ["netball"] };
12
12
  /** The line of scrimmage the football formations are written for, in the frame `nfl-tracking`. */
13
13
  const MIDFIELD = 60;
14
14
  /** The formation a layer names, checked. */
@@ -72,7 +72,7 @@ export function drawFormation(layer, env) {
72
72
  const player = paintOf(s.player);
73
73
  const cls = [player.cls !== "" ? player.cls : mine.cls, lineup && s.player === undefined ? "ky-e" : ""].filter((c) => c !== "").join(" ");
74
74
  const tip = [s.name, s.player === undefined ? "" : [s.player.name, s.player.number].filter((t) => t !== undefined && t !== "").join(" ")].filter((t) => t !== "").join(env.words.said.colon);
75
- marks.push(titled("circle", `${attrs(cls, player.css !== "" ? player.css : mine.css)} cx="${num(s.at[0])}" cy="${num(s.at[1])}" r="${num(size / 2)}"`, s.player?.title ?? tip));
75
+ marks.push(titled("circle", `${attrs(cls, player.css !== "" ? player.css : mine.css)} cx="${num(s.at[0])}" cy="${num(s.at[1])}" r="${num(size / 2)}"${markAttrs(env, s.player?.id ?? s.id, spots.indexOf(s), [s.at[0], s.at[1]])}`, s.player?.title ?? tip));
76
76
  let label = "";
77
77
  if (mode === "position")
78
78
  label = s.id;
@@ -1,9 +1,9 @@
1
1
  import type { Env, Made } from "./layers.env.ts";
2
2
  import type { TeamTable } from "./team.common.ts";
3
- import type { DelaunayLayer, FlowLayer, FormationLayer, HullLayer, NetworkLayer, SonarLayer, VoronoiLayer } from "./team.types.ts";
3
+ import type { DelaunayLayer, FlowLayer, FormationLayer, HullLayer, LineupLayer, NetworkLayer, SonarLayer, VoronoiLayer } from "./team.types.ts";
4
4
  export type { TeamTable } from "./team.common.ts";
5
5
  /** The layers this file draws. */
6
- export type TeamLayer = NetworkLayer | FormationLayer | VoronoiLayer | DelaunayLayer | HullLayer | FlowLayer | SonarLayer;
6
+ export type TeamLayer = NetworkLayer | FormationLayer | LineupLayer | VoronoiLayer | DelaunayLayer | HullLayer | FlowLayer | SonarLayer;
7
7
  /** The kinds of team layer. */
8
8
  export declare const TEAM_KINDS: readonly string[];
9
9
  /** True for a layer of one of the team kinds. */
@@ -1,9 +1,10 @@
1
1
  import { drawFormation, formationTables } from "./team.formation.js";
2
+ import { drawLineup, lineupTables } from "./team.lineup.js";
2
3
  import { drawFlow, drawSonar, flowTable, sonarTables } from "./team.motion.js";
3
4
  import { drawNetwork, networkTables } from "./team.network.js";
4
5
  import { delaunayTable, drawDelaunay, drawHull, drawVoronoi, hullTable, voronoiTable } from "./team.shape.js";
5
6
  /** The kinds of team layer. */
6
- export const TEAM_KINDS = Object.freeze(["network", "formation", "voronoi", "delaunay", "hull", "flow", "sonar"]);
7
+ export const TEAM_KINDS = Object.freeze(["network", "formation", "lineup", "voronoi", "delaunay", "hull", "flow", "sonar"]);
7
8
  /** True for a layer of one of the team kinds. */
8
9
  export const isTeamLayer = (layer) => TEAM_KINDS.includes(layer.kind);
9
10
  /** Draw a team layer. */
@@ -11,6 +12,7 @@ export function drawTeam(layer, env) {
11
12
  switch (layer.kind) {
12
13
  case "network": return drawNetwork(layer, env);
13
14
  case "formation": return drawFormation(layer, env);
15
+ case "lineup": return drawLineup(layer, env);
14
16
  case "voronoi": return drawVoronoi(layer, env);
15
17
  case "delaunay": return drawDelaunay(layer, env);
16
18
  case "hull": return drawHull(layer, env);
@@ -23,6 +25,7 @@ export function teamTables(layer, env) {
23
25
  switch (layer.kind) {
24
26
  case "network": return networkTables(layer, env);
25
27
  case "formation": return formationTables(layer, env);
28
+ case "lineup": return lineupTables(layer, env);
26
29
  case "voronoi": return voronoiTable(layer, env);
27
30
  case "delaunay": return delaunayTable(layer, env);
28
31
  case "hull": return hullTable(layer, env);