@johnmorrisdotca/kyogijo-stadiums 0.0.0-stage → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +129 -0
- package/LICENSE +21 -0
- package/NOTICE.md +293 -0
- package/README.md +600 -2
- package/dist/annotate.d.ts +47 -0
- package/dist/annotate.js +349 -0
- package/dist/atlas-entry.d.ts +4 -0
- package/dist/atlas-entry.js +3 -0
- package/dist/atlas.d.ts +131 -0
- package/dist/atlas.js +149 -0
- package/dist/background.d.ts +56 -0
- package/dist/background.js +127 -0
- package/dist/ballpark.d.ts +48 -0
- package/dist/ballpark.js +129 -0
- package/dist/canvas-entry.d.ts +2 -0
- package/dist/canvas-entry.js +2 -0
- package/dist/canvas.d.ts +85 -0
- package/dist/canvas.js +279 -0
- package/dist/charts-entry.d.ts +10 -0
- package/dist/charts-entry.js +8 -0
- package/dist/charts.baseball.d.ts +30 -0
- package/dist/charts.baseball.js +152 -0
- package/dist/charts.curling.d.ts +52 -0
- package/dist/charts.curling.js +196 -0
- package/dist/charts.d.ts +172 -0
- package/dist/charts.js +508 -0
- package/dist/charts.network.d.ts +66 -0
- package/dist/charts.network.js +64 -0
- package/dist/charts.soccer.d.ts +43 -0
- package/dist/charts.soccer.js +191 -0
- package/dist/charts.types.d.ts +396 -0
- package/dist/charts.types.js +1 -0
- package/dist/colour.d.ts +29 -0
- package/dist/colour.js +97 -0
- package/dist/common.d.ts +14 -0
- package/dist/common.js +32 -0
- package/dist/court.d.ts +89 -0
- package/dist/court.js +248 -0
- package/dist/courts.style.d.ts +20 -0
- package/dist/courts.style.js +54 -0
- package/dist/cricket.board.d.ts +18 -0
- package/dist/cricket.board.js +67 -0
- package/dist/curling.board.d.ts +24 -0
- package/dist/curling.board.js +147 -0
- package/dist/d3-entry.d.ts +3 -0
- package/dist/d3-entry.js +2 -0
- package/dist/d3.d.ts +45 -0
- package/dist/d3.js +56 -0
- package/dist/draw.d.ts +52 -0
- package/dist/draw.js +705 -0
- package/dist/element-define.d.ts +2 -0
- package/dist/element-define.js +4 -0
- package/dist/element-entry.d.ts +1 -0
- package/dist/element-entry.js +2 -0
- package/dist/element.d.ts +39 -0
- package/dist/element.js +321 -0
- package/dist/eras-entry.d.ts +2 -0
- package/dist/eras-entry.js +2 -0
- package/dist/eras.d.ts +204 -0
- package/dist/eras.js +396 -0
- package/dist/events-entry.d.ts +3 -0
- package/dist/events-entry.js +3 -0
- package/dist/events.d.ts +54 -0
- package/dist/events.data.d.ts +59 -0
- package/dist/events.data.js +297 -0
- package/dist/events.js +127 -0
- package/dist/features.d.ts +31 -0
- package/dist/features.js +284 -0
- package/dist/field.score.d.ts +5 -0
- package/dist/field.score.js +33 -0
- package/dist/fields/afl.d.ts +11 -0
- package/dist/fields/afl.js +103 -0
- package/dist/fields/cricket.d.ts +9 -0
- package/dist/fields/cricket.js +81 -0
- package/dist/fields/fieldhockey.d.ts +6 -0
- package/dist/fields/fieldhockey.js +81 -0
- package/dist/fields/gaelic.d.ts +10 -0
- package/dist/fields/gaelic.js +86 -0
- package/dist/fields/handball.d.ts +6 -0
- package/dist/fields/handball.js +85 -0
- package/dist/fields/lacrosse.d.ts +17 -0
- package/dist/fields/lacrosse.js +169 -0
- package/dist/fields/rugby.d.ts +10 -0
- package/dist/fields/rugby.js +130 -0
- package/dist/fields.d.ts +12 -0
- package/dist/fields.js +44 -0
- package/dist/fields.kit.d.ts +63 -0
- package/dist/fields.kit.js +108 -0
- package/dist/fields.style.d.ts +4 -0
- package/dist/fields.style.js +19 -0
- package/dist/fields.types.d.ts +155 -0
- package/dist/fields.types.js +1 -0
- package/dist/format.d.ts +10 -0
- package/dist/format.js +28 -0
- package/dist/formations.d.ts +18 -0
- package/dist/formations.js +211 -0
- package/dist/frames-entry.d.ts +8 -0
- package/dist/frames-entry.js +7 -0
- package/dist/frames.d.ts +199 -0
- package/dist/frames.js +271 -0
- package/dist/frames.providers.d.ts +16 -0
- package/dist/frames.providers.js +302 -0
- package/dist/geojson-entry.d.ts +2 -0
- package/dist/geojson-entry.js +2 -0
- package/dist/geojson.d.ts +79 -0
- package/dist/geojson.js +147 -0
- package/dist/geometry-entry.d.ts +7 -0
- package/dist/geometry-entry.js +5 -0
- package/dist/geometry.d.ts +295 -0
- package/dist/geometry.js +361 -0
- package/dist/grounds-entry.d.ts +4 -0
- package/dist/grounds-entry.js +3 -0
- package/dist/grounds.d.ts +134 -0
- package/dist/grounds.data.d.ts +2 -0
- package/dist/grounds.data.js +1849 -0
- package/dist/grounds.js +74 -0
- package/dist/heat.colour.d.ts +37 -0
- package/dist/heat.colour.js +54 -0
- package/dist/heat.d.ts +43 -0
- package/dist/heat.js +614 -0
- package/dist/heat.stats.d.ts +123 -0
- package/dist/heat.stats.js +413 -0
- package/dist/heat.types.d.ts +145 -0
- package/dist/heat.types.js +1 -0
- package/dist/heat.zones.d.ts +54 -0
- package/dist/heat.zones.js +424 -0
- package/dist/homeside.d.ts +16 -0
- package/dist/homeside.js +43 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +29 -0
- package/dist/layers-entry.d.ts +19 -0
- package/dist/layers-entry.js +11 -0
- package/dist/layers.d.ts +41 -0
- package/dist/layers.env.d.ts +72 -0
- package/dist/layers.env.js +79 -0
- package/dist/layers.js +533 -0
- package/dist/layers.marks.d.ts +45 -0
- package/dist/layers.marks.js +168 -0
- package/dist/layers.types.d.ts +226 -0
- package/dist/layers.types.js +1 -0
- package/dist/layout.d.ts +97 -0
- package/dist/layout.js +381 -0
- package/dist/layout.style.d.ts +6 -0
- package/dist/layout.style.js +17 -0
- package/dist/layouts-entry.d.ts +5 -0
- package/dist/layouts-entry.js +4 -0
- package/dist/leagues/aussie.d.ts +57 -0
- package/dist/leagues/aussie.js +36 -0
- package/dist/leagues/baseball.d.ts +58 -0
- package/dist/leagues/baseball.js +171 -0
- package/dist/leagues/basketball.d.ts +67 -0
- package/dist/leagues/basketball.js +128 -0
- package/dist/leagues/courts.d.ts +212 -0
- package/dist/leagues/courts.js +114 -0
- package/dist/leagues/cricket.d.ts +45 -0
- package/dist/leagues/cricket.js +34 -0
- package/dist/leagues/curling.d.ts +64 -0
- package/dist/leagues/curling.js +53 -0
- package/dist/leagues/fieldhockey.d.ts +48 -0
- package/dist/leagues/fieldhockey.js +29 -0
- package/dist/leagues/fields.d.ts +10 -0
- package/dist/leagues/fields.js +6 -0
- package/dist/leagues/football.d.ts +8 -0
- package/dist/leagues/football.js +82 -0
- package/dist/leagues/gaelic.d.ts +50 -0
- package/dist/leagues/gaelic.js +29 -0
- package/dist/leagues/handball.d.ts +45 -0
- package/dist/leagues/handball.js +32 -0
- package/dist/leagues/hockey.d.ts +63 -0
- package/dist/leagues/hockey.js +166 -0
- package/dist/leagues/lacrosse.d.ts +103 -0
- package/dist/leagues/lacrosse.js +175 -0
- package/dist/leagues/rugby.d.ts +59 -0
- package/dist/leagues/rugby.js +72 -0
- package/dist/leagues/sources.d.ts +18 -0
- package/dist/leagues/sources.js +95 -0
- package/dist/leagues/types.d.ts +43 -0
- package/dist/leagues/types.js +5 -0
- package/dist/leagues-entry.d.ts +23 -0
- package/dist/leagues-entry.js +15 -0
- package/dist/leagues.d.ts +172 -0
- package/dist/leagues.js +462 -0
- package/dist/look.d.ts +50 -0
- package/dist/look.js +186 -0
- package/dist/mark.d.ts +35 -0
- package/dist/mark.js +59 -0
- package/dist/mount-entry.d.ts +2 -0
- package/dist/mount-entry.js +2 -0
- package/dist/mount.d.ts +63 -0
- package/dist/mount.js +167 -0
- package/dist/mow.d.ts +36 -0
- package/dist/mow.js +111 -0
- package/dist/names.d.ts +77 -0
- package/dist/names.js +97 -0
- package/dist/outline.d.ts +25 -0
- package/dist/outline.js +112 -0
- package/dist/parks-entry.d.ts +4 -0
- package/dist/parks-entry.js +3 -0
- package/dist/parks.d.ts +200 -0
- package/dist/parks.data.d.ts +2 -0
- package/dist/parks.data.js +4489 -0
- package/dist/parks.js +92 -0
- package/dist/plot-entry.d.ts +2 -0
- package/dist/plot-entry.js +2 -0
- package/dist/plot.d.ts +74 -0
- package/dist/plot.js +68 -0
- package/dist/png-entry.d.ts +2 -0
- package/dist/png-entry.js +2 -0
- package/dist/png.d.ts +26 -0
- package/dist/png.js +46 -0
- package/dist/ranges.d.ts +33 -0
- package/dist/ranges.js +178 -0
- package/dist/ranges.names.d.ts +31 -0
- package/dist/ranges.names.js +65 -0
- package/dist/react-entry.d.ts +2 -0
- package/dist/react-entry.js +2 -0
- package/dist/react.d.ts +53 -0
- package/dist/react.js +58 -0
- package/dist/replay-entry.d.ts +5 -0
- package/dist/replay-entry.js +4 -0
- package/dist/replay.control.d.ts +47 -0
- package/dist/replay.control.js +184 -0
- package/dist/replay.d.ts +170 -0
- package/dist/replay.dom.d.ts +21 -0
- package/dist/replay.dom.js +388 -0
- package/dist/replay.js +579 -0
- package/dist/replay.types.d.ts +152 -0
- package/dist/replay.types.js +1 -0
- package/dist/restyle.d.ts +21 -0
- package/dist/restyle.js +70 -0
- package/dist/road.d.ts +26 -0
- package/dist/road.draw.d.ts +6 -0
- package/dist/road.draw.js +269 -0
- package/dist/road.js +59 -0
- package/dist/road.model.d.ts +63 -0
- package/dist/road.model.js +215 -0
- package/dist/road.style.d.ts +7 -0
- package/dist/road.style.js +26 -0
- package/dist/road.types.d.ts +122 -0
- package/dist/road.types.js +1 -0
- package/dist/scene-entry.d.ts +7 -0
- package/dist/scene-entry.js +5 -0
- package/dist/scene.css.d.ts +44 -0
- package/dist/scene.css.js +296 -0
- package/dist/scene.d.ts +27 -0
- package/dist/scene.fit.d.ts +12 -0
- package/dist/scene.fit.js +18 -0
- package/dist/scene.flat.d.ts +38 -0
- package/dist/scene.flat.js +167 -0
- package/dist/scene.js +420 -0
- package/dist/scene.path.d.ts +40 -0
- package/dist/scene.path.js +357 -0
- package/dist/scene.types.d.ts +172 -0
- package/dist/scene.types.js +1 -0
- package/dist/scene.xml.d.ts +31 -0
- package/dist/scene.xml.js +81 -0
- package/dist/score.d.ts +70 -0
- package/dist/score.js +212 -0
- package/dist/sets.board.d.ts +22 -0
- package/dist/sets.board.js +89 -0
- package/dist/sheet.d.ts +18 -0
- package/dist/sheet.js +38 -0
- package/dist/special.d.ts +44 -0
- package/dist/special.js +204 -0
- package/dist/special.style.d.ts +8 -0
- package/dist/special.style.js +36 -0
- package/dist/strings.d.ts +581 -0
- package/dist/strings.js +411 -0
- package/dist/style.d.ts +35 -0
- package/dist/style.js +177 -0
- package/dist/surfaces/baseball.d.ts +33 -0
- package/dist/surfaces/baseball.js +208 -0
- package/dist/surfaces/baseball.walls.d.ts +16 -0
- package/dist/surfaces/baseball.walls.js +150 -0
- package/dist/surfaces/basketball.d.ts +8 -0
- package/dist/surfaces/basketball.js +199 -0
- package/dist/surfaces/curling.d.ts +23 -0
- package/dist/surfaces/curling.js +128 -0
- package/dist/surfaces/field.d.ts +12 -0
- package/dist/surfaces/field.js +163 -0
- package/dist/surfaces/football.d.ts +12 -0
- package/dist/surfaces/football.js +208 -0
- package/dist/surfaces/hockey.d.ts +10 -0
- package/dist/surfaces/hockey.js +166 -0
- package/dist/surfaces/net.d.ts +7 -0
- package/dist/surfaces/net.js +151 -0
- package/dist/surfaces/soccer-providers.d.ts +124 -0
- package/dist/surfaces/soccer-providers.js +334 -0
- package/dist/surfaces/soccer-spec.d.ts +138 -0
- package/dist/surfaces/soccer-spec.js +175 -0
- package/dist/surfaces/soccer.d.ts +25 -0
- package/dist/surfaces/soccer.js +270 -0
- package/dist/surfaces/surface.d.ts +180 -0
- package/dist/surfaces/surface.js +49 -0
- package/dist/team.common.d.ts +75 -0
- package/dist/team.common.js +97 -0
- package/dist/team.formation.d.ts +24 -0
- package/dist/team.formation.js +121 -0
- package/dist/team.geometry.d.ts +66 -0
- package/dist/team.geometry.js +304 -0
- package/dist/team.layers.d.ts +16 -0
- package/dist/team.layers.js +32 -0
- package/dist/team.motion.d.ts +9 -0
- package/dist/team.motion.js +325 -0
- package/dist/team.network.d.ts +43 -0
- package/dist/team.network.js +215 -0
- package/dist/team.shape.d.ts +12 -0
- package/dist/team.shape.js +217 -0
- package/dist/team.types.d.ts +189 -0
- package/dist/team.types.js +1 -0
- package/dist/text.d.ts +31 -0
- package/dist/text.js +88 -0
- package/dist/themes.d.ts +14 -0
- package/dist/themes.js +24 -0
- package/dist/types.d.ts +565 -0
- package/dist/types.js +1 -0
- package/dist/units.d.ts +45 -0
- package/dist/units.js +90 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/package.json +234 -4
package/dist/colour.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const HEX = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
|
|
2
|
+
const FUNCTION = /^(?:rgb|hsl)a?\(\s*[-+0-9.%\s,/]+(?:deg|turn|rad)?[-+0-9.%\s,/]*\)$/i;
|
|
3
|
+
const VARIABLE = /^var\(\s*--[a-z0-9_-]+\s*\)$/i;
|
|
4
|
+
const NAME = /^[a-z]{3,24}$/i;
|
|
5
|
+
/**
|
|
6
|
+
* The colour as it may be written into a style attribute, or `undefined`. A hex colour, `rgb()`, `rgba()`, `hsl()`,
|
|
7
|
+
* `hsla()`, a colour name or `var(--name)` is drawn; anything else is ignored, so nothing from a form or an address
|
|
8
|
+
* can end the attribute or add a rule.
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { safeColour } from "@johnmorrisdotca/kyogijo-stadiums";
|
|
12
|
+
* console.log(safeColour("#1f5fa8"), safeColour("red;fill:url(x)"));
|
|
13
|
+
* // #1f5fa8 undefined
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export function safeColour(value) {
|
|
17
|
+
if (typeof value !== "string")
|
|
18
|
+
return undefined;
|
|
19
|
+
const text = value.trim();
|
|
20
|
+
if (text.length === 0 || text.length > 64)
|
|
21
|
+
return undefined;
|
|
22
|
+
if (HEX.test(text) || FUNCTION.test(text) || VARIABLE.test(text) || NAME.test(text))
|
|
23
|
+
return text;
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
const NAMED = {
|
|
27
|
+
white: [255, 255, 255], black: [0, 0, 0], red: [255, 0, 0], green: [0, 128, 0], blue: [0, 0, 255], yellow: [255, 255, 0], orange: [255, 165, 0],
|
|
28
|
+
gold: [255, 215, 0], navy: [0, 0, 128], maroon: [128, 0, 0], purple: [128, 0, 128], teal: [0, 128, 128], gray: [128, 128, 128], grey: [128, 128, 128],
|
|
29
|
+
silver: [192, 192, 192], crimson: [220, 20, 60], royalblue: [65, 105, 225], darkgreen: [0, 100, 0], lightblue: [173, 216, 230], pink: [255, 192, 203],
|
|
30
|
+
};
|
|
31
|
+
/** The red, green and blue of a colour, when it can be read without a browser (a hex colour, `rgb()` or a common name). */
|
|
32
|
+
export function rgbOf(value) {
|
|
33
|
+
if (value === undefined)
|
|
34
|
+
return undefined;
|
|
35
|
+
const text = value.trim().toLowerCase();
|
|
36
|
+
const named = NAMED[text];
|
|
37
|
+
if (named !== undefined)
|
|
38
|
+
return named;
|
|
39
|
+
const hex = /^#([0-9a-f]{3,8})$/.exec(text)?.[1];
|
|
40
|
+
if (hex !== undefined) {
|
|
41
|
+
const full = hex.length <= 4 ? [...hex].map((c) => c + c).join("") : hex;
|
|
42
|
+
if (full.length < 6)
|
|
43
|
+
return undefined;
|
|
44
|
+
return [parseInt(full.slice(0, 2), 16), parseInt(full.slice(2, 4), 16), parseInt(full.slice(4, 6), 16)];
|
|
45
|
+
}
|
|
46
|
+
const rgb = /^rgba?\(\s*(\d+(?:\.\d+)?)[\s,]+(\d+(?:\.\d+)?)[\s,]+(\d+(?:\.\d+)?)/.exec(text);
|
|
47
|
+
if (rgb !== null)
|
|
48
|
+
return [Number(rgb[1]), Number(rgb[2]), Number(rgb[3])];
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
/** The relative luminance of a colour, 0 (black) to 1 (white). */
|
|
52
|
+
export function luminance(rgb) {
|
|
53
|
+
const [r, g, b] = rgb.map((channel) => {
|
|
54
|
+
const c = channel / 255;
|
|
55
|
+
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
56
|
+
});
|
|
57
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
58
|
+
}
|
|
59
|
+
/** White or near-black, whichever reads better on the colour. White when the colour cannot be read. */
|
|
60
|
+
export function inkOn(color) {
|
|
61
|
+
const rgb = rgbOf(color);
|
|
62
|
+
if (rgb === undefined)
|
|
63
|
+
return "#ffffff";
|
|
64
|
+
return luminance(rgb) > 0.4 ? "#161a18" : "#ffffff";
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* A logo's address as it may be written into an image's `href`, or `undefined`. An `https:` or `http:` address, a
|
|
68
|
+
* relative path, an image `data:` address, or an SVG document as text (written as an image, which cannot run scripts).
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* import { safeLogo } from "@johnmorrisdotca/kyogijo-stadiums";
|
|
72
|
+
* console.log(safeLogo("https://example.com/gulls.svg"), safeLogo("javascript:alert(1)"));
|
|
73
|
+
* // https://example.com/gulls.svg undefined
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export function safeLogo(value) {
|
|
77
|
+
if (typeof value !== "string")
|
|
78
|
+
return undefined;
|
|
79
|
+
const text = value.trim();
|
|
80
|
+
if (text.length === 0)
|
|
81
|
+
return undefined;
|
|
82
|
+
if (/^(?:<\?xml|<svg)\b/i.test(text)) {
|
|
83
|
+
if (text.length > 200000)
|
|
84
|
+
return undefined;
|
|
85
|
+
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(text)}`;
|
|
86
|
+
}
|
|
87
|
+
if (text.length > 300000)
|
|
88
|
+
return undefined;
|
|
89
|
+
if (/^data:image\/(?:png|jpe?g|gif|webp|avif|svg\+xml)[;,]/i.test(text))
|
|
90
|
+
return text;
|
|
91
|
+
if (/^https?:\/\/[^\s"'<>]+$/i.test(text))
|
|
92
|
+
return text;
|
|
93
|
+
// A relative path has no scheme: no colon before the first slash.
|
|
94
|
+
if (/^[^\s"'<>:]*(?:\/[^\s"'<>]*)?$/.test(text) && !/^[a-z][a-z0-9+.-]*:/i.test(text) && !text.startsWith("//"))
|
|
95
|
+
return text;
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
package/dist/common.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SurfaceOptions } from "./types.ts";
|
|
2
|
+
import { type Unit } from "./units.ts";
|
|
3
|
+
/** Text made safe for a class name or an id: letters, digits, underscores, spaces and hyphens. */
|
|
4
|
+
export declare const safeClass: (text: string | undefined) => string;
|
|
5
|
+
/** The prefix of the ids inside a drawing: the `id` option, or a hash of the options, so that the same call gives the same drawing. */
|
|
6
|
+
export declare const drawingId: (o: SurfaceOptions) => string;
|
|
7
|
+
/** True when a drawing with these options lies in portrait. */
|
|
8
|
+
export declare function isPortrait(o: Pick<SurfaceOptions, "orientation" | "aspect"> & {
|
|
9
|
+
sport?: SurfaceOptions["sport"];
|
|
10
|
+
}): boolean;
|
|
11
|
+
/** The unit the options' lengths are in: `units`, or feet when it is missing. A unit that is not one is an error, not feet. */
|
|
12
|
+
export declare function lengthUnit(o: Pick<SurfaceOptions, "units">): Unit;
|
|
13
|
+
/** The fence the options give, with each distance in feet. */
|
|
14
|
+
export declare function feetFence(fence: SurfaceOptions["fence"], unit: Unit): SurfaceOptions["fence"];
|
package/dist/common.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// What the drawing and the data layers both need: the drawing's id, whether it lies tall, and the unit its lengths are in.
|
|
2
|
+
// Kept apart from draw.ts so that layers.ts need not import the file that imports it.
|
|
3
|
+
import { hash } from "./format.js";
|
|
4
|
+
import { convertLength, isUnit } from "./units.js";
|
|
5
|
+
import { VERSION } from "./version.js";
|
|
6
|
+
/** Text made safe for a class name or an id: letters, digits, underscores, spaces and hyphens. */
|
|
7
|
+
export const safeClass = (text) => (text ?? "").replace(/[^\w -]/g, "").trim();
|
|
8
|
+
/** The prefix of the ids inside a drawing: the `id` option, or a hash of the options, so that the same call gives the same drawing. */
|
|
9
|
+
export const drawingId = (o) => safeClass(o.id).replace(/ /g, "-") || `ky-${hash(JSON.stringify([o, VERSION]))}`;
|
|
10
|
+
/** True when a drawing with these options lies in portrait. */
|
|
11
|
+
export function isPortrait(o) {
|
|
12
|
+
if (o.orientation === "portrait")
|
|
13
|
+
return true;
|
|
14
|
+
// A curling sheet is 150 feet long and 15 wide: it lies tall unless it is asked to lie wide.
|
|
15
|
+
if (o.orientation === undefined && o.sport === "curling")
|
|
16
|
+
return true;
|
|
17
|
+
return o.orientation === "auto" && typeof o.aspect === "number" && Number.isFinite(o.aspect) && o.aspect > 0 && o.aspect < 1;
|
|
18
|
+
}
|
|
19
|
+
/** The unit the options' lengths are in: `units`, or feet when it is missing. A unit that is not one is an error, not feet. */
|
|
20
|
+
export function lengthUnit(o) {
|
|
21
|
+
if (o.units === undefined)
|
|
22
|
+
return "ft";
|
|
23
|
+
if (!isUnit(o.units))
|
|
24
|
+
throw new Error(`Unknown units "${String(o.units)}": use one of ft, in, yd, m, cm, mm`);
|
|
25
|
+
return o.units;
|
|
26
|
+
}
|
|
27
|
+
/** The fence the options give, with each distance in feet. */
|
|
28
|
+
export function feetFence(fence, unit) {
|
|
29
|
+
if (fence === undefined || unit === "ft")
|
|
30
|
+
return fence;
|
|
31
|
+
return Object.fromEntries(Object.entries(fence).map(([key, value]) => [key, typeof value === "number" ? convertLength(value, unit, "ft") : value]));
|
|
32
|
+
}
|
package/dist/court.d.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { type BadmintonFigures, type BadmintonLeague, type FreeZone, type NetSport, type PickleballFigures, type PickleballLeague, type TableTennisFigures, type TableTennisLeague, type TennisFigures, type TennisLeague, type VolleyballFigures, type VolleyballLeague } from "./leagues/courts.ts";
|
|
2
|
+
import type { SurfaceOptions } from "./types.ts";
|
|
3
|
+
/** A rectangle as `[x0, y0, x1, y1]`, in feet. */
|
|
4
|
+
export type Rect = readonly [x0: number, y0: number, x1: number, y1: number];
|
|
5
|
+
/** A post, an antenna or a stick: a mark of a size, centred on a place. */
|
|
6
|
+
export interface Fixture {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
/** The width of a square mark, or the diameter of a round one. */
|
|
10
|
+
size: number;
|
|
11
|
+
shape: "square" | "round";
|
|
12
|
+
}
|
|
13
|
+
/** The net's plane in the drawing: the line x = `x`, from `y0` to `y1`. */
|
|
14
|
+
export interface NetLine {
|
|
15
|
+
x: number;
|
|
16
|
+
y0: number;
|
|
17
|
+
y1: number;
|
|
18
|
+
}
|
|
19
|
+
/** The looks each sport has, the sport's default first. */
|
|
20
|
+
export declare const LOOKS: Readonly<Record<NetSport, readonly string[]>>;
|
|
21
|
+
/** True for a sport whose court is one of these. */
|
|
22
|
+
export declare const isNetSport: (sport: string) => sport is NetSport;
|
|
23
|
+
interface Common {
|
|
24
|
+
/** The look: a name from `LOOKS`. */
|
|
25
|
+
look: string;
|
|
26
|
+
/** The free zone drawn round the court, in feet, or `null`. */
|
|
27
|
+
zone: FreeZone;
|
|
28
|
+
/** How the free zone was asked for. */
|
|
29
|
+
free: "official" | "least" | "none";
|
|
30
|
+
/** Tennis: the singles sticks are drawn. */
|
|
31
|
+
sticks: boolean;
|
|
32
|
+
}
|
|
33
|
+
/** A court to draw: the sport, its league and figures (in feet), and how it is shown. */
|
|
34
|
+
export type CourtPlan = (Common & {
|
|
35
|
+
sport: "tennis";
|
|
36
|
+
league: TennisLeague;
|
|
37
|
+
figures: TennisFigures;
|
|
38
|
+
}) | (Common & {
|
|
39
|
+
sport: "volleyball";
|
|
40
|
+
league: VolleyballLeague;
|
|
41
|
+
figures: VolleyballFigures;
|
|
42
|
+
}) | (Common & {
|
|
43
|
+
sport: "badminton";
|
|
44
|
+
league: BadmintonLeague;
|
|
45
|
+
figures: BadmintonFigures;
|
|
46
|
+
}) | (Common & {
|
|
47
|
+
sport: "pickleball";
|
|
48
|
+
league: PickleballLeague;
|
|
49
|
+
figures: PickleballFigures;
|
|
50
|
+
}) | (Common & {
|
|
51
|
+
sport: "tabletennis";
|
|
52
|
+
league: TableTennisLeague;
|
|
53
|
+
figures: TableTennisFigures;
|
|
54
|
+
});
|
|
55
|
+
/**
|
|
56
|
+
* The court the options ask for. A `league` picks the rulebook (the sport's own by default; `fivb-beach` for beach volleyball); `net.look` a named look, `net.free`
|
|
57
|
+
* the free zone, `net.sticks` the tennis singles sticks.
|
|
58
|
+
* @throws Error when the look or the free zone is not one the sport has.
|
|
59
|
+
*/
|
|
60
|
+
export declare function courtFor(o: Pick<SurfaceOptions, "sport" | "league" | "net">): CourtPlan;
|
|
61
|
+
/** The length and the width of the court in feet: a tennis court is the doubles court. */
|
|
62
|
+
export declare function courtSize(plan: CourtPlan): {
|
|
63
|
+
length: number;
|
|
64
|
+
width: number;
|
|
65
|
+
};
|
|
66
|
+
/** The court's lines, painted areas and fixtures, in feet. */
|
|
67
|
+
export interface CourtGeometry {
|
|
68
|
+
length: number;
|
|
69
|
+
width: number;
|
|
70
|
+
/** The lines, as the rectangles they cover. */
|
|
71
|
+
lines: Rect[];
|
|
72
|
+
/** Painted areas: the non-volley zones, the volleyball front zones. */
|
|
73
|
+
areas: Rect[];
|
|
74
|
+
/** Short lines outside the court (the attack line's extension, the service zone's limits), a part of `lines` that is drawn only in the full detail. */
|
|
75
|
+
small: Rect[];
|
|
76
|
+
net: NetLine;
|
|
77
|
+
posts: Fixture[];
|
|
78
|
+
antennae: Fixture[];
|
|
79
|
+
sticks: Fixture[];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Where everything stands, from the figures of the league in feet. A line's rectangle covers the line's width: the court's dimensions are to the outside edge of the
|
|
83
|
+
* lines (tennis Rule 1, pickleball 3.A.2, badminton Law 1.1 and Diagram A, volleyball 1.3.2 "drawn inside the dimensions"), so an end line covers
|
|
84
|
+
* x from 0 to its width and an interior line is placed by the edge the rule measures to.
|
|
85
|
+
*/
|
|
86
|
+
export declare function courtGeometry(plan: CourtPlan): CourtGeometry;
|
|
87
|
+
/** How far the court's fixtures reach beyond its side lines: the net's posts and the antennae's marks. */
|
|
88
|
+
export declare function sideReach(geometry: CourtGeometry): number;
|
|
89
|
+
export {};
|
package/dist/court.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// What a net sport's court is for a set of options: the league's figures in feet, the look, the free zone, and the lines, areas and fixtures that follow from
|
|
2
|
+
// them, as plain rectangles in the frame of a drawing lying wide (x along the court from the left end line, y across it from the top side line, both in feet).
|
|
3
|
+
// Kept apart from the drawing so that the outline, the description, the tests and the drawing agree. Every length that is a rulebook's figure is cited in
|
|
4
|
+
// docs/sources.md; the sizes of the symbols (a post, an antenna, the net's line) are ours and are said to be.
|
|
5
|
+
import { chooseLeague, netOf } from "./leagues.js";
|
|
6
|
+
import { BADMINTON_LEAGUES, PICKLEBALL_LEAGUES, TABLE_TENNIS_LEAGUES, TENNIS_LEAGUES, VOLLEYBALL_LEAGUES } from "./leagues/courts.js";
|
|
7
|
+
import { convertLength } from "./units.js";
|
|
8
|
+
/** The looks each sport has, the sport's default first. */
|
|
9
|
+
export const LOOKS = Object.freeze({
|
|
10
|
+
tennis: ["hard-blue", "hard-green", "clay", "grass"],
|
|
11
|
+
volleyball: ["wood", "blue", "sand"],
|
|
12
|
+
badminton: ["green", "blue", "wood"],
|
|
13
|
+
pickleball: ["blue-green", "green", "red"],
|
|
14
|
+
tabletennis: ["blue", "green"],
|
|
15
|
+
});
|
|
16
|
+
/** True for a sport whose court is one of these. */
|
|
17
|
+
export const isNetSport = (sport) => sport === "tennis" || sport === "volleyball" || sport === "badminton" || sport === "pickleball" || sport === "tabletennis";
|
|
18
|
+
const FREE = ["official", "least", "none"];
|
|
19
|
+
/**
|
|
20
|
+
* The court the options ask for. A `league` picks the rulebook (the sport's own by default; `fivb-beach` for beach volleyball); `net.look` a named look, `net.free`
|
|
21
|
+
* the free zone, `net.sticks` the tennis singles sticks.
|
|
22
|
+
* @throws Error when the look or the free zone is not one the sport has.
|
|
23
|
+
*/
|
|
24
|
+
export function courtFor(o) {
|
|
25
|
+
const sport = o.sport;
|
|
26
|
+
if (!isNetSport(sport))
|
|
27
|
+
throw new Error(`"${String(sport)}" is not a net sport`);
|
|
28
|
+
const picked = chooseLeague(o).league;
|
|
29
|
+
const asked = o.net ?? {};
|
|
30
|
+
const free = asked.free === undefined ? "official" : FREE.includes(asked.free) ? asked.free : undefined;
|
|
31
|
+
if (free === undefined)
|
|
32
|
+
throw new Error(`Unknown free zone "${String(asked.free)}": use one of ${FREE.join(", ")}`);
|
|
33
|
+
const beach = picked?.family === "volleyball" && picked.id === "fivb-beach";
|
|
34
|
+
const names = LOOKS[sport];
|
|
35
|
+
const look = asked.look === undefined ? (beach ? "sand" : names[0]) : asked.look;
|
|
36
|
+
if (!names.includes(look))
|
|
37
|
+
throw new Error(`Unknown look "${String(look)}" for ${sport}: use one of ${names.join(", ")}`);
|
|
38
|
+
const common = { look, free, sticks: sport === "tennis" && asked.sticks === true };
|
|
39
|
+
const zoneOf = (zones) => (free === "none" ? null : zones[free]);
|
|
40
|
+
switch (sport) {
|
|
41
|
+
case "tennis": {
|
|
42
|
+
const league = picked?.family === "tennis" ? picked : TENNIS_LEAGUES[0];
|
|
43
|
+
const figures = netOf(league);
|
|
44
|
+
return { ...common, sport, league, figures, zone: zoneOf(figures.free) };
|
|
45
|
+
}
|
|
46
|
+
case "volleyball": {
|
|
47
|
+
const league = picked?.family === "volleyball" ? picked : VOLLEYBALL_LEAGUES[0];
|
|
48
|
+
const figures = netOf(league);
|
|
49
|
+
return { ...common, sport, league, figures, zone: zoneOf(figures.free) };
|
|
50
|
+
}
|
|
51
|
+
case "badminton": {
|
|
52
|
+
const league = picked?.family === "badminton" ? picked : BADMINTON_LEAGUES[0];
|
|
53
|
+
const figures = netOf(league);
|
|
54
|
+
return { ...common, sport, league, figures, zone: zoneOf(figures.free) };
|
|
55
|
+
}
|
|
56
|
+
case "pickleball": {
|
|
57
|
+
const league = picked?.family === "pickleball" ? picked : PICKLEBALL_LEAGUES[0];
|
|
58
|
+
const figures = netOf(league);
|
|
59
|
+
return { ...common, sport, league, figures, zone: zoneOf(figures.free) };
|
|
60
|
+
}
|
|
61
|
+
default: {
|
|
62
|
+
const league = picked?.family === "tabletennis" ? picked : TABLE_TENNIS_LEAGUES[0];
|
|
63
|
+
const figures = netOf(league);
|
|
64
|
+
return { ...common, sport: "tabletennis", league, figures, zone: zoneOf(figures.free) };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** The length and the width of the court in feet: a tennis court is the doubles court. */
|
|
69
|
+
export function courtSize(plan) {
|
|
70
|
+
const f = plan.figures;
|
|
71
|
+
return { length: f.length, width: plan.sport === "tennis" ? f.widthDoubles : f.width };
|
|
72
|
+
}
|
|
73
|
+
const feet = (metres) => convertLength(metres, "m", "ft");
|
|
74
|
+
/**
|
|
75
|
+
* Where everything stands, from the figures of the league in feet. A line's rectangle covers the line's width: the court's dimensions are to the outside edge of the
|
|
76
|
+
* lines (tennis Rule 1, pickleball 3.A.2, badminton Law 1.1 and Diagram A, volleyball 1.3.2 "drawn inside the dimensions"), so an end line covers
|
|
77
|
+
* x from 0 to its width and an interior line is placed by the edge the rule measures to.
|
|
78
|
+
*/
|
|
79
|
+
export function courtGeometry(plan) {
|
|
80
|
+
switch (plan.sport) {
|
|
81
|
+
case "tennis":
|
|
82
|
+
return tennisGeometry(plan.figures, plan.sticks);
|
|
83
|
+
case "volleyball":
|
|
84
|
+
return volleyballGeometry(plan.figures, plan.free);
|
|
85
|
+
case "badminton":
|
|
86
|
+
return badmintonGeometry(plan.figures);
|
|
87
|
+
case "pickleball":
|
|
88
|
+
return pickleballGeometry(plan.figures);
|
|
89
|
+
default:
|
|
90
|
+
return tableGeometry(plan.figures);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function tennisGeometry(f, sticks) {
|
|
94
|
+
const { length: L, widthDoubles: W, widthSingles, serviceLine: S, lineWidth: w } = f;
|
|
95
|
+
const cx = L / 2;
|
|
96
|
+
const cy = W / 2;
|
|
97
|
+
const inset = (W - widthSingles) / 2;
|
|
98
|
+
const cw = f.centreServiceWidth;
|
|
99
|
+
const mw = f.centreMark.width;
|
|
100
|
+
const ml = f.centreMark.length;
|
|
101
|
+
const lines = [
|
|
102
|
+
// Baselines, doubles sidelines and singles sidelines.
|
|
103
|
+
[0, 0, w, W], [L - w, 0, L, W],
|
|
104
|
+
[0, 0, L, w], [0, W - w, L, W],
|
|
105
|
+
[0, inset, L, inset + w], [0, W - inset - w, L, W - inset],
|
|
106
|
+
// The service lines, between the singles sidelines, each 21 feet from the net to its outside edge, and the centre service line between them.
|
|
107
|
+
[cx - S, inset, cx - S + w, W - inset], [cx + S - w, inset, cx + S, W - inset],
|
|
108
|
+
[cx - S, cy - cw / 2, cx + S, cy + cw / 2],
|
|
109
|
+
// The centre marks, inside the court from each baseline.
|
|
110
|
+
[w, cy - mw / 2, w + ml, cy + mw / 2], [L - w - ml, cy - mw / 2, L - w, cy + mw / 2],
|
|
111
|
+
];
|
|
112
|
+
const out = f.net.postsOutside;
|
|
113
|
+
return {
|
|
114
|
+
length: L,
|
|
115
|
+
width: W,
|
|
116
|
+
lines,
|
|
117
|
+
areas: [],
|
|
118
|
+
small: [],
|
|
119
|
+
net: { x: cx, y0: -out, y1: W + out },
|
|
120
|
+
posts: [{ x: cx, y: -out, size: f.net.postSize, shape: "square" }, { x: cx, y: W + out, size: f.net.postSize, shape: "square" }],
|
|
121
|
+
antennae: [],
|
|
122
|
+
sticks: sticks ? [{ x: cx, y: inset - f.net.sticksOutside, size: f.net.stickSize, shape: "square" }, { x: cx, y: W - inset + f.net.sticksOutside, size: f.net.stickSize, shape: "square" }] : [],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function volleyballGeometry(f, free) {
|
|
126
|
+
const { length: L, width: W, lineWidth: w } = f;
|
|
127
|
+
const cx = L / 2;
|
|
128
|
+
const lines = [[0, 0, w, W], [L - w, 0, L, W], [0, 0, L, w], [0, W - w, L, W]];
|
|
129
|
+
const areas = [];
|
|
130
|
+
const small = [];
|
|
131
|
+
if (f.attackLine !== null) {
|
|
132
|
+
const A = f.attackLine;
|
|
133
|
+
// The axis of the centre line is the middle of the court; each attack line's rear edge is 3 m from it.
|
|
134
|
+
lines.push([cx - w / 2, 0, cx + w / 2, W], [cx - A, 0, cx - A + w, W], [cx + A - w, 0, cx + A, W]);
|
|
135
|
+
areas.push([cx - A, 0, cx, W], [cx, 0, cx + A, W]);
|
|
136
|
+
const ext = f.attackExtension;
|
|
137
|
+
// The FIVB's competitions extend each attack line beyond the side lines by five short lines, the first 20 cm from the side line.
|
|
138
|
+
if (ext !== null && free !== "least") {
|
|
139
|
+
for (const [x0, x1] of [[cx - A, cx - A + w], [cx + A - w, cx + A]]) {
|
|
140
|
+
for (let i = 0; i < Math.round(ext.total / (ext.length + ext.gap)); i += 1) {
|
|
141
|
+
const near = ext.gap + i * (ext.length + ext.gap);
|
|
142
|
+
small.push([x0, -(near + ext.length), x1, -near], [x0, W + near, x1, W + near + ext.length]);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const marks = f.serviceMarks;
|
|
148
|
+
// The service zone is limited by two short lines in the extensions of the side lines, 20 cm behind the end line.
|
|
149
|
+
if (marks !== null) {
|
|
150
|
+
for (const [a, b] of [[-(marks.behind + marks.length), -marks.behind], [L + marks.behind, L + marks.behind + marks.length]])
|
|
151
|
+
small.push([a, 0, b, w], [a, W - w, b, W]);
|
|
152
|
+
}
|
|
153
|
+
const out = free === "least" ? f.net.postsOutsideLeast : f.net.postsOutside;
|
|
154
|
+
const half = (W - f.net.length) / 2;
|
|
155
|
+
const post = Math.max(feet(0.1), L * 0.01);
|
|
156
|
+
const antenna = Math.max(f.net.antenna.diameter, L * 0.012);
|
|
157
|
+
return {
|
|
158
|
+
length: L,
|
|
159
|
+
width: W,
|
|
160
|
+
lines,
|
|
161
|
+
areas,
|
|
162
|
+
small,
|
|
163
|
+
net: { x: cx, y0: half, y1: W - half },
|
|
164
|
+
posts: [{ x: cx, y: -out, size: post, shape: "round" }, { x: cx, y: W + out, size: post, shape: "round" }],
|
|
165
|
+
// An antenna is fixed at the outer edge of a side band, which stands directly over the side line: at the outer edge of the side line.
|
|
166
|
+
antennae: [{ x: cx, y: 0, size: antenna, shape: "round" }, { x: cx, y: W, size: antenna, shape: "round" }],
|
|
167
|
+
sticks: [],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function badmintonGeometry(f) {
|
|
171
|
+
const { length: L, width: W, lineWidth: w, singlesInset: inset, doublesLongServiceInset: ls, shortServiceLine: ss } = f;
|
|
172
|
+
const cx = L / 2;
|
|
173
|
+
const lines = [
|
|
174
|
+
[0, 0, w, W], [L - w, 0, L, W],
|
|
175
|
+
[0, 0, L, w], [0, W - w, L, W],
|
|
176
|
+
// The singles side lines: their outside edges 460 mm inside the doubles side lines' (Diagram A: 40 + 420).
|
|
177
|
+
[0, inset, L, inset + w], [0, W - inset - w, L, W - inset],
|
|
178
|
+
// The doubles long service lines: their outside edges 760 mm inside the back boundary lines'.
|
|
179
|
+
[ls, 0, ls + w, W], [L - ls - w, 0, L - ls, W],
|
|
180
|
+
// The short service lines: their edges nearer the net 1.98 m from it.
|
|
181
|
+
[cx - ss - w, 0, cx - ss, W], [cx + ss, 0, cx + ss + w, W],
|
|
182
|
+
// The centre lines, from each short service line to the back boundary.
|
|
183
|
+
[0, W / 2 - w / 2, cx - ss - w, W / 2 + w / 2], [cx + ss + w, W / 2 - w / 2, L, W / 2 + w / 2],
|
|
184
|
+
];
|
|
185
|
+
const post = L * 0.005;
|
|
186
|
+
return {
|
|
187
|
+
length: L,
|
|
188
|
+
width: W,
|
|
189
|
+
lines,
|
|
190
|
+
areas: [],
|
|
191
|
+
small: [],
|
|
192
|
+
net: { x: cx, y0: 0, y1: W },
|
|
193
|
+
// The posts stand on the doubles side lines and do not reach into the court beyond them (Law 1.5).
|
|
194
|
+
posts: [{ x: cx, y: w - post / 2, size: post, shape: "round" }, { x: cx, y: W - w + post / 2, size: post, shape: "round" }],
|
|
195
|
+
antennae: [],
|
|
196
|
+
sticks: [],
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function pickleballGeometry(f) {
|
|
200
|
+
const { length: L, width: W, lineWidth: w, nonVolley: K } = f;
|
|
201
|
+
const cx = L / 2;
|
|
202
|
+
const lines = [
|
|
203
|
+
[0, 0, w, W], [L - w, 0, L, W],
|
|
204
|
+
[0, 0, L, w], [0, W - w, L, W],
|
|
205
|
+
// The non-volley zone lines, each 7 feet from the net to its outside edge, and the centrelines from them to the baselines.
|
|
206
|
+
[cx - K, 0, cx - K + w, W], [cx + K - w, 0, cx + K, W],
|
|
207
|
+
[0, W / 2 - w / 2, cx - K, W / 2 + w / 2], [cx + K, W / 2 - w / 2, L, W / 2 + w / 2],
|
|
208
|
+
];
|
|
209
|
+
// The posts are 22 feet apart, inside to inside, so the inside of each stands 1 foot outside a sideline; each is at most 3 inches across.
|
|
210
|
+
const gap = (f.net.postsInside - W) / 2;
|
|
211
|
+
const d = f.net.postDiameter;
|
|
212
|
+
return {
|
|
213
|
+
length: L,
|
|
214
|
+
width: W,
|
|
215
|
+
lines,
|
|
216
|
+
areas: [[cx - K, 0, cx, W], [cx, 0, cx + K, W]],
|
|
217
|
+
small: [],
|
|
218
|
+
net: { x: cx, y0: -gap, y1: W + gap },
|
|
219
|
+
posts: [{ x: cx, y: -gap - d / 2, size: d, shape: "round" }, { x: cx, y: W + gap + d / 2, size: d, shape: "round" }],
|
|
220
|
+
antennae: [],
|
|
221
|
+
sticks: [],
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function tableGeometry(f) {
|
|
225
|
+
const { length: L, width: W, edgeLine: e, centreLine: c } = f;
|
|
226
|
+
const cx = L / 2;
|
|
227
|
+
const out = f.net.postOutside;
|
|
228
|
+
const post = Math.max(feet(0.03), L * 0.01);
|
|
229
|
+
return {
|
|
230
|
+
length: L,
|
|
231
|
+
width: W,
|
|
232
|
+
lines: [[0, 0, L, e], [0, W - e, L, W], [0, 0, e, W], [L - e, 0, L, W], [0, W / 2 - c / 2, L, W / 2 + c / 2]],
|
|
233
|
+
areas: [],
|
|
234
|
+
small: [],
|
|
235
|
+
// The post's outside limit is 15.25 cm outside the side line; the net runs between the posts.
|
|
236
|
+
net: { x: cx, y0: -out + post, y1: W + out - post },
|
|
237
|
+
posts: [{ x: cx, y: -out + post / 2, size: post, shape: "square" }, { x: cx, y: W + out - post / 2, size: post, shape: "square" }],
|
|
238
|
+
antennae: [],
|
|
239
|
+
sticks: [],
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/** How far the court's fixtures reach beyond its side lines: the net's posts and the antennae's marks. */
|
|
243
|
+
export function sideReach(geometry) {
|
|
244
|
+
let reach = 0;
|
|
245
|
+
for (const post of [...geometry.posts, ...geometry.antennae, ...geometry.sticks])
|
|
246
|
+
reach = Math.max(reach, -(post.y - post.size / 2), post.y + post.size / 2 - geometry.width);
|
|
247
|
+
return reach;
|
|
248
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { NetSport } from "./leagues/courts.ts";
|
|
2
|
+
/** The colours of a look: the court, the free zone round it, the ground round that, a painted area on the court, and the lines. A look may leave the area and the lines to the theme. */
|
|
3
|
+
interface Look {
|
|
4
|
+
surface: string;
|
|
5
|
+
free: string;
|
|
6
|
+
apron: string;
|
|
7
|
+
area?: string;
|
|
8
|
+
line?: string;
|
|
9
|
+
}
|
|
10
|
+
interface Pair {
|
|
11
|
+
light: Look;
|
|
12
|
+
dark: Look;
|
|
13
|
+
}
|
|
14
|
+
/** The looks of every sport, each in light and dark. */
|
|
15
|
+
export declare const COURT_LOOKS: Readonly<Record<NetSport, Readonly<Record<string, Pair>>>>;
|
|
16
|
+
/** The rules of a drawing of this sport in this look: the shared rules and the look's colours. */
|
|
17
|
+
export declare function courtStyle(sport: NetSport, look: string): string;
|
|
18
|
+
/** Every net sport's rules and every look, for a page that draws with `style: "none"` and supplies the stylesheet itself. */
|
|
19
|
+
export declare const KYOGIJO_COURTS_STYLE: string;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const NET = { light: "#20262b", dark: "#e6e9ec" };
|
|
2
|
+
/** The looks of every sport, each in light and dark. */
|
|
3
|
+
export const COURT_LOOKS = {
|
|
4
|
+
tennis: {
|
|
5
|
+
"hard-blue": { light: { surface: "#3d78bd", free: "#2f5f9a", apron: "#dcd8c9" }, dark: { surface: "#2f5f99", free: "#244a78", apron: "#1b1f1c" } },
|
|
6
|
+
"hard-green": { light: { surface: "#4d9468", free: "#3c7852", apron: "#dcd8c9" }, dark: { surface: "#35704b", free: "#285539", apron: "#1b1f1c" } },
|
|
7
|
+
clay: { light: { surface: "#c86b43", free: "#b45a35", apron: "#d9cdb8" }, dark: { surface: "#8e4a2e", free: "#7a3f27", apron: "#1b1f1c" } },
|
|
8
|
+
grass: { light: { surface: "#68a653", free: "#579544", apron: "#cfd6bd" }, dark: { surface: "#3f6e32", free: "#355c2a", apron: "#1b1f1c" } },
|
|
9
|
+
},
|
|
10
|
+
volleyball: {
|
|
11
|
+
wood: { light: { surface: "#dcae6e", free: "#c99a5c", apron: "#c9c3b4", area: "#d3a362" }, dark: { surface: "#8f6a3c", free: "#7d5d35", apron: "#1a1c1b", area: "#876234" } },
|
|
12
|
+
blue: { light: { surface: "#3b78b8", free: "#2b5d92", apron: "#d3d8df", area: "#5b94cc" }, dark: { surface: "#2e5f95", free: "#234a74", apron: "#1a1c1b", area: "#3e72aa" } },
|
|
13
|
+
sand: { light: { surface: "#ecd7a3", free: "#e0c78b", apron: "#cdbf9c", line: "#2a5db0" }, dark: { surface: "#a89466", free: "#957f55", apron: "#1d1c17", line: "#7aa3e6" } },
|
|
14
|
+
},
|
|
15
|
+
badminton: {
|
|
16
|
+
green: { light: { surface: "#3f8f6b", free: "#2f6f52", apron: "#d3d9d0" }, dark: { surface: "#2d6a4e", free: "#225240", apron: "#1a1c1b" } },
|
|
17
|
+
blue: { light: { surface: "#3b6fb0", free: "#2b5489", apron: "#d3d8df" }, dark: { surface: "#2d5688", free: "#22426a", apron: "#1a1c1b" } },
|
|
18
|
+
wood: { light: { surface: "#dcae6e", free: "#c99a5c", apron: "#c9c3b4" }, dark: { surface: "#8f6a3c", free: "#7d5d35", apron: "#1a1c1b" } },
|
|
19
|
+
},
|
|
20
|
+
pickleball: {
|
|
21
|
+
"blue-green": { light: { surface: "#3f85b8", free: "#3f8f5a", apron: "#cdd6c8", area: "#6fb0d8" }, dark: { surface: "#2f6a95", free: "#2f6f45", apron: "#1a1c1b", area: "#4a88b0" } },
|
|
22
|
+
green: { light: { surface: "#4f9a5c", free: "#3a7a47", apron: "#cdd6c8", area: "#82c28d" }, dark: { surface: "#386f43", free: "#2b5835", apron: "#1a1c1b", area: "#4e8c59" } },
|
|
23
|
+
red: { light: { surface: "#b5503a", free: "#8f3b2b", apron: "#d6cdc4", area: "#d27a62" }, dark: { surface: "#8a3d2c", free: "#6c2e21", apron: "#1a1c1b", area: "#a8523d" } },
|
|
24
|
+
},
|
|
25
|
+
tabletennis: {
|
|
26
|
+
blue: { light: { surface: "#1f4f8f", free: "#1f4f8f", apron: "#cfd6df" }, dark: { surface: "#1a4079", free: "#1a4079", apron: "#1a1d21" } },
|
|
27
|
+
green: { light: { surface: "#2f6f4f", free: "#2f6f4f", apron: "#cdd6c8" }, dark: { surface: "#25583f", free: "#25583f", apron: "#1a1d1b" } },
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
const vars = (look, net) => `--ky-surface:${look.surface};--ky-free:${look.free};--ky-apron:${look.apron};--ky-net:${net}${look.area === undefined ? "" : `;--ky-area:${look.area}`}${look.line === undefined ? "" : `;--ky-line:${look.line}`}`;
|
|
31
|
+
/** The three rules of one look: light, dark by the device's setting, and dark by the page's `data-theme`. */
|
|
32
|
+
function lookRules(sport, name) {
|
|
33
|
+
const pair = COURT_LOOKS[sport][name];
|
|
34
|
+
const at = `[data-sport=${sport}][data-look=${name}]`;
|
|
35
|
+
return [
|
|
36
|
+
`:where(.kyogijo${at}){${vars(pair.light, NET.light)}}`,
|
|
37
|
+
`@media (prefers-color-scheme:dark){:where(html:not([data-theme=light]) .kyogijo${at}:not([data-theme=light])){${vars(pair.dark, NET.dark)}}}`,
|
|
38
|
+
`:where(html[data-theme=dark] .kyogijo${at},.kyogijo[data-theme=dark]${at}){${vars(pair.dark, NET.dark)}}`,
|
|
39
|
+
].join("\n");
|
|
40
|
+
}
|
|
41
|
+
/** The rules every net sport's court shares. */
|
|
42
|
+
const CLASSES = `
|
|
43
|
+
.kyogijo .ky-free{fill:var(--ky-free)}
|
|
44
|
+
.kyogijo .ky-area{fill:var(--ky-area,var(--ky-surface))}
|
|
45
|
+
.kyogijo .ky-nt-line,.kyogijo .ky-nt-post{fill:var(--ky-net)}
|
|
46
|
+
.kyogijo .ky-serve{fill:var(--ky-serve,#d8e33b);stroke:rgba(0,0,0,.45);stroke-width:.06}
|
|
47
|
+
.kyogijo .ky-serve-seam{fill:none;stroke:rgba(0,0,0,.45)}
|
|
48
|
+
.kyogijo .ky-tb{font-weight:700;opacity:.9}`.trim();
|
|
49
|
+
/** The rules of a drawing of this sport in this look: the shared rules and the look's colours. */
|
|
50
|
+
export function courtStyle(sport, look) {
|
|
51
|
+
return `${CLASSES}\n${lookRules(sport, look)}`;
|
|
52
|
+
}
|
|
53
|
+
/** Every net sport's rules and every look, for a page that draws with `style: "none"` and supplies the stylesheet itself. */
|
|
54
|
+
export const KYOGIJO_COURTS_STYLE = `${CLASSES}\n${Object.keys(COURT_LOOKS).flatMap((sport) => Object.keys(COURT_LOOKS[sport]).map((name) => lookRules(sport, name))).join("\n")}`;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { TeamView } from "./score.ts";
|
|
2
|
+
import type { PlateSpot } from "./surfaces/surface.ts";
|
|
3
|
+
export interface CricketWords {
|
|
4
|
+
competition?: string;
|
|
5
|
+
foot?: string;
|
|
6
|
+
/** The heads of the columns: the innings, the total (runs for wickets) and the overs. */
|
|
7
|
+
innings: string;
|
|
8
|
+
total: string;
|
|
9
|
+
overs: string;
|
|
10
|
+
}
|
|
11
|
+
/** The total as a cricket score is written: the runs, a stroke, the wickets lost. Runs alone when no wickets are given. */
|
|
12
|
+
export declare const totalText: (t: Pick<TeamView, "score" | "wickets">) => string;
|
|
13
|
+
/** How many innings columns a board has: the most either team gives; none when neither gives any. */
|
|
14
|
+
export declare const inningsCount: (left: readonly string[] | undefined, right: readonly string[] | undefined) => number;
|
|
15
|
+
/** The board as a group centred on the origin. `spot` is the room it has; it takes only what its rows and columns need. */
|
|
16
|
+
export declare function cricketBoardMarkup(spot: PlateSpot, left: TeamView, right: TeamView, words: CricketWords, o: {
|
|
17
|
+
parts: boolean;
|
|
18
|
+
}): string;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// The cricket scoreboard: each team's runs and wickets, the overs, and, for a match of more than one innings, each innings; drawn on the ground below the pitch. The board is drawn in the frame the eye sees; the caller turns it
|
|
2
|
+
// back when the ground is turned.
|
|
3
|
+
import { el, esc } from "./format.js";
|
|
4
|
+
import { fitLines, textWidth } from "./text.js";
|
|
5
|
+
const text = (x, cy, value, size, cls, extra = {}) => el("text", { class: `ky-text ${cls}`, x, y: cy + size * 0.36, "font-size": size, "text-anchor": "middle", ...extra }, esc(value));
|
|
6
|
+
const fit = (value, room, ceiling) => Math.max(0.1, Math.min(ceiling, room / Math.max(1, textWidth(value))));
|
|
7
|
+
/** The total as a cricket score is written: the runs, a stroke, the wickets lost. Runs alone when no wickets are given. */
|
|
8
|
+
export const totalText = (t) => (t.wickets === undefined || t.wickets === "" ? t.score : `${t.score}/${t.wickets}`);
|
|
9
|
+
/** How many innings columns a board has: the most either team gives; none when neither gives any. */
|
|
10
|
+
export const inningsCount = (left, right) => Math.max(left?.length ?? 0, right?.length ?? 0);
|
|
11
|
+
/** The board as a group centred on the origin. `spot` is the room it has; it takes only what its rows and columns need. */
|
|
12
|
+
export function cricketBoardMarkup(spot, left, right, words, o) {
|
|
13
|
+
const n = inningsCount(left.innings, right.innings);
|
|
14
|
+
const team = [left, right];
|
|
15
|
+
const hasOvers = team.some((t) => t.overs !== undefined && t.overs !== "");
|
|
16
|
+
const cols = 3 + n * 1.3 + 1.9 + (hasOvers ? 1.5 : 0);
|
|
17
|
+
const extra = (words.competition ? 0.8 : 0) + (words.foot ? 0.8 : 0);
|
|
18
|
+
const rows = 3 + extra;
|
|
19
|
+
const colU = Math.min(spot.w / cols, spot.h / rows / 0.72);
|
|
20
|
+
const rowH = Math.min(spot.h / rows, colU * 0.72);
|
|
21
|
+
const w = colU * cols;
|
|
22
|
+
const h = rowH * rows;
|
|
23
|
+
const x0 = -w / 2;
|
|
24
|
+
let y = -h / 2;
|
|
25
|
+
const parts = (names) => (o.parts ? names : undefined);
|
|
26
|
+
let inner = el("rect", { class: "ky-plate-bg", x: -w / 2, y: -h / 2, width: w, height: h, rx: Math.min(rowH * 0.5, w * 0.04) });
|
|
27
|
+
const band = (value, cls) => {
|
|
28
|
+
const bandH = rowH * 0.8;
|
|
29
|
+
inner += text(0, y + bandH / 2, value, fit(value, w * 0.92, bandH * 0.62), cls);
|
|
30
|
+
y += bandH;
|
|
31
|
+
};
|
|
32
|
+
if (words.competition)
|
|
33
|
+
band(words.competition, "ky-comp");
|
|
34
|
+
const nameW = colU * 3;
|
|
35
|
+
const inX = (k) => x0 + nameW + colU * 1.3 * (k + 0.5);
|
|
36
|
+
const totalX = x0 + nameW + colU * 1.3 * n + colU * 0.95;
|
|
37
|
+
const oversX = totalX + colU * 0.95 + colU * 0.75;
|
|
38
|
+
for (let k = 0; k < n; k += 1)
|
|
39
|
+
inner += text(inX(k), y + rowH / 2, String(k + 1), rowH * 0.46, "ky-foot");
|
|
40
|
+
inner += text(totalX, y + rowH / 2, words.total, fit(words.total, colU * 1.7, rowH * 0.42), "ky-foot");
|
|
41
|
+
if (hasOvers)
|
|
42
|
+
inner += text(oversX, y + rowH / 2, words.overs, fit(words.overs, colU * 1.3, rowH * 0.42), "ky-foot");
|
|
43
|
+
y += rowH;
|
|
44
|
+
for (const t of team) {
|
|
45
|
+
inner += el("rect", { class: `ky-panel-${t.side}`, x: x0 + 0.1 * colU, y: y + rowH * 0.06, width: nameW - 0.2 * colU, height: rowH * 0.88, rx: rowH * 0.2 });
|
|
46
|
+
const name = fitLines(t.name, (nameW - 0.3 * colU) * 0.92, rowH * 0.8, rowH * 0.5);
|
|
47
|
+
name.lines.forEach((line, k) => {
|
|
48
|
+
inner += text(x0 + 0.1 * colU + (nameW - 0.2 * colU) / 2, y + rowH / 2 + (k - (name.lines.length - 1) / 2) * name.size * 1.1, line, name.size, `ky-ink-${t.side}`);
|
|
49
|
+
});
|
|
50
|
+
for (let k = 0; k < n; k += 1) {
|
|
51
|
+
const v = t.innings?.[k] ?? "";
|
|
52
|
+
if (v !== "")
|
|
53
|
+
inner += text(inX(k), y + rowH / 2, v, fit(v, colU * 1.2, rowH * 0.56), "ky-score");
|
|
54
|
+
}
|
|
55
|
+
const total = totalText(t);
|
|
56
|
+
const size = fit(total, colU * 1.8, rowH * 0.76);
|
|
57
|
+
inner += el("g", { class: `ky-total ky-${t.state}`, part: parts(`score ${t.role}-score`) }, text(totalX, y + rowH / 2, total, size, "ky-score"));
|
|
58
|
+
if (t.state === "win")
|
|
59
|
+
inner += el("rect", { class: `ky-winbar ky-winbar-${t.side}`, x: totalX - (textWidth(total) * size) / 2, y: y + rowH / 2 + size * 0.5, width: textWidth(total) * size, height: Math.max(0.05, size * 0.07) });
|
|
60
|
+
if (hasOvers && t.overs !== undefined && t.overs !== "")
|
|
61
|
+
inner += text(oversX, y + rowH / 2, t.overs, fit(t.overs, colU * 1.3, rowH * 0.56), "ky-score");
|
|
62
|
+
y += rowH;
|
|
63
|
+
}
|
|
64
|
+
if (words.foot)
|
|
65
|
+
band(words.foot, "ky-foot");
|
|
66
|
+
return el("g", { class: "ky-plate ky-scoreboard", part: parts("plate") }, inner);
|
|
67
|
+
}
|