@johnmorrisdotca/kyogijo-stadiums 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,17 @@ All notable changes to this project are written here. The format follows
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.24.0] - 2026-10-20
11
+
12
+ ### Added
13
+
14
+ - **A board over a surface** (`board` in the options of a drawing and of the 3D view, [boards over a surface](./docs/boards.md)): a results board, a bout presentation or a medal table that another package drew (Taisen Brackets', say) is taken as an SVG string with its size and laid over the surface. The package draws no board and imports none: the SVG is written as one image, which cannot run anything, never changed and never stretched (its own shape is kept, so no letter is stretched).
15
+ - **Placed by name or by point.** `position` is `top` (the default), `bottom`, `side` (the right edge), `left`, `corner` (the top right), `top-left`, `top-right`, `bottom-left`, `bottom-right`, or a point of your own, `{ x, y, anchor? }`, as shares of the drawing. A named position keeps to its edge, one margin in, and is made smaller, never moved, where it would reach into the middle of the surface (`BOARD_CLEAR`, a third of the width and height, where the score's panel stands), so the middle is never covered unless `cover: true` or a point of your own says so. `share`, `margin`, `opacity` and a `label` for assistive technology are options; `BOARD_POSITIONS` and `placeBoard(board, box)` are exported.
16
+ - **In 3D the board is a layer over the view, not a plane**: it does not turn, tip or zoom with the surface, never takes the pointer (a drag on it turns the view), is a share of the view's width in its own shape, and its height is held to what keeps the middle of the view clear. `update` replaces it and keeps the view's turn; where the browser cannot draw in 3D the flat drawing is shown with the board on it.
17
+ - **A compact card** takes the board over its surface and not over its strip; **a badge** takes none. A drawing with no board, or with a value that is not a board, is the same text as before.
18
+ - **The demo's boards page** (`boards.html`): a swimming final over the pool, a bout over a judo mat, a 100 m final over the athletics stadium, speed skating over the oval and a medal table in a corner, in 2D and 3D, with the position, the width, the opacity and a point of your own in the address, in English and Japanese. **The docs** have [a page](./docs/boards.md) and [a gallery](https://johnmorrisdotca.github.io/kyogijo-stadiums/docs/gallery-boards.html) of every position, how large a board may be, covering the middle and dark and Japanese.
19
+ - Taisen Brackets 0.17.1 is a development dependency, for the demo's and the gallery's boards (`pnpm boards` writes `demo/boards-data.js`); it is not imported by the package.
20
+
10
21
  ## [0.23.0] - 2026-10-20
11
22
 
12
23
  ### Added
package/README.md CHANGED
@@ -20,14 +20,14 @@ The surfaces of forty-three sports, from football and curling to the budō and t
20
20
  <td align="center" valign="top">
21
21
  <picture>
22
22
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/hero-desk-dark.webp">
23
- <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/hero-desk-light.webp" alt="The demo on a desk, in English: the heading, the language chooser and links, and an NHL rink drawn wide on an ice-blue background with a made-up game on it, the USA on the left, Canada on the right and the score 3 to 2, Final/OT, in the middle. The controls on the right list the sports, the layout and the teams." width="640">
23
+ <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/hero-desk-light.webp" alt="The demo on a desk: an NHL rink drawn wide on an ice-blue background, the USA on the left, Canada on the right and the score 3 to 2, Final/OT, in the middle, with the controls beside it." width="640">
24
24
  </picture>
25
25
  <br><em>The demo on a desk: a rink, with the teams on their sides and the score in the middle.</em>
26
26
  </td>
27
27
  <td align="center" valign="top">
28
28
  <picture>
29
29
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/hero-phone-dark.webp">
30
- <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/hero-phone-light.webp" alt="The demo on a phone: the heading and controls, and under them, on a background of floodlight beams, a football field standing tall, Mexico's green end zone at the top and the USA's navy one at the bottom, each with its flag and name, and a score panel at midfield reading 27 to 24, Final." width="240">
30
+ <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/hero-phone-light.webp" alt="The demo on a phone: a football field standing tall under floodlight beams, Mexico's green end zone at the top and the USA's navy one at the bottom, and a score panel at midfield reading 27 to 24, Final." width="240">
31
31
  </picture>
32
32
  <br><em>On a phone the same surface stands tall, the words still upright.</em>
33
33
  </td>
@@ -73,46 +73,46 @@ console.log(svg.startsWith("<svg"), svg.includes("USA 3, Canada 2"));
73
73
  ## Features
74
74
 
75
75
  - **Forty-three surfaces, to scale.** The NFL, CFL and IFAF fields, the NHL rink, the NBA court, the soccer pitch and futsal court, the baseball field, the curling sheet, twelve net and racket sports, the pools, the athletics stadium, the skating rinks, seven field sports, the budō, the MMA cage, the rings and the wrestling mats. Every figure is cited in [sources](./docs/sources.md). [The surfaces](./docs/surfaces.md).
76
- - **Each league's rulebook.** `league: "fiba"` draws a court as FIBA marks it: 48 variants, from the WNBA, the NCAA and the IIHF to Little League, PONY, softball, Curling Canada, FIVB's beach volleyball, rugby league and the NLL's box lacrosse, each cited. [League variants](./docs/variants.md).
77
- - **A realistic basketball floor.** `court: { hardwood: true }` draws maple boards that run the length of the court with staggered ends, grain, stained keys, the baskets and stanchions with soft shadows, a coaching box and benches, and a satin sheen, in every league and renderer. The clean floor stays the default. [Hardwood](./docs/hardwood.md).
76
+ - **Each league's rulebook.** `league: "fiba"` draws a court as FIBA marks it: 48 variants, from the WNBA and the IIHF to Little League, softball, rugby league and box lacrosse, each cited. [League variants](./docs/variants.md).
77
+ - **A realistic basketball floor.** `court: { hardwood: true }` draws maple boards that run the length of the court with staggered ends, grain and stained keys, the baskets with soft shadows, and benches. The clean floor stays the default. [Hardwood](./docs/hardwood.md).
78
78
  - **The stands and the crowd.** `crowd: true` draws the seating round any surface that has spectators: a bowl, two sides, a horseshoe, courtside rows or a ballpark's grandstands, with the people as seeded heads and shoulders in the home colours and the empty seats showing (`density`, or `attendance` over `capacity`). [The crowd](./docs/crowd.md).
79
79
  - **Curling, from the rule books.** A sheet from World Curling's and Curling Canada's rules, tall by default, with the board and the hammer in a score, `curlingStones` for the stones of an end, `show: "folded"` for the whole sheet with both houses enlarged beside it. [Curling](./docs/curling.md).
80
80
  - **Net, racket and court sports, from their rulebooks.** The ITF's, FIVB's (indoor, beach, sitting), BWF's, USA Pickleball's and ITTF's courts and table, the padel, squash, racquetball, sepak takraw, teqball, roundnet and netball courts, each in its look, with a board of sets and games. [Net, racket and court sports](./docs/courts.md).
81
- - **Swimming, water polo, diving and artistic swimming, from World Aquatics' regulations.** The 50 or 25 m pool with its lanes, ropes, blocks and touch panels; the water polo field; the diving well; the artistic swimming area. A race's lanes carry names, flags, places and times, with a replay. [Pools](./docs/pools.md).
82
- - **Lacrosse, rugby, Australian football, Gaelic games, cricket, field hockey and handball, from their rulebooks.** The fields and floors of World Lacrosse, World Rugby, the NRL, the AFL, the GAA, the FIH and the IHF, and a cricket ground with a boundary of your own. A score is goals and behinds, goals and points, or runs, wickets and overs. [Field sports](./docs/field-sports.md).
83
- - **The athletics stadium, from World Athletics' rules.** The 400 m track with its lanes and staggers worked out from the running lines, curved and relay starts, hurdles, the steeplechase, every field event, a close-up of each start, the covered 200 m track, with lane draws, results and a race replay. [The athletics stadium](./docs/athletics.md).
84
- - **The skating rinks, from the ISU's rules.** The 400 m speed skating track with its lanes, blocks and the lines of every race worked out lane by lane; the short track's oval; the figure skating rink with its judges' seats; and results, a race replay and the elements of a programme. [The skating rinks](./docs/skating.md).
81
+ - **Swimming, water polo, diving and artistic swimming, from World Aquatics' regulations.** The 50 or 25 m pool with its lanes, ropes, blocks and touch panels; the water polo field; the diving well; the artistic swimming area. [Pools](./docs/pools.md).
82
+ - **Lacrosse, rugby, Australian football, Gaelic games, cricket, field hockey and handball, from their rulebooks.** The fields and floors of World Lacrosse, World Rugby, the NRL, the AFL, the GAA, the FIH and the IHF, and a cricket ground. [Field sports](./docs/field-sports.md).
83
+ - **The athletics stadium, from World Athletics' rules.** The 400 m track with its lanes and staggers worked out from the running lines, every race start, hurdles, the steeplechase and every field event, with lane draws, results and a race replay. [The athletics stadium](./docs/athletics.md).
84
+ - **The skating rinks, from the ISU's rules.** The 400 m speed skating track with its lanes, blocks and the lines of every race worked out lane by lane; the short track's oval; the figure skating rink; and results, a race replay and the elements of a programme. [The skating rinks](./docs/skating.md).
85
85
  - **The budō, from the federations' rules.** The IJF's and the All Japan Judo Federation's judo areas, the WKF's karate areas, the kendo shiai-jo, the sumo dohyō, the kyūdōjō, and **a dojo builder**: a floor of tatami laid as real units, in jō or metres, with its walls, doors, windows and shrine. Scores are judo's ippon, waza-ari and shido, karate's and kendo's points and sumo's kimarite. [The budō](./docs/budo.md).
86
- - **Soccer, to the Laws.** A pitch from Law 1 of the IFAB Laws of the Game of any size, with presets (international, UEFA, MLS, NCAA, the FA's 9-, 7- and 5-a-side, FIFA's futsal), half pitches, five mowing patterns, four goal styles, penalties and extra time in a score. [Soccer](./docs/soccer.md).
86
+ - **Soccer, to the Laws.** A pitch from Law 1 of the IFAB Laws of the Game of any size, with presets from international to 5-a-side, half pitches, five mowing patterns and four goal styles. [Soccer](./docs/soccer.md).
87
87
  - **Every ballpark.** The 30 current MLB parks and 44 that MLB clubs have left, and the 12 home parks of Nippon Professional Baseball with eight other NPB grounds, each with its fence drawn from the distances published for it, as it stood in a season. [Ballparks](./docs/ballparks.md).
88
88
  - **Soccer grounds, arenas and an atlas.** 64 soccer grounds at their published sizes, 167 NHL, NBA, WNBA and PWHL arenas (`drawKyogijo({ sport: "nhl", arena: "boston-garden" })`), and every park, ground, arena and budō hall (330 places) on a map, found by any of its names. [The grounds](./docs/grounds.md), [the arenas](./docs/arenas.md), [the atlas](./docs/atlas.md), [names](./docs/ballparks.md#names).
89
- - **Walls and quirks.** How high the fence stands, stretch by stretch and season by season (the Green Monster at 37 feet 2 inches) (`walls`); and Tal's Hill, Wrigley Field's ivy and basket, poles, bullpens and dugouts (`quirks`), each from the park's article, none guessed: [ballparks](./docs/ballparks.md).
89
+ - **Walls and quirks.** How high the fence stands, stretch by stretch and season by season (`walls`), and a park's hills, ivy, poles, bullpens and dugouts (`quirks`), each from the park's article, none guessed: [ballparks](./docs/ballparks.md).
90
90
  - **Special games.** A surface set inside another, to scale: an NHL rink on a ballpark's infield or a football stadium's field or a baseball field laid over a London stadium's pitch. 18 games, each cited. [Special games](./docs/special-games.md).
91
91
  - **Surfaces through the years.** `year: 1975` draws a surface as its league's rules marked it that season, each change cited. [Surfaces through the years](./docs/eras.md).
92
- - **Score mode.** Each team's name and logo on its side (the end zones, the ends of the ice, the halves of the court, the dugouts) and the score large in the middle, with the competition, the status and the date, and the winner emphasised; a flag or a crest can be round, in a circle with a ring (`markShape: "round"`). [Score and layout](./docs/score.md#round-marks).
93
- - **The situation of a game.** `situation` draws what is happening: hockey's power play or empty net and a shootout; baseball's runners lit on the diamond with a scorebug; football's line of scrimmage, first-down line, red zone and down-and-distance tag, with a drive chart; in the badge and the compact card too. [The situation](./docs/situation.md).
92
+ - **Score mode.** Each team's name and logo on its side (the end zones, the ends of the ice, the halves of the court, the dugouts) and the score large in the middle, with the competition, the status and the date, and the winner emphasised. [Score and layout](./docs/score.md#round-marks).
93
+ - **The situation of a game.** `situation` draws what is happening: hockey's power play and shootout, baseball's runners and count, football's line of scrimmage, first-down line, red zone and down and distance, with a drive chart. [The situation](./docs/situation.md).
94
94
  - **Lineups.** A `lineup` layer places a team's players by position code (`GK`, `CB`, `HK`, `C`) with no coordinates: it reads where each stands from a formation (36 of them, in 13 sports), draws a disc or a jersey with the shirt number and the name under it, a captain's C and a flag. [Lineups](./docs/team-shape.md#lineups).
95
- - **A pageant.** Each team's flag or crest laid large over its half of the surface: `pageant: "carpet"` (a flag at its own ratio with a border) or `"stage"` (a crest on a round disc with a gold ring), an emblem or a trophy in the centre circle, the score in the middle or in the strip; an entrance in CSS for readers who have not asked for less motion. [The pageant](./docs/pageant.md).
95
+ - **A pageant.** Each team's flag or crest laid large over its half of the surface: `pageant: "carpet"` (a flag at its own ratio with a border) or `"stage"` (a crest on a round disc with a gold ring), an emblem or a trophy in the centre circle, the score in the middle or in the strip. [The pageant](./docs/pageant.md).
96
96
  - **Trophies, medals, balls and crests, by id.** `useEmblems(renderEmblem)` plugs in the sister package [Dougu Sports](https://johnmorrisdotca.github.io/dougu-sports/) in one line, and an id goes wherever a logo, a flag or a mark is taken: `logo: "crest:shield"`, `pageant: { emblem: "trophy:hockey-cup" }`, `"ball:puck"`. [Emblems](./docs/emblems.md).
97
97
  - **Country flags, by code.** `useFlags(render)` plugs in the sister package [Hata](https://github.com/johnmorrisdotca/hata) through a small adapter, and a team's `flag: "CA"` is its flag: round in a round mark, at its own proportions on a banner, a badge or a carpet. [Flags](./docs/flags.md).
98
- - **Zoom to a part of a surface.** `range: "offensive-zone"` crops a drawing to a rink's zone, a half court, the paint, the red zone, a soccer half, third or penalty area, the infield, a curling house, or a box of your own in any frame, with its data, heat, labels and score following. [Display ranges](./docs/ranges.md).
98
+ - **Zoom to a part of a surface.** `range: "offensive-zone"` crops a drawing to a rink's zone, a half court, the paint, the red zone, a penalty area, the infield, a curling house, or a box of your own, with its data and score following. [Display ranges](./docs/ranges.md).
99
99
  - **Wide, tall or fit the box.** Every surface lies wide or tall: tall turns it a quarter, keeps every word upright and puts the teams top and bottom. With `orientation: "auto"` the browser helpers follow the box.
100
- - **Where a point lies, by the rules, and a click as a place.** `zoneAt("nhl", [69, 22], { frame: "nhl-api" })` names the zone and the area in it from the surface's own figures, with `isThreePointer`, `inSlot`, `isFair`, `isHomeRun` and the like; `clientPointToField(svg, x, y, { provider })` is a pointer's place in any of the frames, with the zone. [Zones](./docs/zones.md), [pointing](./docs/pointer.md).
100
+ - **Where a point lies, by the rules, and a click as a place.** `zoneAt("nhl", [69, 22], { frame: "nhl-api" })` names the zone from the surface's own figures, and `clientPointToField(svg, x, y, { provider })` is a pointer's place in any of the frames, with the zone. [Zones](./docs/zones.md), [pointing](./docs/pointer.md).
101
101
  - **Goals, the strike zone and the stumps, end-on.** A goal mouth for six sports with the shots laid on it, the catcher's view of the strike zone with its pitches numbered, and the wicket from the bowler's end with its zones. [Targets](./docs/targets.md).
102
- - **Data on the surface.** Points, lines, arrows, text, heat maps, hexagons, zone charts and density contours over any surface, in its own feet or a provider's frame; ready-made shot and pass charts. [Data layers](./docs/layers.md), [charts](./docs/charts.md).
103
- - **Small multiples and backgrounds.** `drawGrid` puts many drawings in one SVG, under a title and a subtitle, over a credit line, with a key they share, in a size for a card or a phone; `background` lays a quiet pattern or a photograph behind a drawing or a grid. [Layouts and backgrounds](./docs/layouts.md).
104
- - **Tournament layouts.** Several surfaces laid out together for one large event, to scale, on one floor, as one SVG: thirteen presets from table tennis, pickleball, curling, judo, karate, badminton and tennis, each with its rule and sources, and a stadium round it. [Tournament layouts](./docs/multi.md).
105
- - **Judging assignments.** Who judges where. [Judging](./docs/judging.md).
106
- - **Replay.** Tracking data played on any surface: the players, the ball and its trail, and the passes, shots and goals as they happen, with controls, the keyboard and a live region, and steps for a reader who prefers reduced motion. [Replay](./docs/replay.md).
107
- - **A 3D view.** Any surface as a floor that turns by mouse, touch, wheel or keys, with four viewpoints (overhead, broadcast, end-on, low corner), and what the rulebook stands on it at its height (nets, baskets, boards and glass, goals, a fence, a cage's mesh, a ring's ropes) and the venue round it (raked stands, roofs, a dojo's walls). [The 3D view](./docs/turn.md).
102
+ - **Data on the surface.** Points, lines, arrows, text, heat maps, hexagons and density contours over any surface, in its own feet or a provider's frame; ready-made shot and pass charts. [Data layers](./docs/layers.md), [charts](./docs/charts.md).
103
+ - **Small multiples and backgrounds.** `drawGrid` puts many drawings in one SVG with a title, a shared key and a credit line, in a size for a card or a phone; `background` lays a quiet pattern or a photograph behind a drawing or a grid. [Layouts and backgrounds](./docs/layouts.md).
104
+ - **Tournament layouts.** Several surfaces laid out together for one large event, to scale, on one floor, as one SVG: thirteen presets, each with its rule and sources, and a stadium round it. [Tournament layouts](./docs/multi.md).
105
+ - **Judging assignments.** Who judges where. [Judging](./docs/judging.md). **Boards over a surface.** [Boards](./docs/boards.md).
106
+ - **Replay.** Tracking data played on any surface: the players, the ball and its trail, and the passes, shots and goals as they happen, with controls, the keyboard and a live region. [Replay](./docs/replay.md).
107
+ - **A 3D view.** Any surface as a floor that turns by mouse, touch, wheel or keys, with four viewpoints (overhead, broadcast, end-on, low corner), and what the rulebook stands on it at its height (nets, baskets, goals, a cage's mesh, a ring's ropes) and the venue round it (raked stands, roofs, a dojo's walls). [The 3D view](./docs/turn.md).
108
108
  - **Renderers.** The same drawing as plain data (a scene), on a canvas (a browser's or a Node canvas), in a d3 selection, as a mark of an Observable Plot, as GeoJSON and as a PNG in Node, each an entry of its own. [Renderers](./docs/renderers.md).
109
- - **Units and frames.** Each surface in its rulebook's unit and any other, and 48 coordinate frames (ten soccer providers, the NHL API, HockeyTech, NBA stats, NFL tracking, MLBAM, Retrosheet, and one for each other surface) to move a provider's points onto a surface. [Units](./docs/units.md), [coordinate frames](./docs/frames.md).
109
+ - **Units and frames.** Each surface in its rulebook's unit and any other, and 48 coordinate frames (ten soccer providers, the NHL API, NBA stats, NFL tracking, MLBAM and more) to move a provider's points onto a surface. [Units](./docs/units.md), [coordinate frames](./docs/frames.md).
110
110
  - **A compact badge and a compact card.** One strip for a list, or the surface with that strip under it.
111
- - **Light and dark**, by the device or the page, and every colour a custom property; eight named looks, colours by feature, line widths and dashes, goal styles and mowing patterns, and arrows, axes and labels for teaching. [Theming](./docs/theming.md), [annotations](./docs/annotations.md).
112
- - **Accessible.** Each drawing is an `img` with a title and a description that say the surface and the result, in English or Japanese. [Accessibility](./docs/accessibility.md).
113
- - **Four ways in.** A function that returns SVG text; a helper that draws into a container; a standard custom element; a frame for pages that run no script. A React helper that does not import React.
111
+ - **Light and dark**, by the device or the page; every colour a custom property; eight named looks, colours by feature, line widths and dashes, and arrows, axes and labels for teaching. [Theming](./docs/theming.md), [annotations](./docs/annotations.md).
112
+ - **Accessible.** Each drawing is an `img` with a title and a description of the surface and the result, in English or Japanese. [Accessibility](./docs/accessibility.md).
113
+ - **Four ways in.** A function that returns SVG text; a helper that draws into a container; a custom element; a frame for pages with no script; a React helper that imports no React.
114
114
  - **No dependencies**, no network requests, nothing stored. Text, colours and logo addresses that come from outside are made safe before they are drawn.
115
- - **A docs site.** Guides, a search over every page, a gallery that draws every picture from the code beside it, and [how it compares](./docs/compare.md) with the other packages: [the documentation](./docs/README.md).
115
+ - **A docs site.** Guides, a search, a gallery that draws every picture from the code beside it, and [how it compares](./docs/compare.md): [the documentation](./docs/README.md).
116
116
 
117
117
  ### What's in it
118
118
 
@@ -122,12 +122,12 @@ console.log(svg.startsWith("<svg"), svg.includes("USA 3, Canada 2"));
122
122
  | `SPORTS`, `PARKS`, `PARK_IDS`, `COURT_THEMES` | The forty-three sports, the 94 ballparks, the court looks. |
123
123
  | `SOCCER_PITCHES`, `SOCCER_PROVIDERS`, `rescale`, `soccerSpec` | The soccer pitches, the data providers' coordinate systems and the map between them. |
124
124
  | `GROUNDS`, `groundById` | The 64 soccer grounds, each pitch at its own size. |
125
- | `EVENTS`, `eventOptions` | The 18 special games: a rink in a ballpark, a court on a carrier's deck, with `host` and `placement` in the options. |
125
+ | `EVENTS`, `eventOptions` | The 18 special games: a rink in a ballpark, a court on a carrier's deck. |
126
126
  | `./zones`, `./pointer`, `./target` | Where a point lies by a sport's own rules, and the yes-or-no questions; a click on a drawing as a place in a provider's numbers; a goal, the strike zone or the stumps seen end-on. |
127
127
  | `./geometry`, `./frames`, `./layers`, `./charts`, `./replay` | The dimensions in any unit; the providers' frames; data layers; ready-made charts; tracking data played on a surface. |
128
- | `./athletics` | The athletics stadium: the plan, the track's geometry (the stagger of any lane), the races and field events, and the lane draw, results, race replay and field marks charts. |
129
- | `./budo` | The budō: the judo and karate areas, the kendo shiai-jo, the sumo dohyō, the kyūdōjō and the dojo builder (`dojo`, `drawDojo`), the plans, the tatami layout, the 82 kimarite, the looks and the five halls. |
130
- | `./skating` | The skating rinks: the plan, the long track's geometry (a skater's way round it, the start of a race), the races, and the pairs, starting positions, results, race replay and programme elements charts. |
128
+ | `./athletics` | The athletics stadium: the plan, the track's geometry, the races and field events, and the lane draw, results, race replay and field marks charts. |
129
+ | `./budo` | The budō: the judo and karate areas, the kendo shiai-jo, the sumo dohyō, the kyūdōjō, the dojo builder (`dojo`, `drawDojo`), the 82 kimarite and the five halls. |
130
+ | `./skating` | The skating rinks: the plan, the long track's geometry, the races, and the pairs, starting positions, results, race replay and programme elements charts. |
131
131
  | `./parks`, `./grounds`, `./arenas`, `./atlas`, `./eras`, `./leagues`, `./events` | The ballparks, the grounds, the arenas, every place, the markings of a season, each league's rulebook, the special games. |
132
132
  | `./turn` | A surface as a 3D view that turns, with the net, basket, goal or posts standing on it. |
133
133
  | `./scene`, `./canvas`, `./d3`, `./plot`, `./geojson`, `./png` | The drawing as data; on a canvas; in a d3 selection; as an Observable Plot mark; as GeoJSON; as a PNG (the optional peer `@resvg/resvg-js`). |
@@ -543,6 +543,34 @@ console.log(layout.needs.length, layout.needs.width);
543
543
 
544
544
  The [demo](https://johnmorrisdotca.github.io/kyogijo-stadiums/halls.html) turns it in 3D.
545
545
 
546
+ ### Boards over a surface
547
+
548
+ `board` lays a results board, a bout presentation or a medal table, any SVG, over a surface in 2D and 3D, off its middle and never stretched ([boards](./docs/boards.md)).
549
+
550
+ <p align="center">
551
+ <picture>
552
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/boards-swimming-desk-dark.webp">
553
+ <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/boards-swimming-desk-light.webp" alt="A results board over the top of a swimming pool: eight swimmers, their times and a record badge." width="640">
554
+ </picture>
555
+ <br><em>A final over the pool.</em>
556
+ </p>
557
+
558
+ <p align="center">
559
+ <picture>
560
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/boards-judo-3d-desk-dark.webp">
561
+ <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/boards-judo-3d-desk-light.webp" alt="A bout presentation board over a judo mat seen in 3D, steady while the mat turns." width="640">
562
+ </picture>
563
+ <br><em>A bout over the mat, in 3D.</em>
564
+ </p>
565
+
566
+ <p align="center">
567
+ <picture>
568
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/boards-medals-desk-dark.webp">
569
+ <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/boards-medals-desk-light.webp" alt="A medal table in the top right corner of an athletics stadium, off the middle of the field." width="640">
570
+ </picture>
571
+ <br><em>A medal table in a corner.</em>
572
+ </p>
573
+
546
574
  ### Who judges where
547
575
 
548
576
  `drawJudging` puts each judo mat's officials by seat in the picture and in a chart ([judging](./docs/judging.md)).
@@ -750,7 +778,7 @@ English and Japanese: the title, the description and the standard statuses of a
750
778
 
751
779
  ## Roadmap
752
780
 
753
- Not here yet, and each welcome as an [issue](https://github.com/johnmorrisdotca/kyogijo-stadiums/issues):
781
+ Not here yet, each welcome as an [issue](https://github.com/johnmorrisdotca/kyogijo-stadiums/issues):
754
782
 
755
783
  - **More surfaces**: the FIBA and college courts, the international rink, rugby pitches, a stadium's seating, soccer's beach and street formats.
756
784
  - **More parks of the past**: the 19th-century grounds, and the parks whose articles give no distances yet (the Kingdome, Joe Robbie Stadium).
@@ -848,12 +876,12 @@ Kyogijo Stadiums is one of thirty-two packages, each made for the same site, eac
848
876
  ```sh
849
877
  pnpm install
850
878
  pnpm check # lint, types, every test, the build and the packed package
851
- pnpm test:package # pack, install and import it as somebody who installed it would
852
- pnpm test:demo # build the demo and the docs and play them in a real browser, at a phone's width and a desk's
879
+ pnpm test:package # pack, install and import it, as a user would
880
+ pnpm test:demo # build the site and play it in a real browser, at a phone's width and a desk's
853
881
  pnpm site # build the demo and the docs site into site/, as the Pages workflow publishes it
854
- pnpm screenshots:readme # remake every README picture from the built demo, in light and dark, each on one of the family's backgrounds
882
+ pnpm screenshots:readme # remake every README picture from the built demo, in light and dark
855
883
  pnpm test:readme # run every example in the README and the docs against the built package
856
- pnpm docs:make # rewrite what the docs make from the source: the Japanese words, the leagues, the frames and units tables
884
+ pnpm docs:make # rewrite what the docs make from the source: the Japanese words and the tables
857
885
  ```
858
886
 
859
887
  ## Contributing
package/dist/draw.js CHANGED
@@ -55,6 +55,7 @@ import { KYOGIJO_STRINGS, fill } from "./strings.js";
55
55
  import { courtStyle } from "./courts.style.js";
56
56
  import { annotate, annotated, wideView } from "./annotate.js";
57
57
  import { KYOGIJO_BACKGROUND_STYLE, laidOn } from "./background.js";
58
+ import { overlayMarkup, placeBoard, resolveBoard } from "./overlay.js";
58
59
  import { KYOGIJO_HARDWOOD_STYLE, resolveHardwood } from "./hardwood.js";
59
60
  import { WALL_SEATS, densityOf, layoutOf as crowdLayout, resolveCrowd } from "./crowd.js";
60
61
  import { drawCrowd, KYOGIJO_CROWD_STYLE } from "./crowd.draw.js";
@@ -901,6 +902,8 @@ function drawnKyogijo(o) {
901
902
  let defs = "";
902
903
  let grounded = false;
903
904
  let foldedSheet = false;
905
+ let boarded;
906
+ const board = sized === "badge" ? undefined : resolveBoard(o.board);
904
907
  let pg;
905
908
  if (isScore(o) && left !== undefined && right !== undefined && sized === "badge") {
906
909
  const badge = badgeMarkup(left, right, { sport, status, parts, id, situation: situation === undefined ? undefined : badgeSituation(situation, language) });
@@ -1065,6 +1068,14 @@ function drawnKyogijo(o) {
1065
1068
  body = group;
1066
1069
  viewBox = `${num(v.x)} ${num(v.y)} ${num(v.w)} ${num(v.h)}`;
1067
1070
  }
1071
+ // A board over the surface, in the drawing's own units, over everything else (a strip's score stays clear of it, under the surface).
1072
+ if (board !== undefined) {
1073
+ const laid = overlayMarkup(board, placeBoard(board, v));
1074
+ if (laid !== "") {
1075
+ body += laid;
1076
+ boarded = typeof board.position === "object" ? "custom" : board.position;
1077
+ }
1078
+ }
1068
1079
  }
1069
1080
  const root = el("svg", {
1070
1081
  xmlns: "http://www.w3.org/2000/svg",
@@ -1084,6 +1095,7 @@ function drawnKyogijo(o) {
1084
1095
  "data-track": sport === "athletics" ? o.athletics?.track : undefined,
1085
1096
  "data-winner": isScore(o) ? winner : undefined,
1086
1097
  "data-pageant": pg === undefined ? undefined : pageant?.style,
1098
+ "data-board": boarded,
1087
1099
  "data-version": VERSION,
1088
1100
  width: safeLength(o.width),
1089
1101
  height: safeLength(o.height),
package/dist/index.d.ts CHANGED
@@ -10,6 +10,8 @@ export { OUTCOMES, drawLayers, layerTable } from "./layers.ts";
10
10
  export { RANGES } from "./ranges.names.ts";
11
11
  export { BACKGROUND_OPACITY, BACKGROUND_PATTERNS, KYOGIJO_BACKGROUND_STYLE } from "./background.ts";
12
12
  export type { Background, BackgroundFit, BackgroundOptions, BackgroundPattern } from "./background.ts";
13
+ export { BOARD_CLEAR, BOARD_POSITIONS, placeBoard } from "./overlay.ts";
14
+ export type { BoardBox, BoardOptions, BoardPoint, BoardPosition } from "./overlay.ts";
13
15
  export { FIELD_SPORTS, fieldPlan, isFieldSport } from "./fields.ts";
14
16
  export { POOL_SPORTS, isPoolSport, poolPlan, swimPlan, venueLayout } from "./pool.ts";
15
17
  export type { ArtisticPlan, Board, DivePlan, LanePlan, PoloPlan, PoolPlan, Rope, RopeColour, SwimPlan, VenueItem, VenueLayout } from "./pool.ts";
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ export { useFlags } from "./flags-source.js";
8
8
  export { OUTCOMES, drawLayers, layerTable } from "./layers.js";
9
9
  export { RANGES } from "./ranges.names.js";
10
10
  export { BACKGROUND_OPACITY, BACKGROUND_PATTERNS, KYOGIJO_BACKGROUND_STYLE } from "./background.js";
11
+ export { BOARD_CLEAR, BOARD_POSITIONS, placeBoard } from "./overlay.js";
11
12
  export { FIELD_SPORTS, fieldPlan, isFieldSport } from "./fields.js";
12
13
  export { POOL_SPORTS, isPoolSport, poolPlan, swimPlan, venueLayout } from "./pool.js";
13
14
  export { formatTime, parseTime } from "./pool.race.js";
@@ -0,0 +1,73 @@
1
+ /** Where a board goes. `top` and `bottom` are centred on the drawing's top and bottom edge, `side` is at the right edge, centred, `left` the same at the left, `corner` is the top right corner, and the four corners by their names. */
2
+ export type BoardPosition = "top" | "bottom" | "side" | "left" | "corner" | "top-left" | "top-right" | "bottom-left" | "bottom-right";
3
+ /** A place of your own: the board's top left corner (or, with `anchor: "center"`, its middle) at this share of the drawing's width (`x`) and height (`y`), 0 to 1. The board may then cover the middle of the surface: you asked for it. */
4
+ export interface BoardPoint {
5
+ x: number;
6
+ y: number;
7
+ anchor?: "start" | "center";
8
+ }
9
+ /** A board over a drawing. */
10
+ export interface BoardOptions {
11
+ /** The board, as SVG text (`<svg …>…</svg>`): the string `resultsBoard`, `boutBoard` or `broadcastMedalTable` of Taisen Brackets gives. It is written as an image, so nothing in it can run and its styles stay in it. Anything else is ignored. */
12
+ svg: string;
13
+ /** The board's own width and height, the numbers its maker gives (`resultsBoardSize`, `boutBoardSize`, `broadcastMedalSize`): they give its shape. Left out, the `viewBox` of the SVG is read. A board with no size is ignored. */
14
+ width?: number;
15
+ height?: number;
16
+ /** Where it goes: a name (`BOARD_POSITIONS`) or a point of your own. `top` by default. */
17
+ position?: BoardPosition | BoardPoint;
18
+ /** How opaque it is, 0 to 1. 1 by default. */
19
+ opacity?: number;
20
+ /** Its width as a share of the drawing's width, 0.1 to 1. By default 0.5 at the top and the bottom, 0.3 at a side and 0.36 in a corner, and 0.4 at a point; a board is never made larger than the room that keeps the middle of the surface clear. */
21
+ share?: number;
22
+ /** The gap between the board and the edge of the drawing, as a share of the drawing's width, 0 to 0.2. 0.02 by default. */
23
+ margin?: number;
24
+ /** Let a named position cover the middle of the surface, so that the board may be as large as `share` says (it is still kept inside the drawing). Off by default. */
25
+ cover?: boolean;
26
+ /** What the board says, in a sentence, for a reader who cannot see it: its title and who leads, say. Without it the board is hidden from assistive technology (it is a picture of what the page says elsewhere). */
27
+ label?: string;
28
+ }
29
+ /** A board that has been read: sizes and places that can be used, and the picture as an address. */
30
+ export interface ResolvedBoard {
31
+ href: string;
32
+ width: number;
33
+ height: number;
34
+ position: BoardPosition | {
35
+ x: number;
36
+ y: number;
37
+ anchor: "start" | "center";
38
+ };
39
+ opacity: number;
40
+ share: number;
41
+ margin: number;
42
+ cover: boolean;
43
+ label: string | undefined;
44
+ }
45
+ /** A box: a drawing's viewBox, or the place a board takes in it. */
46
+ export interface BoardBox {
47
+ x: number;
48
+ y: number;
49
+ w: number;
50
+ h: number;
51
+ }
52
+ /** The positions, in the order a menu lists them. */
53
+ export declare const BOARD_POSITIONS: readonly BoardPosition[];
54
+ /** The share of a drawing's width and of its height that is kept clear round its middle: a board placed by name does not reach into it, so the middle of the playing area, where the score's panel stands, is never covered. */
55
+ export declare const BOARD_CLEAR = 0.34;
56
+ /** The shape of the box a 3D view is placed for (16 by 10, the view's own) and the units of it: a board in 3D is placed for this box and keeps its place, as a share of the box, when the box is another shape. */
57
+ export declare const BOARD_TURN_BOX: BoardBox;
58
+ /** The board an option asks for, read: a bad one is `undefined` and the drawing is as it was without it, as a bad `background` image is. */
59
+ export declare function resolveBoard(value: unknown): ResolvedBoard | undefined;
60
+ /**
61
+ * The place a board takes in a box. A named position keeps to its edge and takes the share asked for, unless that would reach into the middle (`BOARD_CLEAR` of the box's width and height, centred): then the board is made smaller, down
62
+ * to nothing in a box too small to hold one, and never moved. `cover` lifts that, and a point of your own is never held to it. The board keeps its own shape, and stays inside the box.
63
+ */
64
+ export declare function placeBoard(board: ResolvedBoard, view: BoardBox): BoardBox;
65
+ /** The board as markup in a drawing: an image in the box `placeBoard` gave, in the drawing's own units, over everything else. Nothing for a box with no room. */
66
+ export declare function overlayMarkup(board: ResolvedBoard, box: BoardBox): string;
67
+ /**
68
+ * The 3D view's board: a layer over the whole view (not in the scene, so it never turns, tips or zooms with it), and in it the picture at its size as a share of the view's width, its own shape kept (`height:auto`, `object-fit:contain`),
69
+ * placed by flex alignment on the view's edges. Its largest height is what the box `BOARD_TURN_BOX` gives it, so that on a wider view than that it still does not reach into the middle. It never takes the pointer: a drag on it turns the view.
70
+ */
71
+ export declare function turnBoardMarkup(board: ResolvedBoard): string;
72
+ /** The rules of the 3D view's board layer, written into `KYOGIJO_TURN_STYLE`. */
73
+ export declare const KYOGIJO_TURN_BOARD_STYLE: string;
@@ -0,0 +1,182 @@
1
+ // A board laid over a drawing: a results board, a bout presentation, a medal table, drawn by anything else as an SVG (Taisen Brackets', say) and placed here, over the surface, in 2D (`board` in the drawing's
2
+ // options) and in 3D (`board` in the options of the 3D view, where it is a screen-space overlay over the view and never a plane that turns). The package takes the board as a string and draws no board of its own and
3
+ // reads nothing from the one it is given but its size: it is written as an image, which cannot run scripts, scaled evenly (its own shape is kept, so no letter is ever stretched), and placed by a named position
4
+ // that keeps the middle of the surface clear, or by a point of the caller's choosing. Pure: the same options always give the same string.
5
+ import { safePicture } from "./colour.js";
6
+ import { el, esc, num } from "./format.js";
7
+ /** The positions, in the order a menu lists them. */
8
+ export const BOARD_POSITIONS = ["top", "bottom", "side", "left", "corner", "top-left", "top-right", "bottom-left", "bottom-right"];
9
+ /** The share of a drawing's width and of its height that is kept clear round its middle: a board placed by name does not reach into it, so the middle of the playing area, where the score's panel stands, is never covered. */
10
+ export const BOARD_CLEAR = 0.34;
11
+ /** The shape of the box a 3D view is placed for (16 by 10, the view's own) and the units of it: a board in 3D is placed for this box and keeps its place, as a share of the box, when the box is another shape. */
12
+ export const BOARD_TURN_BOX = { x: 0, y: 0, w: 1600, h: 1000 };
13
+ const MAX_SIZE = 100000;
14
+ const isPosition = (value) => typeof value === "string" && BOARD_POSITIONS.includes(value);
15
+ const finite = (value) => typeof value === "number" && Number.isFinite(value);
16
+ const between = (value, low, high) => Math.min(high, Math.max(low, value));
17
+ /** The width and height of an SVG document's root, from its `viewBox`. */
18
+ function viewBoxOf(svg) {
19
+ const root = /^\s*(?:<\?xml[^>]*\?>\s*)?(?:<!--[\s\S]*?-->\s*)*<svg\b[^>]*>/i.exec(svg)?.[0];
20
+ const box = root === undefined ? undefined : /\sviewBox\s*=\s*"\s*(-?[\d.]+)[\s,]+(-?[\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)\s*"/i.exec(root);
21
+ if (box === undefined || box === null)
22
+ return undefined;
23
+ const [width, height] = [Number(box[3]), Number(box[4])];
24
+ return Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0 ? { width, height } : undefined;
25
+ }
26
+ const defaultShare = (position) => {
27
+ if (typeof position === "object")
28
+ return 0.4;
29
+ if (position === "top" || position === "bottom")
30
+ return 0.5;
31
+ if (position === "side" || position === "left")
32
+ return 0.3;
33
+ return 0.36;
34
+ };
35
+ /** The board an option asks for, read: a bad one is `undefined` and the drawing is as it was without it, as a bad `background` image is. */
36
+ export function resolveBoard(value) {
37
+ if (typeof value !== "object" || value === null)
38
+ return undefined;
39
+ const o = value;
40
+ if (typeof o.svg !== "string" || !/^\s*(?:<\?xml|<svg)\b/i.test(o.svg))
41
+ return undefined;
42
+ const href = safePicture(o.svg);
43
+ if (href === undefined)
44
+ return undefined;
45
+ const own = viewBoxOf(o.svg);
46
+ const width = finite(o.width) && o.width > 0 ? o.width : own?.width;
47
+ const height = finite(o.height) && o.height > 0 ? o.height : own?.height;
48
+ if (width === undefined || height === undefined || width > MAX_SIZE || height > MAX_SIZE)
49
+ return undefined;
50
+ let position = "top";
51
+ const asked = o.position;
52
+ if (isPosition(asked))
53
+ position = asked;
54
+ else if (typeof asked === "object" && asked !== null && finite(asked.x) && finite(asked.y)) {
55
+ const point = asked;
56
+ position = { x: between(point.x, 0, 1), y: between(point.y, 0, 1), anchor: point.anchor === "center" ? "center" : "start" };
57
+ }
58
+ const label = typeof o.label === "string" && o.label.trim() !== "" ? o.label.trim().slice(0, 300) : undefined;
59
+ return {
60
+ href,
61
+ width,
62
+ height,
63
+ position,
64
+ opacity: finite(o.opacity) ? between(o.opacity, 0, 1) : 1,
65
+ share: finite(o.share) ? between(o.share, 0.1, 1) : defaultShare(position),
66
+ margin: finite(o.margin) ? between(o.margin, 0, 0.2) : 0.02,
67
+ cover: o.cover === true,
68
+ label,
69
+ };
70
+ }
71
+ /** Where a named position stands: which edge it keeps to on each axis. */
72
+ function edges(position) {
73
+ switch (position) {
74
+ case "top":
75
+ return { v: "top", h: "centre" };
76
+ case "bottom":
77
+ return { v: "bottom", h: "centre" };
78
+ case "side":
79
+ return { v: "middle", h: "right" };
80
+ case "left":
81
+ return { v: "middle", h: "left" };
82
+ case "corner":
83
+ case "top-right":
84
+ return { v: "top", h: "right" };
85
+ case "top-left":
86
+ return { v: "top", h: "left" };
87
+ case "bottom-left":
88
+ return { v: "bottom", h: "left" };
89
+ case "bottom-right":
90
+ return { v: "bottom", h: "right" };
91
+ }
92
+ }
93
+ /**
94
+ * The place a board takes in a box. A named position keeps to its edge and takes the share asked for, unless that would reach into the middle (`BOARD_CLEAR` of the box's width and height, centred): then the board is made smaller, down
95
+ * to nothing in a box too small to hold one, and never moved. `cover` lifts that, and a point of your own is never held to it. The board keeps its own shape, and stays inside the box.
96
+ */
97
+ export function placeBoard(board, view) {
98
+ const ratio = board.width / board.height;
99
+ const gap = board.margin * view.w;
100
+ const inner = { x0: view.x + gap, y0: view.y + gap, x1: view.x + view.w - gap, y1: view.y + view.h - gap };
101
+ const innerW = Math.max(0, inner.x1 - inner.x0);
102
+ const innerH = Math.max(0, inner.y1 - inner.y0);
103
+ const wanted = board.share * view.w;
104
+ const point = board.position;
105
+ if (typeof point === "object") {
106
+ const w = Math.max(0, Math.min(wanted, view.w, view.h * ratio));
107
+ const h = w / ratio;
108
+ const x = view.x + point.x * view.w - (point.anchor === "center" ? w / 2 : 0);
109
+ const y = view.y + point.y * view.h - (point.anchor === "center" ? h / 2 : 0);
110
+ return { x: between(x, view.x, view.x + view.w - w), y: between(y, view.y, view.y + view.h - h), w, h };
111
+ }
112
+ const { v, h: side } = edges(point);
113
+ const clear = { x0: view.x + view.w * (0.5 - BOARD_CLEAR / 2), x1: view.x + view.w * (0.5 + BOARD_CLEAR / 2), y0: view.y + view.h * (0.5 - BOARD_CLEAR / 2), y1: view.y + view.h * (0.5 + BOARD_CLEAR / 2) };
114
+ let room = Math.min(innerW, innerH * ratio);
115
+ if (!board.cover) {
116
+ // Beside the middle (to its right, or to its left) and above or below it: how wide the board may be in each.
117
+ const beside = side === "right" ? inner.x1 - (clear.x1 + gap) : side === "left" ? clear.x0 - gap - inner.x0 : 0;
118
+ const above = v === "top" ? clear.y0 - gap - inner.y0 : v === "bottom" ? inner.y1 - (clear.y1 + gap) : 0;
119
+ if (v === "middle")
120
+ room = Math.min(beside, innerH * ratio);
121
+ else if (side === "centre")
122
+ room = Math.min(above * ratio, innerW);
123
+ else
124
+ room = Math.max(Math.min(beside, innerH * ratio), Math.min(above * ratio, innerW));
125
+ }
126
+ const w = Math.max(0, Math.min(wanted, room));
127
+ const h = w / ratio;
128
+ const x = side === "left" ? inner.x0 : side === "right" ? inner.x1 - w : view.x + (view.w - w) / 2;
129
+ const y = v === "top" ? inner.y0 : v === "bottom" ? inner.y1 - h : view.y + (view.h - h) / 2;
130
+ return { x, y, w, h };
131
+ }
132
+ /** The name a position goes by in the markup. */
133
+ const positionName = (board) => (typeof board.position === "object" ? "custom" : board.position);
134
+ /** The board as markup in a drawing: an image in the box `placeBoard` gave, in the drawing's own units, over everything else. Nothing for a box with no room. */
135
+ export function overlayMarkup(board, box) {
136
+ if (!(box.w > 0) || !(box.h > 0))
137
+ return "";
138
+ return el("g", {
139
+ class: "ky-board",
140
+ "data-position": positionName(board),
141
+ opacity: board.opacity < 1 ? board.opacity : undefined,
142
+ "pointer-events": "none",
143
+ role: board.label === undefined ? undefined : "img",
144
+ "aria-label": board.label,
145
+ "aria-hidden": board.label === undefined ? "true" : undefined,
146
+ }, el("image", { class: "ky-board-img", href: board.href, x: box.x, y: box.y, width: box.w, height: box.h, preserveAspectRatio: "xMidYMid meet" }));
147
+ }
148
+ /**
149
+ * The 3D view's board: a layer over the whole view (not in the scene, so it never turns, tips or zooms with it), and in it the picture at its size as a share of the view's width, its own shape kept (`height:auto`, `object-fit:contain`),
150
+ * placed by flex alignment on the view's edges. Its largest height is what the box `BOARD_TURN_BOX` gives it, so that on a wider view than that it still does not reach into the middle. It never takes the pointer: a drag on it turns the view.
151
+ */
152
+ export function turnBoardMarkup(board) {
153
+ const box = placeBoard(board, BOARD_TURN_BOX);
154
+ if (!(box.w > 0))
155
+ return "";
156
+ const percent = (n) => `${num(Math.round(n * 1000) / 10)}%`;
157
+ const style = [`width:${percent(box.w / BOARD_TURN_BOX.w)}`, `max-height:${percent(box.h / BOARD_TURN_BOX.h)}`];
158
+ if (typeof board.position === "object") {
159
+ style.push("position:absolute", `left:${percent(board.position.x)}`, `top:${percent(board.position.y)}`, "margin:0");
160
+ if (board.position.anchor === "center")
161
+ style.push("translate:-50% -50%");
162
+ }
163
+ else
164
+ style.push(`margin:${percent(board.margin)}`);
165
+ if (board.opacity < 1)
166
+ style.push(`opacity:${num(board.opacity)}`);
167
+ const attributes = `class="ky-turn-board" data-position="${positionName(board)}"${board.label === undefined ? ' aria-hidden="true"' : ` role="img" aria-label="${esc(board.label)}"`}`;
168
+ return `<div ${attributes}><img class="ky-turn-board-img" src="${esc(board.href)}" alt="" width="${num(board.width)}" height="${num(board.height)}" style="${style.join(";")}"></div>`;
169
+ }
170
+ /** The rules of the 3D view's board layer, written into `KYOGIJO_TURN_STYLE`. */
171
+ export const KYOGIJO_TURN_BOARD_STYLE = `
172
+ .ky-turn-board{position:absolute;inset:0;z-index:var(--ky-turn-board-z,9999);display:flex;pointer-events:none;overflow:hidden}
173
+ .ky-turn-board-img{display:block;height:auto;object-fit:contain;flex:none;min-width:0}
174
+ .ky-turn-board[data-position=top]{justify-content:center;align-items:flex-start}
175
+ .ky-turn-board[data-position=bottom]{justify-content:center;align-items:flex-end}
176
+ .ky-turn-board[data-position=side]{justify-content:flex-end;align-items:center}
177
+ .ky-turn-board[data-position=left]{justify-content:flex-start;align-items:center}
178
+ .ky-turn-board[data-position=corner],.ky-turn-board[data-position=top-right]{justify-content:flex-end;align-items:flex-start}
179
+ .ky-turn-board[data-position=top-left]{justify-content:flex-start;align-items:flex-start}
180
+ .ky-turn-board[data-position=bottom-left]{justify-content:flex-start;align-items:flex-end}
181
+ .ky-turn-board[data-position=bottom-right]{justify-content:flex-end;align-items:flex-end}
182
+ .ky-turn-board[data-position=custom]{display:block}`.trim();
package/dist/turn.dom.js CHANGED
@@ -8,6 +8,7 @@ import { buildTurn, floorOptions, openingView, turnWords } from "./turn.model.js
8
8
  import { controlsMarkup, layoutOf, turnMarkupOf, worldMarkup } from "./turn.markup.js";
9
9
  import { KYOGIJO_TURN_STYLE } from "./turn.style.js";
10
10
  import { esc } from "./format.js";
11
+ import { turnBoardMarkup } from "./overlay.js";
11
12
  /** True where the page can draw a plane in 3D by one `matrix3d`: that is, nearly everywhere with a stylesheet. */
12
13
  export function turnSupported(win = typeof window === "undefined" ? undefined : window) {
13
14
  const css = win?.CSS;
@@ -47,7 +48,7 @@ export function mountTurn(host, options, settings, build) {
47
48
  if (!supported) {
48
49
  // The flat drawing, with a sentence that says why it does not turn when the browser is the reason.
49
50
  const flat = (next) => {
50
- host.innerHTML = `<div class="ky-turn" data-mode="flat"><style>${KYOGIJO_TURN_STYLE}</style>${model.flat ?? drawKyogijo(floorOptions(next))}${model.turnable ? `<p class="ky-turn-flat">${esc(turnWords(model).flat)}</p>` : ""}</div>`;
51
+ host.innerHTML = `<div class="ky-turn" data-mode="flat"><style>${KYOGIJO_TURN_STYLE}</style>${model.flat ?? drawKyogijo({ ...floorOptions(next), board: next.board })}${model.turnable ? `<p class="ky-turn-flat">${esc(turnWords(model).flat)}</p>` : ""}</div>`;
51
52
  };
52
53
  flat(options);
53
54
  return {
@@ -456,6 +457,10 @@ export function mountTurn(host, options, settings, build) {
456
457
  world.setAttribute("data-theme", forced);
457
458
  }
458
459
  gather();
460
+ // The board is a layer of its own over the view, after the scene: it is written again, and the view's turn is not touched.
461
+ stage.querySelector(".ky-turn-board")?.remove();
462
+ if (model.board !== undefined)
463
+ stage.insertAdjacentHTML("beforeend", turnBoardMarkup(model.board));
459
464
  stage.setAttribute("aria-label", model.title);
460
465
  root.querySelector(".ky-turn-help").textContent = `${turnWords(model).keys}${model.standing === "" ? "" : ` ${model.standing}`}`;
461
466
  const theme = model.options.theme;
@@ -7,6 +7,7 @@ import { cross, facing, fadeOf, n, size, unit } from "./turn.kit.js";
7
7
  import { buildTurn, turnWords } from "./turn.model.js";
8
8
  import { KYOGIJO_TURN_STYLE } from "./turn.style.js";
9
9
  import { esc } from "./format.js";
10
+ import { turnBoardMarkup } from "./overlay.js";
10
11
  const px = (value) => `${Math.round(value * 1000) / 1000}`;
11
12
  /**
12
13
  * The matrix that stands a plane in the scene, before the camera: its x along `u`, its y along `v`, its corner `o`, in pixels from the middle of the scene
@@ -117,7 +118,7 @@ export function turnMarkupOf(model, settings = {}) {
117
118
  const sport = model.options.sport;
118
119
  return (`<div class="ky-turn" id="${esc(model.id)}" data-sport="${esc(String(sport))}" data-mode="3d" data-touch="${settings.touch === "turn" ? "turn" : "scroll"}" data-preset="${presetOf(view, model.presets ?? sport) ?? "custom"}"${theme === "light" || theme === "dark" ? ` data-theme="${theme}"` : ""} lang="${model.language}">` +
119
120
  sheet +
120
- `<div class="ky-turn-view" tabindex="0" role="group" aria-roledescription="${esc(words.region)}" aria-label="${esc(model.title)}" aria-describedby="${esc(model.id)}-help" aria-keyshortcuts="ArrowLeft ArrowRight ArrowUp ArrowDown + - 0 1 2 3 4 Home Space"><div ${worldAttributes(model)}>${worldMarkup(model, layout)}</div></div>` +
121
+ `<div class="ky-turn-view" tabindex="0" role="group" aria-roledescription="${esc(words.region)}" aria-label="${esc(model.title)}" aria-describedby="${esc(model.id)}-help" aria-keyshortcuts="ArrowLeft ArrowRight ArrowUp ArrowDown + - 0 1 2 3 4 Home Space"><div ${worldAttributes(model)}>${worldMarkup(model, layout)}</div>${model.board === undefined ? "" : turnBoardMarkup(model.board)}</div>` +
121
122
  `<p class="ky-turn-help" id="${esc(model.id)}-help">${esc(keys)}</p>` +
122
123
  (settings.controls === false ? "" : controlsMarkup(model, view)) +
123
124
  `<div class="ky-turn-live" role="status" aria-live="polite"></div>` +
@@ -2,6 +2,7 @@ import { KYOGIJO_STRINGS } from "./strings.ts";
2
2
  import { type PresetTable, type TurnPreset, type TurnView } from "./turn.camera.ts";
3
3
  import type { Plane } from "./turn.kit.ts";
4
4
  import { type UprightOptions, type Uprights } from "./uprights.ts";
5
+ import { type ResolvedBoard } from "./overlay.ts";
5
6
  import type { VenueSettings } from "./venue.ts";
6
7
  import type { Box3 } from "./turn.camera.ts";
7
8
  import type { Language, ScoreOptions, SurfaceOptions } from "./types.ts";
@@ -99,6 +100,8 @@ export interface TurnModel {
99
100
  lens?: number;
100
101
  /** The flat drawing to show where the 3D view cannot be drawn, when it is not `drawKyogijo`'s of `options` (a tournament layout's). */
101
102
  flat?: string;
103
+ /** The board the options ask for (`board`), laid over the view: a layer of its own, never a plane of the scene. */
104
+ board?: ResolvedBoard;
102
105
  }
103
106
  /** The options with what makes the floor of a 3D view: the whole surface, lying wide, at its full size. */
104
107
  export declare function floorOptions(options: SurfaceOptions | ScoreOptions): SurfaceOptions | ScoreOptions;
@@ -8,6 +8,7 @@ import { KYOGIJO_STRINGS } from "./strings.js";
8
8
  import { TURN_DEFAULT, clampView, presetView } from "./turn.camera.js";
9
9
  import { describeUprights, planesOf, uprightsFor } from "./uprights.js";
10
10
  import { resolveCrowd } from "./crowd.js";
11
+ import { resolveBoard } from "./overlay.js";
11
12
  /** The pixels in one foot of the scene at a zoom of 1: the floor is drawn this big, and scaled down to fit. A larger floor is sharper. */
12
13
  export const WORLD_PX = 1400;
13
14
  /** The options with what makes the floor of a 3D view: the whole surface, lying wide, at its full size. */
@@ -16,6 +17,8 @@ export function floorOptions(options) {
16
17
  delete rest.range;
17
18
  delete rest.half;
18
19
  delete rest.aspect;
20
+ // A board is a layer over the 3D view, not a picture on the floor.
21
+ delete rest.board;
19
22
  return rest;
20
23
  }
21
24
  const VIEWBOX = /viewBox="(-?[\d.]+) (-?[\d.]+) ([\d.]+) ([\d.]+)"/;
@@ -123,6 +126,7 @@ export function buildTurn(options, settings = {}) {
123
126
  centre,
124
127
  view: openingView(settings, o.sport),
125
128
  turnable: !badge,
129
+ board: badge ? undefined : resolveBoard(options.board),
126
130
  };
127
131
  }
128
132
  /** The words of the 3D view in the model's language. */
@@ -1,4 +1,5 @@
1
1
  import { JUDGING_DARK, JUDGING_LIGHT } from "./judging.style.js";
2
+ import { KYOGIJO_TURN_BOARD_STYLE } from "./overlay.js";
2
3
  /**
3
4
  * The 3D view's stylesheet: the colours of what stands on the surface, in light and dark, and the rules that lay the scene out. Like the drawing's own, the
4
5
  * defaults are in `:where()`, which has no weight, so a page's own rule always wins; the colours are custom properties (`--ky-up-*`) a page can set.
@@ -136,4 +137,5 @@ export const KYOGIJO_TURN_STYLE = `
136
137
  .ky-turn-controls button:disabled{opacity:.4;cursor:default}
137
138
  .ky-turn-help,.ky-turn-live{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
138
139
  .ky-turn-flat{margin:8px 0 0;font-size:.85rem}
140
+ ${KYOGIJO_TURN_BOARD_STYLE}
139
141
  `.trim();
package/dist/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Background } from "./background.ts";
2
+ import type { BoardOptions } from "./overlay.ts";
2
3
  import type { EmblemRef, EmblemRenderer } from "./emblems-source.ts";
3
4
  import type { FlagRenderer } from "./flags-source.ts";
4
5
  import type { Layer } from "./layers.types.ts";
@@ -699,6 +700,12 @@ export interface SurfaceOptions {
699
700
  * Unset, the drawing is byte for byte as before. See `BackgroundOptions`.
700
701
  */
701
702
  background?: Background;
703
+ /**
704
+ * A board laid over the drawing: a results board, a bout presentation or a medal table that another package drew (Taisen Brackets', say), taken as an SVG string with its width and height, and placed at the top, a side, a corner or a point
705
+ * of your own, kept off the middle of the surface unless you say otherwise. It is written as an image, never changed and never stretched, and in the 3D view it is a layer over the view that does not turn with it. A compact strip and a
706
+ * badge take none. Unset, the drawing is byte for byte as before. See `BoardOptions` and `docs/boards.md`.
707
+ */
708
+ board?: BoardOptions;
702
709
  /**
703
710
  * The pageant: each team's flag or logo laid large over its half (a `carpet`) or on a round disc (a `stage`), with an emblem in the centre. `"carpet"`, `"stage"`, `true` (a carpet) or `PageantOptions`.
704
711
  * Off by default; a score drawing only. See `PageantOptions`.
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** The package's version: the same as package.json's, and a test holds them to each other. */
2
- export declare const VERSION = "0.23.0";
2
+ export declare const VERSION = "0.24.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** The package's version: the same as package.json's, and a test holds them to each other. */
2
- export const VERSION = "0.23.0";
2
+ export const VERSION = "0.24.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnmorrisdotca/kyogijo-stadiums",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Kyogijo Stadiums 競技場, 40 sports' surfaces as SVG, drawn to scale from the rulebooks: NFL, CFL and IFAF football fields, an NHL rink, an NBA court, MLB ballparks, soccer pitches, curling sheets, athletics stadiums, speed skating, short track and figure skating rinks, swimming pools, water polo fields, diving wells, artistic swimming areas, and the courts and fields of tennis, volleyball (indoor, beach and sitting), badminton, pickleball, table tennis, padel, squash, racquetball, sepak takraw, teqball, roundnet, netball, lacrosse, rugby, Australian football, Gaelic games, cricket, field hockey and handball, and the budo of judo, karate, kendo, sumo and kyudo with a dojo of tatami laid as real units, the MMA cage, with each team's name and logo on its side and the score in the middle, stands and a crowd, a pageant of flags, and a 3D view. Light and dark, accessible, a compact badge for lists, a helper for React and a custom element. Zero dependencies.",
5
5
  "keywords": [
6
6
  "sports",
@@ -279,6 +279,7 @@
279
279
  "arenas": "node scripts/build-arenas.mjs",
280
280
  "arenas:fetch": "node scripts/fetch-arenas.mjs",
281
281
  "arenas:check": "node scripts/check-arenas.mjs",
282
+ "boards": "node scripts/make-boards.mjs",
282
283
  "grounds": "node scripts/build-grounds.mjs",
283
284
  "grounds:check": "node scripts/check-grounds.mjs",
284
285
  "parks:coordinates": "node scripts/fetch-park-coordinates.mjs",
@@ -292,6 +293,7 @@
292
293
  "@johnmorrisdotca/chizu": "1.4.0",
293
294
  "@johnmorrisdotca/dougu-sports": "0.2.0",
294
295
  "@johnmorrisdotca/hata": "1.5.0",
296
+ "@johnmorrisdotca/taisen-brackets": "0.17.1",
295
297
  "@playwright/test": "^1.63.0",
296
298
  "eslint": "^9.0.0",
297
299
  "marked": "^18.1.1",