@johnmorrisdotca/dougu-sports 0.2.0 → 0.3.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 (35) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/NOTICE.md +3 -1
  3. package/README.md +9 -8
  4. package/dist/emblems/ball/ball.afl.js +2 -1
  5. package/dist/emblems/ball/ball.curlingstone.d.ts +2 -0
  6. package/dist/emblems/ball/ball.curlingstone.js +11 -7
  7. package/dist/emblems/ball/ball.discus.js +1 -1
  8. package/dist/emblems/ball/ball.football.js +9 -7
  9. package/dist/emblems/ball/ball.index.js +3 -1
  10. package/dist/emblems/ball/ball.object.d.ts +5 -2
  11. package/dist/emblems/ball/ball.object.js +2 -1
  12. package/dist/emblems/ball/ball.puck.d.ts +2 -0
  13. package/dist/emblems/ball/ball.puck.js +2 -2
  14. package/dist/emblems/ball/ball.rugby.js +3 -2
  15. package/dist/emblems/ball/ball.shuttlecock.d.ts +4 -0
  16. package/dist/emblems/ball/ball.shuttlecock.js +4 -3
  17. package/dist/emblems/ball/ball.soccer.indoor.d.ts +2 -0
  18. package/dist/emblems/ball/ball.soccer.indoor.js +58 -0
  19. package/dist/emblems/ball/ball.soccer.js +3 -2
  20. package/dist/emblems/ball/ball.soccer.panels.d.ts +38 -0
  21. package/dist/emblems/ball/ball.soccer.panels.js +169 -0
  22. package/dist/emblems/ball/ball.soccer.size.d.ts +2 -0
  23. package/dist/emblems/ball/ball.soccer.size.js +8 -0
  24. package/dist/emblems/ball/ball.tilings.d.ts +61 -0
  25. package/dist/emblems/ball/ball.tilings.js +212 -0
  26. package/dist/emblems/emblem.descriptions.js +29 -0
  27. package/dist/emblems/emblem.flip.js +2 -1
  28. package/dist/emblems/emblem.ids.d.ts +2 -2
  29. package/dist/emblems/emblem.ids.js +1 -1
  30. package/dist/emblems/emblem.shapes.js +2 -1
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.js +2 -0
  33. package/dist/version.d.ts +1 -1
  34. package/dist/version.js +1 -1
  35. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -6,6 +6,31 @@ All notable changes to this project are written here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.3.0] - 2026-10-14
10
+
11
+ Nineteen more soccer balls (26, 18, 14, 12 and 6 panels, winter orange and high-visibility yellow, futsal and beach soccer), 186 emblems in all.
12
+
13
+ ### Added
14
+
15
+ - **Nineteen more soccer balls** ([docs/soccer-balls.md](docs/soccer-balls.md), [日本語](docs/soccer-balls.ja.md)), all in true 3D like `ball:soccer`, turning with `ballFrame`, with their seams on the sphere and every construction a generic derivation (no brand's or tournament's ball is used; the sources are in [docs/references.md](docs/references.md#soccer-balls)): `ball:soccer-26` (the rhombicuboctahedron: 8 triangles and 18 squares), `ball:soccer-18` (three strips on each face of a cube), `ball:soccer-14` (6 propellers and 8 triangles: a truncated cube whose 12 long edges are bent into S-curves), `ball:soccer-12` (the dodecahedron) and `ball:soccer-6` (thermobonded: six twisted gores with long curved seams, our own design).
16
+ - **Winter orange and high-visibility yellow** of the classic ball and of each new construction, as ids of their own (`ball:soccer-orange`, `ball:soccer-26-yellow`... twelve in all): `tint` replaces the colour and `accent` the dark panels.
17
+ - **Futsal and beach soccer**: `ball:futsal` (size 4, circumference 62 to 64 cm, 400 to 440 g, 0.6 to 0.9 atm, a 50 to 65 cm first bounce from 2 m: Futsal Laws of the Game 2025/26, Law 2) and `ball:beach-soccer` (size 5, 68 to 70 cm, 400 to 440 g, 0.4 to 0.6 atm: Beach Soccer Laws of the Game 2024-25, Law 2). Their rules are in `size.rule` and `size.note`, and `test/proportions.test.ts` holds every new ball to its rule's size.
18
+ - The package is now 186 emblems, 66 of them balls and implements.
19
+
20
+ ## [0.2.1] - 2026-10-14
21
+
22
+ Eleven balls and implements drawn to the rulebook's proportions, flat emblems turning with the hand, and the gallery as one page per language.
23
+
24
+ ### Fixed
25
+
26
+ - **Eleven balls and implements were drawn out of proportion to the rulebook, the puck by half.** The puck was drawn 1 : 6 (thickness to diameter) where IIHF Rule 13 makes it 1 : 3, because the half thickness was divided by two twice; the curling stone's body was 0.200 of its diameter where the rules (a 914 mm circumference, a height of at least 114 mm) make it 0.392, and its handle now stands on the taller body; the feather shuttlecocks' feathers were 50 mm long where the Law gives 62 to 70 mm (now 66 mm); the discus was 2.2% too thin; the American, Canadian and IFAF footballs were 9% too slim and the rugby ball 6% too slim, with their outlines 4 to 7% too short; the Australian football was 12% too fat. The footballs and the rugby ball now take their width from the girth (the circumference over pi) and their `point` from the long girth, and the Australian football is an ellipsoid of its two girths. Nothing in the `size` metadata changed. A new test, `test/proportions.test.ts`, draws every ball and implement edge-on, measures the drawing itself (not the constants it was made from) and holds its ratio, size and, for the pointed balls, outline to the figures in `size` within 2%; it also checks that only a ball or an implement claims a real size, and that `lineup` at the true scale gives every piece the same pixels per millimetre. The ball contact sheets in `docs/pieces` are redrawn.
27
+
28
+ - **A flat thing turned the other way from a ball and a cup.** A ball rolls and a cup slides its light so that a positive yaw carries the front toward the right (and a drag to the right, or the Right arrow, is a positive yaw), but the coin turn of a medal, a crest, a pictogram, a piece of gear and a pennant showed its rim on the right, as if the front went left, so that dragging a medal to the right turned it against the hand. The rim is now on the left for a positive yaw (and on the right for a negative one), the edge that comes toward the eye, in the still and the line style alike. Only the side of the rim changes: the squeeze, the back and every angle are as before, `yaw` and `setAngles` mean the same, and `bindTurn` keeps its signs (right is more yaw, up is more pitch). A unit test holds the rim's side for every flat emblem and the ball's roll, and the demo's tests drag a ball, a cup and a medal to the right and measure which way each goes.
29
+
30
+ ### Changed
31
+
32
+ - **The gallery is two pages, one language each.** `docs/gallery.md` (English) and `docs/gallery.ja.md` (日本語) are both written by `scripts/make-gallery.mjs`, linked to each other at the top, with the columns Id, name, kind, source and description. The Constant and Import columns are gone (the long paths made GitHub squeeze the Japanese column to one character wide, so the names stacked): each page states once that the constant is the id in capitals (`trophy:lidded-cup` becomes `TROPHY_LIDDED_CUP`) and that it is imported from `@johnmorrisdotca/dougu-sports/trophy/lidded-cup`, and the generator stops if the rule fails for any emblem. `pnpm test:readme` checks both pages and accepts the rule as naming each emblem's constant.
33
+
9
34
  ## [0.2.0] - 2026-10-14
10
35
 
11
36
  The package is now 167 emblems (24 trophies, 5 awards, 6 medals, 4 crests, 38 sports, 47 balls, 42 gear and a pennant) and every one is its own drawing: the copied art and its licences are gone, the five Japanese sports and budo (sumo, kendo, judo, kyudo, karate) are in, text can be engraved on medals, plaques, cups, a ring, a banner and a pennant, every emblem turns through one coin-flip turn, and there are labels in two languages, data addresses, sprites, a podium, an award case, `sportEquipment` for all 38 sports and `bindTurn` to turn an emblem with a drag, the keys or a clock.
@@ -75,7 +100,8 @@ First preview: the name, the API and two sample emblems. The full set of trophie
75
100
  - Tests for the drawing, the ids, the turns and the bundle size of one item; a package check that installs the packed tarball and bundles one item from it.
76
101
  - The demo (a playground with a turn slider, and a gallery page), `docs/layout.md`, `docs/turning.md`, `docs/gallery.md`, the family header and footer, the CI, Pages and Release workflows, and `LICENSES/` and `NOTICE.md` ready for third-party art.
77
102
 
78
- [Unreleased]: https://github.com/johnmorrisdotca/dougu-sports/compare/v0.2.0...HEAD
103
+ [Unreleased]: https://github.com/johnmorrisdotca/dougu-sports/compare/v0.2.1...HEAD
104
+ [0.2.1]: https://github.com/johnmorrisdotca/dougu-sports/compare/v0.2.0...v0.2.1
79
105
  [0.2.0]: https://github.com/johnmorrisdotca/dougu-sports/compare/v0.1.0...v0.2.0
80
106
  [0.1.0]: https://github.com/johnmorrisdotca/dougu-sports/compare/v0.0.1...v0.1.0
81
107
  [0.0.1]: https://github.com/johnmorrisdotca/dougu-sports/releases/tag/v0.0.1
package/NOTICE.md CHANGED
@@ -21,7 +21,9 @@ Each ball's `size.rule` names the rule; the table gives the document and where i
21
21
 
22
22
  | Balls | Rulebook and rule | Read at |
23
23
  | --- | --- | --- |
24
- | `ball:soccer`, `ball:soccer-colour` | IFAB Laws of the Game, Law 2 The Ball, section 2.1 Qualities and measurements | https://www.theifab.com/laws/latest/the-ball/ |
24
+ | `ball:soccer`, `ball:soccer-colour`, `ball:soccer-26`, `ball:soccer-18`, `ball:soccer-14`, `ball:soccer-12`, `ball:soccer-6`, `ball:soccer-orange`, `ball:soccer-26-orange`, `ball:soccer-18-orange`, `ball:soccer-14-orange`, `ball:soccer-12-orange`, `ball:soccer-6-orange`, `ball:soccer-yellow`, `ball:soccer-26-yellow`, `ball:soccer-18-yellow`, `ball:soccer-14-yellow`, `ball:soccer-12-yellow`, `ball:soccer-6-yellow` | IFAB Laws of the Game, Law 2 The Ball, section 2.1 Qualities and measurements | https://www.theifab.com/laws/latest/the-ball/ |
25
+ | `ball:futsal` | Futsal Laws of the Game 2025/26, Law 2 The Ball, section 1 Qualities and measurements (circumference 62-64 cm, 400-440 g, 0.6-0.9 atm, bounce 50-65 cm from 2 m) | https://digitalhub.fifa.com/m/20d52e6779b53321/original/FUTSAL-Laws-of-the-Game-2025-2026.pdf |
26
+ | `ball:beach-soccer` | Beach Soccer Laws of the Game 2024-25, Law 2 The Ball, section 1 Qualities and measurements (circumference 68-70 cm, 400-440 g, 0.4-0.6 atm); the 2021/22 edition has the same figures | https://www.the-aiff.com/media/uploads/2024/11/Beach-Soccer-Laws-of-the-Game-2024-25.pdf |
25
27
  | `ball:teqball` | FITEQ Official Rules and Regulations of Teqball 2026, Rule 3 The Official Ball, 3.1.1-3.1.6 | https://cdn.buttercms.com/s6Mb1gRkRJuFP38kpMTh |
26
28
  | `ball:basketball` | FIBA Basketball Equipment 2026, Rule 2 Basketballs, 2.4 and Table 1 (size 7 = men's competitions) | https://assets.fiba.basketball/image/upload/documents-corporate-fiba-official-rules-2026-equipment-v1-0.pdf |
27
29
  | `ball:basketball-6` | FIBA Basketball Equipment 2026, Rule 2 Basketballs, 2.4 and Table 1 (size 6 = women's competitions) | https://assets.fiba.basketball/image/upload/documents-corporate-fiba-official-rules-2026-equipment-v1-0.pdf |
package/README.md CHANGED
@@ -31,7 +31,7 @@ Trophies, medals, crests, the icon of every sport, and the ball and gear of the
31
31
  </tr>
32
32
  </table>
33
33
 
34
- > **Status: preview.** 167 emblems: 24 trophies, 5 awards, 6 medals, 4 crests, a pictogram for each of the 38 sports, 47 balls and implements drawn in 3D, 42 pieces of gear (among them the Japanese sports and budo, [docs/japanese-sports.md](./docs/japanese-sports.md)) and a championship pennant; [docs/layout.md](./docs/layout.md) says where each goes. All of it is drawn for this package: no art is copied from another project.
34
+ > **Status: preview.** 186 emblems: 24 trophies, 5 awards, 6 medals, 4 crests, a pictogram for each of the 38 sports, 66 balls and implements drawn in 3D, 42 pieces of gear (among them the Japanese sports and budo, [docs/japanese-sports.md](./docs/japanese-sports.md)) and a championship pennant; [docs/layout.md](./docs/layout.md) says where each goes. All of it is drawn for this package: no art is copied from another project.
35
35
 
36
36
  ## In 30 seconds
37
37
 
@@ -83,7 +83,7 @@ Put the string in a page (`element.innerHTML = svg`), write it to a file, or sen
83
83
  </tr>
84
84
  </table>
85
85
 
86
- Every emblem, with its id in both styles and a button that copies it, is on the [gallery page](https://johnmorrisdotca.github.io/dougu-sports/gallery.html); the same list as text, with its names, kind, source and the import that takes it alone, is [docs/gallery.md](./docs/gallery.md), made from the built package. [docs/emblems.md](./docs/emblems.md) says how the art is drawn.
86
+ Every emblem, with its id in both styles and a button that copies it, is on the [gallery page](https://johnmorrisdotca.github.io/dougu-sports/gallery.html); the same list as text, with its name, kind, source and description, is [docs/gallery.md](./docs/gallery.md) ([日本語](./docs/gallery.ja.md)), made from the built package. [docs/emblems.md](./docs/emblems.md) says how the art is drawn.
87
87
 
88
88
  ## Use it in your project
89
89
 
@@ -392,7 +392,7 @@ An emblem is a small object: `id`, `en` and `ja` names, a `source` (who drew it)
392
392
  | `trophy` | Twenty-four trophies: ten that stand for a league's cup in a generic form (a tall banded cup, a ring of pennants round a baseball, a football on a stand, a ball over a net, a wide two-handled bowl, a globe held up by spiral figures, a gilt dish, a lidded cup with a pineapple, an urn, a gilded cup), plain cups, bowls and dishes, a plaque, a belt, a wreath, a rosette, a championship ring, a rafter banner, a phoenix-handled sumo cup and a championship banner | `src/emblems/trophy/` |
393
393
  | `medal` | Gold, silver and bronze medals on a ribbon of five stripes, and a badge for each of the first three places | `src/emblems/medal/` |
394
394
  | `crest` | Four generic crests, tinted by a team's colours, with its letters | `src/emblems/crest/` |
395
- | `ball` | 47 balls and implements, in 3D, each with its official size: soccer, basketball, baseball, tennis, rugby, the footballs, golf, the puck, the curling stone and the rest | `src/emblems/ball/` |
395
+ | `ball` | 66 balls and implements, in 3D, each with its official size: soccer (in 32, 26, 18, 14, 12 and 6 panels, in three colourways, with futsal and beach soccer; [docs/soccer-balls.md](./docs/soccer-balls.md)), basketball, baseball, tennis, rugby, the footballs, golf, the puck, the curling stone and the rest | `src/emblems/ball/` |
396
396
  | `gear` | 42 pieces of equipment: sticks, rackets, skates, a glove, a helmet, nets and posts, a javelin, starting blocks, and for the Japanese sports a war fan, a sumo belt, a bamboo sword, a kendo helmet, a judo suit, a belt, a long bow, an arrow and a karate suit | `src/emblems/gear/` |
397
397
  | `sport` | One pictogram for each of the thirty-eight sports | `src/emblems/sport/` |
398
398
  | `pennant` | A felt championship pennant on a dowel, with a stitched edge, lettered with its words and its year | `src/emblems/pennant/` |
@@ -424,7 +424,7 @@ The thirty-eight sports the ball, gear and sport emblems are made for are `SPORT
424
424
  | `emblemInfo(id)`, `emblemName(id, language?)`, `emblemModel(id)` | What an emblem is: its short names (which are its accessible names), kind and source, a longer `description` where it has one, and the data it is drawn from |
425
425
  | `EMBLEMS` | Every emblem as markup in the metal style at 64 pixels, by id |
426
426
  | `EMBLEM_TABLE` | Every emblem object, by id |
427
- | `TROPHY_HOCKEY_CUP`, `MEDAL_GOLD`, `CREST_SHIELD`, `SPORT_NHL`, `BALL_SOCCER`, `GEAR_HURLEY`... | Each emblem as a constant, named by its id in capitals (`trophy:hockey-cup`); [docs/gallery.md](./docs/gallery.md) lists every constant with its id and the module that takes it alone |
427
+ | `TROPHY_HOCKEY_CUP`, `MEDAL_GOLD`, `CREST_SHIELD`, `SPORT_NHL`, `BALL_SOCCER`, `GEAR_HURLEY`... | Each emblem as a constant, named by its id in capitals (`trophy:hockey-cup`); [docs/gallery.md](./docs/gallery.md) ([日本語](./docs/gallery.ja.md)) lists every emblem and states the rule for its constant and the module that takes it alone |
428
428
  | `EMBLEM_IDS`, `EMBLEM_GROUPS`, `EMBLEM_FOLDERS`, `EMBLEM_ALIASES` | Every id, the groups, the folder of each group's art, and the short names (`cup` for `trophy:cup`) |
429
429
  | `AWARD_IDS`, `TROPHY_IDS`, `MEDAL_IDS`, `CREST_IDS`, `BALL_IDS`, `GEAR_IDS`, `SPORT_IDS`, `PENNANT_IDS` | The ids of each group |
430
430
  | `MEDAL_KINDS`, `CREST_SHAPES` | The kinds of medal, and the shapes of crest |
@@ -523,7 +523,7 @@ Every emblem is drawn for this package, and nothing is copied from another proje
523
523
  ### The family
524
524
 
525
525
  <!-- family:start (made by scripts/family-readme.mjs from scripts/family-template.mjs; change those, not this) -->
526
- Dougu Sports is one of twenty-seven packages, each made for the same site, each at
526
+ Dougu Sports is one of twenty-eight packages, each made for the same site, each at
527
527
  [github.com/johnmorrisdotca](https://github.com/johnmorrisdotca). The code of every one is MIT.
528
528
 
529
529
  - [Korokoro](https://github.com/johnmorrisdotca/korokoro) (コロコロ): dice, with notation, exact odds, real sounds and the dice of many games. [Demo](https://johnmorrisdotca.github.io/korokoro/).
@@ -553,8 +553,9 @@ Dougu Sports is one of twenty-seven packages, each made for the same site, each
553
553
  - [Kyogijo Stadiums](https://github.com/johnmorrisdotca/kyogijo-stadiums) (競技場): sports fields, rinks, courts and ballparks drawn to scale as SVG, with each team and the score on them. [Demo](https://johnmorrisdotca.github.io/kyogijo-stadiums/).
554
554
  - [Taisen Brackets](https://github.com/johnmorrisdotca/taisen-brackets) (対戦): a sport-agnostic tournament bracket, drawn as SVG, from a list of ties. [Demo](https://johnmorrisdotca.github.io/taisen-brackets/).
555
555
  - [Dougu Sports](https://github.com/johnmorrisdotca/dougu-sports) (道具): sports emblems as SVG: trophies, medals, crests, and the ball and gear of every sport, with optional 3D turning. [Demo](https://johnmorrisdotca.github.io/dougu-sports/).
556
+ - [Senshu Jerseys](https://github.com/johnmorrisdotca/senshu-jerseys) (選手): sports jerseys, sweaters, kits, caps, helmets and retired-number banners as SVG, front and back, with optional 2.5D turning. [Demo](https://johnmorrisdotca.github.io/senshu-jerseys/).
556
557
 
557
- **This package is Dougu Sports.** The demos of all twenty-seven share one header and footer, so each links the rest.
558
+ **This package is Dougu Sports.** The demos of all twenty-eight share one header and footer, so each links the rest.
558
559
  <!-- family:end -->
559
560
 
560
561
  ## Development
@@ -564,7 +565,7 @@ pnpm install
564
565
  pnpm check # lint, types, tests, the build and the packed package
565
566
  pnpm dev # the demo on http://127.0.0.1:7200/
566
567
  pnpm test:demo # the demo in a real browser
567
- pnpm test:readme # the README's and the docs' examples, and docs/gallery.md
568
+ pnpm test:readme # the README's and the docs' examples, and docs/gallery.md and docs/gallery.ja.md
568
569
  pnpm screenshots # retake the README's pictures
569
570
  ```
570
571
 
@@ -576,7 +577,7 @@ Bug reports and ideas go in the [issues](https://github.com/johnmorrisdotca/doug
576
577
 
577
578
  ## Changes
578
579
 
579
- See [CHANGELOG.md](./CHANGELOG.md). 0.2.0 is 167 emblems, every one drawn here, with the Japanese sports, engraving, a turn for every emblem and the compose helpers; 0.1.0 brought the first full set of 148; 0.0.1 was the first preview.
580
+ See [CHANGELOG.md](./CHANGELOG.md). 0.3.0 adds nineteen soccer balls (26, 18, 14, 12 and 6 panels, winter orange and high-visibility yellow, futsal and beach soccer: 186 emblems); 0.2.1 draws every ball and implement to the rulebook's proportions; 0.2.0 is 167 emblems, every one drawn here, with the Japanese sports, engraving, a turn for every emblem and the compose helpers; 0.1.0 brought the first full set of 148; 0.0.1 was the first preview.
580
581
 
581
582
  ## Credits
582
583
 
@@ -9,7 +9,8 @@ export const BALL_AFL = /* @__PURE__ */ defineBall({
9
9
  ja: "オージーボール",
10
10
  size: { extentMm: 280, lengthMm: 280, circumferenceMm: 550.0, longCircumferenceMm: 725.0, rule: "Laws of Australian Football 2026, Law 4 The Football, 4.1 Specifications", note: "The Laws give the two circumferences (720-730 mm and 545-555 mm) and no length: 280 mm is the length of an ellipse of those two girths." },
11
11
  spec: {
12
- axes: [1, 0.7, 0.7],
12
+ // An ellipsoid of the Laws' two girths: 550 mm round the middle makes it 175 mm across, 280 mm long.
13
+ axes: [1, 0.6253, 0.6253],
13
14
  view: { yaw: -30, pitch: 20, roll: -25 },
14
15
  base: "#b4202b",
15
16
  mat: { gloss: 0.55, shade: "#2a0408", grain: { f: 1.0, a: 0.34 } },
@@ -1,3 +1,5 @@
1
1
  export declare const BALL_CURLING_STONE: import("./ball.core.ts").Piece<{
2
2
  slices: number;
3
+ } & {
4
+ view: import("./ball.object.ts").ObjectView;
3
5
  }>;
@@ -7,6 +7,10 @@ import { num } from "./ball.core.js";
7
7
  const GRANITE = "#8d929a";
8
8
  const POLISHED = "#4d525b";
9
9
  const HANDLE = "#d4202c";
10
+ // The stack's y is in units of the radius, so the half height is the height of the stone (114 mm at least) over its largest diameter (291 mm, a 914 mm circumference): 0.392, and the stone runs from -H to H. The slices
11
+ // below are where the profile changes, as a fraction of H.
12
+ const H = 114 / 290.9;
13
+ const y = (t) => t * H;
10
14
  export const BALL_CURLING_STONE = /* @__PURE__ */ defineObject({
11
15
  id: "ball:curling-stone",
12
16
  en: "Curling stone",
@@ -16,11 +20,11 @@ export const BALL_CURLING_STONE = /* @__PURE__ */ defineObject({
16
20
  model: { slices: 6 },
17
21
  draw: (c) => drawStack(c, {
18
22
  slices: [
19
- { y0: -0.2, y1: -0.15, r: [0.74, 0.8], colour: POLISHED, outline: true },
20
- { y0: -0.15, y1: -0.04, r: [0.8, 0.94], colour: GRANITE },
21
- { y0: -0.04, y1: 0.08, r: 1, colour: POLISHED, outline: true },
22
- { y0: 0.08, y1: 0.15, r: [1, 0.9], colour: GRANITE },
23
- { y0: 0.15, y1: 0.2, r: [0.9, 0.78], colour: GRANITE, outline: true },
23
+ { y0: y(-1), y1: y(-0.75), r: [0.74, 0.8], colour: POLISHED, outline: true },
24
+ { y0: y(-0.75), y1: y(-0.2), r: [0.8, 0.94], colour: GRANITE },
25
+ { y0: y(-0.2), y1: y(0.4), r: 1, colour: POLISHED, outline: true },
26
+ { y0: y(0.4), y1: y(0.75), r: [1, 0.9], colour: GRANITE },
27
+ { y0: y(0.75), y1: y(1), r: [0.9, 0.78], colour: GRANITE, outline: true },
24
28
  ],
25
29
  top: "#9a9fa7",
26
30
  bottom: "#6a6f78",
@@ -30,8 +34,8 @@ export const BALL_CURLING_STONE = /* @__PURE__ */ defineObject({
30
34
  dish: { r: 0.74, colour: "#555a63" },
31
35
  extra: (ctx) => {
32
36
  // The handle: a post and a bar, a capsule in 3D (a thick line with round ends).
33
- const post0 = ctx.at([0, 0.2, 0]), post1 = ctx.at([0, 0.34, 0]);
34
- const a = [-0.42, 0.4, 0], b = [0.42, 0.4, 0];
37
+ const post0 = ctx.at([0, H, 0]), post1 = ctx.at([0, H + 0.14, 0]);
38
+ const a = [-0.42, H + 0.2, 0], b = [0.42, H + 0.2, 0];
35
39
  const p = ctx.at(a), q = ctx.at(b);
36
40
  const colour = ctx.opts.style === "line" ? "currentColor" : (ctx.opts.tint ?? HANDLE);
37
41
  const line = ctx.opts.style === "line";
@@ -7,7 +7,7 @@ export const BALL_DISCUS = /* @__PURE__ */ defineBall({
7
7
  ja: "円盤",
8
8
  size: { extentMm: 220, diameterMm: 220, thicknessMm: 45, massG: [2000, 2000], rule: "World Athletics Technical Rules (C2.1), Rule 34 Discus Throw - 'The discus shall conform to the following specifications'", note: "Men's discus, 219 to 221 mm across the rim and 44 to 46 mm thick at the middle; 2 kg is a minimum." },
9
9
  spec: {
10
- axes: [1, 0.2, 1],
10
+ axes: [1, 45 / 220, 1],
11
11
  view: { yaw: 25, pitch: 52, roll: -12 },
12
12
  base: "#3b3f48",
13
13
  mat: { gloss: 0.7, shade: "#050608", grain: { f: 1.4, a: 0.12 } },
@@ -14,18 +14,19 @@ const upperArc = (ring) => {
14
14
  return run;
15
15
  };
16
16
  const BROWN = "#8b4a21";
17
- const AXES = [1, 0.55, 0.55];
17
+ // The shape is worked out from the rule's figures (see test/proportions.test.ts): the width over the length is the girth over pi over the length, and `point` is chosen so that the outline, tip round to tip,
18
+ // is the long girth.
18
19
  const VIEW = { yaw: -20, pitch: 35, roll: -35 };
19
20
  const MAT = { gloss: 0.5, shade: "#2a1006", grain: { f: 0.95, a: 0.42 } };
20
- function football(id, en, ja, size, stripes, colour) {
21
+ function football(id, en, ja, size, shape, stripes, colour) {
21
22
  return defineBall({
22
23
  id,
23
24
  en,
24
25
  ja,
25
26
  size,
26
27
  spec: {
27
- axes: AXES,
28
- point: 0.85,
28
+ axes: [1, shape.width, shape.width],
29
+ point: shape.point,
29
30
  view: VIEW,
30
31
  base: colour,
31
32
  mat: MAT,
@@ -45,7 +46,8 @@ function football(id, en, ja, size, stripes, colour) {
45
46
  },
46
47
  });
47
48
  }
49
+ const NFL_SHAPE = { width: 0.6044, point: 0.541 };
48
50
  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");
51
+ export const BALL_FOOTBALL = /* @__PURE__ */ football("ball:football", "Football (NFL)", "アメフトボール(NFL)", SIZE, NFL_SHAPE, "none", BROWN);
52
+ 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" }, NFL_SHAPE, "ring", BROWN);
53
+ 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." }, { width: 0.599, point: 0.567 }, "top", "#a8693a");
@@ -23,6 +23,8 @@ import { BALL_SHUTTLECOCK_FEATHER, BALL_SHUTTLECOCK_NYLON } from "./ball.shuttle
23
23
  import { BALL_SLIOTAR } from "./ball.sliotar.js";
24
24
  import { BALL_LACROSSE, BALL_RACQUETBALL, BALL_TABLE_TENNIS, BALL_SHOT } from "./ball.smooth.js";
25
25
  import { BALL_SOCCER, BALL_SOCCER_COLOUR } from "./ball.soccer.js";
26
+ import { BALL_SOCCER_26, BALL_SOCCER_18, BALL_SOCCER_14, BALL_SOCCER_12, BALL_SOCCER_6, BALL_SOCCER_ORANGE, BALL_SOCCER_26_ORANGE, BALL_SOCCER_18_ORANGE, BALL_SOCCER_14_ORANGE, BALL_SOCCER_12_ORANGE, BALL_SOCCER_6_ORANGE, BALL_SOCCER_YELLOW, BALL_SOCCER_26_YELLOW, BALL_SOCCER_18_YELLOW, BALL_SOCCER_14_YELLOW, BALL_SOCCER_12_YELLOW, BALL_SOCCER_6_YELLOW } from "./ball.soccer.panels.js";
27
+ import { BALL_FUTSAL, BALL_BEACH_SOCCER } from "./ball.soccer.indoor.js";
26
28
  import { BALL_SOFTBALL } from "./ball.softball.js";
27
29
  import { BALL_SQUASH_DOUBLE_YELLOW, BALL_SQUASH_YELLOW, BALL_SQUASH_RED, BALL_SQUASH_BLUE, BALL_SQUASH_GREEN } from "./ball.squash.js";
28
30
  import { BALL_TENNIS } from "./ball.tennis.js";
@@ -30,7 +32,7 @@ import { BALL_TEQBALL } from "./ball.teqball.js";
30
32
  import { BALL_VOLLEYBALL, BALL_VOLLEYBALL_8, BALL_BEACH_VOLLEYBALL } from "./ball.volleyball.js";
31
33
  import { BALL_WATER_POLO } from "./ball.waterpolo.js";
32
34
  /** Every ball, in the order a gallery shows them. */
33
- export const BALLS = [BALL_SOCCER, BALL_SOCCER_COLOUR, BALL_TEQBALL, BALL_BASKETBALL, BALL_BASKETBALL_6, BALL_BASKETBALL_3X3, BALL_BASEBALL, BALL_SOFTBALL, BALL_TENNIS, BALL_PADEL, BALL_SLIOTAR, BALL_VOLLEYBALL, BALL_VOLLEYBALL_8, BALL_BEACH_VOLLEYBALL, BALL_CRICKET_RED, BALL_CRICKET_WHITE, BALL_CRICKET_PINK, BALL_FOOTBALL, BALL_FOOTBALL_CFL, BALL_FOOTBALL_IFAF, BALL_RUGBY, BALL_AFL, BALL_GAELIC_FOOTBALL, BALL_NETBALL, BALL_PICKLEBALL_OUTDOOR, BALL_PICKLEBALL_INDOOR, BALL_SQUASH_DOUBLE_YELLOW, BALL_SQUASH_YELLOW, BALL_SQUASH_GREEN, BALL_SQUASH_RED, BALL_SQUASH_BLUE, BALL_RACQUETBALL, BALL_LACROSSE, BALL_FIELD_HOCKEY, BALL_TABLE_TENNIS, BALL_HANDBALL, BALL_WATER_POLO, BALL_GOLF, BALL_PUCK, BALL_CURLING_STONE, BALL_SHOT, BALL_DISCUS, BALL_HAMMER, BALL_SEPAK_TAKRAW, BALL_ROUNDNET, BALL_SHUTTLECOCK_FEATHER, BALL_SHUTTLECOCK_NYLON];
35
+ export const BALLS = [BALL_SOCCER, BALL_SOCCER_COLOUR, BALL_SOCCER_26, BALL_SOCCER_18, BALL_SOCCER_14, BALL_SOCCER_12, BALL_SOCCER_6, BALL_SOCCER_ORANGE, BALL_SOCCER_26_ORANGE, BALL_SOCCER_18_ORANGE, BALL_SOCCER_14_ORANGE, BALL_SOCCER_12_ORANGE, BALL_SOCCER_6_ORANGE, BALL_SOCCER_YELLOW, BALL_SOCCER_26_YELLOW, BALL_SOCCER_18_YELLOW, BALL_SOCCER_14_YELLOW, BALL_SOCCER_12_YELLOW, BALL_SOCCER_6_YELLOW, BALL_FUTSAL, BALL_BEACH_SOCCER, BALL_TEQBALL, BALL_BASKETBALL, BALL_BASKETBALL_6, BALL_BASKETBALL_3X3, BALL_BASEBALL, BALL_SOFTBALL, BALL_TENNIS, BALL_PADEL, BALL_SLIOTAR, BALL_VOLLEYBALL, BALL_VOLLEYBALL_8, BALL_BEACH_VOLLEYBALL, BALL_CRICKET_RED, BALL_CRICKET_WHITE, BALL_CRICKET_PINK, BALL_FOOTBALL, BALL_FOOTBALL_CFL, BALL_FOOTBALL_IFAF, BALL_RUGBY, BALL_AFL, BALL_GAELIC_FOOTBALL, BALL_NETBALL, BALL_PICKLEBALL_OUTDOOR, BALL_PICKLEBALL_INDOOR, BALL_SQUASH_DOUBLE_YELLOW, BALL_SQUASH_YELLOW, BALL_SQUASH_GREEN, BALL_SQUASH_RED, BALL_SQUASH_BLUE, BALL_RACQUETBALL, BALL_LACROSSE, BALL_FIELD_HOCKEY, BALL_TABLE_TENNIS, BALL_HANDBALL, BALL_WATER_POLO, BALL_GOLF, BALL_PUCK, BALL_CURLING_STONE, BALL_SHOT, BALL_DISCUS, BALL_HAMMER, BALL_SEPAK_TAKRAW, BALL_ROUNDNET, BALL_SHUTTLECOCK_FEATHER, BALL_SHUTTLECOCK_NYLON];
34
36
  /** The id of every ball: `"ball:soccer"`, `"ball:cricket-red"`. */
35
37
  export const BALL_IDS = BALLS.map((b) => b.id);
36
38
  const TABLE = new Map(BALLS.map((b) => [b.id, b]));
@@ -18,7 +18,7 @@ export interface ObjectContext {
18
18
  /** A point of the object in its own space to the box: x, y and depth (positive toward you); one unit is 46 box units. */
19
19
  at(p: V3): [number, number, number];
20
20
  }
21
- export interface ObjectDef<Model> {
21
+ export interface ObjectDef<Model extends object> {
22
22
  id: string;
23
23
  en: string;
24
24
  ja: string;
@@ -35,4 +35,7 @@ export interface ObjectDef<Model> {
35
35
  body: string;
36
36
  };
37
37
  }
38
- export declare function defineObject<Model>(def: ObjectDef<Model>): Piece<Model>;
38
+ /** An object as a piece. Its `model` is the def's model with the default `view` added, so that a view of it (or a test) can turn it to any aspect from there. */
39
+ export declare function defineObject<Model extends object>(def: ObjectDef<Model>): Piece<Model & {
40
+ view: ObjectView;
41
+ }>;
@@ -2,6 +2,7 @@
2
2
  import { escape, hashOf, labelled, radians, resolveOptions, wrapPiece } from "./ball.core.js";
3
3
  import { apply, rotation } from "./ball.math.js";
4
4
  const finite = (v) => (typeof v === "number" && Number.isFinite(v) ? v : 0);
5
+ /** An object as a piece. Its `model` is the def's model with the default `view` added, so that a view of it (or a test) can turn it to any aspect from there. */
5
6
  export function defineObject(def) {
6
7
  const render = (o, turning) => {
7
8
  const opts = resolveOptions(o);
@@ -16,5 +17,5 @@ export function defineObject(def) {
16
17
  const { defs, body } = def.draw({ rot, opts, turning, id, minWidth: 80 / opts.size, at });
17
18
  return wrapPiece({ title: escape(def.en), size: opts.size, theme: opts.theme, defs, body });
18
19
  };
19
- return { id: def.id, en: def.en, ja: def.ja, source: "generated", kind: "ball", model: def.model, size: def.size, svg: labelled(def.en, def.ja, (o) => render(o, false)), frame: labelled(def.en, def.ja, (o) => render(o, true)) };
20
+ return { id: def.id, en: def.en, ja: def.ja, source: "generated", kind: "ball", model: { ...def.model, view: def.view }, size: def.size, svg: labelled(def.en, def.ja, (o) => render(o, false)), frame: labelled(def.en, def.ja, (o) => render(o, true)) };
20
21
  }
@@ -1,3 +1,5 @@
1
1
  export declare const BALL_PUCK: import("./ball.core.ts").Piece<{
2
2
  slices: number;
3
+ } & {
4
+ view: import("./ball.object.ts").ObjectView;
3
5
  }>;
@@ -1,7 +1,7 @@
1
- // Ice hockey puck: vulcanised black rubber, 76.2 mm across and 25.4 mm thick (so 1 : 3), with a ridged edge. Drawn as a cylinder in true 3D, so it turns.
1
+ // Ice hockey puck: vulcanised black rubber, 76.2 mm across and 25.4 mm thick (so 1 : 3), with a ridged edge. Drawn as a cylinder in true 3D, so it turns. The stack's y is in units of the radius, so the half thickness is 12.7 / 38.1, one third.
2
2
  import { drawStack } from "./ball.stack.js";
3
3
  import { defineObject } from "./ball.object.js";
4
- const H = 25.4 / 76.2 / 2;
4
+ const H = 25.4 / 76.2;
5
5
  export const BALL_PUCK = /* @__PURE__ */ defineObject({
6
6
  id: "ball:puck",
7
7
  en: "Hockey puck",
@@ -13,8 +13,9 @@ export const BALL_RUGBY = /* @__PURE__ */ defineBall({
13
13
  ja: "ラグビーボール",
14
14
  size: { extentMm: 290.0, lengthMm: 290.0, circumferenceMm: 600.0, longCircumferenceMm: 755.0, massG: [410, 460], rule: "World Rugby Laws of the Game (Rugby Union), Law 2 The ball, clauses 1-7" },
15
15
  spec: {
16
- axes: [1, 0.62, 0.62],
17
- point: 0.72,
16
+ // Worked out from the Laws' 600 mm girth, 290 mm length and 755 mm long girth.
17
+ axes: [1, 0.6586, 0.6586],
18
+ point: 0.561,
18
19
  view: { yaw: -30, pitch: 20, roll: -30 },
19
20
  base: "#1f4e9c",
20
21
  ground: WHITE,
@@ -1,6 +1,10 @@
1
1
  export declare const BALL_SHUTTLECOCK_FEATHER: import("./ball.core.ts").Piece<{
2
2
  feathers: number;
3
+ } & {
4
+ view: import("./ball.object.ts").ObjectView;
3
5
  }>;
4
6
  export declare const BALL_SHUTTLECOCK_NYLON: import("./ball.core.ts").Piece<{
5
7
  feathers: number;
8
+ } & {
9
+ view: import("./ball.object.ts").ObjectView;
6
10
  }>;
@@ -4,12 +4,13 @@ import { defineObject } from "./ball.object.js";
4
4
  import {} from "./ball.math.js";
5
5
  import { blend, mix, num } from "./ball.core.js";
6
6
  const FEATHERS = 16;
7
- // One unit is 32 mm: the skirt is 2 units across at the top, the base 0.84 across and the whole 2.06 long.
7
+ // One unit is 32 mm (the scale below makes 92 box units 150 mm): the skirt is 2 units across at the tips (63 mm), the base 0.84 across (27 mm) and the feathers 2.05 long from the tips to the top of the base
8
+ // (66 mm; the Law measures a feather's length that way, 62 to 70 mm).
8
9
  const ROOT_R = 0.42;
9
10
  const TIP_R = 1;
10
- const ROOT_Y = -0.56;
11
+ const ROOT_Y = -1.05;
11
12
  const TIP_Y = 1.0;
12
- const SLOPE = 0.4;
13
+ const SLOPE = 0.56;
13
14
  const drawShuttle = (c, p) => {
14
15
  const line = c.opts.style === "line";
15
16
  const tip = (a, r, y, k = 0) => [Math.cos(a) * r, y, Math.sin(a) * r + k];
@@ -0,0 +1,2 @@
1
+ export declare const BALL_FUTSAL: import("./ball.core.ts").Piece<import("./ball.engine.ts").BallSpec>;
2
+ export declare const BALL_BEACH_SOCCER: import("./ball.core.ts").Piece<import("./ball.engine.ts").BallSpec>;
@@ -0,0 +1,58 @@
1
+ // The soccer balls of the other two games the Laws of the Game cover: futsal (size 4, a low-bouncing ball, often brighter than the outdoor one) and beach soccer (size 5, softer, often bright). Both are drawn
2
+ // on constructions of our own (the 32-panel and the 14-panel propeller layout), in colours of our own choosing: no brand's ball, no tournament's pattern.
3
+ import { defineBall } from "./ball.engine.js";
4
+ import { mix } from "./ball.core.js";
5
+ import { shrink, truncatedIcosahedron } from "./ball.polyhedra.js";
6
+ import { LAYOUTS } from "./ball.soccer.panels.js";
7
+ import { SOCCER_SIZE } from "./ball.soccer.size.js";
8
+ const FUTSAL_BLUE = "#1673e6";
9
+ export const BALL_FUTSAL = /* @__PURE__ */ defineBall({
10
+ id: "ball:futsal",
11
+ en: "Futsal ball",
12
+ ja: "フットサルボール",
13
+ size: {
14
+ extentMm: 200.5,
15
+ diameterMm: 200.5,
16
+ circumferenceMm: 630.0,
17
+ massG: [400, 440],
18
+ rule: "Futsal Laws of the Game 2025/26, Law 2 The Ball, section 1 Qualities and measurements",
19
+ note: "Size 4. Diameter worked out from the middle of the circumference range (62 to 64 cm). Pressure 0.6 to 0.9 atm (600 to 900 g/cm2) at sea level; the first rebound from a 2 m drop must be between 50 cm and 65 cm, which is what makes it a low-bounce ball. The Laws let associations change the size and weight for youth and other categories.",
20
+ },
21
+ spec: {
22
+ view: { yaw: 30, pitch: 24, roll: 8 },
23
+ base: FUTSAL_BLUE,
24
+ ground: "#f6f8fb",
25
+ mat: { gloss: 0.4, shade: "#16305a", grain: { f: 1.4, a: 0.24 } },
26
+ draw(s, base, accent) {
27
+ const t = truncatedIcosahedron();
28
+ let out = "";
29
+ for (const p of t.pentagons)
30
+ out += s.panel(p, base, { ink: true });
31
+ // A smaller pentagon of the second colour inside each one.
32
+ t.pentagons.forEach((p, i) => {
33
+ out += s.panel(shrink(p, t.pentagonCentres[i], 0.5), accent ?? "#ffd21f", { n: 2 });
34
+ });
35
+ out += s.strokes(t.edges, mix(base, -0.4), 1.1);
36
+ out += s.strokes(t.edges, "#fff", 0.4, { deco: true, opacity: 0.4, dx: 0.3, dy: 0.35 });
37
+ return out;
38
+ },
39
+ },
40
+ });
41
+ export const BALL_BEACH_SOCCER = /* @__PURE__ */ defineBall({
42
+ id: "ball:beach-soccer",
43
+ en: "Beach soccer ball",
44
+ ja: "ビーチサッカーボール",
45
+ size: {
46
+ ...SOCCER_SIZE,
47
+ massG: [400, 440],
48
+ rule: "Beach Soccer Laws of the Game 2024-25, Law 2 The Ball, section 1 Qualities and measurements",
49
+ note: "Size 5. Diameter worked out from the middle of the circumference range (68 to 70 cm). Pressure 0.4 to 0.6 atm (400 to 600 g/cm2) at sea level, lower than an 11-a-side ball's, so the ball is softer; the mass is 400 to 440 g, 10 g under the 11-a-side minimum.",
50
+ },
51
+ spec: {
52
+ view: { yaw: 16, pitch: 14, roll: 0 },
53
+ base: "#f5b81c",
54
+ ground: "#f5b81c",
55
+ mat: { gloss: 0.55, shade: "#4a2a10", grain: { f: 1.2, a: 0.1 } },
56
+ draw: (s, base, accent) => LAYOUTS[14](s, { light: base, mid: accent ?? "#f0661e", deep: "#12a8d8", dark: "#1b2a66", seam: mix(base, -0.55) }),
57
+ },
58
+ });
@@ -4,12 +4,13 @@ import { defineBall } from "./ball.engine.js";
4
4
  import { truncatedIcosahedron } from "./ball.polyhedra.js";
5
5
  import { add, mul, norm } from "./ball.math.js";
6
6
  import { blend, mix } from "./ball.core.js";
7
+ import { SOCCER_SIZE } from "./ball.soccer.size.js";
7
8
  const SEAM = "#6b7180";
8
9
  export const BALL_SOCCER = /* @__PURE__ */ defineBall({
9
10
  id: "ball:soccer",
10
11
  en: "Soccer ball",
11
12
  ja: "サッカーボール",
12
- size: { extentMm: 219.6, diameterMm: 219.6, circumferenceMm: 690.0, massG: [410, 450], rule: "IFAB Laws of the Game, Law 2 The Ball, section 2.1 Qualities and measurements", note: "Diameter worked out from the middle of the circumference range." },
13
+ size: SOCCER_SIZE,
13
14
  spec: {
14
15
  view: { yaw: 30, pitch: 24, roll: 8 },
15
16
  base: "#1e2025",
@@ -30,7 +31,7 @@ export const BALL_SOCCER_COLOUR = /* @__PURE__ */ defineBall({
30
31
  id: "ball:soccer-colour",
31
32
  en: "Soccer ball, coloured",
32
33
  ja: "サッカーボール(カラー)",
33
- size: { extentMm: 219.6, diameterMm: 219.6, circumferenceMm: 690.0, massG: [410, 450], rule: "IFAB Laws of the Game, Law 2 The Ball, section 2.1 Qualities and measurements", note: "Diameter worked out from the middle of the circumference range." },
34
+ size: SOCCER_SIZE,
34
35
  spec: {
35
36
  view: { yaw: 30, pitch: 24, roll: 8 },
36
37
  base: "#d4283c",
@@ -0,0 +1,38 @@
1
+ import { type BallSpec, type Surface } from "./ball.engine.ts";
2
+ /** The tones a construction is laid in: the light panels, the dark ones, two in between, and the seam. */
3
+ export interface Tones {
4
+ light: string;
5
+ dark: string;
6
+ mid: string;
7
+ /** A third tone between the light and the mid one, for a third kind of panel. */
8
+ deep: string;
9
+ seam: string;
10
+ }
11
+ export type Layout = (s: Surface, t: Tones) => string;
12
+ export declare const classic: Layout;
13
+ export declare const LAYOUTS: {
14
+ readonly classic: Layout;
15
+ readonly 26: Layout;
16
+ readonly 18: Layout;
17
+ readonly 14: Layout;
18
+ readonly 12: Layout;
19
+ readonly 6: Layout;
20
+ };
21
+ export type Construction = keyof typeof LAYOUTS;
22
+ export declare const BALL_SOCCER_26: import("./ball.core.ts").Piece<BallSpec>;
23
+ export declare const BALL_SOCCER_18: import("./ball.core.ts").Piece<BallSpec>;
24
+ export declare const BALL_SOCCER_14: import("./ball.core.ts").Piece<BallSpec>;
25
+ export declare const BALL_SOCCER_12: import("./ball.core.ts").Piece<BallSpec>;
26
+ export declare const BALL_SOCCER_6: import("./ball.core.ts").Piece<BallSpec>;
27
+ export declare const BALL_SOCCER_ORANGE: import("./ball.core.ts").Piece<BallSpec>;
28
+ export declare const BALL_SOCCER_26_ORANGE: import("./ball.core.ts").Piece<BallSpec>;
29
+ export declare const BALL_SOCCER_18_ORANGE: import("./ball.core.ts").Piece<BallSpec>;
30
+ export declare const BALL_SOCCER_14_ORANGE: import("./ball.core.ts").Piece<BallSpec>;
31
+ export declare const BALL_SOCCER_12_ORANGE: import("./ball.core.ts").Piece<BallSpec>;
32
+ export declare const BALL_SOCCER_6_ORANGE: import("./ball.core.ts").Piece<BallSpec>;
33
+ export declare const BALL_SOCCER_YELLOW: import("./ball.core.ts").Piece<BallSpec>;
34
+ export declare const BALL_SOCCER_26_YELLOW: import("./ball.core.ts").Piece<BallSpec>;
35
+ export declare const BALL_SOCCER_18_YELLOW: import("./ball.core.ts").Piece<BallSpec>;
36
+ export declare const BALL_SOCCER_14_YELLOW: import("./ball.core.ts").Piece<BallSpec>;
37
+ export declare const BALL_SOCCER_12_YELLOW: import("./ball.core.ts").Piece<BallSpec>;
38
+ export declare const BALL_SOCCER_6_YELLOW: import("./ball.core.ts").Piece<BallSpec>;
@@ -0,0 +1,169 @@
1
+ // Soccer balls with other panel counts than the classic 32, each worked out in true 3D from a tiling of the sphere (see ball.tilings.ts): 26 panels, 18, 14, 12 and a thermobonded 6; and the same
2
+ // constructions, with the classic one, in winter orange and in high-visibility yellow. Generic constructions only: no brand's or tournament's ball or graphics.
3
+ import { defineBall } from "./ball.engine.js";
4
+ import { blend, mix } from "./ball.core.js";
5
+ import { strips18 } from "./ball.family.panelled.js";
6
+ import { truncatedIcosahedron } from "./ball.polyhedra.js";
7
+ import { dodecahedron, gores, propellers14, rhombicuboctahedron } from "./ball.tilings.js";
8
+ import { SOCCER_SIZE } from "./ball.soccer.size.js";
9
+ const INK = "#1e2025";
10
+ const WHITE = "#f8f8f6";
11
+ const SEAM = "#6b7180";
12
+ /** The highlight laid on a seam, so that it reads as a raised edge. */
13
+ const lit = (s, seams, width = 0.45) => s.strokes(seams, "#fff", width, { deco: true, opacity: 0.45, dx: 0.3, dy: 0.35 });
14
+ export const classic = (s, t) => {
15
+ const g = truncatedIcosahedron();
16
+ let out = "";
17
+ for (const p of g.pentagons)
18
+ out += s.panel(p, t.dark, { ink: true });
19
+ out += s.strokes(g.edges, t.seam, 1.15);
20
+ return out + lit(s, g.edges);
21
+ };
22
+ /** 26 panels: eight triangles dark, the six squares on the cube's faces mid, the twelve on its edges light. */
23
+ const panels26 = (s, t) => {
24
+ const g = rhombicuboctahedron();
25
+ const fill = [t.dark, t.mid, t.light];
26
+ let out = "";
27
+ g.panels.forEach((p, i) => {
28
+ out += s.panel(p, fill[g.kinds[i]], { n: 6, ink: g.kinds[i] === 0 });
29
+ });
30
+ out += s.strokes(g.seams, t.seam, 1.15);
31
+ return out + lit(s, g.seams);
32
+ };
33
+ /** 18 panels: three strips on each face of a cube, the strips of neighbouring faces square to each other; dark and light in turn. */
34
+ const panels18 = (s, t) => strips18(s, [
35
+ [t.light, t.dark],
36
+ [t.dark, t.light],
37
+ [t.light, t.dark],
38
+ [t.dark, t.light],
39
+ [t.light, t.dark],
40
+ [t.dark, t.light],
41
+ ], t.seam, 1.15);
42
+ /** 14 panels: eight triangles dark and six propellers in three tones, one for each axis of the cube (so no two that touch match). */
43
+ const panels14 = (s, t) => {
44
+ const g = propellers14();
45
+ const tone = [t.mid, t.deep, t.light];
46
+ let out = "";
47
+ g.propellers.forEach((p, i) => {
48
+ out += s.panel(p, tone[g.axis[i]], { n: 1 });
49
+ });
50
+ for (const p of g.triangles)
51
+ out += s.panel(p, t.dark, { n: 6, ink: true });
52
+ out += s.strokes(g.seams, t.seam, 1.15);
53
+ return out + lit(s, g.seams);
54
+ };
55
+ /** 12 panels: a dodecahedron, its pentagons in four tones so that no two neighbours match. */
56
+ const panels12 = (s, t) => {
57
+ const g = dodecahedron();
58
+ const fill = [t.light, t.dark, t.light, t.mid];
59
+ let out = "";
60
+ g.panels.forEach((p, i) => {
61
+ out += s.panel(p, fill[g.colour[i]], { n: 6, ink: g.colour[i] === 1 });
62
+ });
63
+ out += s.strokes(g.seams, t.seam, 1.15);
64
+ return out + lit(s, g.seams);
65
+ };
66
+ /** 6 panels, thermobonded: six twisted gores whose long seams run pole to pole as shallow grooves, and no stitching. */
67
+ const panels6 = (s, t) => {
68
+ const g = gores(6, 180, [0, 0, 28]);
69
+ const tone = [t.light, t.mid, t.dark];
70
+ let out = "";
71
+ // A gore is cut across into four pieces, each filled alike and edged in its own colour so that no hairline shows between them.
72
+ for (const { ring, gore } of g.pieces) {
73
+ const fill = tone[gore % 3];
74
+ out += s.panel(ring, fill, { n: 1, ink: gore % 3 === 2 });
75
+ out += s.strokes([ring], fill, 0.5, { closed: true, deco: true });
76
+ }
77
+ out += s.strokes(g.seams, t.seam, 0.85);
78
+ return out + lit(s, g.seams, 0.5);
79
+ };
80
+ export const LAYOUTS = { classic, 26: panels26, 18: panels18, 14: panels14, 12: panels12, 6: panels6 };
81
+ /** The black-and-white ball, winter orange (an orange that shows against snow and in low light) and high-visibility yellow, with dark panels. */
82
+ const COLOURWAYS = {
83
+ white: { base: INK, accent: "#d9dde6" },
84
+ orange: { base: "#ff6a13", accent: INK },
85
+ yellow: { base: "#e8f21c", accent: INK },
86
+ };
87
+ const gloss = { gloss: 0.8, shade: "#2a3450", grain: { f: 1.1, a: 0.16 } };
88
+ /** The spec of a construction in a colourway. The black-and-white ball is laid on a white ground with `base` the dark panels; a coloured one is laid on `base`, with the dark panels `accent`. */
89
+ function specOf(construction, way, view) {
90
+ const layout = LAYOUTS[construction];
91
+ if (way === "white") {
92
+ return {
93
+ view,
94
+ base: INK,
95
+ ground: WHITE,
96
+ mat: construction === 6 ? { ...gloss, gloss: 0.9, grain: { f: 0.7, a: 0.07 } } : gloss,
97
+ draw: (s, base, accent) => layout(s, { light: WHITE, dark: base, mid: accent ?? "#dfe2ea", deep: blend(accent ?? "#dfe2ea", base, 0.12), seam: SEAM }),
98
+ };
99
+ }
100
+ const c = COLOURWAYS[way];
101
+ return {
102
+ view,
103
+ base: c.base,
104
+ mat: { ...(construction === 6 ? { ...gloss, gloss: 0.9, grain: { f: 0.7, a: 0.07 } } : gloss), shade: mix(c.base, -0.65) },
105
+ draw: (s, base, accent) => layout(s, { light: base, dark: accent ?? c.accent, mid: blend(base, "#ffffff", 0.4), deep: blend(base, "#ffffff", 0.18), seam: mix(base, -0.5) }),
106
+ };
107
+ }
108
+ /** The id, and the English and Japanese names, of each construction in each colourway (short, because the name is the accessible name: 26 characters in English and 12 in Japanese at most). */
109
+ const NAMES = {
110
+ classic: {
111
+ white: { id: "soccer", en: "Soccer ball", ja: "サッカーボール" },
112
+ orange: { id: "soccer-orange", en: "Soccer ball, orange", ja: "32枚貼り・オレンジ" },
113
+ yellow: { id: "soccer-yellow", en: "Soccer ball, yellow", ja: "32枚貼り・蛍光イエロー" },
114
+ },
115
+ 26: {
116
+ white: { id: "soccer-26", en: "Soccer ball, 26 panels", ja: "26枚貼りサッカーボール" },
117
+ orange: { id: "soccer-26-orange", en: "Soccer 26 panels, orange", ja: "26枚貼り・オレンジ" },
118
+ yellow: { id: "soccer-26-yellow", en: "Soccer 26 panels, yellow", ja: "26枚貼り・蛍光イエロー" },
119
+ },
120
+ 18: {
121
+ white: { id: "soccer-18", en: "Soccer ball, 18 panels", ja: "18枚貼りサッカーボール" },
122
+ orange: { id: "soccer-18-orange", en: "Soccer 18 panels, orange", ja: "18枚貼り・オレンジ" },
123
+ yellow: { id: "soccer-18-yellow", en: "Soccer 18 panels, yellow", ja: "18枚貼り・蛍光イエロー" },
124
+ },
125
+ 14: {
126
+ white: { id: "soccer-14", en: "Soccer ball, 14 panels", ja: "14枚貼りサッカーボール" },
127
+ orange: { id: "soccer-14-orange", en: "Soccer 14 panels, orange", ja: "14枚貼り・オレンジ" },
128
+ yellow: { id: "soccer-14-yellow", en: "Soccer 14 panels, yellow", ja: "14枚貼り・蛍光イエロー" },
129
+ },
130
+ 12: {
131
+ white: { id: "soccer-12", en: "Soccer ball, 12 panels", ja: "12枚貼りサッカーボール" },
132
+ orange: { id: "soccer-12-orange", en: "Soccer 12 panels, orange", ja: "12枚貼り・オレンジ" },
133
+ yellow: { id: "soccer-12-yellow", en: "Soccer 12 panels, yellow", ja: "12枚貼り・蛍光イエロー" },
134
+ },
135
+ 6: {
136
+ white: { id: "soccer-6", en: "Soccer ball, 6 panels", ja: "6枚貼りサッカーボール" },
137
+ orange: { id: "soccer-6-orange", en: "Soccer 6 panels, orange", ja: "6枚貼り・オレンジ" },
138
+ yellow: { id: "soccer-6-yellow", en: "Soccer 6 panels, yellow", ja: "6枚貼り・蛍光イエロー" },
139
+ },
140
+ };
141
+ const VIEWS = {
142
+ classic: { yaw: 30, pitch: 24, roll: 8 },
143
+ 26: { yaw: 30, pitch: 24, roll: 8 },
144
+ 18: { yaw: 28, pitch: 22, roll: 0 },
145
+ 14: { yaw: 16, pitch: 14, roll: 0 },
146
+ 12: { yaw: 30, pitch: 24, roll: 8 },
147
+ 6: { yaw: 20, pitch: 22, roll: 0 },
148
+ };
149
+ function make(construction, way) {
150
+ const n = NAMES[construction][way];
151
+ return defineBall({ id: `ball:${n.id}`, en: n.en, ja: n.ja, size: SOCCER_SIZE, spec: specOf(construction, way, VIEWS[construction]) });
152
+ }
153
+ export const BALL_SOCCER_26 = /* @__PURE__ */ make(26, "white");
154
+ export const BALL_SOCCER_18 = /* @__PURE__ */ make(18, "white");
155
+ export const BALL_SOCCER_14 = /* @__PURE__ */ make(14, "white");
156
+ export const BALL_SOCCER_12 = /* @__PURE__ */ make(12, "white");
157
+ export const BALL_SOCCER_6 = /* @__PURE__ */ make(6, "white");
158
+ export const BALL_SOCCER_ORANGE = /* @__PURE__ */ make("classic", "orange");
159
+ export const BALL_SOCCER_26_ORANGE = /* @__PURE__ */ make(26, "orange");
160
+ export const BALL_SOCCER_18_ORANGE = /* @__PURE__ */ make(18, "orange");
161
+ export const BALL_SOCCER_14_ORANGE = /* @__PURE__ */ make(14, "orange");
162
+ export const BALL_SOCCER_12_ORANGE = /* @__PURE__ */ make(12, "orange");
163
+ export const BALL_SOCCER_6_ORANGE = /* @__PURE__ */ make(6, "orange");
164
+ export const BALL_SOCCER_YELLOW = /* @__PURE__ */ make("classic", "yellow");
165
+ export const BALL_SOCCER_26_YELLOW = /* @__PURE__ */ make(26, "yellow");
166
+ export const BALL_SOCCER_18_YELLOW = /* @__PURE__ */ make(18, "yellow");
167
+ export const BALL_SOCCER_14_YELLOW = /* @__PURE__ */ make(14, "yellow");
168
+ export const BALL_SOCCER_12_YELLOW = /* @__PURE__ */ make(12, "yellow");
169
+ export const BALL_SOCCER_6_YELLOW = /* @__PURE__ */ make(6, "yellow");
@@ -0,0 +1,2 @@
1
+ import type { PieceSize } from "./ball.core.ts";
2
+ export declare const SOCCER_SIZE: PieceSize;
@@ -0,0 +1,8 @@
1
+ export const SOCCER_SIZE = {
2
+ extentMm: 219.6,
3
+ diameterMm: 219.6,
4
+ circumferenceMm: 690.0,
5
+ massG: [410, 450],
6
+ rule: "IFAB Laws of the Game, Law 2 The Ball, section 2.1 Qualities and measurements",
7
+ note: "Diameter worked out from the middle of the circumference range (68 to 70 cm).",
8
+ };
@@ -0,0 +1,61 @@
1
+ import { type V3 } from "./ball.math.ts";
2
+ /** A tiling: the panels with their centres, and every edge between two panels once. */
3
+ export interface Tiling {
4
+ /** Each panel as a ring of unit vectors, in order round its centre. */
5
+ panels: V3[][];
6
+ centres: V3[];
7
+ /** Which kind each panel is (its position in `KINDS` of the tiling). */
8
+ kinds: number[];
9
+ /** Every seam once, as a curve on the sphere. */
10
+ seams: V3[][];
11
+ }
12
+ /**
13
+ * The 26-panel layout: a rhombicuboctahedron. Its 24 vertices are the permutations of (+-1, +-1, +-(1 + sqrt 2)); its faces are 8 triangles (about the corners of a cube, kind 0), 6 squares about the
14
+ * face directions (kind 1) and 12 squares about the edge directions (kind 2).
15
+ */
16
+ export declare function rhombicuboctahedron(): Tiling;
17
+ type Dodecahedron = Tiling & {
18
+ colour: number[];
19
+ };
20
+ /**
21
+ * The 12-panel layout: a dodecahedron. Its 20 vertices are (+-1, +-1, +-1) and the cyclic permutations of (0, +-1/phi, +-phi); its 12 pentagons are about the directions of an icosahedron's vertices,
22
+ * (+-1, 0, +-phi) and its cyclic permutations. `colour` gives each pentagon one of four colours so that no two neighbours share one (the icosahedron's vertices can be coloured with four).
23
+ */
24
+ export declare function dodecahedron(): Dodecahedron;
25
+ export interface Propellers {
26
+ /** The eight triangles, about the corners of a cube. */
27
+ triangles: V3[][];
28
+ /** The six propellers, about the faces of a cube, as dense rings (use them with one sample per edge). */
29
+ propellers: V3[][];
30
+ /** Every seam once: the 24 straight edges of the triangles and the 12 S-curves between propellers. */
31
+ seams: V3[][];
32
+ triangleCentres: V3[];
33
+ propellerCentres: V3[];
34
+ /** The axis of the cube each propeller is on (0 for x, 1 for y, 2 for z). */
35
+ axis: number[];
36
+ }
37
+ /**
38
+ * The 14-panel layout: a truncated cube (vertices the permutations of (+-xi, +-1, +-1); the regular one has xi = sqrt 2 - 1, and 0.42 here makes the triangles a little larger) has 8 triangles and
39
+ * 6 octagons, and the octagons meet each other along 12 edges. Each of those edges is bent into an S: `lift` times its length to the side a quarter of the way along and the same the other way
40
+ * three quarters of the way along (eased to meet the corners gently), so the curve is the same turned half a turn about the edge's middle, and the tiling keeps the cube's rotations (and loses its
41
+ * mirrors, so it has a hand). The octagons then have four straight sides, against the triangles, and four S-shaped ones: propellers.
42
+ */
43
+ export declare function propellers14(lift?: number, steps?: number, xi?: number, ease?: number): Propellers;
44
+ export interface Gores {
45
+ /** Each panel as a ring running up one seam and down the next. */
46
+ panels: V3[][];
47
+ centres: V3[];
48
+ /** The seams, each from one pole to the other. */
49
+ seams: V3[][];
50
+ /** Each panel cut across into `bands` pieces, so that every piece is small enough to be drawn as a plain polygon; `gore` is the panel a piece is of. */
51
+ pieces: Array<{
52
+ ring: V3[];
53
+ gore: number;
54
+ }>;
55
+ }
56
+ /**
57
+ * Six (or `count`) gores: panels between seams that each run from one pole to the other and turn through `twist` degrees of longitude on the way, so the seams are long S-like spirals. The
58
+ * whole is then tilted by the rotation `tilt` (yaw, pitch and roll in degrees) so that the poles are not on the ball's spin axis.
59
+ */
60
+ export declare function gores(count?: number, twist?: number, tilt?: readonly [number, number, number], steps?: number, bands?: number): Gores;
61
+ export {};
@@ -0,0 +1,212 @@
1
+ // Tilings of the sphere for the soccer balls with other panel counts than the 32 of the classic one. Each is worked out here from a polyhedron or a curve, never copied from a ball: the 26 panels are the
2
+ // faces of a rhombicuboctahedron, the 12 the faces of a dodecahedron, the 14 a truncated cube whose twelve long edges are bent into an S (so the six octagons become propellers and the eight
3
+ // corners stay triangles), and the 6 a ring of twisted gores. Every panel is a ring of unit vectors; the edges between vertices follow the sphere (spherical interpolation), as an inflated ball's do.
4
+ import { add, apply, arc, cross, dot, len, lonLat, mul, norm, rotation, slerp, sub } from "./ball.math.js";
5
+ const PHI = (1 + Math.sqrt(5)) / 2;
6
+ const signs = [-1, 1];
7
+ /** The vertices of a solid, each normalised onto the unit sphere, with no point twice. */
8
+ function unique(points) {
9
+ const out = [];
10
+ for (const p of points)
11
+ if (!out.some((q) => len(sub(p, q)) < 1e-6))
12
+ out.push(p);
13
+ return out;
14
+ }
15
+ /** The three coordinates in every order. */
16
+ const orders = (a, b, c) => [[a, b, c], [a, c, b], [b, a, c], [b, c, a], [c, a, b], [c, b, a]];
17
+ /** Every vertex of a solid on a face: the ones farthest along the face's outward direction, in order round it. */
18
+ function ringOf(verts, centre) {
19
+ const top = Math.max(...verts.map((v) => dot(v, centre)));
20
+ const on = verts.filter((v) => dot(v, centre) > top - 1e-6);
21
+ const ex = norm(sub(on[0], mul(centre, dot(on[0], centre))));
22
+ const ey = cross(centre, ex);
23
+ return on.sort((p, q) => Math.atan2(dot(p, ey), dot(p, ex)) - Math.atan2(dot(q, ey), dot(q, ex)));
24
+ }
25
+ const keyOf = (v) => v.map((x) => Math.round(x * 1000)).join(",");
26
+ /** The seams of a set of rings: each edge once, as an arc of `samples` steps. */
27
+ function seamsOf(rings, samples) {
28
+ const seen = new Set();
29
+ const out = [];
30
+ for (const ring of rings) {
31
+ for (let i = 0; i < ring.length; i++) {
32
+ const a = ring[i], b = ring[(i + 1) % ring.length];
33
+ const key = [keyOf(a), keyOf(b)].sort().join("|");
34
+ if (seen.has(key))
35
+ continue;
36
+ seen.add(key);
37
+ out.push(arc(a, b, samples));
38
+ }
39
+ }
40
+ return out;
41
+ }
42
+ const tiling = (verts, centres, kinds, samples) => {
43
+ const panels = centres.map((c) => ringOf(verts, c));
44
+ return { panels, centres: [...centres], kinds: [...kinds], seams: seamsOf(panels, samples) };
45
+ };
46
+ let rhombi;
47
+ /**
48
+ * The 26-panel layout: a rhombicuboctahedron. Its 24 vertices are the permutations of (+-1, +-1, +-(1 + sqrt 2)); its faces are 8 triangles (about the corners of a cube, kind 0), 6 squares about the
49
+ * face directions (kind 1) and 12 squares about the edge directions (kind 2).
50
+ */
51
+ export function rhombicuboctahedron() {
52
+ if (rhombi !== undefined)
53
+ return rhombi;
54
+ const L = 1 + Math.SQRT2;
55
+ const verts = unique(signs.flatMap((a) => signs.flatMap((b) => signs.flatMap((c) => orders(a, b, c * L)))).map(norm));
56
+ const corners = [];
57
+ const faces = [];
58
+ const edges = [];
59
+ for (const a of signs)
60
+ for (const b of signs)
61
+ for (const c of signs)
62
+ corners.push(norm([a, b, c]));
63
+ for (const a of signs)
64
+ faces.push([a, 0, 0], [0, a, 0], [0, 0, a]);
65
+ for (const a of signs)
66
+ for (const b of signs)
67
+ edges.push(norm([a, b, 0]), norm([a, 0, b]), norm([0, a, b]));
68
+ rhombi = tiling(verts, [...corners, ...faces, ...edges], [...corners.map(() => 0), ...faces.map(() => 1), ...edges.map(() => 2)], 5);
69
+ return rhombi;
70
+ }
71
+ let dodeca;
72
+ /**
73
+ * The 12-panel layout: a dodecahedron. Its 20 vertices are (+-1, +-1, +-1) and the cyclic permutations of (0, +-1/phi, +-phi); its 12 pentagons are about the directions of an icosahedron's vertices,
74
+ * (+-1, 0, +-phi) and its cyclic permutations. `colour` gives each pentagon one of four colours so that no two neighbours share one (the icosahedron's vertices can be coloured with four).
75
+ */
76
+ export function dodecahedron() {
77
+ if (dodeca !== undefined)
78
+ return dodeca;
79
+ const verts = [];
80
+ for (const a of signs)
81
+ for (const b of signs)
82
+ for (const c of signs)
83
+ verts.push(norm([a, b, c]));
84
+ for (const a of signs)
85
+ for (const b of signs)
86
+ verts.push(norm([0, a / PHI, b * PHI]), norm([a / PHI, b * PHI, 0]), norm([b * PHI, 0, a / PHI]));
87
+ const centres = [];
88
+ for (const a of signs)
89
+ for (const b of signs)
90
+ centres.push(norm([a, 0, b * PHI]), norm([0, b * PHI, a]), norm([b * PHI, a, 0]));
91
+ const t = tiling(verts, centres, centres.map(() => 0), 5);
92
+ // Neighbours are at the icosahedron's edge angle: cos = 1/sqrt 5.
93
+ const next = (i, j) => Math.abs(dot(centres[i], centres[j]) - 1 / Math.sqrt(5)) < 1e-6;
94
+ const colour = centres.map(() => -1);
95
+ const paint = (i) => {
96
+ if (i === centres.length)
97
+ return true;
98
+ for (let c = 0; c < 4; c++) {
99
+ if (centres.every((_, j) => j >= i || !next(i, j) || colour[j] !== c)) {
100
+ colour[i] = c;
101
+ if (paint(i + 1))
102
+ return true;
103
+ }
104
+ }
105
+ colour[i] = -1;
106
+ return false;
107
+ };
108
+ paint(0);
109
+ dodeca = { ...t, colour };
110
+ return dodeca;
111
+ }
112
+ /** A point of the arc from `a` to `b`, `t` of the way along it, pushed sideways (along the sphere, square to the arc) by `lift` times the chord. */
113
+ function bent(a, b, t, lift, ease = 0) {
114
+ const p = slerp(a, b, t);
115
+ const tangent = norm(sub(b, mul(p, dot(b, p))));
116
+ const side = cross(p, tangent);
117
+ return norm(add(p, mul(side, lift * len(sub(b, a)) * Math.sin(2 * Math.PI * t) * (1 - ease + ease * Math.sin(Math.PI * t)))));
118
+ }
119
+ let props;
120
+ /**
121
+ * The 14-panel layout: a truncated cube (vertices the permutations of (+-xi, +-1, +-1); the regular one has xi = sqrt 2 - 1, and 0.42 here makes the triangles a little larger) has 8 triangles and
122
+ * 6 octagons, and the octagons meet each other along 12 edges. Each of those edges is bent into an S: `lift` times its length to the side a quarter of the way along and the same the other way
123
+ * three quarters of the way along (eased to meet the corners gently), so the curve is the same turned half a turn about the edge's middle, and the tiling keeps the cube's rotations (and loses its
124
+ * mirrors, so it has a hand). The octagons then have four straight sides, against the triangles, and four S-shaped ones: propellers.
125
+ */
126
+ export function propellers14(lift = 0.2, steps = 14, xi = 0.42, ease = 0.5) {
127
+ props ??= new Map();
128
+ const key = lift * 1000 + steps + xi * 1e6 + ease * 1e9;
129
+ const hit = props.get(key);
130
+ if (hit !== undefined)
131
+ return hit;
132
+ const verts = unique(signs.flatMap((a) => signs.flatMap((b) => signs.flatMap((c) => orders(a * xi, b, c)))).map(norm));
133
+ const cornerDirs = [];
134
+ for (const a of signs)
135
+ for (const b of signs)
136
+ for (const c of signs)
137
+ cornerDirs.push(norm([a, b, c]));
138
+ const faceDirs = [];
139
+ for (const a of signs)
140
+ faceDirs.push([a, 0, 0], [0, a, 0], [0, 0, a]);
141
+ // Two vertices that differ in one coordinate only are the ends of an edge between two octagons.
142
+ const raw = (v) => {
143
+ const m = Math.max(...v.map(Math.abs));
144
+ return [v[0] / m, v[1] / m, v[2] / m];
145
+ };
146
+ const betweenOctagons = (a, b) => {
147
+ const x = raw(a), y = raw(b);
148
+ return [0, 1, 2].filter((i) => Math.abs(x[i] - y[i]) > 1e-6).length === 1;
149
+ };
150
+ const triangles = cornerDirs.map((c) => ringOf(verts, c));
151
+ const propellers = faceDirs.map((f) => {
152
+ const ring = ringOf(verts, f);
153
+ const out = [];
154
+ for (let i = 0; i < ring.length; i++) {
155
+ const a = ring[i], b = ring[(i + 1) % ring.length];
156
+ if (!betweenOctagons(a, b)) {
157
+ out.push(...arc(a, b, 4).slice(0, 4));
158
+ continue;
159
+ }
160
+ // The same curve from either octagon: it is defined from the end that comes first, and walked backwards from the other.
161
+ const forward = keyOf(a) < keyOf(b);
162
+ const [p, q] = forward ? [a, b] : [b, a];
163
+ for (let k = 0; k < steps; k++)
164
+ out.push(bent(p, q, forward ? k / steps : 1 - k / steps, lift, ease));
165
+ }
166
+ return out;
167
+ });
168
+ const seams = seamsOf(triangles, 6);
169
+ const seen = new Set();
170
+ for (const f of faceDirs) {
171
+ const ring = ringOf(verts, f);
172
+ for (let i = 0; i < ring.length; i++) {
173
+ const a = ring[i], b = ring[(i + 1) % ring.length];
174
+ if (!betweenOctagons(a, b))
175
+ continue;
176
+ const [p, q] = keyOf(a) < keyOf(b) ? [a, b] : [b, a];
177
+ const k = `${keyOf(p)}|${keyOf(q)}`;
178
+ if (seen.has(k))
179
+ continue;
180
+ seen.add(k);
181
+ seams.push(Array.from({ length: steps + 1 }, (_, j) => bent(p, q, j / steps, lift, ease)));
182
+ }
183
+ }
184
+ const result = { triangles, propellers, seams, triangleCentres: cornerDirs, propellerCentres: faceDirs, axis: faceDirs.map((f) => f.findIndex((x) => x !== 0)) };
185
+ props.set(key, result);
186
+ return result;
187
+ }
188
+ /**
189
+ * Six (or `count`) gores: panels between seams that each run from one pole to the other and turn through `twist` degrees of longitude on the way, so the seams are long S-like spirals. The
190
+ * whole is then tilted by the rotation `tilt` (yaw, pitch and roll in degrees) so that the poles are not on the ball's spin axis.
191
+ */
192
+ export function gores(count = 6, twist = 150, tilt = [0, 0, 0], steps = 24, bands = 4) {
193
+ const m = rotation((tilt[0] * Math.PI) / 180, (tilt[1] * Math.PI) / 180, (tilt[2] * Math.PI) / 180);
194
+ const at = (i, s) => apply(m, lonLat((2 * Math.PI * i) / count + ((twist * Math.PI) / 180) * (s / 2), (s * Math.PI) / 2));
195
+ const seam = (i) => Array.from({ length: steps + 1 }, (_, k) => at(i, -1 + (2 * k) / steps));
196
+ const seams = Array.from({ length: count }, (_, i) => seam(i));
197
+ const panels = seams.map((up, i) => {
198
+ const down = [...seam((i + 1) % count)].reverse();
199
+ return [...up, ...down.slice(1, -1)];
200
+ });
201
+ const centres = panels.map((_, i) => apply(m, lonLat((2 * Math.PI * (i + 0.5)) / count, 0)));
202
+ const per = steps / bands;
203
+ const pieces = [];
204
+ for (let i = 0; i < count; i++) {
205
+ const left = seams[i], right = seams[(i + 1) % count];
206
+ for (let b = 0; b < bands; b++) {
207
+ const from = b * per, to = (b + 1) * per;
208
+ pieces.push({ ring: [...left.slice(from, to + 1), ...right.slice(from, to + 1).reverse()], gore: i });
209
+ }
210
+ }
211
+ return { panels, centres, seams, pieces };
212
+ }
@@ -1,3 +1,24 @@
1
+ const SOCCER_PANELS = {
2
+ "": { en: "The 32-panel soccer ball", ja: "32枚貼りのサッカーボール" },
3
+ "-26": { en: "The 26-panel soccer ball", ja: "26枚貼りのサッカーボール" },
4
+ "-18": { en: "The 18-panel soccer ball", ja: "18枚貼りのサッカーボール" },
5
+ "-14": { en: "The 14-panel soccer ball (6 propellers, 8 triangles)", ja: "14枚貼り(プロペラ形6枚、三角形8枚)のサッカーボール" },
6
+ "-12": { en: "The 12-panel soccer ball", ja: "12枚貼りのサッカーボール" },
7
+ "-6": { en: "The thermobonded 6-panel soccer ball", ja: "熱接着の6枚貼りサッカーボール" },
8
+ };
9
+ const SOCCER_COLOURS = {
10
+ orange: { en: "in winter orange, a bright orange that shows against snow and in low light, with dark panels", ja: "雪の上や暗がりでも見やすい明るいオレンジ色で、暗い色のパネルが入る" },
11
+ yellow: { en: "in high-visibility yellow, a fluorescent yellow that shows in low light, with dark panels", ja: "暗がりでも見やすい蛍光イエローで、暗い色のパネルが入る" },
12
+ };
13
+ /** The winter orange and high-visibility yellow balls of each construction: the construction's words and the colour's. */
14
+ function soccerColourways() {
15
+ const out = {};
16
+ for (const [suffix, panels] of Object.entries(SOCCER_PANELS)) {
17
+ for (const [colour, words] of Object.entries(SOCCER_COLOURS))
18
+ out[`ball:soccer${suffix}-${colour}`] = { en: `${panels.en}, ${words.en}`, ja: `${panels.ja}。${words.ja}` };
19
+ }
20
+ return out;
21
+ }
1
22
  export const EMBLEM_DESCRIPTIONS = {
2
23
  "trophy:hockey-cup": { en: "Tall banded silver cup", ja: "帯を何段も重ねた背の高い銀のカップ" },
3
24
  "trophy:basketball": { en: "Basketball trophy", ja: "バスケットボールのトロフィー" },
@@ -57,4 +78,12 @@ export const EMBLEM_DESCRIPTIONS = {
57
78
  "gear:yumi": { en: "The long asymmetric bow, held a third of the way up, with its string and wrapped grip", ja: "上が長い非対称の長弓。弦を張り、握り革と籐巻きが付く" },
58
79
  "gear:ya": { en: "A kyudo arrow with feather fletching, a nock and a steel point", ja: "弓道の矢。矢羽根、筈、矢尻が付く" },
59
80
  "gear:karate-gi": { en: "The karate suit: a jacket with crossed lapels, trousers and a belt", ja: "空手衣。上衣、ズボン、帯" },
81
+ "ball:soccer-26": { en: "26 panels: 8 triangles and 18 squares, the faces of a rhombicuboctahedron", ja: "三角形8枚と正方形18枚の26枚貼り。斜方立方八面体の26面と同じ構成" },
82
+ "ball:soccer-18": { en: "18 panels: three strips on each of the six faces of a cube, the strips of neighbouring faces square to each other", ja: "立方体の6面に3枚ずつ帯状のパネルを貼った18枚貼り。隣り合う面の帯は互いに直角" },
83
+ "ball:soccer-14": { en: "14 panels: 6 propellers and 8 triangles, on a truncated cube whose 12 long edges are bent into S curves", ja: "プロペラ形6枚と三角形8枚の14枚貼り。切頂立方体の長い辺12本をS字に曲げた形" },
84
+ "ball:soccer-12": { en: "12 panels: the 12 pentagons of a dodecahedron", ja: "正十二面体の五角形12枚で作った12枚貼り" },
85
+ "ball:soccer-6": { en: "6 panels, thermobonded (glued, not stitched): six twisted gores with a few long curved seams, and a smooth look", ja: "縫わずに貼り合わせる熱接着の6枚貼り。ねじれた細長いパネル6枚が、長く曲がった数本の継ぎ目でつながり、表面はなめらか" },
86
+ "ball:futsal": { en: "Futsal: size 4, 62 to 64 cm round, 400 to 440 g, low bounce; 32 panels, bright blue and white with a small yellow pentagon on each of the twelve", ja: "フットサル用の4号球。円周62〜64cm、重さ400〜440g、弾みにくい。32枚貼りで、明るい青と白の配色。12枚の五角形それぞれに、小さな黄色の五角形が入る" },
87
+ "ball:beach-soccer": { en: "Beach soccer: size 5, 68 to 70 cm round, 400 to 440 g, soft (low pressure); the 14-panel propeller layout in bright yellow, orange and blue", ja: "ビーチサッカー用の5号球。円周68〜70cm、重さ400〜440g、空気圧が低くやわらかい。14枚貼りのプロペラ形で、黄色、オレンジ、青の明るい配色" },
88
+ ...soccerColourways(),
60
89
  };
@@ -25,7 +25,8 @@ export function flipSvg(svg, o) {
25
25
  const sx = Math.max(Math.abs(across), 0.02);
26
26
  const sy = Math.max(Math.abs(tilt), 0.02);
27
27
  const thick = w * (o.thick ?? 0.035);
28
- const dx = thick * Math.sin(o.yaw * RAD);
28
+ // A turn to a positive yaw carries the front toward the right, as it does for a ball and a cup (and for a drag to the right), so the rim is seen on the left, on the edge that comes toward the eye.
29
+ const dx = -thick * Math.sin(o.yaw * RAD);
29
30
  const dy = -thick * Math.sin(o.pitch * RAD) * 0.6;
30
31
  const mirror = showBack && own === undefined;
31
32
  const cx = x0 + w / 2;
@@ -18,7 +18,7 @@ export type CrestShape = (typeof CREST_SHAPES)[number];
18
18
  export declare const CREST_IDS: readonly ["crest:shield", "crest:roundel", "crest:diamond", "crest:pennant"];
19
19
  export type CrestId = (typeof CREST_IDS)[number];
20
20
  /** The balls: one for each ball and implement the sports use, in 3D, with the official size it comes from. The balls of one family (the three crickets, the five squash balls) share a module. */
21
- export declare const BALL_IDS: readonly ["ball:soccer", "ball:soccer-colour", "ball:teqball", "ball:basketball", "ball:basketball-6", "ball:basketball-3x3", "ball:baseball", "ball:softball", "ball:tennis", "ball:padel", "ball:sliotar", "ball:volleyball", "ball:volleyball-8", "ball:beach-volleyball", "ball:cricket-red", "ball:cricket-white", "ball:cricket-pink", "ball:football", "ball:football-cfl", "ball:football-ifaf", "ball:rugby", "ball:afl", "ball:gaelic-football", "ball:netball", "ball:pickleball-outdoor", "ball:pickleball-indoor", "ball:squash", "ball:squash-yellow", "ball:squash-green", "ball:squash-red", "ball:squash-blue", "ball:racquetball", "ball:lacrosse", "ball:field-hockey", "ball:table-tennis", "ball:handball", "ball:water-polo", "ball:golf", "ball:puck", "ball:curling-stone", "ball:shot", "ball:discus", "ball:hammer", "ball:sepak-takraw", "ball:roundnet", "ball:shuttlecock-feather", "ball:shuttlecock-nylon"];
21
+ export declare const BALL_IDS: readonly ["ball:soccer", "ball:soccer-colour", "ball:soccer-26", "ball:soccer-18", "ball:soccer-14", "ball:soccer-12", "ball:soccer-6", "ball:soccer-orange", "ball:soccer-26-orange", "ball:soccer-18-orange", "ball:soccer-14-orange", "ball:soccer-12-orange", "ball:soccer-6-orange", "ball:soccer-yellow", "ball:soccer-26-yellow", "ball:soccer-18-yellow", "ball:soccer-14-yellow", "ball:soccer-12-yellow", "ball:soccer-6-yellow", "ball:futsal", "ball:beach-soccer", "ball:teqball", "ball:basketball", "ball:basketball-6", "ball:basketball-3x3", "ball:baseball", "ball:softball", "ball:tennis", "ball:padel", "ball:sliotar", "ball:volleyball", "ball:volleyball-8", "ball:beach-volleyball", "ball:cricket-red", "ball:cricket-white", "ball:cricket-pink", "ball:football", "ball:football-cfl", "ball:football-ifaf", "ball:rugby", "ball:afl", "ball:gaelic-football", "ball:netball", "ball:pickleball-outdoor", "ball:pickleball-indoor", "ball:squash", "ball:squash-yellow", "ball:squash-green", "ball:squash-red", "ball:squash-blue", "ball:racquetball", "ball:lacrosse", "ball:field-hockey", "ball:table-tennis", "ball:handball", "ball:water-polo", "ball:golf", "ball:puck", "ball:curling-stone", "ball:shot", "ball:discus", "ball:hammer", "ball:sepak-takraw", "ball:roundnet", "ball:shuttlecock-feather", "ball:shuttlecock-nylon"];
22
22
  export type BallId = (typeof BALL_IDS)[number];
23
23
  /** The pieces of gear: a piece of equipment for each sport, flat, drawn for this package, turning like a coin. */
24
24
  export declare const GEAR_IDS: readonly ["gear:hockey-stick", "gear:goalie-stick", "gear:hockey-skate", "gear:goalie-mask", "gear:goal-net", "gear:field-hockey-stick", "gear:lacrosse-stick", "gear:cricket-bat", "gear:wicket", "gear:table-tennis-paddle", "gear:badminton-racket", "gear:tennis-racket", "gear:squash-racket", "gear:racquetball-racket", "gear:padel-racket", "gear:pickleball-paddle", "gear:roundnet-set", "gear:volleyball-net", "gear:basketball-hoop", "gear:baseball-bat", "gear:baseball-glove", "gear:football-helmet", "gear:goalposts", "gear:hurley", "gear:curling-broom", "gear:figure-skate", "gear:speed-skate", "gear:diving-board", "gear:swim-goggles", "gear:starting-blocks", "gear:relay-baton", "gear:javelin", "gear:hurdles", "gear:gunbai", "gear:mawashi", "gear:shinai", "gear:kendo-men", "gear:judogi", "gear:obi", "gear:yumi", "gear:ya", "gear:karate-gi"];
@@ -40,7 +40,7 @@ export declare const EMBLEM_GROUPS: {
40
40
  readonly trophy: readonly ["trophy:cup", "trophy:lidded-cup", "trophy:bowl", "trophy:salver", "trophy:globe", "trophy:hockey-cup", "trophy:basketball", "trophy:baseball", "trophy:football", "trophy:canadian-bowl", "trophy:soccer-ball", "trophy:soccer-globe", "trophy:tennis-plate", "trophy:tennis-cup", "trophy:cricket-urn", "trophy:rugby-cup", "trophy:plaque", "trophy:belt", "trophy:laurel", "trophy:rosette", "trophy:ring", "trophy:banner", "trophy:sumo-cup", "trophy:championship-banner"];
41
41
  readonly medal: readonly ["medal:gold", "medal:silver", "medal:bronze", "medal:place-1", "medal:place-2", "medal:place-3"];
42
42
  readonly crest: readonly ["crest:shield", "crest:roundel", "crest:diamond", "crest:pennant"];
43
- readonly ball: readonly ["ball:soccer", "ball:soccer-colour", "ball:teqball", "ball:basketball", "ball:basketball-6", "ball:basketball-3x3", "ball:baseball", "ball:softball", "ball:tennis", "ball:padel", "ball:sliotar", "ball:volleyball", "ball:volleyball-8", "ball:beach-volleyball", "ball:cricket-red", "ball:cricket-white", "ball:cricket-pink", "ball:football", "ball:football-cfl", "ball:football-ifaf", "ball:rugby", "ball:afl", "ball:gaelic-football", "ball:netball", "ball:pickleball-outdoor", "ball:pickleball-indoor", "ball:squash", "ball:squash-yellow", "ball:squash-green", "ball:squash-red", "ball:squash-blue", "ball:racquetball", "ball:lacrosse", "ball:field-hockey", "ball:table-tennis", "ball:handball", "ball:water-polo", "ball:golf", "ball:puck", "ball:curling-stone", "ball:shot", "ball:discus", "ball:hammer", "ball:sepak-takraw", "ball:roundnet", "ball:shuttlecock-feather", "ball:shuttlecock-nylon"];
43
+ readonly ball: readonly ["ball:soccer", "ball:soccer-colour", "ball:soccer-26", "ball:soccer-18", "ball:soccer-14", "ball:soccer-12", "ball:soccer-6", "ball:soccer-orange", "ball:soccer-26-orange", "ball:soccer-18-orange", "ball:soccer-14-orange", "ball:soccer-12-orange", "ball:soccer-6-orange", "ball:soccer-yellow", "ball:soccer-26-yellow", "ball:soccer-18-yellow", "ball:soccer-14-yellow", "ball:soccer-12-yellow", "ball:soccer-6-yellow", "ball:futsal", "ball:beach-soccer", "ball:teqball", "ball:basketball", "ball:basketball-6", "ball:basketball-3x3", "ball:baseball", "ball:softball", "ball:tennis", "ball:padel", "ball:sliotar", "ball:volleyball", "ball:volleyball-8", "ball:beach-volleyball", "ball:cricket-red", "ball:cricket-white", "ball:cricket-pink", "ball:football", "ball:football-cfl", "ball:football-ifaf", "ball:rugby", "ball:afl", "ball:gaelic-football", "ball:netball", "ball:pickleball-outdoor", "ball:pickleball-indoor", "ball:squash", "ball:squash-yellow", "ball:squash-green", "ball:squash-red", "ball:squash-blue", "ball:racquetball", "ball:lacrosse", "ball:field-hockey", "ball:table-tennis", "ball:handball", "ball:water-polo", "ball:golf", "ball:puck", "ball:curling-stone", "ball:shot", "ball:discus", "ball:hammer", "ball:sepak-takraw", "ball:roundnet", "ball:shuttlecock-feather", "ball:shuttlecock-nylon"];
44
44
  readonly gear: readonly ["gear:hockey-stick", "gear:goalie-stick", "gear:hockey-skate", "gear:goalie-mask", "gear:goal-net", "gear:field-hockey-stick", "gear:lacrosse-stick", "gear:cricket-bat", "gear:wicket", "gear:table-tennis-paddle", "gear:badminton-racket", "gear:tennis-racket", "gear:squash-racket", "gear:racquetball-racket", "gear:padel-racket", "gear:pickleball-paddle", "gear:roundnet-set", "gear:volleyball-net", "gear:basketball-hoop", "gear:baseball-bat", "gear:baseball-glove", "gear:football-helmet", "gear:goalposts", "gear:hurley", "gear:curling-broom", "gear:figure-skate", "gear:speed-skate", "gear:diving-board", "gear:swim-goggles", "gear:starting-blocks", "gear:relay-baton", "gear:javelin", "gear:hurdles", "gear:gunbai", "gear:mawashi", "gear:shinai", "gear:kendo-men", "gear:judogi", "gear:obi", "gear:yumi", "gear:ya", "gear:karate-gi"];
45
45
  readonly sport: readonly ("sport:sumo" | "sport:kendo" | "sport:judo" | "sport:kyudo" | "sport:karate" | "sport:nfl" | "sport:cfl" | "sport:ifaf" | "sport:nhl" | "sport:nba" | "sport:mlb" | "sport:soccer" | "sport:curling" | "sport:tennis" | "sport:volleyball" | "sport:badminton" | "sport:pickleball" | "sport:tabletennis" | "sport:padel" | "sport:squash" | "sport:racquetball" | "sport:sepaktakraw" | "sport:teqball" | "sport:roundnet" | "sport:netball" | "sport:swimming" | "sport:waterpolo" | "sport:diving" | "sport:artisticswimming" | "sport:lacrosse" | "sport:rugby" | "sport:afl" | "sport:gaelic" | "sport:cricket" | "sport:fieldhockey" | "sport:handball" | "sport:athletics" | "sport:skating")[];
46
46
  readonly pennant: readonly ["pennant:championship"];
@@ -24,7 +24,7 @@ export const CREST_SHAPES = ["shield", "roundel", "diamond", "pennant"];
24
24
  export const CREST_IDS = ["crest:shield", "crest:roundel", "crest:diamond", "crest:pennant"];
25
25
  /** The balls: one for each ball and implement the sports use, in 3D, with the official size it comes from. The balls of one family (the three crickets, the five squash balls) share a module. */
26
26
  export const BALL_IDS = [
27
- "ball:soccer", "ball:soccer-colour", "ball:teqball", "ball:basketball", "ball:basketball-6", "ball:basketball-3x3", "ball:baseball", "ball:softball", "ball:tennis", "ball:padel",
27
+ "ball:soccer", "ball:soccer-colour", "ball:soccer-26", "ball:soccer-18", "ball:soccer-14", "ball:soccer-12", "ball:soccer-6", "ball:soccer-orange", "ball:soccer-26-orange", "ball:soccer-18-orange", "ball:soccer-14-orange", "ball:soccer-12-orange", "ball:soccer-6-orange", "ball:soccer-yellow", "ball:soccer-26-yellow", "ball:soccer-18-yellow", "ball:soccer-14-yellow", "ball:soccer-12-yellow", "ball:soccer-6-yellow", "ball:futsal", "ball:beach-soccer", "ball:teqball", "ball:basketball", "ball:basketball-6", "ball:basketball-3x3", "ball:baseball", "ball:softball", "ball:tennis", "ball:padel",
28
28
  "ball:sliotar", "ball:volleyball", "ball:volleyball-8", "ball:beach-volleyball", "ball:cricket-red", "ball:cricket-white", "ball:cricket-pink", "ball:football", "ball:football-cfl",
29
29
  "ball:football-ifaf", "ball:rugby", "ball:afl", "ball:gaelic-football", "ball:netball", "ball:pickleball-outdoor", "ball:pickleball-indoor", "ball:squash", "ball:squash-yellow",
30
30
  "ball:squash-green", "ball:squash-red", "ball:squash-blue", "ball:racquetball", "ball:lacrosse", "ball:field-hockey", "ball:table-tennis", "ball:handball", "ball:water-polo", "ball:golf",
@@ -96,7 +96,8 @@ export function defineFlat(id, en, ja, source, model) {
96
96
  const shown = across >= 0 ? model.front : (model.back ?? model.front.filter((x) => x[0] === "m" || x[0] === "o" || x[0] === "s"));
97
97
  const sx = Math.max(Math.abs(across), 0.02);
98
98
  const sy = Math.max(Math.abs(tilt), 0.02);
99
- const rim = (model.rim ?? 0) * Math.sin((yaw * Math.PI) / 180);
99
+ // The rim is on the left for a positive yaw, which carries the front toward the right (as it does for a ball, a cup and a drag to the right).
100
+ const rim = -(model.rim ?? 0) * Math.sin((yaw * Math.PI) / 180);
100
101
  const [x0, y0, w, h] = (model.box ?? "0 0 64 64").split(" ").map(Number);
101
102
  const thick = rim === 0 ? "" : `<g transform="translate(${n2(rim)} 0)">${shapesMarkup(model.front.filter((x) => x[0] === "m" || x[0] === "o").map((x) => ["s", x[1]]), ctx)}</g>`;
102
103
  return document(en, model, r, ctx, `<g transform="translate(${n2(x0 + w / 2)} ${n2(y0 + h / 2)}) scale(${n2(sx)} ${n2(sy)}) translate(${n2(-x0 - w / 2)} ${n2(-y0 - h / 2)})">${thick}${face(model, shown, ctx, engraved(model, ctx, across >= 0 ? "front" : "back"))}</g>`);
package/dist/index.d.ts CHANGED
@@ -105,6 +105,8 @@ export { BALL_SHUTTLECOCK_FEATHER, BALL_SHUTTLECOCK_NYLON } from "./emblems/ball
105
105
  export { BALL_SLIOTAR } from "./emblems/ball/ball.sliotar.ts";
106
106
  export { BALL_LACROSSE, BALL_RACQUETBALL, BALL_TABLE_TENNIS, BALL_SHOT } from "./emblems/ball/ball.smooth.ts";
107
107
  export { BALL_SOCCER, BALL_SOCCER_COLOUR } from "./emblems/ball/ball.soccer.ts";
108
+ export { BALL_SOCCER_26, BALL_SOCCER_18, BALL_SOCCER_14, BALL_SOCCER_12, BALL_SOCCER_6, BALL_SOCCER_ORANGE, BALL_SOCCER_26_ORANGE, BALL_SOCCER_18_ORANGE, BALL_SOCCER_14_ORANGE, BALL_SOCCER_12_ORANGE, BALL_SOCCER_6_ORANGE, BALL_SOCCER_YELLOW, BALL_SOCCER_26_YELLOW, BALL_SOCCER_18_YELLOW, BALL_SOCCER_14_YELLOW, BALL_SOCCER_12_YELLOW, BALL_SOCCER_6_YELLOW } from "./emblems/ball/ball.soccer.panels.ts";
109
+ export { BALL_FUTSAL, BALL_BEACH_SOCCER } from "./emblems/ball/ball.soccer.indoor.ts";
108
110
  export { BALL_SOFTBALL } from "./emblems/ball/ball.softball.ts";
109
111
  export { BALL_SQUASH_DOUBLE_YELLOW, BALL_SQUASH_YELLOW, BALL_SQUASH_RED, BALL_SQUASH_BLUE, BALL_SQUASH_GREEN } from "./emblems/ball/ball.squash.ts";
110
112
  export { BALL_TENNIS } from "./emblems/ball/ball.tennis.ts";
package/dist/index.js CHANGED
@@ -106,6 +106,8 @@ export { BALL_SHUTTLECOCK_FEATHER, BALL_SHUTTLECOCK_NYLON } from "./emblems/ball
106
106
  export { BALL_SLIOTAR } from "./emblems/ball/ball.sliotar.js";
107
107
  export { BALL_LACROSSE, BALL_RACQUETBALL, BALL_TABLE_TENNIS, BALL_SHOT } from "./emblems/ball/ball.smooth.js";
108
108
  export { BALL_SOCCER, BALL_SOCCER_COLOUR } from "./emblems/ball/ball.soccer.js";
109
+ export { BALL_SOCCER_26, BALL_SOCCER_18, BALL_SOCCER_14, BALL_SOCCER_12, BALL_SOCCER_6, BALL_SOCCER_ORANGE, BALL_SOCCER_26_ORANGE, BALL_SOCCER_18_ORANGE, BALL_SOCCER_14_ORANGE, BALL_SOCCER_12_ORANGE, BALL_SOCCER_6_ORANGE, BALL_SOCCER_YELLOW, BALL_SOCCER_26_YELLOW, BALL_SOCCER_18_YELLOW, BALL_SOCCER_14_YELLOW, BALL_SOCCER_12_YELLOW, BALL_SOCCER_6_YELLOW } from "./emblems/ball/ball.soccer.panels.js";
110
+ export { BALL_FUTSAL, BALL_BEACH_SOCCER } from "./emblems/ball/ball.soccer.indoor.js";
109
111
  export { BALL_SOFTBALL } from "./emblems/ball/ball.softball.js";
110
112
  export { BALL_SQUASH_DOUBLE_YELLOW, BALL_SQUASH_YELLOW, BALL_SQUASH_RED, BALL_SQUASH_BLUE, BALL_SQUASH_GREEN } from "./emblems/ball/ball.squash.js";
111
113
  export { BALL_TENNIS } from "./emblems/ball/ball.tennis.js";
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.2.0";
1
+ export declare const VERSION = "0.3.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.2.0";
1
+ export const VERSION = "0.3.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnmorrisdotca/dougu-sports",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Dougu Sports 道具, sports emblems as SVG strings: trophies, medals, crests, sport icons, and the ball and gear of every sport, in metal, line or flat colour, with optional 3D turning. Light and dark, accessible, tree-shakeable. Zero dependencies.",
5
5
  "keywords": [
6
6
  "sports",