@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.
- package/CHANGELOG.md +27 -0
- package/NOTICE.md +14 -0
- package/README.md +58 -26
- package/dist/brands/brand.ball.d.ts +29 -0
- package/dist/brands/brand.ball.js +23 -0
- package/dist/brands/brand.data.d.ts +47 -0
- package/dist/brands/brand.data.js +308 -0
- package/dist/brands/brand.letters.d.ts +31 -0
- package/dist/brands/brand.letters.js +88 -0
- package/dist/brands/brand.render.d.ts +47 -0
- package/dist/brands/brand.render.js +106 -0
- package/dist/brands-entry.d.ts +8 -0
- package/dist/brands-entry.js +5 -0
- package/dist/emblems/award/ribbon.d.ts +2 -0
- package/dist/emblems/award/ribbon.js +17 -0
- 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/emblem.equipment.js +3 -3
- package/dist/emblems/emblem.ids.d.ts +4 -4
- package/dist/emblems/emblem.ids.js +2 -2
- package/dist/emblems/emblem.registry.js +2 -0
- package/dist/emblems/gear/gear.index.js +2 -1
- package/dist/emblems/gear/gear.nets.d.ts +3 -0
- package/dist/emblems/gear/gear.nets.js +103 -0
- package/dist/emblems-entry.d.ts +1 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +5 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +8 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Baseball: a white leather ball with 108 red double stitches in V's along the figure-eight seam.
|
|
2
|
-
import { seamedBall } from "./ball.family.seamed.js";
|
|
2
|
+
import { SEAMED_HOME, seamedBall } from "./ball.family.seamed.js";
|
|
3
3
|
export const BALL_BASEBALL = /* @__PURE__ */ seamedBall({
|
|
4
4
|
id: "ball:baseball",
|
|
5
5
|
en: "Baseball",
|
|
@@ -9,6 +9,7 @@ export const BALL_BASEBALL = /* @__PURE__ */ seamedBall({
|
|
|
9
9
|
b: 0.4,
|
|
10
10
|
view: { yaw: 18, pitch: 28, roll: -10 },
|
|
11
11
|
mat: { gloss: 0.3, shade: "#4a3a2a", grain: { f: 1.4, a: 0.12 } },
|
|
12
|
+
home: SEAMED_HOME("baseball"),
|
|
12
13
|
groove: "#8a7a62",
|
|
13
14
|
stitch: { colour: "#c4262e", count: 108, half: 0.075, width: 0.85, lean: 0.032 },
|
|
14
15
|
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** The contrast of two colours, 1 to 21. */
|
|
2
|
+
export declare function contrast(a: string, b: string): number;
|
|
3
|
+
/** The stitch colour for a ball: the wanted one if it is a colour, else the ball's own thread, cream, navy or near-black, whichever shows on every colour the ball is. */
|
|
4
|
+
export declare function stitchFor(grounds: readonly string[], wanted: string | undefined, fallback: string): string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Contrast of colours, for choosing a thread or an ink that shows on the ball. Imports only the core's colour reader.
|
|
2
|
+
import { hexOf, readColour } from "./ball.core.js";
|
|
3
|
+
const luminance = (hex) => {
|
|
4
|
+
const rgb = readColour(hex) ?? [128, 128, 128];
|
|
5
|
+
const f = (c) => {
|
|
6
|
+
const v = c / 255;
|
|
7
|
+
return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
|
|
8
|
+
};
|
|
9
|
+
return 0.2126 * f(rgb[0]) + 0.7152 * f(rgb[1]) + 0.0722 * f(rgb[2]);
|
|
10
|
+
};
|
|
11
|
+
/** The contrast of two colours, 1 to 21. */
|
|
12
|
+
export function contrast(a, b) {
|
|
13
|
+
const x = luminance(a), y = luminance(b);
|
|
14
|
+
return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05);
|
|
15
|
+
}
|
|
16
|
+
/** The stitch colour for a ball: the wanted one if it is a colour, else the ball's own thread, cream, navy or near-black, whichever shows on every colour the ball is. */
|
|
17
|
+
export function stitchFor(grounds, wanted, fallback) {
|
|
18
|
+
const w = wanted === undefined ? undefined : readColour(wanted);
|
|
19
|
+
if (w !== undefined)
|
|
20
|
+
return hexOf(w);
|
|
21
|
+
return [fallback, "#f4f0e6", "#1b2a5c", "#101418"].find((c) => grounds.every((g) => contrast(c, g) >= 2.2)) ?? fallback;
|
|
22
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { FlagRenderer, PuckFace } from "./ball.face.ts";
|
|
2
|
+
import type { Surface } from "./ball.engine.ts";
|
|
3
|
+
import type { BallDecal, BallMark, BallPaint, BallScript, BallSpots, BallStamp } from "./ball.print.ts";
|
|
2
4
|
/** `metal` (also written `colour`): full colour, with shading. `line`: one outline in `currentColor`. */
|
|
3
5
|
export type PieceStyle = "metal" | "colour" | "color" | "line";
|
|
4
6
|
/** `inherit` (the default): the page's text colour. `auto`: dark ink on a light device and pale ink on a dark one. `light` and `dark` force one. */
|
|
@@ -27,7 +29,23 @@ export interface PieceOptions {
|
|
|
27
29
|
flags?: FlagRenderer;
|
|
28
30
|
/** A judogi seen from behind: the back number on it (see `Backnumber`). The back views of the judogi (`gear:judogi-back`, `gear:judogi-blue-back`) take it; anything else ignores it. */
|
|
29
31
|
backnumber?: Backnumber;
|
|
32
|
+
/** A baseball, a softball and the classic soccer balls: the colour of the ball, as one colour or two (see `BallPaint`). Any other ball takes the body colour as a tint. */
|
|
33
|
+
paint?: string | BallPaint;
|
|
34
|
+
/** A baseball, a softball and the classic soccer balls: a logo printed on the side of the ball, wrapped to the sphere (see `BallDecal`). An SVG string, a `data:` image URI or an object. */
|
|
35
|
+
decal?: string | BallDecal;
|
|
36
|
+
/** The same balls: stamped text like a ball's stamp, `true` for the default (see `BallStamp`). */
|
|
37
|
+
stamp?: boolean | string | readonly string[] | BallStamp;
|
|
38
|
+
/** The same balls: a cursive script word where a maker's mark sits, `true` for the ball's own word, or your text (see `BallScript`). */
|
|
39
|
+
script?: boolean | string | BallScript;
|
|
40
|
+
/** The same balls: a small mark under the script, `true` for the circled letter, an SVG string of your own, or an object (see `BallMark`). */
|
|
41
|
+
mark?: boolean | string | BallMark;
|
|
42
|
+
/** The same balls: where on the ball the logo and the lettering go (see `BallSpots`). */
|
|
43
|
+
spots?: BallSpots;
|
|
44
|
+
/** @internal The function that draws the print of a ball (`printLayers`). `renderEmblem`, the registry and `printed()` set it; a ball taken from its own module has none and ignores `decal`, `stamp`, `script` and `mark`. */
|
|
45
|
+
printer?: PrintRenderer;
|
|
30
46
|
}
|
|
47
|
+
/** What draws the print of a ball on its surface: the options, the ball's own word, and the colours the print lies on. */
|
|
48
|
+
export type PrintRenderer = (s: Surface, p: PrintOptions | undefined, word: string, grounds: readonly string[]) => string;
|
|
31
49
|
/**
|
|
32
50
|
* The back number sewn on the back of a judogi (IJF Sport and Organisation Rules, C1.8): the competitor's family name and the three letters of the National Olympic Committee, on a white patch under a coloured band.
|
|
33
51
|
* Only `gear:judogi-back` and `gear:judogi-blue-back` take it; anything else ignores it.
|
|
@@ -112,7 +130,31 @@ export interface ResolvedOptions {
|
|
|
112
130
|
flags: FlagRenderer | undefined;
|
|
113
131
|
/** A string that tells two faces apart, for the ids of a drawing. */
|
|
114
132
|
faceKey: string;
|
|
133
|
+
/** The colours of a printed or painted ball, read and made safe; undefined when none was asked for. */
|
|
134
|
+
paint: ResolvedPaint | undefined;
|
|
135
|
+
/** What a ball is printed with, as the caller gave it (made safe where it is drawn, by `resolvePrint`); undefined when nothing was asked for. */
|
|
136
|
+
print: PrintOptions | undefined;
|
|
137
|
+
/** What draws the print, when the caller has it (see `PieceOptions.printer`). */
|
|
138
|
+
printer: PrintRenderer | undefined;
|
|
115
139
|
}
|
|
140
|
+
/** The colours of a painted ball, as hex colours. */
|
|
141
|
+
export interface ResolvedPaint {
|
|
142
|
+
body: string | undefined;
|
|
143
|
+
second: string | undefined;
|
|
144
|
+
split: "seam" | "half";
|
|
145
|
+
angle: number;
|
|
146
|
+
stitch: string | undefined;
|
|
147
|
+
}
|
|
148
|
+
/** The printing options of a ball, as given. */
|
|
149
|
+
export interface PrintOptions {
|
|
150
|
+
decal?: PieceOptions["decal"];
|
|
151
|
+
stamp?: PieceOptions["stamp"];
|
|
152
|
+
script?: PieceOptions["script"];
|
|
153
|
+
mark?: PieceOptions["mark"];
|
|
154
|
+
spots?: PieceOptions["spots"];
|
|
155
|
+
}
|
|
156
|
+
/** True when a ball is asked to carry a logo, lettering or a mark. */
|
|
157
|
+
export declare const hasPrint: (p: PrintOptions | undefined) => boolean;
|
|
116
158
|
export declare function resolveOptions(o: PieceOptions | undefined): ResolvedOptions;
|
|
117
159
|
/** The ink of a drawing when a theme sets it, and the page it is made to read on. */
|
|
118
160
|
export declare const PIECE_INK: {
|
|
@@ -42,6 +42,39 @@ export function hashOf(text) {
|
|
|
42
42
|
h = Math.imul(h ^ text.charCodeAt(i), 16777619);
|
|
43
43
|
return (h >>> 0).toString(36);
|
|
44
44
|
}
|
|
45
|
+
/** True when a ball is asked to carry a logo, lettering or a mark. */
|
|
46
|
+
export const hasPrint = (p) => p !== undefined && ((p.decal !== undefined && p.decal !== false) || [p.stamp, p.script, p.mark].some((v) => v !== undefined && v !== false));
|
|
47
|
+
function paintOf(value) {
|
|
48
|
+
const o = typeof value === "string" ? { body: value } : typeof value === "object" && value !== null ? value : undefined;
|
|
49
|
+
if (o === undefined)
|
|
50
|
+
return undefined;
|
|
51
|
+
const pick = (v) => {
|
|
52
|
+
const rgb = typeof v === "string" ? readColour(v) : undefined;
|
|
53
|
+
return rgb === undefined ? undefined : hexOf(rgb);
|
|
54
|
+
};
|
|
55
|
+
const body = pick(o.body), second = pick(o.second), stitch = pick(o.stitch);
|
|
56
|
+
if (body === undefined && second === undefined && stitch === undefined)
|
|
57
|
+
return undefined;
|
|
58
|
+
const angle = typeof o.angle === "number" && Number.isFinite(o.angle) ? Math.max(-360, Math.min(360, o.angle)) : 0;
|
|
59
|
+
return { body, second, split: o.split === "half" ? "half" : "seam", angle, stitch };
|
|
60
|
+
}
|
|
61
|
+
/** The printing options that were given, copied (a hostile object is never held), or undefined when there are none. */
|
|
62
|
+
function printOf(o) {
|
|
63
|
+
if (o === undefined)
|
|
64
|
+
return undefined;
|
|
65
|
+
const p = {};
|
|
66
|
+
if (o.decal !== undefined)
|
|
67
|
+
p.decal = o.decal;
|
|
68
|
+
if (o.stamp !== undefined)
|
|
69
|
+
p.stamp = o.stamp;
|
|
70
|
+
if (o.script !== undefined)
|
|
71
|
+
p.script = o.script;
|
|
72
|
+
if (o.mark !== undefined)
|
|
73
|
+
p.mark = o.mark;
|
|
74
|
+
if (o.spots !== undefined)
|
|
75
|
+
p.spots = o.spots;
|
|
76
|
+
return Object.keys(p).length === 0 ? undefined : p;
|
|
77
|
+
}
|
|
45
78
|
/** What tells one face from another: its plain data (a renderer is a function, and only its name goes in). */
|
|
46
79
|
function faceKeyOf(face) {
|
|
47
80
|
if (typeof face !== "object" || face === null)
|
|
@@ -68,7 +101,7 @@ export function resolveOptions(o) {
|
|
|
68
101
|
const theme = o?.theme === "auto" || o?.theme === "light" || o?.theme === "dark" ? o.theme : "inherit";
|
|
69
102
|
const tint = readColour(o?.tint);
|
|
70
103
|
const accent = readColour(o?.accent);
|
|
71
|
-
return { style: o?.style === "line" ? "line" : "colour", size, theme, tint: tint === undefined ? undefined : hexOf(tint), accent: accent === undefined ? undefined : hexOf(accent), colors: coloursOf(o?.colors), grain: typeof o?.grain === "boolean" ? o.grain : undefined, face: typeof o?.face === "object" && o.face !== null ? o.face : undefined, flags: typeof o?.flags === "function" ? o.flags : undefined, faceKey: faceKeyOf(o?.face) + (typeof o?.flags === "function" ? `|fn:${o.flags.name}` : "") };
|
|
104
|
+
return { style: o?.style === "line" ? "line" : "colour", size, theme, tint: tint === undefined ? undefined : hexOf(tint), accent: accent === undefined ? undefined : hexOf(accent), colors: coloursOf(o?.colors), grain: typeof o?.grain === "boolean" ? o.grain : undefined, face: typeof o?.face === "object" && o.face !== null ? o.face : undefined, flags: typeof o?.flags === "function" ? o.flags : undefined, faceKey: faceKeyOf(o?.face) + (typeof o?.flags === "function" ? `|fn:${o.flags.name}` : ""), paint: paintOf(o?.paint), print: printOf(o), printer: typeof o?.printer === "function" ? o.printer : undefined };
|
|
72
105
|
}
|
|
73
106
|
/** The ink of a drawing when a theme sets it, and the page it is made to read on. */
|
|
74
107
|
export const PIECE_INK = { light: "#20262d", dark: "#eef1f3" };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type Piece, type PieceSize, type ResolvedOptions } from "./ball.core.ts";
|
|
2
|
-
import { type V3 } from "./ball.math.ts";
|
|
1
|
+
import { type Piece, type PieceSize, type PrintOptions, type PrintRenderer, type ResolvedOptions, type ResolvedPaint } from "./ball.core.ts";
|
|
2
|
+
import { type M3, type V3 } from "./ball.math.ts";
|
|
3
3
|
/** How a ball takes the light. */
|
|
4
4
|
export interface Material {
|
|
5
5
|
/** The colour of the shadowed side; by default the base colour made darker. */
|
|
@@ -32,12 +32,14 @@ export interface Surface {
|
|
|
32
32
|
ink?: boolean;
|
|
33
33
|
opacity?: number;
|
|
34
34
|
n?: number;
|
|
35
|
+
rule?: "evenodd";
|
|
35
36
|
}): string;
|
|
36
37
|
/** Several closed polygons of one fill, drawn as one path (a smaller document than a path for each). */
|
|
37
38
|
panels(rings: ReadonlyArray<readonly V3[]>, fill: string, o?: {
|
|
38
39
|
ink?: boolean;
|
|
39
40
|
opacity?: number;
|
|
40
41
|
n?: number;
|
|
42
|
+
rule?: "evenodd";
|
|
41
43
|
}): string;
|
|
42
44
|
/** A curve on the surface: only what faces you. `closed` joins its ends; `deco` marks a line that is not part of the ball's form (a highlight), which the line style leaves out. */
|
|
43
45
|
stroke(pts: readonly V3[], colour: string, width: number, o?: StrokeOptions): string;
|
|
@@ -59,8 +61,16 @@ export interface Surface {
|
|
|
59
61
|
facing(u: V3): number;
|
|
60
62
|
/** A point of space, in units of the ball's radius from its centre (not on the surface): its screen position and depth (positive is toward you). */
|
|
61
63
|
space(p: V3): [number, number, number];
|
|
64
|
+
/** The whole disc of the ball filled with a colour (a sphere's silhouette). */
|
|
65
|
+
fill(colour: string): string;
|
|
66
|
+
/** The ball's view, for a module that draws what the engine does not (`ball.region.ts`). Opaque to everything else. */
|
|
67
|
+
readonly view: Geometry;
|
|
68
|
+
/** Where a ball's printing sits: the middle of its label area in the ball's own frame, and the right-hand and upward directions there. Only a ball that is printed has one. */
|
|
69
|
+
readonly home: HomeFrame | undefined;
|
|
62
70
|
}
|
|
63
71
|
export interface StrokeOptions {
|
|
72
|
+
/** Digits kept after the point in the path (1 by default; lettering asks for 2). */
|
|
73
|
+
digits?: number;
|
|
64
74
|
closed?: boolean;
|
|
65
75
|
ink?: boolean;
|
|
66
76
|
deco?: boolean;
|
|
@@ -88,8 +98,23 @@ export interface BallSpec {
|
|
|
88
98
|
/** The colour the whole ball is filled with before its panels are laid on, where it is not `base` (a soccer ball is white, its pentagons are `base`). */
|
|
89
99
|
ground?: string;
|
|
90
100
|
mat: Material;
|
|
91
|
-
/** Lays the panels and seams. `base` is the main colour, or the tint; `accent` is the second colour or undefined. */
|
|
92
|
-
draw: (s: Surface, base: string, accent: string | undefined, colors?: readonly [string, string, string]) => string;
|
|
101
|
+
/** Lays the panels and seams. `base` is the main colour, or the tint; `accent` is the second colour or undefined. `custom` is what a caller asked to paint and print (a ball that takes it reads it; the rest ignore it). */
|
|
102
|
+
draw: (s: Surface, base: string, accent: string | undefined, colors?: readonly [string, string, string], custom?: Custom) => string;
|
|
103
|
+
/**
|
|
104
|
+
* Where a ball carries a logo and lettering: the middle of the label area (`c`) and the way up there (`up`), as directions in the ball's own frame, how far it reaches from the middle (`reach`, radians),
|
|
105
|
+
* the view that shows it (`view`, used instead of `view` when the caller prints something) and the ball's own word for the script and the stamp (`word`).
|
|
106
|
+
*/
|
|
107
|
+
home?: {
|
|
108
|
+
c: V3;
|
|
109
|
+
up: V3;
|
|
110
|
+
reach: number;
|
|
111
|
+
view: {
|
|
112
|
+
yaw: number;
|
|
113
|
+
pitch: number;
|
|
114
|
+
roll: number;
|
|
115
|
+
};
|
|
116
|
+
word: string;
|
|
117
|
+
};
|
|
93
118
|
/** Where the ball's centre is in the 100 by 100 box (50, 50 by default): a hammer's head sits in a corner to leave room for the wire. */
|
|
94
119
|
centre?: readonly [number, number];
|
|
95
120
|
/** Where the ball is in space, in units of its radius from the middle of the whole (0, 0, 0 by default), turned with it: a hammer's head is at one end of the hammer, which turns about its middle. */
|
|
@@ -102,6 +127,47 @@ export interface BallSpec {
|
|
|
102
127
|
front: string;
|
|
103
128
|
};
|
|
104
129
|
}
|
|
130
|
+
/** The label area of a printed ball: its middle, and the right-hand and upward directions there, and how far it reaches. */
|
|
131
|
+
export interface HomeFrame {
|
|
132
|
+
c: V3;
|
|
133
|
+
ex: V3;
|
|
134
|
+
up: V3;
|
|
135
|
+
reach: number;
|
|
136
|
+
}
|
|
137
|
+
/** What a caller asked a ball to be painted and printed with. */
|
|
138
|
+
export interface Custom {
|
|
139
|
+
paint: ResolvedPaint | undefined;
|
|
140
|
+
print: PrintOptions | undefined;
|
|
141
|
+
/** Draws the print, when the caller has it; without it a ball draws no print. */
|
|
142
|
+
printer: PrintRenderer | undefined;
|
|
143
|
+
}
|
|
144
|
+
export declare const EPS = 0.000001;
|
|
145
|
+
export interface Geometry {
|
|
146
|
+
/** The centre of the ball on the screen, and the middle of the whole (where a point of `space` is measured from). */
|
|
147
|
+
cx: number;
|
|
148
|
+
cy: number;
|
|
149
|
+
ox: number;
|
|
150
|
+
oy: number;
|
|
151
|
+
axes: V3;
|
|
152
|
+
rot: M3;
|
|
153
|
+
k: number;
|
|
154
|
+
/** The unit sphere's point is visible when its dot with `w` is above 0. */
|
|
155
|
+
w: V3;
|
|
156
|
+
wl: number;
|
|
157
|
+
/** The third row of the rotation: the direction of the eye in the ball's own frame. */
|
|
158
|
+
rz: V3;
|
|
159
|
+
/** The exponent of a pointed ball's profile, or undefined for an ellipsoid. */
|
|
160
|
+
point: number | undefined;
|
|
161
|
+
}
|
|
162
|
+
/** 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. */
|
|
163
|
+
export declare const visOf: (g: Geometry, u: V3) => number;
|
|
164
|
+
export declare const project: (g: Geometry, u: V3) => [number, number];
|
|
165
|
+
export interface Run {
|
|
166
|
+
pts: Array<[number, number, number]>;
|
|
167
|
+
}
|
|
168
|
+
/** The parts of a curve that face you, each as screen points with their facing. The curve is cut exactly at the limb. */
|
|
169
|
+
export declare function visibleRuns(g: Geometry, pts: readonly V3[], closed: boolean): Run[];
|
|
170
|
+
export declare const pathOf: (pts: Array<[number, number, number]>, digits?: number) => string;
|
|
105
171
|
/** Draws a ball: the document for a spec, the options and a turn in radians. */
|
|
106
172
|
export declare function drawBall(title: string, key: string, spec: BallSpec, opts: ResolvedOptions, turn: {
|
|
107
173
|
yaw: number;
|
|
@@ -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
|
}
|