@johnmorrisdotca/dougu-sports 0.0.1 → 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.
Files changed (338) hide show
  1. package/CHANGELOG.md +61 -1
  2. package/LICENSES/README.md +4 -3
  3. package/NOTICE.md +54 -14
  4. package/README.md +251 -80
  5. package/dist/emblems/award/badge.d.ts +2 -0
  6. package/dist/emblems/award/badge.js +14 -0
  7. package/dist/emblems/award/crown.d.ts +2 -0
  8. package/dist/emblems/award/crown.js +15 -0
  9. package/dist/emblems/award/medal.d.ts +1 -1
  10. package/dist/emblems/award/medal.js +10 -15
  11. package/dist/emblems/award/shield.d.ts +2 -0
  12. package/dist/emblems/award/shield.js +11 -0
  13. package/dist/emblems/award/star.d.ts +2 -0
  14. package/dist/emblems/award/star.js +11 -0
  15. package/dist/emblems/ball/ball.afl.d.ts +1 -0
  16. package/dist/emblems/ball/ball.afl.js +24 -0
  17. package/dist/emblems/ball/ball.baseball.d.ts +1 -0
  18. package/dist/emblems/ball/ball.baseball.js +14 -0
  19. package/dist/emblems/ball/ball.basketball.d.ts +7 -0
  20. package/dist/emblems/ball/ball.basketball.js +32 -0
  21. package/dist/emblems/ball/ball.basketball3x3.d.ts +1 -0
  22. package/dist/emblems/ball/ball.basketball3x3.js +10 -0
  23. package/dist/emblems/ball/ball.basketball6.d.ts +1 -0
  24. package/dist/emblems/ball/ball.basketball6.js +10 -0
  25. package/dist/emblems/ball/ball.core.d.ts +111 -0
  26. package/dist/emblems/ball/ball.core.js +79 -0
  27. package/dist/emblems/ball/ball.cricket.d.ts +5 -0
  28. package/dist/emblems/ball/ball.cricket.js +31 -0
  29. package/dist/emblems/ball/ball.curlingstone.d.ts +3 -0
  30. package/dist/emblems/ball/ball.curlingstone.js +46 -0
  31. package/dist/emblems/ball/ball.discus.d.ts +1 -0
  32. package/dist/emblems/ball/ball.discus.js +24 -0
  33. package/dist/emblems/ball/ball.engine.d.ts +114 -0
  34. package/dist/emblems/ball/ball.engine.js +342 -0
  35. package/dist/emblems/ball/ball.family.dotted.d.ts +27 -0
  36. package/dist/emblems/ball/ball.family.dotted.js +20 -0
  37. package/dist/emblems/ball/ball.family.panelled.d.ts +7 -0
  38. package/dist/emblems/ball/ball.family.panelled.js +60 -0
  39. package/dist/emblems/ball/ball.family.prolate.d.ts +16 -0
  40. package/dist/emblems/ball/ball.family.prolate.js +29 -0
  41. package/dist/emblems/ball/ball.family.seamed.d.ts +31 -0
  42. package/dist/emblems/ball/ball.family.seamed.js +26 -0
  43. package/dist/emblems/ball/ball.fieldhockey.d.ts +1 -0
  44. package/dist/emblems/ball/ball.fieldhockey.js +15 -0
  45. package/dist/emblems/ball/ball.football.d.ts +3 -0
  46. package/dist/emblems/ball/ball.football.js +51 -0
  47. package/dist/emblems/ball/ball.gaelic.d.ts +2 -0
  48. package/dist/emblems/ball/ball.gaelic.js +30 -0
  49. package/dist/emblems/ball/ball.golf.d.ts +1 -0
  50. package/dist/emblems/ball/ball.golf.js +15 -0
  51. package/dist/emblems/ball/ball.hammer.d.ts +1 -0
  52. package/dist/emblems/ball/ball.hammer.js +31 -0
  53. package/dist/emblems/ball/ball.handball.d.ts +1 -0
  54. package/dist/emblems/ball/ball.handball.js +25 -0
  55. package/dist/emblems/ball/ball.index.d.ts +48 -0
  56. package/dist/emblems/ball/ball.index.js +103 -0
  57. package/dist/emblems/ball/ball.math.d.ts +38 -0
  58. package/dist/emblems/ball/ball.math.js +105 -0
  59. package/dist/emblems/ball/ball.object.d.ts +38 -0
  60. package/dist/emblems/ball/ball.object.js +20 -0
  61. package/dist/emblems/ball/ball.padel.d.ts +1 -0
  62. package/dist/emblems/ball/ball.padel.js +13 -0
  63. package/dist/emblems/ball/ball.pickleball.d.ts +2 -0
  64. package/dist/emblems/ball/ball.pickleball.js +26 -0
  65. package/dist/emblems/ball/ball.polyhedra.d.ts +15 -0
  66. package/dist/emblems/ball/ball.polyhedra.js +63 -0
  67. package/dist/emblems/ball/ball.puck.d.ts +3 -0
  68. package/dist/emblems/ball/ball.puck.js +22 -0
  69. package/dist/emblems/ball/ball.roundnet.d.ts +1 -0
  70. package/dist/emblems/ball/ball.roundnet.js +15 -0
  71. package/dist/emblems/ball/ball.rugby.d.ts +1 -0
  72. package/dist/emblems/ball/ball.rugby.js +35 -0
  73. package/dist/emblems/ball/ball.sepaktakraw.d.ts +1 -0
  74. package/dist/emblems/ball/ball.sepaktakraw.js +48 -0
  75. package/dist/emblems/ball/ball.shuttlecock.d.ts +6 -0
  76. package/dist/emblems/ball/ball.shuttlecock.js +93 -0
  77. package/dist/emblems/ball/ball.sliotar.d.ts +1 -0
  78. package/dist/emblems/ball/ball.sliotar.js +14 -0
  79. package/dist/emblems/ball/ball.smooth.d.ts +4 -0
  80. package/dist/emblems/ball/ball.smooth.js +51 -0
  81. package/dist/emblems/ball/ball.soccer.d.ts +2 -0
  82. package/dist/emblems/ball/ball.soccer.js +54 -0
  83. package/dist/emblems/ball/ball.softball.d.ts +1 -0
  84. package/dist/emblems/ball/ball.softball.js +14 -0
  85. package/dist/emblems/ball/ball.squash.d.ts +7 -0
  86. package/dist/emblems/ball/ball.squash.js +27 -0
  87. package/dist/emblems/ball/ball.stack.d.ts +48 -0
  88. package/dist/emblems/ball/ball.stack.js +127 -0
  89. package/dist/emblems/ball/ball.stitch.d.ts +10 -0
  90. package/dist/emblems/ball/ball.stitch.js +59 -0
  91. package/dist/emblems/ball/ball.tennis.d.ts +1 -0
  92. package/dist/emblems/ball/ball.tennis.js +13 -0
  93. package/dist/emblems/ball/ball.teqball.d.ts +1 -0
  94. package/dist/emblems/ball/ball.teqball.js +27 -0
  95. package/dist/emblems/ball/ball.volleyball.d.ts +3 -0
  96. package/dist/emblems/ball/ball.volleyball.js +56 -0
  97. package/dist/emblems/ball/ball.waterpolo.d.ts +1 -0
  98. package/dist/emblems/ball/ball.waterpolo.js +31 -0
  99. package/dist/emblems/crest/diamond.d.ts +2 -0
  100. package/dist/emblems/crest/diamond.js +18 -0
  101. package/dist/emblems/crest/pennant.d.ts +2 -0
  102. package/dist/emblems/crest/pennant.js +18 -0
  103. package/dist/emblems/crest/roundel.d.ts +2 -0
  104. package/dist/emblems/crest/roundel.js +18 -0
  105. package/dist/emblems/crest/shield.d.ts +2 -0
  106. package/dist/emblems/crest/shield.js +17 -0
  107. package/dist/emblems/emblem.api.d.ts +77 -9
  108. package/dist/emblems/emblem.api.js +56 -10
  109. package/dist/emblems/emblem.compose.d.ts +31 -0
  110. package/dist/emblems/emblem.compose.js +91 -0
  111. package/dist/emblems/emblem.core.d.ts +89 -7
  112. package/dist/emblems/emblem.core.js +85 -11
  113. package/dist/emblems/emblem.descriptions.d.ts +7 -0
  114. package/dist/emblems/emblem.descriptions.js +60 -0
  115. package/dist/emblems/emblem.display.d.ts +75 -0
  116. package/dist/emblems/emblem.display.js +214 -0
  117. package/dist/emblems/emblem.engrave.d.ts +50 -0
  118. package/dist/emblems/emblem.engrave.js +145 -0
  119. package/dist/emblems/emblem.equipment.d.ts +24 -0
  120. package/dist/emblems/emblem.equipment.js +57 -0
  121. package/dist/emblems/emblem.flip.d.ts +10 -0
  122. package/dist/emblems/emblem.flip.js +45 -0
  123. package/dist/emblems/emblem.ids.d.ts +44 -30
  124. package/dist/emblems/emblem.ids.js +51 -17
  125. package/dist/emblems/emblem.lathe.d.ts +62 -0
  126. package/dist/emblems/emblem.lathe.js +426 -0
  127. package/dist/emblems/emblem.medal.d.ts +6 -0
  128. package/dist/emblems/emblem.medal.js +38 -0
  129. package/dist/emblems/emblem.pennant.d.ts +3 -0
  130. package/dist/emblems/emblem.pennant.js +79 -0
  131. package/dist/emblems/emblem.registry.js +162 -4
  132. package/dist/emblems/emblem.shapes.d.ts +48 -12
  133. package/dist/emblems/emblem.shapes.js +138 -31
  134. package/dist/emblems/emblem.sprite.d.ts +38 -0
  135. package/dist/emblems/emblem.sprite.js +65 -0
  136. package/dist/emblems/emblem.uri.d.ts +22 -0
  137. package/dist/emblems/emblem.uri.js +29 -0
  138. package/dist/emblems/gear/gear.badmintonracket.d.ts +1 -0
  139. package/dist/emblems/gear/gear.badmintonracket.js +19 -0
  140. package/dist/emblems/gear/gear.baseballglove.d.ts +1 -0
  141. package/dist/emblems/gear/gear.baseballglove.js +21 -0
  142. package/dist/emblems/gear/gear.budo.d.ts +17 -0
  143. package/dist/emblems/gear/gear.budo.js +60 -0
  144. package/dist/emblems/gear/gear.core.d.ts +64 -0
  145. package/dist/emblems/gear/gear.core.js +127 -0
  146. package/dist/emblems/gear/gear.cricketbat.d.ts +1 -0
  147. package/dist/emblems/gear/gear.cricketbat.js +17 -0
  148. package/dist/emblems/gear/gear.field.d.ts +6 -0
  149. package/dist/emblems/gear/gear.field.js +81 -0
  150. package/dist/emblems/gear/gear.fieldhockeystick.d.ts +1 -0
  151. package/dist/emblems/gear/gear.fieldhockeystick.js +16 -0
  152. package/dist/emblems/gear/gear.footballhelmet.d.ts +1 -0
  153. package/dist/emblems/gear/gear.footballhelmet.js +16 -0
  154. package/dist/emblems/gear/gear.goaliemask.d.ts +1 -0
  155. package/dist/emblems/gear/gear.goaliemask.js +23 -0
  156. package/dist/emblems/gear/gear.goalnet.d.ts +1 -0
  157. package/dist/emblems/gear/gear.goalnet.js +25 -0
  158. package/dist/emblems/gear/gear.hockeystick.d.ts +1 -0
  159. package/dist/emblems/gear/gear.hockeystick.js +15 -0
  160. package/dist/emblems/gear/gear.index.d.ts +22 -0
  161. package/dist/emblems/gear/gear.index.js +49 -0
  162. package/dist/emblems/gear/gear.kendo.d.ts +2 -0
  163. package/dist/emblems/gear/gear.kendo.js +41 -0
  164. package/dist/emblems/gear/gear.kyudo.d.ts +2 -0
  165. package/dist/emblems/gear/gear.kyudo.js +31 -0
  166. package/dist/emblems/gear/gear.lacrossestick.d.ts +1 -0
  167. package/dist/emblems/gear/gear.lacrossestick.js +26 -0
  168. package/dist/emblems/gear/gear.rackets.d.ts +4 -0
  169. package/dist/emblems/gear/gear.rackets.js +60 -0
  170. package/dist/emblems/gear/gear.skates.d.ts +3 -0
  171. package/dist/emblems/gear/gear.skates.js +39 -0
  172. package/dist/emblems/gear/gear.sticks.d.ts +3 -0
  173. package/dist/emblems/gear/gear.sticks.js +34 -0
  174. package/dist/emblems/gear/gear.sumo.d.ts +2 -0
  175. package/dist/emblems/gear/gear.sumo.js +43 -0
  176. package/dist/emblems/gear/gear.tabletennispaddle.d.ts +1 -0
  177. package/dist/emblems/gear/gear.tabletennispaddle.js +16 -0
  178. package/dist/emblems/gear/gear.tennisracket.d.ts +1 -0
  179. package/dist/emblems/gear/gear.tennisracket.js +17 -0
  180. package/dist/emblems/gear/gear.track.d.ts +4 -0
  181. package/dist/emblems/gear/gear.track.js +45 -0
  182. package/dist/emblems/gear/gear.water.d.ts +2 -0
  183. package/dist/emblems/gear/gear.water.js +25 -0
  184. package/dist/emblems/medal/bronze.d.ts +2 -0
  185. package/dist/emblems/medal/bronze.js +4 -0
  186. package/dist/emblems/medal/gold.d.ts +2 -0
  187. package/dist/emblems/medal/gold.js +4 -0
  188. package/dist/emblems/medal/place-1.d.ts +2 -0
  189. package/dist/emblems/medal/place-1.js +4 -0
  190. package/dist/emblems/medal/place-2.d.ts +2 -0
  191. package/dist/emblems/medal/place-2.js +4 -0
  192. package/dist/emblems/medal/place-3.d.ts +2 -0
  193. package/dist/emblems/medal/place-3.js +4 -0
  194. package/dist/emblems/medal/silver.d.ts +2 -0
  195. package/dist/emblems/medal/silver.js +4 -0
  196. package/dist/emblems/pennant/championship.d.ts +2 -0
  197. package/dist/emblems/pennant/championship.js +4 -0
  198. package/dist/emblems/sport/afl.d.ts +2 -0
  199. package/dist/emblems/sport/afl.js +12 -0
  200. package/dist/emblems/sport/artisticswimming.d.ts +2 -0
  201. package/dist/emblems/sport/artisticswimming.js +16 -0
  202. package/dist/emblems/sport/athletics.d.ts +2 -0
  203. package/dist/emblems/sport/athletics.js +12 -0
  204. package/dist/emblems/sport/badminton.d.ts +2 -0
  205. package/dist/emblems/sport/badminton.js +13 -0
  206. package/dist/emblems/sport/cfl.d.ts +2 -0
  207. package/dist/emblems/sport/cfl.js +5 -0
  208. package/dist/emblems/sport/cricket.d.ts +2 -0
  209. package/dist/emblems/sport/cricket.js +14 -0
  210. package/dist/emblems/sport/curling.d.ts +2 -0
  211. package/dist/emblems/sport/curling.js +14 -0
  212. package/dist/emblems/sport/diving.d.ts +2 -0
  213. package/dist/emblems/sport/diving.js +18 -0
  214. package/dist/emblems/sport/fieldhockey.d.ts +2 -0
  215. package/dist/emblems/sport/fieldhockey.js +11 -0
  216. package/dist/emblems/sport/gaelic.d.ts +2 -0
  217. package/dist/emblems/sport/gaelic.js +11 -0
  218. package/dist/emblems/sport/handball.d.ts +2 -0
  219. package/dist/emblems/sport/handball.js +15 -0
  220. package/dist/emblems/sport/ifaf.d.ts +2 -0
  221. package/dist/emblems/sport/ifaf.js +5 -0
  222. package/dist/emblems/sport/judo.d.ts +2 -0
  223. package/dist/emblems/sport/judo.js +13 -0
  224. package/dist/emblems/sport/karate.d.ts +2 -0
  225. package/dist/emblems/sport/karate.js +13 -0
  226. package/dist/emblems/sport/kendo.d.ts +2 -0
  227. package/dist/emblems/sport/kendo.js +14 -0
  228. package/dist/emblems/sport/kyudo.d.ts +2 -0
  229. package/dist/emblems/sport/kyudo.js +15 -0
  230. package/dist/emblems/sport/lacrosse.d.ts +2 -0
  231. package/dist/emblems/sport/lacrosse.js +13 -0
  232. package/dist/emblems/sport/mlb.d.ts +2 -0
  233. package/dist/emblems/sport/mlb.js +28 -0
  234. package/dist/emblems/sport/nba.d.ts +2 -0
  235. package/dist/emblems/sport/nba.js +12 -0
  236. package/dist/emblems/sport/netball.d.ts +2 -0
  237. package/dist/emblems/sport/netball.js +14 -0
  238. package/dist/emblems/sport/nfl.d.ts +3 -0
  239. package/dist/emblems/sport/nfl.js +15 -0
  240. package/dist/emblems/sport/nhl.d.ts +2 -0
  241. package/dist/emblems/sport/nhl.js +13 -0
  242. package/dist/emblems/sport/padel.d.ts +2 -0
  243. package/dist/emblems/sport/padel.js +14 -0
  244. package/dist/emblems/sport/pickleball.d.ts +2 -0
  245. package/dist/emblems/sport/pickleball.js +13 -0
  246. package/dist/emblems/sport/racquetball.d.ts +2 -0
  247. package/dist/emblems/sport/racquetball.js +13 -0
  248. package/dist/emblems/sport/roundnet.d.ts +2 -0
  249. package/dist/emblems/sport/roundnet.js +15 -0
  250. package/dist/emblems/sport/rugby.d.ts +2 -0
  251. package/dist/emblems/sport/rugby.js +12 -0
  252. package/dist/emblems/sport/sepaktakraw.d.ts +2 -0
  253. package/dist/emblems/sport/sepaktakraw.js +11 -0
  254. package/dist/emblems/sport/skating.d.ts +2 -0
  255. package/dist/emblems/sport/skating.js +14 -0
  256. package/dist/emblems/sport/soccer.d.ts +2 -0
  257. package/dist/emblems/sport/soccer.js +31 -0
  258. package/dist/emblems/sport/squash.d.ts +2 -0
  259. package/dist/emblems/sport/squash.js +15 -0
  260. package/dist/emblems/sport/sumo.d.ts +2 -0
  261. package/dist/emblems/sport/sumo.js +15 -0
  262. package/dist/emblems/sport/swimming.d.ts +2 -0
  263. package/dist/emblems/sport/swimming.js +15 -0
  264. package/dist/emblems/sport/tabletennis.d.ts +2 -0
  265. package/dist/emblems/sport/tabletennis.js +12 -0
  266. package/dist/emblems/sport/tennis.d.ts +2 -0
  267. package/dist/emblems/sport/tennis.js +15 -0
  268. package/dist/emblems/sport/teqball.d.ts +2 -0
  269. package/dist/emblems/sport/teqball.js +15 -0
  270. package/dist/emblems/sport/volleyball.d.ts +2 -0
  271. package/dist/emblems/sport/volleyball.js +25 -0
  272. package/dist/emblems/sport/waterpolo.d.ts +2 -0
  273. package/dist/emblems/sport/waterpolo.js +15 -0
  274. package/dist/emblems/trophy/banner.d.ts +2 -0
  275. package/dist/emblems/trophy/banner.js +20 -0
  276. package/dist/emblems/trophy/baseball.d.ts +2 -0
  277. package/dist/emblems/trophy/baseball.js +15 -0
  278. package/dist/emblems/trophy/basketball.d.ts +2 -0
  279. package/dist/emblems/trophy/basketball.js +17 -0
  280. package/dist/emblems/trophy/belt.d.ts +2 -0
  281. package/dist/emblems/trophy/belt.js +17 -0
  282. package/dist/emblems/trophy/bowl.d.ts +2 -0
  283. package/dist/emblems/trophy/bowl.js +8 -0
  284. package/dist/emblems/trophy/canadian-bowl.d.ts +2 -0
  285. package/dist/emblems/trophy/canadian-bowl.js +23 -0
  286. package/dist/emblems/trophy/championship-banner.d.ts +2 -0
  287. package/dist/emblems/trophy/championship-banner.js +67 -0
  288. package/dist/emblems/trophy/cricket-urn.d.ts +2 -0
  289. package/dist/emblems/trophy/cricket-urn.js +23 -0
  290. package/dist/emblems/trophy/cup.d.ts +2 -0
  291. package/dist/emblems/trophy/cup.js +16 -0
  292. package/dist/emblems/trophy/football.d.ts +2 -0
  293. package/dist/emblems/trophy/football.js +11 -0
  294. package/dist/emblems/trophy/globe.d.ts +2 -0
  295. package/dist/emblems/trophy/globe.js +9 -0
  296. package/dist/emblems/trophy/hockey-cup.d.ts +2 -0
  297. package/dist/emblems/trophy/hockey-cup.js +22 -0
  298. package/dist/emblems/trophy/laurel.d.ts +2 -0
  299. package/dist/emblems/trophy/laurel.js +40 -0
  300. package/dist/emblems/trophy/lidded-cup.d.ts +2 -0
  301. package/dist/emblems/trophy/lidded-cup.js +21 -0
  302. package/dist/emblems/trophy/plaque.d.ts +2 -0
  303. package/dist/emblems/trophy/plaque.js +17 -0
  304. package/dist/emblems/trophy/ring.d.ts +2 -0
  305. package/dist/emblems/trophy/ring.js +30 -0
  306. package/dist/emblems/trophy/rosette.d.ts +2 -0
  307. package/dist/emblems/trophy/rosette.js +27 -0
  308. package/dist/emblems/trophy/rugby-cup.d.ts +2 -0
  309. package/dist/emblems/trophy/rugby-cup.js +23 -0
  310. package/dist/emblems/trophy/salver.d.ts +2 -0
  311. package/dist/emblems/trophy/salver.js +9 -0
  312. package/dist/emblems/trophy/soccer-ball.d.ts +2 -0
  313. package/dist/emblems/trophy/soccer-ball.js +9 -0
  314. package/dist/emblems/trophy/soccer-globe.d.ts +2 -0
  315. package/dist/emblems/trophy/soccer-globe.js +18 -0
  316. package/dist/emblems/trophy/sumo-cup.d.ts +2 -0
  317. package/dist/emblems/trophy/sumo-cup.js +28 -0
  318. package/dist/emblems/trophy/tennis-cup.d.ts +2 -0
  319. package/dist/emblems/trophy/tennis-cup.js +24 -0
  320. package/dist/emblems/trophy/tennis-plate.d.ts +2 -0
  321. package/dist/emblems/trophy/tennis-plate.js +16 -0
  322. package/dist/emblems-entry.d.ts +20 -7
  323. package/dist/emblems-entry.js +8 -2
  324. package/dist/index.d.ts +131 -1
  325. package/dist/index.js +131 -3
  326. package/dist/turn-control.d.ts +101 -0
  327. package/dist/turn-control.js +363 -0
  328. package/dist/turn-entry.d.ts +2 -0
  329. package/dist/turn-entry.js +1 -0
  330. package/dist/version.d.ts +1 -1
  331. package/dist/version.js +1 -1
  332. package/package.json +33 -7
  333. package/LICENSES/Apache-2.0.txt +0 -55
  334. package/LICENSES/CC-BY-3.0.md +0 -15
  335. package/dist/emblems/ball/soccer.d.ts +0 -2
  336. package/dist/emblems/ball/soccer.js +0 -9
  337. package/dist/emblems/emblem.ball.d.ts +0 -28
  338. package/dist/emblems/emblem.ball.js +0 -70
@@ -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;
@@ -0,0 +1,57 @@
1
+ const GOLD = "medal:gold";
2
+ const TABLE = {
3
+ nfl: ["ball:football", ["gear:football-helmet", "gear:goalposts"], "trophy:football"],
4
+ cfl: ["ball:football-cfl", ["gear:football-helmet", "gear:goalposts"], "trophy:canadian-bowl"],
5
+ ifaf: ["ball:football-ifaf", ["gear:football-helmet", "gear:goalposts"], "trophy:cup"],
6
+ nhl: ["ball:puck", ["gear:hockey-stick", "gear:hockey-skate", "gear:goalie-mask", "gear:goal-net"], "trophy:hockey-cup"],
7
+ nba: ["ball:basketball", ["gear:basketball-hoop"], "trophy:basketball"],
8
+ mlb: ["ball:baseball", ["gear:baseball-bat", "gear:baseball-glove"], "trophy:baseball"],
9
+ soccer: ["ball:soccer", ["gear:goal-net"], "trophy:soccer-globe", GOLD],
10
+ curling: ["ball:curling-stone", ["gear:curling-broom"], "trophy:bowl", GOLD],
11
+ tennis: ["ball:tennis", ["gear:tennis-racket"], "trophy:tennis-cup", GOLD],
12
+ volleyball: ["ball:volleyball", ["gear:volleyball-net"], "trophy:cup", GOLD],
13
+ badminton: ["ball:shuttlecock-feather", ["gear:badminton-racket"], "trophy:cup", GOLD],
14
+ pickleball: ["ball:pickleball-outdoor", ["gear:pickleball-paddle"], "trophy:cup"],
15
+ tabletennis: ["ball:table-tennis", ["gear:table-tennis-paddle"], "trophy:cup", GOLD],
16
+ padel: ["ball:padel", ["gear:padel-racket"], "trophy:cup"],
17
+ squash: ["ball:squash", ["gear:squash-racket"], "trophy:cup", GOLD],
18
+ racquetball: ["ball:racquetball", ["gear:racquetball-racket"], "trophy:cup"],
19
+ sepaktakraw: ["ball:sepak-takraw", [], "trophy:cup"],
20
+ teqball: ["ball:teqball", [], "trophy:cup"],
21
+ roundnet: ["ball:roundnet", ["gear:roundnet-set"], "trophy:cup"],
22
+ netball: ["ball:netball", [], "trophy:cup"],
23
+ swimming: [undefined, ["gear:swim-goggles"], "trophy:cup", GOLD],
24
+ waterpolo: ["ball:water-polo", ["gear:swim-goggles"], "trophy:cup", GOLD],
25
+ diving: [undefined, ["gear:diving-board"], "trophy:cup", GOLD],
26
+ artisticswimming: [undefined, ["gear:swim-goggles"], "trophy:cup", GOLD],
27
+ lacrosse: ["ball:lacrosse", ["gear:lacrosse-stick"], "trophy:cup", GOLD],
28
+ rugby: ["ball:rugby", ["gear:goalposts"], "trophy:rugby-cup", GOLD],
29
+ afl: ["ball:afl", ["gear:goalposts"], "trophy:cup"],
30
+ gaelic: ["ball:sliotar", ["gear:hurley", "gear:goalposts"], "trophy:cup"],
31
+ cricket: ["ball:cricket-red", ["gear:cricket-bat", "gear:wicket"], "trophy:cricket-urn", GOLD],
32
+ fieldhockey: ["ball:field-hockey", ["gear:field-hockey-stick", "gear:goal-net"], "trophy:cup", GOLD],
33
+ handball: ["ball:handball", ["gear:goal-net"], "trophy:cup", GOLD],
34
+ athletics: ["ball:shot", ["gear:starting-blocks", "gear:relay-baton", "gear:javelin", "gear:hurdles"], "trophy:laurel", GOLD],
35
+ skating: [undefined, ["gear:figure-skate", "gear:speed-skate"], "trophy:cup", GOLD],
36
+ sumo: [undefined, ["gear:gunbai", "gear:mawashi"], "trophy:sumo-cup"],
37
+ kendo: [undefined, ["gear:shinai", "gear:kendo-men"], "trophy:cup"],
38
+ judo: [undefined, ["gear:judogi", "gear:obi"], "trophy:cup", GOLD],
39
+ kyudo: [undefined, ["gear:yumi", "gear:ya"], "trophy:cup"],
40
+ karate: [undefined, ["gear:karate-gi"], "trophy:cup", GOLD],
41
+ };
42
+ /**
43
+ * 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.
44
+ * @example
45
+ * ```ts
46
+ * import { sportEquipment } from "@johnmorrisdotca/dougu-sports/emblems";
47
+ * const nhl = sportEquipment("nhl");
48
+ * console.log(nhl.ball, nhl.trophy);
49
+ * // ball:puck trophy:hockey-cup
50
+ * ```
51
+ */
52
+ export function sportEquipment(sport) {
53
+ if (!Object.hasOwn(TABLE, sport))
54
+ throw new Error(`Unknown sport "${String(sport).slice(0, 40)}": the equipment is for the names in SPORT_NAMES.`);
55
+ const [ball, gear, trophy, medal] = TABLE[sport];
56
+ return { sport, ...(ball === undefined ? {} : { ball }), gear: [...gear], trophy, ...(medal === undefined ? {} : { medal }) };
57
+ }
@@ -0,0 +1,10 @@
1
+ /** What a turn needs. `line` is the line style (the rim is drawn in the page's ink), `back` is the document of the back view, and `thick` is the thickness as a fraction of the width. */
2
+ export interface FlipOptions {
3
+ yaw: number;
4
+ pitch: number;
5
+ line?: boolean;
6
+ back?: string;
7
+ thick?: number;
8
+ }
9
+ /** A document turned by `yaw` and `pitch` degrees from its still drawing. With no turn it is the document itself. */
10
+ export declare function flipSvg(svg: string, o: FlipOptions): string;
@@ -0,0 +1,45 @@
1
+ // The coin-flip turn for any flat drawing: it takes a finished document and returns it turned. The face is squeezed by the cosine of the yaw and shows its back past a quarter turn, and the rim
2
+ // (the thickness of the sheet) shows as a dark edge on the side that is turning away, widest when the drawing is edge on. A piece with no drawing of its own for the back (a bat, a racket, a pictogram)
3
+ // shows its front mirrored and a little darker, as the other side of a cut-out; a pennant passes the drawing of its back. A pure function of strings and numbers; it imports nothing.
4
+ const RAD = Math.PI / 180;
5
+ const n2 = (value) => String(Math.round(value * 100) / 100 + 0);
6
+ /** The parts of a document of SVG: its opening tag, its titles, and the rest. */
7
+ function split(svg) {
8
+ const open = /^<svg[^>]*>/.exec(svg)?.[0] ?? "";
9
+ const after = svg.slice(open.length, svg.length - "</svg>".length);
10
+ const head = /^<title>[\s\S]*?<\/title>(?:<desc>[\s\S]*?<\/desc>)?/.exec(after)?.[0] ?? "";
11
+ const box = (/viewBox="([^"]*)"/.exec(open)?.[1] ?? "0 0 64 64").split(/\s+/).map(Number);
12
+ return { open, head, rest: after.slice(head.length), box: [box[0] ?? 0, box[1] ?? 0, box[2] || 64, box[3] || 64] };
13
+ }
14
+ /** A document turned by `yaw` and `pitch` degrees from its still drawing. With no turn it is the document itself. */
15
+ export function flipSvg(svg, o) {
16
+ if (o.yaw === 0 && o.pitch === 0)
17
+ return svg;
18
+ const across = Math.cos(o.yaw * RAD);
19
+ const tilt = Math.cos(o.pitch * RAD);
20
+ const front = split(svg);
21
+ const showBack = across < 0;
22
+ const own = showBack && o.back !== undefined ? split(o.back) : undefined;
23
+ const face = own ?? front;
24
+ const [x0, y0, w, h] = front.box;
25
+ const sx = Math.max(Math.abs(across), 0.02);
26
+ const sy = Math.max(Math.abs(tilt), 0.02);
27
+ const thick = w * (o.thick ?? 0.035);
28
+ const dx = thick * Math.sin(o.yaw * RAD);
29
+ const dy = -thick * Math.sin(o.pitch * RAD) * 0.6;
30
+ const mirror = showBack && own === undefined;
31
+ const cx = x0 + w / 2;
32
+ const cy = y0 + h / 2;
33
+ // The turned face, then the filter that adds the rim behind it (and dims a mirrored back).
34
+ // The id names everything the definition depends on (its region comes from the box), so two drawings in one page that share an id share one filter.
35
+ const id = `dgf${o.line === true ? "l" : "c"}${mirror ? "b" : "f"}${Math.round(dx * 100)}_${Math.round(dy * 100)}_${[x0, y0, w, h].map((v) => Math.round(v)).join("_")}`;
36
+ const flood = o.line === true ? '<feFlood flood-color="currentColor" flood-opacity=".5"/>' : '<feFlood flood-color="#000" flood-opacity=".42"/>';
37
+ const steps = [1, 2, 3].map((k) => (Math.abs(dx) + Math.abs(dy) < 0.05 ? "" : `<feOffset in="s" dx="${n2((dx * k) / 3)}" dy="${n2((dy * k) / 3)}" result="o${k}"/>`)).join("");
38
+ const nodes = Math.abs(dx) + Math.abs(dy) < 0.05 ? "" : '<feMergeNode in="o3"/><feMergeNode in="o2"/><feMergeNode in="o1"/>';
39
+ const dim = mirror && o.line !== true ? '<feColorMatrix in="SourceGraphic" values=".8 0 0 0 0 0 .8 0 0 0 0 0 .8 0 0 0 0 0 1 0" result="d"/>' : "";
40
+ const filter = `<filter id="${id}" x="${n2(x0 - w * 0.15)}" y="${n2(y0 - h * 0.15)}" width="${n2(w * 1.3)}" height="${n2(h * 1.3)}" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">` +
41
+ `${flood}<feComposite in2="SourceAlpha" operator="in" result="s"/>${steps}${dim}<feMerge>${nodes}<feMergeNode in="${dim === "" ? "SourceGraphic" : "d"}"/></feMerge></filter>`;
42
+ const flip = mirror ? -1 : 1;
43
+ const group = `<g filter="url(#${id})"><g transform="translate(${n2(cx)} ${n2(cy)}) scale(${n2(sx * flip)} ${n2(sy)}) translate(${n2(-cx)} ${n2(-cy)})">${face.rest}</g></g>`;
44
+ return `${front.open}${front.head}<defs>${filter}</defs>${group}</svg>`;
45
+ }