@johnmorrisdotca/dougu-sports 0.7.2 → 0.9.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 (46) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/NOTICE.md +1 -1
  3. package/README.md +58 -28
  4. package/dist/emblems/ball/ball.baseball.js +2 -1
  5. package/dist/emblems/ball/ball.contrast.d.ts +4 -0
  6. package/dist/emblems/ball/ball.contrast.js +22 -0
  7. package/dist/emblems/ball/ball.core.d.ts +42 -0
  8. package/dist/emblems/ball/ball.core.js +34 -1
  9. package/dist/emblems/ball/ball.engine.d.ts +70 -4
  10. package/dist/emblems/ball/ball.engine.js +70 -20
  11. package/dist/emblems/ball/ball.family.seamed.d.ts +4 -0
  12. package/dist/emblems/ball/ball.family.seamed.js +42 -7
  13. package/dist/emblems/ball/ball.image.d.ts +8 -0
  14. package/dist/emblems/ball/ball.image.js +14 -5
  15. package/dist/emblems/ball/ball.index.js +2 -4
  16. package/dist/emblems/ball/ball.letters.d.ts +3 -0
  17. package/dist/emblems/ball/ball.letters.js +51 -0
  18. package/dist/emblems/ball/ball.print.d.ts +81 -0
  19. package/dist/emblems/ball/ball.print.js +268 -0
  20. package/dist/emblems/ball/ball.printable.d.ts +6 -0
  21. package/dist/emblems/ball/ball.printable.js +13 -0
  22. package/dist/emblems/ball/ball.region.d.ts +12 -0
  23. package/dist/emblems/ball/ball.region.js +94 -0
  24. package/dist/emblems/ball/ball.script.d.ts +15 -0
  25. package/dist/emblems/ball/ball.script.js +107 -0
  26. package/dist/emblems/ball/ball.soccer.js +7 -3
  27. package/dist/emblems/ball/ball.soccer.panels.d.ts +2 -0
  28. package/dist/emblems/ball/ball.soccer.panels.js +6 -3
  29. package/dist/emblems/ball/ball.soccer.size.d.ts +2 -0
  30. package/dist/emblems/ball/ball.soccer.size.js +8 -0
  31. package/dist/emblems/ball/ball.softball.js +2 -1
  32. package/dist/emblems/ball/ball.warp.d.ts +48 -0
  33. package/dist/emblems/ball/ball.warp.js +441 -0
  34. package/dist/emblems/emblem.core.d.ts +13 -0
  35. package/dist/emblems-entry.d.ts +1 -0
  36. package/dist/index.d.ts +5 -5
  37. package/dist/index.js +4 -5
  38. package/dist/turn-control.d.ts +24 -5
  39. package/dist/turn-control.js +67 -23
  40. package/dist/turn-full.d.ts +15 -0
  41. package/dist/turn-full.js +19 -0
  42. package/dist/turn-registry.d.ts +1 -0
  43. package/dist/turn-registry.js +3 -0
  44. package/dist/version.d.ts +1 -1
  45. package/dist/version.js +1 -1
  46. package/package.json +10 -10
@@ -1,9 +1,9 @@
1
1
  // The ball renderer: one procedural engine for every sphere and every prolate ball. A ball is drawn in true 3D: a rotation, an orthographic projection, back faces dropped, panels cut at the limb
2
2
  // along the limb itself, then radial-gradient shading and a grain that stays where it is while the ball turns. A ball is a `BallSpec`: its shape (the semi-axes of an ellipsoid), its
3
3
  // view, its material and a `draw` function that lays panels, seams and dots on the unit sphere; the engine does the rest.
4
- import { grainDef, escape, hashOf, labelled, mix, num, radians, resolveOptions, wrapPiece } from "./ball.core.js";
4
+ import { grainDef, hasPrint, blend, escape, hashOf, labelled, mix, num, radians, resolveOptions, wrapPiece } from "./ball.core.js";
5
5
  import { add, apply, basis, cross, dot, ellipseOf, len, mul, norm, rotation, sampleRing, sub } from "./ball.math.js";
6
- const EPS = 1e-6;
6
+ export const EPS = 1e-6;
7
7
  const STEP = Math.PI / 18;
8
8
  /** How a point of the unit sphere is carried to the ball's surface: an ellipsoid's scaling, and for a pointed ball a narrowing of the cross-section toward the ends. */
9
9
  const shapeOf = (g, u) => {
@@ -11,7 +11,7 @@ const shapeOf = (g, u) => {
11
11
  return [u[0] * g.axes[0], u[1] * g.axes[1] * f, u[2] * g.axes[2] * f];
12
12
  };
13
13
  /** How squarely a point of the sphere faces the eye, before it is scaled: above 0 is the side you see. For an ellipsoid it is the dot of the point with `w`; for a pointed ball the surface's normal is worked out. */
14
- const visOf = (g, u) => {
14
+ export const visOf = (g, u) => {
15
15
  if (g.point === undefined)
16
16
  return dot(u, g.w);
17
17
  const rest = Math.max(0, 1 - u[0] * u[0]);
@@ -20,7 +20,7 @@ const visOf = (g, u) => {
20
20
  const nx = 2 * b * g.point * u[0] * rest ** (g.point - 0.5);
21
21
  return (g.rz[0] * nx + g.rz[1] * a * u[1] + g.rz[2] * a * u[2]) / (a * b);
22
22
  };
23
- const project = (g, u) => {
23
+ export const project = (g, u) => {
24
24
  const q = apply(g.rot, shapeOf(g, u));
25
25
  return [g.cx + g.k * q[0], g.cy - g.k * q[1]];
26
26
  };
@@ -82,7 +82,7 @@ function limbArc(g, from, to) {
82
82
  return out;
83
83
  }
84
84
  /** The parts of a curve that face you, each as screen points with their facing. The curve is cut exactly at the limb. */
85
- function visibleRuns(g, pts, closed) {
85
+ export function visibleRuns(g, pts, closed) {
86
86
  const n = pts.length;
87
87
  const vis = pts.map((p) => visOf(g, p) / g.wl);
88
88
  const at = (u, f) => {
@@ -130,9 +130,34 @@ function visibleRuns(g, pts, closed) {
130
130
  }
131
131
  return runs;
132
132
  }
133
- const pathOf = (pts) => pts.map((p, i) => `${i === 0 ? "M" : "L"}${num(p[0])} ${num(p[1])}`).join("");
133
+ export const pathOf = (pts, digits = 1) => pts.map((p, i) => `${i === 0 ? "M" : "L"}${num(p[0], digits)} ${num(p[1], digits)}`).join("");
134
+ /** A number written as short as SVG allows: no leading zero, and the space left out where the next number's own sign or point shows where it starts. */
135
+ const tight = (text, after) => {
136
+ const t = text.replace(/^(-?)0\./, "$1.");
137
+ return after === "" || t.startsWith("-") || (t.startsWith(".") && after.includes(".")) ? t : ` ${t}`;
138
+ };
139
+ /** A run as a path of relative moves in `digits` decimals, written tight: about half the bytes of `pathOf`. Each step is the difference of the rounded points, so the line does not drift. */
140
+ function relativePath(pts, digits) {
141
+ const r = (v) => Math.round(v * 10 ** digits) / 10 ** digits;
142
+ const first = pts[0];
143
+ let x = r(first[0]), y = r(first[1]);
144
+ let out = `M${num(x, digits)} ${num(y, digits)}l`;
145
+ let last = "";
146
+ for (let i = 1; i < pts.length; i++) {
147
+ const p = pts[i];
148
+ const nx = r(p[0]), ny = r(p[1]);
149
+ for (const v of [nx - x, ny - y]) {
150
+ const t = tight(num(v, digits), last);
151
+ out += t;
152
+ last = t.trim();
153
+ }
154
+ x = nx;
155
+ y = ny;
156
+ }
157
+ return out;
158
+ }
134
159
  /** A curve's visible parts as paths: a stroke is thinner where it turns toward the limb (three steps), so it does not sit on the ball like a wire. */
135
- function runPaths(runs, colour, width, extra) {
160
+ function runPaths(runs, colour, width, extra, digits = 1) {
136
161
  const tiers = [[], [], []];
137
162
  const factor = [1, 0.74, 0.5];
138
163
  const tierOf = (f) => (f > 0.55 ? 0 : f > 0.25 ? 1 : 2);
@@ -141,7 +166,7 @@ function runPaths(runs, colour, width, extra) {
141
166
  let piece = [];
142
167
  const flush = () => {
143
168
  if (piece.length > 1)
144
- tiers[kind].push(pathOf(piece));
169
+ tiers[kind].push(digits > 1 ? relativePath(piece, digits) : pathOf(piece, digits));
145
170
  piece = [];
146
171
  };
147
172
  for (const p of run.pts) {
@@ -159,7 +184,7 @@ function runPaths(runs, colour, width, extra) {
159
184
  const make = (d, w) => (d.length === 0 ? "" : `<path d="${d.join("")}" fill="none" stroke="${colour}" stroke-width="${num(w, 2)}" stroke-linecap="round" stroke-linejoin="round"${extra}/>`);
160
185
  return make(tiers[2], width * factor[2]) + make(tiers[1], width * factor[1]) + make(tiers[0], width);
161
186
  }
162
- function surfaceOf(g, line, minWidth) {
187
+ function surfaceOf(g, line, minWidth, home) {
163
188
  const INK = "currentColor";
164
189
  const facing = (u) => visOf(g, u) / g.wl;
165
190
  const s = {
@@ -181,7 +206,7 @@ function surfaceOf(g, line, minWidth) {
181
206
  const d = ringPath(g, sampleRing(ring, o?.n ?? 4));
182
207
  if (d === "")
183
208
  return "";
184
- return `<path d="${d}" fill="${line ? INK : fill}"${o?.opacity === undefined ? "" : ` opacity="${o.opacity}"`}/>`;
209
+ return `<path d="${d}" fill="${line ? INK : fill}"${o?.rule === undefined ? "" : ` fill-rule="${o.rule}"`}${o?.opacity === undefined ? "" : ` opacity="${o.opacity}"`}/>`;
185
210
  },
186
211
  panels(rings, fill, o) {
187
212
  if (line && o?.ink !== true)
@@ -189,8 +214,16 @@ function surfaceOf(g, line, minWidth) {
189
214
  const d = rings.map((ring) => ringPath(g, sampleRing(ring, o?.n ?? 4))).join("");
190
215
  if (d === "")
191
216
  return "";
192
- return `<path d="${d}" fill="${line ? INK : fill}"${o?.opacity === undefined ? "" : ` opacity="${o.opacity}"`}/>`;
217
+ return `<path d="${d}" fill="${line ? INK : fill}"${o?.rule === undefined ? "" : ` fill-rule="${o.rule}"`}${o?.opacity === undefined ? "" : ` opacity="${o.opacity}"`}/>`;
193
218
  },
219
+ fill(colour) {
220
+ if (line)
221
+ return "";
222
+ const sil = silhouette(g);
223
+ return sil.d === "" ? `<circle ${sil.tag} fill="${colour}"/>` : `<path d="${sil.d}" fill="${colour}"/>`;
224
+ },
225
+ view: g,
226
+ home,
194
227
  stroke: (pts, colour, width, o) => s.strokes([pts], colour, width, o),
195
228
  strokes(curves, colour, width, o) {
196
229
  if (line && (o?.deco === true || o?.ink === false))
@@ -199,7 +232,7 @@ function surfaceOf(g, line, minWidth) {
199
232
  if (runs.length === 0)
200
233
  return "";
201
234
  const extra = `${o?.opacity === undefined || line ? "" : ` opacity="${o.opacity}"`}${o?.dash === undefined ? "" : ` stroke-dasharray="${o.dash}"`}${o?.dx === undefined && o?.dy === undefined ? "" : ` transform="translate(${o.dx ?? 0} ${o.dy ?? 0})"`}`;
202
- return runPaths(runs, line ? INK : colour, Math.max(width, minWidth), extra);
235
+ return runPaths(runs, line ? INK : colour, Math.max(width, minWidth), extra, o?.digits);
203
236
  },
204
237
  seam(pts, colour, width, o) {
205
238
  if (line)
@@ -297,11 +330,15 @@ export function drawBall(title, key, spec, opts, turn, turning) {
297
330
  const g = { ox, oy, cx: ox + kk * away[0], cy: oy - kk * away[1], axes, rot, k: (46 * (spec.fit ?? 1)) / Math.max(...axes), w, wl: len(w), rz, point: spec.point };
298
331
  const line = opts.style === "line";
299
332
  const minWidth = 80 / opts.size;
300
- const base = opts.tint ?? spec.base;
301
- const surface = surfaceOf(g, line, minWidth);
302
- const layers = spec.draw(surface, base, opts.accent, opts.colors);
333
+ // A ball with a second colour of its own (a soccer ball's panels on a white ground) paints the body as the ground and the second colour as the panels; one with a single colour takes the body as its main colour.
334
+ const paint = opts.paint;
335
+ const two = spec.ground !== undefined;
336
+ const base = opts.tint ?? (two ? paint?.second : paint?.body) ?? spec.base;
337
+ const ground = two ? paint?.body ?? spec.ground : undefined;
338
+ const surface = surfaceOf(g, line, minWidth, homeOf(spec));
339
+ const layers = spec.draw(surface, base, opts.accent, opts.colors, { paint: opts.paint, print: opts.print, printer: opts.printer });
303
340
  const sil = silhouette(g);
304
- const id = `k${hashOf(`${key}|${opts.style}|${opts.tint ?? ""}|${opts.accent ?? ""}${opts.colors === undefined ? "" : `|${opts.colors.join("")}`}|${sil.tag}${sil.d}`)}`;
341
+ const id = `k${hashOf(`${key}|${opts.style}|${opts.tint ?? ""}|${opts.accent ?? ""}${opts.colors === undefined ? "" : `|${opts.colors.join("")}`}|${sil.tag}${sil.d}${paint === undefined && (opts.print === undefined || opts.printer === undefined) ? "" : `|${JSON.stringify([paint, opts.printer === undefined ? undefined : opts.print])}`}`)}`;
305
342
  // A long outline (the hull of a pointed ball) is written once, in the definitions, and used wherever the silhouette is.
306
343
  const long = sil.d.length > 400;
307
344
  const outline = long ? `<path id="${id}s" d="${sil.d}"/>` : "";
@@ -312,7 +349,8 @@ export function drawBall(title, key, spec, opts, turn, turning) {
312
349
  return wrapPiece({ title, size: opts.size, theme: opts.theme, defs: outline, body: `<g fill="none" stroke-linecap="round" stroke-linejoin="round">${body}</g>` });
313
350
  }
314
351
  const m = spec.mat;
315
- const shade = m.shade ?? mix(base, -0.62);
352
+ const painted = paint?.body !== undefined || paint?.second !== undefined;
353
+ const shade = painted && !two ? mix(paint?.second === undefined || paint.body === undefined ? base : blend(paint.body, paint.second, 0.5), -0.6) : m.shade ?? mix(base, -0.62);
316
354
  const grain = opts.grain ?? (opts.size >= 48 && !turning);
317
355
  const gr = grain && m.grain !== undefined ? m.grain : undefined;
318
356
  const rim = m.rim ?? 1;
@@ -325,17 +363,29 @@ export function drawBall(title, key, spec, opts, turn, turning) {
325
363
  (gr === undefined
326
364
  ? ""
327
365
  : grainDef(id, gr.f, gr.a, gr.seed));
328
- const inner = `<g clip-path="url(#${id}c)"${gr === undefined ? "" : ` filter="url(#${id}n)"`}>${shape(`fill="${spec.ground ?? base}"`)}${layers}</g>`;
366
+ const inner = `<g clip-path="url(#${id}c)"${gr === undefined ? "" : ` filter="url(#${id}n)"`}>${shape(`fill="${ground ?? spec.ground ?? base}"`)}${layers}</g>`;
329
367
  const metal = m.metal === true ? shape(`fill="url(#${id}m)"`) : "";
330
368
  const overlays = shape(`fill="url(#${id}d)"`) + metal + shape(`fill="url(#${id}b)"`) + shape(`fill="url(#${id}h)"`);
331
369
  const at = spec.attach?.(surface, base);
332
370
  return wrapPiece({ title, size: opts.size, theme: opts.theme, defs: defs + metalDef(id, m.metal === true), body: `${at?.behind ?? ""}${inner}${overlays}${at?.front ?? ""}` });
333
371
  }
372
+ /** 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
+ function homeOf(spec) {
374
+ const h = spec.home;
375
+ if (h === undefined)
376
+ return undefined;
377
+ const c = norm(h.c);
378
+ const ex = norm(cross(h.up, c));
379
+ return { c, ex, up: cross(c, ex), reach: h.reach };
380
+ }
334
381
  const finite = (v) => (typeof v === "number" && Number.isFinite(v) ? v : 0);
335
382
  /** A ball from its definition: the `Piece` the registry and the gallery take. */
336
383
  export function defineBall(def) {
337
- const view = def.spec.view;
338
- const render = (o, turning) => drawBall(escape(def.en), def.id, def.spec, resolveOptions(o), { yaw: radians(view.yaw + finite(o?.yaw), 0), pitch: radians(view.pitch + finite(o?.pitch), 0), roll: radians(view.roll + finite(o?.roll), 0) }, turning);
384
+ const render = (o, turning) => {
385
+ const opts = resolveOptions(o);
386
+ const view = hasPrint(opts.print) && opts.printer !== undefined && opts.style !== "line" && def.spec.home !== undefined ? def.spec.home.view : def.spec.view;
387
+ return drawBall(escape(def.en), def.id, def.spec, opts, { yaw: radians(view.yaw + finite(o?.yaw), 0), pitch: radians(view.pitch + finite(o?.pitch), 0), roll: radians(view.roll + finite(o?.roll), 0) }, turning);
388
+ };
339
389
  return {
340
390
  id: def.id,
341
391
  en: def.en,
@@ -34,5 +34,9 @@ export interface SeamedOptions {
34
34
  };
35
35
  /** A thin groove along the seam. */
36
36
  groove?: string;
37
+ /** Where the ball takes its logo and lettering (see `BallSpec.home`): a baseball and a softball do. */
38
+ home?: BallSpec["home"];
37
39
  }
40
+ /** The label area of a baseball's or a softball's seam (b = 0.4): the middle of one of its two leather pieces, the way up there, and the view that shows it. The point opposite is in the same leather piece, 0.6 radians clear of the seam, so lettering can go on the far side of the ball. */
41
+ export declare const SEAMED_HOME: (word: string) => NonNullable<BallSpec["home"]>;
38
42
  export declare function seamedBall(o: SeamedOptions): Piece<BallSpec>;
@@ -2,17 +2,44 @@
2
2
  // A baseball and a softball have red stitches that cross it in Vs; a tennis or padel ball has a white rubber strip along it; a sliotar a raised rib.
3
3
  import { defineBall } from "./ball.engine.js";
4
4
  import { mix } from "./ball.core.js";
5
+ import { stitchFor } from "./ball.contrast.js";
6
+ import { region } from "./ball.region.js";
5
7
  import { scuffs } from "./ball.decal.js";
6
- import { seamCurve } from "./ball.math.js";
8
+ import { add, cross, greatCircle, mul, norm, seamCurve } from "./ball.math.js";
7
9
  import { stitches } from "./ball.stitch.js";
10
+ /** The label area of a baseball's or a softball's seam (b = 0.4): the middle of one of its two leather pieces, the way up there, and the view that shows it. The point opposite is in the same leather piece, 0.6 radians clear of the seam, so lettering can go on the far side of the ball. */
11
+ export const SEAMED_HOME = (word) => ({
12
+ c: [0.668, -0.669, -0.326],
13
+ up: [0.601, 0.743, -0.293],
14
+ reach: 0.82,
15
+ view: { yaw: -127.8, pitch: -47.4, roll: -7.9 },
16
+ word,
17
+ });
8
18
  export function seamedBall(o) {
9
19
  const spec = {
10
20
  view: o.view ?? { yaw: 0, pitch: 0, roll: 0 },
11
21
  base: o.base,
12
22
  mat: o.mat,
13
- draw: (s, base) => {
23
+ home: o.home,
24
+ draw: (s, base, _accent, _colors, custom) => {
14
25
  const seam = seamCurve(o.b, 160);
26
+ const paint = custom?.paint;
27
+ const second = paint?.second;
15
28
  let out = "";
29
+ // A two-tone ball: the second colour fills the region on the right of the seam (or a half of the ball), the body colour the rest.
30
+ if (second !== undefined && !s.line) {
31
+ if (paint?.split === "half" && o.home !== undefined) {
32
+ const c = norm(o.home.c), ex = norm(cross(o.home.up, c));
33
+ const turn = ((paint.angle ?? 0) * Math.PI) / 180;
34
+ const n = add(mul(ex, Math.cos(turn)), mul(cross(c, ex), Math.sin(turn)));
35
+ out += region(s, greatCircle(n, 120), second);
36
+ }
37
+ else
38
+ out += region(s, [...seam].reverse(), second);
39
+ }
40
+ const grounds = second === undefined ? [base] : [base, second];
41
+ if (custom?.print !== undefined && o.home !== undefined)
42
+ out += custom.printer?.(s, custom.print, o.home.word, [base]) ?? "";
16
43
  if (o.groove !== undefined)
17
44
  out += s.stroke(seam, o.groove, 0.8, { closed: true, opacity: 0.55 });
18
45
  if (o.strip !== undefined)
@@ -21,11 +48,19 @@ export function seamedBall(o) {
21
48
  out += scuffs(s, mix(base, -0.55), mix(base, 0.6), { dark: o.wear.dark, pale: o.wear.pale, seed: o.wear.seed });
22
49
  if (o.stitch !== undefined) {
23
50
  const marks = stitches(seam, true, o.stitch.count, o.stitch.half, { v: true, lean: o.stitch.lean });
24
- const second = o.stitch.colour2;
25
- if (second === undefined)
26
- out += s.strokes(marks, o.stitch.colour, o.stitch.width, { ink: false });
27
- else
28
- out += s.strokes(marks.filter((_, i) => i % 2 === 0), o.stitch.colour, o.stitch.width, { ink: false }) + s.strokes(marks.filter((_, i) => i % 2 === 1), second, o.stitch.width, { ink: false });
51
+ const colour2 = o.stitch.colour2;
52
+ if (second !== undefined && paint?.split !== "half") {
53
+ // On a ball split along its seam each stitch's two legs lie on their own colour, and take the thread that shows on it.
54
+ const legs = (pick) => marks.map((m) => (pick === 0 ? [m[0], m[1]] : [m[1], m[2]]));
55
+ out += s.strokes(legs(0), stitchFor([base], paint?.stitch, o.stitch.colour), o.stitch.width, { ink: false }) + s.strokes(legs(1), stitchFor([second], paint?.stitch, o.stitch.colour), o.stitch.width, { ink: false });
56
+ }
57
+ else {
58
+ const colour = stitchFor(grounds, paint?.stitch, o.stitch.colour);
59
+ if (colour2 === undefined || colour !== o.stitch.colour)
60
+ out += s.strokes(marks, colour, o.stitch.width, { ink: false });
61
+ else
62
+ out += s.strokes(marks.filter((_, i) => i % 2 === 0), colour, o.stitch.width, { ink: false }) + s.strokes(marks.filter((_, i) => i % 2 === 1), colour2, o.stitch.width, { ink: false });
63
+ }
29
64
  }
30
65
  if (o.strip === undefined && o.stitch === undefined)
31
66
  out += s.stroke(seam, "currentColor", 1.6, { closed: true });
@@ -3,6 +3,14 @@ export interface SafeImage {
3
3
  /** x, y, width and height of the drawing's own coordinates (its viewBox). */
4
4
  box: readonly [number, number, number, number];
5
5
  markup: string;
6
+ /** The same drawing as a list of elements in order, with the attributes that survived (ids already renamed), for a caller that draws it its own way (`ball.warp.ts` wraps it onto a sphere). */
7
+ nodes: readonly SafeNode[];
8
+ }
9
+ /** One element of a safe drawing: opened (`open`), closed (`close`) or both at once (`self`), with its safe attributes. */
10
+ export interface SafeNode {
11
+ kind: "open" | "close" | "self";
12
+ name: string;
13
+ attrs: Readonly<Record<string, string>>;
6
14
  }
7
15
  /** The markup of an SVG made safe, or `undefined` when there is none to be had: not text, too long, no `<svg>` root, nothing drawable left. `prefix` is put in front of every id and every reference to one. */
8
16
  export declare function safeImage(source: unknown, prefix: string): SafeImage | undefined;
@@ -54,6 +54,7 @@ export function safeImage(source, prefix) {
54
54
  const cleaned = source.replace(/<!--[\s\S]*?-->/g, "").replace(/<!\[CDATA\[[\s\S]*?\]\]>/g, "").replace(/<\?[\s\S]*?\?>/g, "").replace(/<![^>]*>/g, "");
55
55
  const tag = /<(\/?)([A-Za-z][\w:-]*)((?:[^>"']|"[^"]*"|'[^']*')*?)(\/?)>/g;
56
56
  let out = "";
57
+ const list = [];
57
58
  let nodes = 0;
58
59
  let root;
59
60
  // The depth inside an element being dropped (0 when none is).
@@ -73,8 +74,10 @@ export function safeImage(source, prefix) {
73
74
  }
74
75
  if (closing) {
75
76
  const top = open.pop();
76
- if (top !== undefined && top !== "#root")
77
+ if (top !== undefined && top !== "#root") {
77
78
  out += `</${top}>`;
79
+ list.push({ kind: "close", name: top, attrs: {} });
80
+ }
78
81
  continue;
79
82
  }
80
83
  const attrs = {};
@@ -98,6 +101,7 @@ export function safeImage(source, prefix) {
98
101
  if (attrs["style"] !== undefined)
99
102
  pairs.push(...styleAttrs(attrs["style"]));
100
103
  let written = "";
104
+ const kept = {};
101
105
  const seen = new Set();
102
106
  for (const [key, raw] of pairs) {
103
107
  if (!ATTRS.has(key) || seen.has(key))
@@ -106,8 +110,10 @@ export function safeImage(source, prefix) {
106
110
  if (v === undefined)
107
111
  continue;
108
112
  seen.add(key);
113
+ kept[key] = v;
109
114
  written += ` ${key}="${escape(v)}"`;
110
115
  }
116
+ list.push({ kind: selfClosed ? "self" : "open", name, attrs: kept });
111
117
  out += `<${name}${written}${selfClosed ? "/>" : ">"}`;
112
118
  if (!selfClosed)
113
119
  open.push(name);
@@ -115,8 +121,11 @@ export function safeImage(source, prefix) {
115
121
  if (root === undefined || !/<(path|circle|ellipse|rect|line|polyline|polygon)\b/.test(out))
116
122
  return undefined;
117
123
  // Close what the text left open, so the markup is always balanced.
118
- for (let top = open.pop(); top !== undefined; top = open.pop())
119
- if (top !== "#root")
120
- out += `</${top}>`;
121
- return { box: boxOf(root), markup: out };
124
+ for (let top = open.pop(); top !== undefined; top = open.pop()) {
125
+ if (top === "#root")
126
+ continue;
127
+ out += `</${top}>`;
128
+ list.push({ kind: "close", name: top, attrs: {} });
129
+ }
130
+ return { box: boxOf(root), markup: out, nodes: list };
122
131
  }
@@ -1,6 +1,5 @@
1
1
  import { escape, num, resolveOptions, PIECE_INK } from "./ball.core.js";
2
2
  import { BALL_AFL } from "./ball.afl.js";
3
- import { BALL_BASEBALL } from "./ball.baseball.js";
4
3
  import { BALL_BASKETBALL } from "./ball.basketball.js";
5
4
  import { BALL_BASKETBALL_3X3 } from "./ball.basketball3x3.js";
6
5
  import { BALL_BASKETBALL_6 } from "./ball.basketball6.js";
@@ -17,16 +16,15 @@ import { BALL_PADEL } from "./ball.padel.js";
17
16
  import { BALL_PICKLEBALL_OUTDOOR, BALL_PICKLEBALL_INDOOR } from "./ball.pickleball.js";
18
17
  import { BALL_PUCK } from "./ball.puck.js";
19
18
  import { PUCKS } from "./ball.puck.print.js";
19
+ import { BALL_BASEBALL, BALL_SOCCER, BALL_SOCCER_COLOUR, BALL_SOCCER_ORANGE, BALL_SOCCER_YELLOW, BALL_SOFTBALL } from "./ball.printable.js";
20
20
  import { BALL_ROUNDNET } from "./ball.roundnet.js";
21
21
  import { BALL_RUGBY } from "./ball.rugby.js";
22
22
  import { BALL_SEPAK_TAKRAW } from "./ball.sepaktakraw.js";
23
23
  import { BALL_SHUTTLECOCK_FEATHER, BALL_SHUTTLECOCK_NYLON } from "./ball.shuttlecock.js";
24
24
  import { BALL_SLIOTAR } from "./ball.sliotar.js";
25
25
  import { BALL_LACROSSE, BALL_RACQUETBALL, BALL_TABLE_TENNIS, BALL_SHOT } from "./ball.smooth.js";
26
- import { BALL_SOCCER, BALL_SOCCER_COLOUR } from "./ball.soccer.js";
27
- 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";
26
+ import { BALL_SOCCER_26, BALL_SOCCER_18, BALL_SOCCER_14, BALL_SOCCER_12, BALL_SOCCER_6, BALL_SOCCER_26_ORANGE, BALL_SOCCER_18_ORANGE, BALL_SOCCER_14_ORANGE, BALL_SOCCER_12_ORANGE, BALL_SOCCER_6_ORANGE, 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";
28
27
  import { BALL_FUTSAL, BALL_BEACH_SOCCER } from "./ball.soccer.indoor.js";
29
- import { BALL_SOFTBALL } from "./ball.softball.js";
30
28
  import { BALL_SOCCER_1870_HAND_STITCHED, BALL_SOCCER_1900_LACED, BALL_SOCCER_1930_18_PANEL, BALL_SOCCER_1950_12_PANEL, BALL_SOCCER_1960_32_PANEL_LEATHER } from "./ball.soccer.vintage.js";
31
29
  import { BALL_SOCCER_1970_BLACK_WHITE, BALL_SOCCER_1978_TRIAD, BALL_SOCCER_1986_DECORATED_TRIAD, BALL_SOCCER_1998_COLOURED_TRIAD, BALL_SOCCER_2002_TRIANGLE_SWEEP, BALL_SOCCER_2006_PROPELLER, BALL_SOCCER_2010_GROOVED, BALL_SOCCER_2014_RIBBON, BALL_SOCCER_2018_PIXEL, BALL_SOCCER_2022_SPEED_SHELL, BALL_SOCCER_TRAINING_HIGH_VIS } from "./ball.soccer.era.js";
32
30
  import { BALL_SOCCER_STARS_BLUE, BALL_SOCCER_STARS_RED, BALL_SOCCER_STARS_NIGHT, BALL_SOCCER_HEXAGONS_CYAN, BALL_SOCCER_HEXAGONS_MAGENTA, BALL_SOCCER_HEXAGONS_LIME, BALL_SOCCER_FLAMES_FIRE, BALL_SOCCER_FLAMES_ICE, BALL_SOCCER_FLAMES_TOXIC, BALL_SOCCER_CHEVRONS_RED, BALL_SOCCER_CHEVRONS_GREEN, BALL_SOCCER_CHEVRONS_BLUE, BALL_SOCCER_SWIRL_BLUE, BALL_SOCCER_SWIRL_RED, BALL_SOCCER_SWIRL_TEAL, BALL_SOCCER_METALLIC_GOLD, BALL_SOCCER_METALLIC_SILVER, BALL_SOCCER_METALLIC_COBALT } from "./ball.soccer.store.js";
@@ -22,6 +22,7 @@ export interface Lettering {
22
22
  export declare function textWidth(text: string, o: Lettering): number;
23
23
  /** The size at which a text is as wide as `width` at most and as tall as `height` at most. */
24
24
  export declare function fitSize(text: string, o: Omit<Lettering, "size">, width: number, height: number): number;
25
+ type Pt = readonly [number, number];
25
26
  /** A line of text on a straight line, centred on (`x`, `y`), as one path: draw it with a stroke of `weight * size`. */
26
27
  export declare function linePath(text: string, o: Lettering, x: number, y: number): string;
27
28
  /**
@@ -29,4 +30,6 @@ export declare function linePath(text: string, o: Lettering, x: number, y: numbe
29
30
  * clockwise (the top of the ring); with `inside` true they stand inward and read left to right (the bottom of the ring, `centre` 180). One path.
30
31
  */
31
32
  export declare function arcPath(text: string, o: Lettering, r: number, centre: number, inside?: boolean): string;
33
+ /** The centre lines of a text on a straight line, centred on (`x`, `y`), as lists of points (curves sampled `steps` to a segment), for a caller that carries them onto a surface; draw them with a stroke of `weight * size`. */
34
+ export declare function lineStrokes(text: string, o: Lettering, x: number, y: number, steps?: number): Pt[][];
32
35
  export {};
@@ -140,3 +140,54 @@ export function arcPath(text, o, r, centre, inside = false) {
140
140
  });
141
141
  }).join("");
142
142
  }
143
+ /** The centre lines of a text on a straight line, centred on (`x`, `y`), as lists of points (curves sampled `steps` to a segment), for a caller that carries them onto a surface; draw them with a stroke of `weight * size`. */
144
+ export function lineStrokes(text, o, x, y, steps = 6) {
145
+ const glyphs = glyphsOf(text);
146
+ const s = setting(glyphs, o);
147
+ const k = o.size / GLYPH_H;
148
+ const left = x - (s.total * k) / 2;
149
+ const top = y - o.size / 2;
150
+ const shape = shaped(o);
151
+ const out = [];
152
+ glyphs.forEach((g, i) => {
153
+ let cur = [];
154
+ let at = [0, 0];
155
+ let start = [0, 0];
156
+ const put = (p) => {
157
+ const q = shape(p);
158
+ cur.push([left + (s.at[i] + q[0]) * k, top + q[1] * k]);
159
+ };
160
+ const flush = () => {
161
+ if (cur.length > 1)
162
+ out.push(cur);
163
+ cur = [];
164
+ };
165
+ for (const part of g.d.match(/[MLCZ][^MLCZ]*/g) ?? []) {
166
+ const n = (part.slice(1).match(/-?\d+(?:\.\d+)?/g) ?? []).map(Number);
167
+ if (part[0] === "M") {
168
+ flush();
169
+ at = start = [n[0], n[1]];
170
+ put(at);
171
+ }
172
+ else if (part[0] === "L") {
173
+ at = [n[0], n[1]];
174
+ put(at);
175
+ }
176
+ else if (part[0] === "C") {
177
+ const [x1, y1, x2, y2, x3, y3] = n;
178
+ const [x0, y0] = at;
179
+ for (let j = 1; j <= steps; j++) {
180
+ const t = j / steps, u = 1 - t;
181
+ put([u * u * u * x0 + 3 * u * u * t * x1 + 3 * u * t * t * x2 + t * t * t * x3, u * u * u * y0 + 3 * u * u * t * y1 + 3 * u * t * t * y2 + t * t * t * y3]);
182
+ }
183
+ at = [x3, y3];
184
+ }
185
+ else {
186
+ at = start;
187
+ put(at);
188
+ }
189
+ }
190
+ flush();
191
+ });
192
+ return out;
193
+ }
@@ -0,0 +1,81 @@
1
+ import { type Piece } from "./ball.core.ts";
2
+ import { type PrintOptions } from "./ball.core.ts";
3
+ import type { BallSpec, Surface } from "./ball.engine.ts";
4
+ /** The colours of a painted ball: `body` is the ball (or a soccer ball's hexagons), `second` the other half of a two-tone ball (or a soccer ball's pentagons). */
5
+ export interface BallPaint {
6
+ /** The ball's colour: `#rgb`, `#rrggbb` or `rgb()`; anything else is ignored. */
7
+ body?: string;
8
+ /** The second colour. A baseball or a softball becomes two-tone, split along its seam or in halves; a soccer ball's pentagons take it. */
9
+ second?: string;
10
+ /** How a two-tone baseball or softball is divided: `"seam"` (the default: the figure-eight seam parts the two colours, as a commemorative ball is) or `"half"` (a flat cut through the middle). */
11
+ split?: "seam" | "half";
12
+ /** For `split: "half"`: the turn of the cut in degrees about the label's middle (0 is a vertical cut, body on the left). */
13
+ angle?: number;
14
+ /** The colour of the stitches. By default one that shows against the ball (red, cream or navy; on a two-tone ball each stitch's legs are the colour that shows on their own side). */
15
+ stitch?: string;
16
+ }
17
+ /** A place on the ball in degrees from the middle of its default view: `lon` toward the right, `lat` toward the top, in the ball's own frame (it turns with the ball). */
18
+ export interface Spot {
19
+ lon?: number;
20
+ lat?: number;
21
+ }
22
+ /** Where the logo and the lettering go, as a place on the ball in degrees from the middle of its default view. By default the logo is there and so is the lettering, or the lettering is on the far side of the ball (`lon: 180`) when there is a logo. */
23
+ export interface BallSpots {
24
+ decal?: Spot;
25
+ label?: Spot;
26
+ }
27
+ /** A logo printed on the ball. */
28
+ export interface BallDecal {
29
+ /** An SVG string. It is made safe first (only shapes, gradients and groups survive; no script, text, image, style or address), a gradient is its mean colour, and it is wrapped exactly onto the sphere. */
30
+ svg?: string;
31
+ /** Or a `data:` URI of an image: an SVG is read as `svg` is, a PNG, JPEG, GIF or WebP is laid on in small triangles (close, not exact). An address on the network is refused. */
32
+ href?: string;
33
+ /** How wide the logo is, in degrees of the sphere (about 55 by default). */
34
+ size?: number;
35
+ /** A turn of the logo on the ball, in degrees. */
36
+ turn?: number;
37
+ }
38
+ /** The stamp: lines of bold block lettering, the first with a small star on each side, and an optional signature-style line under them. */
39
+ export interface BallStamp {
40
+ /** The lines (up to three, 24 letters each, any letter, digit or `- . , ' / : &`; the ball's own default is `UNOFFICIAL` and `DIGITAL BASEBALL`). */
41
+ lines?: readonly string[];
42
+ /** Stars on each side of the first line (on by default). */
43
+ stars?: boolean;
44
+ /** A made-up signature under the lines: `true`, a seed (text or a number), or `{ seed, caption }` with a small line of words under it. The same seed always draws the same squiggle, and no seed is anyone's real hand. */
45
+ sign?: boolean | string | number | {
46
+ seed?: string | number;
47
+ caption?: string;
48
+ };
49
+ /** The ink, `#rgb`, `#rrggbb` or `rgb()`. Navy by default, or white or black where navy would not show on the ball. */
50
+ colour?: string;
51
+ }
52
+ /** The cursive word where a maker's mark would sit. */
53
+ export interface BallScript {
54
+ /** The word (up to 16 letters; a-z and a space; a capital without a drawing of its own is a tall small letter). By default the ball's own: `Baseball`, `Softball` or `Soccer`. */
55
+ text?: string;
56
+ colour?: string;
57
+ }
58
+ /** The small mark under the script: a letter in a circle, or a mark of your own. */
59
+ export interface BallMark {
60
+ /** The letter in the circle (one letter or digit). By default the first letter of the script. */
61
+ letter?: string;
62
+ /** A mark of your own as an SVG string (made safe first, as a decal is), fitted in the circle's place. It replaces the circled letter. */
63
+ svg?: string;
64
+ /** Or a `data:` URI of an SVG (a raster picture is not taken for a mark). */
65
+ href?: string;
66
+ colour?: string;
67
+ }
68
+ /** The ink for a print on the given grounds: the wanted colour if it shows on all of them (3 to 1), else the first of navy, white and near-black that does. */
69
+ export declare function inkFor(grounds: readonly string[], wanted?: string): string;
70
+ /**
71
+ * The print of a ball as layers for its surface: the logo, then the label (the script, the circled letter, the stamp), each at its own place. `grounds` are the colours the print lies on, for the ink (the label's far side is the same colour as the front on every ball that has one). A ball
72
+ * in the line style has none. Returns an empty string when nothing was asked for.
73
+ */
74
+ export declare function printLayers(s: Surface, p: PrintOptions | undefined, word: string, grounds: readonly string[]): string;
75
+ /** What `printLayers` was given, for the id of a drawing (so two prints of one ball have their own definitions). */
76
+ export declare const printKey: (p: PrintOptions | undefined) => string;
77
+ /**
78
+ * A ball that can be printed. The balls themselves carry no printing code (a ball bundled alone stays small): `renderEmblem` and the registry use this, and so can you for one ball taken from its own module:
79
+ * `const ball = printed(BALL_BASEBALL); ball.svg({ stamp: true })`. The same ball with the printer in its options; options that ask for no print draw exactly what the plain ball draws.
80
+ */
81
+ export declare const printed: <P extends Piece<BallSpec>>(piece: P) => P;