@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.
- package/CHANGELOG.md +44 -0
- package/NOTICE.md +1 -1
- package/README.md +58 -28
- package/dist/emblems/ball/ball.baseball.js +2 -1
- package/dist/emblems/ball/ball.contrast.d.ts +4 -0
- package/dist/emblems/ball/ball.contrast.js +22 -0
- package/dist/emblems/ball/ball.core.d.ts +42 -0
- package/dist/emblems/ball/ball.core.js +34 -1
- package/dist/emblems/ball/ball.engine.d.ts +70 -4
- package/dist/emblems/ball/ball.engine.js +70 -20
- package/dist/emblems/ball/ball.family.seamed.d.ts +4 -0
- package/dist/emblems/ball/ball.family.seamed.js +42 -7
- package/dist/emblems/ball/ball.image.d.ts +8 -0
- package/dist/emblems/ball/ball.image.js +14 -5
- package/dist/emblems/ball/ball.index.js +2 -4
- package/dist/emblems/ball/ball.letters.d.ts +3 -0
- package/dist/emblems/ball/ball.letters.js +51 -0
- package/dist/emblems/ball/ball.print.d.ts +81 -0
- package/dist/emblems/ball/ball.print.js +268 -0
- package/dist/emblems/ball/ball.printable.d.ts +6 -0
- package/dist/emblems/ball/ball.printable.js +13 -0
- package/dist/emblems/ball/ball.region.d.ts +12 -0
- package/dist/emblems/ball/ball.region.js +94 -0
- package/dist/emblems/ball/ball.script.d.ts +15 -0
- package/dist/emblems/ball/ball.script.js +107 -0
- package/dist/emblems/ball/ball.soccer.js +7 -3
- package/dist/emblems/ball/ball.soccer.panels.d.ts +2 -0
- package/dist/emblems/ball/ball.soccer.panels.js +6 -3
- package/dist/emblems/ball/ball.soccer.size.d.ts +2 -0
- package/dist/emblems/ball/ball.soccer.size.js +8 -0
- package/dist/emblems/ball/ball.softball.js +2 -1
- package/dist/emblems/ball/ball.warp.d.ts +48 -0
- package/dist/emblems/ball/ball.warp.js +441 -0
- package/dist/emblems/emblem.core.d.ts +13 -0
- package/dist/emblems-entry.d.ts +1 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +4 -5
- package/dist/turn-control.d.ts +24 -5
- package/dist/turn-control.js +67 -23
- package/dist/turn-full.d.ts +15 -0
- package/dist/turn-full.js +19 -0
- package/dist/turn-registry.d.ts +1 -0
- package/dist/turn-registry.js +3 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- 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
|
-
|
|
301
|
-
const
|
|
302
|
-
const
|
|
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
|
|
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
|
|
338
|
-
|
|
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
|
-
|
|
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
|
|
25
|
-
if (second
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
out += s.strokes(
|
|
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
|
|
120
|
-
|
|
121
|
-
|
|
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 {
|
|
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;
|