@johnmorrisdotca/dougu-sports 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,20 @@ All notable changes to this project are written here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.15.0] - 2026-10-21
10
+
11
+ The pickleballs get real holes: cut through the shell, with the inside of the ball behind them, and the page showing through where two holes line up.
12
+
13
+ ### Changed
14
+
15
+ - **`ball:pickleball-outdoor` and `ball:pickleball-indoor` have real holes** (new module `src/emblems/ball/ball.holes.ts`, taken only by the pickleballs). Each hole on the near side is cut out of the shell by a mask, so its shading, highlight and grain are cut with it; behind it is the inside of the hollow ball (the far shell's inner surface, darker, lit low and to the right) with the far holes cut out of it, so where a near hole and a far hole line up the SVG is transparent and the page shows through, on any background. A hole is a cylinder along the line to the centre: near the edge it is a thin ellipse and the bore's wall shows as a crescent, toned by the light, and the cut edge carries a soft shadow and a thin lit rim (the thickness of the shell). It turns with `ballFrame`, `emblemFrame` and `bindTurn`; every turn has ids of its own. The line style still draws a mark for each hole, and the hole counts and sizes are unchanged (40 small, 26 large). A pickleball's drawing is about 8 kB where it was 3.7 kB (about 1.7 to 2 kB gzipped); its bundle is 8.0 kB gzipped (the other balls' are unchanged).
16
+ - Every other ball with round marks (a golf ball's and a field hockey ball's dimples, a squash ball's dots) is drawn exactly as before: `BallSpec` gained an optional `holes` function that only the pickleballs set, and `test/holes.test.ts` holds the drawings of 0.14.0 by hash.
17
+
18
+ ### Added
19
+
20
+ - **`docs/pickleball.md`**, with pictures of both balls at rest and turned, light and dark, and a turning picture (`pnpm animations`, scene `pickleball-turn`; `pnpm screenshots`, sheet `pickleball`). The README shows the still picture under "The best of it", and the balls sheet is redrawn with the new hole.
21
+ - **`e2e/holes.demo.ts`**: in a real browser, a pixel seen through a near hole and the far hole behind it is transparent and equals the page's own on four page colours; the shell round a hole is solid; a turned ball shows the dark inside; a turning pickleball has open holes at some angle and a golf ball never does.
22
+
9
23
  ## [0.14.0] - 2026-10-21
10
24
 
11
25
  The auto-turn can go either way, and the pictures and the demo are slower and wait for the pointer.
package/README.md CHANGED
@@ -79,6 +79,15 @@ The ball and the gear of every sport, trophies, medals, crests and the icon of e
79
79
  </picture>
80
80
  </p>
81
81
 
82
+ - **Pickleballs with real holes**: the holes are cut through the shell, so you see the dark inside of the ball and, where a near hole and a far one line up, the page itself ([pickleballs](./docs/pickleball.md)).
83
+
84
+ <p align="center">
85
+ <picture>
86
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/dougu-sports/main/docs/images/pickleball-dark.webp">
87
+ <img alt="Two pickleballs, the yellow outdoor ball with its 40 small holes above and the white indoor ball with its 26 large ones below, each at rest and at three turns: through each hole the dark inside of the ball shows, and where a hole in the near side lines up with one in the far side the background shows through." src="https://raw.githubusercontent.com/johnmorrisdotca/dougu-sports/main/docs/images/pickleball-light.webp" width="760">
88
+ </picture>
89
+ </p>
90
+
82
91
  - **Trophies, medals, crests and the icon of every sport**, in metal, one line or flat colour, light and dark.
83
92
 
84
93
  <p align="center">
@@ -658,7 +667,7 @@ Bug reports and ideas go in the [issues](https://github.com/johnmorrisdotca/doug
658
667
 
659
668
  ## Changes
660
669
 
661
- See [CHANGELOG.md](./CHANGELOG.md). 0.14.0 lets the auto-turn go either way (`direction: "clockwise"` or `"counterclockwise"`, `setDirection()`); 0.13.0 adds a pictogram for each of four combat sports, MMA (a cage), boxing (a glove), wrestling (two wrestlers on the mat) and professional wrestling (a ring with ropes), in the three styles (325 emblems); 0.12.0 adds forty-seven named trophies, each a close likeness measured on a photograph (321 emblems); 0.11.0 gives the era soccer balls neutral ids (the old ones stay as aliases), new looks and a second 1980s ball (274 emblems); 0.10.0 adds a standalone ribbon and the nets and posts of sepak takraw, netball and teqball from their rulebooks' sizes (273 emblems); 0.9.0 lets the baseball, the softball and the classic soccer balls be painted and printed: solid and two-tone colours, a logo of your own wrapped onto the ball, stamped text, an original cursive word and a small mark, all turning with the ball ([docs/custom-balls.md](./docs/custom-balls.md)); 0.8.0 makes `bindTurn` light: it takes an emblem object and then needs no registry, so a turnable cup is about 9.4 kB gzipped instead of about 81 kB, and an id loads the registry on demand; 0.7.3 turns the build, docs and picture scripts, the browser tests and the demo's source from JavaScript into TypeScript, with no change to the package; 0.7.2 makes every docs page single-language (a page in Japanese beside each page that had Japanese words in it, and a test that holds them apart), with no change to the pictures; 0.7.1 puts real countries (Canada, Japan) in the crest and puck examples in place of made-up teams, with no change to the code; 0.7.0 gives the national volleyballs layouts that tell them apart (Japan a crimson band, Poland white above and red below, the USA a navy field), strengthens the 2010-style grooved soccer ball, and adds six pictures of how the judo belt is tied, `gear:obi-step-1` to `gear:obi-step-6` (269 emblems: [docs/obi-steps.md](./docs/obi-steps.md)); 0.6.0 gives the judo suit and the karate suit their sleeves (a judogi has no short ones: they reach the wrist) and adds the judogi seen from behind with the IJF-style back number, a family name and a country code on a patch, with a `backnumber` option (269 emblems: [docs/judogi-back.md](./docs/judogi-back.md)); 0.5.1 redraws the judo suit, the belt and the football pictograms and adds the blue and folded suits and the belts of every grade (261 emblems: [docs/japanese-sports.md](./docs/japanese-sports.md)); 0.5.0 adds fifty balls: vintage leather soccer balls, era-style and shop-wall soccer balls, baseballs of other threads and volleyballs in national colours, with a `colors` option (244 emblems: [docs/vintage-balls.md](./docs/vintage-balls.md)); 0.4.0 adds eight printed pucks and a `face` option (194 emblems: [docs/pucks.md](./docs/pucks.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.
670
+ See [CHANGELOG.md](./CHANGELOG.md). 0.15.0 gives the pickleballs real holes, cut through the shell and drawn with the inside of the ball behind them, so the page shows through where two holes line up ([docs/pickleball.md](./docs/pickleball.md)); 0.14.0 lets the auto-turn go either way (`direction: "clockwise"` or `"counterclockwise"`, `setDirection()`); 0.13.0 adds a pictogram for each of four combat sports, MMA (a cage), boxing (a glove), wrestling (two wrestlers on the mat) and professional wrestling (a ring with ropes), in the three styles (325 emblems); 0.12.0 adds forty-seven named trophies, each a close likeness measured on a photograph (321 emblems); 0.11.0 gives the era soccer balls neutral ids (the old ones stay as aliases), new looks and a second 1980s ball (274 emblems); 0.10.0 adds a standalone ribbon and the nets and posts of sepak takraw, netball and teqball from their rulebooks' sizes (273 emblems); 0.9.0 lets the baseball, the softball and the classic soccer balls be painted and printed: solid and two-tone colours, a logo of your own wrapped onto the ball, stamped text, an original cursive word and a small mark, all turning with the ball ([docs/custom-balls.md](./docs/custom-balls.md)); 0.8.0 makes `bindTurn` light: it takes an emblem object and then needs no registry, so a turnable cup is about 9.4 kB gzipped instead of about 81 kB, and an id loads the registry on demand; 0.7.3 turns the build, docs and picture scripts, the browser tests and the demo's source from JavaScript into TypeScript, with no change to the package; 0.7.2 makes every docs page single-language (a page in Japanese beside each page that had Japanese words in it, and a test that holds them apart), with no change to the pictures; 0.7.1 puts real countries (Canada, Japan) in the crest and puck examples in place of made-up teams, with no change to the code; 0.7.0 gives the national volleyballs layouts that tell them apart (Japan a crimson band, Poland white above and red below, the USA a navy field), strengthens the 2010-style grooved soccer ball, and adds six pictures of how the judo belt is tied, `gear:obi-step-1` to `gear:obi-step-6` (269 emblems: [docs/obi-steps.md](./docs/obi-steps.md)); 0.6.0 gives the judo suit and the karate suit their sleeves (a judogi has no short ones: they reach the wrist) and adds the judogi seen from behind with the IJF-style back number, a family name and a country code on a patch, with a `backnumber` option (269 emblems: [docs/judogi-back.md](./docs/judogi-back.md)); 0.5.1 redraws the judo suit, the belt and the football pictograms and adds the blue and folded suits and the belts of every grade (261 emblems: [docs/japanese-sports.md](./docs/japanese-sports.md)); 0.5.0 adds fifty balls: vintage leather soccer balls, era-style and shop-wall soccer balls, baseballs of other threads and volleyballs in national colours, with a `colors` option (244 emblems: [docs/vintage-balls.md](./docs/vintage-balls.md)); 0.4.0 adds eight printed pucks and a `face` option (194 emblems: [docs/pucks.md](./docs/pucks.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.
662
671
 
663
672
  ## Credits
664
673
 
@@ -1,4 +1,5 @@
1
1
  import { type Piece, type PieceSize, type PrintOptions, type PrintRenderer, type ResolvedOptions, type ResolvedPaint } from "./ball.core.ts";
2
+ import type { HoleLayers } from "./ball.holes.ts";
2
3
  import { type M3, type V3 } from "./ball.math.ts";
3
4
  /** How a ball takes the light. */
4
5
  export interface Material {
@@ -121,6 +122,11 @@ export interface BallSpec {
121
122
  at?: V3;
122
123
  /** The fraction of the box's radius the ball takes (1 by default): a hammer's head is small, to leave room for its wire. */
123
124
  fit?: number;
125
+ /**
126
+ * A ball with real holes (a pickleball), drilled through its shell: each near hole is cut out and shows the inside of the ball, and where a near and a far hole line up the page shows through.
127
+ * It is a function (`drillHoles` in ball.holes.ts, bound to the hole centres) so that a ball without holes does not carry the code. The line style does not call it (a `draw` that wants marks there draws them).
128
+ */
129
+ holes?: (s: Surface, id: string, base: string, shape: (attrs: string) => string, minWidth: number) => HoleLayers;
124
130
  /** What is attached to the ball and sticks out of it (a hammer's wire and handle): drawn behind the ball or in front of it, by what is nearer to you. */
125
131
  attach?: (s: Surface, base: string) => {
126
132
  behind: string;
@@ -363,11 +363,13 @@ export function drawBall(title, key, spec, opts, turn, turning) {
363
363
  (gr === undefined
364
364
  ? ""
365
365
  : grainDef(id, gr.f, gr.a, gr.seed));
366
- const inner = `<g clip-path="url(#${id}c)"${gr === undefined ? "" : ` filter="url(#${id}n)"`}>${shape(`fill="${ground ?? spec.ground ?? base}"`)}${layers}</g>`;
366
+ const hol = spec.holes?.(surface, id, base, shape, minWidth);
367
+ const inner = `<g clip-path="url(#${id}c)"${gr === undefined ? "" : ` filter="url(#${id}n)"`}>${shape(`fill="${ground ?? spec.ground ?? base}"`)}${layers}${hol?.edge ?? ""}</g>`;
367
368
  const metal = m.metal === true ? shape(`fill="url(#${id}m)"`) : "";
368
369
  const overlays = shape(`fill="url(#${id}d)"`) + metal + shape(`fill="url(#${id}b)"`) + shape(`fill="url(#${id}h)"`);
369
370
  const at = spec.attach?.(surface, base);
370
- return wrapPiece({ title, size: opts.size, theme: opts.theme, defs: defs + metalDef(id, m.metal === true), body: `${at?.behind ?? ""}${inner}${overlays}${at?.front ?? ""}` });
371
+ const shell = hol?.mask === undefined ? `${inner}${overlays}` : `<g mask="url(#${hol.mask})">${inner}${overlays}</g>`;
372
+ return wrapPiece({ title, size: opts.size, theme: opts.theme, defs: defs + (hol?.defs ?? "") + metalDef(id, m.metal === true), body: `${at?.behind ?? ""}${hol?.behind ?? ""}${shell}${at?.front ?? ""}` });
371
373
  }
372
374
  /** A ball's label area as a surface sees it: the middle, the right-hand and the upward direction, orthogonal, and how far it reaches. */
373
375
  function homeOf(spec) {
@@ -2,6 +2,7 @@ import { type BallDef } from "./ball.engine.ts";
2
2
  import { type V3 } from "./ball.math.ts";
3
3
  import type { Piece } from "./ball.core.ts";
4
4
  import type { BallSpec } from "./ball.engine.ts";
5
+ import type { Drill } from "./ball.holes.ts";
5
6
  export interface DottedOptions {
6
7
  id: string;
7
8
  en: string;
@@ -19,6 +20,8 @@ export interface DottedOptions {
19
20
  mark?: string;
20
21
  /** A lit rim on each mark (not for a dimple pattern of two hundred, which is large enough already). */
21
22
  light?: boolean;
23
+ /** Real holes through the shell, not marks on it (`holeLayers` from ball.holes.ts, taken by the pickleball only): the near ones are cut out and show the inside of the ball, and where two line up the page shows through. The line style still draws marks. */
24
+ hollow?: Drill;
22
25
  /** Turns the spread, so a ball does not show its spiral's pole straight on. */
23
26
  phase?: number;
24
27
  }
@@ -13,8 +13,11 @@ export function dottedBall(o) {
13
13
  mat: o.mat,
14
14
  draw: (s, base) => {
15
15
  const mark = o.mark ?? mix(base, -0.55);
16
+ if (o.hollow !== undefined && !s.line)
17
+ return "";
16
18
  return s.dots(centres, o.radius, mark, { opacity: o.depth ?? 0.55, ink: true, light: o.light ?? true });
17
19
  },
20
+ holes: o.hollow === undefined ? undefined : (s, id, base, shape, minWidth) => o.hollow(s, id, { centres, radius: o.radius }, base, shape, minWidth),
18
21
  };
19
22
  return defineBall({ id: o.id, en: o.en, ja: o.ja, spec, size: o.size });
20
23
  }
@@ -0,0 +1,18 @@
1
+ import type { Surface } from "./ball.engine.ts";
2
+ import { type V3 } from "./ball.math.ts";
3
+ /** Where a ball is drilled: the hole centres on the unit sphere, a hole's radius as the angle of its cap in radians, and the shell's thickness as a fraction of the ball's radius (0.05 by default). */
4
+ export interface Holes {
5
+ centres: readonly V3[];
6
+ radius: number;
7
+ wall?: number;
8
+ }
9
+ /** What `holeLayers` makes: the definitions, the inside of the ball (drawn first), the edges drawn on the shell, and the mask that cuts the near holes out of everything on the shell, if there is one. */
10
+ export interface HoleLayers {
11
+ defs: string;
12
+ behind: string;
13
+ edge: string;
14
+ mask: string | undefined;
15
+ }
16
+ /** The signature of `holeLayers`, which a ball is handed so that its own module is the only one that carries the code. */
17
+ export type Drill = (s: Surface, id: string, holes: Holes, base: string, shape: (attrs: string) => string, minWidth: number) => HoleLayers;
18
+ export declare const holeLayers: Drill;
@@ -0,0 +1,88 @@
1
+ import { hashOf, mix, num } from "./ball.core.js";
2
+ import { basis, ellipseOf, mul } from "./ball.math.js";
3
+ /** The circle of the hole's bore at `depth` below the surface (0 on the outside), as the ellipse it makes on the screen; undefined where it is too thin to draw. */
4
+ function bore(s, m, radius, depth) {
5
+ const [t1, t2] = basis(m);
6
+ const sr = Math.sin(radius), cr = Math.cos(radius) - depth;
7
+ const c = s.project(mul(m, cr));
8
+ const o = s.project([0, 0, 0]);
9
+ const pa = s.project(mul(t1, sr));
10
+ const pb = s.project(mul(t2, sr));
11
+ const da = [pa[0] - o[0], pa[1] - o[1]];
12
+ const db = [pb[0] - o[0], pb[1] - o[1]];
13
+ const { rx, ry, angle } = ellipseOf([[da[0], db[0], 0], [da[1], db[1], 0]]);
14
+ if (rx < 0.05)
15
+ return undefined;
16
+ return { x: c[0], y: c[1], rx, ry: Math.max(ry, 0.05), deg: (angle * 180) / Math.PI, ax: Math.cos(angle) * rx, ay: Math.sin(angle) * rx, facing: s.facing(m) };
17
+ }
18
+ const ellipse = (e) => {
19
+ const arcs = `a${num(e.rx)} ${num(e.ry)} ${num(e.deg, 0)} 1 0`;
20
+ return `M${num(e.x + e.ax)} ${num(e.y + e.ay)}${arcs}${num(-2 * e.ax)} ${num(-2 * e.ay)}${arcs}${num(2 * e.ax)} ${num(2 * e.ay)}`;
21
+ };
22
+ /** The light comes from the upper left, as it does everywhere else in the engine. */
23
+ const LIGHT = [-0.62, -0.78];
24
+ /** The tones a wall is drawn in, darkest first, as amounts of the ball's colour mixed toward black. */
25
+ const TONES = [-0.6, -0.45, -0.3, -0.15];
26
+ export const holeLayers = (s, id, holes, base, shape, minWidth) => {
27
+ const thick = holes.wall ?? 0.05;
28
+ const front = [];
29
+ const backOut = [];
30
+ const backIn = [];
31
+ for (const m of holes.centres) {
32
+ const f = s.facing(m);
33
+ if (f >= 0.08) {
34
+ const out = bore(s, m, holes.radius, 0);
35
+ if (out !== undefined)
36
+ front.push({ out, inn: bore(s, m, holes.radius, thick) });
37
+ }
38
+ else if (f <= -0.08) {
39
+ const out = bore(s, m, holes.radius, 0);
40
+ const inn = bore(s, m, holes.radius, thick);
41
+ if (out !== undefined && inn !== undefined) {
42
+ backOut.push(out);
43
+ backIn.push(inn);
44
+ }
45
+ }
46
+ }
47
+ if (front.length === 0 && backOut.length === 0)
48
+ return { defs: "", behind: "", edge: "", mask: undefined };
49
+ const frontPath = front.map((h) => ellipse(h.out)).join("");
50
+ const backOutPath = backOut.map(ellipse).join("");
51
+ const backInPath = backIn.map(ellipse).join("");
52
+ const key = hashOf(frontPath + "|" + backOutPath + "|" + backInPath);
53
+ // The inside of the ball: lit a little where the light that comes through the near holes lands (low and to the right), dark toward the edge.
54
+ let defs = `<radialGradient id="${id}i" cx=".62" cy=".66" r=".62"><stop offset="0" stop-color="${mix(base, -0.36)}"/><stop offset="1" stop-color="${mix(base, -0.72)}"/></radialGradient>`;
55
+ let disc = shape(`fill="url(#${id}i)"`);
56
+ if (backOut.length > 0) {
57
+ // A far hole is open where its inner opening and its outer opening overlap: the bore leans away from you, so a hole near the far edge is a slit.
58
+ defs += `<clipPath id="${id}o${key}"><path d="${backOutPath}"/></clipPath><mask id="${id}b${key}" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100"><rect width="100" height="100" fill="#fff"/><path d="${backInPath}" fill="#000" clip-path="url(#${id}o${key})"/></mask>`;
59
+ disc = shape(`fill="url(#${id}i)" mask="url(#${id}b${key})"`);
60
+ }
61
+ // The wall of each near hole: what is inside the outer opening and outside the inner one (the part of the bore that faces you), toned by whether it faces the light and how near the edge it is.
62
+ const o = s.project([0, 0, 0]);
63
+ const buckets = TONES.map(() => []);
64
+ for (const h of front) {
65
+ if (h.inn === undefined)
66
+ continue;
67
+ const dx = h.out.x - o[0], dy = h.out.y - o[1];
68
+ const d = Math.hypot(dx, dy) || 1;
69
+ // The wall you see is the one on the far side of the hole, facing the middle of the ball on the screen.
70
+ const lit = (-dx / d) * LIGHT[0] + (-dy / d) * LIGHT[1];
71
+ const k = -0.3 + 0.2 * lit - 0.25 * (1 - h.out.facing);
72
+ let best = 0;
73
+ for (let i = 1; i < TONES.length; i++)
74
+ if (Math.abs(TONES[i] - k) < Math.abs(TONES[best] - k))
75
+ best = i;
76
+ buckets[best].push(ellipse(h.out) + ellipse(h.inn));
77
+ }
78
+ const walls = buckets.map((b, i) => (b.length === 0 ? "" : `<path d="${b.join("")}" fill="${mix(base, TONES[i])}" fill-rule="evenodd"/>`)).join("");
79
+ const behind = `<g clip-path="url(#${id}c)">${disc}${walls}</g>`;
80
+ if (front.length === 0)
81
+ return { defs, behind, edge: "", mask: undefined };
82
+ // The outline of the near holes is written once and used three times.
83
+ defs += `<path id="${id}f${key}" d="${frontPath}"/><mask id="${id}k${key}" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100"><rect width="100" height="100" fill="#fff"/><use href="#${id}f${key}" fill="#000"/></mask>`;
84
+ // On the shell, round each cut: a soft shadow where the plastic turns in, and a thin lit edge (the cut face of the shell). Half of each stroke lies inside the cut and is masked away.
85
+ const edge = `<use href="#${id}f${key}" fill="none" stroke="${mix(base, -0.6)}" stroke-width="${num(Math.max(1.1, minWidth * 0.8), 2)}" opacity=".22"/>` +
86
+ `<use href="#${id}f${key}" fill="none" stroke="${mix(base, 0.5)}" stroke-width="${num(Math.max(0.3, minWidth * 0.3), 2)}" opacity=".5"/>`;
87
+ return { defs, behind, edge, mask: `${id}k${key}` };
88
+ };
@@ -1,5 +1,6 @@
1
1
  // Pickleball: a perforated plastic ball. The outdoor ball has 40 small holes; the indoor ball has 26 larger ones (the rulebook gives 26 to 40 holes; the split is how balls are sold).
2
2
  import { dottedBall } from "./ball.family.dotted.js";
3
+ import { holeLayers } from "./ball.holes.js";
3
4
  export const BALL_PICKLEBALL_OUTDOOR = /* @__PURE__ */ dottedBall({
4
5
  id: "ball:pickleball-outdoor",
5
6
  en: "Pickleball, outdoor",
@@ -11,6 +12,7 @@ export const BALL_PICKLEBALL_OUTDOOR = /* @__PURE__ */ dottedBall({
11
12
  mat: { gloss: 0.55, shade: "#4a5008", grain: { f: 1.4, a: 0.1 } },
12
13
  mark: "#454b0e",
13
14
  depth: 0.75,
15
+ hollow: holeLayers,
14
16
  });
15
17
  export const BALL_PICKLEBALL_INDOOR = /* @__PURE__ */ dottedBall({
16
18
  id: "ball:pickleball-indoor",
@@ -23,4 +25,5 @@ export const BALL_PICKLEBALL_INDOOR = /* @__PURE__ */ dottedBall({
23
25
  mat: { gloss: 0.6, shade: "#3a3a40", grain: { f: 1.4, a: 0.08 } },
24
26
  mark: "#3d3d44",
25
27
  depth: 0.7,
28
+ hollow: holeLayers,
26
29
  });
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.14.0";
1
+ export declare const VERSION = "0.15.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.14.0";
1
+ export const VERSION = "0.15.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnmorrisdotca/dougu-sports",
3
- "version": "0.14.0",
3
+ "version": "0.15.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",