@fusion.dev/helix-graph 0.123.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 +30 -13
- package/dist/connect-graph/arrange.d.ts +10 -0
- package/dist/connect-graph/arrange.js +6 -1
- package/dist/connect-graph/connect-graph.js +7 -2
- package/dist/connect-graph/shape-field.d.ts +10 -0
- package/dist/connect-graph/stencil.d.ts +8 -1
- package/dist/connect-graph/stencil.js +51 -4
- package/dist/overlay/overlay.d.ts +6 -1
- package/dist/overlay/overlay.js +91 -46
- package/dist/shapes/index.d.ts +1 -0
- package/dist/shapes/index.js +1 -0
- package/dist/shapes/triangles.js +15 -1
- package/dist/sigma/node-program.js +5 -0
- package/dist/three/surface.d.ts +6 -1
- package/dist/three/surface.js +2 -2
- package/dist/three/three-driver.js +445 -36
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -75,11 +75,15 @@ socket.addEventListener('message', ({ data }) => {
|
|
|
75
75
|
```
|
|
76
76
|
|
|
77
77
|
**`frame.signal(from, to, { tone })` shows something passing between two
|
|
78
|
-
notes**: a message, a request, a payment. It is drawn as a
|
|
79
|
-
|
|
80
|
-
second's, or in the tone's color where it
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
notes**: a message, a request, a payment. It is drawn as a comet traveling
|
|
79
|
+
from one to the other along a gently bowed path, its tail tapering and turning
|
|
80
|
+
from the first note's color to the second's, or in the tone's color where it
|
|
81
|
+
has one. A small ring spreads from the note it leaves, and the note it reaches
|
|
82
|
+
brightens and ripples. A reply on the same link bows the other way, so the two
|
|
83
|
+
do not run over each other, and in three dimensions the path arcs over the
|
|
84
|
+
shape. It is drawn over the graph without redrawing it, so a busy feed costs a
|
|
85
|
+
few strokes each. A reader who asks for less motion sees the path lit for a
|
|
86
|
+
moment instead.
|
|
83
87
|
|
|
84
88
|
```js
|
|
85
89
|
frame.signal('Checkout', 'Payments', { tone: 'success' })
|
|
@@ -108,7 +112,15 @@ frame.nodes = [
|
|
|
108
112
|
```
|
|
109
113
|
|
|
110
114
|
`sigmaDriver(sigma, { material, shape, emphasis })` sets what a note is drawn as
|
|
111
|
-
where it does not say.
|
|
115
|
+
where it does not say. A note without a material is matte, lit across so it
|
|
116
|
+
reads as round. In three dimensions every note is a solid: a circle is a
|
|
117
|
+
sphere, worked out for each pixel and written at its depth, so notes meet the
|
|
118
|
+
shape and each other as solids do; a square is a cube and a diamond an
|
|
119
|
+
octahedron. On a dark ground a sphere is an orb of energy, a dark glassy body
|
|
120
|
+
with a glowing rim and a hot point at its middle, and on a light ground frosted
|
|
121
|
+
glass; a cube or an octahedron is a hologram, faint faces with fine edges of
|
|
122
|
+
light. Each catches a glint from a light fixed in the scene, so the glint moves
|
|
123
|
+
as the reader turns the graph. The selected note keeps its own color and is told apart by
|
|
112
124
|
a ring and a glow in that color.
|
|
113
125
|
|
|
114
126
|
**A note without a `size` is as large as it is connected**, so hubs read at a
|
|
@@ -220,6 +232,7 @@ makes one from what a page has, and a field is a stencil itself:
|
|
|
220
232
|
| `inflate(field, { depth, roundness })` | A solid puffed out of a flat field, as a cushion is, cut through where the flat shape has a hole |
|
|
221
233
|
| `sphere`, `torus`, `box` | Solids measured exactly from their formulas |
|
|
222
234
|
| `onSphere(field, { radius, thickness })` | A flat map wrapped round a ball, as a world map is round a globe |
|
|
235
|
+
| `surfaceOf(field, { at, cells })` | The surface of a solid as triangles, to draw or to save as a model |
|
|
223
236
|
|
|
224
237
|
```js
|
|
225
238
|
const stencil = { ...(await shapeFromModel('/models/brain.glb')), place: 'surface' }
|
|
@@ -254,12 +267,16 @@ connectGraph(frame, threeDriver(box), {
|
|
|
254
267
|
|
|
255
268
|
- **A solid stencil draws the notes in the solid**: all through it, or, with
|
|
256
269
|
`place: 'surface'`, in a thin shell just inside its edge, which is how a
|
|
257
|
-
solid reads as the thing it is shaped like. Each group
|
|
258
|
-
the
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
only the notes in front.
|
|
270
|
+
solid reads as the thing it is shaped like. **Each group is given a piece
|
|
271
|
+
of the shape of its own**, as many places as it has notes, and each hub a
|
|
272
|
+
piece of its group's, so no group is cut in two: on a knot, a group is a
|
|
273
|
+
stretch of its tube, and on a brain, a region of it.
|
|
274
|
+
- **On a surface, the shape itself is drawn** just inside the notes, so what
|
|
275
|
+
is behind it is hidden and the pointer finds only the notes in front.
|
|
276
|
+
**Each region is washed in its notes' color**, a translucent stain that
|
|
277
|
+
fades into the next region's, and glows in that color at the edge turning
|
|
278
|
+
away. It is lit faintly from above, and a model's own surface is drawn,
|
|
279
|
+
with every fold the field's cells are too coarse to hold.
|
|
263
280
|
- **Without a stencil, a layout runs in three dimensions** with d3-force-3d.
|
|
264
281
|
- **Dragging turns the graph, and the wheel and a pinch bring it nearer.** It
|
|
265
282
|
turns slowly by itself while nobody is handling it, and never for a reader
|
|
@@ -284,7 +301,7 @@ connectGraph(frame, threeDriver(box), {
|
|
|
284
301
|
| `@fusion.dev/helix-graph/sigma` | `sigmaDriver(sigma, options)` |
|
|
285
302
|
| `@fusion.dev/helix-graph/cosmos` | `cosmosDriver(graph, box, options)` |
|
|
286
303
|
| `@fusion.dev/helix-graph/three` | `threeDriver(box, options)` |
|
|
287
|
-
| `@fusion.dev/helix-graph/shapes` | `shapeFromModel`, `shapeFromTriangles`, `shapeFromSvg`, `shapeFromImage`, `shapeFromPath`, `shapeFromInside`, `inflate`, `sphere`, `torus`, `box`, `onSphere`, and the `ShapeField` type |
|
|
304
|
+
| `@fusion.dev/helix-graph/shapes` | `shapeFromModel`, `shapeFromTriangles`, `shapeFromSvg`, `shapeFromImage`, `shapeFromPath`, `shapeFromInside`, `inflate`, `sphere`, `torus`, `box`, `onSphere`, `surfaceOf`, and the `ShapeField` type |
|
|
288
305
|
|
|
289
306
|
**Sigma, cosmos.gl, three.js and d3-force-3d are optional peers, and graphology is the page's.** Each
|
|
290
307
|
driver types its renderer by the calls it makes on it, so a page brings
|
|
@@ -4,6 +4,16 @@ export interface ArrangedNode {
|
|
|
4
4
|
group?: string | undefined;
|
|
5
5
|
id: string;
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* The hub each note hangs from, and how connected each note is: a note
|
|
9
|
+
* hangs from its most connected neighbor in its group, where that neighbor
|
|
10
|
+
* is more connected than it is, and the note at the end of the chain is the
|
|
11
|
+
* hub. A note nothing is more connected than is its own hub.
|
|
12
|
+
*/
|
|
13
|
+
export declare const hubsOf: (nodes: readonly ArrangedNode[], links: readonly SeedLink[]) => {
|
|
14
|
+
hubOf: (id: string) => string;
|
|
15
|
+
rank: (id: string) => number;
|
|
16
|
+
};
|
|
7
17
|
/**
|
|
8
18
|
* Places for every note from the graph's structure alone, with no layout to
|
|
9
19
|
* wait for: **each group around a circle, or a sphere in a solid, each hub in its group, and each
|
|
@@ -30,7 +30,7 @@ const plus = (one, other) => ({
|
|
|
30
30
|
y: one.y + other.y,
|
|
31
31
|
...(one.z === undefined && other.z === undefined ? {} : { z: (one.z ?? 0) + (other.z ?? 0) }),
|
|
32
32
|
});
|
|
33
|
-
export const
|
|
33
|
+
export const hubsOf = (nodes, links) => {
|
|
34
34
|
const groupOf = new Map(nodes.map((node) => [node.id, node.group ?? '']));
|
|
35
35
|
const degree = new Map();
|
|
36
36
|
const neighbors = new Map();
|
|
@@ -75,6 +75,11 @@ export const arrangeByGroup = (nodes, links, { solid = false } = {}) => {
|
|
|
75
75
|
}
|
|
76
76
|
return at;
|
|
77
77
|
};
|
|
78
|
+
return { hubOf, rank };
|
|
79
|
+
};
|
|
80
|
+
export const arrangeByGroup = (nodes, links, { solid = false } = {}) => {
|
|
81
|
+
const groupOf = new Map(nodes.map((node) => [node.id, node.group ?? '']));
|
|
82
|
+
const { hubOf, rank } = hubsOf(nodes, links);
|
|
78
83
|
const groups = new Map();
|
|
79
84
|
for (const { id } of nodes) {
|
|
80
85
|
const group = groupOf.get(id);
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { HelixGraphPanEvent, HelixGraphPartChangeEvent, HelixGraphSignalEvent, HelixGraphZoomEvent, isRtl, watchTheme, } from '@fusion.dev/helix-core';
|
|
2
|
-
import { arrangeByGroup } from './arrange.js';
|
|
2
|
+
import { arrangeByGroup, hubsOf } from './arrange.js';
|
|
3
3
|
import { diffGraph, isEmpty, linkKey, } from './diff.js';
|
|
4
4
|
import { graphColorsFrom, looksOf, toneColor, } from './looks.js';
|
|
5
5
|
import { seedPositions } from './seed.js';
|
|
6
6
|
import { fieldOf, fitInStencil } from './stencil.js';
|
|
7
|
+
const clustersOf = (nodes, links) => {
|
|
8
|
+
const groupOf = new Map(nodes.map((node) => [node.id, node.group ?? '']));
|
|
9
|
+
const { hubOf } = hubsOf(nodes, links);
|
|
10
|
+
return (id) => [groupOf.get(id) ?? '', hubOf(id)];
|
|
11
|
+
};
|
|
7
12
|
const viewportOf = (frame) => {
|
|
8
13
|
const box = (frame.shadowRoot?.querySelector('[part="viewport"]') ?? frame).getBoundingClientRect();
|
|
9
14
|
return { height: box.height, width: box.width };
|
|
@@ -98,7 +103,7 @@ export const connectGraph = (frame, driver, options = {}) => {
|
|
|
98
103
|
const fit = options.stencil && ids.size > 0 && unplaced.size === ids.size
|
|
99
104
|
? fitInStencil(arrangeByGroup(nodes, [...nextLinks.values()], {
|
|
100
105
|
solid: fieldOf(options.stencil).depth > 0,
|
|
101
|
-
}), options.stencil)
|
|
106
|
+
}), options.stencil, { clustersOf: clustersOf(nodes, [...nextLinks.values()]) })
|
|
102
107
|
: undefined;
|
|
103
108
|
const shaped = fit?.positions;
|
|
104
109
|
if (fit?.frame) {
|
|
@@ -14,6 +14,16 @@ export interface ShapeField {
|
|
|
14
14
|
distance(x: number, y: number, z?: number): number;
|
|
15
15
|
/** How tall the box is. Its y points down, as an SVG's does. */
|
|
16
16
|
readonly height: number;
|
|
17
|
+
/**
|
|
18
|
+
* The surface the field was read from, where it was read from one, in the
|
|
19
|
+
* box's units: corners three numbers each, and triangles three corners
|
|
20
|
+
* each. A renderer draws it in place of a surface it would make from the
|
|
21
|
+
* field, since it holds every detail the field's cells are too coarse for.
|
|
22
|
+
*/
|
|
23
|
+
readonly mesh?: {
|
|
24
|
+
corners: Float32Array;
|
|
25
|
+
triangles: Uint32Array;
|
|
26
|
+
} | undefined;
|
|
17
27
|
/** How wide the box is. */
|
|
18
28
|
readonly width: number;
|
|
19
29
|
}
|
|
@@ -67,7 +67,14 @@ export interface StencilFrame {
|
|
|
67
67
|
shell: number;
|
|
68
68
|
}
|
|
69
69
|
/** `fitToStencil`, saying where it put the stencil's box as well, where it fitted the places at all. */
|
|
70
|
-
export declare const fitInStencil: (positions: ReadonlyMap<string, GraphPoint>, stencil: GraphStencil
|
|
70
|
+
export declare const fitInStencil: (positions: ReadonlyMap<string, GraphPoint>, stencil: GraphStencil, { clustersOf, }?: {
|
|
71
|
+
/**
|
|
72
|
+
* The clusters a note is in, broadest first, such as its group and then
|
|
73
|
+
* its hub. Each cluster is given a piece of a solid of its own, as many
|
|
74
|
+
* places as it has notes, so no cluster is cut in two.
|
|
75
|
+
*/
|
|
76
|
+
clustersOf?: ((id: string) => readonly string[]) | undefined;
|
|
77
|
+
}) => {
|
|
71
78
|
frame?: StencilFrame | undefined;
|
|
72
79
|
positions: Map<string, GraphPoint>;
|
|
73
80
|
};
|
|
@@ -95,7 +95,7 @@ const nested = (items, keys) => {
|
|
|
95
95
|
return out;
|
|
96
96
|
};
|
|
97
97
|
export const fitToStencil = (positions, stencil) => fitInStencil(positions, stencil).positions;
|
|
98
|
-
export const fitInStencil = (positions, stencil) => {
|
|
98
|
+
export const fitInStencil = (positions, stencil, { clustersOf, } = {}) => {
|
|
99
99
|
const notes = [...positions].map(([id, at]) => ({ at, id }));
|
|
100
100
|
if (notes.length === 0) {
|
|
101
101
|
return { positions: new Map() };
|
|
@@ -175,7 +175,8 @@ export const fitInStencil = (positions, stencil) => {
|
|
|
175
175
|
const ordered = [...places].sort((one, other) => (targets[one][axis] ?? 0) - (targets[other][axis] ?? 0));
|
|
176
176
|
return [ordered.slice(0, first), ordered.slice(first)];
|
|
177
177
|
};
|
|
178
|
-
const
|
|
178
|
+
const keysOf = new Map(notes.map((note) => [note.id, clustersOf?.(note.id) ?? []]));
|
|
179
|
+
const halve = (ours, theirs, level = 0) => {
|
|
179
180
|
if (ours.length === 1) {
|
|
180
181
|
fitted.set(ours[0].id, targets[theirs[0]]);
|
|
181
182
|
return;
|
|
@@ -183,12 +184,58 @@ export const fitInStencil = (positions, stencil) => {
|
|
|
183
184
|
if (ours.length === 0) {
|
|
184
185
|
return;
|
|
185
186
|
}
|
|
187
|
+
const clusters = new Map();
|
|
188
|
+
for (const note of ours) {
|
|
189
|
+
const key = keysOf.get(note.id)?.[level];
|
|
190
|
+
if (key !== undefined) {
|
|
191
|
+
const list = clusters.get(key);
|
|
192
|
+
if (list) {
|
|
193
|
+
list.push(note);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
clusters.set(key, [note]);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (clusters.size === 1) {
|
|
201
|
+
halve(ours, theirs, level + 1);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (clusters.size > 1 &&
|
|
205
|
+
[...clusters.values()].reduce((sum, list) => sum + list.length, 0) === ours.length) {
|
|
206
|
+
const middles = [...clusters.values()].map((list) => ({
|
|
207
|
+
list,
|
|
208
|
+
middle: {
|
|
209
|
+
x: list.reduce((sum, note) => sum + note.at.x, 0) / list.length,
|
|
210
|
+
y: list.reduce((sum, note) => sum + note.at.y, 0) / list.length,
|
|
211
|
+
z: list.reduce((sum, note) => sum + (note.at.z ?? 0), 0) / list.length,
|
|
212
|
+
},
|
|
213
|
+
}));
|
|
214
|
+
const axis = widest(middles.map((one) => one.middle));
|
|
215
|
+
middles.sort((one, other) => one.middle[axis] - other.middle[axis]);
|
|
216
|
+
let cut = 1;
|
|
217
|
+
let count = 0;
|
|
218
|
+
let best = Number.POSITIVE_INFINITY;
|
|
219
|
+
let first = 0;
|
|
220
|
+
for (let at = 0; at < middles.length - 1; at++) {
|
|
221
|
+
count += middles[at].list.length;
|
|
222
|
+
if (Math.abs(count - ours.length / 2) < best) {
|
|
223
|
+
best = Math.abs(count - ours.length / 2);
|
|
224
|
+
cut = at + 1;
|
|
225
|
+
first = count;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const [near, rest] = halveTargets(theirs, first);
|
|
229
|
+
halve(middles.slice(0, cut).flatMap((one) => one.list), near, level);
|
|
230
|
+
halve(middles.slice(cut).flatMap((one) => one.list), rest, level);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
186
233
|
const mine = widest(ours.map((note) => note.at));
|
|
187
234
|
ours.sort((one, other) => (one.at[mine] ?? 0) - (other.at[mine] ?? 0));
|
|
188
235
|
const half = Math.floor(ours.length / 2);
|
|
189
236
|
const [near, rest] = halveTargets(theirs, half);
|
|
190
|
-
halve(ours.slice(0, half), near);
|
|
191
|
-
halve(ours.slice(half), rest);
|
|
237
|
+
halve(ours.slice(0, half), near, level);
|
|
238
|
+
halve(ours.slice(half), rest, level);
|
|
192
239
|
};
|
|
193
240
|
halve([...notes], targets.map((_, at) => at));
|
|
194
241
|
return { frame: { field, middle, scale, shell: thickness }, positions: fitted };
|
|
@@ -7,6 +7,11 @@ export type OnScreen = (id: string) => {
|
|
|
7
7
|
x: number;
|
|
8
8
|
y: number;
|
|
9
9
|
} | undefined;
|
|
10
|
+
/** A point and how to bend toward it: the control point of a curve on screen. */
|
|
11
|
+
export type Bend = (from: string, to: string) => {
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
} | undefined;
|
|
10
15
|
/** Whether an `rgb()` color is light, which a light ink says the ground is dark. */
|
|
11
16
|
export declare const isLight: (color: string) => boolean;
|
|
12
17
|
/** Eases in and out, so a signal leaves and arrives gently. */
|
|
@@ -24,7 +29,7 @@ export interface Box {
|
|
|
24
29
|
* over ten thousand notes draw ten dots. It draws only while something is in
|
|
25
30
|
* flight, and takes no pointer, so a press reaches the note under it.
|
|
26
31
|
*/
|
|
27
|
-
export declare const signalsOver: (container: HTMLElement, onScreen: OnScreen) => {
|
|
32
|
+
export declare const signalsOver: (container: HTMLElement, onScreen: OnScreen, bend?: Bend) => {
|
|
28
33
|
look: (looks: GraphLooks) => void;
|
|
29
34
|
signal: (from: string, to: string, colors: {
|
|
30
35
|
from: string;
|
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
|
}
|
package/dist/shapes/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
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';
|
|
2
3
|
export { type ImageOptions, shapeFromImage, shapeFromSvg } from './flat.js';
|
|
3
4
|
export { shapeFromModel } from './model.js';
|
|
4
5
|
export { box, onSphere, sphere, torus } from './solids.js';
|
package/dist/shapes/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { fieldFromInside as shapeFromInside, fieldFromPath as shapeFromPath, inflate, } from '../connect-graph/shape-field.js';
|
|
2
|
+
export { surfaceOf } from '../three/surface.js';
|
|
2
3
|
export { shapeFromImage, shapeFromSvg } from './flat.js';
|
|
3
4
|
export { shapeFromModel } from './model.js';
|
|
4
5
|
export { box, onSphere, sphere, torus } from './solids.js';
|
package/dist/shapes/triangles.js
CHANGED
|
@@ -70,9 +70,23 @@ export const shapeFromTriangles = (positions, index, { resolution = SOLID } = {}
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
-
|
|
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, {
|
|
74
83
|
depth: nz * cell,
|
|
75
84
|
height: ny * cell,
|
|
76
85
|
width: nx * cell,
|
|
77
86
|
});
|
|
87
|
+
return {
|
|
88
|
+
...field,
|
|
89
|
+
distance: field.distance,
|
|
90
|
+
mesh: { corners: meshCorners, triangles: meshTriangles },
|
|
91
|
+
};
|
|
78
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
|
|
package/dist/three/surface.d.ts
CHANGED
|
@@ -7,7 +7,12 @@ import type { ShapeField } from '../connect-graph/shape-field.js';
|
|
|
7
7
|
* each edge it crosses. Smooth enough to hide what is behind a shape, and
|
|
8
8
|
* made in a moment.
|
|
9
9
|
*/
|
|
10
|
-
export declare const surfaceOf: (field: ShapeField, at
|
|
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
|
+
}) => {
|
|
11
16
|
corners: Float32Array;
|
|
12
17
|
triangles: Uint32Array;
|
|
13
18
|
};
|
package/dist/three/surface.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const CELLS = 72;
|
|
2
|
-
export const surfaceOf = (field, at = 0) => {
|
|
2
|
+
export const surfaceOf = (field, { at = 0, cells = CELLS, } = {}) => {
|
|
3
3
|
const { depth, height, width } = field;
|
|
4
|
-
const cell = Math.max(width, height, depth) /
|
|
4
|
+
const cell = Math.max(width, height, depth) / cells;
|
|
5
5
|
const nx = Math.ceil(width / cell) + 3;
|
|
6
6
|
const ny = Math.ceil(height / cell) + 3;
|
|
7
7
|
const nz = Math.ceil(depth / cell) + 3;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { forceCenter, forceLink, forceManyBody, forceSimulation, } from 'd3-force-3d';
|
|
2
|
-
import { AdditiveBlending, BufferAttribute, BufferGeometry, DoubleSide, LineSegments, Mesh, NormalBlending, PerspectiveCamera, Points, Raycaster, Scene, ShaderMaterial, TOUCH, Vector2, Vector3, WebGLRenderer, } from 'three';
|
|
2
|
+
import { AdditiveBlending, BoxGeometry, BufferAttribute, BufferGeometry, DoubleSide, Euler, InstancedBufferAttribute, InstancedMesh, LineSegments, Matrix4, Mesh, NormalBlending, OctahedronGeometry, PerspectiveCamera, Points, Quaternion, Raycaster, Scene, ShaderMaterial, TOUCH, Vector2, Vector3, WebGLRenderer, } from 'three';
|
|
3
3
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
4
|
+
import { mergeVertices } from 'three/addons/utils/BufferGeometryUtils.js';
|
|
4
5
|
import { ease, gesturesHeld, groupsOver, isLight, namesOver, signalsOver, } from '../overlay/overlay.js';
|
|
5
6
|
import { surfaceOf } from './surface.js';
|
|
6
7
|
const SHAPES = { circle: 0, diamond: 2, ring: 0, square: 1 };
|
|
@@ -11,6 +12,10 @@ const ZOOM = 1.5;
|
|
|
11
12
|
const SAMPLE = 96;
|
|
12
13
|
const LINKS = { dark: { close: 0.3, far: 0.05 }, light: { close: 0.55, far: 0.12 } };
|
|
13
14
|
const REST = 8000;
|
|
15
|
+
const WASH = { dark: 0.14, full: 1.5, light: 0.18, notes: 6, reach: 3.5 };
|
|
16
|
+
const BLOOM = { dark: 0.28, light: 0 };
|
|
17
|
+
const LIGHT = new Vector3(-0.4, 0.8, 0.45).normalize();
|
|
18
|
+
const LIFT = 0.42;
|
|
14
19
|
const FOV = 40;
|
|
15
20
|
const FADE = `
|
|
16
21
|
uniform float middle;
|
|
@@ -23,6 +28,29 @@ float fade(float depth) {
|
|
|
23
28
|
return mix(gentle, sharp, steep);
|
|
24
29
|
}
|
|
25
30
|
`;
|
|
31
|
+
const ORB = `
|
|
32
|
+
uniform vec3 light;
|
|
33
|
+
uniform float dark;
|
|
34
|
+
|
|
35
|
+
vec4 orb(vec3 base, vec3 n) {
|
|
36
|
+
float diffuse = max(dot(n, light), 0.0);
|
|
37
|
+
float rim = pow(1.0 - clamp(n.z, 0.0, 1.0), 2.2);
|
|
38
|
+
float core = pow(clamp(n.z, 0.0, 1.0), 5.0);
|
|
39
|
+
float glint = pow(max(dot(n, normalize(light + vec3(0.0, 0.0, 1.0))), 0.0), 140.0);
|
|
40
|
+
vec3 energy = base * (0.08 + 0.2 * diffuse);
|
|
41
|
+
energy += base * core * 0.5;
|
|
42
|
+
energy += mix(base, vec3(1.0), 0.6) * pow(clamp(n.z, 0.0, 1.0), 40.0) * 0.55;
|
|
43
|
+
energy += mix(base, vec3(1.0), 0.25) * rim * 1.2;
|
|
44
|
+
energy += vec3(glint * 0.55);
|
|
45
|
+
vec3 frost = mix(base, vec3(1.0), 0.45 * pow(clamp(n.z, 0.0, 1.0), 1.5));
|
|
46
|
+
frost *= 0.86 + 0.14 * diffuse;
|
|
47
|
+
frost = mix(frost, base * 0.78, rim * 0.75);
|
|
48
|
+
frost += vec3(glint * 0.6);
|
|
49
|
+
return dark > 0.5
|
|
50
|
+
? vec4(energy, mix(0.5, 1.0, max(rim, core)))
|
|
51
|
+
: vec4(frost, mix(0.78, 1.0, rim));
|
|
52
|
+
}
|
|
53
|
+
`;
|
|
26
54
|
const VERTEX = `
|
|
27
55
|
${FADE}
|
|
28
56
|
attribute float size;
|
|
@@ -32,18 +60,29 @@ attribute float glow;
|
|
|
32
60
|
|
|
33
61
|
uniform float scale;
|
|
34
62
|
uniform float unit;
|
|
63
|
+
uniform float bloom;
|
|
64
|
+
uniform float halo;
|
|
35
65
|
|
|
36
66
|
varying vec4 vTint;
|
|
37
67
|
varying float vShape;
|
|
38
68
|
varying float vGlow;
|
|
39
69
|
varying float vFade;
|
|
70
|
+
varying float vRoom;
|
|
71
|
+
varying vec3 vCenter;
|
|
72
|
+
varying float vRadius;
|
|
40
73
|
|
|
41
74
|
void main() {
|
|
42
75
|
vec4 view = modelViewMatrix * vec4(position, 1.0);
|
|
43
76
|
gl_Position = projectionMatrix * view;
|
|
44
77
|
float depth = max(-view.z, 0.001);
|
|
78
|
+
/* The halo's pass has room around the note for it, which every note has
|
|
79
|
+
where the ground is dark, and more for a glowing one; the body's pass
|
|
80
|
+
is the note and no more. */
|
|
81
|
+
vRoom = halo > 0.5 ? 1.0 + (bloom > 0.0 ? 0.9 : 0.0) + glow * 1.4 : 1.0;
|
|
45
82
|
/* As large as the note is, at the distance it is: nearer is larger. */
|
|
46
|
-
gl_PointSize = size * unit * scale / depth *
|
|
83
|
+
gl_PointSize = size * unit * scale / depth * vRoom;
|
|
84
|
+
vCenter = view.xyz;
|
|
85
|
+
vRadius = size * unit * 0.5;
|
|
47
86
|
vFade = fade(depth);
|
|
48
87
|
vTint = tint;
|
|
49
88
|
vShape = shape;
|
|
@@ -51,24 +90,88 @@ void main() {
|
|
|
51
90
|
}
|
|
52
91
|
`;
|
|
53
92
|
const FRAGMENT = `
|
|
93
|
+
uniform mat4 projectionMatrix;
|
|
94
|
+
${ORB}
|
|
95
|
+
varying vec4 vTint;
|
|
96
|
+
varying float vShape;
|
|
97
|
+
varying float vFade;
|
|
98
|
+
varying vec3 vCenter;
|
|
99
|
+
varying float vRadius;
|
|
100
|
+
|
|
101
|
+
void main() {
|
|
102
|
+
if (vShape > 0.5) discard;
|
|
103
|
+
vec2 q = (gl_PointCoord * 2.0 - 1.0) * vec2(1.0, -1.0);
|
|
104
|
+
float d2 = dot(q, q);
|
|
105
|
+
if (d2 > 1.0) discard;
|
|
106
|
+
vec3 n = vec3(q, sqrt(1.0 - d2));
|
|
107
|
+
vec4 clip = projectionMatrix * vec4(vCenter + n * vRadius, 1.0);
|
|
108
|
+
gl_FragDepth = clip.z / clip.w * 0.5 + 0.5;
|
|
109
|
+
/* Antialiased in the last few percent only, so the glowing rim runs to the edge. */
|
|
110
|
+
float edge = 1.0 - smoothstep(0.965, 1.0, sqrt(d2));
|
|
111
|
+
vec4 lit = orb(vTint.rgb, n);
|
|
112
|
+
float alpha = edge * lit.a * vTint.a * vFade;
|
|
113
|
+
if (alpha < 0.01) discard;
|
|
114
|
+
gl_FragColor = vec4(lit.rgb, alpha);
|
|
115
|
+
}
|
|
116
|
+
`;
|
|
117
|
+
const HALO_FRAGMENT = `
|
|
118
|
+
uniform float bloom;
|
|
54
119
|
varying vec4 vTint;
|
|
55
120
|
varying float vShape;
|
|
56
121
|
varying float vGlow;
|
|
57
122
|
varying float vFade;
|
|
123
|
+
varying float vRoom;
|
|
58
124
|
|
|
59
125
|
void main() {
|
|
60
|
-
vec2
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
float
|
|
66
|
-
|
|
67
|
-
vec3 color = vTint.rgb * mix(1.15, 0.8, clamp(d, 0.0, 1.0));
|
|
68
|
-
float halo = vGlow * (1.0 - smoothstep(1.0, 1.0 / body, d)) * 0.55;
|
|
69
|
-
float alpha = (inside + halo * (1.0 - inside)) * vTint.a * vFade;
|
|
126
|
+
vec2 q = (gl_PointCoord * 2.0 - 1.0) * vRoom;
|
|
127
|
+
float d = vShape > 1.5 ? (abs(q.x) + abs(q.y)) * 0.78 : vShape > 0.5 ? max(abs(q.x), abs(q.y)) * 1.08 : length(q);
|
|
128
|
+
/* From just inside the body's edge, under its antialiasing, so no seam of
|
|
129
|
+
dark ground shows between the body and its halo. */
|
|
130
|
+
if (d < 0.965) discard;
|
|
131
|
+
float past = clamp((d - 0.965) / max(vRoom - 0.965, 0.001), 0.0, 1.0);
|
|
132
|
+
float alpha = (bloom + vGlow * 0.6) * pow(1.0 - past, 2.4) * vTint.a * vFade;
|
|
70
133
|
if (alpha < 0.01) discard;
|
|
71
|
-
gl_FragColor = vec4(
|
|
134
|
+
gl_FragColor = vec4(vTint.rgb, alpha);
|
|
135
|
+
}
|
|
136
|
+
`;
|
|
137
|
+
const SHAPE_VERTEX = `
|
|
138
|
+
${FADE}
|
|
139
|
+
attribute vec4 tone;
|
|
140
|
+
attribute vec4 frame;
|
|
141
|
+
varying vec3 vNormal;
|
|
142
|
+
varying vec4 vTone;
|
|
143
|
+
varying vec4 vFrame;
|
|
144
|
+
varying float vFade;
|
|
145
|
+
|
|
146
|
+
void main() {
|
|
147
|
+
vec4 view = viewMatrix * modelMatrix * instanceMatrix * vec4(position, 1.0);
|
|
148
|
+
gl_Position = projectionMatrix * view;
|
|
149
|
+
vNormal = normalize(normalMatrix * mat3(instanceMatrix) * normal);
|
|
150
|
+
vTone = tone;
|
|
151
|
+
vFrame = frame;
|
|
152
|
+
vFade = fade(max(-view.z, 0.001));
|
|
153
|
+
}
|
|
154
|
+
`;
|
|
155
|
+
const SHAPE_FRAGMENT = `
|
|
156
|
+
uniform float dark;
|
|
157
|
+
varying vec3 vNormal;
|
|
158
|
+
varying vec4 vTone;
|
|
159
|
+
varying vec4 vFrame;
|
|
160
|
+
varying float vFade;
|
|
161
|
+
|
|
162
|
+
void main() {
|
|
163
|
+
float e = min(min(vFrame.x, vFrame.y), min(vFrame.z, vFrame.w));
|
|
164
|
+
float w = fwidth(e);
|
|
165
|
+
/* The edge as a line about a pixel and a half wide, whatever the size. */
|
|
166
|
+
float line = 1.0 - smoothstep(w * 0.6, w * 2.2, e);
|
|
167
|
+
float inner = exp(-e * 12.0);
|
|
168
|
+
float facing = abs(normalize(vNormal).z);
|
|
169
|
+
vec3 edge = dark > 0.5 ? mix(vTone.rgb, vec3(1.0), 0.35) : vTone.rgb * 0.8;
|
|
170
|
+
vec3 face = vTone.rgb * (dark > 0.5 ? 0.3 + 0.7 * inner : 1.0);
|
|
171
|
+
float faceAlpha = (dark > 0.5 ? 0.1 : 0.16) + 0.3 * inner + 0.12 * (1.0 - facing);
|
|
172
|
+
float alpha = mix(faceAlpha, 1.0, line) * vTone.a * vFade;
|
|
173
|
+
if (alpha < 0.01) discard;
|
|
174
|
+
gl_FragColor = vec4(mix(face, edge, line), alpha);
|
|
72
175
|
}
|
|
73
176
|
`;
|
|
74
177
|
const LINE_VERTEX = `
|
|
@@ -94,29 +197,64 @@ void main() {
|
|
|
94
197
|
}
|
|
95
198
|
`;
|
|
96
199
|
const SOLID_VERTEX = `
|
|
200
|
+
attribute vec4 wash;
|
|
97
201
|
varying vec3 vNormal;
|
|
98
202
|
varying vec3 vView;
|
|
203
|
+
varying vec4 vWash;
|
|
99
204
|
|
|
100
205
|
void main() {
|
|
101
206
|
vec4 view = modelViewMatrix * vec4(position, 1.0);
|
|
102
207
|
gl_Position = projectionMatrix * view;
|
|
103
208
|
vNormal = normalize(normalMatrix * normal);
|
|
104
209
|
vView = normalize(-view.xyz);
|
|
210
|
+
vWash = wash;
|
|
105
211
|
}
|
|
106
212
|
`;
|
|
107
213
|
const SOLID_FRAGMENT = `
|
|
108
214
|
uniform vec3 ground;
|
|
109
215
|
uniform vec3 ink;
|
|
216
|
+
uniform float strength;
|
|
110
217
|
varying vec3 vNormal;
|
|
111
218
|
varying vec3 vView;
|
|
219
|
+
varying vec4 vWash;
|
|
112
220
|
|
|
113
221
|
void main() {
|
|
114
|
-
/*
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
222
|
+
/* **Each region washed in its notes' color**, as a translucent stain on
|
|
223
|
+
the ground's color, and lit: a breath of ink where light from above and
|
|
224
|
+
to the left falls, and the region's own color glowing at the edge
|
|
225
|
+
turning away. Felt more than seen: the notes on it are what is drawn. */
|
|
226
|
+
vec3 normal = normalize(vNormal);
|
|
227
|
+
vec3 view = normalize(vView);
|
|
228
|
+
if (dot(normal, view) < 0.0) normal = -normal;
|
|
229
|
+
float edge = 1.0 - dot(normal, view);
|
|
230
|
+
float lit = max(dot(normal, normalize(vec3(-0.4, 0.8, 0.45))), 0.0);
|
|
231
|
+
float washed = strength * vWash.a;
|
|
232
|
+
vec3 base = mix(ground, vWash.rgb, washed);
|
|
233
|
+
vec3 shaded = mix(base, ink, 0.1 * pow(edge, 2.0) + 0.17 * lit * lit);
|
|
234
|
+
vec3 rim = mix(shaded, vWash.rgb, washed * 1.6 * pow(edge, 3.0));
|
|
235
|
+
gl_FragColor = vec4(rim, 1.0);
|
|
118
236
|
}
|
|
119
237
|
`;
|
|
238
|
+
const framed = (geometry) => {
|
|
239
|
+
const uv = geometry.getAttribute('uv');
|
|
240
|
+
const count = geometry.getAttribute('position').count;
|
|
241
|
+
const frame = new Float32Array(count * 4);
|
|
242
|
+
if (geometry.getIndex() && uv) {
|
|
243
|
+
for (let at = 0; at < count; at++) {
|
|
244
|
+
const u = uv.getX(at);
|
|
245
|
+
const v = uv.getY(at);
|
|
246
|
+
frame.set([u, 1 - u, v, 1 - v], at * 4);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
for (let at = 0; at < count; at++) {
|
|
251
|
+
const corner = at % 3;
|
|
252
|
+
frame.set([corner === 0 ? 1 : 0, corner === 1 ? 1 : 0, corner === 2 ? 1 : 0, 1], at * 4);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
geometry.setAttribute('frame', new BufferAttribute(frame, 4));
|
|
256
|
+
return geometry;
|
|
257
|
+
};
|
|
120
258
|
const channels = (color) => {
|
|
121
259
|
const [red = 128, green = 128, blue = 128] = color.match(/[\d.]+/g)?.map(Number) ?? [];
|
|
122
260
|
return [red / 255, green / 255, blue / 255];
|
|
@@ -160,6 +298,9 @@ export const threeDriver = (container, options = {}) => {
|
|
|
160
298
|
let scale = 1;
|
|
161
299
|
let positions = new Float32Array();
|
|
162
300
|
const uniforms = {
|
|
301
|
+
bloom: { value: BLOOM.dark },
|
|
302
|
+
dark: { value: 1 },
|
|
303
|
+
light: { value: new Vector3(-0.4, 0.8, 0.45).normalize() },
|
|
163
304
|
middle: { value: 1 },
|
|
164
305
|
radius: { value: 1 },
|
|
165
306
|
scale: { value: 1 },
|
|
@@ -179,13 +320,46 @@ export const threeDriver = (container, options = {}) => {
|
|
|
179
320
|
vertexShader: LINE_VERTEX,
|
|
180
321
|
});
|
|
181
322
|
const points = new Points(new BufferGeometry(), new ShaderMaterial({
|
|
182
|
-
depthWrite: false,
|
|
183
323
|
fragmentShader: FRAGMENT,
|
|
184
324
|
transparent: true,
|
|
185
|
-
uniforms,
|
|
325
|
+
uniforms: { ...uniforms, halo: { value: 0 } },
|
|
186
326
|
vertexShader: VERTEX,
|
|
187
327
|
}));
|
|
188
328
|
points.frustumCulled = false;
|
|
329
|
+
const halos = new Points(points.geometry, new ShaderMaterial({
|
|
330
|
+
depthWrite: false,
|
|
331
|
+
fragmentShader: HALO_FRAGMENT,
|
|
332
|
+
transparent: true,
|
|
333
|
+
uniforms: { ...uniforms, halo: { value: 1 } },
|
|
334
|
+
vertexShader: VERTEX,
|
|
335
|
+
}));
|
|
336
|
+
halos.frustumCulled = false;
|
|
337
|
+
halos.renderOrder = 1;
|
|
338
|
+
const shapeMaterial = new ShaderMaterial({
|
|
339
|
+
depthWrite: false,
|
|
340
|
+
fragmentShader: SHAPE_FRAGMENT,
|
|
341
|
+
side: DoubleSide,
|
|
342
|
+
transparent: true,
|
|
343
|
+
uniforms: {
|
|
344
|
+
dark: uniforms.dark,
|
|
345
|
+
middle: uniforms.middle,
|
|
346
|
+
radius: uniforms.radius,
|
|
347
|
+
steep: uniforms.steep,
|
|
348
|
+
},
|
|
349
|
+
vertexShader: SHAPE_VERTEX,
|
|
350
|
+
});
|
|
351
|
+
const solids = {
|
|
352
|
+
cube: {
|
|
353
|
+
geometry: framed(new BoxGeometry(1.5, 1.5, 1.5)),
|
|
354
|
+
mesh: undefined,
|
|
355
|
+
spots: [],
|
|
356
|
+
},
|
|
357
|
+
octahedron: {
|
|
358
|
+
geometry: framed(new OctahedronGeometry(1.15)),
|
|
359
|
+
mesh: undefined,
|
|
360
|
+
spots: [],
|
|
361
|
+
},
|
|
362
|
+
};
|
|
189
363
|
const quiet = new LineSegments(new BufferGeometry(), lines(LINKS.dark.far));
|
|
190
364
|
const lit = new LineSegments(new BufferGeometry(), lines(0.9));
|
|
191
365
|
quiet.frustumCulled = false;
|
|
@@ -196,13 +370,17 @@ export const threeDriver = (container, options = {}) => {
|
|
|
196
370
|
polygonOffsetFactor: 2,
|
|
197
371
|
polygonOffsetUnits: 2,
|
|
198
372
|
side: DoubleSide,
|
|
199
|
-
uniforms: {
|
|
373
|
+
uniforms: {
|
|
374
|
+
ground: { value: [0, 0, 0] },
|
|
375
|
+
ink: { value: [1, 1, 1] },
|
|
376
|
+
strength: { value: WASH.dark },
|
|
377
|
+
},
|
|
200
378
|
vertexShader: SOLID_VERTEX,
|
|
201
379
|
}));
|
|
202
380
|
solid.renderOrder = -1;
|
|
203
381
|
solid.visible = false;
|
|
204
382
|
solid.frustumCulled = false;
|
|
205
|
-
scene.add(solid, quiet, lit, points);
|
|
383
|
+
scene.add(solid, quiet, lit, points, halos);
|
|
206
384
|
let shapeFrame;
|
|
207
385
|
let surfaced = false;
|
|
208
386
|
let solidAt = Number.NaN;
|
|
@@ -217,21 +395,146 @@ export const threeDriver = (container, options = {}) => {
|
|
|
217
395
|
return;
|
|
218
396
|
}
|
|
219
397
|
solidAt = scale;
|
|
220
|
-
const {
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
398
|
+
const { field } = frame;
|
|
399
|
+
const inset = frame.shell * 1.15;
|
|
400
|
+
let geometry = new BufferGeometry();
|
|
401
|
+
if (field.mesh) {
|
|
402
|
+
geometry.setAttribute('position', new BufferAttribute(field.mesh.corners.slice(), 3));
|
|
403
|
+
geometry.setIndex(new BufferAttribute(field.mesh.triangles, 1));
|
|
404
|
+
geometry = mergeVertices(geometry);
|
|
405
|
+
const place = geometry.getAttribute('position');
|
|
406
|
+
const step = Math.max(field.width, field.height, field.depth) / 64;
|
|
407
|
+
for (let at = 0; at < place.count; at++) {
|
|
408
|
+
const x = place.getX(at);
|
|
409
|
+
const y = place.getY(at);
|
|
410
|
+
const z = place.getZ(at);
|
|
411
|
+
const gx = field.distance(x + step, y, z) - field.distance(x - step, y, z);
|
|
412
|
+
const gy = field.distance(x, y + step, z) - field.distance(x, y - step, z);
|
|
413
|
+
const gz = field.distance(x, y, z + step) - field.distance(x, y, z - step);
|
|
414
|
+
const length = Math.hypot(gx, gy, gz) || 1;
|
|
415
|
+
place.setXYZ(at, x - (gx / length) * inset, y - (gy / length) * inset, z - (gz / length) * inset);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
const { corners, triangles } = surfaceOf(field, { at: -inset });
|
|
420
|
+
geometry.setAttribute('position', new BufferAttribute(corners, 3));
|
|
421
|
+
geometry.setIndex(new BufferAttribute(triangles, 1));
|
|
228
422
|
}
|
|
423
|
+
const place = geometry.getAttribute('position');
|
|
424
|
+
const { depth, height, width } = field;
|
|
425
|
+
for (let at = 0; at < place.count; at++) {
|
|
426
|
+
place.setXYZ(at, (frame.middle.x + (place.getX(at) - width / 2) * frame.scale) * scale, (frame.middle.y - (place.getY(at) - height / 2) * frame.scale) * scale, (frame.middle.z + (place.getZ(at) - depth / 2) * frame.scale) * scale);
|
|
427
|
+
}
|
|
428
|
+
geometry.computeVertexNormals();
|
|
229
429
|
solid.geometry.dispose();
|
|
230
|
-
solid.geometry =
|
|
231
|
-
solid.geometry.setAttribute('position', new BufferAttribute(corners, 3));
|
|
232
|
-
solid.geometry.setIndex(new BufferAttribute(triangles, 1));
|
|
233
|
-
solid.geometry.computeVertexNormals();
|
|
430
|
+
solid.geometry = geometry;
|
|
234
431
|
solid.visible = true;
|
|
432
|
+
tieSurface();
|
|
433
|
+
};
|
|
434
|
+
let washFrom = new Int32Array();
|
|
435
|
+
let washWeight = new Float32Array();
|
|
436
|
+
let noteTints = new Float32Array();
|
|
437
|
+
const tieSurface = () => {
|
|
438
|
+
const place = solid.geometry.getAttribute('position');
|
|
439
|
+
if (!solid.visible || !place || ids.length === 0) {
|
|
440
|
+
washFrom = new Int32Array();
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const sample = Math.min(ids.length, 200);
|
|
444
|
+
const nearest = [];
|
|
445
|
+
for (let step = 0; step < sample; step++) {
|
|
446
|
+
const one = Math.floor((step * ids.length) / sample);
|
|
447
|
+
let best = Number.POSITIVE_INFINITY;
|
|
448
|
+
for (let other = 0; other < ids.length; other++) {
|
|
449
|
+
if (other !== one) {
|
|
450
|
+
best = Math.min(best, at(one).distanceToSquared(at(other)));
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
nearest.push(Math.sqrt(best));
|
|
454
|
+
}
|
|
455
|
+
nearest.sort((one, other) => one - other);
|
|
456
|
+
const reach = (nearest[Math.floor(nearest.length / 2)] ?? 1) * WASH.reach;
|
|
457
|
+
const cells = new Map();
|
|
458
|
+
const key = (x, y, z) => `${Math.floor(x / reach)},${Math.floor(y / reach)},${Math.floor(z / reach)}`;
|
|
459
|
+
for (let spot = 0; spot < ids.length; spot++) {
|
|
460
|
+
const k = key(positions[spot * 3], positions[spot * 3 + 1], positions[spot * 3 + 2]);
|
|
461
|
+
const list = cells.get(k);
|
|
462
|
+
if (list) {
|
|
463
|
+
list.push(spot);
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
cells.set(k, [spot]);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
washFrom = new Int32Array(place.count * WASH.notes).fill(-1);
|
|
470
|
+
washWeight = new Float32Array(place.count * WASH.notes);
|
|
471
|
+
const best = new Float32Array(WASH.notes);
|
|
472
|
+
const which = new Int32Array(WASH.notes);
|
|
473
|
+
for (let corner = 0; corner < place.count; corner++) {
|
|
474
|
+
const x = place.getX(corner);
|
|
475
|
+
const y = place.getY(corner);
|
|
476
|
+
const z = place.getZ(corner);
|
|
477
|
+
best.fill(Number.POSITIVE_INFINITY);
|
|
478
|
+
which.fill(-1);
|
|
479
|
+
const cx = Math.floor(x / reach);
|
|
480
|
+
const cy = Math.floor(y / reach);
|
|
481
|
+
const cz = Math.floor(z / reach);
|
|
482
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
483
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
484
|
+
for (let dz = -1; dz <= 1; dz++) {
|
|
485
|
+
for (const spot of cells.get(`${cx + dx},${cy + dy},${cz + dz}`) ?? []) {
|
|
486
|
+
const far = (positions[spot * 3] - x) ** 2 +
|
|
487
|
+
(positions[spot * 3 + 1] - y) ** 2 +
|
|
488
|
+
(positions[spot * 3 + 2] - z) ** 2;
|
|
489
|
+
if (far < best[WASH.notes - 1]) {
|
|
490
|
+
let slot = WASH.notes - 1;
|
|
491
|
+
while (slot > 0 && best[slot - 1] > far) {
|
|
492
|
+
best[slot] = best[slot - 1];
|
|
493
|
+
which[slot] = which[slot - 1];
|
|
494
|
+
slot -= 1;
|
|
495
|
+
}
|
|
496
|
+
best[slot] = far;
|
|
497
|
+
which[slot] = spot;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
for (let slot = 0; slot < WASH.notes; slot++) {
|
|
504
|
+
washFrom[corner * WASH.notes + slot] = which[slot];
|
|
505
|
+
const far = Math.sqrt(best[slot]) / reach;
|
|
506
|
+
washWeight[corner * WASH.notes + slot] = far < 1 ? (1 - far * far) ** 2 : 0;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
washSurface();
|
|
510
|
+
};
|
|
511
|
+
const washSurface = () => {
|
|
512
|
+
const place = solid.geometry.getAttribute('position');
|
|
513
|
+
if (!place || washFrom.length !== place.count * WASH.notes || noteTints.length === 0) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const wash = new Float32Array(place.count * 4);
|
|
517
|
+
for (let corner = 0; corner < place.count; corner++) {
|
|
518
|
+
let red = 0;
|
|
519
|
+
let green = 0;
|
|
520
|
+
let blue = 0;
|
|
521
|
+
let total = 0;
|
|
522
|
+
for (let slot = 0; slot < WASH.notes; slot++) {
|
|
523
|
+
const spot = washFrom[corner * WASH.notes + slot];
|
|
524
|
+
const weight = washWeight[corner * WASH.notes + slot] * (noteTints[spot * 4 + 3] ?? 0);
|
|
525
|
+
if (spot < 0 || weight <= 0) {
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
red += noteTints[spot * 4] * weight;
|
|
529
|
+
green += noteTints[spot * 4 + 1] * weight;
|
|
530
|
+
blue += noteTints[spot * 4 + 2] * weight;
|
|
531
|
+
total += weight;
|
|
532
|
+
}
|
|
533
|
+
if (total > 0) {
|
|
534
|
+
wash.set([red / total, green / total, blue / total, Math.min(total / WASH.full, 1)], corner * 4);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
solid.geometry.setAttribute('wash', new BufferAttribute(wash, 4));
|
|
235
538
|
};
|
|
236
539
|
const at = (spot) => new Vector3(positions[spot * 3], positions[spot * 3 + 1], positions[spot * 3 + 2]);
|
|
237
540
|
const onScreen = (id) => {
|
|
@@ -256,7 +559,30 @@ export const threeDriver = (container, options = {}) => {
|
|
|
256
559
|
};
|
|
257
560
|
};
|
|
258
561
|
const held = gesturesHeld(container);
|
|
259
|
-
const
|
|
562
|
+
const arc = (from, to) => {
|
|
563
|
+
const one = index.get(from);
|
|
564
|
+
const other = index.get(to);
|
|
565
|
+
if (one === undefined || other === undefined) {
|
|
566
|
+
return undefined;
|
|
567
|
+
}
|
|
568
|
+
const a = at(one);
|
|
569
|
+
const b = at(other);
|
|
570
|
+
const middle = a.clone().add(b).multiplyScalar(0.5);
|
|
571
|
+
const center = points.geometry.boundingSphere?.center ?? controls.target;
|
|
572
|
+
const out = middle.clone().sub(center);
|
|
573
|
+
if (out.lengthSq() < 1e-9) {
|
|
574
|
+
out.copy(camera.up);
|
|
575
|
+
}
|
|
576
|
+
const peak = middle.add(out.normalize().multiplyScalar(a.distanceTo(b) * LIFT * 2));
|
|
577
|
+
const depth = peak.clone().applyMatrix4(camera.matrixWorldInverse).z;
|
|
578
|
+
if (depth > 0) {
|
|
579
|
+
return undefined;
|
|
580
|
+
}
|
|
581
|
+
peak.project(camera);
|
|
582
|
+
const { clientHeight: height, clientWidth: width } = container;
|
|
583
|
+
return { x: ((peak.x + 1) / 2) * width, y: ((1 - peak.y) / 2) * height };
|
|
584
|
+
};
|
|
585
|
+
const signals = signalsOver(container, (id) => nearOnScreen(id), arc);
|
|
260
586
|
const nearOnScreen = (id) => {
|
|
261
587
|
const spot = index.get(id);
|
|
262
588
|
if (spot === undefined) {
|
|
@@ -345,22 +671,91 @@ export const threeDriver = (container, options = {}) => {
|
|
|
345
671
|
});
|
|
346
672
|
const geometry = points.geometry;
|
|
347
673
|
geometry.setAttribute('tint', new BufferAttribute(tints, 4));
|
|
674
|
+
noteTints = tints;
|
|
675
|
+
washSurface();
|
|
348
676
|
geometry.setAttribute('size', new BufferAttribute(drawn, 1));
|
|
349
677
|
geometry.setAttribute('shape', new BufferAttribute(shapes, 1));
|
|
350
678
|
geometry.setAttribute('glow', new BufferAttribute(glows, 1));
|
|
679
|
+
drawnSizes = drawn;
|
|
680
|
+
meshShapes(shapes, tints);
|
|
351
681
|
dark = !isLight(looks.ground);
|
|
352
682
|
const solidUniforms = solid.material.uniforms;
|
|
353
683
|
solidUniforms.ground.value = channels(looks.ground);
|
|
354
684
|
solidUniforms.ink.value = channels(looks.ink);
|
|
685
|
+
solid.material.uniforms.strength.value = dark ? WASH.dark : WASH.light;
|
|
686
|
+
uniforms.bloom.value = dark ? BLOOM.dark : BLOOM.light;
|
|
687
|
+
uniforms.dark.value = dark ? 1 : 0;
|
|
355
688
|
const blending = dark ? AdditiveBlending : NormalBlending;
|
|
356
|
-
|
|
689
|
+
halos.material.blending = blending;
|
|
690
|
+
shapeMaterial.blending = blending;
|
|
691
|
+
shapeMaterial.needsUpdate = true;
|
|
357
692
|
quiet.material.blending = blending;
|
|
358
693
|
lit.material.blending = blending;
|
|
359
|
-
|
|
694
|
+
halos.material.needsUpdate = true;
|
|
360
695
|
quiet.material.needsUpdate = true;
|
|
361
696
|
lit.material.needsUpdate = true;
|
|
362
697
|
paintLinks();
|
|
363
698
|
};
|
|
699
|
+
let drawnSizes = new Float32Array();
|
|
700
|
+
const meshShapes = (shapes, tints) => {
|
|
701
|
+
for (const [code, solidShape] of [
|
|
702
|
+
[SHAPES.square, solids.cube],
|
|
703
|
+
[SHAPES.diamond, solids.octahedron],
|
|
704
|
+
]) {
|
|
705
|
+
const spots = [];
|
|
706
|
+
shapes.forEach((shape, spot) => {
|
|
707
|
+
if (shape === code) {
|
|
708
|
+
spots.push(spot);
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
solidShape.spots = spots;
|
|
712
|
+
if (!solidShape.mesh || solidShape.mesh.count !== spots.length) {
|
|
713
|
+
if (solidShape.mesh) {
|
|
714
|
+
scene.remove(solidShape.mesh);
|
|
715
|
+
solidShape.mesh.dispose();
|
|
716
|
+
}
|
|
717
|
+
solidShape.mesh =
|
|
718
|
+
spots.length > 0
|
|
719
|
+
? new InstancedMesh(solidShape.geometry, shapeMaterial, spots.length)
|
|
720
|
+
: undefined;
|
|
721
|
+
if (solidShape.mesh) {
|
|
722
|
+
solidShape.mesh.frustumCulled = false;
|
|
723
|
+
scene.add(solidShape.mesh);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
const mesh = solidShape.mesh;
|
|
727
|
+
if (mesh) {
|
|
728
|
+
const tone = new Float32Array(spots.length * 4);
|
|
729
|
+
spots.forEach((spot, at) => {
|
|
730
|
+
tone.set(tints.subarray(spot * 4, spot * 4 + 4), at * 4);
|
|
731
|
+
});
|
|
732
|
+
mesh.geometry = solidShape.geometry;
|
|
733
|
+
mesh.geometry.setAttribute('tone', new InstancedBufferAttribute(tone, 4));
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
placeShapes();
|
|
737
|
+
};
|
|
738
|
+
const turned = new Quaternion();
|
|
739
|
+
const placed = new Matrix4();
|
|
740
|
+
const where = new Vector3();
|
|
741
|
+
const scaled = new Vector3();
|
|
742
|
+
const placeShapes = () => {
|
|
743
|
+
for (const solidShape of [solids.cube, solids.octahedron]) {
|
|
744
|
+
const mesh = solidShape.mesh;
|
|
745
|
+
if (!mesh) {
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
solidShape.spots.forEach((spot, at) => {
|
|
749
|
+
const size = (drawnSizes[spot] ?? 1) * uniforms.unit.value * 0.5;
|
|
750
|
+
const twist = depthOf(ids[spot] ?? '');
|
|
751
|
+
turned.setFromEuler(new Euler(0.45 + twist * 0.3, 0.6 + twist, 0.2 * twist));
|
|
752
|
+
where.set(positions[spot * 3], positions[spot * 3 + 1], positions[spot * 3 + 2]);
|
|
753
|
+
scaled.setScalar(size);
|
|
754
|
+
mesh.setMatrixAt(at, placed.compose(where, turned, scaled));
|
|
755
|
+
});
|
|
756
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
757
|
+
}
|
|
758
|
+
};
|
|
364
759
|
const paintLinks = () => {
|
|
365
760
|
if (!looks) {
|
|
366
761
|
return;
|
|
@@ -425,6 +820,9 @@ export const threeDriver = (container, options = {}) => {
|
|
|
425
820
|
points.geometry.computeBoundingSphere();
|
|
426
821
|
hovered = undefined;
|
|
427
822
|
paint();
|
|
823
|
+
if (solid.visible) {
|
|
824
|
+
tieSurface();
|
|
825
|
+
}
|
|
428
826
|
};
|
|
429
827
|
const toWorld = (node) => [
|
|
430
828
|
node.x * scale,
|
|
@@ -434,6 +832,8 @@ export const threeDriver = (container, options = {}) => {
|
|
|
434
832
|
let morph;
|
|
435
833
|
let layout;
|
|
436
834
|
const cue = () => {
|
|
835
|
+
camera.updateMatrixWorld();
|
|
836
|
+
uniforms.light.value.copy(LIGHT).transformDirection(camera.matrixWorldInverse);
|
|
437
837
|
const distance = camera.position.distanceTo(controls.target);
|
|
438
838
|
const { center, radius } = points.geometry.boundingSphere ?? {
|
|
439
839
|
center: controls.target,
|
|
@@ -518,6 +918,9 @@ export const threeDriver = (container, options = {}) => {
|
|
|
518
918
|
now - handled > REST;
|
|
519
919
|
controls.update();
|
|
520
920
|
cue();
|
|
921
|
+
if (solids.cube.mesh || solids.octahedron.mesh) {
|
|
922
|
+
placeShapes();
|
|
923
|
+
}
|
|
521
924
|
renderer.render(scene, camera);
|
|
522
925
|
groups.draw();
|
|
523
926
|
drawNames();
|
|
@@ -725,6 +1128,12 @@ export const threeDriver = (container, options = {}) => {
|
|
|
725
1128
|
canvas.removeEventListener('pointermove', hover);
|
|
726
1129
|
canvas.removeEventListener('pointerleave', leave);
|
|
727
1130
|
controls.dispose();
|
|
1131
|
+
for (const solidShape of [solids.cube, solids.octahedron]) {
|
|
1132
|
+
solidShape.mesh?.dispose();
|
|
1133
|
+
solidShape.geometry.dispose();
|
|
1134
|
+
}
|
|
1135
|
+
shapeMaterial.dispose();
|
|
1136
|
+
halos.material.dispose();
|
|
728
1137
|
for (const object of [points, quiet, lit, solid]) {
|
|
729
1138
|
object.geometry.dispose();
|
|
730
1139
|
object.material.dispose();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusion.dev/helix-graph",
|
|
3
3
|
"description": "Connects the Helix Graph frame to a graph renderer: Sigma.js, cosmos.gl, three.js, or a driver of your own.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.124.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ross Sheppard <ross.sheppard@fusion.dev>",
|
|
7
7
|
"repository": {
|
|
@@ -47,12 +47,12 @@
|
|
|
47
47
|
},
|
|
48
48
|
"bugs": "https://github.com/project-fusion/fusion/issues",
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@fusion.dev/helix-core": "0.
|
|
50
|
+
"@fusion.dev/helix-core": "0.124.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@cosmos.gl/graph": "^3.4.2",
|
|
54
|
-
"@fusion.dev/helix-headless": "0.
|
|
55
|
-
"@fusion.dev/helix-testing": "0.
|
|
54
|
+
"@fusion.dev/helix-headless": "0.124.0",
|
|
55
|
+
"@fusion.dev/helix-testing": "0.124.0",
|
|
56
56
|
"@types/bun": "^1.3.0",
|
|
57
57
|
"@types/three": "^0.186.0",
|
|
58
58
|
"d3-force-3d": "^3.0.6",
|