@fusion.dev/helix-graph 0.122.0 → 0.124.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -12
- package/dist/connect-graph/arrange.d.ts +10 -0
- package/dist/connect-graph/arrange.js +6 -1
- package/dist/connect-graph/connect-graph.d.ts +5 -3
- package/dist/connect-graph/connect-graph.js +16 -6
- package/dist/connect-graph/shape-field.d.ts +24 -0
- package/dist/connect-graph/shape-field.js +3 -3
- package/dist/connect-graph/stencil.d.ts +30 -0
- package/dist/connect-graph/stencil.js +75 -25
- package/dist/connect-graph/surface-web.d.ts +18 -0
- package/dist/connect-graph/surface-web.js +99 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/overlay/overlay.d.ts +6 -1
- package/dist/overlay/overlay.js +91 -46
- package/dist/shapes/flat.d.ts +28 -0
- package/dist/shapes/flat.js +63 -0
- package/dist/shapes/index.d.ts +5 -0
- package/dist/shapes/index.js +5 -0
- package/dist/shapes/model.d.ts +12 -0
- package/dist/shapes/model.js +31 -0
- package/dist/shapes/solids.d.ts +28 -0
- package/dist/shapes/solids.js +47 -0
- package/dist/shapes/triangles.d.ts +20 -0
- package/dist/shapes/triangles.js +92 -0
- package/dist/sigma/node-program.js +5 -0
- package/dist/three/surface.d.ts +18 -0
- package/dist/three/surface.js +112 -0
- package/dist/three/three-driver.js +516 -24
- package/package.json +4 -4
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { forceCenter, forceLink, forceManyBody, forceSimulation, } from 'd3-force-3d';
|
|
2
|
-
import { AdditiveBlending, BufferAttribute, BufferGeometry, LineSegments, 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';
|
|
6
|
+
import { surfaceOf } from './surface.js';
|
|
5
7
|
const SHAPES = { circle: 0, diamond: 2, ring: 0, square: 1 };
|
|
6
8
|
const REACH = 100;
|
|
7
9
|
const STEP = 250;
|
|
@@ -10,6 +12,10 @@ const ZOOM = 1.5;
|
|
|
10
12
|
const SAMPLE = 96;
|
|
11
13
|
const LINKS = { dark: { close: 0.3, far: 0.05 }, light: { close: 0.55, far: 0.12 } };
|
|
12
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;
|
|
13
19
|
const FOV = 40;
|
|
14
20
|
const FADE = `
|
|
15
21
|
uniform float middle;
|
|
@@ -22,6 +28,29 @@ float fade(float depth) {
|
|
|
22
28
|
return mix(gentle, sharp, steep);
|
|
23
29
|
}
|
|
24
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
|
+
`;
|
|
25
54
|
const VERTEX = `
|
|
26
55
|
${FADE}
|
|
27
56
|
attribute float size;
|
|
@@ -31,18 +60,29 @@ attribute float glow;
|
|
|
31
60
|
|
|
32
61
|
uniform float scale;
|
|
33
62
|
uniform float unit;
|
|
63
|
+
uniform float bloom;
|
|
64
|
+
uniform float halo;
|
|
34
65
|
|
|
35
66
|
varying vec4 vTint;
|
|
36
67
|
varying float vShape;
|
|
37
68
|
varying float vGlow;
|
|
38
69
|
varying float vFade;
|
|
70
|
+
varying float vRoom;
|
|
71
|
+
varying vec3 vCenter;
|
|
72
|
+
varying float vRadius;
|
|
39
73
|
|
|
40
74
|
void main() {
|
|
41
75
|
vec4 view = modelViewMatrix * vec4(position, 1.0);
|
|
42
76
|
gl_Position = projectionMatrix * view;
|
|
43
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;
|
|
44
82
|
/* As large as the note is, at the distance it is: nearer is larger. */
|
|
45
|
-
gl_PointSize = size * unit * scale / depth *
|
|
83
|
+
gl_PointSize = size * unit * scale / depth * vRoom;
|
|
84
|
+
vCenter = view.xyz;
|
|
85
|
+
vRadius = size * unit * 0.5;
|
|
46
86
|
vFade = fade(depth);
|
|
47
87
|
vTint = tint;
|
|
48
88
|
vShape = shape;
|
|
@@ -50,24 +90,88 @@ void main() {
|
|
|
50
90
|
}
|
|
51
91
|
`;
|
|
52
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;
|
|
53
119
|
varying vec4 vTint;
|
|
54
120
|
varying float vShape;
|
|
55
121
|
varying float vGlow;
|
|
56
122
|
varying float vFade;
|
|
123
|
+
varying float vRoom;
|
|
57
124
|
|
|
58
125
|
void main() {
|
|
59
|
-
vec2
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
float
|
|
65
|
-
|
|
66
|
-
vec3 color = vTint.rgb * mix(1.15, 0.8, clamp(d, 0.0, 1.0));
|
|
67
|
-
float halo = vGlow * (1.0 - smoothstep(1.0, 1.0 / body, d)) * 0.55;
|
|
68
|
-
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;
|
|
69
133
|
if (alpha < 0.01) discard;
|
|
70
|
-
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);
|
|
71
175
|
}
|
|
72
176
|
`;
|
|
73
177
|
const LINE_VERTEX = `
|
|
@@ -92,6 +196,65 @@ void main() {
|
|
|
92
196
|
gl_FragColor = vec4(vColor, opacity * vFade);
|
|
93
197
|
}
|
|
94
198
|
`;
|
|
199
|
+
const SOLID_VERTEX = `
|
|
200
|
+
attribute vec4 wash;
|
|
201
|
+
varying vec3 vNormal;
|
|
202
|
+
varying vec3 vView;
|
|
203
|
+
varying vec4 vWash;
|
|
204
|
+
|
|
205
|
+
void main() {
|
|
206
|
+
vec4 view = modelViewMatrix * vec4(position, 1.0);
|
|
207
|
+
gl_Position = projectionMatrix * view;
|
|
208
|
+
vNormal = normalize(normalMatrix * normal);
|
|
209
|
+
vView = normalize(-view.xyz);
|
|
210
|
+
vWash = wash;
|
|
211
|
+
}
|
|
212
|
+
`;
|
|
213
|
+
const SOLID_FRAGMENT = `
|
|
214
|
+
uniform vec3 ground;
|
|
215
|
+
uniform vec3 ink;
|
|
216
|
+
uniform float strength;
|
|
217
|
+
varying vec3 vNormal;
|
|
218
|
+
varying vec3 vView;
|
|
219
|
+
varying vec4 vWash;
|
|
220
|
+
|
|
221
|
+
void main() {
|
|
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);
|
|
236
|
+
}
|
|
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
|
+
};
|
|
95
258
|
const channels = (color) => {
|
|
96
259
|
const [red = 128, green = 128, blue = 128] = color.match(/[\d.]+/g)?.map(Number) ?? [];
|
|
97
260
|
return [red / 255, green / 255, blue / 255];
|
|
@@ -135,6 +298,9 @@ export const threeDriver = (container, options = {}) => {
|
|
|
135
298
|
let scale = 1;
|
|
136
299
|
let positions = new Float32Array();
|
|
137
300
|
const uniforms = {
|
|
301
|
+
bloom: { value: BLOOM.dark },
|
|
302
|
+
dark: { value: 1 },
|
|
303
|
+
light: { value: new Vector3(-0.4, 0.8, 0.45).normalize() },
|
|
138
304
|
middle: { value: 1 },
|
|
139
305
|
radius: { value: 1 },
|
|
140
306
|
scale: { value: 1 },
|
|
@@ -154,18 +320,222 @@ export const threeDriver = (container, options = {}) => {
|
|
|
154
320
|
vertexShader: LINE_VERTEX,
|
|
155
321
|
});
|
|
156
322
|
const points = new Points(new BufferGeometry(), new ShaderMaterial({
|
|
157
|
-
depthWrite: false,
|
|
158
323
|
fragmentShader: FRAGMENT,
|
|
159
324
|
transparent: true,
|
|
160
|
-
uniforms,
|
|
325
|
+
uniforms: { ...uniforms, halo: { value: 0 } },
|
|
161
326
|
vertexShader: VERTEX,
|
|
162
327
|
}));
|
|
163
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
|
+
};
|
|
164
363
|
const quiet = new LineSegments(new BufferGeometry(), lines(LINKS.dark.far));
|
|
165
364
|
const lit = new LineSegments(new BufferGeometry(), lines(0.9));
|
|
166
365
|
quiet.frustumCulled = false;
|
|
167
366
|
lit.frustumCulled = false;
|
|
168
|
-
|
|
367
|
+
const solid = new Mesh(new BufferGeometry(), new ShaderMaterial({
|
|
368
|
+
fragmentShader: SOLID_FRAGMENT,
|
|
369
|
+
polygonOffset: true,
|
|
370
|
+
polygonOffsetFactor: 2,
|
|
371
|
+
polygonOffsetUnits: 2,
|
|
372
|
+
side: DoubleSide,
|
|
373
|
+
uniforms: {
|
|
374
|
+
ground: { value: [0, 0, 0] },
|
|
375
|
+
ink: { value: [1, 1, 1] },
|
|
376
|
+
strength: { value: WASH.dark },
|
|
377
|
+
},
|
|
378
|
+
vertexShader: SOLID_VERTEX,
|
|
379
|
+
}));
|
|
380
|
+
solid.renderOrder = -1;
|
|
381
|
+
solid.visible = false;
|
|
382
|
+
solid.frustumCulled = false;
|
|
383
|
+
scene.add(solid, quiet, lit, points, halos);
|
|
384
|
+
let shapeFrame;
|
|
385
|
+
let surfaced = false;
|
|
386
|
+
let solidAt = Number.NaN;
|
|
387
|
+
const shapeSolid = () => {
|
|
388
|
+
const frame = shapeFrame;
|
|
389
|
+
if (!frame || !surfaced || frame.field.depth <= 0) {
|
|
390
|
+
solid.visible = false;
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (solidAt === scale) {
|
|
394
|
+
solid.visible = true;
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
solidAt = scale;
|
|
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));
|
|
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();
|
|
429
|
+
solid.geometry.dispose();
|
|
430
|
+
solid.geometry = geometry;
|
|
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));
|
|
538
|
+
};
|
|
169
539
|
const at = (spot) => new Vector3(positions[spot * 3], positions[spot * 3 + 1], positions[spot * 3 + 2]);
|
|
170
540
|
const onScreen = (id) => {
|
|
171
541
|
const spot = index.get(id);
|
|
@@ -189,14 +559,39 @@ export const threeDriver = (container, options = {}) => {
|
|
|
189
559
|
};
|
|
190
560
|
};
|
|
191
561
|
const held = gesturesHeld(container);
|
|
192
|
-
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);
|
|
193
586
|
const nearOnScreen = (id) => {
|
|
194
587
|
const spot = index.get(id);
|
|
195
588
|
if (spot === undefined) {
|
|
196
589
|
return undefined;
|
|
197
590
|
}
|
|
198
591
|
const depth = -at(spot).applyMatrix4(camera.matrixWorldInverse).z;
|
|
199
|
-
return uniforms.steep.value > 0 && depth > uniforms.middle.value
|
|
592
|
+
return (uniforms.steep.value > 0 || solid.visible) && depth > uniforms.middle.value
|
|
593
|
+
? undefined
|
|
594
|
+
: onScreen(id);
|
|
200
595
|
};
|
|
201
596
|
const groups = groupsOver(container, nearOnScreen);
|
|
202
597
|
const names = namesOver(container, onScreen);
|
|
@@ -276,19 +671,91 @@ export const threeDriver = (container, options = {}) => {
|
|
|
276
671
|
});
|
|
277
672
|
const geometry = points.geometry;
|
|
278
673
|
geometry.setAttribute('tint', new BufferAttribute(tints, 4));
|
|
674
|
+
noteTints = tints;
|
|
675
|
+
washSurface();
|
|
279
676
|
geometry.setAttribute('size', new BufferAttribute(drawn, 1));
|
|
280
677
|
geometry.setAttribute('shape', new BufferAttribute(shapes, 1));
|
|
281
678
|
geometry.setAttribute('glow', new BufferAttribute(glows, 1));
|
|
679
|
+
drawnSizes = drawn;
|
|
680
|
+
meshShapes(shapes, tints);
|
|
282
681
|
dark = !isLight(looks.ground);
|
|
682
|
+
const solidUniforms = solid.material.uniforms;
|
|
683
|
+
solidUniforms.ground.value = channels(looks.ground);
|
|
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;
|
|
283
688
|
const blending = dark ? AdditiveBlending : NormalBlending;
|
|
284
|
-
|
|
689
|
+
halos.material.blending = blending;
|
|
690
|
+
shapeMaterial.blending = blending;
|
|
691
|
+
shapeMaterial.needsUpdate = true;
|
|
285
692
|
quiet.material.blending = blending;
|
|
286
693
|
lit.material.blending = blending;
|
|
287
|
-
|
|
694
|
+
halos.material.needsUpdate = true;
|
|
288
695
|
quiet.material.needsUpdate = true;
|
|
289
696
|
lit.material.needsUpdate = true;
|
|
290
697
|
paintLinks();
|
|
291
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
|
+
};
|
|
292
759
|
const paintLinks = () => {
|
|
293
760
|
if (!looks) {
|
|
294
761
|
return;
|
|
@@ -353,6 +820,9 @@ export const threeDriver = (container, options = {}) => {
|
|
|
353
820
|
points.geometry.computeBoundingSphere();
|
|
354
821
|
hovered = undefined;
|
|
355
822
|
paint();
|
|
823
|
+
if (solid.visible) {
|
|
824
|
+
tieSurface();
|
|
825
|
+
}
|
|
356
826
|
};
|
|
357
827
|
const toWorld = (node) => [
|
|
358
828
|
node.x * scale,
|
|
@@ -362,6 +832,8 @@ export const threeDriver = (container, options = {}) => {
|
|
|
362
832
|
let morph;
|
|
363
833
|
let layout;
|
|
364
834
|
const cue = () => {
|
|
835
|
+
camera.updateMatrixWorld();
|
|
836
|
+
uniforms.light.value.copy(LIGHT).transformDirection(camera.matrixWorldInverse);
|
|
365
837
|
const distance = camera.position.distanceTo(controls.target);
|
|
366
838
|
const { center, radius } = points.geometry.boundingSphere ?? {
|
|
367
839
|
center: controls.target,
|
|
@@ -446,6 +918,9 @@ export const threeDriver = (container, options = {}) => {
|
|
|
446
918
|
now - handled > REST;
|
|
447
919
|
controls.update();
|
|
448
920
|
cue();
|
|
921
|
+
if (solids.cube.mesh || solids.octahedron.mesh) {
|
|
922
|
+
placeShapes();
|
|
923
|
+
}
|
|
449
924
|
renderer.render(scene, camera);
|
|
450
925
|
groups.draw();
|
|
451
926
|
drawNames();
|
|
@@ -483,9 +958,14 @@ export const threeDriver = (container, options = {}) => {
|
|
|
483
958
|
hovering = requestAnimationFrame(() => {
|
|
484
959
|
raycaster.setFromCamera(pointer, camera);
|
|
485
960
|
raycaster.params.Points = { threshold: uniforms.unit.value * 2.4 };
|
|
961
|
+
const front = solid.visible
|
|
962
|
+
? (raycaster.intersectObject(solid, false)[0]?.distance ?? Number.POSITIVE_INFINITY)
|
|
963
|
+
: Number.POSITIVE_INFINITY;
|
|
486
964
|
const hit = raycaster.intersectObject(points, false).find((one) => {
|
|
487
965
|
const id = one.index === undefined ? undefined : ids[one.index];
|
|
488
|
-
return id !== undefined &&
|
|
966
|
+
return (id !== undefined &&
|
|
967
|
+
looks?.node(id).hidden !== true &&
|
|
968
|
+
one.distance < front + uniforms.unit.value * 3);
|
|
489
969
|
});
|
|
490
970
|
const next = hit?.index;
|
|
491
971
|
if (next !== hovered) {
|
|
@@ -520,6 +1000,7 @@ export const threeDriver = (container, options = {}) => {
|
|
|
520
1000
|
});
|
|
521
1001
|
moved = false;
|
|
522
1002
|
load(nodes, next, places);
|
|
1003
|
+
shapeSolid();
|
|
523
1004
|
fitAll(0);
|
|
524
1005
|
},
|
|
525
1006
|
fit: (fitted, animate) => {
|
|
@@ -627,8 +1108,13 @@ export const threeDriver = (container, options = {}) => {
|
|
|
627
1108
|
layout = undefined;
|
|
628
1109
|
};
|
|
629
1110
|
},
|
|
630
|
-
shape: (place) => {
|
|
631
|
-
|
|
1111
|
+
shape: (place, frame) => {
|
|
1112
|
+
surfaced = place === 'surface';
|
|
1113
|
+
if (frame !== shapeFrame) {
|
|
1114
|
+
shapeFrame = frame;
|
|
1115
|
+
solidAt = Number.NaN;
|
|
1116
|
+
}
|
|
1117
|
+
uniforms.steep.value = surfaced && !frame ? 1 : 0;
|
|
632
1118
|
},
|
|
633
1119
|
signal: (from, to, colors, animate) => {
|
|
634
1120
|
signals.signal(from, to, colors, animate);
|
|
@@ -642,7 +1128,13 @@ export const threeDriver = (container, options = {}) => {
|
|
|
642
1128
|
canvas.removeEventListener('pointermove', hover);
|
|
643
1129
|
canvas.removeEventListener('pointerleave', leave);
|
|
644
1130
|
controls.dispose();
|
|
645
|
-
for (const
|
|
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();
|
|
1137
|
+
for (const object of [points, quiet, lit, solid]) {
|
|
646
1138
|
object.geometry.dispose();
|
|
647
1139
|
object.material.dispose();
|
|
648
1140
|
}
|