@johnmorrisdotca/dougu-sports 0.0.1 → 0.1.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 (296) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/LICENSES/Apache-2.0.txt +202 -55
  3. package/LICENSES/CC-BY-3.0.txt +319 -0
  4. package/LICENSES/README.md +4 -3
  5. package/LICENSES/upstream/game-icons.license.txt +45 -0
  6. package/LICENSES/upstream/noto-emoji-2D-svg.LICENSE +13 -0
  7. package/LICENSES/upstream/pictogrammers-mdi.LICENSE +20 -0
  8. package/NOTICE.md +92 -3
  9. package/README.md +104 -69
  10. package/dist/emblems/award/badge.d.ts +2 -0
  11. package/dist/emblems/award/badge.js +5 -0
  12. package/dist/emblems/award/crown.d.ts +2 -0
  13. package/dist/emblems/award/crown.js +5 -0
  14. package/dist/emblems/award/medal.d.ts +2 -2
  15. package/dist/emblems/award/medal.js +5 -19
  16. package/dist/emblems/award/shield.d.ts +2 -0
  17. package/dist/emblems/award/shield.js +5 -0
  18. package/dist/emblems/award/star.d.ts +2 -0
  19. package/dist/emblems/award/star.js +5 -0
  20. package/dist/emblems/ball/ball.afl.d.ts +1 -0
  21. package/dist/emblems/ball/ball.afl.js +24 -0
  22. package/dist/emblems/ball/ball.baseball.d.ts +1 -0
  23. package/dist/emblems/ball/ball.baseball.js +14 -0
  24. package/dist/emblems/ball/ball.basketball.d.ts +7 -0
  25. package/dist/emblems/ball/ball.basketball.js +32 -0
  26. package/dist/emblems/ball/ball.basketball3x3.d.ts +1 -0
  27. package/dist/emblems/ball/ball.basketball3x3.js +10 -0
  28. package/dist/emblems/ball/ball.basketball6.d.ts +1 -0
  29. package/dist/emblems/ball/ball.basketball6.js +10 -0
  30. package/dist/emblems/ball/ball.core.d.ts +105 -0
  31. package/dist/emblems/ball/ball.core.js +66 -0
  32. package/dist/emblems/ball/ball.cricket.d.ts +5 -0
  33. package/dist/emblems/ball/ball.cricket.js +31 -0
  34. package/dist/emblems/ball/ball.curlingstone.d.ts +3 -0
  35. package/dist/emblems/ball/ball.curlingstone.js +46 -0
  36. package/dist/emblems/ball/ball.discus.d.ts +1 -0
  37. package/dist/emblems/ball/ball.discus.js +24 -0
  38. package/dist/emblems/ball/ball.engine.d.ts +114 -0
  39. package/dist/emblems/ball/ball.engine.js +342 -0
  40. package/dist/emblems/ball/ball.family.dotted.d.ts +27 -0
  41. package/dist/emblems/ball/ball.family.dotted.js +20 -0
  42. package/dist/emblems/ball/ball.family.panelled.d.ts +7 -0
  43. package/dist/emblems/ball/ball.family.panelled.js +60 -0
  44. package/dist/emblems/ball/ball.family.prolate.d.ts +16 -0
  45. package/dist/emblems/ball/ball.family.prolate.js +29 -0
  46. package/dist/emblems/ball/ball.family.seamed.d.ts +31 -0
  47. package/dist/emblems/ball/ball.family.seamed.js +26 -0
  48. package/dist/emblems/ball/ball.fieldhockey.d.ts +1 -0
  49. package/dist/emblems/ball/ball.fieldhockey.js +15 -0
  50. package/dist/emblems/ball/ball.football.d.ts +3 -0
  51. package/dist/emblems/ball/ball.football.js +51 -0
  52. package/dist/emblems/ball/ball.gaelic.d.ts +2 -0
  53. package/dist/emblems/ball/ball.gaelic.js +30 -0
  54. package/dist/emblems/ball/ball.golf.d.ts +1 -0
  55. package/dist/emblems/ball/ball.golf.js +15 -0
  56. package/dist/emblems/ball/ball.hammer.d.ts +1 -0
  57. package/dist/emblems/ball/ball.hammer.js +31 -0
  58. package/dist/emblems/ball/ball.handball.d.ts +1 -0
  59. package/dist/emblems/ball/ball.handball.js +25 -0
  60. package/dist/emblems/ball/ball.index.d.ts +48 -0
  61. package/dist/emblems/ball/ball.index.js +103 -0
  62. package/dist/emblems/ball/ball.math.d.ts +38 -0
  63. package/dist/emblems/ball/ball.math.js +105 -0
  64. package/dist/emblems/ball/ball.object.d.ts +38 -0
  65. package/dist/emblems/ball/ball.object.js +20 -0
  66. package/dist/emblems/ball/ball.padel.d.ts +1 -0
  67. package/dist/emblems/ball/ball.padel.js +13 -0
  68. package/dist/emblems/ball/ball.pickleball.d.ts +2 -0
  69. package/dist/emblems/ball/ball.pickleball.js +26 -0
  70. package/dist/emblems/ball/ball.polyhedra.d.ts +15 -0
  71. package/dist/emblems/ball/ball.polyhedra.js +63 -0
  72. package/dist/emblems/ball/ball.puck.d.ts +3 -0
  73. package/dist/emblems/ball/ball.puck.js +22 -0
  74. package/dist/emblems/ball/ball.roundnet.d.ts +1 -0
  75. package/dist/emblems/ball/ball.roundnet.js +15 -0
  76. package/dist/emblems/ball/ball.rugby.d.ts +1 -0
  77. package/dist/emblems/ball/ball.rugby.js +35 -0
  78. package/dist/emblems/ball/ball.sepaktakraw.d.ts +1 -0
  79. package/dist/emblems/ball/ball.sepaktakraw.js +48 -0
  80. package/dist/emblems/ball/ball.shuttlecock.d.ts +6 -0
  81. package/dist/emblems/ball/ball.shuttlecock.js +93 -0
  82. package/dist/emblems/ball/ball.sliotar.d.ts +1 -0
  83. package/dist/emblems/ball/ball.sliotar.js +14 -0
  84. package/dist/emblems/ball/ball.smooth.d.ts +4 -0
  85. package/dist/emblems/ball/ball.smooth.js +51 -0
  86. package/dist/emblems/ball/ball.soccer.d.ts +2 -0
  87. package/dist/emblems/ball/ball.soccer.js +54 -0
  88. package/dist/emblems/ball/ball.softball.d.ts +1 -0
  89. package/dist/emblems/ball/ball.softball.js +14 -0
  90. package/dist/emblems/ball/ball.squash.d.ts +7 -0
  91. package/dist/emblems/ball/ball.squash.js +27 -0
  92. package/dist/emblems/ball/ball.stack.d.ts +48 -0
  93. package/dist/emblems/ball/ball.stack.js +127 -0
  94. package/dist/emblems/ball/ball.stitch.d.ts +10 -0
  95. package/dist/emblems/ball/ball.stitch.js +59 -0
  96. package/dist/emblems/ball/ball.tennis.d.ts +1 -0
  97. package/dist/emblems/ball/ball.tennis.js +13 -0
  98. package/dist/emblems/ball/ball.teqball.d.ts +1 -0
  99. package/dist/emblems/ball/ball.teqball.js +27 -0
  100. package/dist/emblems/ball/ball.volleyball.d.ts +3 -0
  101. package/dist/emblems/ball/ball.volleyball.js +56 -0
  102. package/dist/emblems/ball/ball.waterpolo.d.ts +1 -0
  103. package/dist/emblems/ball/ball.waterpolo.js +31 -0
  104. package/dist/emblems/crest/diamond.d.ts +2 -0
  105. package/dist/emblems/crest/diamond.js +18 -0
  106. package/dist/emblems/crest/pennant.d.ts +2 -0
  107. package/dist/emblems/crest/pennant.js +18 -0
  108. package/dist/emblems/crest/roundel.d.ts +2 -0
  109. package/dist/emblems/crest/roundel.js +18 -0
  110. package/dist/emblems/crest/shield.d.ts +2 -0
  111. package/dist/emblems/crest/shield.js +17 -0
  112. package/dist/emblems/emblem.api.d.ts +39 -9
  113. package/dist/emblems/emblem.api.js +38 -10
  114. package/dist/emblems/emblem.core.d.ts +42 -6
  115. package/dist/emblems/emblem.core.js +55 -11
  116. package/dist/emblems/emblem.ids.d.ts +33 -27
  117. package/dist/emblems/emblem.ids.js +35 -16
  118. package/dist/emblems/emblem.lathe.d.ts +59 -0
  119. package/dist/emblems/emblem.lathe.js +404 -0
  120. package/dist/emblems/emblem.material.d.ts +9 -0
  121. package/dist/emblems/emblem.material.data.d.ts +34 -0
  122. package/dist/emblems/emblem.material.data.js +85 -0
  123. package/dist/emblems/emblem.material.js +23 -0
  124. package/dist/emblems/emblem.medal.d.ts +6 -0
  125. package/dist/emblems/emblem.medal.js +38 -0
  126. package/dist/emblems/emblem.registry.js +142 -4
  127. package/dist/emblems/emblem.shapes.d.ts +31 -12
  128. package/dist/emblems/emblem.shapes.js +99 -33
  129. package/dist/emblems/gear/gear.badmintonracket.d.ts +1 -0
  130. package/dist/emblems/gear/gear.badmintonracket.js +15 -0
  131. package/dist/emblems/gear/gear.baseballglove.d.ts +1 -0
  132. package/dist/emblems/gear/gear.baseballglove.js +15 -0
  133. package/dist/emblems/gear/gear.core.d.ts +83 -0
  134. package/dist/emblems/gear/gear.core.js +120 -0
  135. package/dist/emblems/gear/gear.cricketbat.d.ts +1 -0
  136. package/dist/emblems/gear/gear.cricketbat.js +15 -0
  137. package/dist/emblems/gear/gear.field.d.ts +6 -0
  138. package/dist/emblems/gear/gear.field.js +81 -0
  139. package/dist/emblems/gear/gear.fieldhockeystick.d.ts +1 -0
  140. package/dist/emblems/gear/gear.fieldhockeystick.js +15 -0
  141. package/dist/emblems/gear/gear.footballhelmet.d.ts +1 -0
  142. package/dist/emblems/gear/gear.footballhelmet.js +15 -0
  143. package/dist/emblems/gear/gear.goaliemask.d.ts +1 -0
  144. package/dist/emblems/gear/gear.goaliemask.js +17 -0
  145. package/dist/emblems/gear/gear.goalnet.d.ts +1 -0
  146. package/dist/emblems/gear/gear.goalnet.js +15 -0
  147. package/dist/emblems/gear/gear.hockeystick.d.ts +1 -0
  148. package/dist/emblems/gear/gear.hockeystick.js +15 -0
  149. package/dist/emblems/gear/gear.index.d.ts +22 -0
  150. package/dist/emblems/gear/gear.index.js +45 -0
  151. package/dist/emblems/gear/gear.lacrossestick.d.ts +1 -0
  152. package/dist/emblems/gear/gear.lacrossestick.js +15 -0
  153. package/dist/emblems/gear/gear.rackets.d.ts +4 -0
  154. package/dist/emblems/gear/gear.rackets.js +60 -0
  155. package/dist/emblems/gear/gear.skates.d.ts +3 -0
  156. package/dist/emblems/gear/gear.skates.js +39 -0
  157. package/dist/emblems/gear/gear.sticks.d.ts +3 -0
  158. package/dist/emblems/gear/gear.sticks.js +34 -0
  159. package/dist/emblems/gear/gear.tabletennispaddle.d.ts +1 -0
  160. package/dist/emblems/gear/gear.tabletennispaddle.js +15 -0
  161. package/dist/emblems/gear/gear.tennisracket.d.ts +1 -0
  162. package/dist/emblems/gear/gear.tennisracket.js +15 -0
  163. package/dist/emblems/gear/gear.track.d.ts +4 -0
  164. package/dist/emblems/gear/gear.track.js +45 -0
  165. package/dist/emblems/gear/gear.water.d.ts +2 -0
  166. package/dist/emblems/gear/gear.water.js +25 -0
  167. package/dist/emblems/medal/bronze.d.ts +2 -0
  168. package/dist/emblems/medal/bronze.js +4 -0
  169. package/dist/emblems/medal/gold.d.ts +2 -0
  170. package/dist/emblems/medal/gold.js +4 -0
  171. package/dist/emblems/medal/place-1.d.ts +2 -0
  172. package/dist/emblems/medal/place-1.js +4 -0
  173. package/dist/emblems/medal/place-2.d.ts +2 -0
  174. package/dist/emblems/medal/place-2.js +4 -0
  175. package/dist/emblems/medal/place-3.d.ts +2 -0
  176. package/dist/emblems/medal/place-3.js +4 -0
  177. package/dist/emblems/medal/silver.d.ts +2 -0
  178. package/dist/emblems/medal/silver.js +4 -0
  179. package/dist/emblems/sport/afl.d.ts +2 -0
  180. package/dist/emblems/sport/afl.js +12 -0
  181. package/dist/emblems/sport/artisticswimming.d.ts +2 -0
  182. package/dist/emblems/sport/artisticswimming.js +16 -0
  183. package/dist/emblems/sport/athletics.d.ts +2 -0
  184. package/dist/emblems/sport/athletics.js +12 -0
  185. package/dist/emblems/sport/badminton.d.ts +2 -0
  186. package/dist/emblems/sport/badminton.js +13 -0
  187. package/dist/emblems/sport/cfl.d.ts +2 -0
  188. package/dist/emblems/sport/cfl.js +5 -0
  189. package/dist/emblems/sport/cricket.d.ts +2 -0
  190. package/dist/emblems/sport/cricket.js +5 -0
  191. package/dist/emblems/sport/curling.d.ts +2 -0
  192. package/dist/emblems/sport/curling.js +14 -0
  193. package/dist/emblems/sport/diving.d.ts +2 -0
  194. package/dist/emblems/sport/diving.js +18 -0
  195. package/dist/emblems/sport/fieldhockey.d.ts +2 -0
  196. package/dist/emblems/sport/fieldhockey.js +11 -0
  197. package/dist/emblems/sport/gaelic.d.ts +2 -0
  198. package/dist/emblems/sport/gaelic.js +11 -0
  199. package/dist/emblems/sport/handball.d.ts +2 -0
  200. package/dist/emblems/sport/handball.js +5 -0
  201. package/dist/emblems/sport/ifaf.d.ts +2 -0
  202. package/dist/emblems/sport/ifaf.js +5 -0
  203. package/dist/emblems/sport/lacrosse.d.ts +2 -0
  204. package/dist/emblems/sport/lacrosse.js +13 -0
  205. package/dist/emblems/sport/mlb.d.ts +2 -0
  206. package/dist/emblems/sport/mlb.js +5 -0
  207. package/dist/emblems/sport/nba.d.ts +2 -0
  208. package/dist/emblems/sport/nba.js +5 -0
  209. package/dist/emblems/sport/netball.d.ts +2 -0
  210. package/dist/emblems/sport/netball.js +14 -0
  211. package/dist/emblems/sport/nfl.d.ts +2 -0
  212. package/dist/emblems/sport/nfl.js +5 -0
  213. package/dist/emblems/sport/nhl.d.ts +2 -0
  214. package/dist/emblems/sport/nhl.js +5 -0
  215. package/dist/emblems/sport/padel.d.ts +2 -0
  216. package/dist/emblems/sport/padel.js +14 -0
  217. package/dist/emblems/sport/pickleball.d.ts +2 -0
  218. package/dist/emblems/sport/pickleball.js +13 -0
  219. package/dist/emblems/sport/racquetball.d.ts +2 -0
  220. package/dist/emblems/sport/racquetball.js +13 -0
  221. package/dist/emblems/sport/roundnet.d.ts +2 -0
  222. package/dist/emblems/sport/roundnet.js +15 -0
  223. package/dist/emblems/sport/rugby.d.ts +2 -0
  224. package/dist/emblems/sport/rugby.js +5 -0
  225. package/dist/emblems/sport/sepaktakraw.d.ts +2 -0
  226. package/dist/emblems/sport/sepaktakraw.js +11 -0
  227. package/dist/emblems/sport/skating.d.ts +2 -0
  228. package/dist/emblems/sport/skating.js +14 -0
  229. package/dist/emblems/sport/soccer.d.ts +2 -0
  230. package/dist/emblems/sport/soccer.js +5 -0
  231. package/dist/emblems/sport/squash.d.ts +2 -0
  232. package/dist/emblems/sport/squash.js +15 -0
  233. package/dist/emblems/sport/swimming.d.ts +2 -0
  234. package/dist/emblems/sport/swimming.js +15 -0
  235. package/dist/emblems/sport/tabletennis.d.ts +2 -0
  236. package/dist/emblems/sport/tabletennis.js +12 -0
  237. package/dist/emblems/sport/tennis.d.ts +2 -0
  238. package/dist/emblems/sport/tennis.js +5 -0
  239. package/dist/emblems/sport/teqball.d.ts +2 -0
  240. package/dist/emblems/sport/teqball.js +15 -0
  241. package/dist/emblems/sport/volleyball.d.ts +2 -0
  242. package/dist/emblems/sport/volleyball.js +5 -0
  243. package/dist/emblems/sport/waterpolo.d.ts +2 -0
  244. package/dist/emblems/sport/waterpolo.js +15 -0
  245. package/dist/emblems/trophy/baseball.d.ts +2 -0
  246. package/dist/emblems/trophy/baseball.js +14 -0
  247. package/dist/emblems/trophy/basketball.d.ts +2 -0
  248. package/dist/emblems/trophy/basketball.js +16 -0
  249. package/dist/emblems/trophy/belt.d.ts +2 -0
  250. package/dist/emblems/trophy/belt.js +16 -0
  251. package/dist/emblems/trophy/bowl.d.ts +2 -0
  252. package/dist/emblems/trophy/bowl.js +7 -0
  253. package/dist/emblems/trophy/canadian-bowl.d.ts +2 -0
  254. package/dist/emblems/trophy/canadian-bowl.js +22 -0
  255. package/dist/emblems/trophy/cricket-urn.d.ts +2 -0
  256. package/dist/emblems/trophy/cricket-urn.js +22 -0
  257. package/dist/emblems/trophy/cup.d.ts +2 -0
  258. package/dist/emblems/trophy/cup.js +5 -0
  259. package/dist/emblems/trophy/football.d.ts +2 -0
  260. package/dist/emblems/trophy/football.js +10 -0
  261. package/dist/emblems/trophy/globe.d.ts +2 -0
  262. package/dist/emblems/trophy/globe.js +8 -0
  263. package/dist/emblems/trophy/hockey-cup.d.ts +2 -0
  264. package/dist/emblems/trophy/hockey-cup.js +21 -0
  265. package/dist/emblems/trophy/laurel.d.ts +2 -0
  266. package/dist/emblems/trophy/laurel.js +40 -0
  267. package/dist/emblems/trophy/lidded-cup.d.ts +2 -0
  268. package/dist/emblems/trophy/lidded-cup.js +20 -0
  269. package/dist/emblems/trophy/plaque.d.ts +2 -0
  270. package/dist/emblems/trophy/plaque.js +16 -0
  271. package/dist/emblems/trophy/rosette.d.ts +2 -0
  272. package/dist/emblems/trophy/rosette.js +27 -0
  273. package/dist/emblems/trophy/rugby-cup.d.ts +2 -0
  274. package/dist/emblems/trophy/rugby-cup.js +22 -0
  275. package/dist/emblems/trophy/salver.d.ts +2 -0
  276. package/dist/emblems/trophy/salver.js +8 -0
  277. package/dist/emblems/trophy/soccer-ball.d.ts +2 -0
  278. package/dist/emblems/trophy/soccer-ball.js +8 -0
  279. package/dist/emblems/trophy/soccer-globe.d.ts +2 -0
  280. package/dist/emblems/trophy/soccer-globe.js +17 -0
  281. package/dist/emblems/trophy/tennis-cup.d.ts +2 -0
  282. package/dist/emblems/trophy/tennis-cup.js +23 -0
  283. package/dist/emblems/trophy/tennis-plate.d.ts +2 -0
  284. package/dist/emblems/trophy/tennis-plate.js +15 -0
  285. package/dist/emblems-entry.d.ts +10 -6
  286. package/dist/emblems-entry.js +3 -2
  287. package/dist/index.d.ts +114 -1
  288. package/dist/index.js +116 -3
  289. package/dist/version.d.ts +1 -1
  290. package/dist/version.js +1 -1
  291. package/package.json +28 -8
  292. package/LICENSES/CC-BY-3.0.md +0 -15
  293. package/dist/emblems/ball/soccer.d.ts +0 -2
  294. package/dist/emblems/ball/soccer.js +0 -9
  295. package/dist/emblems/emblem.ball.d.ts +0 -28
  296. package/dist/emblems/emblem.ball.js +0 -70
@@ -0,0 +1,342 @@
1
+ // The ball renderer: one procedural engine for every sphere and every prolate ball. A ball is drawn in true 3D: a rotation, an orthographic projection, back faces dropped, panels cut at the limb
2
+ // along the limb itself, then radial-gradient shading and a grain that stays where it is while the ball turns. A ball is a `BallSpec`: its shape (the semi-axes of an ellipsoid), its
3
+ // view, its material and a `draw` function that lays panels, seams and dots on the unit sphere; the engine does the rest.
4
+ import { grainDef, escape, hashOf, mix, num, radians, resolveOptions, wrapPiece } from "./ball.core.js";
5
+ import { add, apply, basis, cross, dot, ellipseOf, len, mul, norm, rotation, sampleRing, sub } from "./ball.math.js";
6
+ const EPS = 1e-6;
7
+ const STEP = Math.PI / 18;
8
+ /** How a point of the unit sphere is carried to the ball's surface: an ellipsoid's scaling, and for a pointed ball a narrowing of the cross-section toward the ends. */
9
+ const shapeOf = (g, u) => {
10
+ const f = g.point === undefined ? 1 : Math.max(0, 1 - u[0] * u[0]) ** (g.point - 0.5);
11
+ return [u[0] * g.axes[0], u[1] * g.axes[1] * f, u[2] * g.axes[2] * f];
12
+ };
13
+ /** How squarely a point of the sphere faces the eye, before it is scaled: above 0 is the side you see. For an ellipsoid it is the dot of the point with `w`; for a pointed ball the surface's normal is worked out. */
14
+ const visOf = (g, u) => {
15
+ if (g.point === undefined)
16
+ return dot(u, g.w);
17
+ const rest = Math.max(0, 1 - u[0] * u[0]);
18
+ const a = g.axes[0];
19
+ const b = g.axes[1];
20
+ const nx = 2 * b * g.point * u[0] * rest ** (g.point - 0.5);
21
+ return (g.rz[0] * nx + g.rz[1] * a * u[1] + g.rz[2] * a * u[2]) / (a * b);
22
+ };
23
+ const project = (g, u) => {
24
+ const q = apply(g.rot, shapeOf(g, u));
25
+ return [g.cx + g.k * q[0], g.cy - g.k * q[1]];
26
+ };
27
+ const point = (g, u) => {
28
+ const [x, y] = project(g, u);
29
+ return `${num(x)} ${num(y)}`;
30
+ };
31
+ const crossing = (a, b, va, vb) => {
32
+ const t = va / (va - vb);
33
+ return norm(add(mul(a, 1 - t), mul(b, t)));
34
+ };
35
+ /** The outline of a ring on the surface, cut where it passes behind the limb and joined along the limb. */
36
+ function ringPath(g, ring) {
37
+ const vis = ring.map((p) => visOf(g, p));
38
+ const start = vis.findIndex((v) => v > EPS);
39
+ if (start < 0)
40
+ return "";
41
+ const n = ring.length;
42
+ const out = [];
43
+ let lastExit;
44
+ out.push(`M${point(g, ring[start])}`);
45
+ for (let j = 1; j <= n; j++) {
46
+ const i = (start + j) % n;
47
+ const p = (start + j - 1) % n;
48
+ const vp = vis[p], vi = vis[i];
49
+ if (vp > EPS && vi > EPS) {
50
+ if (j < n)
51
+ out.push(`L${point(g, ring[i])}`);
52
+ }
53
+ else if (vp > EPS) {
54
+ lastExit = crossing(ring[p], ring[i], vp, vi);
55
+ out.push(`L${point(g, lastExit)}`);
56
+ }
57
+ else if (vi > EPS && lastExit !== undefined) {
58
+ const entry = crossing(ring[p], ring[i], vp, vi);
59
+ for (const q of limbArc(g, lastExit, entry))
60
+ out.push(`L${point(g, q)}`);
61
+ out.push(`L${point(g, entry)}`);
62
+ if (j < n)
63
+ out.push(`L${point(g, ring[i])}`);
64
+ }
65
+ }
66
+ return `${out.join("")}Z`;
67
+ }
68
+ /** The points of the limb between two points on it, the short way round (the ends left out). */
69
+ function limbArc(g, from, to) {
70
+ const wn = norm(g.w);
71
+ const e1 = norm(sub(from, mul(wn, dot(from, wn))));
72
+ const e2 = cross(wn, e1);
73
+ let delta = Math.atan2(dot(to, e2), dot(to, e1));
74
+ if (delta === 0 && dot(to, e1) < 0)
75
+ delta = Math.PI;
76
+ const steps = Math.max(1, Math.ceil(Math.abs(delta) / STEP));
77
+ const out = [];
78
+ for (let i = 1; i < steps; i++) {
79
+ const t = (delta * i) / steps;
80
+ out.push(add(mul(e1, Math.cos(t)), mul(e2, Math.sin(t))));
81
+ }
82
+ return out;
83
+ }
84
+ /** The parts of a curve that face you, each as screen points with their facing. The curve is cut exactly at the limb. */
85
+ function visibleRuns(g, pts, closed) {
86
+ const n = pts.length;
87
+ const vis = pts.map((p) => visOf(g, p) / g.wl);
88
+ const at = (u, f) => {
89
+ const [x, y] = project(g, u);
90
+ return [x, y, f];
91
+ };
92
+ const runs = [];
93
+ let cur;
94
+ const total = closed ? n : n - 1;
95
+ // Start a closed curve at a hidden point so that no run is split at the start; a curve wholly facing you is one closed run.
96
+ let first = 0;
97
+ if (closed) {
98
+ const hidden = vis.findIndex((v) => v <= 0);
99
+ if (hidden < 0) {
100
+ return [{ pts: [...pts.map((p, i) => at(p, vis[i])), at(pts[0], vis[0])] }];
101
+ }
102
+ first = hidden;
103
+ }
104
+ for (let j = 0; j < total; j++) {
105
+ const i = (first + j) % n;
106
+ const m = (first + j + 1) % n;
107
+ const a = pts[i], b = pts[m];
108
+ const va = vis[i], vb = vis[m];
109
+ if (va > 0 && vb > 0) {
110
+ if (cur === undefined) {
111
+ cur = { pts: [at(a, va)] };
112
+ runs.push(cur);
113
+ }
114
+ cur.pts.push(at(b, vb));
115
+ }
116
+ else if (va > 0) {
117
+ const c = crossing(a, b, va * g.wl, vb * g.wl);
118
+ if (cur === undefined) {
119
+ cur = { pts: [at(a, va)] };
120
+ runs.push(cur);
121
+ }
122
+ cur.pts.push(at(c, 0));
123
+ cur = undefined;
124
+ }
125
+ else if (vb > 0) {
126
+ const c = crossing(a, b, va * g.wl, vb * g.wl);
127
+ cur = { pts: [at(c, 0), at(b, vb)] };
128
+ runs.push(cur);
129
+ }
130
+ }
131
+ return runs;
132
+ }
133
+ const pathOf = (pts) => pts.map((p, i) => `${i === 0 ? "M" : "L"}${num(p[0])} ${num(p[1])}`).join("");
134
+ /** A curve's visible parts as paths: a stroke is thinner where it turns toward the limb (three steps), so it does not sit on the ball like a wire. */
135
+ function runPaths(runs, colour, width, extra) {
136
+ const tiers = [[], [], []];
137
+ const factor = [1, 0.74, 0.5];
138
+ const tierOf = (f) => (f > 0.55 ? 0 : f > 0.25 ? 1 : 2);
139
+ for (const run of runs) {
140
+ let kind = -1;
141
+ let piece = [];
142
+ const flush = () => {
143
+ if (piece.length > 1)
144
+ tiers[kind].push(pathOf(piece));
145
+ piece = [];
146
+ };
147
+ for (const p of run.pts) {
148
+ const k = tierOf(p[2]);
149
+ if (k !== kind) {
150
+ const last = piece.at(-1);
151
+ flush();
152
+ kind = k;
153
+ piece = last === undefined ? [] : [last];
154
+ }
155
+ piece.push(p);
156
+ }
157
+ flush();
158
+ }
159
+ const make = (d, w) => (d.length === 0 ? "" : `<path d="${d.join("")}" fill="none" stroke="${colour}" stroke-width="${num(w, 2)}" stroke-linecap="round" stroke-linejoin="round"${extra}/>`);
160
+ return make(tiers[2], width * factor[2]) + make(tiers[1], width * factor[1]) + make(tiers[0], width);
161
+ }
162
+ function surfaceOf(g, line, minWidth) {
163
+ const INK = "currentColor";
164
+ const facing = (u) => visOf(g, u) / g.wl;
165
+ const s = {
166
+ line,
167
+ radius: g.k * Math.max(...g.axes),
168
+ minWidth,
169
+ project: (u) => {
170
+ const [x, y] = project(g, u);
171
+ return [x, y, facing(u)];
172
+ },
173
+ facing,
174
+ space: (p) => {
175
+ const q = apply(g.rot, p);
176
+ return [g.ox + g.k * q[0], g.oy - g.k * q[1], q[2]];
177
+ },
178
+ panel(ring, fill, o) {
179
+ if (line && o?.ink !== true)
180
+ return "";
181
+ const d = ringPath(g, sampleRing(ring, o?.n ?? 4));
182
+ if (d === "")
183
+ return "";
184
+ return `<path d="${d}" fill="${line ? INK : fill}"${o?.opacity === undefined ? "" : ` opacity="${o.opacity}"`}/>`;
185
+ },
186
+ stroke: (pts, colour, width, o) => s.strokes([pts], colour, width, o),
187
+ strokes(curves, colour, width, o) {
188
+ if (line && (o?.deco === true || o?.ink === false))
189
+ return "";
190
+ const runs = curves.flatMap((pts) => visibleRuns(g, pts, o?.closed === true));
191
+ if (runs.length === 0)
192
+ return "";
193
+ const extra = `${o?.opacity === undefined || line ? "" : ` opacity="${o.opacity}"`}${o?.dash === undefined ? "" : ` stroke-dasharray="${o.dash}"`}${o?.dx === undefined && o?.dy === undefined ? "" : ` transform="translate(${o.dx ?? 0} ${o.dy ?? 0})"`}`;
194
+ return runPaths(runs, line ? INK : colour, Math.max(width, minWidth), extra);
195
+ },
196
+ seam(pts, colour, width, o) {
197
+ if (line)
198
+ return s.stroke(pts, colour, width, { closed: o?.closed });
199
+ const closed = o?.closed === true;
200
+ const w = Math.max(width, minWidth);
201
+ return (s.stroke(pts, "#000", w * 1.25, { closed, opacity: 0.28, dx: w * 0.18, dy: w * 0.22 }) +
202
+ s.stroke(pts, colour, w, { closed, opacity: o?.opacity }) +
203
+ s.stroke(pts, o?.light ?? "#fff", w * 0.32, { closed, opacity: 0.38, dx: -w * 0.16, dy: -w * 0.2 }));
204
+ },
205
+ dots(centres, radius, fill, o) {
206
+ if (line && o?.ink !== true)
207
+ return "";
208
+ const sr = Math.sin(radius), cr = Math.cos(radius);
209
+ const d = [];
210
+ for (const m of centres) {
211
+ if (facing(m) < 0.08)
212
+ continue;
213
+ const [t1, t2] = basis(m);
214
+ const centre = project(g, mul(m, cr));
215
+ const pa = project(g, mul(t1, sr));
216
+ const pb = project(g, mul(t2, sr));
217
+ const p0 = project(g, [0, 0, 0]);
218
+ const da = [pa[0] - p0[0], pa[1] - p0[1]];
219
+ const db = [pb[0] - p0[0], pb[1] - p0[1]];
220
+ const { rx, ry, angle } = ellipseOf([[da[0], db[0], 0], [da[1], db[1], 0]]);
221
+ if (rx < 0.05)
222
+ continue;
223
+ const ax = Math.cos(angle) * rx, ay = Math.sin(angle) * rx;
224
+ const deg = num((angle * 180) / Math.PI, 0);
225
+ const arcs = `a${num(rx)} ${num(Math.max(ry, 0.05))} ${deg} 1 0`;
226
+ d.push(`M${num(centre[0] + ax)} ${num(centre[1] + ay)}${arcs}${num(-2 * ax)} ${num(-2 * ay)}${arcs}${num(2 * ax)} ${num(2 * ay)}`);
227
+ }
228
+ if (d.length === 0)
229
+ return "";
230
+ const path = d.join("");
231
+ const hi = o?.light === false || line ? "" : `<path d="${path}" fill="none" stroke="#fff" stroke-width="${num(Math.max(0.35, minWidth * 0.3), 2)}" opacity=".3" transform="translate(.25 .35)"/>`;
232
+ return `${hi}<path d="${path}" fill="${line ? INK : fill}"${o?.opacity === undefined ? "" : ` opacity="${o.opacity}"`}/>`;
233
+ },
234
+ };
235
+ return s;
236
+ }
237
+ /** The silhouette of the ball in the box: a circle for a sphere, an ellipse otherwise, as the shape every layer is clipped to and shaded on. */
238
+ function silhouette(g) {
239
+ if (g.point !== undefined) {
240
+ // A pointed ball is convex, so its outline is the hull of its projected surface.
241
+ const pts = [];
242
+ for (let i = 0; i <= 36; i++) {
243
+ const x = Math.cos((Math.PI * i) / 36);
244
+ const r = Math.sqrt(Math.max(0, 1 - x * x));
245
+ for (let j = 0; j < 36; j++)
246
+ pts.push(project(g, [x, r * Math.cos((2 * Math.PI * j) / 36), r * Math.sin((2 * Math.PI * j) / 36)]));
247
+ }
248
+ pts.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
249
+ const turn = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
250
+ const half = (list) => {
251
+ const hull = [];
252
+ for (const p of list) {
253
+ while (hull.length >= 2 && turn(hull[hull.length - 2], hull[hull.length - 1], p) <= 0)
254
+ hull.pop();
255
+ hull.push(p);
256
+ }
257
+ hull.pop();
258
+ return hull;
259
+ };
260
+ const hull = [...half(pts), ...half([...pts].reverse())];
261
+ return { tag: "", d: `M${hull.map((p) => `${num(p[0], 1)} ${num(p[1], 1)}`).join("L")}Z` };
262
+ }
263
+ const rows = [
264
+ [g.rot[0][0] * g.axes[0], g.rot[0][1] * g.axes[1], g.rot[0][2] * g.axes[2]],
265
+ [-g.rot[1][0] * g.axes[0], -g.rot[1][1] * g.axes[1], -g.rot[1][2] * g.axes[2]],
266
+ ];
267
+ const { rx, ry, angle } = ellipseOf(rows);
268
+ const a = g.k * rx, b = g.k * ry;
269
+ if (Math.abs(a - b) < 0.02)
270
+ return { tag: `cx="${num(g.cx, 2)}" cy="${num(g.cy, 2)}" r="${num(a, 2)}"`, d: "" };
271
+ const ax = Math.cos(angle) * a, ay = Math.sin(angle) * a;
272
+ const deg = num((angle * 180) / Math.PI, 1);
273
+ const arcs = `${num(a, 2)} ${num(b, 2)} ${deg} 1 0`;
274
+ return { tag: "", d: `M${num(g.cx + ax, 2)} ${num(g.cy + ay, 2)}A${arcs} ${num(g.cx - ax, 2)} ${num(g.cy - ay, 2)}A${arcs} ${num(g.cx + ax, 2)} ${num(g.cy + ay, 2)}Z` };
275
+ }
276
+ /** The sky and ground mirrored in a polished ball: bright above, a dark horizon, a lighter bounce below. */
277
+ const metalDef = (id, on) => on
278
+ ? `<linearGradient id="${id}m" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#fff" stop-opacity=".6"/><stop offset=".3" stop-color="#fff" stop-opacity=".15"/><stop offset=".52" stop-color="#000" stop-opacity=".4"/><stop offset=".75" stop-color="#000" stop-opacity=".12"/><stop offset="1" stop-color="#fff" stop-opacity=".3"/></linearGradient>`
279
+ : "";
280
+ /** Draws a ball: the document for a spec, the options and a turn in radians. */
281
+ export function drawBall(title, key, spec, opts, turn, turning) {
282
+ const axes = spec.axes ?? [1, 1, 1];
283
+ const rot = rotation(turn.yaw, turn.pitch, turn.roll);
284
+ const rz = [rot[2][0], rot[2][1], rot[2][2]];
285
+ const w = [rz[0] / axes[0], rz[1] / axes[1], rz[2] / axes[2]];
286
+ const ox = spec.centre?.[0] ?? 50, oy = spec.centre?.[1] ?? 50;
287
+ const kk = (46 * (spec.fit ?? 1)) / Math.max(...axes);
288
+ const away = apply(rot, spec.at ?? [0, 0, 0]);
289
+ const g = { ox, oy, cx: ox + kk * away[0], cy: oy - kk * away[1], axes, rot, k: (46 * (spec.fit ?? 1)) / Math.max(...axes), w, wl: len(w), rz, point: spec.point };
290
+ const line = opts.style === "line";
291
+ const minWidth = 80 / opts.size;
292
+ const base = opts.tint ?? spec.base;
293
+ const surface = surfaceOf(g, line, minWidth);
294
+ const layers = spec.draw(surface, base, opts.accent);
295
+ const sil = silhouette(g);
296
+ const id = `k${hashOf(`${key}|${opts.style}|${opts.tint ?? ""}|${opts.accent ?? ""}|${sil.tag}${sil.d}`)}`;
297
+ // A long outline (the hull of a pointed ball) is written once, in the definitions, and used wherever the silhouette is.
298
+ const long = sil.d.length > 400;
299
+ const outline = long ? `<path id="${id}s" d="${sil.d}"/>` : "";
300
+ const shape = (attrs) => (sil.d === "" ? `<circle ${sil.tag} ${attrs}/>` : long ? `<use href="#${id}s" ${attrs}/>` : `<path d="${sil.d}" ${attrs}/>`);
301
+ if (line) {
302
+ const at = spec.attach?.(surface, base);
303
+ const body = `${at?.behind ?? ""}${shape(`fill="none" stroke="currentColor" stroke-width="${num(Math.max(3, minWidth * 1.6), 2)}"`)}<g>${layers}</g>${at?.front ?? ""}`;
304
+ return wrapPiece({ title, size: opts.size, theme: opts.theme, defs: outline, body: `<g fill="none" stroke-linecap="round" stroke-linejoin="round">${body}</g>` });
305
+ }
306
+ const m = spec.mat;
307
+ const shade = m.shade ?? mix(base, -0.62);
308
+ const grain = opts.grain ?? (opts.size >= 48 && !turning);
309
+ const gr = grain && m.grain !== undefined ? m.grain : undefined;
310
+ const rim = m.rim ?? 1;
311
+ const stop = (o, colour, a) => `<stop offset="${o}" stop-color="${colour}" stop-opacity="${num(a, 2)}"/>`;
312
+ const defs = outline +
313
+ `<clipPath id="${id}c">${shape("")}</clipPath>` +
314
+ `<radialGradient id="${id}d" cx=".42" cy=".36" r=".86" fx=".36" fy=".3">${stop(0, shade, 0)}${stop(0.42, shade, 0)}${stop(0.78, shade, 0.34 * rim)}${stop(1, shade, 0.78 * rim)}</radialGradient>` +
315
+ `<radialGradient id="${id}h" cx=".34" cy=".28" r=".4">${stop(0, "#fff", 0.28 + m.gloss * 0.62)}${stop(0.32, "#fff", 0.1 + m.gloss * 0.22)}${stop(1, "#fff", 0)}</radialGradient>` +
316
+ `<radialGradient id="${id}b" cx=".5" cy=".5" r=".5">${stop(0.8, "#fff", 0)}${stop(1, "#fff", 0.16)}</radialGradient>` +
317
+ (gr === undefined
318
+ ? ""
319
+ : grainDef(id, gr.f, gr.a, gr.seed));
320
+ const inner = `<g clip-path="url(#${id}c)"${gr === undefined ? "" : ` filter="url(#${id}n)"`}>${shape(`fill="${spec.ground ?? base}"`)}${layers}</g>`;
321
+ const metal = m.metal === true ? shape(`fill="url(#${id}m)"`) : "";
322
+ const overlays = shape(`fill="url(#${id}d)"`) + metal + shape(`fill="url(#${id}b)"`) + shape(`fill="url(#${id}h)"`);
323
+ const at = spec.attach?.(surface, base);
324
+ return wrapPiece({ title, size: opts.size, theme: opts.theme, defs: defs + metalDef(id, m.metal === true), body: `${at?.behind ?? ""}${inner}${overlays}${at?.front ?? ""}` });
325
+ }
326
+ const finite = (v) => (typeof v === "number" && Number.isFinite(v) ? v : 0);
327
+ /** A ball from its definition: the `Piece` the registry and the gallery take. */
328
+ export function defineBall(def) {
329
+ const view = def.spec.view;
330
+ const render = (o, turning) => drawBall(escape(def.en), def.id, def.spec, resolveOptions(o), { yaw: radians(view.yaw + finite(o?.yaw), 0), pitch: radians(view.pitch + finite(o?.pitch), 0), roll: radians(view.roll + finite(o?.roll), 0) }, turning);
331
+ return {
332
+ id: def.id,
333
+ en: def.en,
334
+ ja: def.ja,
335
+ source: "generated",
336
+ kind: "ball",
337
+ model: def.spec,
338
+ size: def.size,
339
+ svg: (o) => render(o, false),
340
+ frame: (o) => render(o, true),
341
+ };
342
+ }
@@ -0,0 +1,27 @@
1
+ import { type BallDef } from "./ball.engine.ts";
2
+ import { type V3 } from "./ball.math.ts";
3
+ import type { Piece } from "./ball.core.ts";
4
+ import type { BallSpec } from "./ball.engine.ts";
5
+ export interface DottedOptions {
6
+ id: string;
7
+ en: string;
8
+ ja: string;
9
+ size: BallDef["size"];
10
+ base: string;
11
+ count: number;
12
+ /** A cap's radius in radians. */
13
+ radius: number;
14
+ /** How dark a mark is against the ball: 0 to 1. */
15
+ depth?: number;
16
+ view?: BallSpec["view"];
17
+ mat: BallSpec["mat"];
18
+ /** A mark's own colour, instead of a darker ball colour (a hole shows what is inside). */
19
+ mark?: string;
20
+ /** A lit rim on each mark (not for a dimple pattern of two hundred, which is large enough already). */
21
+ light?: boolean;
22
+ /** Turns the spread, so a ball does not show its spiral's pole straight on. */
23
+ phase?: number;
24
+ }
25
+ /** The mark centres: `count` points, turned so that no pole faces the default view. */
26
+ export declare const dotCentres: (count: number, phase?: number) => V3[];
27
+ export declare function dottedBall(o: DottedOptions): Piece<BallSpec>;
@@ -0,0 +1,20 @@
1
+ // Balls covered in round marks: the holes of a pickleball, the dimples of a golf ball or a field hockey ball, the coloured dots of a squash ball. The centres are an even spread over the sphere
2
+ // (a Fibonacci spiral), and each mark is a true cap on the sphere, so it foreshortens toward the limb.
3
+ import { defineBall } from "./ball.engine.js";
4
+ import { mix } from "./ball.core.js";
5
+ import { spread } from "./ball.math.js";
6
+ /** The mark centres: `count` points, turned so that no pole faces the default view. */
7
+ export const dotCentres = (count, phase = 0) => spread(count, phase);
8
+ export function dottedBall(o) {
9
+ const centres = dotCentres(o.count, o.phase ?? 0);
10
+ const spec = {
11
+ view: o.view ?? { yaw: 20, pitch: 25, roll: 0 },
12
+ base: o.base,
13
+ mat: o.mat,
14
+ draw: (s, base) => {
15
+ const mark = o.mark ?? mix(base, -0.55);
16
+ return s.dots(centres, o.radius, mark, { opacity: o.depth ?? 0.55, ink: true, light: o.light ?? true });
17
+ },
18
+ };
19
+ return defineBall({ id: o.id, en: o.en, ja: o.ja, spec, size: o.size });
20
+ }
@@ -0,0 +1,7 @@
1
+ import type { Surface } from "./ball.engine.ts";
2
+ /** The 18 panels of a ball. `colours[i]` is the three strips of face i, or one colour for the middle strip and one for the outer two. */
3
+ export declare function strips18(s: Surface, colours: ReadonlyArray<readonly [string, string]>, seam: string, width?: number): string;
4
+ /** The 8 panels of an octahedral ball: the octants, coloured by `pick(sx, sy, sz)`. */
5
+ export declare function octants8(s: Surface, pick: (sx: number, sy: number, sz: number) => string, seam: string, width?: number): string;
6
+ /** A darker seam colour for a main colour. */
7
+ export declare const seamOf: (base: string, amount?: number) => string;
@@ -0,0 +1,60 @@
1
+ // Balls made of curved panels: the 18-panel layout of a volleyball (six groups of three strips; the strips of a face run square to those of its neighbours), and the 8 panels of an octahedral ball.
2
+ // The strips are the faces of a cube mapped onto the sphere (a straight line on a face becomes an arc of a great circle), so every seam is a true great-circle arc.
3
+ import { arc, greatCircle, norm } from "./ball.math.js";
4
+ import { mix } from "./ball.core.js";
5
+ /** The six faces of the cube as: the axis the face looks along, its sign, the axis its strips run along, and the axis across them. Neighbouring faces run square to each other. */
6
+ const FACES = [
7
+ { a: 2, sign: 1, p: 1, q: 0 },
8
+ { a: 2, sign: -1, p: 1, q: 0 },
9
+ { a: 0, sign: 1, p: 2, q: 1 },
10
+ { a: 0, sign: -1, p: 2, q: 1 },
11
+ { a: 1, sign: 1, p: 0, q: 2 },
12
+ { a: 1, sign: -1, p: 0, q: 2 },
13
+ ];
14
+ const SPLIT = 1 / 3;
15
+ const onFace = (f, p, q) => {
16
+ const v = [0, 0, 0];
17
+ v[f.a] = f.sign;
18
+ v[f.p] = p;
19
+ v[f.q] = q;
20
+ return norm(v);
21
+ };
22
+ /** The 18 panels of a ball. `colours[i]` is the three strips of face i, or one colour for the middle strip and one for the outer two. */
23
+ export function strips18(s, colours, seam, width = 1.1) {
24
+ let out = "";
25
+ FACES.forEach((f, fi) => {
26
+ const [outer, middle] = colours[fi % colours.length];
27
+ const bounds = [-1, -SPLIT, SPLIT, 1];
28
+ for (let i = 0; i < 3; i++) {
29
+ const ring = [onFace(f, -1, bounds[i]), onFace(f, 1, bounds[i]), onFace(f, 1, bounds[i + 1]), onFace(f, -1, bounds[i + 1])];
30
+ out += s.panel(ring, i === 1 ? middle : outer, { n: 6, ink: false });
31
+ }
32
+ });
33
+ const curves = [];
34
+ for (const f of FACES)
35
+ for (const q of [-SPLIT, SPLIT])
36
+ curves.push(arc(onFace(f, -1, q), onFace(f, 1, q), 10));
37
+ for (const a of [-1, 1])
38
+ for (const b of [-1, 1]) {
39
+ curves.push(arc(norm([a, b, -1]), norm([a, b, 1]), 12));
40
+ curves.push(arc(norm([a, -1, b]), norm([a, 1, b]), 12));
41
+ curves.push(arc(norm([-1, a, b]), norm([1, a, b]), 12));
42
+ }
43
+ out += s.strokes(curves, seam, width);
44
+ out += s.strokes(curves, "#fff", width * 0.35, { deco: true, opacity: 0.4, dx: 0.25, dy: 0.3 });
45
+ return out;
46
+ }
47
+ /** The 8 panels of an octahedral ball: the octants, coloured by `pick(sx, sy, sz)`. */
48
+ export function octants8(s, pick, seam, width = 1.1) {
49
+ let out = "";
50
+ for (const sx of [-1, 1])
51
+ for (const sy of [-1, 1])
52
+ for (const sz of [-1, 1])
53
+ out += s.panel([[sx, 0, 0], [0, sy, 0], [0, 0, sz]], pick(sx, sy, sz), { n: 12 });
54
+ const circles = [greatCircle([1, 0, 0], 48), greatCircle([0, 1, 0], 48), greatCircle([0, 0, 1], 48)];
55
+ out += s.strokes(circles, seam, width, { closed: true });
56
+ out += s.strokes(circles, "#fff", width * 0.35, { closed: true, deco: true, opacity: 0.4, dx: 0.25, dy: 0.3 });
57
+ return out;
58
+ }
59
+ /** A darker seam colour for a main colour. */
60
+ export const seamOf = (base, amount = -0.45) => mix(base, amount);
@@ -0,0 +1,16 @@
1
+ import type { V3 } from "./ball.math.ts";
2
+ import type { Surface } from "./ball.engine.ts";
3
+ /** The point on the meridian at angle `a` from the top (+y) toward +x, in the plane z = 0. */
4
+ export declare const meridian: (a: number) => V3;
5
+ /** The lace of an American or Canadian football: `count` laces across a slot along the top meridian. */
6
+ export declare function laces(s: Surface, lace: string, slot: string, count?: number, span?: number, reach?: number): string;
7
+ /** The four seams of a panelled football: two great circles through the tips, at right angles. */
8
+ export declare const footballSeams: (rotate?: number) => V3[][];
9
+ /** A ring round the ball, near one tip: the white stripes of a Canadian football. */
10
+ export declare const tipRing: (sign: 1 | -1, angle: number, n?: number) => V3[];
11
+ /** A point on the long axis' meridian at `lon` round the axis, `along` radians from the equator toward +x. */
12
+ export declare const onBall: (along: number, round: number) => V3;
13
+ /** The panel between the seam at `round` radians round the long axis and the seam a quarter turn on: a lune from tip to tip. Its first three points are the first seam. */
14
+ export declare const lune: (round: number) => V3[];
15
+ /** The seam at `round` radians round the long axis: from one tip to the other, sampled along the sphere. */
16
+ export declare const seamAt: (round: number) => V3[];
@@ -0,0 +1,29 @@
1
+ // The prolate balls: American and Canadian football, rugby and Australian football. Each is an ellipsoid longer along x, with four panels whose seams run tip to tip.
2
+ import { add, arc, greatCircle, norm, slerp, smallCircle } from "./ball.math.js";
3
+ /** The point on the meridian at angle `a` from the top (+y) toward +x, in the plane z = 0. */
4
+ export const meridian = (a) => [Math.sin(a), Math.cos(a), 0];
5
+ /** The lace of an American or Canadian football: `count` laces across a slot along the top meridian. */
6
+ export function laces(s, lace, slot, count = 8, span = 0.62, reach = 0.17) {
7
+ const top = Array.from({ length: 25 }, (_, i) => meridian(-span + (2 * span * i) / 24));
8
+ let out = s.stroke(top, slot, 1.2, { ink: true });
9
+ const cross = [];
10
+ for (let i = 0; i < count; i++) {
11
+ const a = -span * 0.82 + (2 * span * 0.82 * i) / (count - 1);
12
+ const m = meridian(a);
13
+ cross.push([norm(add(m, [0, 0, -reach])), norm(add(m, [0, 0, reach]))]);
14
+ }
15
+ out += s.stroke(top, lace, 1.7, { ink: true });
16
+ for (const c of cross)
17
+ out += s.seam([c[0], slerp(c[0], c[1], 0.5), c[1]], lace, 1.9);
18
+ return out;
19
+ }
20
+ /** The four seams of a panelled football: two great circles through the tips, at right angles. */
21
+ export const footballSeams = (rotate = 0) => [greatCircle([0, Math.cos(rotate), Math.sin(rotate)], 96), greatCircle([0, -Math.sin(rotate), Math.cos(rotate)], 96)];
22
+ /** A ring round the ball, near one tip: the white stripes of a Canadian football. */
23
+ export const tipRing = (sign, angle, n = 72) => smallCircle([sign, 0, 0], angle, n);
24
+ /** A point on the long axis' meridian at `lon` round the axis, `along` radians from the equator toward +x. */
25
+ export const onBall = (along, round) => norm([Math.sin(along), Math.cos(along) * Math.cos(round), Math.cos(along) * Math.sin(round)]);
26
+ /** The panel between the seam at `round` radians round the long axis and the seam a quarter turn on: a lune from tip to tip. Its first three points are the first seam. */
27
+ export const lune = (round) => [onBall(Math.PI / 2, round), onBall(0, round), onBall(-Math.PI / 2, round), onBall(0, round + Math.PI / 2)];
28
+ /** The seam at `round` radians round the long axis: from one tip to the other, sampled along the sphere. */
29
+ export const seamAt = (round) => [...arc(onBall(Math.PI / 2, round), onBall(0, round), 10), ...arc(onBall(0, round), onBall(-Math.PI / 2, round), 10).slice(1)];
@@ -0,0 +1,31 @@
1
+ import { type BallDef } from "./ball.engine.ts";
2
+ import type { Piece } from "./ball.core.ts";
3
+ import type { BallSpec } from "./ball.engine.ts";
4
+ export interface SeamedOptions {
5
+ id: string;
6
+ en: string;
7
+ ja: string;
8
+ size: BallDef["size"];
9
+ base: string;
10
+ /** The seam's parameter b. */
11
+ b: number;
12
+ view?: BallSpec["view"];
13
+ mat: BallSpec["mat"];
14
+ /** A strip along the seam: its colour and width. */
15
+ strip?: {
16
+ colour: string;
17
+ width: number;
18
+ raised?: boolean;
19
+ };
20
+ /** Stitches across the seam: colour, count, the half-length across it (radians) and the stroke width. */
21
+ stitch?: {
22
+ colour: string;
23
+ count: number;
24
+ half: number;
25
+ width: number;
26
+ lean: number;
27
+ };
28
+ /** A thin groove along the seam. */
29
+ groove?: string;
30
+ }
31
+ export declare function seamedBall(o: SeamedOptions): Piece<BallSpec>;
@@ -0,0 +1,26 @@
1
+ // Balls with the seam of a baseball or a tennis ball: one closed curve that winds twice round the sphere, x=(1-b)cos t + b cos 3t, y=(1-b)sin t - b sin 3t, z=2sqrt(b(1-b)) sin 2t.
2
+ // A baseball and a softball have red stitches that cross it in Vs; a tennis or padel ball has a white rubber strip along it; a sliotar a raised rib.
3
+ import { defineBall } from "./ball.engine.js";
4
+ import { seamCurve } from "./ball.math.js";
5
+ import { stitches } from "./ball.stitch.js";
6
+ export function seamedBall(o) {
7
+ const spec = {
8
+ view: o.view ?? { yaw: 0, pitch: 0, roll: 0 },
9
+ base: o.base,
10
+ mat: o.mat,
11
+ draw: (s) => {
12
+ const seam = seamCurve(o.b, 160);
13
+ let out = "";
14
+ if (o.groove !== undefined)
15
+ out += s.stroke(seam, o.groove, 0.8, { closed: true, opacity: 0.55 });
16
+ if (o.strip !== undefined)
17
+ out += o.strip.raised === true ? s.seam(seam, o.strip.colour, o.strip.width, { closed: true }) : s.stroke(seam, o.strip.colour, o.strip.width, { closed: true });
18
+ if (o.stitch !== undefined)
19
+ out += s.strokes(stitches(seam, true, o.stitch.count, o.stitch.half, { v: true, lean: o.stitch.lean }), o.stitch.colour, o.stitch.width, { ink: false });
20
+ if (o.strip === undefined && o.stitch === undefined)
21
+ out += s.stroke(seam, "currentColor", 1.6, { closed: true });
22
+ return out;
23
+ },
24
+ };
25
+ return defineBall({ id: o.id, en: o.en, ja: o.ja, spec, size: o.size });
26
+ }
@@ -0,0 +1 @@
1
+ export declare const BALL_FIELD_HOCKEY: import("./ball.core.ts").Piece<import("./ball.engine.ts").BallSpec>;
@@ -0,0 +1,15 @@
1
+ // Field hockey ball: a hard white plastic ball with dimples.
2
+ import { dottedBall } from "./ball.family.dotted.js";
3
+ export const BALL_FIELD_HOCKEY = /* @__PURE__ */ dottedBall({
4
+ id: "ball:field-hockey",
5
+ en: "Field hockey ball",
6
+ ja: "フィールドホッケーボール",
7
+ size: { extentMm: 73.0, diameterMm: 73.0, circumferenceMm: 229.5, massG: [156, 163], rule: "FIH Rules of Hockey, Equipment section 3 Ball, 3.1 a-e", note: "Diameter worked out from the middle of the circumference range." },
8
+ base: "#f6f6f2",
9
+ count: 110,
10
+ radius: 0.088,
11
+ depth: 0.22,
12
+ light: false,
13
+ mark: "#6a7080",
14
+ mat: { gloss: 0.85, shade: "#2a3350", grain: undefined },
15
+ });
@@ -0,0 +1,3 @@
1
+ export declare const BALL_FOOTBALL: import("./ball.core.ts").Piece<import("./ball.engine.ts").BallSpec>;
2
+ export declare const BALL_FOOTBALL_CFL: import("./ball.core.ts").Piece<import("./ball.engine.ts").BallSpec>;
3
+ export declare const BALL_FOOTBALL_IFAF: import("./ball.core.ts").Piece<import("./ball.engine.ts").BallSpec>;
@@ -0,0 +1,51 @@
1
+ // American and Canadian football: a pebbled leather ball drawn as a prolate shape whose ends come to a point (`point` in the spec), a ball ("natural tan" in the NFL and IFAF rules), with four panels and a row of laces on one seam. The Canadian ball has a
2
+ // 1-inch white stripe all the way round near each end (CFL Rule 1, Section 2); the IFAF ball has a stripe near each end on the two panels next to the laces only (IFAF Rule 1, Section 3).
3
+ import { defineBall } from "./ball.engine.js";
4
+ import { footballSeams, laces, tipRing } from "./ball.family.prolate.js";
5
+ import { mix } from "./ball.core.js";
6
+ /** The part of a ring that lies on the laces' side (y at or above 0), as one run: the ring is turned to start below it. */
7
+ const upperArc = (ring) => {
8
+ const k = ring.findIndex((p) => p[1] < 0);
9
+ const turned = [...ring.slice(k), ...ring.slice(0, k)];
10
+ const run = [];
11
+ for (const p of turned)
12
+ if (p[1] >= 0)
13
+ run.push(p);
14
+ return run;
15
+ };
16
+ const BROWN = "#8b4a21";
17
+ const AXES = [1, 0.55, 0.55];
18
+ const VIEW = { yaw: -20, pitch: 35, roll: -35 };
19
+ const MAT = { gloss: 0.5, shade: "#2a1006", grain: { f: 0.95, a: 0.42 } };
20
+ function football(id, en, ja, size, stripes, colour) {
21
+ return defineBall({
22
+ id,
23
+ en,
24
+ ja,
25
+ size,
26
+ spec: {
27
+ axes: AXES,
28
+ point: 0.85,
29
+ view: VIEW,
30
+ base: colour,
31
+ mat: MAT,
32
+ draw: (s, base) => {
33
+ let out = "";
34
+ if (stripes === "ring")
35
+ for (const sign of [1, -1])
36
+ out += s.stroke(tipRing(sign, 0.99), "#f4f0e6", 7, { closed: true, ink: false });
37
+ if (stripes === "top")
38
+ for (const sign of [1, -1])
39
+ out += s.stroke(upperArc(tipRing(sign, 1.1, 96)), "#f4f0e6", 7, { ink: false });
40
+ out += s.strokes(footballSeams(), mix(base, -0.55), 1.1);
41
+ out += s.strokes(footballSeams(), mix(base, 0.4), 0.45, { deco: true, opacity: 0.45, dx: 0.25, dy: 0.3 });
42
+ out += laces(s, "#f4f0e6", mix(base, -0.7));
43
+ return out;
44
+ },
45
+ },
46
+ });
47
+ }
48
+ const SIZE = { extentMm: 282.6, lengthMm: 282.6, circumferenceMm: 536.58, longCircumferenceMm: 717.55, massG: [396.89, 425.24], rule: "NFL Rulebook, Rule 2 The Ball, Section 1 (Dimensions)" };
49
+ export const BALL_FOOTBALL = /* @__PURE__ */ football("ball:football", "Football (NFL)", "アメリカンフットボールのボール(NFL)", SIZE, "none", BROWN);
50
+ export const BALL_FOOTBALL_CFL = /* @__PURE__ */ football("ball:football-cfl", "Football (CFL)", "カナディアンフットボールのボール(CFL)", { extentMm: 282.6, lengthMm: 282.6, circumferenceMm: 536.58, longCircumferenceMm: 717.55, massG: [396.89, 425.24], rule: "CFL Official Playing Rules, Rule 1 Conduct of the Game, Section 2 The Ball" }, "ring", BROWN);
51
+ export const BALL_FOOTBALL_IFAF = /* @__PURE__ */ football("ball:football-ifaf", "Football (IFAF)", "アメリカンフットボールのボール(IFAF)", { extentMm: 283.4, lengthMm: 283.4, circumferenceMm: 533.4, longCircumferenceMm: 714.38, massG: [396.89, 425.24], rule: "IFAF Football Rules and Interpretations 2026, Rule 1 (The Game, Field, Players and Equipment), Section 3 The Ball, Article 1 a-i", note: "The IFAF figures are in a diagram of the rules, read from the rendered page." }, "top", "#a8693a");
@@ -0,0 +1,2 @@
1
+ export declare const BALL_GAELIC_FOOTBALL: import("./ball.core.ts").Piece<import("./ball.engine.ts").BallSpec>;
2
+ export declare const BALL_NETBALL: import("./ball.core.ts").Piece<import("./ball.engine.ts").BallSpec>;