@logomarks/core 0.1.9 → 0.2.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 +49 -0
- package/README.md +10 -1
- package/package.json +59 -4
- package/src/batch/args.d.ts +18 -0
- package/src/batch/args.js +44 -0
- package/src/batch/index.d.ts +7 -0
- package/src/batch/index.js +10 -0
- package/src/batch/run.d.ts +53 -0
- package/src/batch/run.js +92 -0
- package/src/batch/slug.d.ts +2 -0
- package/src/batch/slug.js +12 -0
- package/src/batch/source.d.ts +12 -0
- package/src/batch/source.js +23 -0
- package/src/fit/index.d.ts +4 -0
- package/src/fit/index.js +29 -5
- package/src/font/glyphs.d.ts +31 -0
- package/src/font/glyphs.js +228 -0
- package/src/font/index.d.ts +10 -0
- package/src/font/index.js +10 -0
- package/src/font/measure.d.ts +16 -0
- package/src/font/measure.js +201 -0
- package/src/font/moves.d.ts +42 -0
- package/src/font/moves.js +263 -0
- package/src/forms/discs.d.ts +20 -0
- package/src/forms/discs.js +114 -0
- package/src/forms/emblems.d.ts +22 -0
- package/src/forms/emblems.js +109 -0
- package/src/forms/flow.d.ts +21 -0
- package/src/forms/flow.js +80 -0
- package/src/forms/folds.d.ts +18 -0
- package/src/forms/folds.js +147 -0
- package/src/forms/geo.d.ts +46 -0
- package/src/forms/geo.js +233 -0
- package/src/forms/hooks.d.ts +40 -0
- package/src/forms/hooks.js +204 -0
- package/src/forms/index.d.ts +22 -0
- package/src/forms/index.js +24 -0
- package/src/forms/jog.d.ts +33 -0
- package/src/forms/jog.js +162 -0
- package/src/forms/lines.d.ts +32 -0
- package/src/forms/lines.js +233 -0
- package/src/forms/ref.d.ts +41 -0
- package/src/forms/ref.js +246 -0
- package/src/forms/shatter.d.ts +40 -0
- package/src/forms/shatter.js +341 -0
- package/src/forms/slats.d.ts +35 -0
- package/src/forms/slats.js +78 -0
- package/src/forms/swash.d.ts +23 -0
- package/src/forms/swash.js +74 -0
- package/src/forms/tech.d.ts +40 -0
- package/src/forms/tech.js +293 -0
- package/src/forms/tracks.d.ts +36 -0
- package/src/forms/tracks.js +121 -0
- package/src/geometry/index.d.ts +1 -0
- package/src/geometry/index.js +1 -0
- package/src/geometry/mitre.d.ts +8 -0
- package/src/geometry/mitre.js +64 -0
- package/src/geometry/modular.d.ts +1 -1
- package/src/index.js +2 -1
- package/src/iso/index.d.ts +5 -0
- package/src/iso/index.js +13 -0
- package/src/iso/objects.d.ts +168 -0
- package/src/iso/objects.js +1042 -0
- package/src/iso/planes.d.ts +27 -0
- package/src/iso/planes.js +138 -0
- package/src/iso/voxels.d.ts +43 -0
- package/src/iso/voxels.js +255 -0
- package/src/letters/blackletter.d.ts +31 -0
- package/src/letters/blackletter.js +199 -0
- package/src/letters/dotstroke.d.ts +16 -0
- package/src/letters/dotstroke.js +77 -0
- package/src/letters/echo.d.ts +42 -0
- package/src/letters/echo.js +246 -0
- package/src/letters/frame.d.ts +6 -0
- package/src/letters/frame.js +14 -0
- package/src/letters/index.d.ts +16 -0
- package/src/letters/index.js +18 -0
- package/src/letters/inktrap.d.ts +66 -0
- package/src/letters/inktrap.js +244 -0
- package/src/letters/mono.d.ts +145 -0
- package/src/letters/mono.js +684 -0
- package/src/letters/pillar.d.ts +31 -0
- package/src/letters/pillar.js +257 -0
- package/src/letters/serif.d.ts +109 -0
- package/src/letters/serif.js +666 -0
- package/src/letters/stair.d.ts +8 -0
- package/src/letters/stair.js +43 -0
- package/src/letters/stem.d.ts +29 -0
- package/src/letters/stem.js +205 -0
- package/src/objects/animals.d.ts +105 -0
- package/src/objects/animals.js +469 -0
- package/src/objects/bio.d.ts +14 -0
- package/src/objects/bio.js +76 -0
- package/src/objects/devglyphs.d.ts +34 -0
- package/src/objects/devglyphs.js +226 -0
- package/src/objects/devices.d.ts +31 -0
- package/src/objects/devices.js +114 -0
- package/src/objects/faces.d.ts +94 -0
- package/src/objects/faces.js +344 -0
- package/src/objects/food.d.ts +12 -0
- package/src/objects/food.js +74 -0
- package/src/objects/index.d.ts +13 -0
- package/src/objects/index.js +18 -0
- package/src/objects/rosettes.d.ts +14 -0
- package/src/objects/rosettes.js +64 -0
- package/src/objects/sport.d.ts +8 -0
- package/src/objects/sport.js +37 -0
- package/src/objects/weapons.d.ts +31 -0
- package/src/objects/weapons.js +179 -0
- package/src/parts/index.d.ts +2 -0
- package/src/parts/index.js +2 -0
- package/src/parts/pixels.d.ts +11 -0
- package/src/parts/pixels.js +74 -0
- package/src/parts/run.js +4 -2
- package/src/parts/span.d.ts +13 -0
- package/src/parts/span.js +100 -0
- package/src/pixels/dither.d.ts +78 -0
- package/src/pixels/dither.js +399 -0
- package/src/pixels/emote.d.ts +6 -0
- package/src/pixels/emote.js +76 -0
- package/src/pixels/index.d.ts +7 -0
- package/src/pixels/index.js +8 -0
- package/src/sdf/fields.js +135 -0
- package/src/sdf/gap.js +42 -0
- package/src/sdf/index.d.ts +58 -0
- package/src/sdf/index.js +14 -0
- package/src/sdf/ops.js +102 -0
- package/src/sdf/trace.js +90 -0
- package/src/sdf/y2k.d.ts +12 -0
- package/src/sdf/y2k.js +80 -0
- package/src/skill/bundle.js +1 -1
- package/src/util/vec.d.ts +14 -0
- package/src/util/vec.js +61 -0
package/src/sdf/gap.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The house gap in fields: cuts that stay exactly g wide, and one shape laid over another.
|
|
3
|
+
*/
|
|
4
|
+
import { segT } from './fields.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A band g wide cut along each line, its edges left crisp.
|
|
8
|
+
*
|
|
9
|
+
* @param {import('./index.js').Field} f The field.
|
|
10
|
+
* @param {readonly (readonly (readonly number[])[])[]} lines Segments or polylines, each a list of points.
|
|
11
|
+
* @param {number} g The gap.
|
|
12
|
+
* @returns {import('./index.js').Field} The field, cut.
|
|
13
|
+
*/
|
|
14
|
+
export const gapCut = (f, lines, g) => (x, y) => {
|
|
15
|
+
let m = Infinity;
|
|
16
|
+
for (const L of lines) for (let i = 1; i < L.length; i++) m = Math.min(m, segT(L[i - 1], L[i], x, y)[0]);
|
|
17
|
+
return Math.max(f(x, y), g / 2 - m);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* One field laid over another, cut free of it by the gap. `front` must be a true distance near its edge.
|
|
22
|
+
*
|
|
23
|
+
* @param {import('./index.js').Field} back The field behind.
|
|
24
|
+
* @param {import('./index.js').Field} front The field in front.
|
|
25
|
+
* @param {number} g The gap.
|
|
26
|
+
* @returns {import('./index.js').Field} Both, front over back.
|
|
27
|
+
*/
|
|
28
|
+
export const fover = (back, front, g) => (x, y) => Math.min(front(x, y), Math.max(back(x, y), g - front(x, y)));
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Two fields linked: a over b where `aOver` holds (a half-plane, say), b over a elsewhere.
|
|
32
|
+
*
|
|
33
|
+
* @param {import('./index.js').Field} a One field.
|
|
34
|
+
* @param {import('./index.js').Field} b The other.
|
|
35
|
+
* @param {number} g The gap.
|
|
36
|
+
* @param {(x: number, y: number) => boolean} aOver Where a goes over.
|
|
37
|
+
* @returns {import('./index.js').Field} The link.
|
|
38
|
+
*/
|
|
39
|
+
export const flink = (a, b, g, aOver) => {
|
|
40
|
+
const ab = fover(b, a, g), ba = fover(a, b, g);
|
|
41
|
+
return (x, y) => (aOver(x, y) ? ab(x, y) : ba(x, y));
|
|
42
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Shape } from '../geometry/types.js';
|
|
2
|
+
|
|
3
|
+
/** A signed distance field: (x, y) → distance, negative inside, positive outside. */
|
|
4
|
+
export type Field = (x: number, y: number) => number;
|
|
5
|
+
/** A point. */
|
|
6
|
+
export type Pt = readonly number[];
|
|
7
|
+
|
|
8
|
+
/** A disc. */
|
|
9
|
+
export function sdCircle(cx: number, cy: number, r: number): Field;
|
|
10
|
+
/** The distance to the segment a–b, and t (0 at a, 1 at b) where the nearest point lies. */
|
|
11
|
+
export function segT(a: Pt, b: Pt, x: number, y: number): [number, number];
|
|
12
|
+
/** The segment a–b with round ends, w wide. */
|
|
13
|
+
export function sdCapsule(a: Pt, b: Pt, w: number): Field;
|
|
14
|
+
/** A round-ended stroke along a polyline, w wide (or w(t) along the run). */
|
|
15
|
+
export function sdPath(pts: readonly Pt[], w: number | ((t: number) => number)): Field;
|
|
16
|
+
/** Points along an arc from a0 to a1 degrees (y down), n + 1 of them (n = 48). */
|
|
17
|
+
export function arcPts(cx: number, cy: number, r: number, a0: number, a1: number, n?: number): [number, number][];
|
|
18
|
+
/** A rounded box centred on (cx, cy). */
|
|
19
|
+
export function sdBox(cx: number, cy: number, w: number, h: number, r?: number): Field;
|
|
20
|
+
/** An ellipse turned deg, as a scaled disc (approximate). */
|
|
21
|
+
export function sdEllipse(cx: number, cy: number, rx: number, ry: number, deg?: number): Field;
|
|
22
|
+
/** A ring of centre-line radius r and width w. */
|
|
23
|
+
export function sdRing(cx: number, cy: number, r: number, w: number): Field;
|
|
24
|
+
/** A polygon, exact. */
|
|
25
|
+
export function sdPoly(pts: readonly Pt[]): Field;
|
|
26
|
+
|
|
27
|
+
/** A band w wide along a field's zero line. */
|
|
28
|
+
export function shell(f: Field, w: number): Field;
|
|
29
|
+
/** A field turned deg about (cx, cy) (256, 256). */
|
|
30
|
+
export function turn(f: Field, deg: number, cx?: number, cy?: number): Field;
|
|
31
|
+
/** A field grown (d > 0) or shrunk (d < 0). */
|
|
32
|
+
export function offset(f: Field, d: number): Field;
|
|
33
|
+
/** The polynomial smooth minimum of two distances, blend k. */
|
|
34
|
+
export function smin2(a: number, b: number, k: number): number;
|
|
35
|
+
/** A smooth union, blend k. */
|
|
36
|
+
export function smin(k: number, ...fs: Field[]): Field;
|
|
37
|
+
/** A smooth intersection, blend k. */
|
|
38
|
+
export function smax(k: number, ...fs: Field[]): Field;
|
|
39
|
+
/** a minus b, the cut's edges softened by k. */
|
|
40
|
+
export function ssub(k: number, a: Field, b: Field): Field;
|
|
41
|
+
/** A sharp union. */
|
|
42
|
+
export function fmin(...fs: Field[]): Field;
|
|
43
|
+
/** A sharp intersection. */
|
|
44
|
+
export function fmax(...fs: Field[]): Field;
|
|
45
|
+
/** a minus b, sharp. */
|
|
46
|
+
export function fsub(a: Field, b: Field): Field;
|
|
47
|
+
|
|
48
|
+
/** A band g wide cut along each line (a list of points), edges crisp. */
|
|
49
|
+
export function gapCut(f: Field, lines: readonly (readonly Pt[])[], g: number): Field;
|
|
50
|
+
/** front laid over back, cut free of it by the gap. */
|
|
51
|
+
export function fover(back: Field, front: Field, g: number): Field;
|
|
52
|
+
/** a over b where aOver holds, b over a elsewhere. */
|
|
53
|
+
export function flink(a: Field, b: Field, g: number, aOver: (x: number, y: number) => boolean): Field;
|
|
54
|
+
|
|
55
|
+
/** A field's zero contour traced into a kit shape (marching squares, step h = 1, over −64…576). */
|
|
56
|
+
export function shapeOf(f: Field, area?: { h?: number; x0?: number; y0?: number; x1?: number; y1?: number }): Shape;
|
|
57
|
+
|
|
58
|
+
export { pinch, ballArm, node, blob, stadium } from './y2k.js';
|
package/src/sdf/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @logomarks/core/sdf
|
|
3
|
+
*
|
|
4
|
+
* Shapes drawn as signed distance fields and traced to polygons, so unions melt into each other with real fillets
|
|
5
|
+
* (a smooth minimum) instead of meeting at a crease. A field is (x, y) → distance, negative inside, on the 512 canvas.
|
|
6
|
+
* Build fields, combine them (smin, smax, ssub, fmin, fmax, fsub), cut the house gap (gapCut, fover, flink), then
|
|
7
|
+
* shapeOf() traces the result into a kit shape for the usual booleans and fit.
|
|
8
|
+
* The Y2K parts (pinch, ballArm, node, blob, stadium) are built from them.
|
|
9
|
+
*/
|
|
10
|
+
export { sdCircle, segT, sdCapsule, sdPath, arcPts, sdBox, sdEllipse, sdRing, sdPoly } from './fields.js';
|
|
11
|
+
export { shell, turn, offset, smin2, smin, smax, ssub, fmin, fmax, fsub } from './ops.js';
|
|
12
|
+
export { gapCut, fover, flink } from './gap.js';
|
|
13
|
+
export { shapeOf } from './trace.js';
|
|
14
|
+
export { pinch, ballArm, node, blob, stadium } from './y2k.js';
|
package/src/sdf/ops.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Combining and changing fields: smooth and sharp unions, intersections and cuts, turns, offsets and outlines.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A band along a field's zero line: any outline turned into a ring of width w.
|
|
7
|
+
*
|
|
8
|
+
* @param {import('./index.js').Field} f The field.
|
|
9
|
+
* @param {number} w The band's width.
|
|
10
|
+
* @returns {import('./index.js').Field} The band.
|
|
11
|
+
*/
|
|
12
|
+
export const shell = (f, w) => (x, y) => Math.abs(f(x, y)) - w / 2;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A field turned about a point.
|
|
16
|
+
*
|
|
17
|
+
* @param {import('./index.js').Field} f The field.
|
|
18
|
+
* @param {number} deg The turn, in degrees (clockwise on the y-down canvas).
|
|
19
|
+
* @param {number} [cx] The pivot (256).
|
|
20
|
+
* @param {number} [cy] The pivot (256).
|
|
21
|
+
* @returns {import('./index.js').Field} The turned field.
|
|
22
|
+
*/
|
|
23
|
+
export const turn = (f, deg, cx = 256, cy = 256) => {
|
|
24
|
+
const a = (-deg * Math.PI) / 180, c = Math.cos(a), s = Math.sin(a);
|
|
25
|
+
return (x, y) => f(cx + (x - cx) * c - (y - cy) * s, cy + (x - cx) * s + (y - cy) * c);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A field grown (d > 0) or shrunk (d < 0). Shrinking a polygon by k and growing it back rounds its convex corners to k.
|
|
30
|
+
*
|
|
31
|
+
* @param {import('./index.js').Field} f The field.
|
|
32
|
+
* @param {number} d How far.
|
|
33
|
+
* @returns {import('./index.js').Field} The field, offset.
|
|
34
|
+
*/
|
|
35
|
+
export const offset = (f, d) => (x, y) => f(x, y) - d;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The polynomial smooth minimum of two distances: a union whose seam melts into a fillet about k wide.
|
|
39
|
+
*
|
|
40
|
+
* @param {number} a One distance.
|
|
41
|
+
* @param {number} b The other.
|
|
42
|
+
* @param {number} k The blend (0 for a sharp minimum).
|
|
43
|
+
* @returns {number} The blended distance.
|
|
44
|
+
*/
|
|
45
|
+
export const smin2 = (a, b, k) => {
|
|
46
|
+
if (k <= 0) return Math.min(a, b);
|
|
47
|
+
const h = Math.max(k - Math.abs(a - b), 0) / k;
|
|
48
|
+
return Math.min(a, b) - (h * h * k) / 4;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A smooth union.
|
|
53
|
+
*
|
|
54
|
+
* @param {number} k The blend.
|
|
55
|
+
* @param {...import('./index.js').Field} fs The fields.
|
|
56
|
+
* @returns {import('./index.js').Field} Their union, seams filleted.
|
|
57
|
+
*/
|
|
58
|
+
export const smin = (k, ...fs) => (x, y) => fs.reduce((m, f, i) => (i ? smin2(m, f(x, y), k) : f(x, y)), 0);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A smooth intersection.
|
|
62
|
+
*
|
|
63
|
+
* @param {number} k The blend.
|
|
64
|
+
* @param {...import('./index.js').Field} fs The fields.
|
|
65
|
+
* @returns {import('./index.js').Field} Their intersection, edges softened.
|
|
66
|
+
*/
|
|
67
|
+
export const smax = (k, ...fs) => (x, y) => -fs.reduce((m, f, i) => (i ? smin2(m, -f(x, y), k) : -f(x, y)), 0);
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* One field cut by another, the cut's edges softened.
|
|
71
|
+
*
|
|
72
|
+
* @param {number} k The blend.
|
|
73
|
+
* @param {import('./index.js').Field} a The field.
|
|
74
|
+
* @param {import('./index.js').Field} b What is cut out of it.
|
|
75
|
+
* @returns {import('./index.js').Field} a minus b.
|
|
76
|
+
*/
|
|
77
|
+
export const ssub = (k, a, b) => (x, y) => -smin2(-a(x, y), b(x, y), k);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A sharp union.
|
|
81
|
+
*
|
|
82
|
+
* @param {...import('./index.js').Field} fs The fields.
|
|
83
|
+
* @returns {import('./index.js').Field} Their union.
|
|
84
|
+
*/
|
|
85
|
+
export const fmin = (...fs) => (x, y) => Math.min(...fs.map((f) => f(x, y)));
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A sharp intersection.
|
|
89
|
+
*
|
|
90
|
+
* @param {...import('./index.js').Field} fs The fields.
|
|
91
|
+
* @returns {import('./index.js').Field} Their intersection.
|
|
92
|
+
*/
|
|
93
|
+
export const fmax = (...fs) => (x, y) => Math.max(...fs.map((f) => f(x, y)));
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* A sharp cut.
|
|
97
|
+
*
|
|
98
|
+
* @param {import('./index.js').Field} a The field.
|
|
99
|
+
* @param {import('./index.js').Field} b What is cut out of it.
|
|
100
|
+
* @returns {import('./index.js').Field} a minus b.
|
|
101
|
+
*/
|
|
102
|
+
export const fsub = (a, b) => (x, y) => Math.max(a(x, y), -b(x, y));
|
package/src/sdf/trace.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracing a field into a kit shape: marching squares on its zero contour, so the kit's booleans work on the result.
|
|
3
|
+
*/
|
|
4
|
+
import { X } from '../geometry/index.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A closed ring with the points that lie (within tol) on the line through their neighbours dropped.
|
|
8
|
+
*
|
|
9
|
+
* @param {number[][]} r The ring.
|
|
10
|
+
* @param {number} [tol] How far off the line a point must be to stay (0.04).
|
|
11
|
+
* @returns {number[][]} The ring, thinned.
|
|
12
|
+
*/
|
|
13
|
+
const thin = (r, tol = 0.04) => {
|
|
14
|
+
const out = [];
|
|
15
|
+
for (let i = 0; i < r.length; i++) {
|
|
16
|
+
const a = out.length ? out[out.length - 1] : r[r.length - 1], p = r[i], b = r[(i + 1) % r.length];
|
|
17
|
+
const L = Math.hypot(b[0] - a[0], b[1] - a[1]) || 1e-9, d = Math.abs((b[0] - a[0]) * (a[1] - p[1]) - (a[0] - p[0]) * (b[1] - a[1])) / L;
|
|
18
|
+
if (d > tol) out.push(p);
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A field's zero contour (inside < 0) as a kit shape: marching squares on a grid of step h with linear interpolation,
|
|
25
|
+
* saddles split by the cell's mean, rings XORed so holes come out as holes.
|
|
26
|
+
*
|
|
27
|
+
* @param {import('./index.js').Field} f The field.
|
|
28
|
+
* @param {{ h?: number, x0?: number, y0?: number, x1?: number, y1?: number }} [area] The grid step (1) and the area
|
|
29
|
+
* traced (−64 to 576 each way).
|
|
30
|
+
* @returns {import('../geometry/types.js').Shape} The shape (empty when the field is nowhere inside).
|
|
31
|
+
*/
|
|
32
|
+
export function shapeOf(f, { h = 1, x0 = -64, y0 = -64, x1 = 576, y1 = 576 } = {}) {
|
|
33
|
+
const nx = Math.ceil((x1 - x0) / h) + 1, ny = Math.ceil((y1 - y0) / h) + 1, v = new Float64Array(nx * ny);
|
|
34
|
+
for (let j = 0; j < ny; j++) for (let i = 0; i < nx; i++) {
|
|
35
|
+
let d = f(x0 + i * h, y0 + j * h);
|
|
36
|
+
if (d === 0) d = 1e-9;
|
|
37
|
+
v[j * nx + i] = d;
|
|
38
|
+
}
|
|
39
|
+
const V = (/** @type {number} */ i, /** @type {number} */ j) => v[j * nx + i];
|
|
40
|
+
/** @type {Map<string, number[]>} */ const P = new Map();
|
|
41
|
+
/** @type {Map<string, string[]>} */ const adj = new Map();
|
|
42
|
+
// an edge point, keyed by its grid edge: 'h' runs from (i, j) to (i+1, j), 'v' from (i, j) to (i, j+1)
|
|
43
|
+
const pt = (/** @type {'h' | 'v'} */ kind, /** @type {number} */ i, /** @type {number} */ j) => {
|
|
44
|
+
const key = `${kind}${i},${j}`;
|
|
45
|
+
if (!P.has(key)) {
|
|
46
|
+
const a = V(i, j), b = kind === 'h' ? V(i + 1, j) : V(i, j + 1), t = a / (a - b);
|
|
47
|
+
P.set(key, kind === 'h' ? [x0 + (i + t) * h, y0 + j * h] : [x0 + i * h, y0 + (j + t) * h]);
|
|
48
|
+
}
|
|
49
|
+
return key;
|
|
50
|
+
};
|
|
51
|
+
const link = (/** @type {string} */ a, /** @type {string} */ b) => {
|
|
52
|
+
(adj.get(a) || adj.set(a, []).get(a))?.push(b);
|
|
53
|
+
(adj.get(b) || adj.set(b, []).get(b))?.push(a);
|
|
54
|
+
};
|
|
55
|
+
for (let j = 0; j < ny - 1; j++) for (let i = 0; i < nx - 1; i++) {
|
|
56
|
+
const tl = V(i, j) < 0, tr = V(i + 1, j) < 0, br = V(i + 1, j + 1) < 0, bl = V(i, j + 1) < 0;
|
|
57
|
+
const c = +tl * 8 + +tr * 4 + +br * 2 + +bl;
|
|
58
|
+
if (c === 0 || c === 15) continue;
|
|
59
|
+
const T = () => pt('h', i, j), B = () => pt('h', i, j + 1), L = () => pt('v', i, j), R = () => pt('v', i + 1, j);
|
|
60
|
+
const mid = (V(i, j) + V(i + 1, j) + V(i + 1, j + 1) + V(i, j + 1)) / 4 < 0;
|
|
61
|
+
switch (c) {
|
|
62
|
+
case 1: case 14: link(L(), B()); break;
|
|
63
|
+
case 2: case 13: link(B(), R()); break;
|
|
64
|
+
case 3: case 12: link(L(), R()); break;
|
|
65
|
+
case 4: case 11: link(T(), R()); break;
|
|
66
|
+
case 6: case 9: link(T(), B()); break;
|
|
67
|
+
case 7: case 8: link(L(), T()); break;
|
|
68
|
+
case 5: if (mid) { link(L(), T()); link(B(), R()); } else { link(T(), R()); link(L(), B()); } break;
|
|
69
|
+
case 10: if (mid) { link(T(), R()); link(L(), B()); } else { link(L(), T()); link(B(), R()); } break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const seen = new Set(), rings = [];
|
|
73
|
+
for (const start of adj.keys()) {
|
|
74
|
+
if (seen.has(start)) continue;
|
|
75
|
+
const ring = [];
|
|
76
|
+
/** @type {string | null} */ let prev = null;
|
|
77
|
+
/** @type {string | undefined} */ let cur = start;
|
|
78
|
+
while (cur && !seen.has(cur)) {
|
|
79
|
+
seen.add(cur);
|
|
80
|
+
ring.push(/** @type {number[]} */ (P.get(cur)));
|
|
81
|
+
const nb = /** @type {string[]} */ (adj.get(cur));
|
|
82
|
+
const nxt = nb[0] === prev ? nb[1] : nb[0];
|
|
83
|
+
prev = cur;
|
|
84
|
+
cur = nxt;
|
|
85
|
+
}
|
|
86
|
+
const r = thin(ring);
|
|
87
|
+
if (r.length >= 3) rings.push(r);
|
|
88
|
+
}
|
|
89
|
+
return rings.length ? X(...rings.map((r) => [[...r, r[0]]])) : [];
|
|
90
|
+
}
|
package/src/sdf/y2k.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Field, Pt } from './index.js';
|
|
2
|
+
|
|
3
|
+
/** A bar from a to b, w wide at its ends, waisted n times to `waist` wide (a cosine profile). */
|
|
4
|
+
export function pinch(a: Pt, b: Pt, w: number, waist: number, n?: number): Field;
|
|
5
|
+
/** An arm from the hub c at deg: a stem ending in a ball of radius r, melted with blend k. */
|
|
6
|
+
export function ballArm(c: Pt, deg: number, len: number, stem: number, r: number, k?: number): Field;
|
|
7
|
+
/** n ball-ended arms from (cx, cy) melted into a hub. */
|
|
8
|
+
export function node(cx: number, cy: number, n: number, len: number, stem: number, r: number, o?: { deg0?: number; hub?: number; k?: number; kArm?: number }): Field;
|
|
9
|
+
/** A smooth blob: discs [cx, cy, r] melted with blend k. */
|
|
10
|
+
export function blob(discs: readonly (readonly number[])[], k?: number): Field;
|
|
11
|
+
/** A stadium (a pill) centred on (cx, cy), w × h. */
|
|
12
|
+
export function stadium(cx: number, cy: number, w: number, h: number): Field;
|
package/src/sdf/y2k.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Y2K family's parts as fields: a bar waisted along its length, an arm ending in a ball, a node of ball-ended arms
|
|
3
|
+
* melted into a hub, a smooth blob of discs and a stadium frame. Combine and trace them like any other field.
|
|
4
|
+
* From the Logomarks tools (y2kparts.mjs, batch 232, "Bold · Y2K").
|
|
5
|
+
*/
|
|
6
|
+
import { sdCircle, segT, sdCapsule } from './fields.js';
|
|
7
|
+
import { smin } from './ops.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A bar from a to b, w wide at its ends, waisted n times along its length to `waist` wide (n = 1 an hourglass bar, 2
|
|
11
|
+
* two beads' necks…; a waist wider than w swells instead). The waist follows a cosine, so the outline has no corners;
|
|
12
|
+
* the ends are round.
|
|
13
|
+
*
|
|
14
|
+
* @param {readonly number[]} a One end.
|
|
15
|
+
* @param {readonly number[]} b The other.
|
|
16
|
+
* @param {number} w The width at the ends.
|
|
17
|
+
* @param {number} waist The width at each waist.
|
|
18
|
+
* @param {number} [n=1] Waists along it.
|
|
19
|
+
* @returns {import('./index.js').Field} The bar.
|
|
20
|
+
*/
|
|
21
|
+
export const pinch = (a, b, w, waist, n = 1) => (x, y) => {
|
|
22
|
+
const [d, t] = segT(a, b, x, y);
|
|
23
|
+
return d - (w - ((w - waist) * (1 - Math.cos(2 * Math.PI * n * t))) / 2) / 2;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* An arm from the hub c at `deg`, `len` to the ball's centre: a stem `stem` wide ending in a ball of radius r, melted
|
|
28
|
+
* with blend k.
|
|
29
|
+
*
|
|
30
|
+
* @param {readonly number[]} c The hub.
|
|
31
|
+
* @param {number} deg The arm's direction (degrees, y down).
|
|
32
|
+
* @param {number} len Hub to ball centre.
|
|
33
|
+
* @param {number} stem The stem's width.
|
|
34
|
+
* @param {number} r The ball's radius.
|
|
35
|
+
* @param {number} [k=30] The blend.
|
|
36
|
+
* @returns {import('./index.js').Field} The arm.
|
|
37
|
+
*/
|
|
38
|
+
export const ballArm = (c, deg, len, stem, r, k = 30) => {
|
|
39
|
+
const a = (deg * Math.PI) / 180, e = [c[0] + len * Math.cos(a), c[1] + len * Math.sin(a)];
|
|
40
|
+
return smin(k, sdCapsule(c, e, stem), sdCircle(e[0], e[1], r));
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A node: n ball-ended arms from (cx, cy), the first at deg0, melted into a hub of radius `hub` (0: none).
|
|
45
|
+
*
|
|
46
|
+
* @param {number} cx The hub's centre.
|
|
47
|
+
* @param {number} cy The hub's centre.
|
|
48
|
+
* @param {number} n Arms.
|
|
49
|
+
* @param {number} len Hub to ball centre.
|
|
50
|
+
* @param {number} stem The stems' width.
|
|
51
|
+
* @param {number} r The balls' radius.
|
|
52
|
+
* @param {{ deg0?: number, hub?: number, k?: number, kArm?: number }} [o] The first arm's direction (−90), the hub's
|
|
53
|
+
* radius (0), the hub's blend (40) and each arm's (30).
|
|
54
|
+
* @returns {import('./index.js').Field} The node.
|
|
55
|
+
*/
|
|
56
|
+
export const node = (cx, cy, n, len, stem, r, { deg0 = -90, hub = 0, k = 40, kArm = 30 } = {}) =>
|
|
57
|
+
smin(k, ...(hub ? [sdCircle(cx, cy, hub)] : []), ...[...Array(n)].map((_, i) => ballArm([cx, cy], deg0 + (i * 360) / n, len, stem, r, kArm)));
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A smooth blob: discs [cx, cy, r] melted with blend k.
|
|
61
|
+
*
|
|
62
|
+
* @param {readonly (readonly number[])[]} discs The discs.
|
|
63
|
+
* @param {number} [k=90] The blend.
|
|
64
|
+
* @returns {import('./index.js').Field} The blob.
|
|
65
|
+
*/
|
|
66
|
+
export const blob = (discs, k = 90) => smin(k, ...discs.map(([cx, cy, r]) => sdCircle(cx, cy, r)));
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A stadium (a pill) centred on (cx, cy), w × h, as a field.
|
|
70
|
+
*
|
|
71
|
+
* @param {number} cx The centre.
|
|
72
|
+
* @param {number} cy The centre.
|
|
73
|
+
* @param {number} w The width.
|
|
74
|
+
* @param {number} h The height.
|
|
75
|
+
* @returns {import('./index.js').Field} The stadium.
|
|
76
|
+
*/
|
|
77
|
+
export const stadium = (cx, cy, w, h) => {
|
|
78
|
+
const r = Math.min(w, h) / 2;
|
|
79
|
+
return w >= h ? sdCapsule([cx - w / 2 + r, cy], [cx + w / 2 - r, cy], h) : sdCapsule([cx, cy - h / 2 + r], [cx, cy + h / 2 - r], w);
|
|
80
|
+
};
|
package/src/skill/bundle.js
CHANGED
|
@@ -14,7 +14,7 @@ const SRC = new URL('../', import.meta.url);
|
|
|
14
14
|
|
|
15
15
|
/** The geometry kit's modules, in dependency order. */
|
|
16
16
|
const KIT = ['geometry/core.js', 'geometry/booleans.js', 'geometry/polygon.js', 'geometry/random.js', 'geometry/shapes.js', 'geometry/stroke.js',
|
|
17
|
-
'geometry/path.js', 'geometry/offset.js', 'geometry/iso.js', 'geometry/modular.js', 'geometry/badges.js', 'geometry/letters.js',
|
|
17
|
+
'geometry/path.js', 'geometry/offset.js', 'geometry/mitre.js', 'geometry/iso.js', 'geometry/modular.js', 'geometry/badges.js', 'geometry/letters.js',
|
|
18
18
|
'svg/path.js', 'svg/shapes.js', 'svg/rings.js', 'svg/nonzero.js', 'svg/shape.js', 'geometry/glitch.js', 'geometry/needle.js'];
|
|
19
19
|
|
|
20
20
|
/** The fit's modules, in dependency order. */
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Point, Point3, Polygon } from '../geometry/types.js';
|
|
2
|
+
|
|
3
|
+
/** a − b, in 2D. */
|
|
4
|
+
export function sub(a: readonly number[], b: readonly number[]): Point;
|
|
5
|
+
/** a + b, in 2D. */
|
|
6
|
+
export function add(a: readonly number[], b: readonly number[]): Point;
|
|
7
|
+
/** a scaled by k, in 2D. */
|
|
8
|
+
export function mul(a: readonly number[], k: number): Point;
|
|
9
|
+
/** The cross product of two 3D vectors. */
|
|
10
|
+
export function cross3(a: readonly number[], b: readonly number[]): Point3;
|
|
11
|
+
/** A ring's signed area by the shoelace sum, added point by point. */
|
|
12
|
+
export function shoelace(r: readonly (readonly number[])[]): number;
|
|
13
|
+
/** A rectangle from its corners (x0, y0) and (x1, y1). */
|
|
14
|
+
export function box(x0: number, y0: number, x1: number, y1: number): Polygon;
|
package/src/util/vec.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small vector helpers several part families share (the Logomarks tools had a copy in each of the ink-trap, tech,
|
|
3
|
+
* echo and plane parts), each written once here so they all draw with the same arithmetic.
|
|
4
|
+
*/
|
|
5
|
+
import { rect } from '../geometry/index.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* a − b, in 2D.
|
|
9
|
+
*
|
|
10
|
+
* @param {readonly number[]} a The point.
|
|
11
|
+
* @param {readonly number[]} b The point taken away.
|
|
12
|
+
* @returns {import('../geometry/types.js').Point} The difference.
|
|
13
|
+
*/
|
|
14
|
+
export const sub = (a, b) => [a[0] - b[0], a[1] - b[1]];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* a + b, in 2D.
|
|
18
|
+
*
|
|
19
|
+
* @param {readonly number[]} a The point.
|
|
20
|
+
* @param {readonly number[]} b The point added.
|
|
21
|
+
* @returns {import('../geometry/types.js').Point} The sum.
|
|
22
|
+
*/
|
|
23
|
+
export const add = (a, b) => [a[0] + b[0], a[1] + b[1]];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* a scaled by k, in 2D.
|
|
27
|
+
*
|
|
28
|
+
* @param {readonly number[]} a The vector.
|
|
29
|
+
* @param {number} k The factor.
|
|
30
|
+
* @returns {import('../geometry/types.js').Point} The scaled vector.
|
|
31
|
+
*/
|
|
32
|
+
export const mul = (a, k) => [a[0] * k, a[1] * k];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The cross product of two 3D vectors.
|
|
36
|
+
*
|
|
37
|
+
* @param {readonly number[]} a The first.
|
|
38
|
+
* @param {readonly number[]} b The second.
|
|
39
|
+
* @returns {import('../geometry/types.js').Point3} a × b.
|
|
40
|
+
*/
|
|
41
|
+
export const cross3 = (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]];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A ring's signed area by the shoelace sum, added up point by point (positive when it runs clockwise on screen, y
|
|
45
|
+
* down). The geometry kit's signedArea() sums the same terms in another order, so the two can differ in the last bit.
|
|
46
|
+
*
|
|
47
|
+
* @param {readonly (readonly number[])[]} r The ring.
|
|
48
|
+
* @returns {number} The area, signed.
|
|
49
|
+
*/
|
|
50
|
+
export const shoelace = (r) => r.reduce((s, p, i) => { const q = r[(i + 1) % r.length]; return s + p[0] * q[1] - q[0] * p[1]; }, 0) / 2;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A rectangle from its corners (x0, y0) and (x1, y1).
|
|
54
|
+
*
|
|
55
|
+
* @param {number} x0 Left.
|
|
56
|
+
* @param {number} y0 Top.
|
|
57
|
+
* @param {number} x1 Right.
|
|
58
|
+
* @param {number} y1 Bottom.
|
|
59
|
+
* @returns {import('../geometry/types.js').Polygon} The rectangle.
|
|
60
|
+
*/
|
|
61
|
+
export const box = (x0, y0, x1, y1) => rect(x0, y0, x1 - x0, y1 - y0);
|