@johnmorrisdotca/dougu-sports 0.26.0 → 0.28.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 (60) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +3 -3
  3. package/dist/emblems/emblem.descriptions.js +16 -4
  4. package/dist/emblems/emblem.ids.d.ts +2 -2
  5. package/dist/emblems/emblem.ids.js +12 -0
  6. package/dist/emblems/emblem.lathe.d.ts +124 -4
  7. package/dist/emblems/emblem.lathe.js +28 -6
  8. package/dist/emblems/emblem.named.js +125 -5
  9. package/dist/emblems/emblem.registry.js +24 -0
  10. package/dist/emblems/lathe.ballrods.d.ts +2 -0
  11. package/dist/emblems/lathe.ballrods.js +155 -0
  12. package/dist/emblems/lathe.conference.d.ts +2 -0
  13. package/dist/emblems/lathe.conference.js +219 -0
  14. package/dist/emblems/lathe.extras.d.ts +3 -1
  15. package/dist/emblems/lathe.extras.js +77 -2
  16. package/dist/emblems/lathe.framed.d.ts +30 -0
  17. package/dist/emblems/lathe.framed.js +98 -0
  18. package/dist/emblems/lathe.sculpt.d.ts +6 -0
  19. package/dist/emblems/lathe.sculpt.js +320 -0
  20. package/dist/emblems/nba.parts.d.ts +27 -0
  21. package/dist/emblems/nba.parts.js +34 -0
  22. package/dist/emblems/trophy/bill-russell-trophy.d.ts +1 -1
  23. package/dist/emblems/trophy/bill-russell-trophy.js +17 -13
  24. package/dist/emblems/trophy/bob-cousy-trophy.d.ts +2 -0
  25. package/dist/emblems/trophy/bob-cousy-trophy.js +18 -0
  26. package/dist/emblems/trophy/calder-cup.js +4 -4
  27. package/dist/emblems/trophy/foundation-player-award.d.ts +2 -0
  28. package/dist/emblems/trophy/foundation-player-award.js +31 -0
  29. package/dist/emblems/trophy/george-halas-trophy.d.ts +2 -0
  30. package/dist/emblems/trophy/george-halas-trophy.js +6 -0
  31. package/dist/emblems/trophy/jackie-robinson-award.d.ts +2 -0
  32. package/dist/emblems/trophy/jackie-robinson-award.js +60 -0
  33. package/dist/emblems/trophy/kelly-cup.js +7 -6
  34. package/dist/emblems/trophy/lamar-hunt-trophy.d.ts +1 -1
  35. package/dist/emblems/trophy/lamar-hunt-trophy.js +5 -16
  36. package/dist/emblems/trophy/larry-bird-trophy.d.ts +2 -0
  37. package/dist/emblems/trophy/larry-bird-trophy.js +23 -0
  38. package/dist/emblems/trophy/larry-obrien-trophy-1977.d.ts +2 -0
  39. package/dist/emblems/trophy/larry-obrien-trophy-1977.js +23 -0
  40. package/dist/emblems/trophy/larry-obrien-trophy.d.ts +1 -1
  41. package/dist/emblems/trophy/larry-obrien-trophy.js +18 -11
  42. package/dist/emblems/trophy/magic-johnson-trophy.d.ts +2 -0
  43. package/dist/emblems/trophy/magic-johnson-trophy.js +20 -0
  44. package/dist/emblems/trophy/messier-leadership-award.d.ts +2 -0
  45. package/dist/emblems/trophy/messier-leadership-award.js +79 -0
  46. package/dist/emblems/trophy/nba-eastern-conference-2001.d.ts +2 -0
  47. package/dist/emblems/trophy/nba-eastern-conference-2001.js +18 -0
  48. package/dist/emblems/trophy/nba-western-conference-2001.d.ts +2 -0
  49. package/dist/emblems/trophy/nba-western-conference-2001.js +19 -0
  50. package/dist/emblems/trophy/oscar-robertson-trophy.d.ts +2 -0
  51. package/dist/emblems/trophy/oscar-robertson-trophy.js +19 -0
  52. package/dist/emblems/trophy/willie-oree-award.d.ts +2 -0
  53. package/dist/emblems/trophy/willie-oree-award.js +55 -0
  54. package/dist/emblems/trophy/wnba-championship-trophy.d.ts +1 -1
  55. package/dist/emblems/trophy/wnba-championship-trophy.js +6 -17
  56. package/dist/index.d.ts +12 -0
  57. package/dist/index.js +12 -0
  58. package/dist/version.d.ts +1 -1
  59. package/dist/version.js +1 -1
  60. package/package.json +1 -1
@@ -9,7 +9,7 @@ const pt = (x, y) => `${f(x)} ${f(y)}`;
9
9
  export function LATHE_EXTRAS(c, d) {
10
10
  const { p, solid, yaw, where, at } = c;
11
11
  if (d[0] === "mesh") {
12
- const [, y0, y1, n, rows] = d;
12
+ const [, y0, y1, n, rows, name, width = 0.55] = d;
13
13
  let m = "";
14
14
  for (let i = 0; i < n; i++) {
15
15
  for (const way of [1, -1]) {
@@ -27,7 +27,13 @@ export function LATHE_EXTRAS(c, d) {
27
27
  }
28
28
  }
29
29
  }
30
- return m === "" ? "" : `<path d="${m}" stroke="${mix(c.base.rgb, 0.55)}" stroke-width=".55" stroke-opacity=".9"/>`;
30
+ if (m === "")
31
+ return "";
32
+ if (name === undefined)
33
+ return `<path d="${m}" stroke="${mix(c.base.rgb, 0.55)}" stroke-width=".55" stroke-opacity=".9"/>`;
34
+ // An etched net in a tone of its own: a dark line, then the tone laid on it a little to the side.
35
+ const t = c.tone(name);
36
+ return `<path d="${m}" transform="translate(.2 .25)" stroke="${mix(c.base.rgb, -0.6)}" stroke-width="${width}" stroke-opacity=".4"/><path d="${m}" stroke="${mix(t.rgb, 0.05)}" stroke-width="${width}"/>`;
31
37
  }
32
38
  if (d[0] === "patch") {
33
39
  const [, y0, y1, deg0, deg1, name] = d;
@@ -57,3 +63,72 @@ export function LATHE_EXTRAS(c, d) {
57
63
  };
58
64
  return `<path d="M${lerp(y0, u0)}L${lerp(y0, u1)}L${lerp(y1, u1)}L${lerp(y1, u0)}z" fill="${t.fill}" stroke="${t.edge}" stroke-width=".5"/>`;
59
65
  }
66
+ const rotate = (v, lz, lx, yaw) => {
67
+ const [x0, y0, z0] = v;
68
+ const x1 = x0 * Math.cos(lz) - y0 * Math.sin(lz);
69
+ const y1 = x0 * Math.sin(lz) + y0 * Math.cos(lz);
70
+ const y2 = y1 * Math.cos(lx) - z0 * Math.sin(lx);
71
+ const z2 = y1 * Math.sin(lx) + z0 * Math.cos(lx);
72
+ return [x1 * Math.cos(yaw) + z2 * Math.sin(yaw), y2, -x1 * Math.sin(yaw) + z2 * Math.cos(yaw)];
73
+ };
74
+ /** The great circles of a basketball's seams, by their normals: the equator, the meridian, and the two curved seams that run from pole to pole at 60 degrees either side of it. */
75
+ const SEAMS = [[0, 1, 0], [1, 0, 0], [Math.cos(Math.PI / 3), 0, -Math.sin(Math.PI / 3)], [Math.cos(Math.PI / 3), 0, Math.sin(Math.PI / 3)]];
76
+ /** Draws the seams of a basketball as grooves (each the front half of a great circle: one elliptical arc), and the specks of a grained finish. */
77
+ export function LATHE_BALL(c) {
78
+ const { ball, cx, cy } = c;
79
+ const r = ball.r;
80
+ const lz = ((ball.lean?.[0] ?? 0) * Math.PI) / 180;
81
+ const lx = ((ball.lean?.[1] ?? 0) * Math.PI) / 180;
82
+ const yaw = (c.yaw * Math.PI) / 180;
83
+ const e = (c.elevation * Math.PI) / 180;
84
+ const se = Math.sin(e);
85
+ const ce = Math.cos(e);
86
+ const view = [0, se, ce];
87
+ const screen = (v) => [v[0], -v[1] * ce + v[2] * se];
88
+ const t = c.tone(ball.tone);
89
+ let m = "";
90
+ for (const n0 of SEAMS) {
91
+ const n = rotate(n0, lz, lx, yaw);
92
+ const facing = n[0] * view[0] + n[1] * view[1] + n[2] * view[2];
93
+ const limb = [n[1] * view[2] - n[2] * view[1], n[2] * view[0] - n[0] * view[2], n[0] * view[1] - n[1] * view[0]];
94
+ const size = Math.hypot(...limb);
95
+ if (size < 0.02)
96
+ continue;
97
+ const a = screen([limb[0] / size, limb[1] / size, limb[2] / size]);
98
+ const near = [view[0] - facing * n[0], view[1] - facing * n[1], view[2] - facing * n[2]];
99
+ const k = Math.hypot(...near);
100
+ const mid = screen([near[0] / k, near[1] / k, near[2] / k]);
101
+ const sweep = a[0] * mid[1] - a[1] * mid[0] > 0 ? 1 : 0;
102
+ const angle = (Math.atan2(a[1], a[0]) * 180) / Math.PI;
103
+ m += `M${pt(cx + r * a[0], cy + r * a[1])}A${f(r)} ${f(Math.max(0.05, r * Math.abs(facing)))} ${f(angle)} 0 ${sweep} ${pt(cx - r * a[0], cy - r * a[1])}`;
104
+ }
105
+ let out = "";
106
+ if (m !== "") {
107
+ if (c.line)
108
+ out += `<path d="${m}" stroke-width="1.4"/>`;
109
+ else if (c.flat)
110
+ out += `<path d="${m}" stroke="${mix(t.rgb, -0.4)}" stroke-width="1"/>`;
111
+ else {
112
+ const inlay = ball.seam === undefined ? undefined : c.tone(ball.seam);
113
+ out += `<path d="${m}" stroke="${mix(t.rgb, -0.5)}" stroke-width="${inlay === undefined ? 0.75 : 0.85}"/>`;
114
+ out += inlay === undefined ? `<path d="${m}" transform="translate(.35 .4)" stroke="${mix(t.rgb, 0.55)}" stroke-width=".3" stroke-opacity=".8"/>` : `<path d="${m}" stroke="${mix(inlay.rgb, 0.1)}" stroke-width=".42"/>`;
115
+ }
116
+ }
117
+ const grain = ball.grain ?? 0;
118
+ if (grain > 0 && !c.line) {
119
+ let dots = "";
120
+ for (let i = 0; i < grain; i++) {
121
+ const y = 1 - (2 * (i + 0.5)) / grain;
122
+ const ring = Math.sqrt(1 - y * y);
123
+ const theta = i * 2.39996;
124
+ const p = rotate([ring * Math.cos(theta), y, ring * Math.sin(theta)], lz, lx, yaw);
125
+ if (p[0] * view[0] + p[1] * view[1] + p[2] * view[2] < 0.12)
126
+ continue;
127
+ const [sx, sy] = screen(p);
128
+ dots += `M${pt(cx + r * sx, cy + r * sy)}h0`;
129
+ }
130
+ if (dots !== "")
131
+ out += `<path d="${dots}" stroke="${mix(t.rgb, -0.5)}" stroke-width=".36" stroke-opacity=".6"/>`;
132
+ }
133
+ return out;
134
+ }
@@ -0,0 +1,30 @@
1
+ import { type Rgb } from "./emblem.core.ts";
2
+ import type { FramedCtx } from "./emblem.lathe.ts";
3
+ /** A point or a direction: x to the right, y down, z toward the eye at rest, in the lathe's units about the axis x = 0 (drawn at x = 32). */
4
+ export type V3 = readonly [x: number, y: number, z: number];
5
+ /** The turn and the eye of a drawing: rotate a point about the axis, place it on the page, and say how near the eye it is and whether a direction faces the eye. */
6
+ export declare function viewOf(c: Pick<FramedCtx, "yaw" | "s">): {
7
+ rot: (v: V3) => V3;
8
+ at: (r: V3) => readonly [number, number];
9
+ depth: (r: V3) => number;
10
+ facing: (n: V3) => number;
11
+ };
12
+ export declare const sub: (a: V3, b: V3) => V3;
13
+ export declare const cross: (a: V3, b: V3) => V3;
14
+ export declare const unit: (a: V3) => V3;
15
+ /** The normal of a facet from its first, middle and last points (a quadrilateral's diagonals). */
16
+ export declare const normalOf: (p: readonly V3[]) => V3;
17
+ /**
18
+ * How light polished metal is where a facet faces some way: -1 (black) to 1 (white), for a rotated, unit normal and the elevation of the eye `s`. Two sided, because a thin sheet is seen from either side. A polished
19
+ * surface shows what it mirrors, so the shade is read off a small room: a bright sky above, a dark band at the horizon, a mid floor and a bright window to the upper left. A facet that turns from the sky to the floor
20
+ * as a band twists runs from white through black to grey, the way chrome does.
21
+ */
22
+ export declare function chrome(n: V3, s: number): number;
23
+ /** A colour for a shade from `chrome`, kept to a few steps so that neighbouring facets that differ by a hair are one colour. */
24
+ export declare const shadeOf: (rgb: Rgb, v: number, steps?: number) => string;
25
+ /** A smooth curve through a table of `[x, y]` points that never overshoots them (monotone cubic), as a function of x; outside the table it holds the end values. */
26
+ export declare function curve(table: readonly (readonly [number, number])[]): (x: number) => number;
27
+ /** A straight-line table: the value at x between the two points either side of it. */
28
+ export declare function lin(table: readonly (readonly [number, number])[]): (x: number) => number;
29
+ /** The points as a path: a closed polygon, or an open line. */
30
+ export declare const line: (pts: readonly (readonly [number, number])[], closed: boolean, digits?: 1 | 2) => string;
@@ -0,0 +1,98 @@
1
+ // The small 3D that a body of revolution's own `framed` drawer use (`FramedCtx` in emblem.lathe.ts): a point turned by the yaw and placed the way the body's surface is placed, a polished-metal shade for a facet from the way it
2
+ // faces, a smooth curve through a table of measurements, and the paths of polygons. Nothing here knows any one emblem; a body with no framed parts does not take this module. Imports only the core.
3
+ import { mix } from "./emblem.core.js";
4
+ const RAD = Math.PI / 180;
5
+ const one = (n) => String(Math.round(n * 10) / 10 + 0);
6
+ const two = (n) => String(Math.round(n * 100) / 100 + 0);
7
+ /** The turn and the eye of a drawing: rotate a point about the axis, place it on the page, and say how near the eye it is and whether a direction faces the eye. */
8
+ export function viewOf(c) {
9
+ const cy = Math.cos(c.yaw * RAD);
10
+ const sy = Math.sin(c.yaw * RAD);
11
+ const ce = Math.sqrt(1 - c.s * c.s);
12
+ return {
13
+ rot: (v) => [v[0] * cy + v[2] * sy, v[1], v[2] * cy - v[0] * sy],
14
+ at: (r) => [32 + r[0], r[1] + c.s * r[2]],
15
+ depth: (r) => r[2] * ce - r[1] * c.s,
16
+ facing: (n) => n[2] * ce - n[1] * c.s,
17
+ };
18
+ }
19
+ export const sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
20
+ export const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
21
+ export const unit = (a) => {
22
+ const m = Math.hypot(a[0], a[1], a[2]) || 1;
23
+ return [a[0] / m, a[1] / m, a[2] / m];
24
+ };
25
+ /** The normal of a facet from its first, middle and last points (a quadrilateral's diagonals). */
26
+ export const normalOf = (p) => unit(cross(sub(p[2], p[0]), sub(p[p.length - 1], p[1])));
27
+ const smooth = (x, a, b) => {
28
+ const t = Math.max(0, Math.min(1, (x - a) / (b - a)));
29
+ return t * t * (3 - 2 * t);
30
+ };
31
+ /**
32
+ * How light polished metal is where a facet faces some way: -1 (black) to 1 (white), for a rotated, unit normal and the elevation of the eye `s`. Two sided, because a thin sheet is seen from either side. A polished
33
+ * surface shows what it mirrors, so the shade is read off a small room: a bright sky above, a dark band at the horizon, a mid floor and a bright window to the upper left. A facet that turns from the sky to the floor
34
+ * as a band twists runs from white through black to grey, the way chrome does.
35
+ */
36
+ export function chrome(n, s) {
37
+ const ce = Math.sqrt(1 - s * s);
38
+ const eye = [0, -s, ce];
39
+ let m = n;
40
+ let d = m[0] * eye[0] + m[1] * eye[1] + m[2] * eye[2];
41
+ if (d < 0) {
42
+ m = [-m[0], -m[1], -m[2]];
43
+ d = -d;
44
+ }
45
+ const r = [2 * d * m[0] - eye[0], 2 * d * m[1] - eye[1], 2 * d * m[2] - eye[2]];
46
+ const up = -r[1];
47
+ const window = r[2] > 0 ? Math.exp(-(((r[0] + 0.5) / 0.34) ** 2 + ((up - 0.4) / 0.3) ** 2)) : 0;
48
+ const v = -0.12 + 0.95 * smooth(up, -0.12, 0.4) - 0.6 * Math.exp(-(((up + 0.02) / 0.14) ** 2)) + 0.75 * window;
49
+ return Math.max(-0.72, Math.min(0.9, v));
50
+ }
51
+ /** A colour for a shade from `chrome`, kept to a few steps so that neighbouring facets that differ by a hair are one colour. */
52
+ export const shadeOf = (rgb, v, steps = 24) => mix(rgb, Math.round(v * steps) / steps);
53
+ /** A smooth curve through a table of `[x, y]` points that never overshoots them (monotone cubic), as a function of x; outside the table it holds the end values. */
54
+ export function curve(table) {
55
+ const n = table.length;
56
+ const dx = table.slice(1).map((p, i) => p[0] - table[i][0]);
57
+ const dy = table.slice(1).map((p, i) => p[1] - table[i][1]);
58
+ const m = dy.map((d, i) => d / dx[i]);
59
+ const t = table.map((_, i) => (i === 0 ? m[0] : i === n - 1 ? m[n - 2] : m[i - 1] * m[i] <= 0 ? 0 : (2 * m[i - 1] * m[i]) / (m[i - 1] + m[i])));
60
+ return (x) => {
61
+ const first = table[0];
62
+ const last = table[n - 1];
63
+ if (x <= first[0])
64
+ return first[1];
65
+ if (x >= last[0])
66
+ return last[1];
67
+ let i = 0;
68
+ while (x > table[i + 1][0])
69
+ i++;
70
+ const a = table[i];
71
+ const w = dx[i];
72
+ const u = (x - a[0]) / w;
73
+ const u2 = u * u;
74
+ const u3 = u2 * u;
75
+ return (2 * u3 - 3 * u2 + 1) * a[1] + (u3 - 2 * u2 + u) * w * t[i] + (-2 * u3 + 3 * u2) * table[i + 1][1] + (u3 - u2) * w * t[i + 1];
76
+ };
77
+ }
78
+ /** A straight-line table: the value at x between the two points either side of it. */
79
+ export function lin(table) {
80
+ return (x) => {
81
+ const first = table[0];
82
+ if (x <= first[0])
83
+ return first[1];
84
+ for (let i = 1; i < table.length; i++) {
85
+ const b = table[i];
86
+ if (x <= b[0]) {
87
+ const a = table[i - 1];
88
+ return a[1] + ((b[1] - a[1]) * (x - a[0])) / (b[0] - a[0]);
89
+ }
90
+ }
91
+ return table[table.length - 1][1];
92
+ };
93
+ }
94
+ /** The points as a path: a closed polygon, or an open line. */
95
+ export const line = (pts, closed, digits = 1) => {
96
+ const w = digits === 1 ? one : two;
97
+ return `M${pts.map((p) => `${w(p[0])} ${w(p[1])}`).join("L")}${closed ? "z" : ""}`;
98
+ };
@@ -0,0 +1,6 @@
1
+ import type { Part, SculptCtx } from "./emblem.lathe.ts";
2
+ /** Draws the parts of a model: the ones that are `under` the bodies, and the rest, each group from far to near. */
3
+ export declare function LATHE_SCULPT(c: SculptCtx, parts: readonly Part[]): {
4
+ under: string;
5
+ over: string;
6
+ };
@@ -0,0 +1,320 @@
1
+ // The parts a body of revolution cannot be: a flat cut-out with thickness (a player, a jersey, a plate of glass) and a ribbon that twists as it rises. A cut-out is a silhouette drawn as original paths
2
+ // and extruded; it stands in a plane that turns with the body, so it squeezes by the cosine of the turn, shows its back past a quarter turn (the front mirrored and a little darker) and shows its thickness
3
+ // as a solid edge, widest when it is edge on. A ribbon is a centre line with a width that turns about it, drawn as a strip of shaded quadrilaterals. A model that wants them sets `sculptor: LATHE_SCULPT`;
4
+ // one that does not never imports this file, so its bundle carries none of the code. Plain SVG.
5
+ import { mix } from "./emblem.core.js";
6
+ import { engravingInk, sideText } from "./emblem.engrave.js";
7
+ const CX = 32;
8
+ const RAD = Math.PI / 180;
9
+ const f = (n) => String(Math.round(n * 10) / 10 + 0);
10
+ const CACHE = new Map();
11
+ /** The polygons of a path of absolute `M L H V C Q Z` commands, curves cut into short straight pieces. */
12
+ function polys(d) {
13
+ const known = CACHE.get(d);
14
+ if (known !== undefined)
15
+ return known;
16
+ const tok = d.match(/[MLHVCQZ]|-?\d*\.?\d+/g) ?? [];
17
+ const out = [];
18
+ let cur = [];
19
+ let cmd = "";
20
+ let x = 0;
21
+ let y = 0;
22
+ let i = 0;
23
+ const num = () => Number(tok[i++]);
24
+ while (i < tok.length) {
25
+ const t = tok[i];
26
+ if (/[A-Z]/.test(t)) {
27
+ cmd = t;
28
+ i++;
29
+ if (cmd === "Z") {
30
+ if (cur.length > 0)
31
+ out.push(cur);
32
+ cur = [];
33
+ }
34
+ continue;
35
+ }
36
+ if (cmd === "M") {
37
+ x = num();
38
+ y = num();
39
+ if (cur.length > 0)
40
+ out.push(cur);
41
+ cur = [[x, y]];
42
+ cmd = "L";
43
+ }
44
+ else if (cmd === "L") {
45
+ x = num();
46
+ y = num();
47
+ cur.push([x, y]);
48
+ }
49
+ else if (cmd === "H") {
50
+ x = num();
51
+ cur.push([x, y]);
52
+ }
53
+ else if (cmd === "V") {
54
+ y = num();
55
+ cur.push([x, y]);
56
+ }
57
+ else if (cmd === "Q") {
58
+ const [x1, y1, x2, y2] = [num(), num(), num(), num()];
59
+ const qn = Math.max(2, Math.min(5, Math.ceil(Math.hypot(x2 - x, y2 - y) / 3.4)));
60
+ for (let k = 1; k <= qn; k++) {
61
+ const u = k / qn;
62
+ cur.push([(1 - u) ** 2 * x + 2 * (1 - u) * u * x1 + u * u * x2, (1 - u) ** 2 * y + 2 * (1 - u) * u * y1 + u * u * y2]);
63
+ }
64
+ x = x2;
65
+ y = y2;
66
+ }
67
+ else if (cmd === "C") {
68
+ const [x1, y1, x2, y2, x3, y3] = [num(), num(), num(), num(), num(), num()];
69
+ const cn = Math.max(2, Math.min(6, Math.ceil(Math.hypot(x3 - x, y3 - y) / 3.3)));
70
+ for (let k = 1; k <= cn; k++) {
71
+ const u = k / cn;
72
+ const a = (1 - u) ** 3;
73
+ const b = 3 * (1 - u) ** 2 * u;
74
+ const c = 3 * (1 - u) * u * u;
75
+ const e = u ** 3;
76
+ cur.push([a * x + b * x1 + c * x2 + e * x3, a * y + b * y1 + c * y2 + e * y3]);
77
+ }
78
+ x = x3;
79
+ y = y3;
80
+ }
81
+ else {
82
+ i++;
83
+ }
84
+ }
85
+ if (cur.length > 0)
86
+ out.push(cur);
87
+ CACHE.set(d, out);
88
+ return out;
89
+ }
90
+ function drawCut(c, stands) {
91
+ const { cut, ax, az, dy } = stands[0];
92
+ const { yaw, s, tone, line, flat } = c;
93
+ const th = yaw * RAD;
94
+ const phi = ((cut.rot ?? 0) + yaw) * RAD;
95
+ const cosP = Math.cos(phi);
96
+ const sinP = Math.sin(phi);
97
+ const A = ax * Math.cos(th) + az * Math.sin(th);
98
+ const B = az * Math.cos(th) - ax * Math.sin(th);
99
+ if ((cut.decal === true && cosP <= 0.03) || (line && cut.fine === true))
100
+ return undefined;
101
+ const t = cut.t ?? 1;
102
+ const sign = cosP >= 0 ? 1 : -1;
103
+ const path = (d, depth) => {
104
+ let out = "";
105
+ for (const st of stands) {
106
+ const sa = st.ax * Math.cos(th) + st.az * Math.sin(th);
107
+ const sb = st.az * Math.cos(th) - st.ax * Math.sin(th);
108
+ for (const poly of polys(d))
109
+ out += `M${poly.map(([px, py]) => `${f(CX + sa + (px - CX) * cosP + depth * sinP)} ${f(py + st.dy + s * (sb - (px - CX) * sinP + depth * cosP))}`).join("L")}Z`;
110
+ }
111
+ return out;
112
+ };
113
+ const near = (sign * t) / 2;
114
+ const first = cut.layers[0] ?? ["main", ""];
115
+ const base = tone(first[0]);
116
+ let svg = "";
117
+ const reach = t * Math.hypot(sinP, s * cosP);
118
+ const glass = first[2] === "gl";
119
+ if (first[1] === "") {
120
+ // A text only: nothing is drawn but the engraving.
121
+ }
122
+ else if (line) {
123
+ if (reach > 1.3 && Math.abs(sinP) > 0.3)
124
+ svg += `<path d="${path(first[1], -near)}" stroke-opacity=".6"/>`;
125
+ }
126
+ else if (glass) {
127
+ // Clear glass: its sides are the hull of the front and the back (a block of glass is convex), with the far edges faint inside it.
128
+ const hull = convexHull(polys(first[1]).flat().flatMap(([px, py]) => [[CX + A + (px - CX) * cosP + near * sinP, py + dy + s * (B - (px - CX) * sinP + near * cosP)], [CX + A + (px - CX) * cosP - near * sinP, py + dy + s * (B - (px - CX) * sinP - near * cosP)]]));
129
+ svg += `<path d="M${hull.map(([x, y]) => `${f(x)} ${f(y)}`).join("L")}Z" fill="${mix(base.rgb, 0.2)}" fill-opacity=".2" stroke="${base.edge}" stroke-opacity=".5" stroke-width=".4"/>`;
130
+ if (reach > 0.6)
131
+ svg += `<path d="${path(first[1], -near)}" stroke="${base.edge}" stroke-opacity=".4" stroke-width=".4"/>`;
132
+ }
133
+ else {
134
+ // The edge: the silhouette moved a little further along the thickness, back to front, so that together they are a solid band.
135
+ const steps = reach < 0.3 ? 0 : Math.min(cut.n ?? 8, Math.ceil(reach / 0.8));
136
+ const side = flat ? mix(base.rgb, -0.3) : mix(base.rgb, -0.42);
137
+ for (let k = 0; k < steps; k++)
138
+ svg += `<path d="${path(first[1], -near + (2 * near * k) / steps)}" fill="${side}" stroke="${side}" stroke-width="${f(Math.max(0.6, (reach / steps) * 1.3))}"/>`;
139
+ }
140
+ for (const [name, d, mode] of cut.layers) {
141
+ const q = tone(name);
142
+ if (line) {
143
+ if (mode === undefined || mode === "gl" || mode === "rf" || mode === "fl")
144
+ svg += `<path d="${path(d, near)}"/>`;
145
+ else if (mode === "ln")
146
+ svg += `<path d="${path(d, near)}" stroke-width="1.1"/>`;
147
+ continue;
148
+ }
149
+ const p = path(d, near);
150
+ if (mode === undefined)
151
+ svg += `<path d="${p}" fill="${q.fill}" stroke="${q.edge}" stroke-width=".3"/>`;
152
+ else if (mode === "dk")
153
+ svg += `<path d="${p}" fill="${mix(q.rgb, -0.55)}" fill-opacity=".5" stroke="none"/>`;
154
+ else if (mode === "lt")
155
+ svg += `<path d="${p}" fill="${mix(q.rgb, 0.55)}" fill-opacity=".55" stroke="none"/>`;
156
+ else if (mode === "ln")
157
+ svg += `<path d="${p}" stroke="${q.edge}" stroke-width=".5"/>`;
158
+ else if (mode === "gl")
159
+ svg += `<path d="${p}" fill="${mix(q.rgb, 0.35)}" fill-opacity=".3" stroke="${q.edge}" stroke-width=".45"/>`;
160
+ else if (mode === "fl")
161
+ svg += `<path d="${p}" fill="${mix(q.rgb, 0.08)}" stroke="${q.edge}" stroke-width=".3"/>`;
162
+ else if (mode === "rf")
163
+ svg += `<path d="${p}" fill="${q.fill}" fill-opacity=".8" stroke="${q.edge}" stroke-width=".3"/>`;
164
+ else
165
+ svg += `<path d="${p}" fill="#fff" fill-opacity=".6" stroke="none"/>`;
166
+ }
167
+ if (sign < 0 && !line && first[1] !== "")
168
+ svg += `<path d="${path(first[1], near)}" fill="#000" fill-opacity=".22" stroke="none"/>`;
169
+ if (cut.plate !== undefined && c.words !== "") {
170
+ const [x0, x1, y0, y1] = cut.plate;
171
+ const y = (y0 + y1) / 2;
172
+ const end = (px) => [CX + A + (px - CX) * cosP + near * sinP, y + dy + s * (B - (px - CX) * sinP + near * cosP)];
173
+ svg += sideText(c.words, x1 - x0, y1 - y0, engravingInk(line ? "line" : flat ? "color" : "metal", base.rgb), end(x0), end(x1), cosP);
174
+ }
175
+ return { z: B, svg };
176
+ }
177
+ /** The convex hull of some points (Andrew's monotone chain), counter-clockwise. */
178
+ function convexHull(points) {
179
+ const sorted = [...points].sort((a, b) => a[0] - b[0] || a[1] - b[1]);
180
+ const cross = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
181
+ const build = (list) => {
182
+ const out = [];
183
+ for (const p of list) {
184
+ while (out.length >= 2 && cross(out[out.length - 2], out[out.length - 1], p) <= 0)
185
+ out.pop();
186
+ out.push(p);
187
+ }
188
+ out.pop();
189
+ return out;
190
+ };
191
+ return [...build(sorted), ...build([...sorted].reverse())];
192
+ }
193
+ const cr = (a, b, c, d, t) => 0.5 * (2 * b + (-a + c) * t + (2 * a - 5 * b + 4 * c - d) * t * t + (-a + 3 * b - 3 * c + d) * t * t * t);
194
+ function drawRibbon(c, r) {
195
+ const { yaw, s, tone, line } = c;
196
+ const th = yaw * RAD;
197
+ const base = tone(r.tone);
198
+ const n = r.steps ?? 14;
199
+ const k = r.pts.length;
200
+ const samples = [];
201
+ for (let i = 0; i <= n; i++) {
202
+ const u = (i / n) * (k - 1);
203
+ const j = Math.min(k - 2, Math.floor(u));
204
+ const t = u - j;
205
+ const g = (q, m) => r.pts[Math.max(0, Math.min(k - 1, q))][m];
206
+ const mixed = (m) => cr(g(j - 1, m), g(j, m), g(j + 1, m), g(j + 2, m), t);
207
+ samples.push({ x: mixed(0), y: mixed(1), z: mixed(2), ph: g(j, 3) + (g(j + 1, 3) - g(j, 3)) * t, w: g(j, 4) + (g(j + 1, 4) - g(j, 4)) * t });
208
+ }
209
+ const turned = samples.map((p) => {
210
+ const phi = (p.ph + yaw) * RAD;
211
+ const px = p.x * Math.cos(th) + p.z * Math.sin(th);
212
+ const pz = p.z * Math.cos(th) - p.x * Math.sin(th);
213
+ return { px, py: p.y, pz, wx: p.w * Math.cos(phi), wz: -p.w * Math.sin(phi) };
214
+ });
215
+ const pt = (px, py, pz) => `${f(CX + px)} ${f(py + s * pz)}`;
216
+ if (line) {
217
+ let left = "";
218
+ let right = "";
219
+ turned.forEach((p, i) => {
220
+ left += `${i === 0 ? "M" : "L"}${pt(p.px - p.wx, p.py, p.pz - p.wz)}`;
221
+ right += `${i === 0 ? "M" : "L"}${pt(p.px + p.wx, p.py, p.pz + p.wz)}`;
222
+ });
223
+ const a = turned[0];
224
+ const b = turned[n];
225
+ const ends = `M${pt(a.px - a.wx, a.py, a.pz - a.wz)}L${pt(a.px + a.wx, a.py, a.pz + a.wz)}M${pt(b.px - b.wx, b.py, b.pz - b.wz)}L${pt(b.px + b.wx, b.py, b.pz + b.wz)}`;
226
+ return { z: turned.reduce((m, p) => m + p.pz, 0) / turned.length, svg: `<path d="${left}${right}${ends}"/>` };
227
+ }
228
+ const L = [-0.5, -0.6, 0.65];
229
+ const len = Math.hypot(L[0], L[1], L[2]);
230
+ const quads = [];
231
+ for (let i = 0; i < n; i++) {
232
+ const p = turned[i];
233
+ const q = turned[i + 1];
234
+ const tx = q.px - p.px;
235
+ const ty = q.py - p.py;
236
+ const tz = q.pz - p.pz;
237
+ const wx = p.wx + q.wx;
238
+ const wz = p.wz + q.wz;
239
+ // The normal of the strip: the tangent crossed with the width (which lies flat).
240
+ const nx = ty * wz;
241
+ const ny = tz * wx - tx * wz;
242
+ const nz = -ty * wx;
243
+ const nl = Math.hypot(nx, ny, nz) || 1;
244
+ const lambert = Math.abs((nx * L[0] + ny * L[1] + nz * L[2]) / (nl * len));
245
+ const bucket = Math.round(Math.max(-0.45, Math.min(0.5, (lambert - 0.45) * 1.1)) * 10) / 10;
246
+ const colour = mix(base.rgb, bucket);
247
+ quads.push({ z: (p.pz + q.pz) / 2, d: `M${pt(p.px - p.wx, p.py, p.pz - p.wz)}L${pt(p.px + p.wx, p.py, p.pz + p.wz)}L${pt(q.px + q.wx, q.py, q.pz + q.wz)}L${pt(q.px - q.wx, q.py, q.pz - q.wz)}Z`, colour });
248
+ }
249
+ quads.sort((a, b) => a.z - b.z);
250
+ let svg = "";
251
+ let run = "";
252
+ let colour = "";
253
+ for (const q of quads) {
254
+ if (q.colour !== colour && run !== "") {
255
+ svg += `<path d="${run}" fill="${colour}" stroke="${colour}" stroke-width=".3"/>`;
256
+ run = "";
257
+ }
258
+ colour = q.colour;
259
+ run += q.d;
260
+ }
261
+ if (run !== "")
262
+ svg += `<path d="${run}" fill="${colour}" stroke="${colour}" stroke-width=".3"/>`;
263
+ return { z: turned.reduce((m, p) => m + p.pz, 0) / turned.length, svg };
264
+ }
265
+ function drawBlock(c, b) {
266
+ const { yaw, s, tone, line } = c;
267
+ const th = yaw * RAD;
268
+ const cos = Math.cos(th);
269
+ const sin = Math.sin(th);
270
+ const t = tone(b.tone);
271
+ const up = tone(b.top ?? b.tone);
272
+ const [x0, x1] = b.x;
273
+ const [y0, y1] = b.y;
274
+ const [z0, z1] = b.z;
275
+ const at = (x, y, z) => `${f(CX + x * cos + z * sin)} ${f(y + s * (z * cos - x * sin))}`;
276
+ let svg = "";
277
+ // The four sides, each by the way it faces: the ones that look toward the eye, lit from the left.
278
+ const faces = [
279
+ [0, 1, [x0, z1], [x1, z1]],
280
+ [1, 0, [x1, z1], [x1, z0]],
281
+ [0, -1, [x1, z0], [x0, z0]],
282
+ [-1, 0, [x0, z0], [x0, z1]],
283
+ ];
284
+ for (const [nx, nz, a, e] of faces) {
285
+ const facing = nz * cos - nx * sin;
286
+ if (facing <= 0.01)
287
+ continue;
288
+ const lit = -(nx * cos + nz * sin);
289
+ svg += `<path d="M${at(a[0], y0, a[1])}L${at(e[0], y0, e[1])}L${at(e[0], y1, e[1])}L${at(a[0], y1, a[1])}Z" fill="${line ? "none" : mix(t.rgb, 0.05 + 0.27 * lit)}" stroke="${line ? "currentColor" : t.edge}"/>`;
290
+ }
291
+ if (s > 0.02)
292
+ svg += `<path d="M${at(x0, y0, z0)}L${at(x1, y0, z0)}L${at(x1, y0, z1)}L${at(x0, y0, z1)}Z" fill="${line ? "none" : mix(up.rgb, 0.3)}" stroke="${line ? "currentColor" : b.rim === undefined ? up.edge : mix(tone(b.rim).rgb, 0.2)}"${b.rim === undefined || line ? "" : ' stroke-width=".8"'}/>`;
293
+ return svg;
294
+ }
295
+ /** Draws the parts of a model: the ones that are `under` the bodies, and the rest, each group from far to near. */
296
+ export function LATHE_SCULPT(c, parts) {
297
+ const under = [];
298
+ const over = [];
299
+ const boxes = [];
300
+ for (const part of parts) {
301
+ if (part.kind === "box") {
302
+ boxes.push(drawBlock(c, part));
303
+ continue;
304
+ }
305
+ if (part.kind === "ribbon") {
306
+ (part.under === true ? under : over).push(drawRibbon(c, part));
307
+ continue;
308
+ }
309
+ const [x, z] = part.at ?? [0, 0];
310
+ const stands = [{ cut: part, ax: x, az: z, dy: part.y ?? 0 }, ...(part.copies ?? []).map(([cx, cz, cy]) => ({ cut: part, ax: x + cx, az: z + cz, dy: (part.y ?? 0) + (cy ?? 0) }))];
311
+ // The copies of a decal are one path (they lie on one face and cannot hide each other); those of a cut-out are drawn one by one, far to near.
312
+ for (const group of part.decal === true ? [stands] : stands.map((st) => [st])) {
313
+ const made = drawCut(c, group);
314
+ if (made !== undefined)
315
+ (part.under === true ? under : over).push(made);
316
+ }
317
+ }
318
+ const join = (list) => list.sort((a, b) => a.z - b.z).map((p) => p.svg).join("");
319
+ return { under: join(under), over: boxes.join("") + join(over) };
320
+ }
@@ -0,0 +1,27 @@
1
+ import type { Pt } from "./emblem.lathe.ts";
2
+ /** The tones the NBA's postseason trophies share: the ball's silver (and the frosted silver of the earlier conference balls), the black of the rings and feet, and the pale gold of the etched net. */
3
+ export declare const NBA_TONES: {
4
+ readonly silver: "#d3d8df";
5
+ readonly frost: "#e6e9ec";
6
+ readonly dark: "#16171b";
7
+ readonly etch: "#f0cf66";
8
+ };
9
+ /** The trumpet from its flange down to its foot, in the 64 unit box: the flange's rim is at 24.5 and the foot ends at 61.4. */
10
+ export declare const TRUMPET: readonly Pt[];
11
+ /** The ball's ring of the Western trophy: four bands that cross under the ball and rise round its lower half (the real ones are two rings). */
12
+ export declare const WEST_BAND: readonly [readonly [0, 38.2], readonly [4.2, 37.6], readonly [8.4, 36.2], readonly [12, 33.6], readonly [14.6, 30.4], readonly [15.5, 28.8]];
13
+ /** The Western base from the gold dome down: a black ring, a thin band, a flared black drum, a tall flared skirt and a black foot. In the earlier pair the thin band is black and the skirt polished silver. */
14
+ export declare const westBase: (earlier: boolean) => readonly Pt[];
15
+ /** The Eastern base from its plate down: a plate, three black tiers, a tall drum and a black foot. In the earlier pair the plate and the drum are polished silver. */
16
+ export declare const eastBase: (earlier: boolean) => readonly Pt[];
17
+ /** One of the three posts of the Eastern trophy and its stem and dish, as solids (the posts stand at 0, 120 and 240 degrees). */
18
+ export declare const eastTop: () => ({
19
+ at: readonly [12.2, number];
20
+ p: readonly [readonly [33.4, 0.9, 1], readonly [40.6, 0.9, 1]];
21
+ } | {
22
+ p: readonly [readonly [37.6, 1.2, 1], readonly [40.7, 1.4, 1]];
23
+ open?: undefined;
24
+ } | {
25
+ open: boolean;
26
+ p: readonly [readonly [33.2, 10, 1], readonly [34, 9.7], readonly [36, 7.2], readonly [37.6, 3.2], readonly [38.2, 1.3, 1]];
27
+ })[];
@@ -0,0 +1,34 @@
1
+ /** The tones the NBA's postseason trophies share: the ball's silver (and the frosted silver of the earlier conference balls), the black of the rings and feet, and the pale gold of the etched net. */
2
+ export const NBA_TONES = { silver: "#d3d8df", frost: "#e6e9ec", dark: "#16171b", etch: "#f0cf66" };
3
+ /** The trumpet from its flange down to its foot, in the 64 unit box: the flange's rim is at 24.5 and the foot ends at 61.4. */
4
+ export const TRUMPET = [
5
+ [23, 10.3, 1], [23.2, 11.1], [24.3, 11.25, 1], [24.6, 11.2], [24.6, 9.95],
6
+ [26, 9.7], [28, 9.15], [30, 8.65], [32, 8.15], [34, 7.65], [36, 7.1], [38, 6.55], [40, 6.05], [42, 5.6], [44, 5.15], [46, 4.8], [48, 4.5], [49.4, 4.4], [50.6, 4.55], [51.8, 5.15], [52.8, 6.3], [53.7, 7.6], [54.4, 8.4], [54.8, 8.6, 1],
7
+ [54.8, 8.9, 1, "dark"], [56.4, 8.9, 1, "dark"], [56.4, 9.35, 1], [59.8, 9.4, 1], [59.8, 9.5, 1, "dark"], [61.4, 9.5, 1, "dark"],
8
+ ];
9
+ /** The ball's ring of the Western trophy: four bands that cross under the ball and rise round its lower half (the real ones are two rings). */
10
+ export const WEST_BAND = [[0, 38.2], [4.2, 37.6], [8.4, 36.2], [12, 33.6], [14.6, 30.4], [15.5, 28.8]];
11
+ /** The Western base from the gold dome down: a black ring, a thin band, a flared black drum, a tall flared skirt and a black foot. In the earlier pair the thin band is black and the skirt polished silver. */
12
+ export const westBase = (earlier) => [
13
+ [38, 3, 1], [38.5, 8.6], [39.6, 11], [40, 11.4, 1],
14
+ [40, 13.4, 1, "dark"], [42.2, 13.8, 1, "dark"],
15
+ [42.2, 14.3, 1, earlier ? "dark" : "main"], [44.8, 14.6, 1, earlier ? "dark" : "main"],
16
+ [44.8, 15.3, 1, "dark"], [46, 16], [48, 17.6], [50.4, 20, 1, "dark"],
17
+ [50.4, 20.2, 1, earlier ? "silver" : "main"], [53, 21], [56, 22], [60.6, 23, 1, earlier ? "silver" : "main"],
18
+ [60.6, 23.3, 1, "dark"], [62.4, 23.4, 1, "dark"],
19
+ ];
20
+ /** The Eastern base from its plate down: a plate, three black tiers, a tall drum and a black foot. In the earlier pair the plate and the drum are polished silver. */
21
+ export const eastBase = (earlier) => {
22
+ const light = earlier ? "silver" : "main";
23
+ return [
24
+ [40.6, 10.2, 1, light], [42.2, 10.4, 1, light],
25
+ [42.2, 12.2, 1, "dark"], [43.8, 12.2, 1, "dark"], [43.8, 14, 1, "dark"], [45.4, 14, 1, "dark"], [45.4, 15.8, 1, "dark"], [47, 15.8, 1, "dark"],
26
+ [47, 19.2, 1, light], [58.8, 19.5, 1, light],
27
+ [58.8, 20, 1, "dark"], [60.4, 20.6, 1, "dark"], [62.5, 20.9, 1, "dark"],
28
+ ];
29
+ };
30
+ /** One of the three posts of the Eastern trophy and its stem and dish, as solids (the posts stand at 0, 120 and 240 degrees). */
31
+ export const eastTop = () => {
32
+ const post = (deg) => ({ at: [12.2, deg], p: [[33.4, 0.9, 1], [40.6, 0.9, 1]] });
33
+ return [{ p: [[37.6, 1.2, 1], [40.7, 1.4, 1]] }, { open: true, p: [[33.2, 10, 1], [34, 9.7], [36, 7.2], [37.6, 3.2], [38.2, 1.3, 1]] }, post(0), post(120), post(240)];
34
+ };
@@ -1,2 +1,2 @@
1
- /** The Bill Russell NBA Finals Most Valuable Player Trophy: a gold basketball on a flared net goblet over a stepped round foot. */
1
+ /** The Bill Russell NBA Finals Most Valuable Player Trophy: a gold basketball with silver seams in a net-etched gold trumpet over a stepped round foot. */
2
2
  export declare const TROPHY_BILL_RUSSELL_TROPHY: import("../emblem.core.ts").Emblem<import("../emblem.lathe.ts").LatheModel>;