@fusion.dev/helix-graph 0.123.0 → 0.125.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 +495 -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;
|
|
124
|
+
|
|
125
|
+
void main() {
|
|
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;
|
|
133
|
+
if (alpha < 0.01) discard;
|
|
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;
|
|
58
161
|
|
|
59
162
|
void main() {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
float
|
|
64
|
-
float
|
|
65
|
-
float
|
|
66
|
-
|
|
67
|
-
vec3
|
|
68
|
-
float
|
|
69
|
-
float alpha = (
|
|
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;
|
|
70
173
|
if (alpha < 0.01) discard;
|
|
71
|
-
gl_FragColor = vec4(
|
|
174
|
+
gl_FragColor = vec4(mix(face, edge, line), alpha);
|
|
72
175
|
}
|
|
73
176
|
`;
|
|
74
177
|
const LINE_VERTEX = `
|
|
@@ -94,29 +197,100 @@ 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
|
+
};
|
|
258
|
+
const SMOOTH = { surface: 3, ways: 6 };
|
|
259
|
+
const neighborsOf = (index, count) => {
|
|
260
|
+
const sets = Array.from({ length: count }, () => new Set());
|
|
261
|
+
for (let at = 0; at < index.count; at += 3) {
|
|
262
|
+
const a = index.getX(at);
|
|
263
|
+
const b = index.getX(at + 1);
|
|
264
|
+
const c = index.getX(at + 2);
|
|
265
|
+
sets[a]?.add(b).add(c);
|
|
266
|
+
sets[b]?.add(a).add(c);
|
|
267
|
+
sets[c]?.add(a).add(b);
|
|
268
|
+
}
|
|
269
|
+
return sets.map((set) => Int32Array.from(set));
|
|
270
|
+
};
|
|
271
|
+
const averaged = (values, neighbors, share) => {
|
|
272
|
+
const out = new Float32Array(values.length);
|
|
273
|
+
neighbors.forEach((around, at) => {
|
|
274
|
+
let x = 0;
|
|
275
|
+
let y = 0;
|
|
276
|
+
let z = 0;
|
|
277
|
+
for (const other of around) {
|
|
278
|
+
x += values[other * 3];
|
|
279
|
+
y += values[other * 3 + 1];
|
|
280
|
+
z += values[other * 3 + 2];
|
|
281
|
+
}
|
|
282
|
+
const many = around.length || 1;
|
|
283
|
+
for (const [axis, sum] of [
|
|
284
|
+
[0, x],
|
|
285
|
+
[1, y],
|
|
286
|
+
[2, z],
|
|
287
|
+
]) {
|
|
288
|
+
const value = values[at * 3 + axis];
|
|
289
|
+
out[at * 3 + axis] = around.length ? value + share * (sum / many - value) : value;
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
return out;
|
|
293
|
+
};
|
|
120
294
|
const channels = (color) => {
|
|
121
295
|
const [red = 128, green = 128, blue = 128] = color.match(/[\d.]+/g)?.map(Number) ?? [];
|
|
122
296
|
return [red / 255, green / 255, blue / 255];
|
|
@@ -160,6 +334,9 @@ export const threeDriver = (container, options = {}) => {
|
|
|
160
334
|
let scale = 1;
|
|
161
335
|
let positions = new Float32Array();
|
|
162
336
|
const uniforms = {
|
|
337
|
+
bloom: { value: BLOOM.dark },
|
|
338
|
+
dark: { value: 1 },
|
|
339
|
+
light: { value: new Vector3(-0.4, 0.8, 0.45).normalize() },
|
|
163
340
|
middle: { value: 1 },
|
|
164
341
|
radius: { value: 1 },
|
|
165
342
|
scale: { value: 1 },
|
|
@@ -179,13 +356,46 @@ export const threeDriver = (container, options = {}) => {
|
|
|
179
356
|
vertexShader: LINE_VERTEX,
|
|
180
357
|
});
|
|
181
358
|
const points = new Points(new BufferGeometry(), new ShaderMaterial({
|
|
182
|
-
depthWrite: false,
|
|
183
359
|
fragmentShader: FRAGMENT,
|
|
184
360
|
transparent: true,
|
|
185
|
-
uniforms,
|
|
361
|
+
uniforms: { ...uniforms, halo: { value: 0 } },
|
|
186
362
|
vertexShader: VERTEX,
|
|
187
363
|
}));
|
|
188
364
|
points.frustumCulled = false;
|
|
365
|
+
const halos = new Points(points.geometry, new ShaderMaterial({
|
|
366
|
+
depthWrite: false,
|
|
367
|
+
fragmentShader: HALO_FRAGMENT,
|
|
368
|
+
transparent: true,
|
|
369
|
+
uniforms: { ...uniforms, halo: { value: 1 } },
|
|
370
|
+
vertexShader: VERTEX,
|
|
371
|
+
}));
|
|
372
|
+
halos.frustumCulled = false;
|
|
373
|
+
halos.renderOrder = 1;
|
|
374
|
+
const shapeMaterial = new ShaderMaterial({
|
|
375
|
+
depthWrite: false,
|
|
376
|
+
fragmentShader: SHAPE_FRAGMENT,
|
|
377
|
+
side: DoubleSide,
|
|
378
|
+
transparent: true,
|
|
379
|
+
uniforms: {
|
|
380
|
+
dark: uniforms.dark,
|
|
381
|
+
middle: uniforms.middle,
|
|
382
|
+
radius: uniforms.radius,
|
|
383
|
+
steep: uniforms.steep,
|
|
384
|
+
},
|
|
385
|
+
vertexShader: SHAPE_VERTEX,
|
|
386
|
+
});
|
|
387
|
+
const solids = {
|
|
388
|
+
cube: {
|
|
389
|
+
geometry: framed(new BoxGeometry(1.5, 1.5, 1.5)),
|
|
390
|
+
mesh: undefined,
|
|
391
|
+
spots: [],
|
|
392
|
+
},
|
|
393
|
+
octahedron: {
|
|
394
|
+
geometry: framed(new OctahedronGeometry(1.15)),
|
|
395
|
+
mesh: undefined,
|
|
396
|
+
spots: [],
|
|
397
|
+
},
|
|
398
|
+
};
|
|
189
399
|
const quiet = new LineSegments(new BufferGeometry(), lines(LINKS.dark.far));
|
|
190
400
|
const lit = new LineSegments(new BufferGeometry(), lines(0.9));
|
|
191
401
|
quiet.frustumCulled = false;
|
|
@@ -196,13 +406,17 @@ export const threeDriver = (container, options = {}) => {
|
|
|
196
406
|
polygonOffsetFactor: 2,
|
|
197
407
|
polygonOffsetUnits: 2,
|
|
198
408
|
side: DoubleSide,
|
|
199
|
-
uniforms: {
|
|
409
|
+
uniforms: {
|
|
410
|
+
ground: { value: [0, 0, 0] },
|
|
411
|
+
ink: { value: [1, 1, 1] },
|
|
412
|
+
strength: { value: WASH.dark },
|
|
413
|
+
},
|
|
200
414
|
vertexShader: SOLID_VERTEX,
|
|
201
415
|
}));
|
|
202
416
|
solid.renderOrder = -1;
|
|
203
417
|
solid.visible = false;
|
|
204
418
|
solid.frustumCulled = false;
|
|
205
|
-
scene.add(solid, quiet, lit, points);
|
|
419
|
+
scene.add(solid, quiet, lit, points, halos);
|
|
206
420
|
let shapeFrame;
|
|
207
421
|
let surfaced = false;
|
|
208
422
|
let solidAt = Number.NaN;
|
|
@@ -217,21 +431,160 @@ export const threeDriver = (container, options = {}) => {
|
|
|
217
431
|
return;
|
|
218
432
|
}
|
|
219
433
|
solidAt = scale;
|
|
220
|
-
const {
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
434
|
+
const { field } = frame;
|
|
435
|
+
const inset = frame.shell * 1.15;
|
|
436
|
+
let geometry = new BufferGeometry();
|
|
437
|
+
if (field.mesh) {
|
|
438
|
+
geometry.setAttribute('position', new BufferAttribute(field.mesh.corners.slice(), 3));
|
|
439
|
+
geometry.setIndex(new BufferAttribute(field.mesh.triangles, 1));
|
|
440
|
+
geometry = mergeVertices(geometry);
|
|
441
|
+
const place = geometry.getAttribute('position');
|
|
442
|
+
const count = place.count;
|
|
443
|
+
const neighbors = neighborsOf(geometry.getIndex(), count);
|
|
444
|
+
const step = Math.max(field.width, field.height, field.depth) / 64;
|
|
445
|
+
let way = new Float32Array(count * 3);
|
|
446
|
+
for (let at = 0; at < count; at++) {
|
|
447
|
+
const x = place.getX(at);
|
|
448
|
+
const y = place.getY(at);
|
|
449
|
+
const z = place.getZ(at);
|
|
450
|
+
way[at * 3] = field.distance(x + step, y, z) - field.distance(x - step, y, z);
|
|
451
|
+
way[at * 3 + 1] = field.distance(x, y + step, z) - field.distance(x, y - step, z);
|
|
452
|
+
way[at * 3 + 2] = field.distance(x, y, z + step) - field.distance(x, y, z - step);
|
|
453
|
+
}
|
|
454
|
+
for (let pass = 0; pass < SMOOTH.ways; pass++) {
|
|
455
|
+
way = averaged(way, neighbors, 0.5);
|
|
456
|
+
}
|
|
457
|
+
for (let at = 0; at < count; at++) {
|
|
458
|
+
const length = Math.hypot(way[at * 3], way[at * 3 + 1], way[at * 3 + 2]) ||
|
|
459
|
+
1;
|
|
460
|
+
place.setXYZ(at, place.getX(at) - (way[at * 3] / length) * inset, place.getY(at) - (way[at * 3 + 1] / length) * inset, place.getZ(at) - (way[at * 3 + 2] / length) * inset);
|
|
461
|
+
}
|
|
462
|
+
let corners = place.array;
|
|
463
|
+
for (let pass = 0; pass < SMOOTH.surface; pass++) {
|
|
464
|
+
corners = averaged(averaged(corners, neighbors, 0.5), neighbors, -0.53);
|
|
465
|
+
}
|
|
466
|
+
place.array.set(corners);
|
|
228
467
|
}
|
|
468
|
+
else {
|
|
469
|
+
const { corners, triangles } = surfaceOf(field, { at: -inset });
|
|
470
|
+
geometry.setAttribute('position', new BufferAttribute(corners, 3));
|
|
471
|
+
geometry.setIndex(new BufferAttribute(triangles, 1));
|
|
472
|
+
}
|
|
473
|
+
const place = geometry.getAttribute('position');
|
|
474
|
+
const { depth, height, width } = field;
|
|
475
|
+
for (let at = 0; at < place.count; at++) {
|
|
476
|
+
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);
|
|
477
|
+
}
|
|
478
|
+
geometry.computeVertexNormals();
|
|
229
479
|
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();
|
|
480
|
+
solid.geometry = geometry;
|
|
234
481
|
solid.visible = true;
|
|
482
|
+
tieSurface();
|
|
483
|
+
};
|
|
484
|
+
let washFrom = new Int32Array();
|
|
485
|
+
let washWeight = new Float32Array();
|
|
486
|
+
let noteTints = new Float32Array();
|
|
487
|
+
const tieSurface = () => {
|
|
488
|
+
const place = solid.geometry.getAttribute('position');
|
|
489
|
+
if (!solid.visible || !place || ids.length === 0) {
|
|
490
|
+
washFrom = new Int32Array();
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const sample = Math.min(ids.length, 200);
|
|
494
|
+
const nearest = [];
|
|
495
|
+
for (let step = 0; step < sample; step++) {
|
|
496
|
+
const one = Math.floor((step * ids.length) / sample);
|
|
497
|
+
let best = Number.POSITIVE_INFINITY;
|
|
498
|
+
for (let other = 0; other < ids.length; other++) {
|
|
499
|
+
if (other !== one) {
|
|
500
|
+
best = Math.min(best, at(one).distanceToSquared(at(other)));
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
nearest.push(Math.sqrt(best));
|
|
504
|
+
}
|
|
505
|
+
nearest.sort((one, other) => one - other);
|
|
506
|
+
const reach = (nearest[Math.floor(nearest.length / 2)] ?? 1) * WASH.reach;
|
|
507
|
+
const cells = new Map();
|
|
508
|
+
const key = (x, y, z) => `${Math.floor(x / reach)},${Math.floor(y / reach)},${Math.floor(z / reach)}`;
|
|
509
|
+
for (let spot = 0; spot < ids.length; spot++) {
|
|
510
|
+
const k = key(positions[spot * 3], positions[spot * 3 + 1], positions[spot * 3 + 2]);
|
|
511
|
+
const list = cells.get(k);
|
|
512
|
+
if (list) {
|
|
513
|
+
list.push(spot);
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
cells.set(k, [spot]);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
washFrom = new Int32Array(place.count * WASH.notes).fill(-1);
|
|
520
|
+
washWeight = new Float32Array(place.count * WASH.notes);
|
|
521
|
+
const best = new Float32Array(WASH.notes);
|
|
522
|
+
const which = new Int32Array(WASH.notes);
|
|
523
|
+
for (let corner = 0; corner < place.count; corner++) {
|
|
524
|
+
const x = place.getX(corner);
|
|
525
|
+
const y = place.getY(corner);
|
|
526
|
+
const z = place.getZ(corner);
|
|
527
|
+
best.fill(Number.POSITIVE_INFINITY);
|
|
528
|
+
which.fill(-1);
|
|
529
|
+
const cx = Math.floor(x / reach);
|
|
530
|
+
const cy = Math.floor(y / reach);
|
|
531
|
+
const cz = Math.floor(z / reach);
|
|
532
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
533
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
534
|
+
for (let dz = -1; dz <= 1; dz++) {
|
|
535
|
+
for (const spot of cells.get(`${cx + dx},${cy + dy},${cz + dz}`) ?? []) {
|
|
536
|
+
const far = (positions[spot * 3] - x) ** 2 +
|
|
537
|
+
(positions[spot * 3 + 1] - y) ** 2 +
|
|
538
|
+
(positions[spot * 3 + 2] - z) ** 2;
|
|
539
|
+
if (far < best[WASH.notes - 1]) {
|
|
540
|
+
let slot = WASH.notes - 1;
|
|
541
|
+
while (slot > 0 && best[slot - 1] > far) {
|
|
542
|
+
best[slot] = best[slot - 1];
|
|
543
|
+
which[slot] = which[slot - 1];
|
|
544
|
+
slot -= 1;
|
|
545
|
+
}
|
|
546
|
+
best[slot] = far;
|
|
547
|
+
which[slot] = spot;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
for (let slot = 0; slot < WASH.notes; slot++) {
|
|
554
|
+
washFrom[corner * WASH.notes + slot] = which[slot];
|
|
555
|
+
const far = Math.sqrt(best[slot]) / reach;
|
|
556
|
+
washWeight[corner * WASH.notes + slot] = far < 1 ? (1 - far * far) ** 2 : 0;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
washSurface();
|
|
560
|
+
};
|
|
561
|
+
const washSurface = () => {
|
|
562
|
+
const place = solid.geometry.getAttribute('position');
|
|
563
|
+
if (!place || washFrom.length !== place.count * WASH.notes || noteTints.length === 0) {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
const wash = new Float32Array(place.count * 4);
|
|
567
|
+
for (let corner = 0; corner < place.count; corner++) {
|
|
568
|
+
let red = 0;
|
|
569
|
+
let green = 0;
|
|
570
|
+
let blue = 0;
|
|
571
|
+
let total = 0;
|
|
572
|
+
for (let slot = 0; slot < WASH.notes; slot++) {
|
|
573
|
+
const spot = washFrom[corner * WASH.notes + slot];
|
|
574
|
+
const weight = washWeight[corner * WASH.notes + slot] * (noteTints[spot * 4 + 3] ?? 0);
|
|
575
|
+
if (spot < 0 || weight <= 0) {
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
red += noteTints[spot * 4] * weight;
|
|
579
|
+
green += noteTints[spot * 4 + 1] * weight;
|
|
580
|
+
blue += noteTints[spot * 4 + 2] * weight;
|
|
581
|
+
total += weight;
|
|
582
|
+
}
|
|
583
|
+
if (total > 0) {
|
|
584
|
+
wash.set([red / total, green / total, blue / total, Math.min(total / WASH.full, 1)], corner * 4);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
solid.geometry.setAttribute('wash', new BufferAttribute(wash, 4));
|
|
235
588
|
};
|
|
236
589
|
const at = (spot) => new Vector3(positions[spot * 3], positions[spot * 3 + 1], positions[spot * 3 + 2]);
|
|
237
590
|
const onScreen = (id) => {
|
|
@@ -256,7 +609,30 @@ export const threeDriver = (container, options = {}) => {
|
|
|
256
609
|
};
|
|
257
610
|
};
|
|
258
611
|
const held = gesturesHeld(container);
|
|
259
|
-
const
|
|
612
|
+
const arc = (from, to) => {
|
|
613
|
+
const one = index.get(from);
|
|
614
|
+
const other = index.get(to);
|
|
615
|
+
if (one === undefined || other === undefined) {
|
|
616
|
+
return undefined;
|
|
617
|
+
}
|
|
618
|
+
const a = at(one);
|
|
619
|
+
const b = at(other);
|
|
620
|
+
const middle = a.clone().add(b).multiplyScalar(0.5);
|
|
621
|
+
const center = points.geometry.boundingSphere?.center ?? controls.target;
|
|
622
|
+
const out = middle.clone().sub(center);
|
|
623
|
+
if (out.lengthSq() < 1e-9) {
|
|
624
|
+
out.copy(camera.up);
|
|
625
|
+
}
|
|
626
|
+
const peak = middle.add(out.normalize().multiplyScalar(a.distanceTo(b) * LIFT * 2));
|
|
627
|
+
const depth = peak.clone().applyMatrix4(camera.matrixWorldInverse).z;
|
|
628
|
+
if (depth > 0) {
|
|
629
|
+
return undefined;
|
|
630
|
+
}
|
|
631
|
+
peak.project(camera);
|
|
632
|
+
const { clientHeight: height, clientWidth: width } = container;
|
|
633
|
+
return { x: ((peak.x + 1) / 2) * width, y: ((1 - peak.y) / 2) * height };
|
|
634
|
+
};
|
|
635
|
+
const signals = signalsOver(container, (id) => nearOnScreen(id), arc);
|
|
260
636
|
const nearOnScreen = (id) => {
|
|
261
637
|
const spot = index.get(id);
|
|
262
638
|
if (spot === undefined) {
|
|
@@ -345,22 +721,91 @@ export const threeDriver = (container, options = {}) => {
|
|
|
345
721
|
});
|
|
346
722
|
const geometry = points.geometry;
|
|
347
723
|
geometry.setAttribute('tint', new BufferAttribute(tints, 4));
|
|
724
|
+
noteTints = tints;
|
|
725
|
+
washSurface();
|
|
348
726
|
geometry.setAttribute('size', new BufferAttribute(drawn, 1));
|
|
349
727
|
geometry.setAttribute('shape', new BufferAttribute(shapes, 1));
|
|
350
728
|
geometry.setAttribute('glow', new BufferAttribute(glows, 1));
|
|
729
|
+
drawnSizes = drawn;
|
|
730
|
+
meshShapes(shapes, tints);
|
|
351
731
|
dark = !isLight(looks.ground);
|
|
352
732
|
const solidUniforms = solid.material.uniforms;
|
|
353
733
|
solidUniforms.ground.value = channels(looks.ground);
|
|
354
734
|
solidUniforms.ink.value = channels(looks.ink);
|
|
735
|
+
solid.material.uniforms.strength.value = dark ? WASH.dark : WASH.light;
|
|
736
|
+
uniforms.bloom.value = dark ? BLOOM.dark : BLOOM.light;
|
|
737
|
+
uniforms.dark.value = dark ? 1 : 0;
|
|
355
738
|
const blending = dark ? AdditiveBlending : NormalBlending;
|
|
356
|
-
|
|
739
|
+
halos.material.blending = blending;
|
|
740
|
+
shapeMaterial.blending = blending;
|
|
741
|
+
shapeMaterial.needsUpdate = true;
|
|
357
742
|
quiet.material.blending = blending;
|
|
358
743
|
lit.material.blending = blending;
|
|
359
|
-
|
|
744
|
+
halos.material.needsUpdate = true;
|
|
360
745
|
quiet.material.needsUpdate = true;
|
|
361
746
|
lit.material.needsUpdate = true;
|
|
362
747
|
paintLinks();
|
|
363
748
|
};
|
|
749
|
+
let drawnSizes = new Float32Array();
|
|
750
|
+
const meshShapes = (shapes, tints) => {
|
|
751
|
+
for (const [code, solidShape] of [
|
|
752
|
+
[SHAPES.square, solids.cube],
|
|
753
|
+
[SHAPES.diamond, solids.octahedron],
|
|
754
|
+
]) {
|
|
755
|
+
const spots = [];
|
|
756
|
+
shapes.forEach((shape, spot) => {
|
|
757
|
+
if (shape === code) {
|
|
758
|
+
spots.push(spot);
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
solidShape.spots = spots;
|
|
762
|
+
if (!solidShape.mesh || solidShape.mesh.count !== spots.length) {
|
|
763
|
+
if (solidShape.mesh) {
|
|
764
|
+
scene.remove(solidShape.mesh);
|
|
765
|
+
solidShape.mesh.dispose();
|
|
766
|
+
}
|
|
767
|
+
solidShape.mesh =
|
|
768
|
+
spots.length > 0
|
|
769
|
+
? new InstancedMesh(solidShape.geometry, shapeMaterial, spots.length)
|
|
770
|
+
: undefined;
|
|
771
|
+
if (solidShape.mesh) {
|
|
772
|
+
solidShape.mesh.frustumCulled = false;
|
|
773
|
+
scene.add(solidShape.mesh);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
const mesh = solidShape.mesh;
|
|
777
|
+
if (mesh) {
|
|
778
|
+
const tone = new Float32Array(spots.length * 4);
|
|
779
|
+
spots.forEach((spot, at) => {
|
|
780
|
+
tone.set(tints.subarray(spot * 4, spot * 4 + 4), at * 4);
|
|
781
|
+
});
|
|
782
|
+
mesh.geometry = solidShape.geometry;
|
|
783
|
+
mesh.geometry.setAttribute('tone', new InstancedBufferAttribute(tone, 4));
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
placeShapes();
|
|
787
|
+
};
|
|
788
|
+
const turned = new Quaternion();
|
|
789
|
+
const placed = new Matrix4();
|
|
790
|
+
const where = new Vector3();
|
|
791
|
+
const scaled = new Vector3();
|
|
792
|
+
const placeShapes = () => {
|
|
793
|
+
for (const solidShape of [solids.cube, solids.octahedron]) {
|
|
794
|
+
const mesh = solidShape.mesh;
|
|
795
|
+
if (!mesh) {
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
solidShape.spots.forEach((spot, at) => {
|
|
799
|
+
const size = (drawnSizes[spot] ?? 1) * uniforms.unit.value * 0.5;
|
|
800
|
+
const twist = depthOf(ids[spot] ?? '');
|
|
801
|
+
turned.setFromEuler(new Euler(0.45 + twist * 0.3, 0.6 + twist, 0.2 * twist));
|
|
802
|
+
where.set(positions[spot * 3], positions[spot * 3 + 1], positions[spot * 3 + 2]);
|
|
803
|
+
scaled.setScalar(size);
|
|
804
|
+
mesh.setMatrixAt(at, placed.compose(where, turned, scaled));
|
|
805
|
+
});
|
|
806
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
807
|
+
}
|
|
808
|
+
};
|
|
364
809
|
const paintLinks = () => {
|
|
365
810
|
if (!looks) {
|
|
366
811
|
return;
|
|
@@ -425,6 +870,9 @@ export const threeDriver = (container, options = {}) => {
|
|
|
425
870
|
points.geometry.computeBoundingSphere();
|
|
426
871
|
hovered = undefined;
|
|
427
872
|
paint();
|
|
873
|
+
if (solid.visible) {
|
|
874
|
+
tieSurface();
|
|
875
|
+
}
|
|
428
876
|
};
|
|
429
877
|
const toWorld = (node) => [
|
|
430
878
|
node.x * scale,
|
|
@@ -434,6 +882,8 @@ export const threeDriver = (container, options = {}) => {
|
|
|
434
882
|
let morph;
|
|
435
883
|
let layout;
|
|
436
884
|
const cue = () => {
|
|
885
|
+
camera.updateMatrixWorld();
|
|
886
|
+
uniforms.light.value.copy(LIGHT).transformDirection(camera.matrixWorldInverse);
|
|
437
887
|
const distance = camera.position.distanceTo(controls.target);
|
|
438
888
|
const { center, radius } = points.geometry.boundingSphere ?? {
|
|
439
889
|
center: controls.target,
|
|
@@ -518,6 +968,9 @@ export const threeDriver = (container, options = {}) => {
|
|
|
518
968
|
now - handled > REST;
|
|
519
969
|
controls.update();
|
|
520
970
|
cue();
|
|
971
|
+
if (solids.cube.mesh || solids.octahedron.mesh) {
|
|
972
|
+
placeShapes();
|
|
973
|
+
}
|
|
521
974
|
renderer.render(scene, camera);
|
|
522
975
|
groups.draw();
|
|
523
976
|
drawNames();
|
|
@@ -725,6 +1178,12 @@ export const threeDriver = (container, options = {}) => {
|
|
|
725
1178
|
canvas.removeEventListener('pointermove', hover);
|
|
726
1179
|
canvas.removeEventListener('pointerleave', leave);
|
|
727
1180
|
controls.dispose();
|
|
1181
|
+
for (const solidShape of [solids.cube, solids.octahedron]) {
|
|
1182
|
+
solidShape.mesh?.dispose();
|
|
1183
|
+
solidShape.geometry.dispose();
|
|
1184
|
+
}
|
|
1185
|
+
shapeMaterial.dispose();
|
|
1186
|
+
halos.material.dispose();
|
|
728
1187
|
for (const object of [points, quiet, lit, solid]) {
|
|
729
1188
|
object.geometry.dispose();
|
|
730
1189
|
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.125.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.125.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.125.0",
|
|
55
|
+
"@fusion.dev/helix-testing": "0.125.0",
|
|
56
56
|
"@types/bun": "^1.3.0",
|
|
57
57
|
"@types/three": "^0.186.0",
|
|
58
58
|
"d3-force-3d": "^3.0.6",
|