@johnmorrisdotca/dougu-sports 0.8.0 → 0.10.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 (59) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/NOTICE.md +14 -0
  3. package/README.md +58 -26
  4. package/dist/brands/brand.ball.d.ts +29 -0
  5. package/dist/brands/brand.ball.js +23 -0
  6. package/dist/brands/brand.data.d.ts +47 -0
  7. package/dist/brands/brand.data.js +308 -0
  8. package/dist/brands/brand.letters.d.ts +31 -0
  9. package/dist/brands/brand.letters.js +88 -0
  10. package/dist/brands/brand.render.d.ts +47 -0
  11. package/dist/brands/brand.render.js +106 -0
  12. package/dist/brands-entry.d.ts +8 -0
  13. package/dist/brands-entry.js +5 -0
  14. package/dist/emblems/award/ribbon.d.ts +2 -0
  15. package/dist/emblems/award/ribbon.js +17 -0
  16. package/dist/emblems/ball/ball.baseball.js +2 -1
  17. package/dist/emblems/ball/ball.contrast.d.ts +4 -0
  18. package/dist/emblems/ball/ball.contrast.js +22 -0
  19. package/dist/emblems/ball/ball.core.d.ts +42 -0
  20. package/dist/emblems/ball/ball.core.js +34 -1
  21. package/dist/emblems/ball/ball.engine.d.ts +70 -4
  22. package/dist/emblems/ball/ball.engine.js +70 -20
  23. package/dist/emblems/ball/ball.family.seamed.d.ts +4 -0
  24. package/dist/emblems/ball/ball.family.seamed.js +42 -7
  25. package/dist/emblems/ball/ball.image.d.ts +8 -0
  26. package/dist/emblems/ball/ball.image.js +14 -5
  27. package/dist/emblems/ball/ball.index.js +2 -4
  28. package/dist/emblems/ball/ball.letters.d.ts +3 -0
  29. package/dist/emblems/ball/ball.letters.js +51 -0
  30. package/dist/emblems/ball/ball.print.d.ts +81 -0
  31. package/dist/emblems/ball/ball.print.js +268 -0
  32. package/dist/emblems/ball/ball.printable.d.ts +6 -0
  33. package/dist/emblems/ball/ball.printable.js +13 -0
  34. package/dist/emblems/ball/ball.region.d.ts +12 -0
  35. package/dist/emblems/ball/ball.region.js +94 -0
  36. package/dist/emblems/ball/ball.script.d.ts +15 -0
  37. package/dist/emblems/ball/ball.script.js +107 -0
  38. package/dist/emblems/ball/ball.soccer.js +7 -3
  39. package/dist/emblems/ball/ball.soccer.panels.d.ts +2 -0
  40. package/dist/emblems/ball/ball.soccer.panels.js +6 -3
  41. package/dist/emblems/ball/ball.soccer.size.d.ts +2 -0
  42. package/dist/emblems/ball/ball.soccer.size.js +8 -0
  43. package/dist/emblems/ball/ball.softball.js +2 -1
  44. package/dist/emblems/ball/ball.warp.d.ts +48 -0
  45. package/dist/emblems/ball/ball.warp.js +441 -0
  46. package/dist/emblems/emblem.core.d.ts +13 -0
  47. package/dist/emblems/emblem.equipment.js +3 -3
  48. package/dist/emblems/emblem.ids.d.ts +4 -4
  49. package/dist/emblems/emblem.ids.js +2 -2
  50. package/dist/emblems/emblem.registry.js +2 -0
  51. package/dist/emblems/gear/gear.index.js +2 -1
  52. package/dist/emblems/gear/gear.nets.d.ts +3 -0
  53. package/dist/emblems/gear/gear.nets.js +103 -0
  54. package/dist/emblems-entry.d.ts +1 -0
  55. package/dist/index.d.ts +6 -4
  56. package/dist/index.js +5 -4
  57. package/dist/version.d.ts +1 -1
  58. package/dist/version.js +1 -1
  59. package/package.json +8 -1
@@ -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;
@@ -0,0 +1,268 @@
1
+ // What a ball is printed with: a logo wrapped onto the side of the ball, a stamp of text, a cursive maker's word and a small mark, laid out as a label on a flat plane that touches the ball at one point and
2
+ // carried onto the sphere with an equal-distance map, so every line of it curves with the ball and turns with it. Everything is our own drawing: our own cursive script (ball.script.ts), our own block
3
+ // lettering (ball.letters.ts), our own stars and circled letter, and no maker's, league's or team's name or mark; a logo and a mark of the caller's own are drawn from the SVG they give, made safe first.
4
+ // Imports only this folder's helpers.
5
+ import { escape, hashOf, hexOf, readColour } from "./ball.core.js";
6
+ import { contrast } from "./ball.contrast.js";
7
+ import { hasPrint } from "./ball.core.js";
8
+ import { safeImage } from "./ball.image.js";
9
+ import { lineStrokes, textWidth } from "./ball.letters.js";
10
+ import { add, mul, norm } from "./ball.math.js";
11
+ import { scriptRuns, scriptWidth } from "./ball.script.js";
12
+ import { signaturePath } from "./ball.signature.js";
13
+ import { drawPicture, drawShapes, flattenPath, shapesOf } from "./ball.warp.js";
14
+ const NAVY = "#1b2a5c";
15
+ const MAX_SVG = 60000;
16
+ /** 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. */
17
+ export function inkFor(grounds, wanted) {
18
+ const w = wanted === undefined ? undefined : readColour(wanted);
19
+ const list = [...(w === undefined ? [] : [hexOf(w)]), NAVY, "#ffffff", "#101418"];
20
+ return list.find((c) => grounds.every((g) => contrast(c, g) >= 3)) ?? list[list.length - 1];
21
+ }
22
+ const clean = (v, max) => {
23
+ if (typeof v !== "string" && typeof v !== "number")
24
+ return undefined;
25
+ const t = [...String(v).replace(/\p{Cc}/gu, " ").replace(/\s+/g, " ").trim()].slice(0, max).join("");
26
+ return t === "" ? undefined : t;
27
+ };
28
+ const degrees = (v, d, lo, hi) => (typeof v === "number" && Number.isFinite(v) ? Math.max(lo, Math.min(hi, v)) : d);
29
+ /** An SVG from a data URI, if the URI is one of an SVG: base64 or percent-encoded. */
30
+ function svgOfUri(uri) {
31
+ const m = /^data:image\/svg\+xml(?:;charset=[\w-]+)?(;base64)?,([\s\S]*)$/i.exec(uri.trim());
32
+ if (m === null || m[2].length > MAX_SVG * 1.4)
33
+ return undefined;
34
+ try {
35
+ const decode = globalThis.atob;
36
+ return m[1] === undefined ? decodeURIComponent(m[2]) : decode === undefined ? undefined : decode(m[2]);
37
+ }
38
+ catch {
39
+ return undefined;
40
+ }
41
+ }
42
+ /** A raster data URI that is safe to write into an attribute: PNG, JPEG, GIF or WebP, base64 only, of a size that fits in a document. */
43
+ const RASTER = /^data:image\/(?:png|jpe?g|gif|webp);base64,[A-Za-z0-9+/]+={0,2}$/;
44
+ const MAX_RASTER = 400000;
45
+ /** A logo or mark from a string and a URI, made safe; `undefined` when nothing drawable is left. A picture is taken only when `pictures` is true. */
46
+ function artOf(svg, href, prefix, pictures) {
47
+ let source = typeof svg === "string" ? svg : undefined;
48
+ let uri = typeof href === "string" ? href.trim() : undefined;
49
+ if (source !== undefined && /^\s*data:/i.test(source)) {
50
+ uri = source.trim();
51
+ source = undefined;
52
+ }
53
+ if (source === undefined && uri !== undefined) {
54
+ source = svgOfUri(uri);
55
+ if (source === undefined && pictures && uri.length <= MAX_RASTER && RASTER.test(uri))
56
+ return { kind: "picture", href: uri };
57
+ }
58
+ const img = safeImage(source, prefix);
59
+ return img === undefined ? undefined : { kind: "vector", img };
60
+ }
61
+ /** The frame a plane is laid on the ball in: at `spot` degrees from the label's middle, turned `turn` degrees. The map is equal-distance, so a length from the middle is the same on the ball in radians. */
62
+ function planeOf(home, spot, turn = 0) {
63
+ const lon = (degrees(spot?.lon, 0, -360, 360) * Math.PI) / 180, lat = (degrees(spot?.lat, 0, -90, 90) * Math.PI) / 180;
64
+ const c1 = add(mul(home.c, Math.cos(lon)), mul(home.ex, Math.sin(lon)));
65
+ const ex1 = add(mul(home.ex, Math.cos(lon)), mul(home.c, -Math.sin(lon)));
66
+ const c = add(mul(c1, Math.cos(lat)), mul(home.up, Math.sin(lat)));
67
+ const up = add(mul(home.up, Math.cos(lat)), mul(c1, -Math.sin(lat)));
68
+ const t = (turn * Math.PI) / 180, ct = Math.cos(t), st = Math.sin(t);
69
+ return (x, y) => {
70
+ const px = x * ct - y * st, py = x * st + y * ct;
71
+ const r = Math.min(Math.hypot(px, py), Math.PI);
72
+ if (r < 1e-9)
73
+ return c;
74
+ return norm(add(mul(c, Math.cos(r)), mul(add(mul(ex1, px), mul(up, -py)), Math.sin(r) / Math.hypot(px, py))));
75
+ };
76
+ }
77
+ const ringOf = (cx, cy, r, n = 28) => Array.from({ length: n }, (_, i) => [cx + r * Math.cos((i * 2 * Math.PI) / n), cy + r * Math.sin((i * 2 * Math.PI) / n)]);
78
+ const starOf = (cx, cy, r) => Array.from({ length: 10 }, (_, i) => [cx + (i % 2 === 0 ? r : r * 0.42) * Math.cos(-Math.PI / 2 + (i * Math.PI) / 5), cy + (i % 2 === 0 ? r : r * 0.42) * Math.sin(-Math.PI / 2 + (i * Math.PI) / 5)]);
79
+ /** The print asked for, made safe. `word` is the ball's own word, the default of the script and of the stamp's second line. */
80
+ function partsOf(p, word) {
81
+ const out = {};
82
+ const d = typeof p.decal === "string" ? { svg: p.decal } : typeof p.decal === "object" && p.decal !== null ? p.decal : undefined;
83
+ if (d !== undefined) {
84
+ const art = artOf(d.svg, d.href, "d", true);
85
+ if (art !== undefined)
86
+ out.decal = { art, size: (degrees(d.size, 0, 5, 170) * Math.PI) / 180, turn: degrees(d.turn, 0, -360, 360) };
87
+ }
88
+ const sc = p.script === true ? {} : typeof p.script === "string" ? { text: p.script } : typeof p.script === "object" && p.script !== null ? p.script : undefined;
89
+ if (sc !== undefined) {
90
+ const t = clean(sc.text, 16) ?? `${word[0]?.toUpperCase()}${word.slice(1)}`;
91
+ if (scriptWidth(t) > 0)
92
+ out.script = { text: t, colour: typeof sc.colour === "string" ? sc.colour : undefined };
93
+ }
94
+ const mk = p.mark === true ? {} : typeof p.mark === "string" ? (/^\s*(<|data:)/i.test(p.mark) ? { svg: p.mark } : { letter: p.mark }) : typeof p.mark === "object" && p.mark !== null ? p.mark : undefined;
95
+ // The script brings its circled letter with it, unless the mark is switched off.
96
+ const mark = mk ?? (out.script !== undefined && p.mark !== false ? {} : undefined);
97
+ if (mark !== undefined) {
98
+ const art = artOf(mark.svg, mark.href, "m", false);
99
+ const letter = [...(clean(mark.letter, 1) ?? out.script?.text.trim()[0] ?? word[0] ?? "B")][0];
100
+ out.mark = { letter: letter.toUpperCase(), colour: typeof mark.colour === "string" ? mark.colour : undefined, art };
101
+ }
102
+ const st = p.stamp === true ? {} : typeof p.stamp === "string" ? { lines: [p.stamp] } : Array.isArray(p.stamp) ? { lines: p.stamp } : typeof p.stamp === "object" && p.stamp !== null ? p.stamp : undefined;
103
+ if (st !== undefined) {
104
+ const given = Array.isArray(st.lines) ? st.lines.slice(0, 3).flatMap((l) => clean(l, 24) ?? []) : [];
105
+ const lines = given.length > 0 ? given : ["UNOFFICIAL", `DIGITAL ${word.toUpperCase()}`];
106
+ const sg = st.sign;
107
+ const sign = sg === true ? { seed: "dougu" } : typeof sg === "string" || typeof sg === "number" ? { seed: sg } : typeof sg === "object" && sg !== null ? { seed: sg.seed ?? "dougu", caption: clean(sg.caption, 24) } : undefined;
108
+ out.stamp = { lines, stars: st.stars !== false, colour: typeof st.colour === "string" ? st.colour : undefined, sign };
109
+ }
110
+ return out;
111
+ }
112
+ /** The ink of one part: its own colour if it is one, else the label's. */
113
+ const pen = (own, label) => {
114
+ const c = own === undefined ? undefined : readColour(own);
115
+ return c === undefined ? label : hexOf(c);
116
+ };
117
+ /** Points of a flat curve onto the ball, each given as `[x, y]` in the plane's units. */
118
+ const lay = (map, pts) => pts.map((p) => map(p[0], p[1]));
119
+ /** The size of the label's full width, in radians, for a ball that reaches `reach` from the middle of its label area. */
120
+ const labelWidth = (reach) => Math.min(1.7, reach * 1.4);
121
+ /** The id of a drawing's definitions: from its own text, so that two logos in a page do not share. */
122
+ const idOf = (text) => `p${hashOf(text)}`;
123
+ /**
124
+ * 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
125
+ * in the line style has none. Returns an empty string when nothing was asked for.
126
+ */
127
+ export function printLayers(s, p, word, grounds) {
128
+ const home = s.home;
129
+ if (p === undefined || home === undefined || s.line)
130
+ return "";
131
+ const parts = partsOf(p, word);
132
+ let out = "";
133
+ const labelled = parts.script !== undefined || parts.mark !== undefined || parts.stamp !== undefined;
134
+ if (parts.decal !== undefined) {
135
+ const d = parts.decal;
136
+ const ink = inkFor(grounds);
137
+ const map = planeOf(home, p.spots?.decal, d.turn);
138
+ const size = d.size > 0 ? d.size : Math.min(1.3, home.reach * 1.25);
139
+ if (d.art.kind === "picture")
140
+ out += drawPicture(s, d.art.href, map, size, idOf(d.art.href.slice(0, 64) + d.art.href.length));
141
+ else
142
+ out += drawShapes(s, shapesOf(d.art.img, ink), d.art.img.box, map, size);
143
+ }
144
+ if (labelled) {
145
+ const spot = p.spots?.label ?? (parts.decal !== undefined ? { lon: 180 } : undefined);
146
+ out += labelLayers(s, parts, planeOf(home, spot), labelWidth(home.reach), inkFor(grounds, parts.stamp?.colour));
147
+ }
148
+ return out;
149
+ }
150
+ /** The label: its items from the top down, centred on the plane's origin. */
151
+ function labelLayers(s, parts, map, W, ink) {
152
+ const items = [];
153
+ const gap = W * 0.085;
154
+ const digits = 2;
155
+ const stroke = (curves, colour, width, extra) => s.strokes(curves.map((c) => lay(map, c)), colour, width * s.radius, { digits, ...extra });
156
+ if (parts.script !== undefined) {
157
+ const sc = parts.script;
158
+ const runs = scriptRuns(sc.text);
159
+ const wide = scriptWidth(sc.text);
160
+ const k = Math.min((W * 0.82) / wide, W * 0.03);
161
+ const high = 22 * k;
162
+ const colour = pen(sc.colour, ink);
163
+ items.push({
164
+ h: high,
165
+ draw: (cy) => {
166
+ const ox = (-wide * k) / 2 + wide * k * 0.02, oy = cy + high / 2 - k * 0.5;
167
+ const tiers = [[], [], []];
168
+ for (const run of runs) {
169
+ let cur = [];
170
+ let tier = -1;
171
+ run.pts.forEach((pt, i) => {
172
+ const q = [ox + pt[0] * k, oy - pt[1] * k];
173
+ if (i === 0) {
174
+ cur = [q];
175
+ return;
176
+ }
177
+ const t = run.tier[i - 1];
178
+ if (t !== tier && cur.length > 1) {
179
+ tiers[tier]?.push(cur);
180
+ cur = [cur[cur.length - 1]];
181
+ }
182
+ tier = t;
183
+ cur.push(q);
184
+ });
185
+ if (cur.length > 1)
186
+ tiers[tier]?.push(cur);
187
+ }
188
+ const w = [0.34, 0.7, 1.2];
189
+ return stroke(tiers[0], colour, w[0] * k) + stroke(tiers[1], colour, w[1] * k) + stroke(tiers[2], colour, w[2] * k);
190
+ },
191
+ });
192
+ }
193
+ if (parts.mark !== undefined) {
194
+ const mk = parts.mark;
195
+ const r = W * 0.062;
196
+ const colour = pen(mk.colour, ink);
197
+ items.push({
198
+ h: r * 2,
199
+ draw: (cy) => {
200
+ if (mk.art !== undefined && mk.art.kind === "vector")
201
+ return drawShapes(s, shapesOf(mk.art.img, colour), mk.art.img.box, (x, y) => map(x, y + cy), r * 2.6);
202
+ const L = { size: r * 1.15, weight: 0.2, wide: 0.9 };
203
+ return stroke([ringOf(0, cy, r)], colour, r * 0.2, { closed: true }) + stroke(lineStrokes(mk.letter, L, 0, cy, 5), colour, L.size * L.weight);
204
+ },
205
+ });
206
+ }
207
+ if (parts.stamp !== undefined) {
208
+ const st = parts.stamp;
209
+ const colour = pen(st.colour, ink);
210
+ st.lines.forEach((text, i) => {
211
+ const want = i === 0 ? { weight: 0.22, wide: 0.78 } : { weight: 0.22, wide: 0.78 };
212
+ const room = W * (i === 0 ? 0.62 : 0.98);
213
+ const base = i === 0 ? W * 0.075 : W * 0.105;
214
+ const w1 = textWidth(text, { ...want, size: 1 });
215
+ const size = Math.min(base, room / w1);
216
+ const L = { ...want, size };
217
+ items.push({
218
+ h: size * 1.1,
219
+ draw: (cy) => {
220
+ let o = stroke(lineStrokes(text, L, 0, cy, 5), colour, size * L.weight);
221
+ if (i === 0 && st.stars) {
222
+ const half = textWidth(text, L) / 2;
223
+ const r = size * 0.62;
224
+ for (const sx of [-1, 1])
225
+ o += s.panel(lay(map, starOf(sx * (half + r * 1.5), cy, r)), colour, { n: 1, ink: true });
226
+ }
227
+ return o;
228
+ },
229
+ });
230
+ });
231
+ if (st.sign !== undefined) {
232
+ const sg = st.sign;
233
+ const seed = sg.seed;
234
+ const width = W * 0.52;
235
+ items.push({
236
+ h: width * 0.34,
237
+ draw: (cy) => {
238
+ const subs = flattenPath(signaturePath(seed), 8);
239
+ return stroke(subs.map((q) => q.pts.map((pt) => [pt[0] * width, pt[1] * width + cy])), colour, width * 0.014);
240
+ },
241
+ });
242
+ if (sg.caption !== undefined) {
243
+ const size = W * 0.036;
244
+ const L = { size, weight: 0.16, wide: 0.8, track: 0.12 };
245
+ items.push({ h: size * 1.4, draw: (cy) => stroke(lineStrokes(sg.caption, L, 0, cy, 4), colour, size * L.weight) });
246
+ }
247
+ }
248
+ }
249
+ const total = items.reduce((a, it) => a + it.h, 0) + gap * Math.max(0, items.length - 1);
250
+ let y = -total / 2;
251
+ let out = "";
252
+ for (const it of items) {
253
+ out += it.draw(y + it.h / 2);
254
+ y += it.h + gap;
255
+ }
256
+ return out;
257
+ }
258
+ /** What `printLayers` was given, for the id of a drawing (so two prints of one ball have their own definitions). */
259
+ export const printKey = (p) => (p === undefined ? "" : escape(hashOf(JSON.stringify(p))));
260
+ /**
261
+ * 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:
262
+ * `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.
263
+ */
264
+ export const printed = (piece) => ({
265
+ ...piece,
266
+ svg: (o) => piece.svg(hasPrint(o) ? { ...o, printer: printLayers } : o),
267
+ frame: (o) => piece.frame(hasPrint(o) ? { ...o, printer: printLayers } : o),
268
+ });
@@ -0,0 +1,6 @@
1
+ export declare const BALL_BASEBALL: import("./ball.core.ts").Piece<import("./ball.engine.ts").BallSpec>;
2
+ export declare const BALL_SOFTBALL: import("./ball.core.ts").Piece<import("./ball.engine.ts").BallSpec>;
3
+ export declare const BALL_SOCCER: import("./ball.core.ts").Piece<import("./ball.engine.ts").BallSpec>;
4
+ export declare const BALL_SOCCER_COLOUR: import("./ball.core.ts").Piece<import("./ball.engine.ts").BallSpec>;
5
+ export declare const BALL_SOCCER_ORANGE: import("./ball.core.ts").Piece<import("./ball.engine.ts").BallSpec>;
6
+ export declare const BALL_SOCCER_YELLOW: import("./ball.core.ts").Piece<import("./ball.engine.ts").BallSpec>;
@@ -0,0 +1,13 @@
1
+ // The balls that take a logo, lettering and a mark, with the printer in hand: the same balls as `ball.baseball.ts`, `ball.softball.ts` and `ball.soccer.ts` and the classic soccer balls of `ball.soccer.panels.ts`, made
2
+ // with `printed()`. The balls' own modules carry no printing code (so one bundled alone stays small); the registry and the main entry take these.
3
+ import { BALL_BASEBALL as PLAIN_BASEBALL } from "./ball.baseball.js";
4
+ import { printed } from "./ball.print.js";
5
+ import { BALL_SOCCER_ORANGE as PLAIN_SOCCER_ORANGE, BALL_SOCCER_YELLOW as PLAIN_SOCCER_YELLOW } from "./ball.soccer.panels.js";
6
+ import { BALL_SOCCER as PLAIN_SOCCER, BALL_SOCCER_COLOUR as PLAIN_SOCCER_COLOUR } from "./ball.soccer.js";
7
+ import { BALL_SOFTBALL as PLAIN_SOFTBALL } from "./ball.softball.js";
8
+ export const BALL_BASEBALL = /* @__PURE__ */ printed(PLAIN_BASEBALL);
9
+ export const BALL_SOFTBALL = /* @__PURE__ */ printed(PLAIN_SOFTBALL);
10
+ export const BALL_SOCCER = /* @__PURE__ */ printed(PLAIN_SOCCER);
11
+ export const BALL_SOCCER_COLOUR = /* @__PURE__ */ printed(PLAIN_SOCCER_COLOUR);
12
+ export const BALL_SOCCER_ORANGE = /* @__PURE__ */ printed(PLAIN_SOCCER_ORANGE);
13
+ export const BALL_SOCCER_YELLOW = /* @__PURE__ */ printed(PLAIN_SOCCER_YELLOW);
@@ -0,0 +1,12 @@
1
+ import { type Geometry, type Surface } from "./ball.engine.ts";
2
+ import { type V3 } from "./ball.math.ts";
3
+ /**
4
+ * The visible part of the surface on the left of a closed curve on a sphere, as the path of its outline: the curve's visible runs, joined along the limb. Walking a run with the region on your left, the limb is
5
+ * followed counterclockwise (as the eye sees it) from where the run leaves the disc to the next place a run comes in, so the outline closes on the correct side even for a region that is not convex.
6
+ */
7
+ export declare function regionPath(g: Geometry, ring: readonly V3[]): string;
8
+ /**
9
+ * The part of the surface on the left of a closed curve (looking at the sphere from outside, walking along the curve in the order given), filled with a colour: what you see of it, bounded by the curve and the
10
+ * limb. For spheres only. In the line style nothing is drawn.
11
+ */
12
+ export declare function region(s: Surface, ring: readonly V3[], fill: string): string;
@@ -0,0 +1,94 @@
1
+ // A region of a sphere on the left of a closed curve, drawn as the outline it has on the screen. Kept out of the engine so that a ball with no two-tone colouring does not carry it: the seamed balls import it.
2
+ import { num } from "./ball.core.js";
3
+ import { EPS, pathOf, project, visOf, visibleRuns } from "./ball.engine.js";
4
+ import { cross, dot, norm, sub } from "./ball.math.js";
5
+ /** The signed area of a polygon of screen points, y up (positive when it runs counterclockwise as you see it). */
6
+ function areaOf(pts) {
7
+ let a = 0;
8
+ for (let i = 0; i < pts.length; i++) {
9
+ const p = pts[i], q = pts[(i + 1) % pts.length];
10
+ a += p[0] * -q[1] - q[0] * -p[1];
11
+ }
12
+ return a / 2;
13
+ }
14
+ /** Whether a point of the sphere is on the left of a closed curve, by the nearest sample's side. */
15
+ function leftOf(ring, p) {
16
+ let best = Infinity, left = false;
17
+ const n = ring.length;
18
+ for (let i = 0; i < n; i++) {
19
+ const q = ring[i];
20
+ const d = 1 - dot(p, q);
21
+ if (d < best) {
22
+ best = d;
23
+ left = dot(p, cross(q, norm(sub(ring[(i + 1) % n], ring[(i + n - 1) % n])))) > 0;
24
+ }
25
+ }
26
+ return left;
27
+ }
28
+ /**
29
+ * The visible part of the surface on the left of a closed curve on a sphere, as the path of its outline: the curve's visible runs, joined along the limb. Walking a run with the region on your left, the limb is
30
+ * followed counterclockwise (as the eye sees it) from where the run leaves the disc to the next place a run comes in, so the outline closes on the correct side even for a region that is not convex.
31
+ */
32
+ export function regionPath(g, ring) {
33
+ const R = g.k;
34
+ const vis = ring.map((p) => visOf(g, p));
35
+ const disc = () => `M${num(g.cx - R, 2)} ${num(g.cy, 2)}A${num(R, 2)} ${num(R, 2)} 0 1 0 ${num(g.cx + R, 2)} ${num(g.cy, 2)}A${num(R, 2)} ${num(R, 2)} 0 1 0 ${num(g.cx - R, 2)} ${num(g.cy, 2)}Z`;
36
+ if (!vis.some((v) => v > EPS))
37
+ return leftOf(ring, norm(g.w)) ? disc() : "";
38
+ if (vis.every((v) => v > EPS)) {
39
+ const pts = ring.map((p) => {
40
+ const [x, y] = project(g, p);
41
+ return [x, y, 1];
42
+ });
43
+ const poly = `${pathOf(pts, 2)}Z`;
44
+ return areaOf(pts) > 0 ? poly : disc() + poly;
45
+ }
46
+ const runs = visibleRuns(g, ring, true).filter((r) => r.pts.length > 1);
47
+ if (runs.length === 0)
48
+ return "";
49
+ const angle = (p) => Math.atan2(-(p[1] - g.cy), p[0] - g.cx);
50
+ const first = (r) => r.pts[0];
51
+ const last = (r) => r.pts[r.pts.length - 1];
52
+ const next = runs.map((r) => {
53
+ const out = angle(last(r));
54
+ let best = 0, gap = Infinity;
55
+ runs.forEach((q, j) => {
56
+ let d = angle(first(q)) - out;
57
+ d -= Math.floor(d / (2 * Math.PI)) * 2 * Math.PI;
58
+ if (d < gap) {
59
+ gap = d;
60
+ best = j;
61
+ }
62
+ });
63
+ return { to: best, gap };
64
+ });
65
+ const seen = new Set();
66
+ let d = "";
67
+ runs.forEach((_, start) => {
68
+ if (seen.has(start))
69
+ return;
70
+ let at = start;
71
+ d += `M${num(first(runs[at])[0], 2)} ${num(first(runs[at])[1], 2)}`;
72
+ while (!seen.has(at)) {
73
+ seen.add(at);
74
+ const r = runs[at];
75
+ d += pathOf(r.pts.slice(1), 2).replace(/^M/, "L");
76
+ const n = next[at];
77
+ const e = first(runs[n.to]);
78
+ d += n.gap < 1e-4 ? `L${num(e[0], 2)} ${num(e[1], 2)}` : `A${num(R, 2)} ${num(R, 2)} 0 ${n.gap > Math.PI ? 1 : 0} 0 ${num(e[0], 2)} ${num(e[1], 2)}`;
79
+ at = n.to;
80
+ }
81
+ d += "Z";
82
+ });
83
+ return d;
84
+ }
85
+ /**
86
+ * The part of the surface on the left of a closed curve (looking at the sphere from outside, walking along the curve in the order given), filled with a colour: what you see of it, bounded by the curve and the
87
+ * limb. For spheres only. In the line style nothing is drawn.
88
+ */
89
+ export function region(s, ring, fill) {
90
+ if (s.line)
91
+ return "";
92
+ const d = regionPath(s.view, ring);
93
+ return d === "" ? "" : `<path d="${d}" fill="${fill}" fill-rule="evenodd"/>`;
94
+ }
@@ -0,0 +1,15 @@
1
+ type Pt = readonly [number, number];
2
+ /** One piece of a word's centre line: its points in the grid, and `tier` 0 (hairline), 1 (medium) or 2 (down stroke), for each segment. */
3
+ export interface ScriptRun {
4
+ pts: Pt[];
5
+ /** One entry per segment (`pts.length - 1`). */
6
+ tier: number[];
7
+ }
8
+ /** The width of a word in the grid. */
9
+ export declare function scriptWidth(text: string): number;
10
+ /**
11
+ * A word as pen runs in the grid (y up, the baseline at 0, the left end of the first letter at 0), leaning right, each run with the thickness tier of its segments. Characters with no letter are left out;
12
+ * a space is a gap.
13
+ */
14
+ export declare function scriptRuns(text: string): ScriptRun[];
15
+ export {};