@johnmorrisdotca/dougu-sports 0.1.0 → 0.2.1
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 +53 -1
- package/NOTICE.md +9 -58
- package/README.md +174 -37
- package/dist/emblems/award/badge.d.ts +2 -2
- package/dist/emblems/award/badge.js +14 -5
- package/dist/emblems/award/crown.d.ts +1 -1
- package/dist/emblems/award/crown.js +14 -4
- package/dist/emblems/award/medal.d.ts +2 -2
- package/dist/emblems/award/medal.js +14 -5
- package/dist/emblems/award/shield.d.ts +1 -1
- package/dist/emblems/award/shield.js +10 -4
- package/dist/emblems/award/star.d.ts +1 -1
- package/dist/emblems/award/star.js +10 -4
- package/dist/emblems/ball/ball.afl.js +3 -2
- package/dist/emblems/ball/ball.basketball6.js +1 -1
- package/dist/emblems/ball/ball.core.d.ts +9 -3
- package/dist/emblems/ball/ball.core.js +13 -0
- package/dist/emblems/ball/ball.cricket.js +1 -1
- package/dist/emblems/ball/ball.curlingstone.d.ts +2 -0
- package/dist/emblems/ball/ball.curlingstone.js +11 -7
- package/dist/emblems/ball/ball.discus.js +1 -1
- package/dist/emblems/ball/ball.engine.js +3 -3
- package/dist/emblems/ball/ball.football.js +9 -7
- package/dist/emblems/ball/ball.gaelic.js +1 -1
- package/dist/emblems/ball/ball.object.d.ts +5 -2
- package/dist/emblems/ball/ball.object.js +3 -2
- package/dist/emblems/ball/ball.puck.d.ts +2 -0
- package/dist/emblems/ball/ball.puck.js +2 -2
- package/dist/emblems/ball/ball.rugby.js +3 -2
- package/dist/emblems/ball/ball.shuttlecock.d.ts +4 -0
- package/dist/emblems/ball/ball.shuttlecock.js +6 -5
- package/dist/emblems/ball/ball.squash.js +5 -5
- package/dist/emblems/emblem.api.d.ts +39 -1
- package/dist/emblems/emblem.api.js +20 -2
- package/dist/emblems/emblem.compose.d.ts +31 -0
- package/dist/emblems/emblem.compose.js +91 -0
- package/dist/emblems/emblem.core.d.ts +48 -2
- package/dist/emblems/emblem.core.js +30 -0
- package/dist/emblems/emblem.descriptions.d.ts +7 -0
- package/dist/emblems/emblem.descriptions.js +60 -0
- package/dist/emblems/emblem.display.d.ts +75 -0
- package/dist/emblems/emblem.display.js +214 -0
- package/dist/emblems/emblem.engrave.d.ts +50 -0
- package/dist/emblems/emblem.engrave.js +145 -0
- package/dist/emblems/emblem.equipment.d.ts +24 -0
- package/dist/emblems/emblem.equipment.js +57 -0
- package/dist/emblems/emblem.flip.d.ts +10 -0
- package/dist/emblems/emblem.flip.js +46 -0
- package/dist/emblems/emblem.ids.d.ts +19 -11
- package/dist/emblems/emblem.ids.js +21 -6
- package/dist/emblems/emblem.lathe.d.ts +3 -0
- package/dist/emblems/emblem.lathe.js +26 -4
- package/dist/emblems/emblem.medal.js +4 -4
- package/dist/emblems/emblem.pennant.d.ts +3 -0
- package/dist/emblems/emblem.pennant.js +79 -0
- package/dist/emblems/emblem.registry.js +20 -0
- package/dist/emblems/emblem.shapes.d.ts +19 -2
- package/dist/emblems/emblem.shapes.js +55 -13
- package/dist/emblems/emblem.sprite.d.ts +38 -0
- package/dist/emblems/emblem.sprite.js +65 -0
- package/dist/emblems/emblem.uri.d.ts +22 -0
- package/dist/emblems/emblem.uri.js +29 -0
- package/dist/emblems/gear/gear.badmintonracket.d.ts +1 -1
- package/dist/emblems/gear/gear.badmintonracket.js +18 -14
- package/dist/emblems/gear/gear.baseballglove.d.ts +1 -1
- package/dist/emblems/gear/gear.baseballglove.js +20 -14
- package/dist/emblems/gear/gear.budo.d.ts +17 -0
- package/dist/emblems/gear/gear.budo.js +60 -0
- package/dist/emblems/gear/gear.core.d.ts +12 -31
- package/dist/emblems/gear/gear.core.js +61 -54
- package/dist/emblems/gear/gear.cricketbat.d.ts +1 -1
- package/dist/emblems/gear/gear.cricketbat.js +16 -14
- package/dist/emblems/gear/gear.fieldhockeystick.d.ts +1 -1
- package/dist/emblems/gear/gear.fieldhockeystick.js +15 -14
- package/dist/emblems/gear/gear.footballhelmet.d.ts +1 -1
- package/dist/emblems/gear/gear.footballhelmet.js +15 -14
- package/dist/emblems/gear/gear.goaliemask.d.ts +1 -1
- package/dist/emblems/gear/gear.goaliemask.js +22 -16
- package/dist/emblems/gear/gear.goalnet.d.ts +1 -1
- package/dist/emblems/gear/gear.goalnet.js +24 -14
- package/dist/emblems/gear/gear.hockeystick.d.ts +1 -1
- package/dist/emblems/gear/gear.hockeystick.js +14 -14
- package/dist/emblems/gear/gear.index.js +5 -1
- package/dist/emblems/gear/gear.kendo.d.ts +2 -0
- package/dist/emblems/gear/gear.kendo.js +41 -0
- package/dist/emblems/gear/gear.kyudo.d.ts +2 -0
- package/dist/emblems/gear/gear.kyudo.js +31 -0
- package/dist/emblems/gear/gear.lacrossestick.d.ts +1 -1
- package/dist/emblems/gear/gear.lacrossestick.js +25 -14
- package/dist/emblems/gear/gear.skates.js +1 -1
- package/dist/emblems/gear/gear.sticks.js +2 -2
- package/dist/emblems/gear/gear.sumo.d.ts +2 -0
- package/dist/emblems/gear/gear.sumo.js +43 -0
- package/dist/emblems/gear/gear.tabletennispaddle.d.ts +1 -1
- package/dist/emblems/gear/gear.tabletennispaddle.js +15 -14
- package/dist/emblems/gear/gear.tennisracket.d.ts +1 -1
- package/dist/emblems/gear/gear.tennisracket.js +16 -14
- package/dist/emblems/pennant/championship.d.ts +2 -0
- package/dist/emblems/pennant/championship.js +4 -0
- package/dist/emblems/sport/afl.js +1 -1
- package/dist/emblems/sport/artisticswimming.js +1 -1
- package/dist/emblems/sport/cfl.d.ts +1 -1
- package/dist/emblems/sport/cfl.js +4 -4
- package/dist/emblems/sport/cricket.d.ts +1 -1
- package/dist/emblems/sport/cricket.js +13 -4
- package/dist/emblems/sport/fieldhockey.js +1 -1
- package/dist/emblems/sport/gaelic.js +1 -1
- package/dist/emblems/sport/handball.d.ts +1 -1
- package/dist/emblems/sport/handball.js +14 -4
- package/dist/emblems/sport/ifaf.d.ts +1 -1
- package/dist/emblems/sport/ifaf.js +4 -4
- package/dist/emblems/sport/judo.d.ts +2 -0
- package/dist/emblems/sport/judo.js +13 -0
- package/dist/emblems/sport/karate.d.ts +2 -0
- package/dist/emblems/sport/karate.js +13 -0
- package/dist/emblems/sport/kendo.d.ts +2 -0
- package/dist/emblems/sport/kendo.js +14 -0
- package/dist/emblems/sport/kyudo.d.ts +2 -0
- package/dist/emblems/sport/kyudo.js +15 -0
- package/dist/emblems/sport/mlb.d.ts +1 -1
- package/dist/emblems/sport/mlb.js +27 -4
- package/dist/emblems/sport/nba.d.ts +1 -1
- package/dist/emblems/sport/nba.js +11 -4
- package/dist/emblems/sport/netball.js +1 -1
- package/dist/emblems/sport/nfl.d.ts +2 -1
- package/dist/emblems/sport/nfl.js +14 -4
- package/dist/emblems/sport/nhl.d.ts +1 -1
- package/dist/emblems/sport/nhl.js +12 -4
- package/dist/emblems/sport/pickleball.js +1 -1
- package/dist/emblems/sport/racquetball.js +1 -1
- package/dist/emblems/sport/roundnet.js +1 -1
- package/dist/emblems/sport/rugby.d.ts +1 -1
- package/dist/emblems/sport/rugby.js +11 -4
- package/dist/emblems/sport/sepaktakraw.js +1 -1
- package/dist/emblems/sport/soccer.d.ts +1 -1
- package/dist/emblems/sport/soccer.js +30 -4
- package/dist/emblems/sport/squash.js +1 -1
- package/dist/emblems/sport/sumo.d.ts +2 -0
- package/dist/emblems/sport/sumo.js +15 -0
- package/dist/emblems/sport/tabletennis.js +1 -1
- package/dist/emblems/sport/tennis.d.ts +1 -1
- package/dist/emblems/sport/tennis.js +14 -4
- package/dist/emblems/sport/teqball.js +1 -1
- package/dist/emblems/sport/volleyball.d.ts +1 -1
- package/dist/emblems/sport/volleyball.js +24 -4
- package/dist/emblems/trophy/banner.d.ts +2 -0
- package/dist/emblems/trophy/banner.js +20 -0
- package/dist/emblems/trophy/baseball.js +1 -0
- package/dist/emblems/trophy/basketball.js +1 -0
- package/dist/emblems/trophy/belt.js +2 -1
- package/dist/emblems/trophy/bowl.js +1 -0
- package/dist/emblems/trophy/canadian-bowl.js +1 -0
- package/dist/emblems/trophy/championship-banner.d.ts +2 -0
- package/dist/emblems/trophy/championship-banner.js +67 -0
- package/dist/emblems/trophy/cricket-urn.js +1 -0
- package/dist/emblems/trophy/cup.d.ts +1 -1
- package/dist/emblems/trophy/cup.js +15 -4
- package/dist/emblems/trophy/football.js +2 -1
- package/dist/emblems/trophy/globe.js +1 -0
- package/dist/emblems/trophy/hockey-cup.js +2 -1
- package/dist/emblems/trophy/lidded-cup.js +1 -0
- package/dist/emblems/trophy/plaque.js +2 -1
- package/dist/emblems/trophy/ring.d.ts +2 -0
- package/dist/emblems/trophy/ring.js +30 -0
- package/dist/emblems/trophy/rugby-cup.js +1 -0
- package/dist/emblems/trophy/salver.js +1 -0
- package/dist/emblems/trophy/soccer-ball.js +2 -1
- package/dist/emblems/trophy/soccer-globe.js +1 -0
- package/dist/emblems/trophy/sumo-cup.d.ts +2 -0
- package/dist/emblems/trophy/sumo-cup.js +28 -0
- package/dist/emblems/trophy/tennis-cup.js +2 -1
- package/dist/emblems/trophy/tennis-plate.js +1 -0
- package/dist/emblems-entry.d.ts +15 -6
- package/dist/emblems-entry.js +7 -2
- package/dist/index.d.ts +17 -0
- package/dist/index.js +15 -0
- package/dist/turn-control.d.ts +101 -0
- package/dist/turn-control.js +363 -0
- package/dist/turn-entry.d.ts +2 -0
- package/dist/turn-entry.js +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -1
- package/LICENSES/Apache-2.0.txt +0 -202
- package/LICENSES/CC-BY-3.0.txt +0 -319
- package/LICENSES/upstream/game-icons.license.txt +0 -45
- package/LICENSES/upstream/noto-emoji-2D-svg.LICENSE +0 -13
- package/LICENSES/upstream/pictogrammers-mdi.LICENSE +0 -20
- package/dist/emblems/emblem.material.d.ts +0 -9
- package/dist/emblems/emblem.material.data.d.ts +0 -34
- package/dist/emblems/emblem.material.data.js +0 -85
- package/dist/emblems/emblem.material.js +0 -23
|
@@ -6,6 +6,8 @@ export declare const EMBLEM_STYLES: readonly ["metal", "line", "color"];
|
|
|
6
6
|
* one, for an image (an `<img>`, a data address) that cannot inherit. `light` and `dark` force one.
|
|
7
7
|
*/
|
|
8
8
|
export type EmblemTheme = "inherit" | "auto" | "light" | "dark";
|
|
9
|
+
/** The languages an emblem's `<title>` can be written in. */
|
|
10
|
+
export type EmblemLocale = "en" | "ja";
|
|
9
11
|
export interface EmblemOptions {
|
|
10
12
|
/** `metal` (the default), `line` or `color`. */
|
|
11
13
|
style?: EmblemStyle;
|
|
@@ -21,6 +23,27 @@ export interface EmblemOptions {
|
|
|
21
23
|
initials?: string;
|
|
22
24
|
/** A ball: force the grain of its surface on or off. By default it is on for a ball of 48 pixels or more that is not turning. */
|
|
23
25
|
grain?: boolean;
|
|
26
|
+
/** The language of the `<title>`: `"en"` (the default) or `"ja"`, from the emblem's English or Japanese name. */
|
|
27
|
+
locale?: EmblemLocale;
|
|
28
|
+
/** The `<title>`: a string writes that title (escaped); `false` drops it and marks the drawing `aria-hidden="true"`, for an emblem that sits beside text that says the same. */
|
|
29
|
+
title?: string | false;
|
|
30
|
+
/**
|
|
31
|
+
* Text cut into the face of a medal or place badge, the plaque, the belt, and the plinth of a cup: any text, Japanese included, up to three lines (a line break, or a space between a word and a year, makes lines).
|
|
32
|
+
* It wins over `label`, `year` and `rank`.
|
|
33
|
+
*/
|
|
34
|
+
engrave?: string;
|
|
35
|
+
/** The words to engrave ("MVP"), with `year` and `rank` as lines of their own. Ignored when `engrave` is given. */
|
|
36
|
+
label?: string;
|
|
37
|
+
/** A year, engraved small under the label (on a plaque, on its nameplate). A number or a string. */
|
|
38
|
+
year?: number | string;
|
|
39
|
+
/** A place or a number, engraved large ("1"). On a place badge it is the badge's own place unless given. */
|
|
40
|
+
rank?: number | string;
|
|
41
|
+
/** The engraving of a medal's back, shown when it is turned over. Without it the back is plain. */
|
|
42
|
+
engraveBack?: string;
|
|
43
|
+
/** The colour of a pennant's lettering when it is not the second colour. A hex colour or `rgb()`. */
|
|
44
|
+
numberColor?: string;
|
|
45
|
+
/** Which side of a pennant is drawn at rest: `"front"` (the default) or `"back"`. */
|
|
46
|
+
view?: "front" | "back";
|
|
24
47
|
}
|
|
25
48
|
/** A turn, in degrees from the resting view (0 and 0 is the still drawing): `yaw` about the vertical axis, `pitch` the tilt toward the eye. */
|
|
26
49
|
export interface FrameOptions extends EmblemOptions {
|
|
@@ -46,8 +69,8 @@ export declare const METALS: {
|
|
|
46
69
|
};
|
|
47
70
|
/** A metal by name, or `flat` for a colour that is no metal (a crest's blue, a sport's plain ink). */
|
|
48
71
|
export type Metal = keyof typeof METALS | "flat";
|
|
49
|
-
/** Where an emblem's art comes from: drawn for this package (`ours`) or
|
|
50
|
-
export type EmblemSource = "ours" | "
|
|
72
|
+
/** Where an emblem's art comes from: drawn for this package (`ours`, `drawn`) or made by this package's own code (`generated`). Nothing is copied from another project. */
|
|
73
|
+
export type EmblemSource = "ours" | "generated" | "drawn";
|
|
51
74
|
/** What an emblem is: a flat object (a front and a back, to flip), a ball (a sphere or a solid, turned in 3D), a body of revolution (`lathe`), an icon (one drawing, not turned) or a piece of gear (a flat drawing, not turned). */
|
|
52
75
|
export type EmblemKind = "flat" | "ball" | "lathe" | "icon" | "gear";
|
|
53
76
|
export type Rgb = readonly [number, number, number];
|
|
@@ -58,6 +81,19 @@ export declare const hexOf: (rgb: Rgb) => string;
|
|
|
58
81
|
export declare function mix(rgb: Rgb, amount: number): string;
|
|
59
82
|
/** Text made safe to write between tags and inside a double-quoted attribute. */
|
|
60
83
|
export declare const escape: (text: string) => string;
|
|
84
|
+
/**
|
|
85
|
+
* The `<title>` of a finished drawing, as the options ask: a string is the title (escaped), `false` drops it and hides the drawing from a screen reader, and `locale: "ja"` writes the Japanese name. With none
|
|
86
|
+
* of the three the drawing is returned as it is.
|
|
87
|
+
*/
|
|
88
|
+
export declare function relabel(svg: string, en: string, ja: string, o: {
|
|
89
|
+
locale?: unknown;
|
|
90
|
+
title?: unknown;
|
|
91
|
+
} | undefined): string;
|
|
92
|
+
/** A drawing function whose result carries the title the options ask for. */
|
|
93
|
+
export declare const labelled: <O extends {
|
|
94
|
+
locale?: unknown;
|
|
95
|
+
title?: unknown;
|
|
96
|
+
}>(en: string, ja: string, draw: (options?: O) => string) => (options?: O) => string;
|
|
61
97
|
/** A number written for a path: two decimals at most, and never `-0`. */
|
|
62
98
|
export declare const n2: (value: number) => string;
|
|
63
99
|
/** The options as the renderers use them. */
|
|
@@ -70,6 +106,16 @@ export interface Resolved {
|
|
|
70
106
|
initials: string;
|
|
71
107
|
}
|
|
72
108
|
export declare function resolve(o: EmblemOptions | undefined): Resolved;
|
|
109
|
+
/** What is to be cut into a face: the free text, or the label, year and rank, and the text of the back. A line is at most 24 letters and has no control character. */
|
|
110
|
+
export interface Engraving {
|
|
111
|
+
free?: string;
|
|
112
|
+
label?: string;
|
|
113
|
+
year?: string;
|
|
114
|
+
rank?: string;
|
|
115
|
+
back?: string;
|
|
116
|
+
}
|
|
117
|
+
/** A text from outside made safe: control characters removed, at most 24 letters a line and `lines` lines, or `undefined` when nothing is left. */
|
|
118
|
+
export declare const clean: (value: unknown, lines?: number) => string | undefined;
|
|
73
119
|
/** The main colour of a metal, or the tint when there is one. */
|
|
74
120
|
export declare const mainOf: (metal: Metal, tint: Rgb | undefined) => Rgb;
|
|
75
121
|
/** The relative luminance of a colour, 0 to 1. */
|
|
@@ -29,6 +29,24 @@ export function mix(rgb, amount) {
|
|
|
29
29
|
}
|
|
30
30
|
/** Text made safe to write between tags and inside a double-quoted attribute. */
|
|
31
31
|
export const escape = (text) => text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
32
|
+
/**
|
|
33
|
+
* The `<title>` of a finished drawing, as the options ask: a string is the title (escaped), `false` drops it and hides the drawing from a screen reader, and `locale: "ja"` writes the Japanese name. With none
|
|
34
|
+
* of the three the drawing is returned as it is.
|
|
35
|
+
*/
|
|
36
|
+
export function relabel(svg, en, ja, o) {
|
|
37
|
+
const title = o?.title;
|
|
38
|
+
const japanese = o?.locale === "ja";
|
|
39
|
+
if (title === false)
|
|
40
|
+
return svg.replace(/<title>[^<]*<\/title>/, "").replace(' role="img"', ' aria-hidden="true"');
|
|
41
|
+
const custom = typeof title === "string" && title.trim() !== "";
|
|
42
|
+
if (!custom && !japanese)
|
|
43
|
+
return svg;
|
|
44
|
+
const text = custom ? title.trim().slice(0, 300) : ja;
|
|
45
|
+
const titled = svg.replace(/<title>[^<]*<\/title>/, () => `<title>${escape(text)}</title>`);
|
|
46
|
+
return custom ? titled : titled.replace(' role="img"', ' role="img" lang="ja"');
|
|
47
|
+
}
|
|
48
|
+
/** A drawing function whose result carries the title the options ask for. */
|
|
49
|
+
export const labelled = (en, ja, draw) => (options) => relabel(draw(options), en, ja, options);
|
|
32
50
|
/** A number written for a path: two decimals at most, and never `-0`. */
|
|
33
51
|
export const n2 = (value) => String(Math.round(value * 100) / 100 + 0);
|
|
34
52
|
export function resolve(o) {
|
|
@@ -39,6 +57,18 @@ export function resolve(o) {
|
|
|
39
57
|
const initials = typeof o?.initials === "string" ? [...o.initials.trim()].slice(0, 3).join("").toUpperCase() : "";
|
|
40
58
|
return { style, size, theme, tint: readColour(o?.tint), accent: accent === undefined ? undefined : hexOf(accent), initials };
|
|
41
59
|
}
|
|
60
|
+
/** A text from outside made safe: control characters removed, at most 24 letters a line and `lines` lines, or `undefined` when nothing is left. */
|
|
61
|
+
export const clean = (value, lines = 1) => {
|
|
62
|
+
if (typeof value !== "string" && typeof value !== "number")
|
|
63
|
+
return undefined;
|
|
64
|
+
const text = String(value)
|
|
65
|
+
.split(/\r?\n/)
|
|
66
|
+
.slice(0, lines)
|
|
67
|
+
.map((line) => [...line.replace(/\p{Cc}/gu, "").trim()].slice(0, 24).join(""))
|
|
68
|
+
.filter((line) => line !== "")
|
|
69
|
+
.join("\n");
|
|
70
|
+
return text === "" ? undefined : text;
|
|
71
|
+
};
|
|
42
72
|
/** The main colour of a metal, or the tint when there is one. */
|
|
43
73
|
export const mainOf = (metal, tint) => tint ?? readColour(metal === "flat" ? "#8a949e" : METALS[metal]);
|
|
44
74
|
/** The relative luminance of a colour, 0 to 1. */
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { EmblemId } from "./emblem.ids.ts";
|
|
2
|
+
/** An emblem's description in the two languages. */
|
|
3
|
+
export interface EmblemDescription {
|
|
4
|
+
readonly en: string;
|
|
5
|
+
readonly ja: string;
|
|
6
|
+
}
|
|
7
|
+
export declare const EMBLEM_DESCRIPTIONS: Readonly<Partial<Record<EmblemId, EmblemDescription>>>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export const EMBLEM_DESCRIPTIONS = {
|
|
2
|
+
"trophy:hockey-cup": { en: "Tall banded silver cup", ja: "帯を何段も重ねた背の高い銀のカップ" },
|
|
3
|
+
"trophy:basketball": { en: "Basketball trophy", ja: "バスケットボールのトロフィー" },
|
|
4
|
+
"trophy:football": { en: "Football trophy", ja: "アメリカンフットボールのトロフィー" },
|
|
5
|
+
"trophy:soccer-ball": { en: "Soccer ball trophy", ja: "サッカーボールのトロフィー" },
|
|
6
|
+
"trophy:tennis-cup": { en: "Two-handled cup with a pineapple", ja: "取っ手が二つあり、ふたにパイナップルの飾りが付いたカップ" },
|
|
7
|
+
"sport:ifaf": { en: "American football, international rules", ja: "国際ルールのアメリカンフットボール" },
|
|
8
|
+
"sport:pickleball": { en: "Pickleball paddle and ball", ja: "ピックルボールのパドルとボール" },
|
|
9
|
+
"sport:tabletennis": { en: "Table tennis paddle and ball", ja: "卓球のラケットとボール" },
|
|
10
|
+
"sport:squash": { en: "Squash racket and ball", ja: "スカッシュのラケットとボール" },
|
|
11
|
+
"sport:racquetball": { en: "Racquetball racket and ball", ja: "ラケットボールのラケットとボール" },
|
|
12
|
+
"sport:sepaktakraw": { en: "Woven ball", ja: "セパタクローの編み目のボール" },
|
|
13
|
+
"sport:teqball": { en: "Curved table and ball", ja: "テックボールの湾曲した台とボール" },
|
|
14
|
+
"sport:roundnet": { en: "Round net", ja: "ラウンドネットの円形ネット" },
|
|
15
|
+
"sport:netball": { en: "Ball and hoop", ja: "ネットボールのボールとゴールリング" },
|
|
16
|
+
"sport:artisticswimming": { en: "Raised legs", ja: "水面に上げた脚(アーティスティックスイミング)" },
|
|
17
|
+
"sport:afl": { en: "Oval ball", ja: "オーストラリアンフットボールのボール" },
|
|
18
|
+
"sport:gaelic": { en: "Round ball with seams", ja: "ゲーリックフットボールのボール" },
|
|
19
|
+
"sport:fieldhockey": { en: "Hooked stick and ball", ja: "フィールドホッケーのスティックとボール" },
|
|
20
|
+
"ball:basketball-6": { en: "Basketball, size 6", ja: "バスケットボール(6号球)" },
|
|
21
|
+
"ball:cricket-pink": { en: "Cricket ball, pink", ja: "クリケットボール(ピンク)" },
|
|
22
|
+
"ball:football": { en: "Football (NFL)", ja: "アメリカンフットボールのボール(NFL)" },
|
|
23
|
+
"ball:football-cfl": { en: "Football (CFL)", ja: "カナディアンフットボールのボール(CFL)" },
|
|
24
|
+
"ball:football-ifaf": { en: "Football (IFAF)", ja: "アメリカンフットボールのボール(IFAF)" },
|
|
25
|
+
"ball:afl": { en: "Australian football", ja: "オーストラリアンフットボールのボール" },
|
|
26
|
+
"ball:gaelic-football": { en: "Gaelic football", ja: "ゲーリックフットボールのボール" },
|
|
27
|
+
"ball:squash": { en: "Squash ball, double yellow dot", ja: "スカッシュボール(ダブルイエロードット)" },
|
|
28
|
+
"ball:squash-yellow": { en: "Squash ball, yellow dot", ja: "スカッシュボール(イエロードット)" },
|
|
29
|
+
"ball:squash-green": { en: "Squash ball, green dot", ja: "スカッシュボール(グリーンドット)" },
|
|
30
|
+
"ball:squash-red": { en: "Squash ball, red dot", ja: "スカッシュボール(レッドドット)" },
|
|
31
|
+
"ball:squash-blue": { en: "Squash ball, blue dot", ja: "スカッシュボール(ブルードット)" },
|
|
32
|
+
"ball:shuttlecock-feather": { en: "Shuttlecock, feather", ja: "シャトルコック(水鳥の羽根)" },
|
|
33
|
+
"ball:shuttlecock-nylon": { en: "Shuttlecock, nylon", ja: "シャトルコック(ナイロン)" },
|
|
34
|
+
"gear:hockey-stick": { en: "Hockey stick and puck", ja: "アイスホッケーのスティックとパック" },
|
|
35
|
+
"gear:goalie-stick": { en: "Ice hockey goalie stick", ja: "アイスホッケーのゴーリーのスティック" },
|
|
36
|
+
"gear:hockey-skate": { en: "Hockey skate", ja: "アイスホッケーのスケート靴" },
|
|
37
|
+
"gear:field-hockey-stick": { en: "Field hockey stick and ball", ja: "フィールドホッケーのスティックとボール" },
|
|
38
|
+
"gear:cricket-bat": { en: "Cricket bat and ball", ja: "クリケットのバットとボール" },
|
|
39
|
+
"gear:table-tennis-paddle": { en: "Table-tennis paddle and ball", ja: "卓球のラケットとボール" },
|
|
40
|
+
"gear:badminton-racket": { en: "Badminton racket and shuttlecock", ja: "バドミントンのラケットとシャトルコック" },
|
|
41
|
+
"gear:football-helmet": { en: "Football helmet", ja: "アメリカンフットボールのヘルメット" },
|
|
42
|
+
"gear:hurley": { en: "Hurley", ja: "ハーリー(ハーリングのスティック)" },
|
|
43
|
+
"trophy:sumo-cup": { en: "A tall silver cup on a tall stepped stand, with an ornate phoenix-crested handle on each side", ja: "段のある高い台に載った、背の高い銀の杯。両側の取っ手に鳳凰の飾りが付く" },
|
|
44
|
+
"trophy:championship-banner": { en: "A fringed banner on a pole with a gilt finial and an embroidered border, with no lettering unless some is given", ja: "金の先端飾りの付いた竿に掛けた、刺繍の縁と房の付いた旗。文字は指定したときだけ入る" },
|
|
45
|
+
"sport:sumo": { en: "Two wrestlers in the crouch they face each other in at the start of a bout, with their belts", ja: "仕切りの姿勢で向かい合う、廻しを締めた二人の力士" },
|
|
46
|
+
"sport:handball": { en: "A hand throwing the ball, with lines of speed", ja: "ボールを投げる手と、速さを表す線" },
|
|
47
|
+
"sport:kendo": { en: "The kendo helmet", ja: "剣道の面" },
|
|
48
|
+
"sport:judo": { en: "The judo suit and belt", ja: "柔道衣と帯" },
|
|
49
|
+
"sport:kyudo": { en: "A long bow, its string and an arrow", ja: "弦を張った長弓と矢" },
|
|
50
|
+
"sport:karate": { en: "A clenched fist", ja: "握りしめた拳" },
|
|
51
|
+
"gear:gunbai": { en: "The sumo referee's war fan, with its tassel", ja: "行司が持つ軍配と、その房" },
|
|
52
|
+
"gear:mawashi": { en: "The wrestler's belt, with the stiff cords that hang from its front", ja: "力士の廻しと、前に垂らした下がり" },
|
|
53
|
+
"gear:shinai": { en: "The kendo practice sword: four bamboo staves with a leather tip, a tie and a hilt", ja: "剣道の竹刀。四つ割りの竹に先革、中結、柄が付く" },
|
|
54
|
+
"gear:kendo-men": { en: "The kendo helmet: a steel grille, cloth flaps and a throat guard", ja: "剣道の面。面金、面布団、突き垂れが付く" },
|
|
55
|
+
"gear:judogi": { en: "The judo suit: a jacket with crossed lapels, trousers and a belt", ja: "柔道衣。上衣、ズボン、帯" },
|
|
56
|
+
"gear:obi": { en: "A tied belt in the colour of a rank, white to black", ja: "段級に応じた色の、結んだ帯(白から黒まで)" },
|
|
57
|
+
"gear:yumi": { en: "The long asymmetric bow, held a third of the way up, with its string and wrapped grip", ja: "上が長い非対称の長弓。弦を張り、握り革と籐巻きが付く" },
|
|
58
|
+
"gear:ya": { en: "A kyudo arrow with feather fletching, a nock and a steel point", ja: "弓道の矢。矢羽根、筈、矢尻が付く" },
|
|
59
|
+
"gear:karate-gi": { en: "The karate suit: a jacket with crossed lapels, trousers and a belt", ja: "空手衣。上衣、ズボン、帯" },
|
|
60
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { type EmblemLocale, type EmblemStyle, type EmblemTheme } from "./emblem.core.ts";
|
|
2
|
+
import { type EmblemRef } from "./emblem.api.ts";
|
|
3
|
+
/** The options both displays share. */
|
|
4
|
+
export interface DisplayOptions {
|
|
5
|
+
/** The width in pixels; the height follows the drawing's proportions. A podium is 320 by default, an award case as wide as its cells. */
|
|
6
|
+
width?: number;
|
|
7
|
+
/** `metal` (the default), `line` or `color`: the style of the emblems and of the steps and the shelf. */
|
|
8
|
+
style?: EmblemStyle;
|
|
9
|
+
/** `inherit` (the default), `auto`, `light` or `dark`, as for an emblem. */
|
|
10
|
+
theme?: EmblemTheme;
|
|
11
|
+
/** `"en"` (the default) or `"ja"`: the language of the default title and description, and of the emblems' names in the description. */
|
|
12
|
+
locale?: EmblemLocale;
|
|
13
|
+
/** A string is the `<title>`; `false` drops the title and the description and marks the drawing `aria-hidden="true"`. By default it is "Podium" or "Award case". */
|
|
14
|
+
title?: string | false;
|
|
15
|
+
/** The `<desc>`: by default it lists what is drawn, in words. */
|
|
16
|
+
desc?: string;
|
|
17
|
+
}
|
|
18
|
+
/** One place of a podium. */
|
|
19
|
+
export interface PodiumPlace {
|
|
20
|
+
/** The medal or trophy for the place: any emblem. By default the gold, silver and bronze medals. */
|
|
21
|
+
emblem?: EmblemRef;
|
|
22
|
+
/** The name of the winner, written under the step's top. Long names are cut short with an ellipsis. */
|
|
23
|
+
name?: string;
|
|
24
|
+
/** A flag for the winner: SVG markup (`<svg …>`), which is drawn as an image and so cannot run or load anything, or a URL (`https:`, a path on your site, or a `data:image/…` address). Anything else is left out. */
|
|
25
|
+
flag?: string;
|
|
26
|
+
/** A line on the step: a time, a score, a number of points. */
|
|
27
|
+
label?: string;
|
|
28
|
+
}
|
|
29
|
+
/** What `podium` takes. */
|
|
30
|
+
export type PodiumOptions = DisplayOptions;
|
|
31
|
+
/** One kind of trophy in an award case. */
|
|
32
|
+
export interface AwardItem {
|
|
33
|
+
/** The trophy, medal or crest: any emblem. */
|
|
34
|
+
emblem: EmblemRef;
|
|
35
|
+
/** How many the team has won; a badge `×N` shows when it is more than one. 1 by default; less than 1 leaves the item out, and the badge stops at 999. */
|
|
36
|
+
count?: number;
|
|
37
|
+
/** A line under the shelf: the years, or a name. */
|
|
38
|
+
label?: string;
|
|
39
|
+
}
|
|
40
|
+
/** What `awardCase` takes. */
|
|
41
|
+
export interface AwardCaseOptions extends DisplayOptions {
|
|
42
|
+
/** Items in a row before the case wraps into another (5 by default, 1 to 12). */
|
|
43
|
+
perRow?: number;
|
|
44
|
+
/** The width of an emblem in the drawing's own units (56 by default, 24 to 128): the pixel size at the default width. */
|
|
45
|
+
size?: number;
|
|
46
|
+
/** `shelf` (the default) stands the emblems on a plank under each row; `row` draws them with no plank. */
|
|
47
|
+
layout?: "shelf" | "row";
|
|
48
|
+
}
|
|
49
|
+
/** An image address that is safe to put in an `<image>`: SVG markup becomes a data address (an image cannot run script or load anything), a URL must be `https:`, a path of your own site, or an image data address. */
|
|
50
|
+
export declare function safeImage(source: unknown): string | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* A podium: first, second and third on tiered steps (second on the left, first in the middle, third on the right), each with a medal or a trophy, and optionally a name, a flag and a line of text. `places[0]` is first;
|
|
53
|
+
* a place left out is an empty step. The drawing is the page's text colour on tinted steps, so it reads on light and dark; it has a title and a description that says who stands where. A flag is SVG markup or a URL
|
|
54
|
+
* and is drawn as an `<image>`, so it cannot run script or reach anything it was not given; anything that is neither is left out.
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* import { podium } from "@johnmorrisdotca/dougu-sports/emblems";
|
|
58
|
+
* const svg = podium([{ name: "Aoki", label: "9.80" }, { name: "Brown" }, { name: "Chen" }]);
|
|
59
|
+
* console.log(svg.includes("<desc>1st: Aoki (9.80); 2nd: Brown; 3rd: Chen</desc>"));
|
|
60
|
+
* // true
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export declare function podium(places: readonly (PodiumPlace | null | undefined)[], options?: PodiumOptions): string;
|
|
64
|
+
/**
|
|
65
|
+
* An award case: a team's trophies on a shelf (or in a row), each drawn once with a `×N` badge when the team has won it more than once, wrapping into more rows after `perRow` items. Each item can carry a line of
|
|
66
|
+
* text, such as its years. The drawing follows the page's text colour, has a title and a description that lists what is in the case, and holds at most 60 items.
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* import { awardCase } from "@johnmorrisdotca/dougu-sports/emblems";
|
|
70
|
+
* const svg = awardCase([{ emblem: "trophy:hockey-cup", count: 3 }, { emblem: "medal:gold" }]);
|
|
71
|
+
* console.log(svg.includes("×3"));
|
|
72
|
+
* // true
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export declare function awardCase(items: readonly AwardItem[], options?: AwardCaseOptions): string;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// Two displays made from emblems: a podium (first, second and third on tiered steps) and an award case (a team's trophies on a shelf, with a count for each). Both are one SVG string, pure functions of their
|
|
2
|
+
// arguments, in the page's text colour so they follow light and dark, with a title and a description for a screen reader. The emblems inside are the registry's, drawn without their own titles.
|
|
3
|
+
import { EMBLEM_INK, METALS, escape, mix, n2, readColour } from "./emblem.core.js";
|
|
4
|
+
import { emblemOf, renderEmblem } from "./emblem.api.js";
|
|
5
|
+
import { IdSpace, nest, partsOf } from "./emblem.compose.js";
|
|
6
|
+
import { encodeSvg } from "./emblem.uri.js";
|
|
7
|
+
const WORDS = {
|
|
8
|
+
en: { podium: "Podium", case: "Award case", places: ["1st", "2nd", "3rd"] },
|
|
9
|
+
ja: { podium: "表彰台", case: "トロフィーケース", places: ["1位", "2位", "3位"] },
|
|
10
|
+
};
|
|
11
|
+
const FONT = 'font-family="system-ui,sans-serif"';
|
|
12
|
+
/** Text cut to a number of character widths, with an ellipsis where it was cut. */
|
|
13
|
+
function fit(text, max) {
|
|
14
|
+
const chars = [...text.replace(/\s+/g, " ").trim()];
|
|
15
|
+
let width = 0;
|
|
16
|
+
const out = [];
|
|
17
|
+
for (const c of chars) {
|
|
18
|
+
const w = (c.codePointAt(0) ?? 0) > 0x2e7f ? 2 : 1;
|
|
19
|
+
if (width + w > max)
|
|
20
|
+
return `${out.join("").trimEnd()}…`;
|
|
21
|
+
width += w;
|
|
22
|
+
out.push(c);
|
|
23
|
+
}
|
|
24
|
+
return out.join("");
|
|
25
|
+
}
|
|
26
|
+
/** An image address that is safe to put in an `<image>`: SVG markup becomes a data address (an image cannot run script or load anything), a URL must be `https:`, a path of your own site, or an image data address. */
|
|
27
|
+
export function safeImage(source) {
|
|
28
|
+
if (typeof source !== "string")
|
|
29
|
+
return undefined;
|
|
30
|
+
const text = source.trim();
|
|
31
|
+
if (text === "" || text.length > 100_000)
|
|
32
|
+
return undefined;
|
|
33
|
+
if (/^<svg[\s>]/i.test(text)) {
|
|
34
|
+
const root = /^<svg[^>]*>/i.exec(text);
|
|
35
|
+
if (root === null)
|
|
36
|
+
return undefined;
|
|
37
|
+
const withNamespace = /\sxmlns\s*=/.test(root[0]) ? text : text.replace(/^<svg/i, '<svg xmlns="http://www.w3.org/2000/svg"');
|
|
38
|
+
// An `=` is encoded as well, so that nothing in the address reads as an attribute to a scanner that looks for one.
|
|
39
|
+
return `data:image/svg+xml;utf8,${encodeSvg(withNamespace).replace(/=/g, "%3D")}`;
|
|
40
|
+
}
|
|
41
|
+
if (/^https:\/\/[^\s"'<>\\@]+$/i.test(text))
|
|
42
|
+
return text;
|
|
43
|
+
if (/^data:image\/(png|jpeg|gif|webp|avif|svg\+xml)[;,][^\s"'<>\\]*$/i.test(text))
|
|
44
|
+
return text;
|
|
45
|
+
if (/^(?:\/(?!\/)|\.{1,2}\/)[\w\-./~%+@]*$/.test(text))
|
|
46
|
+
return text;
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
/** The emblem as a part of another drawing: its body and its definitions, with no title. */
|
|
50
|
+
function inner(ref, style, space) {
|
|
51
|
+
const parts = partsOf(renderEmblem(ref, { style, theme: "inherit", title: false, grain: false }));
|
|
52
|
+
return { box: parts.box, body: space.add(parts) };
|
|
53
|
+
}
|
|
54
|
+
/** The document that holds a display: size, theme, title and description. */
|
|
55
|
+
function document(o, kind, box, defs, body, desc, defaultWidth) {
|
|
56
|
+
const [x, y, w, h] = box;
|
|
57
|
+
const width = typeof o.width === "number" && Number.isFinite(o.width) && o.width > 0 ? Math.min(Math.round(o.width * 100) / 100, 4096) : defaultWidth;
|
|
58
|
+
const locale = o.locale === "ja" ? "ja" : "en";
|
|
59
|
+
const theme = o.theme === "auto" || o.theme === "light" || o.theme === "dark" ? o.theme : "inherit";
|
|
60
|
+
const colour = theme === "dark" ? EMBLEM_INK.dark : theme === "inherit" ? "" : EMBLEM_INK.light;
|
|
61
|
+
const auto = theme === "auto" ? `<style>@media(prefers-color-scheme:dark){svg{color:${EMBLEM_INK.dark}}}</style>` : "";
|
|
62
|
+
const hidden = o.title === false;
|
|
63
|
+
const title = typeof o.title === "string" && o.title.trim() !== "" ? o.title.trim().slice(0, 300) : WORDS[locale][kind === "case" ? "case" : "podium"];
|
|
64
|
+
const text = typeof o.desc === "string" && o.desc.trim() !== "" ? o.desc.trim().slice(0, 1000) : desc;
|
|
65
|
+
const lang = locale === "ja" && typeof o.title !== "string" && !hidden ? ' lang="ja"' : "";
|
|
66
|
+
return (`<svg xmlns="http://www.w3.org/2000/svg" viewBox="${n2(x)} ${n2(y)} ${n2(w)} ${n2(h)}" width="${n2(width)}" height="${n2((width * h) / w)}"${hidden ? ' aria-hidden="true"' : ` role="img"${lang}`}${colour === "" ? "" : ` color="${colour}"`}>` +
|
|
67
|
+
`${hidden ? "" : `<title>${escape(title)}</title><desc>${escape(text)}</desc>`}${auto}${defs.length === 0 ? "" : `<defs>${defs.join("")}</defs>`}` +
|
|
68
|
+
`<g ${FONT} fill="none" stroke="none">${body}</g></svg>`);
|
|
69
|
+
}
|
|
70
|
+
const NAME_OF = (ref, locale) => (locale === "ja" ? emblemOf(ref).emblem.ja : emblemOf(ref).emblem.en);
|
|
71
|
+
/** A step: the three metals tint the steps in the metal and colour styles; in the line style they are outlines. */
|
|
72
|
+
function step(style, place, x, y, w, h, defs) {
|
|
73
|
+
const metal = readColour([METALS.gold, METALS.silver, METALS.bronze][place] ?? METALS.gold);
|
|
74
|
+
const edge = mix(metal, -0.3);
|
|
75
|
+
if (style === "line")
|
|
76
|
+
return `<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="3" stroke="currentColor" stroke-width="2"/>`;
|
|
77
|
+
if (style === "color")
|
|
78
|
+
return `<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="3" fill="${mix(metal, 0)}" fill-opacity=".34" stroke="${edge}" stroke-width="1.5"/>`;
|
|
79
|
+
const id = `dgpd${place + 1}`;
|
|
80
|
+
const hex = mix(metal, 0);
|
|
81
|
+
const def = `<linearGradient id="${id}" x2="0" y2="1"><stop offset="0" stop-color="${mix(metal, 0.35)}" stop-opacity=".78"/><stop offset="1" stop-color="${hex}" stop-opacity=".22"/></linearGradient>`;
|
|
82
|
+
if (!defs.includes(def))
|
|
83
|
+
defs.push(def);
|
|
84
|
+
return `<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="3" fill="url(#${id})" stroke="${edge}" stroke-width="1.5"/>`;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* A podium: first, second and third on tiered steps (second on the left, first in the middle, third on the right), each with a medal or a trophy, and optionally a name, a flag and a line of text. `places[0]` is first;
|
|
88
|
+
* a place left out is an empty step. The drawing is the page's text colour on tinted steps, so it reads on light and dark; it has a title and a description that says who stands where. A flag is SVG markup or a URL
|
|
89
|
+
* and is drawn as an `<image>`, so it cannot run script or reach anything it was not given; anything that is neither is left out.
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* import { podium } from "@johnmorrisdotca/dougu-sports/emblems";
|
|
93
|
+
* const svg = podium([{ name: "Aoki", label: "9.80" }, { name: "Brown" }, { name: "Chen" }]);
|
|
94
|
+
* console.log(svg.includes("<desc>1st: Aoki (9.80); 2nd: Brown; 3rd: Chen</desc>"));
|
|
95
|
+
* // true
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
export function podium(places, options = {}) {
|
|
99
|
+
const style = options.style === "line" || options.style === "color" ? options.style : "metal";
|
|
100
|
+
const locale = options.locale === "ja" ? "ja" : "en";
|
|
101
|
+
const space = new IdSpace();
|
|
102
|
+
const stepDefs = [];
|
|
103
|
+
const base = 206;
|
|
104
|
+
const heights = [100, 78, 60];
|
|
105
|
+
const centres = [160, 56, 264];
|
|
106
|
+
const defaults = ["medal:gold", "medal:silver", "medal:bronze"];
|
|
107
|
+
let top = base;
|
|
108
|
+
let body = "";
|
|
109
|
+
const said = [];
|
|
110
|
+
for (const place of [1, 0, 2]) {
|
|
111
|
+
const entry = places[place] ?? undefined;
|
|
112
|
+
const c = centres[place];
|
|
113
|
+
const h = heights[place];
|
|
114
|
+
const t = base - h;
|
|
115
|
+
body += step(style, place, c - 48, t, 96, h, stepDefs);
|
|
116
|
+
body += `<text x="${c}" y="${t + 31}" text-anchor="middle" font-size="26" font-weight="800" fill="currentColor" fill-opacity=".85">${place + 1}</text>`;
|
|
117
|
+
if (entry === undefined)
|
|
118
|
+
continue;
|
|
119
|
+
const label = typeof entry.label === "string" ? fit(entry.label, 18) : "";
|
|
120
|
+
if (label !== "")
|
|
121
|
+
body += `<text x="${c}" y="${t + 50}" text-anchor="middle" font-size="11" fill="currentColor">${escape(label)}</text>`;
|
|
122
|
+
const ref = entry.emblem ?? defaults[place];
|
|
123
|
+
const emblem = inner(ref, style, space);
|
|
124
|
+
const emblemTop = t - 62;
|
|
125
|
+
body += nest(emblem.box, emblem.body, c - 30, emblemTop, 60, 60);
|
|
126
|
+
let cursor = emblemTop;
|
|
127
|
+
const flag = safeImage(entry.flag);
|
|
128
|
+
if (flag !== undefined) {
|
|
129
|
+
cursor -= 20;
|
|
130
|
+
body += `<image href="${escape(flag)}" x="${c - 12}" y="${cursor}" width="24" height="16" preserveAspectRatio="xMidYMid meet"/><rect x="${c - 12}" y="${cursor}" width="24" height="16" stroke="currentColor" stroke-opacity=".4" stroke-width=".75"/>`;
|
|
131
|
+
}
|
|
132
|
+
const name = typeof entry.name === "string" ? fit(entry.name, 16) : "";
|
|
133
|
+
if (name !== "") {
|
|
134
|
+
cursor -= 6;
|
|
135
|
+
body += `<text x="${c}" y="${cursor}" text-anchor="middle" font-size="12" font-weight="600" fill="currentColor">${escape(name)}</text>`;
|
|
136
|
+
cursor -= 11;
|
|
137
|
+
}
|
|
138
|
+
top = Math.min(top, cursor);
|
|
139
|
+
const who = name !== "" ? (typeof entry.name === "string" ? entry.name.replace(/\s+/g, " ").trim() : name) : NAME_OF(ref, locale);
|
|
140
|
+
const line = typeof entry.label === "string" ? entry.label.replace(/\s+/g, " ").trim() : "";
|
|
141
|
+
// Japanese writes its description with full-width punctuation and 、 between the places.
|
|
142
|
+
said[place] = locale === "ja" ? `${WORDS.ja.places[place]}:${who}${line !== "" ? `(${line})` : ""}` : `${WORDS.en.places[place]}: ${who}${line !== "" ? ` (${line})` : ""}`;
|
|
143
|
+
}
|
|
144
|
+
top = Math.max(0, Math.min(top, base - 100 - 62) - 6);
|
|
145
|
+
const desc = said.filter(Boolean).join(locale === "ja" ? "、" : "; ");
|
|
146
|
+
return document(options, "podium", [0, top, 320, base + 6 - top], [...space.defs, ...stepDefs], body, desc, 320);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* An award case: a team's trophies on a shelf (or in a row), each drawn once with a `×N` badge when the team has won it more than once, wrapping into more rows after `perRow` items. Each item can carry a line of
|
|
150
|
+
* text, such as its years. The drawing follows the page's text colour, has a title and a description that lists what is in the case, and holds at most 60 items.
|
|
151
|
+
* @example
|
|
152
|
+
* ```ts
|
|
153
|
+
* import { awardCase } from "@johnmorrisdotca/dougu-sports/emblems";
|
|
154
|
+
* const svg = awardCase([{ emblem: "trophy:hockey-cup", count: 3 }, { emblem: "medal:gold" }]);
|
|
155
|
+
* console.log(svg.includes("×3"));
|
|
156
|
+
* // true
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
export function awardCase(items, options = {}) {
|
|
160
|
+
const style = options.style === "line" || options.style === "color" ? options.style : "metal";
|
|
161
|
+
const locale = options.locale === "ja" ? "ja" : "en";
|
|
162
|
+
const perRow = typeof options.perRow === "number" && Number.isFinite(options.perRow) ? Math.max(1, Math.min(12, Math.floor(options.perRow))) : 5;
|
|
163
|
+
const size = typeof options.size === "number" && Number.isFinite(options.size) ? Math.max(24, Math.min(128, options.size)) : 56;
|
|
164
|
+
const shelf = options.layout !== "row";
|
|
165
|
+
const list = items
|
|
166
|
+
.filter((item) => item !== null && typeof item === "object")
|
|
167
|
+
.map((item) => ({ item, count: item.count === undefined ? 1 : Math.min(999, Math.floor(Number(item.count))) }))
|
|
168
|
+
.filter(({ count }) => count >= 1)
|
|
169
|
+
.slice(0, 60);
|
|
170
|
+
const space = new IdSpace();
|
|
171
|
+
const gap = shelf ? 24 : 16;
|
|
172
|
+
const cell = size + gap;
|
|
173
|
+
const columns = Math.max(1, Math.min(perRow, list.length));
|
|
174
|
+
const labelled = list.some(({ item }) => typeof item.label === "string" && item.label.trim() !== "");
|
|
175
|
+
const rowHeight = 10 + size + (shelf ? 8 : 0) + (labelled ? 18 : 0) + 8;
|
|
176
|
+
const rows = Math.max(1, Math.ceil(list.length / perRow));
|
|
177
|
+
const pad = 8;
|
|
178
|
+
const width = columns * cell + 2 * pad;
|
|
179
|
+
const wide = Math.max(4, Math.floor(cell / 5.4));
|
|
180
|
+
let body = "";
|
|
181
|
+
const said = [];
|
|
182
|
+
for (let r = 0; r < rows; r++) {
|
|
183
|
+
const y0 = pad + r * rowHeight;
|
|
184
|
+
const emblemY = y0 + 10;
|
|
185
|
+
const plank = emblemY + size;
|
|
186
|
+
if (shelf)
|
|
187
|
+
body += `<rect x="${pad}" y="${plank}" width="${columns * cell}" height="6" rx="2" fill="currentColor" fill-opacity=".16" stroke="currentColor" stroke-opacity=".5" stroke-width="1"/>`;
|
|
188
|
+
for (let c = 0; c < columns; c++) {
|
|
189
|
+
const entry = list[r * perRow + c];
|
|
190
|
+
if (entry === undefined)
|
|
191
|
+
break;
|
|
192
|
+
const x = pad + c * cell + gap / 2;
|
|
193
|
+
const emblem = inner(entry.item.emblem, style, space);
|
|
194
|
+
body += nest(emblem.box, emblem.body, x, emblemY, size, size);
|
|
195
|
+
const label = typeof entry.item.label === "string" ? fit(entry.item.label, wide) : "";
|
|
196
|
+
if (label !== "")
|
|
197
|
+
body += `<text x="${n2(x + size / 2)}" y="${plank + (shelf ? 20 : 14)}" text-anchor="middle" font-size="10" fill="currentColor">${escape(label)}</text>`;
|
|
198
|
+
if (entry.count > 1) {
|
|
199
|
+
const text = `×${entry.count}`;
|
|
200
|
+
const w = 10 + text.length * 6.4;
|
|
201
|
+
const right = x + size + 4;
|
|
202
|
+
body += `<rect x="${n2(right - w)}" y="${n2(emblemY - 4)}" width="${n2(w)}" height="16" rx="8" fill="${EMBLEM_INK.light}" stroke="${EMBLEM_INK.dark}" stroke-width="1"/><text x="${n2(right - w / 2)}" y="${n2(emblemY + 7.5)}" text-anchor="middle" font-size="11" font-weight="700" fill="#ffffff">${text}</text>`;
|
|
203
|
+
}
|
|
204
|
+
const name = NAME_OF(entry.item.emblem, locale);
|
|
205
|
+
const note = typeof entry.item.label === "string" ? entry.item.label.replace(/\s+/g, " ").trim() : "";
|
|
206
|
+
// The badge is drawn ×N; spoken in Japanese the count is N回, because a screen reader reads × unevenly.
|
|
207
|
+
if (locale === "ja")
|
|
208
|
+
said.push(`${name}${entry.count > 1 ? ` ${entry.count}回` : ""}${note === "" ? "" : `(${note})`}`);
|
|
209
|
+
else
|
|
210
|
+
said.push(`${name}${entry.count > 1 ? ` ×${entry.count}` : ""}${note === "" ? "" : ` (${note})`}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return document(options, "case", [0, 0, width, pad * 2 + rows * rowHeight - 8], space.defs, body, said.join(locale === "ja" ? "、" : ", "), width);
|
|
214
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type EmblemOptions, type Engraving, type Rgb } from "./emblem.core.ts";
|
|
2
|
+
/** A box on a flat face: its centre and its size (`w` along the text), and for a plate that runs up or down a side, the turn of the text in degrees about the centre. */
|
|
3
|
+
export interface Plate {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
w: number;
|
|
7
|
+
h: number;
|
|
8
|
+
rot?: number;
|
|
9
|
+
}
|
|
10
|
+
/** A line of text and how big it is next to the others (1 is the biggest). */
|
|
11
|
+
export interface Line {
|
|
12
|
+
text: string;
|
|
13
|
+
weight: number;
|
|
14
|
+
}
|
|
15
|
+
/** The width of one character in em: a guess that holds for the usual sans-serif fonts, whole for Japanese and the wide forms. */
|
|
16
|
+
export declare function advance(ch: string): number;
|
|
17
|
+
/** The width of a text in em. */
|
|
18
|
+
export declare const widthOf: (text: string) => number;
|
|
19
|
+
/** The contrast ratio of two colours, 1 to 21. */
|
|
20
|
+
export declare const contrastOf: (a: Rgb, b: Rgb) => number;
|
|
21
|
+
/** Black or white, or a deep or pale tone of the colour itself, whichever reads at 4.5 to 1 on the colour (the best of them when none does). */
|
|
22
|
+
export declare function inkFor(main: Rgb): string;
|
|
23
|
+
/** The lines to cut, as candidates (the layout takes the one that gives the biggest letters), and the year when a plate of its own holds it. */
|
|
24
|
+
export declare function linesOf(e: Engraving, place?: {
|
|
25
|
+
rank?: string;
|
|
26
|
+
yearApart?: boolean;
|
|
27
|
+
}): {
|
|
28
|
+
main: Line[][];
|
|
29
|
+
year: string | undefined;
|
|
30
|
+
};
|
|
31
|
+
/** The ink of an engraving: the page's ink in the line style, else the colour that reads on the metal. */
|
|
32
|
+
export declare const engravingInk: (style: string, main: Rgb) => string;
|
|
33
|
+
/** Lines cut into a plate, as markup: the ink, and in the metal style a faint light under each line where the cut catches it. `candidates` are alternative line breaks, and the best fit is used. */
|
|
34
|
+
export declare function plateMarkup(candidates: readonly (readonly Line[])[], plate: Plate, main: Rgb, style: string): string;
|
|
35
|
+
/** A band round a body of revolution that holds text: the heights it spans, the tone it is cut into, the angle it is centred on and, for a body with flat sides, the side. */
|
|
36
|
+
export interface Band {
|
|
37
|
+
y0: number;
|
|
38
|
+
y1: number;
|
|
39
|
+
tone?: string;
|
|
40
|
+
at?: number;
|
|
41
|
+
face?: number;
|
|
42
|
+
}
|
|
43
|
+
/** The letters of a text set round a body at a radius: `angle` is the turn of the body now, `place` puts a point of the surface on the screen. A letter on the far side is left out. */
|
|
44
|
+
export declare function cylinderText(text: string, radius: number, height: number, ink: string, angle: number, centre: number, place: (deg: number) => readonly [x: number, y: number, depth: number]): string;
|
|
45
|
+
/** The letters of a text set on one flat side of a body, from one edge of the side to the other, as that side faces the eye. `a` and `b` are the two edges on the screen, `depth` how squarely the side faces. */
|
|
46
|
+
export declare function sideText(text: string, chord: number, height: number, ink: string, a: readonly [number, number], b: readonly [number, number], depth: number): string;
|
|
47
|
+
/** The text of an engraving as one line, for a band that holds one: the free text with its lines joined, or the rank, label and year. */
|
|
48
|
+
export declare const flatText: (e: Engraving) => string;
|
|
49
|
+
/** What is to be cut into a face, from the options: the free text, or the label, year and rank, and the text of the back. */
|
|
50
|
+
export declare function engravingOf(o: EmblemOptions | undefined): Engraving;
|