@fusion.dev/helix-graph 0.122.0 → 0.124.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/README.md +49 -12
- package/dist/connect-graph/arrange.d.ts +10 -0
- package/dist/connect-graph/arrange.js +6 -1
- package/dist/connect-graph/connect-graph.d.ts +5 -3
- package/dist/connect-graph/connect-graph.js +16 -6
- package/dist/connect-graph/shape-field.d.ts +24 -0
- package/dist/connect-graph/shape-field.js +3 -3
- package/dist/connect-graph/stencil.d.ts +30 -0
- package/dist/connect-graph/stencil.js +75 -25
- package/dist/connect-graph/surface-web.d.ts +18 -0
- package/dist/connect-graph/surface-web.js +99 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/overlay/overlay.d.ts +6 -1
- package/dist/overlay/overlay.js +91 -46
- package/dist/shapes/flat.d.ts +28 -0
- package/dist/shapes/flat.js +63 -0
- package/dist/shapes/index.d.ts +5 -0
- package/dist/shapes/index.js +5 -0
- package/dist/shapes/model.d.ts +12 -0
- package/dist/shapes/model.js +31 -0
- package/dist/shapes/solids.d.ts +28 -0
- package/dist/shapes/solids.js +47 -0
- package/dist/shapes/triangles.d.ts +20 -0
- package/dist/shapes/triangles.js +92 -0
- package/dist/sigma/node-program.js +5 -0
- package/dist/three/surface.d.ts +18 -0
- package/dist/three/surface.js +112 -0
- package/dist/three/three-driver.js +516 -24
- package/package.json +4 -4
package/dist/overlay/overlay.js
CHANGED
|
@@ -2,68 +2,111 @@ import { mixRgb } from '../connect-graph/looks.js';
|
|
|
2
2
|
const SPEED = 340;
|
|
3
3
|
const QUICKEST = 450;
|
|
4
4
|
const SLOWEST = 1400;
|
|
5
|
-
const ARRIVAL =
|
|
5
|
+
const ARRIVAL = 520;
|
|
6
6
|
const FLYING = 400;
|
|
7
7
|
const withAlpha = (color, alpha) => {
|
|
8
8
|
const [red = 128, green = 128, blue = 128] = color.match(/[\d.]+/g)?.map(Number) ?? [];
|
|
9
9
|
return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
|
|
10
10
|
};
|
|
11
|
-
const TAIL =
|
|
12
|
-
const
|
|
13
|
-
|
|
11
|
+
const TAIL = 120;
|
|
12
|
+
const PIECES = 14;
|
|
13
|
+
const BOW = 0.16;
|
|
14
|
+
const BOW_MOST = 90;
|
|
15
|
+
const SPARK = 0.22;
|
|
16
|
+
const on = (a, c, b, t) => {
|
|
17
|
+
const u = 1 - t;
|
|
18
|
+
return {
|
|
19
|
+
x: u * u * a.x + 2 * u * t * c.x + t * t * b.x,
|
|
20
|
+
y: u * u * a.y + 2 * u * t * c.y + t * t * b.y,
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
const bowed = (from, to) => {
|
|
24
|
+
const dx = to.x - from.x;
|
|
25
|
+
const dy = to.y - from.y;
|
|
26
|
+
const length = Math.hypot(dx, dy) || 1;
|
|
27
|
+
const bow = Math.min(length * BOW, BOW_MOST) * 2;
|
|
28
|
+
return {
|
|
29
|
+
x: (from.x + to.x) / 2 - (dy / length) * bow,
|
|
30
|
+
y: (from.y + to.y) / 2 + (dx / length) * bow,
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
const bounded = (from, to, toward) => {
|
|
34
|
+
const middle = { x: (from.x + to.x) / 2, y: (from.y + to.y) / 2 };
|
|
35
|
+
const far = Math.hypot(toward.x - middle.x, toward.y - middle.y);
|
|
36
|
+
const most = Math.max(Math.hypot(to.x - from.x, to.y - from.y) * 0.9, 12);
|
|
37
|
+
if (far <= most) {
|
|
38
|
+
return toward;
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
x: middle.x + ((toward.x - middle.x) * most) / far,
|
|
42
|
+
y: middle.y + ((toward.y - middle.y) * most) / far,
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
const trace = (context, a, c, b) => {
|
|
46
|
+
context.beginPath();
|
|
47
|
+
context.moveTo(a.x, a.y);
|
|
48
|
+
context.quadraticCurveTo(c.x, c.y, b.x, b.y);
|
|
49
|
+
};
|
|
50
|
+
const comet = (context, leaves, arrives, from, bend, to, along, through, glowing) => {
|
|
51
|
+
const strength = glowing ? 1.25 : 1;
|
|
52
|
+
const length = Math.hypot(bend.x - from.x, bend.y - from.y) + Math.hypot(to.x - bend.x, to.y - bend.y) || 1;
|
|
14
53
|
const color = mixRgb(leaves, arrives, along);
|
|
15
|
-
const length = Math.hypot(to.x - from.x, to.y - from.y) || 1;
|
|
16
|
-
const head = { x: from.x + (to.x - from.x) * along, y: from.y + (to.y - from.y) * along };
|
|
17
|
-
const behind = Math.max(along - TAIL / length, 0);
|
|
18
|
-
const tail = { x: from.x + (to.x - from.x) * behind, y: from.y + (to.y - from.y) * behind };
|
|
19
54
|
context.lineCap = 'round';
|
|
20
|
-
context.globalAlpha = 0.14 * Math.sin(Math.PI * through);
|
|
21
55
|
const line = context.createLinearGradient(from.x, from.y, to.x, to.y);
|
|
22
|
-
line.addColorStop(0, leaves);
|
|
23
|
-
line.addColorStop(1, arrives);
|
|
56
|
+
line.addColorStop(0, withAlpha(leaves, 0.16 * Math.sin(Math.PI * through)));
|
|
57
|
+
line.addColorStop(1, withAlpha(arrives, 0.16 * Math.sin(Math.PI * through)));
|
|
24
58
|
context.strokeStyle = line;
|
|
25
59
|
context.lineWidth = 1;
|
|
26
|
-
context
|
|
27
|
-
context.moveTo(from.x, from.y);
|
|
28
|
-
context.lineTo(to.x, to.y);
|
|
29
|
-
context.stroke();
|
|
30
|
-
const fade = context.createLinearGradient(tail.x, tail.y, head.x, head.y);
|
|
31
|
-
const back = mixRgb(leaves, arrives, behind);
|
|
32
|
-
fade.addColorStop(0, withAlpha(back, 0));
|
|
33
|
-
fade.addColorStop(0.6, withAlpha(mixRgb(back, color, 0.6), 0.25));
|
|
34
|
-
fade.addColorStop(1, withAlpha(color, 0.95));
|
|
35
|
-
context.strokeStyle = fade;
|
|
36
|
-
context.shadowColor = color;
|
|
37
|
-
context.shadowBlur = 8;
|
|
38
|
-
context.globalAlpha = Math.min(0.35 * strength, 1);
|
|
39
|
-
context.lineWidth = 3.5;
|
|
40
|
-
context.beginPath();
|
|
41
|
-
context.moveTo(tail.x, tail.y);
|
|
42
|
-
context.lineTo(head.x, head.y);
|
|
43
|
-
context.stroke();
|
|
44
|
-
context.shadowBlur = 0;
|
|
45
|
-
context.globalAlpha = 1;
|
|
46
|
-
context.lineWidth = 1.4 * strength;
|
|
60
|
+
trace(context, from, bend, to);
|
|
47
61
|
context.stroke();
|
|
48
|
-
const reach =
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
62
|
+
const reach = Math.min(TAIL / length, along);
|
|
63
|
+
context.lineCap = 'butt';
|
|
64
|
+
let behind = on(from, bend, to, along - reach);
|
|
65
|
+
for (let piece = 1; piece <= PIECES; piece++) {
|
|
66
|
+
const share = piece / PIECES;
|
|
67
|
+
const t = along - reach * (1 - share);
|
|
68
|
+
const ahead = on(from, bend, to, t);
|
|
69
|
+
context.strokeStyle = withAlpha(mixRgb(leaves, arrives, t), (glowing ? 0.85 : 1) * share * share);
|
|
70
|
+
context.lineWidth = (0.4 + 2.2 * share) * strength;
|
|
71
|
+
context.beginPath();
|
|
72
|
+
context.moveTo(behind.x, behind.y);
|
|
73
|
+
context.lineTo(ahead.x, ahead.y);
|
|
74
|
+
context.stroke();
|
|
75
|
+
behind = ahead;
|
|
76
|
+
}
|
|
77
|
+
const head = on(from, bend, to, along);
|
|
78
|
+
const bloom = 9 * strength;
|
|
79
|
+
const light = context.createRadialGradient(head.x, head.y, 0, head.x, head.y, bloom);
|
|
80
|
+
light.addColorStop(0, glowing ? 'rgba(255, 255, 255, 0.95)' : withAlpha(color, 1));
|
|
81
|
+
light.addColorStop(0.18, glowing ? withAlpha(mixRgb(color, 'rgb(255, 255, 255)', 0.5), 0.9) : withAlpha(color, 0.85));
|
|
82
|
+
light.addColorStop(0.45, withAlpha(color, 0.35));
|
|
52
83
|
light.addColorStop(1, withAlpha(color, 0));
|
|
53
84
|
context.fillStyle = light;
|
|
54
85
|
context.beginPath();
|
|
55
|
-
context.arc(head.x, head.y,
|
|
86
|
+
context.arc(head.x, head.y, bloom, 0, Math.PI * 2);
|
|
56
87
|
context.fill();
|
|
57
88
|
};
|
|
89
|
+
const spark = (context, color, at, gone) => {
|
|
90
|
+
context.strokeStyle = withAlpha(color, 0.6 * (1 - gone) ** 2);
|
|
91
|
+
context.lineWidth = 1.2;
|
|
92
|
+
context.beginPath();
|
|
93
|
+
context.arc(at.x, at.y, at.radius + 2 + 8 * ease(gone), 0, Math.PI * 2);
|
|
94
|
+
context.stroke();
|
|
95
|
+
};
|
|
58
96
|
const arrive = (context, color, at, gone) => {
|
|
59
|
-
const reach = at.radius +
|
|
97
|
+
const reach = at.radius + 7;
|
|
60
98
|
const glow = context.createRadialGradient(at.x, at.y, at.radius * 0.4, at.x, at.y, reach);
|
|
61
|
-
glow.addColorStop(0, withAlpha(color, 0.
|
|
99
|
+
glow.addColorStop(0, withAlpha(color, 0.55 * (1 - gone) ** 2));
|
|
62
100
|
glow.addColorStop(1, withAlpha(color, 0));
|
|
63
101
|
context.fillStyle = glow;
|
|
64
102
|
context.beginPath();
|
|
65
103
|
context.arc(at.x, at.y, reach, 0, Math.PI * 2);
|
|
66
104
|
context.fill();
|
|
105
|
+
context.strokeStyle = withAlpha(color, 0.7 * (1 - gone) ** 1.5);
|
|
106
|
+
context.lineWidth = 1.5 * (1 - gone) + 0.5;
|
|
107
|
+
context.beginPath();
|
|
108
|
+
context.arc(at.x, at.y, at.radius + 3 + 16 * ease(gone), 0, Math.PI * 2);
|
|
109
|
+
context.stroke();
|
|
67
110
|
};
|
|
68
111
|
export const isLight = (color) => {
|
|
69
112
|
const [red = 0, green = 0, blue = 0] = color.match(/[\d.]+/g)?.map(Number) ?? [];
|
|
@@ -91,7 +134,7 @@ const nameGroups = (context, groups, looks, at) => {
|
|
|
91
134
|
points.push(point);
|
|
92
135
|
}
|
|
93
136
|
}
|
|
94
|
-
if (count < 2) {
|
|
137
|
+
if (count < Math.max(2, group.ids.length * 0.3)) {
|
|
95
138
|
continue;
|
|
96
139
|
}
|
|
97
140
|
x /= count;
|
|
@@ -159,7 +202,7 @@ const fresh = (canvas, container) => {
|
|
|
159
202
|
context.clearRect(0, 0, width, height);
|
|
160
203
|
return context;
|
|
161
204
|
};
|
|
162
|
-
export const signalsOver = (container, onScreen) => {
|
|
205
|
+
export const signalsOver = (container, onScreen, bend) => {
|
|
163
206
|
let overlay;
|
|
164
207
|
let flights = [];
|
|
165
208
|
let flying = 0;
|
|
@@ -178,16 +221,20 @@ export const signalsOver = (container, onScreen) => {
|
|
|
178
221
|
if (!from || !to) {
|
|
179
222
|
return false;
|
|
180
223
|
}
|
|
181
|
-
flight.crossing ??= Math.min(Math.max((Math.hypot(to.x - from.x, to.y - from.y) / SPEED) * 1000, QUICKEST), SLOWEST);
|
|
224
|
+
flight.crossing ??= Math.min(Math.max(((Math.hypot(to.x - from.x, to.y - from.y) * 1.08) / SPEED) * 1000, QUICKEST), SLOWEST);
|
|
182
225
|
const crossing = flight.crossing;
|
|
183
226
|
const gone = (now - flight.started) / crossing;
|
|
184
227
|
if (gone >= 1 + ARRIVAL / crossing) {
|
|
185
228
|
return false;
|
|
186
229
|
}
|
|
230
|
+
const toward = bounded(from, to, bend?.(flight.from, flight.to) ?? bowed(from, to));
|
|
187
231
|
context.save();
|
|
188
232
|
if (gone < 1) {
|
|
189
233
|
if (flight.animate) {
|
|
190
|
-
|
|
234
|
+
comet(context, flight.leaves, flight.arrives, from, toward, to, ease(gone), gone, glowing);
|
|
235
|
+
if (gone < SPARK) {
|
|
236
|
+
spark(context, flight.leaves, from, gone / SPARK);
|
|
237
|
+
}
|
|
191
238
|
}
|
|
192
239
|
else {
|
|
193
240
|
const line = context.createLinearGradient(from.x, from.y, to.x, to.y);
|
|
@@ -196,9 +243,7 @@ export const signalsOver = (container, onScreen) => {
|
|
|
196
243
|
context.globalAlpha = 1 - gone;
|
|
197
244
|
context.strokeStyle = line;
|
|
198
245
|
context.lineWidth = 2;
|
|
199
|
-
context
|
|
200
|
-
context.moveTo(from.x, from.y);
|
|
201
|
-
context.lineTo(to.x, to.y);
|
|
246
|
+
trace(context, from, toward, to);
|
|
202
247
|
context.stroke();
|
|
203
248
|
}
|
|
204
249
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type ShapeField } from '../connect-graph/shape-field.js';
|
|
2
|
+
/**
|
|
3
|
+
* A flat field from an SVG: an `<svg>` element, or its markup. Its box is
|
|
4
|
+
* its `viewBox`, and its shape every path, rectangle, circle, ellipse and
|
|
5
|
+
* polygon in it, together, their holes cut by the even-odd rule.
|
|
6
|
+
*
|
|
7
|
+
* Transforms on the shapes are not read: draw the outline in the `viewBox`'s
|
|
8
|
+
* own units, as an exported icon or logo is.
|
|
9
|
+
*/
|
|
10
|
+
export declare const shapeFromSvg: (svg: SVGSVGElement | string, resolution?: number) => ShapeField;
|
|
11
|
+
/** How an image is read as a shape. */
|
|
12
|
+
export interface ImageOptions {
|
|
13
|
+
/** How opaque a pixel is to be inside, from 0 to 1. 0.5 by default. */
|
|
14
|
+
threshold?: number | undefined;
|
|
15
|
+
/** How many cells the field has along the image's longer side. 512 by default. */
|
|
16
|
+
resolution?: number | undefined;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A flat field from an image's opaque pixels: a logo, a silhouette, or a
|
|
20
|
+
* mask drawn for the purpose. Its box is the image's size in pixels.
|
|
21
|
+
*
|
|
22
|
+
* An image from another site is read only where that site allows it, as a
|
|
23
|
+
* canvas reads any image.
|
|
24
|
+
*/
|
|
25
|
+
export declare const shapeFromImage: (image: CanvasImageSource & {
|
|
26
|
+
height: number | SVGAnimatedLength;
|
|
27
|
+
width: number | SVGAnimatedLength;
|
|
28
|
+
}, { resolution, threshold }?: ImageOptions) => ShapeField;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { FLAT, fieldFromCells, fieldFromPath, } from '../connect-graph/shape-field.js';
|
|
2
|
+
const pathOf = (element) => {
|
|
3
|
+
const number = (name) => Number(element.getAttribute(name) ?? 0);
|
|
4
|
+
switch (element.localName) {
|
|
5
|
+
case 'path':
|
|
6
|
+
return element.getAttribute('d') ?? '';
|
|
7
|
+
case 'rect': {
|
|
8
|
+
const [x, y, width, height] = [number('x'), number('y'), number('width'), number('height')];
|
|
9
|
+
return `M${x} ${y}h${width}v${height}h${-width}Z`;
|
|
10
|
+
}
|
|
11
|
+
case 'circle':
|
|
12
|
+
case 'ellipse': {
|
|
13
|
+
const [cx, cy] = [number('cx'), number('cy')];
|
|
14
|
+
const rx = element.localName === 'circle' ? number('r') : number('rx');
|
|
15
|
+
const ry = element.localName === 'circle' ? number('r') : number('ry');
|
|
16
|
+
return `M${cx - rx} ${cy}a${rx} ${ry} 0 1 0 ${rx * 2} 0a${rx} ${ry} 0 1 0 ${-rx * 2} 0Z`;
|
|
17
|
+
}
|
|
18
|
+
case 'polygon': {
|
|
19
|
+
const points = element.getAttribute('points')?.trim() ?? '';
|
|
20
|
+
return points ? `M${points}Z` : '';
|
|
21
|
+
}
|
|
22
|
+
default:
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
export const shapeFromSvg = (svg, resolution = FLAT) => {
|
|
27
|
+
const element = typeof svg === 'string'
|
|
28
|
+
? new DOMParser().parseFromString(svg, 'image/svg+xml').documentElement
|
|
29
|
+
: svg;
|
|
30
|
+
const [, , width = 0, height = 0] = (element.getAttribute('viewBox') ?? '')
|
|
31
|
+
.trim()
|
|
32
|
+
.split(/[\s,]+/)
|
|
33
|
+
.map(Number);
|
|
34
|
+
const box = {
|
|
35
|
+
height: height || Number(element.getAttribute('height') ?? 0),
|
|
36
|
+
width: width || Number(element.getAttribute('width') ?? 0),
|
|
37
|
+
};
|
|
38
|
+
const path = [...element.querySelectorAll('path, rect, circle, ellipse, polygon')]
|
|
39
|
+
.map(pathOf)
|
|
40
|
+
.join(' ');
|
|
41
|
+
return fieldFromPath(path, box.width || 1, box.height || 1, resolution);
|
|
42
|
+
};
|
|
43
|
+
export const shapeFromImage = (image, { resolution = FLAT, threshold = 0.5 } = {}) => {
|
|
44
|
+
const size = (value) => typeof value === 'number' ? value : value.baseVal.value;
|
|
45
|
+
const width = Math.max(size(image.width), 1);
|
|
46
|
+
const height = Math.max(size(image.height), 1);
|
|
47
|
+
const cell = Math.max(width, height) / resolution;
|
|
48
|
+
const nx = Math.max(1, Math.ceil(width / cell));
|
|
49
|
+
const ny = Math.max(1, Math.ceil(height / cell));
|
|
50
|
+
const canvas = document.createElement('canvas');
|
|
51
|
+
canvas.width = nx;
|
|
52
|
+
canvas.height = ny;
|
|
53
|
+
const context = canvas.getContext('2d', { willReadFrequently: true });
|
|
54
|
+
const cells = new Uint8Array(nx * ny);
|
|
55
|
+
if (context) {
|
|
56
|
+
context.drawImage(image, 0, 0, nx, ny);
|
|
57
|
+
const alpha = context.getImageData(0, 0, nx, ny).data;
|
|
58
|
+
for (let at = 0; at < cells.length; at++) {
|
|
59
|
+
cells[at] = (alpha[at * 4 + 3] ?? 0) / 255 >= threshold ? 1 : 0;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return fieldFromCells(cells, [nx, ny, 1], cell, { depth: 0, height, width });
|
|
63
|
+
};
|
package/dist/shapes/index.d.ts
CHANGED
|
@@ -1 +1,6 @@
|
|
|
1
1
|
export { fieldFromInside as shapeFromInside, fieldFromPath as shapeFromPath, inflate, type ShapeField, } from '../connect-graph/shape-field.js';
|
|
2
|
+
export { surfaceOf } from '../three/surface.js';
|
|
3
|
+
export { type ImageOptions, shapeFromImage, shapeFromSvg } from './flat.js';
|
|
4
|
+
export { shapeFromModel } from './model.js';
|
|
5
|
+
export { box, onSphere, sphere, torus } from './solids.js';
|
|
6
|
+
export { shapeFromTriangles, type TriangleOptions } from './triangles.js';
|
package/dist/shapes/index.js
CHANGED
|
@@ -1 +1,6 @@
|
|
|
1
1
|
export { fieldFromInside as shapeFromInside, fieldFromPath as shapeFromPath, inflate, } from '../connect-graph/shape-field.js';
|
|
2
|
+
export { surfaceOf } from '../three/surface.js';
|
|
3
|
+
export { shapeFromImage, shapeFromSvg } from './flat.js';
|
|
4
|
+
export { shapeFromModel } from './model.js';
|
|
5
|
+
export { box, onSphere, sphere, torus } from './solids.js';
|
|
6
|
+
export { shapeFromTriangles } from './triangles.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ShapeField } from '../connect-graph/shape-field.js';
|
|
2
|
+
import { type TriangleOptions } from './triangles.js';
|
|
3
|
+
/**
|
|
4
|
+
* A solid field from a glTF model, the web's standard format for one: a
|
|
5
|
+
* URL to a `.glb` or `.gltf` file, or the file's bytes. Every mesh in it is
|
|
6
|
+
* read, placed as the model places it.
|
|
7
|
+
*
|
|
8
|
+
* **three.js reads the file**, imported when this is first called, so a page
|
|
9
|
+
* that draws no model never downloads a loader. It is an optional peer of
|
|
10
|
+
* this package.
|
|
11
|
+
*/
|
|
12
|
+
export declare const shapeFromModel: (source: string | ArrayBuffer, options?: TriangleOptions) => Promise<ShapeField>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { shapeFromTriangles } from './triangles.js';
|
|
2
|
+
export const shapeFromModel = async (source, options = {}) => {
|
|
3
|
+
const [{ GLTFLoader }, { Mesh, Vector3 }] = await Promise.all([
|
|
4
|
+
import('three/addons/loaders/GLTFLoader.js'),
|
|
5
|
+
import('three'),
|
|
6
|
+
]);
|
|
7
|
+
const loader = new GLTFLoader();
|
|
8
|
+
const model = typeof source === 'string'
|
|
9
|
+
? await loader.loadAsync(source)
|
|
10
|
+
: await loader.parseAsync(source, '');
|
|
11
|
+
model.scene.updateMatrixWorld(true);
|
|
12
|
+
const corners = [];
|
|
13
|
+
const point = new Vector3();
|
|
14
|
+
model.scene.traverse((object) => {
|
|
15
|
+
if (!(object instanceof Mesh)) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const geometry = object.geometry;
|
|
19
|
+
const place = geometry.getAttribute('position');
|
|
20
|
+
if (!place) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const index = geometry.getIndex();
|
|
24
|
+
const count = index ? index.count : place.count;
|
|
25
|
+
for (let at = 0; at < count; at++) {
|
|
26
|
+
point.fromBufferAttribute(place, index ? index.getX(at) : at).applyMatrix4(object.matrixWorld);
|
|
27
|
+
corners.push(point.x, point.y, point.z);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
return shapeFromTriangles(corners, null, options);
|
|
31
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ShapeField } from '../connect-graph/shape-field.js';
|
|
2
|
+
/** A ball `radius` across from its middle. */
|
|
3
|
+
export declare const sphere: ({ radius }?: {
|
|
4
|
+
radius?: number;
|
|
5
|
+
}) => ShapeField;
|
|
6
|
+
/** A ring: `radius` from its middle to the middle of its tube, which is `tube` thick from its own middle out. */
|
|
7
|
+
export declare const torus: ({ radius, tube, }?: {
|
|
8
|
+
radius?: number;
|
|
9
|
+
tube?: number;
|
|
10
|
+
}) => ShapeField;
|
|
11
|
+
/** A box `width` by `height` by `depth`, its edges rounded `round` in. */
|
|
12
|
+
export declare const box: ({ depth, height, round, width, }?: {
|
|
13
|
+
depth?: number;
|
|
14
|
+
height?: number;
|
|
15
|
+
round?: number;
|
|
16
|
+
width?: number;
|
|
17
|
+
}) => ShapeField;
|
|
18
|
+
/**
|
|
19
|
+
* A flat shape wrapped round a ball, as a map of the world is round a globe:
|
|
20
|
+
* the flat shape's width is the way round and its height pole to pole, as an
|
|
21
|
+
* equirectangular map is drawn. The solid is a shell `thickness` deep at the
|
|
22
|
+
* ball's surface, wherever the flat shape is, so notes placed in it draw the
|
|
23
|
+
* map in three dimensions.
|
|
24
|
+
*/
|
|
25
|
+
export declare const onSphere: (flat: ShapeField, { radius, thickness }?: {
|
|
26
|
+
radius?: number;
|
|
27
|
+
thickness?: number;
|
|
28
|
+
}) => ShapeField;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export const sphere = ({ radius = 50 } = {}) => ({
|
|
2
|
+
depth: radius * 2,
|
|
3
|
+
distance: (x, y, z = radius) => Math.hypot(x - radius, y - radius, z - radius) - radius,
|
|
4
|
+
height: radius * 2,
|
|
5
|
+
width: radius * 2,
|
|
6
|
+
});
|
|
7
|
+
export const torus = ({ radius = 50, tube = 18, } = {}) => {
|
|
8
|
+
const reach = radius + tube;
|
|
9
|
+
return {
|
|
10
|
+
depth: tube * 2,
|
|
11
|
+
distance: (x, y, z = tube) => Math.hypot(Math.hypot(x - reach, y - reach) - radius, z - tube) - tube,
|
|
12
|
+
height: reach * 2,
|
|
13
|
+
width: reach * 2,
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
export const box = ({ depth = 100, height = 100, round = 0, width = 100, } = {}) => ({
|
|
17
|
+
depth,
|
|
18
|
+
distance: (x, y, z = depth / 2) => {
|
|
19
|
+
const qx = Math.abs(x - width / 2) - width / 2 + round;
|
|
20
|
+
const qy = Math.abs(y - height / 2) - height / 2 + round;
|
|
21
|
+
const qz = Math.abs(z - depth / 2) - depth / 2 + round;
|
|
22
|
+
return (Math.hypot(Math.max(qx, 0), Math.max(qy, 0), Math.max(qz, 0)) +
|
|
23
|
+
Math.min(Math.max(qx, qy, qz), 0) -
|
|
24
|
+
round);
|
|
25
|
+
},
|
|
26
|
+
height,
|
|
27
|
+
width,
|
|
28
|
+
});
|
|
29
|
+
export const onSphere = (flat, { radius = 50, thickness = radius * 0.08 } = {}) => {
|
|
30
|
+
const along = (Math.PI * 2 * radius) / Math.max(flat.width, 1e-9);
|
|
31
|
+
return {
|
|
32
|
+
depth: radius * 2,
|
|
33
|
+
distance: (x, y, z = radius) => {
|
|
34
|
+
const dx = x - radius;
|
|
35
|
+
const dy = y - radius;
|
|
36
|
+
const dz = z - radius;
|
|
37
|
+
const out = Math.hypot(dx, dy, dz);
|
|
38
|
+
const shell = Math.abs(out - (radius - thickness / 2)) - thickness / 2;
|
|
39
|
+
const longitude = Math.atan2(dx, dz);
|
|
40
|
+
const latitude = Math.acos(Math.min(Math.max(dy / (out || 1), -1), 1));
|
|
41
|
+
const map = flat.distance(((longitude + Math.PI) / (Math.PI * 2)) * flat.width, (latitude / Math.PI) * flat.height);
|
|
42
|
+
return Math.max(shell, map * along);
|
|
43
|
+
},
|
|
44
|
+
height: radius * 2,
|
|
45
|
+
width: radius * 2,
|
|
46
|
+
};
|
|
47
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ShapeField } from '../connect-graph/shape-field.js';
|
|
2
|
+
/** How a solid is read from triangles. */
|
|
3
|
+
export interface TriangleOptions {
|
|
4
|
+
/** How many cells the field has along the solid's longest side. 96 by default. */
|
|
5
|
+
resolution?: number | undefined;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* A solid field from a closed surface of triangles: `positions` three
|
|
9
|
+
* numbers a corner, and `index` three corners a triangle, or every three
|
|
10
|
+
* corners in order where there is none. Its y points up, as a model's does,
|
|
11
|
+
* and its box is the surface's own bounds, a cell of room around them.
|
|
12
|
+
*
|
|
13
|
+
* **Read a column of cells at a time**: each triangle marks where it crosses
|
|
14
|
+
* the columns under it, and a cell is inside where an odd number of
|
|
15
|
+
* crossings lie in front of it. A model of a hundred thousand triangles is
|
|
16
|
+
* read in a moment, where testing each cell against each triangle would take
|
|
17
|
+
* minutes. A surface with gaps is read as closed where it can be, and a gap
|
|
18
|
+
* shows as a column filled or emptied through.
|
|
19
|
+
*/
|
|
20
|
+
export declare const shapeFromTriangles: (positions: ArrayLike<number>, index?: ArrayLike<number> | null | undefined, { resolution }?: TriangleOptions) => ShapeField;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { fieldFromCells, SOLID } from '../connect-graph/shape-field.js';
|
|
2
|
+
const NUDGE = 1e-6 * Math.PI;
|
|
3
|
+
export const shapeFromTriangles = (positions, index, { resolution = SOLID } = {}) => {
|
|
4
|
+
const corners = Math.floor(positions.length / 3);
|
|
5
|
+
const low = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY];
|
|
6
|
+
const high = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY];
|
|
7
|
+
for (let at = 0; at < corners; at++) {
|
|
8
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
9
|
+
const value = positions[at * 3 + axis];
|
|
10
|
+
low[axis] = Math.min(low[axis], value);
|
|
11
|
+
high[axis] = Math.max(high[axis], value);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
if (corners === 0) {
|
|
15
|
+
return fieldFromCells(new Uint8Array(1), [1, 1, 1], 1, { depth: 1, height: 1, width: 1 });
|
|
16
|
+
}
|
|
17
|
+
const span = Math.max(high[0] - low[0], high[1] - low[1], high[2] - low[2], 1e-9);
|
|
18
|
+
const cell = span / Math.max(resolution - 2, 1);
|
|
19
|
+
const origin = low.map((value) => value - cell);
|
|
20
|
+
const nx = Math.ceil((high[0] - low[0]) / cell) + 2;
|
|
21
|
+
const ny = Math.ceil((high[1] - low[1]) / cell) + 2;
|
|
22
|
+
const nz = Math.ceil((high[2] - low[2]) / cell) + 2;
|
|
23
|
+
const crossings = Array.from({ length: nx * ny }, () => []);
|
|
24
|
+
const triangles = index ? Math.floor(index.length / 3) : Math.floor(corners / 3);
|
|
25
|
+
const corner = (triangle, which) => {
|
|
26
|
+
const at = index ? index[triangle * 3 + which] : triangle * 3 + which;
|
|
27
|
+
return [
|
|
28
|
+
(positions[at * 3] - origin[0]) / cell,
|
|
29
|
+
(high[1] + cell - positions[at * 3 + 1]) / cell,
|
|
30
|
+
(positions[at * 3 + 2] - origin[2]) / cell,
|
|
31
|
+
];
|
|
32
|
+
};
|
|
33
|
+
for (let triangle = 0; triangle < triangles; triangle++) {
|
|
34
|
+
const [ax, ay, az] = corner(triangle, 0);
|
|
35
|
+
const [bx, by, bz] = corner(triangle, 1);
|
|
36
|
+
const [cx, cy, cz] = corner(triangle, 2);
|
|
37
|
+
const area = (bx - ax) * (cy - ay) - (cx - ax) * (by - ay);
|
|
38
|
+
if (Math.abs(area) < 1e-12) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const left = Math.max(0, Math.ceil(Math.min(ax, bx, cx) - 0.5));
|
|
42
|
+
const right = Math.min(nx - 1, Math.floor(Math.max(ax, bx, cx) - 0.5));
|
|
43
|
+
const top = Math.max(0, Math.ceil(Math.min(ay, by, cy) - 0.5));
|
|
44
|
+
const bottom = Math.min(ny - 1, Math.floor(Math.max(ay, by, cy) - 0.5));
|
|
45
|
+
for (let row = top; row <= bottom; row++) {
|
|
46
|
+
for (let column = left; column <= right; column++) {
|
|
47
|
+
const px = column + 0.5 + NUDGE;
|
|
48
|
+
const py = row + 0.5 + NUDGE * Math.SQRT2;
|
|
49
|
+
const u = ((bx - px) * (cy - py) - (cx - px) * (by - py)) / area;
|
|
50
|
+
const v = ((cx - px) * (ay - py) - (ax - px) * (cy - py)) / area;
|
|
51
|
+
const w = 1 - u - v;
|
|
52
|
+
if (u < 0 || v < 0 || w < 0) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
;
|
|
56
|
+
crossings[row * nx + column].push(u * az + v * bz + w * cz);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const cells = new Uint8Array(nx * ny * nz);
|
|
61
|
+
for (let row = 0; row < ny; row++) {
|
|
62
|
+
for (let column = 0; column < nx; column++) {
|
|
63
|
+
const along = crossings[row * nx + column].sort((one, other) => one - other);
|
|
64
|
+
for (let pair = 0; pair + 1 < along.length; pair += 2) {
|
|
65
|
+
const from = Math.max(0, Math.ceil(along[pair] - 0.5));
|
|
66
|
+
const to = Math.min(nz - 1, Math.floor(along[pair + 1] - 0.5));
|
|
67
|
+
for (let layer = from; layer <= to; layer++) {
|
|
68
|
+
cells[layer * nx * ny + row * nx + column] = 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const meshCorners = new Float32Array(corners * 3);
|
|
74
|
+
for (let at = 0; at < corners; at++) {
|
|
75
|
+
meshCorners[at * 3] = positions[at * 3] - origin[0];
|
|
76
|
+
meshCorners[at * 3 + 1] = high[1] + cell - positions[at * 3 + 1];
|
|
77
|
+
meshCorners[at * 3 + 2] = positions[at * 3 + 2] - origin[2];
|
|
78
|
+
}
|
|
79
|
+
const meshTriangles = index
|
|
80
|
+
? Uint32Array.from({ length: triangles * 3 }, (_, at) => index[at])
|
|
81
|
+
: Uint32Array.from({ length: triangles * 3 }, (_, at) => at);
|
|
82
|
+
const field = fieldFromCells(cells, [nx, ny, nz], cell, {
|
|
83
|
+
depth: nz * cell,
|
|
84
|
+
height: ny * cell,
|
|
85
|
+
width: nx * cell,
|
|
86
|
+
});
|
|
87
|
+
return {
|
|
88
|
+
...field,
|
|
89
|
+
distance: field.distance,
|
|
90
|
+
mesh: { corners: meshCorners, triangles: meshTriangles },
|
|
91
|
+
};
|
|
92
|
+
};
|
|
@@ -110,6 +110,11 @@ void main(void) {
|
|
|
110
110
|
color = mix(color, vec3(1.0), rim * 0.14);
|
|
111
111
|
alpha = 0.86;
|
|
112
112
|
} else {
|
|
113
|
+
/* Flat, which unset is: matte, with a gentle fall of light across it so
|
|
114
|
+
a note reads as round, brighter toward its middle the larger it is
|
|
115
|
+
drawn, and a fine rim a shade darker so notes that touch stay two. */
|
|
116
|
+
color *= mix(0.82, 1.1, lit);
|
|
117
|
+
color = mix(color, min(color * 1.25, vec3(1.0)), (1.0 - smoothstep(0.0, 0.8, length(p - vec2(-0.25, 0.3)))) * 0.35 * drawn);
|
|
113
118
|
color = mix(color, color * 0.72, rim * 0.6);
|
|
114
119
|
}
|
|
115
120
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ShapeField } from '../connect-graph/shape-field.js';
|
|
2
|
+
/**
|
|
3
|
+
* The surface of a solid field where its distance is `at`, as triangles:
|
|
4
|
+
* corners three numbers each in the field's box, and triangles three corners
|
|
5
|
+
* each. Surface nets: a corner in each cell the surface passes through, at
|
|
6
|
+
* the middle of where it crosses the cell's edges, and two triangles across
|
|
7
|
+
* each edge it crosses. Smooth enough to hide what is behind a shape, and
|
|
8
|
+
* made in a moment.
|
|
9
|
+
*/
|
|
10
|
+
export declare const surfaceOf: (field: ShapeField, { at, cells, }?: {
|
|
11
|
+
/** The distance the surface is drawn at: below zero inside the shape. 0 by default. */
|
|
12
|
+
at?: number | undefined;
|
|
13
|
+
/** How many cells the field is read in along its longest side. 72 by default. */
|
|
14
|
+
cells?: number | undefined;
|
|
15
|
+
}) => {
|
|
16
|
+
corners: Float32Array;
|
|
17
|
+
triangles: Uint32Array;
|
|
18
|
+
};
|