@johnmorrisdotca/dougu-sports 0.1.0 → 0.2.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 +38 -1
- package/NOTICE.md +9 -58
- package/README.md +168 -32
- 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 +1 -1
- 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.engine.js +3 -3
- package/dist/emblems/ball/ball.football.js +3 -3
- package/dist/emblems/ball/ball.gaelic.js +1 -1
- package/dist/emblems/ball/ball.object.js +2 -2
- package/dist/emblems/ball/ball.shuttlecock.js +2 -2
- 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 +45 -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 +53 -12
- 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
|
@@ -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;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Engraving: text cut into a face. A plate is a box on a flat face; its lines are fitted to it (the biggest size at which the longest line and the stack of lines both fit), and the ink is chosen
|
|
2
|
+
// to read at 4.5 to 1 on the metal under it, the same way a crest's letters are. A plinth is a band round a body of revolution: its letters are set one by one at their angle round it, so the
|
|
3
|
+
// text foreshortens, goes behind the edge and comes round as the body turns. Imports only the core.
|
|
4
|
+
import { clean, escape, lum, mix, n2, readColour } from "./emblem.core.js";
|
|
5
|
+
const FONT = "system-ui,-apple-system,'Segoe UI','Hiragino Sans','Noto Sans JP','Yu Gothic',sans-serif";
|
|
6
|
+
/** The width of one character in em: a guess that holds for the usual sans-serif fonts, whole for Japanese and the wide forms. */
|
|
7
|
+
export function advance(ch) {
|
|
8
|
+
const c = ch.codePointAt(0);
|
|
9
|
+
if (c >= 0x2e80 && c <= 0xffef && !(c >= 0xff61 && c <= 0xff9f))
|
|
10
|
+
return 1.02;
|
|
11
|
+
if (ch === " ")
|
|
12
|
+
return 0.3;
|
|
13
|
+
if (/[iljI.,:;'!|]/.test(ch))
|
|
14
|
+
return 0.34;
|
|
15
|
+
if (/[mwMW]/.test(ch))
|
|
16
|
+
return 0.92;
|
|
17
|
+
if (/[A-Z]/.test(ch))
|
|
18
|
+
return 0.72;
|
|
19
|
+
if (/[0-9]/.test(ch))
|
|
20
|
+
return 0.62;
|
|
21
|
+
return 0.58;
|
|
22
|
+
}
|
|
23
|
+
/** The width of a text in em. */
|
|
24
|
+
export const widthOf = (text) => [...text].reduce((sum, ch) => sum + advance(ch), 0);
|
|
25
|
+
/** The contrast ratio of two colours, 1 to 21. */
|
|
26
|
+
export const contrastOf = (a, b) => (Math.max(lum(a), lum(b)) + 0.05) / (Math.min(lum(a), lum(b)) + 0.05);
|
|
27
|
+
/** 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). */
|
|
28
|
+
export function inkFor(main) {
|
|
29
|
+
const dark = lum(main) > 0.18;
|
|
30
|
+
const list = dark ? [mix(main, -0.72), "#000000", mix(main, 0.9), "#ffffff"] : [mix(main, 0.9), "#ffffff", mix(main, -0.72), "#000000"];
|
|
31
|
+
const scored = list.map((c) => [c, contrastOf(readColour(c), main)]);
|
|
32
|
+
return (scored.find((s) => s[1] >= 4.5) ?? scored.reduce((a, b) => (b[1] > a[1] ? b : a)))[0];
|
|
33
|
+
}
|
|
34
|
+
/** 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. */
|
|
35
|
+
export function linesOf(e, place) {
|
|
36
|
+
const yearApart = place?.yearApart === true;
|
|
37
|
+
if (e.free !== undefined) {
|
|
38
|
+
const rows = e.free.split("\n");
|
|
39
|
+
if (rows.length > 1)
|
|
40
|
+
return { main: [rows.map((text, i) => ({ text, weight: i === 0 ? 1 : i === 1 ? 0.62 : 0.5 }))], year: undefined };
|
|
41
|
+
const words = (e.free.match(/\S+/g) ?? [e.free]);
|
|
42
|
+
const cut = words.slice(1).map((_, i) => [words.slice(0, i + 1).join(" "), words.slice(i + 1).join(" ")]);
|
|
43
|
+
const one = [{ text: e.free, weight: 1 }];
|
|
44
|
+
return { main: [one, ...cut.map((pair) => pair.map((text, i) => ({ text, weight: i === 0 ? 1 : 0.62 })))], year: undefined };
|
|
45
|
+
}
|
|
46
|
+
const rank = e.rank ?? place?.rank;
|
|
47
|
+
const lines = [];
|
|
48
|
+
const big = rank !== undefined;
|
|
49
|
+
if (rank !== undefined)
|
|
50
|
+
lines.push({ text: rank, weight: 1 });
|
|
51
|
+
if (e.label !== undefined)
|
|
52
|
+
lines.push({ text: e.label, weight: big ? 0.5 : 1 });
|
|
53
|
+
if (e.year !== undefined && !yearApart)
|
|
54
|
+
lines.push({ text: e.year, weight: big ? 0.42 : 0.6 });
|
|
55
|
+
return { main: lines.length === 0 ? [] : [lines], year: yearApart ? e.year : undefined };
|
|
56
|
+
}
|
|
57
|
+
/** Lines fitted to a plate: each line's size and baseline, and the smallest size (the score of the fit). */
|
|
58
|
+
function fit(lines, p) {
|
|
59
|
+
const lh = 1.14;
|
|
60
|
+
const unit = p.h / lines.reduce((sum, l) => sum + l.weight * lh, 0);
|
|
61
|
+
const sizes = lines.map((l) => Math.min(unit * l.weight, (p.w * 0.96) / Math.max(widthOf(l.text), 0.4)));
|
|
62
|
+
const total = sizes.reduce((sum, s) => sum + s * lh, 0);
|
|
63
|
+
let top = p.y - total / 2;
|
|
64
|
+
const rows = lines.map((l, i) => {
|
|
65
|
+
const size = sizes[i];
|
|
66
|
+
const y = top + (size * lh) / 2 + size * 0.36;
|
|
67
|
+
top += size * lh;
|
|
68
|
+
return { text: l.text, size, y, width: widthOf(l.text) * size };
|
|
69
|
+
});
|
|
70
|
+
return { rows, score: Math.min(...sizes) };
|
|
71
|
+
}
|
|
72
|
+
/** The ink of an engraving: the page's ink in the line style, else the colour that reads on the metal. */
|
|
73
|
+
export const engravingInk = (style, main) => (style === "line" ? "currentColor" : inkFor(main));
|
|
74
|
+
/** 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. */
|
|
75
|
+
export function plateMarkup(candidates, plate, main, style) {
|
|
76
|
+
const fits = candidates.filter((c) => c.length > 0).map((c) => fit(c, plate));
|
|
77
|
+
if (fits.length === 0)
|
|
78
|
+
return "";
|
|
79
|
+
const best = fits.reduce((a, b) => (b.score > a.score + 0.01 ? b : a));
|
|
80
|
+
const text = (dx) => best.rows
|
|
81
|
+
.map((r) => `<text x="${n2(plate.x + dx)}" y="${n2(r.y + dx)}" font-size="${n2(r.size)}" textLength="${n2(r.width)}" lengthAdjust="spacingAndGlyphs">${escape(r.text)}</text>`)
|
|
82
|
+
.join("");
|
|
83
|
+
const head = `font-family="${FONT}" font-weight="800" text-anchor="middle" stroke="none"`;
|
|
84
|
+
const glint = style === "metal" ? `<g ${head} fill="${mix(main, 0.7)}" fill-opacity=".7">${text(0.3)}</g>` : "";
|
|
85
|
+
const marks = `${glint}<g ${head} fill="${engravingInk(style, main)}">${text(0)}</g>`;
|
|
86
|
+
return plate.rot === undefined ? marks : `<g transform="rotate(${plate.rot} ${n2(plate.x)} ${n2(plate.y)})">${marks}</g>`;
|
|
87
|
+
}
|
|
88
|
+
/** 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. */
|
|
89
|
+
export function cylinderText(text, radius, height, ink, angle, centre, place) {
|
|
90
|
+
const em = Math.max(widthOf(text), 0.4);
|
|
91
|
+
const size = Math.min(height * 0.8, (radius * 1.9) / em);
|
|
92
|
+
const letters = [...text];
|
|
93
|
+
let run = -(em * size) / 2;
|
|
94
|
+
let out = "";
|
|
95
|
+
for (const ch of letters) {
|
|
96
|
+
const w = advance(ch) * size;
|
|
97
|
+
const deg = centre + angle + ((run + w / 2) / radius) * (180 / Math.PI);
|
|
98
|
+
run += w;
|
|
99
|
+
if (ch === " ")
|
|
100
|
+
continue;
|
|
101
|
+
const [x, y, depth] = place(deg);
|
|
102
|
+
if (depth < 0.14)
|
|
103
|
+
continue;
|
|
104
|
+
out += `<text transform="translate(${n2(x)} ${n2(y + size * 0.36)}) scale(${n2(depth)} 1)">${escape(ch)}</text>`;
|
|
105
|
+
}
|
|
106
|
+
return out === "" ? "" : `<g font-family="${FONT}" font-weight="800" text-anchor="middle" font-size="${n2(size)}" fill="${ink}" fill-opacity=".92" stroke="none">${out}</g>`;
|
|
107
|
+
}
|
|
108
|
+
/** 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. */
|
|
109
|
+
export function sideText(text, chord, height, ink, a, b, depth) {
|
|
110
|
+
if (depth < 0.14)
|
|
111
|
+
return "";
|
|
112
|
+
const em = Math.max(widthOf(text), 0.4);
|
|
113
|
+
const size = Math.min(height * 0.8, (chord * 0.8) / em);
|
|
114
|
+
const slope = (b[1] - a[1]) / (b[0] - a[0] || 1);
|
|
115
|
+
let run = -(em * size) / 2;
|
|
116
|
+
let out = "";
|
|
117
|
+
for (const ch of text) {
|
|
118
|
+
const w = advance(ch) * size;
|
|
119
|
+
const u = 0.5 + (run + w / 2) / chord;
|
|
120
|
+
run += w;
|
|
121
|
+
if (ch === " ")
|
|
122
|
+
continue;
|
|
123
|
+
out += `<text transform="matrix(${n2(depth)} ${n2(slope * depth)} 0 1 ${n2(a[0] + (b[0] - a[0]) * u)} ${n2(a[1] + (b[1] - a[1]) * u + size * 0.36)})">${escape(ch)}</text>`;
|
|
124
|
+
}
|
|
125
|
+
return `<g font-family="${FONT}" font-weight="800" text-anchor="middle" font-size="${n2(size)}" fill="${ink}" fill-opacity=".92" stroke="none">${out}</g>`;
|
|
126
|
+
}
|
|
127
|
+
/** 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. */
|
|
128
|
+
export const flatText = (e) => (e.free ?? [e.rank, e.label, e.year].filter((part) => part !== undefined).join(" ")).replace(/\n/g, " ");
|
|
129
|
+
/** 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. */
|
|
130
|
+
export function engravingOf(o) {
|
|
131
|
+
const e = {};
|
|
132
|
+
const free = clean(o?.engrave, 3);
|
|
133
|
+
const [label, year, rank, back] = [clean(o?.label), clean(o?.year), clean(o?.rank), clean(o?.engraveBack, 3)];
|
|
134
|
+
if (free !== undefined)
|
|
135
|
+
e.free = free;
|
|
136
|
+
if (label !== undefined)
|
|
137
|
+
e.label = label;
|
|
138
|
+
if (year !== undefined)
|
|
139
|
+
e.year = year;
|
|
140
|
+
if (rank !== undefined)
|
|
141
|
+
e.rank = rank;
|
|
142
|
+
if (back !== undefined)
|
|
143
|
+
e.back = back;
|
|
144
|
+
return e;
|
|
145
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { BallId, GearId, MedalId, SportName, TrophyId } from "./emblem.ids.ts";
|
|
2
|
+
/** The equipment of one sport. */
|
|
3
|
+
export interface SportEquipment {
|
|
4
|
+
sport: SportName;
|
|
5
|
+
/** The ball, or the implement that stands for one (the puck, the shuttlecock, the shot). Swimming, diving, artistic swimming, skating and the Japanese sports (sumo, kendo, judo, kyudo, karate) have none. */
|
|
6
|
+
ball?: BallId;
|
|
7
|
+
/** The gear the sport has an emblem for, most telling first. Empty where the package has none yet. */
|
|
8
|
+
gear: GearId[];
|
|
9
|
+
/** The trophy that stands for the sport's cup: its own where the package draws one, otherwise a plain cup, bowl or globe. */
|
|
10
|
+
trophy: TrophyId;
|
|
11
|
+
/** The medal of the sport's champion, for a sport in the Games. */
|
|
12
|
+
medal?: MedalId;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* What a sport is played and won with, as ids of the registry: `sportEquipment("nhl")` is the puck, a stick, a skate, a goalie's mask and a net, and the hockey cup. The arrays are new each call. An unknown sport is an error.
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { sportEquipment } from "@johnmorrisdotca/dougu-sports/emblems";
|
|
19
|
+
* const nhl = sportEquipment("nhl");
|
|
20
|
+
* console.log(nhl.ball, nhl.trophy);
|
|
21
|
+
* // ball:puck trophy:hockey-cup
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function sportEquipment(sport: SportName): SportEquipment;
|