@fusion.dev/helix-graph 0.121.0 → 0.123.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 +75 -2
- package/dist/connect-graph/arrange.d.ts +4 -2
- package/dist/connect-graph/arrange.js +45 -12
- package/dist/connect-graph/connect-graph.d.ts +10 -1
- package/dist/connect-graph/connect-graph.js +13 -4
- package/dist/connect-graph/diff.d.ts +2 -0
- package/dist/connect-graph/diff.js +2 -1
- package/dist/connect-graph/seed.d.ts +3 -0
- package/dist/connect-graph/seed.js +10 -2
- package/dist/connect-graph/shape-field.d.ts +61 -0
- package/dist/connect-graph/shape-field.js +172 -0
- package/dist/connect-graph/stencil.d.ts +56 -17
- package/dist/connect-graph/stencil.js +160 -79
- package/dist/connect-graph/surface-web.d.ts +18 -0
- package/dist/connect-graph/surface-web.js +99 -0
- package/dist/cosmos/cosmos-driver.js +8 -56
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/overlay/overlay.d.ts +18 -0
- package/dist/overlay/overlay.js +63 -1
- 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 +78 -0
- package/dist/three/index.d.ts +1 -0
- package/dist/three/index.js +1 -0
- package/dist/three/surface.d.ts +13 -0
- package/dist/three/surface.js +112 -0
- package/dist/three/three-driver.d.ts +32 -0
- package/dist/three/three-driver.js +797 -0
- package/package.json +28 -7
|
@@ -0,0 +1,797 @@
|
|
|
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';
|
|
3
|
+
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
4
|
+
import { ease, gesturesHeld, groupsOver, isLight, namesOver, signalsOver, } from '../overlay/overlay.js';
|
|
5
|
+
import { surfaceOf } from './surface.js';
|
|
6
|
+
const SHAPES = { circle: 0, diamond: 2, ring: 0, square: 1 };
|
|
7
|
+
const REACH = 100;
|
|
8
|
+
const STEP = 250;
|
|
9
|
+
const MORPH = 1400;
|
|
10
|
+
const ZOOM = 1.5;
|
|
11
|
+
const SAMPLE = 96;
|
|
12
|
+
const LINKS = { dark: { close: 0.3, far: 0.05 }, light: { close: 0.55, far: 0.12 } };
|
|
13
|
+
const REST = 8000;
|
|
14
|
+
const FOV = 40;
|
|
15
|
+
const FADE = `
|
|
16
|
+
uniform float middle;
|
|
17
|
+
uniform float radius;
|
|
18
|
+
uniform float steep;
|
|
19
|
+
|
|
20
|
+
float fade(float depth) {
|
|
21
|
+
float gentle = mix(1.0, 0.45, smoothstep(middle - radius, middle + radius, depth));
|
|
22
|
+
float sharp = mix(1.0, 0.1, smoothstep(middle - radius * 0.2, middle + radius * 0.4, depth));
|
|
23
|
+
return mix(gentle, sharp, steep);
|
|
24
|
+
}
|
|
25
|
+
`;
|
|
26
|
+
const VERTEX = `
|
|
27
|
+
${FADE}
|
|
28
|
+
attribute float size;
|
|
29
|
+
attribute vec4 tint;
|
|
30
|
+
attribute float shape;
|
|
31
|
+
attribute float glow;
|
|
32
|
+
|
|
33
|
+
uniform float scale;
|
|
34
|
+
uniform float unit;
|
|
35
|
+
|
|
36
|
+
varying vec4 vTint;
|
|
37
|
+
varying float vShape;
|
|
38
|
+
varying float vGlow;
|
|
39
|
+
varying float vFade;
|
|
40
|
+
|
|
41
|
+
void main() {
|
|
42
|
+
vec4 view = modelViewMatrix * vec4(position, 1.0);
|
|
43
|
+
gl_Position = projectionMatrix * view;
|
|
44
|
+
float depth = max(-view.z, 0.001);
|
|
45
|
+
/* As large as the note is, at the distance it is: nearer is larger. */
|
|
46
|
+
gl_PointSize = size * unit * scale / depth * (1.0 + glow * 1.4);
|
|
47
|
+
vFade = fade(depth);
|
|
48
|
+
vTint = tint;
|
|
49
|
+
vShape = shape;
|
|
50
|
+
vGlow = glow;
|
|
51
|
+
}
|
|
52
|
+
`;
|
|
53
|
+
const FRAGMENT = `
|
|
54
|
+
varying vec4 vTint;
|
|
55
|
+
varying float vShape;
|
|
56
|
+
varying float vGlow;
|
|
57
|
+
varying float vFade;
|
|
58
|
+
|
|
59
|
+
void main() {
|
|
60
|
+
vec2 p = gl_PointCoord * 2.0 - 1.0;
|
|
61
|
+
/* The note's body inside the sprite; the rest is room for its glow. */
|
|
62
|
+
float body = 1.0 / (1.0 + vGlow * 1.4);
|
|
63
|
+
float reach = vShape > 1.5 ? (abs(p.x) + abs(p.y)) * 0.78 : vShape > 0.5 ? max(abs(p.x), abs(p.y)) * 1.08 : length(p);
|
|
64
|
+
float d = reach / body;
|
|
65
|
+
float inside = 1.0 - smoothstep(0.86, 1.0, d);
|
|
66
|
+
/* Lit from the middle out, so a note reads as round. */
|
|
67
|
+
vec3 color = vTint.rgb * mix(1.15, 0.8, clamp(d, 0.0, 1.0));
|
|
68
|
+
float halo = vGlow * (1.0 - smoothstep(1.0, 1.0 / body, d)) * 0.55;
|
|
69
|
+
float alpha = (inside + halo * (1.0 - inside)) * vTint.a * vFade;
|
|
70
|
+
if (alpha < 0.01) discard;
|
|
71
|
+
gl_FragColor = vec4(color, alpha);
|
|
72
|
+
}
|
|
73
|
+
`;
|
|
74
|
+
const LINE_VERTEX = `
|
|
75
|
+
${FADE}
|
|
76
|
+
attribute vec3 color;
|
|
77
|
+
varying vec3 vColor;
|
|
78
|
+
varying float vFade;
|
|
79
|
+
|
|
80
|
+
void main() {
|
|
81
|
+
vec4 view = modelViewMatrix * vec4(position, 1.0);
|
|
82
|
+
gl_Position = projectionMatrix * view;
|
|
83
|
+
vColor = color;
|
|
84
|
+
vFade = fade(max(-view.z, 0.001));
|
|
85
|
+
}
|
|
86
|
+
`;
|
|
87
|
+
const LINE_FRAGMENT = `
|
|
88
|
+
uniform float opacity;
|
|
89
|
+
varying vec3 vColor;
|
|
90
|
+
varying float vFade;
|
|
91
|
+
|
|
92
|
+
void main() {
|
|
93
|
+
gl_FragColor = vec4(vColor, opacity * vFade);
|
|
94
|
+
}
|
|
95
|
+
`;
|
|
96
|
+
const SOLID_VERTEX = `
|
|
97
|
+
varying vec3 vNormal;
|
|
98
|
+
varying vec3 vView;
|
|
99
|
+
|
|
100
|
+
void main() {
|
|
101
|
+
vec4 view = modelViewMatrix * vec4(position, 1.0);
|
|
102
|
+
gl_Position = projectionMatrix * view;
|
|
103
|
+
vNormal = normalize(normalMatrix * normal);
|
|
104
|
+
vView = normalize(-view.xyz);
|
|
105
|
+
}
|
|
106
|
+
`;
|
|
107
|
+
const SOLID_FRAGMENT = `
|
|
108
|
+
uniform vec3 ground;
|
|
109
|
+
uniform vec3 ink;
|
|
110
|
+
varying vec3 vNormal;
|
|
111
|
+
varying vec3 vView;
|
|
112
|
+
|
|
113
|
+
void main() {
|
|
114
|
+
/* The ground's color, a breath of ink at the edge turning away, so the
|
|
115
|
+
shape is felt and not seen: the notes on it are what is drawn. */
|
|
116
|
+
float edge = 1.0 - abs(dot(normalize(vNormal), normalize(vView)));
|
|
117
|
+
gl_FragColor = vec4(mix(ground, ink, 0.16 * pow(edge, 2.0)), 1.0);
|
|
118
|
+
}
|
|
119
|
+
`;
|
|
120
|
+
const channels = (color) => {
|
|
121
|
+
const [red = 128, green = 128, blue = 128] = color.match(/[\d.]+/g)?.map(Number) ?? [];
|
|
122
|
+
return [red / 255, green / 255, blue / 255];
|
|
123
|
+
};
|
|
124
|
+
const depthOf = (id) => {
|
|
125
|
+
let hash = 2166136261;
|
|
126
|
+
for (let at = 0; at < id.length; at++) {
|
|
127
|
+
hash = Math.imul(hash ^ id.charCodeAt(at), 16777619);
|
|
128
|
+
}
|
|
129
|
+
return ((hash >>> 0) / 4294967296 - 0.5) * 2;
|
|
130
|
+
};
|
|
131
|
+
export const threeDriver = (container, options = {}) => {
|
|
132
|
+
const still = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
|
|
133
|
+
const renderer = new WebGLRenderer({ alpha: true, antialias: true });
|
|
134
|
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
|
135
|
+
renderer.setClearColor(0x000000, 0);
|
|
136
|
+
const canvas = renderer.domElement;
|
|
137
|
+
canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block';
|
|
138
|
+
container.append(canvas);
|
|
139
|
+
const scene = new Scene();
|
|
140
|
+
const camera = new PerspectiveCamera(FOV, 1, 0.1, REACH * 40);
|
|
141
|
+
camera.position.set(0, 0, REACH * 3);
|
|
142
|
+
const controls = new OrbitControls(camera, canvas);
|
|
143
|
+
controls.enableDamping = !still;
|
|
144
|
+
controls.dampingFactor = 0.08;
|
|
145
|
+
controls.rotateSpeed = 0.6;
|
|
146
|
+
controls.autoRotate = !still && options.autoRotate !== false;
|
|
147
|
+
controls.autoRotateSpeed = 0.5;
|
|
148
|
+
let ids = [];
|
|
149
|
+
let index = new Map();
|
|
150
|
+
let drawnNow = new Map();
|
|
151
|
+
let links = [];
|
|
152
|
+
let sizes = [];
|
|
153
|
+
let looks;
|
|
154
|
+
let hovered;
|
|
155
|
+
let selected;
|
|
156
|
+
let near = new Set();
|
|
157
|
+
let stopped = false;
|
|
158
|
+
let moved = false;
|
|
159
|
+
let handled = 0;
|
|
160
|
+
let scale = 1;
|
|
161
|
+
let positions = new Float32Array();
|
|
162
|
+
const uniforms = {
|
|
163
|
+
middle: { value: 1 },
|
|
164
|
+
radius: { value: 1 },
|
|
165
|
+
scale: { value: 1 },
|
|
166
|
+
steep: { value: 0 },
|
|
167
|
+
unit: { value: 1 },
|
|
168
|
+
};
|
|
169
|
+
const lines = (opacity) => new ShaderMaterial({
|
|
170
|
+
depthWrite: false,
|
|
171
|
+
fragmentShader: LINE_FRAGMENT,
|
|
172
|
+
transparent: true,
|
|
173
|
+
uniforms: {
|
|
174
|
+
middle: uniforms.middle,
|
|
175
|
+
opacity: { value: opacity },
|
|
176
|
+
radius: uniforms.radius,
|
|
177
|
+
steep: uniforms.steep,
|
|
178
|
+
},
|
|
179
|
+
vertexShader: LINE_VERTEX,
|
|
180
|
+
});
|
|
181
|
+
const points = new Points(new BufferGeometry(), new ShaderMaterial({
|
|
182
|
+
depthWrite: false,
|
|
183
|
+
fragmentShader: FRAGMENT,
|
|
184
|
+
transparent: true,
|
|
185
|
+
uniforms,
|
|
186
|
+
vertexShader: VERTEX,
|
|
187
|
+
}));
|
|
188
|
+
points.frustumCulled = false;
|
|
189
|
+
const quiet = new LineSegments(new BufferGeometry(), lines(LINKS.dark.far));
|
|
190
|
+
const lit = new LineSegments(new BufferGeometry(), lines(0.9));
|
|
191
|
+
quiet.frustumCulled = false;
|
|
192
|
+
lit.frustumCulled = false;
|
|
193
|
+
const solid = new Mesh(new BufferGeometry(), new ShaderMaterial({
|
|
194
|
+
fragmentShader: SOLID_FRAGMENT,
|
|
195
|
+
polygonOffset: true,
|
|
196
|
+
polygonOffsetFactor: 2,
|
|
197
|
+
polygonOffsetUnits: 2,
|
|
198
|
+
side: DoubleSide,
|
|
199
|
+
uniforms: { ground: { value: [0, 0, 0] }, ink: { value: [1, 1, 1] } },
|
|
200
|
+
vertexShader: SOLID_VERTEX,
|
|
201
|
+
}));
|
|
202
|
+
solid.renderOrder = -1;
|
|
203
|
+
solid.visible = false;
|
|
204
|
+
solid.frustumCulled = false;
|
|
205
|
+
scene.add(solid, quiet, lit, points);
|
|
206
|
+
let shapeFrame;
|
|
207
|
+
let surfaced = false;
|
|
208
|
+
let solidAt = Number.NaN;
|
|
209
|
+
const shapeSolid = () => {
|
|
210
|
+
const frame = shapeFrame;
|
|
211
|
+
if (!frame || !surfaced || frame.field.depth <= 0) {
|
|
212
|
+
solid.visible = false;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (solidAt === scale) {
|
|
216
|
+
solid.visible = true;
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
solidAt = scale;
|
|
220
|
+
const { corners, triangles } = surfaceOf(frame.field, -frame.shell * 0.6);
|
|
221
|
+
const { depth, height, width } = frame.field;
|
|
222
|
+
for (let at = 0; at < corners.length; at += 3) {
|
|
223
|
+
corners[at] = (frame.middle.x + (corners[at] - width / 2) * frame.scale) * scale;
|
|
224
|
+
corners[at + 1] =
|
|
225
|
+
(frame.middle.y - (corners[at + 1] - height / 2) * frame.scale) * scale;
|
|
226
|
+
corners[at + 2] =
|
|
227
|
+
(frame.middle.z + (corners[at + 2] - depth / 2) * frame.scale) * scale;
|
|
228
|
+
}
|
|
229
|
+
solid.geometry.dispose();
|
|
230
|
+
solid.geometry = new BufferGeometry();
|
|
231
|
+
solid.geometry.setAttribute('position', new BufferAttribute(corners, 3));
|
|
232
|
+
solid.geometry.setIndex(new BufferAttribute(triangles, 1));
|
|
233
|
+
solid.geometry.computeVertexNormals();
|
|
234
|
+
solid.visible = true;
|
|
235
|
+
};
|
|
236
|
+
const at = (spot) => new Vector3(positions[spot * 3], positions[spot * 3 + 1], positions[spot * 3 + 2]);
|
|
237
|
+
const onScreen = (id) => {
|
|
238
|
+
const spot = index.get(id);
|
|
239
|
+
if (spot === undefined) {
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
const world = at(spot);
|
|
243
|
+
const depth = world.clone().applyMatrix4(camera.matrixWorldInverse).z;
|
|
244
|
+
const projected = world.project(camera);
|
|
245
|
+
if (projected.z > 1 || depth > 0) {
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
const { clientHeight: height, clientWidth: width } = container;
|
|
249
|
+
const pixels = ((sizes[spot] ?? 1) * uniforms.unit.value * uniforms.scale.value) /
|
|
250
|
+
-depth /
|
|
251
|
+
renderer.getPixelRatio();
|
|
252
|
+
return {
|
|
253
|
+
radius: pixels / 2,
|
|
254
|
+
x: ((projected.x + 1) / 2) * width,
|
|
255
|
+
y: ((1 - projected.y) / 2) * height,
|
|
256
|
+
};
|
|
257
|
+
};
|
|
258
|
+
const held = gesturesHeld(container);
|
|
259
|
+
const signals = signalsOver(container, onScreen);
|
|
260
|
+
const nearOnScreen = (id) => {
|
|
261
|
+
const spot = index.get(id);
|
|
262
|
+
if (spot === undefined) {
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
const depth = -at(spot).applyMatrix4(camera.matrixWorldInverse).z;
|
|
266
|
+
return (uniforms.steep.value > 0 || solid.visible) && depth > uniforms.middle.value
|
|
267
|
+
? undefined
|
|
268
|
+
: onScreen(id);
|
|
269
|
+
};
|
|
270
|
+
const groups = groupsOver(container, nearOnScreen);
|
|
271
|
+
const names = namesOver(container, onScreen);
|
|
272
|
+
const drawNames = () => {
|
|
273
|
+
const pointed = hovered === undefined ? undefined : ids[hovered];
|
|
274
|
+
const shown = [...new Set([selected, pointed, ...near])].flatMap((id) => id === undefined
|
|
275
|
+
? []
|
|
276
|
+
: [{ id, name: drawnNow.get(id)?.label ?? id, strong: id === selected || id === pointed }]);
|
|
277
|
+
names.draw(looks, shown);
|
|
278
|
+
};
|
|
279
|
+
let glide;
|
|
280
|
+
const glideTo = (to, distance, duration) => {
|
|
281
|
+
const from = controls.target.clone();
|
|
282
|
+
const fromDistance = camera.position.distanceTo(controls.target);
|
|
283
|
+
if (duration <= 0 || still) {
|
|
284
|
+
const way = camera.position.clone().sub(controls.target).normalize();
|
|
285
|
+
controls.target.copy(to);
|
|
286
|
+
camera.position.copy(to.clone().add(way.multiplyScalar(distance)));
|
|
287
|
+
glide = undefined;
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
glide = { duration, from, fromDistance, start: performance.now(), to, toDistance: distance };
|
|
291
|
+
};
|
|
292
|
+
const sphereOf = (spots) => {
|
|
293
|
+
const center = new Vector3();
|
|
294
|
+
for (const spot of spots) {
|
|
295
|
+
center.add(at(spot));
|
|
296
|
+
}
|
|
297
|
+
center.divideScalar(Math.max(spots.length, 1));
|
|
298
|
+
let radius = 0;
|
|
299
|
+
for (const spot of spots) {
|
|
300
|
+
radius = Math.max(radius, at(spot).distanceTo(center));
|
|
301
|
+
}
|
|
302
|
+
return { center, radius: Math.max(radius, 1) };
|
|
303
|
+
};
|
|
304
|
+
const frame = (spots, duration) => {
|
|
305
|
+
if (spots.length === 0) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const { center, radius } = sphereOf(spots);
|
|
309
|
+
const fov = ((camera.fov / 2) * Math.PI) / 180;
|
|
310
|
+
const fits = radius / Math.tan(Math.min(fov, Math.atan(Math.tan(fov) * camera.aspect)));
|
|
311
|
+
glideTo(center, fits * 1.02, duration);
|
|
312
|
+
};
|
|
313
|
+
const fitAll = (duration) => {
|
|
314
|
+
if (!moved) {
|
|
315
|
+
frame(ids.map((_, spot) => spot), duration);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
let crowding = 1;
|
|
319
|
+
let dark = true;
|
|
320
|
+
const sizeOf = (size) => 1.6 + 1.35 * Math.max(size, 0);
|
|
321
|
+
const paint = () => {
|
|
322
|
+
if (!looks) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const tints = new Float32Array(ids.length * 4);
|
|
326
|
+
const drawn = new Float32Array(ids.length);
|
|
327
|
+
const shapes = new Float32Array(ids.length);
|
|
328
|
+
const glows = new Float32Array(ids.length);
|
|
329
|
+
selected = undefined;
|
|
330
|
+
near = new Set();
|
|
331
|
+
ids.forEach((id, spot) => {
|
|
332
|
+
const look = looks.node(id);
|
|
333
|
+
const [red, green, blue] = channels(look.color);
|
|
334
|
+
tints.set([red, green, blue, look.hidden ? 0 : 1], spot * 4);
|
|
335
|
+
drawn[spot] =
|
|
336
|
+
(sizes[spot] ?? 1) * (look.selected ? 1.5 : look.raised || spot === hovered ? 1.25 : 1);
|
|
337
|
+
shapes[spot] = SHAPES[look.shape ?? options.shape ?? 'circle'];
|
|
338
|
+
glows[spot] = look.selected ? 1 : spot === hovered ? 0.6 : 0;
|
|
339
|
+
if (look.selected) {
|
|
340
|
+
selected = id;
|
|
341
|
+
}
|
|
342
|
+
else if (look.raised) {
|
|
343
|
+
near.add(id);
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
const geometry = points.geometry;
|
|
347
|
+
geometry.setAttribute('tint', new BufferAttribute(tints, 4));
|
|
348
|
+
geometry.setAttribute('size', new BufferAttribute(drawn, 1));
|
|
349
|
+
geometry.setAttribute('shape', new BufferAttribute(shapes, 1));
|
|
350
|
+
geometry.setAttribute('glow', new BufferAttribute(glows, 1));
|
|
351
|
+
dark = !isLight(looks.ground);
|
|
352
|
+
const solidUniforms = solid.material.uniforms;
|
|
353
|
+
solidUniforms.ground.value = channels(looks.ground);
|
|
354
|
+
solidUniforms.ink.value = channels(looks.ink);
|
|
355
|
+
const blending = dark ? AdditiveBlending : NormalBlending;
|
|
356
|
+
points.material.blending = blending;
|
|
357
|
+
quiet.material.blending = blending;
|
|
358
|
+
lit.material.blending = blending;
|
|
359
|
+
points.material.needsUpdate = true;
|
|
360
|
+
quiet.material.needsUpdate = true;
|
|
361
|
+
lit.material.needsUpdate = true;
|
|
362
|
+
paintLinks();
|
|
363
|
+
};
|
|
364
|
+
const paintLinks = () => {
|
|
365
|
+
if (!looks) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const on = hovered === undefined ? undefined : ids[hovered];
|
|
369
|
+
const quietEnds = [];
|
|
370
|
+
const quietTints = [];
|
|
371
|
+
const litEnds = [];
|
|
372
|
+
const litTints = [];
|
|
373
|
+
for (const link of links) {
|
|
374
|
+
const from = index.get(link.source);
|
|
375
|
+
const to = index.get(link.target);
|
|
376
|
+
if (from === undefined || to === undefined) {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
const look = looks.link(link.source, link.target);
|
|
380
|
+
if (look.hidden) {
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
const touches = on !== undefined && (link.source === on || link.target === on);
|
|
384
|
+
const whole = look.raised || touches;
|
|
385
|
+
const color = channels(touches ? looks.node(on).color : look.color);
|
|
386
|
+
const ends = whole ? litEnds : quietEnds;
|
|
387
|
+
const tints = whole ? litTints : quietTints;
|
|
388
|
+
ends.push(from, to);
|
|
389
|
+
tints.push(...color, ...color);
|
|
390
|
+
}
|
|
391
|
+
const fill = (line, ends, tints) => {
|
|
392
|
+
const place = new Float32Array(ends.length * 3);
|
|
393
|
+
ends.forEach((spot, step) => {
|
|
394
|
+
place.set(positions.subarray(spot * 3, spot * 3 + 3), step * 3);
|
|
395
|
+
});
|
|
396
|
+
line.geometry.setAttribute('position', new BufferAttribute(place, 3));
|
|
397
|
+
line.geometry.setAttribute('color', new BufferAttribute(new Float32Array(tints), 3));
|
|
398
|
+
line.userData.ends = ends;
|
|
399
|
+
};
|
|
400
|
+
fill(quiet, quietEnds, quietTints);
|
|
401
|
+
fill(lit, litEnds, litTints);
|
|
402
|
+
};
|
|
403
|
+
const followLinks = () => {
|
|
404
|
+
for (const line of [quiet, lit]) {
|
|
405
|
+
const ends = line.userData.ends;
|
|
406
|
+
const place = line.geometry.getAttribute('position');
|
|
407
|
+
if (!ends || !place) {
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
ends.forEach((spot, step) => {
|
|
411
|
+
place.array.set(positions.subarray(spot * 3, spot * 3 + 3), step * 3);
|
|
412
|
+
});
|
|
413
|
+
place.needsUpdate = true;
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
const load = (nodes, next, places) => {
|
|
417
|
+
ids = nodes.map((node) => node.id);
|
|
418
|
+
index = new Map(ids.map((id, spot) => [id, spot]));
|
|
419
|
+
drawnNow = new Map(nodes.map((node) => [node.id, node]));
|
|
420
|
+
crowding = Math.min(Math.max(8 / Math.sqrt(Math.max(ids.length, 1)), 0.25), 1.5);
|
|
421
|
+
sizes = nodes.map((node) => sizeOf(node.size));
|
|
422
|
+
links = next.filter((link) => index.has(link.source) && index.has(link.target));
|
|
423
|
+
positions = places;
|
|
424
|
+
points.geometry.setAttribute('position', new BufferAttribute(positions, 3));
|
|
425
|
+
points.geometry.computeBoundingSphere();
|
|
426
|
+
hovered = undefined;
|
|
427
|
+
paint();
|
|
428
|
+
};
|
|
429
|
+
const toWorld = (node) => [
|
|
430
|
+
node.x * scale,
|
|
431
|
+
node.y * scale,
|
|
432
|
+
(node.z ?? (node.id ? depthOf(node.id) : 0)) * scale,
|
|
433
|
+
];
|
|
434
|
+
let morph;
|
|
435
|
+
let layout;
|
|
436
|
+
const cue = () => {
|
|
437
|
+
const distance = camera.position.distanceTo(controls.target);
|
|
438
|
+
const { center, radius } = points.geometry.boundingSphere ?? {
|
|
439
|
+
center: controls.target,
|
|
440
|
+
radius: REACH,
|
|
441
|
+
};
|
|
442
|
+
const nearest = Math.max(distance - radius * 1.5, distance * 0.01);
|
|
443
|
+
const furthest = distance + radius * 1.5;
|
|
444
|
+
if (Math.abs(camera.near - nearest) > nearest * 0.05 ||
|
|
445
|
+
Math.abs(camera.far - furthest) > furthest * 0.05) {
|
|
446
|
+
camera.near = nearest;
|
|
447
|
+
camera.far = furthest;
|
|
448
|
+
camera.updateProjectionMatrix();
|
|
449
|
+
}
|
|
450
|
+
uniforms.unit.value = (radius / REACH) * crowding;
|
|
451
|
+
uniforms.middle.value = camera.position.distanceTo(center);
|
|
452
|
+
uniforms.radius.value = radius;
|
|
453
|
+
uniforms.scale.value =
|
|
454
|
+
renderer.domElement.height / 2 / Math.tan(((camera.fov / 2) * Math.PI) / 180);
|
|
455
|
+
const closeness = Math.min(Math.max(1 - distance / (radius * 3), 0), 1);
|
|
456
|
+
const shade = LINKS[dark ? 'dark' : 'light'];
|
|
457
|
+
quiet.material.uniforms.opacity.value =
|
|
458
|
+
shade.far + (shade.close - shade.far) * closeness;
|
|
459
|
+
};
|
|
460
|
+
let pending = 0;
|
|
461
|
+
const tick = (now) => {
|
|
462
|
+
if (stopped) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
pending = requestAnimationFrame(tick);
|
|
466
|
+
if (glide) {
|
|
467
|
+
const through = Math.min((now - glide.start) / glide.duration, 1);
|
|
468
|
+
const eased = ease(through);
|
|
469
|
+
const target = glide.from.clone().lerp(glide.to, eased);
|
|
470
|
+
const distance = glide.fromDistance + (glide.toDistance - glide.fromDistance) * eased;
|
|
471
|
+
const way = camera.position.clone().sub(controls.target).normalize();
|
|
472
|
+
controls.target.copy(target);
|
|
473
|
+
camera.position.copy(target.clone().add(way.multiplyScalar(distance)));
|
|
474
|
+
if (through >= 1) {
|
|
475
|
+
glide = undefined;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (morph) {
|
|
479
|
+
const through = still ? 1 : Math.min((now - morph.start) / MORPH, 1);
|
|
480
|
+
const eased = ease(through);
|
|
481
|
+
for (let step = 0; step < positions.length; step++) {
|
|
482
|
+
positions[step] =
|
|
483
|
+
morph.from[step] +
|
|
484
|
+
(morph.to[step] - morph.from[step]) * eased;
|
|
485
|
+
}
|
|
486
|
+
;
|
|
487
|
+
points.geometry.getAttribute('position').needsUpdate = true;
|
|
488
|
+
followLinks();
|
|
489
|
+
if (through >= 1) {
|
|
490
|
+
morph = undefined;
|
|
491
|
+
points.geometry.computeBoundingSphere();
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (layout) {
|
|
495
|
+
const started = performance.now();
|
|
496
|
+
while (performance.now() - started < 10 &&
|
|
497
|
+
layout.simulation.alpha() > layout.simulation.alphaMin()) {
|
|
498
|
+
layout.simulation.tick();
|
|
499
|
+
}
|
|
500
|
+
layout.nodes.forEach((node, spot) => {
|
|
501
|
+
positions.set([node.x * scale, node.y * scale, node.z * scale], spot * 3);
|
|
502
|
+
});
|
|
503
|
+
points.geometry.getAttribute('position').needsUpdate = true;
|
|
504
|
+
points.geometry.computeBoundingSphere();
|
|
505
|
+
followLinks();
|
|
506
|
+
if (layout.simulation.alpha() <= layout.simulation.alphaMin() || now > layout.limit) {
|
|
507
|
+
layout.done();
|
|
508
|
+
}
|
|
509
|
+
else if (!moved && !glide) {
|
|
510
|
+
fitAll(0);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
controls.autoRotate =
|
|
514
|
+
!still &&
|
|
515
|
+
options.autoRotate !== false &&
|
|
516
|
+
hovered === undefined &&
|
|
517
|
+
selected === undefined &&
|
|
518
|
+
now - handled > REST;
|
|
519
|
+
controls.update();
|
|
520
|
+
cue();
|
|
521
|
+
renderer.render(scene, camera);
|
|
522
|
+
groups.draw();
|
|
523
|
+
drawNames();
|
|
524
|
+
};
|
|
525
|
+
const resize = () => {
|
|
526
|
+
const width = Math.max(container.clientWidth, 1);
|
|
527
|
+
const height = Math.max(container.clientHeight, 1);
|
|
528
|
+
renderer.setSize(width, height, false);
|
|
529
|
+
camera.aspect = width / height;
|
|
530
|
+
camera.updateProjectionMatrix();
|
|
531
|
+
};
|
|
532
|
+
let resizing = 0;
|
|
533
|
+
const sized = new ResizeObserver(() => {
|
|
534
|
+
cancelAnimationFrame(resizing);
|
|
535
|
+
resizing = requestAnimationFrame(resize);
|
|
536
|
+
});
|
|
537
|
+
sized.observe(container);
|
|
538
|
+
resize();
|
|
539
|
+
pending = requestAnimationFrame(tick);
|
|
540
|
+
controls.addEventListener('start', () => {
|
|
541
|
+
moved = true;
|
|
542
|
+
handled = Number.POSITIVE_INFINITY;
|
|
543
|
+
glide = undefined;
|
|
544
|
+
});
|
|
545
|
+
controls.addEventListener('end', () => {
|
|
546
|
+
handled = performance.now();
|
|
547
|
+
});
|
|
548
|
+
const raycaster = new Raycaster();
|
|
549
|
+
const pointer = new Vector2();
|
|
550
|
+
let hovering = 0;
|
|
551
|
+
const hover = (event) => {
|
|
552
|
+
const box = canvas.getBoundingClientRect();
|
|
553
|
+
pointer.set(((event.clientX - box.left) / box.width) * 2 - 1, -((event.clientY - box.top) / box.height) * 2 + 1);
|
|
554
|
+
cancelAnimationFrame(hovering);
|
|
555
|
+
hovering = requestAnimationFrame(() => {
|
|
556
|
+
raycaster.setFromCamera(pointer, camera);
|
|
557
|
+
raycaster.params.Points = { threshold: uniforms.unit.value * 2.4 };
|
|
558
|
+
const front = solid.visible
|
|
559
|
+
? (raycaster.intersectObject(solid, false)[0]?.distance ?? Number.POSITIVE_INFINITY)
|
|
560
|
+
: Number.POSITIVE_INFINITY;
|
|
561
|
+
const hit = raycaster.intersectObject(points, false).find((one) => {
|
|
562
|
+
const id = one.index === undefined ? undefined : ids[one.index];
|
|
563
|
+
return (id !== undefined &&
|
|
564
|
+
looks?.node(id).hidden !== true &&
|
|
565
|
+
one.distance < front + uniforms.unit.value * 3);
|
|
566
|
+
});
|
|
567
|
+
const next = hit?.index;
|
|
568
|
+
if (next !== hovered) {
|
|
569
|
+
hovered = next;
|
|
570
|
+
canvas.style.cursor = next === undefined ? '' : 'pointer';
|
|
571
|
+
paint();
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
};
|
|
575
|
+
const leave = () => {
|
|
576
|
+
if (hovered !== undefined) {
|
|
577
|
+
hovered = undefined;
|
|
578
|
+
canvas.style.cursor = '';
|
|
579
|
+
paint();
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
canvas.addEventListener('pointermove', hover);
|
|
583
|
+
canvas.addEventListener('pointerleave', leave);
|
|
584
|
+
return {
|
|
585
|
+
center: (id, animate) => {
|
|
586
|
+
const spot = index.get(id);
|
|
587
|
+
if (spot !== undefined) {
|
|
588
|
+
glideTo(at(spot), camera.position.distanceTo(controls.target), animate ? STEP * 2 : 0);
|
|
589
|
+
}
|
|
590
|
+
},
|
|
591
|
+
draw: ({ links: next, nodes }) => {
|
|
592
|
+
const reach = Math.max(1e-6, ...nodes.map((node) => Math.max(Math.abs(node.x), Math.abs(node.y), Math.abs(node.z ?? 0))));
|
|
593
|
+
scale = REACH / reach;
|
|
594
|
+
const places = new Float32Array(nodes.length * 3);
|
|
595
|
+
nodes.forEach((node, spot) => {
|
|
596
|
+
places.set(toWorld(node), spot * 3);
|
|
597
|
+
});
|
|
598
|
+
moved = false;
|
|
599
|
+
load(nodes, next, places);
|
|
600
|
+
shapeSolid();
|
|
601
|
+
fitAll(0);
|
|
602
|
+
},
|
|
603
|
+
fit: (fitted, animate) => {
|
|
604
|
+
frame(fitted.flatMap((id) => {
|
|
605
|
+
const spot = index.get(id);
|
|
606
|
+
return spot === undefined ? [] : [spot];
|
|
607
|
+
}), animate ? STEP * 2 : 0);
|
|
608
|
+
},
|
|
609
|
+
gestures: (next) => {
|
|
610
|
+
held.set(next);
|
|
611
|
+
controls.enabled = next !== 'off';
|
|
612
|
+
controls.touches.ONE = next === 'cooperative' ? null : TOUCH.ROTATE;
|
|
613
|
+
},
|
|
614
|
+
groups: (next) => {
|
|
615
|
+
if (options.groupLabels !== false) {
|
|
616
|
+
groups.set(next.map((group) => ({ ...group, ids: group.ids.slice(0, SAMPLE) })));
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
look: (next) => {
|
|
620
|
+
looks = next;
|
|
621
|
+
signals.look(next);
|
|
622
|
+
groups.look(next);
|
|
623
|
+
paint();
|
|
624
|
+
},
|
|
625
|
+
onPress: (press) => {
|
|
626
|
+
let down;
|
|
627
|
+
const start = (event) => {
|
|
628
|
+
down = { time: performance.now(), x: event.clientX, y: event.clientY };
|
|
629
|
+
};
|
|
630
|
+
const end = (event) => {
|
|
631
|
+
const was = down;
|
|
632
|
+
down = undefined;
|
|
633
|
+
if (was &&
|
|
634
|
+
Math.hypot(event.clientX - was.x, event.clientY - was.y) < 5 &&
|
|
635
|
+
performance.now() - was.time < 500 &&
|
|
636
|
+
hovered !== undefined) {
|
|
637
|
+
const id = ids[hovered];
|
|
638
|
+
if (id !== undefined) {
|
|
639
|
+
press(id);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
canvas.addEventListener('pointerdown', start);
|
|
644
|
+
canvas.addEventListener('pointerup', end);
|
|
645
|
+
return () => {
|
|
646
|
+
canvas.removeEventListener('pointerdown', start);
|
|
647
|
+
canvas.removeEventListener('pointerup', end);
|
|
648
|
+
};
|
|
649
|
+
},
|
|
650
|
+
panBy: (x, y, animate) => {
|
|
651
|
+
const { clientHeight: height, clientWidth: width } = container;
|
|
652
|
+
const offset = camera.position.clone().sub(controls.target);
|
|
653
|
+
const turn = new Vector3(0, 1, 0);
|
|
654
|
+
offset.applyAxisAngle(turn, (-x / Math.max(width, 1)) * Math.PI);
|
|
655
|
+
const side = new Vector3().crossVectors(turn, offset).normalize();
|
|
656
|
+
offset.applyAxisAngle(side, (y / Math.max(height, 1)) * (Math.PI / 2));
|
|
657
|
+
const to = controls.target.clone().add(offset);
|
|
658
|
+
if (!animate || still) {
|
|
659
|
+
camera.position.copy(to);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
const from = camera.position.clone();
|
|
663
|
+
const start = performance.now();
|
|
664
|
+
const step = (now) => {
|
|
665
|
+
const through = Math.min((now - start) / STEP, 1);
|
|
666
|
+
camera.position.copy(from.clone().lerp(to, ease(through)));
|
|
667
|
+
if (through < 1 && !stopped) {
|
|
668
|
+
requestAnimationFrame(step);
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
requestAnimationFrame(step);
|
|
672
|
+
},
|
|
673
|
+
settle: (unplaced, placed) => {
|
|
674
|
+
const nodes = ids.map((id, spot) => {
|
|
675
|
+
const x = positions[spot * 3] / scale;
|
|
676
|
+
const y = positions[spot * 3 + 1] / scale;
|
|
677
|
+
const z = positions[spot * 3 + 2] / scale;
|
|
678
|
+
return unplaced.has(id) ? { id, x, y, z } : { fx: x, fy: y, fz: z, id, x, y, z };
|
|
679
|
+
});
|
|
680
|
+
const simulation = forceSimulation(nodes, 3)
|
|
681
|
+
.force('link', forceLink(links.map((link) => ({ source: link.source, target: link.target })))
|
|
682
|
+
.id((node) => node.id)
|
|
683
|
+
.distance(1.6))
|
|
684
|
+
.force('charge', forceManyBody().strength(-1.2).theta(1.2))
|
|
685
|
+
.force('center', forceCenter(0, 0, 0))
|
|
686
|
+
.alphaDecay(0.03)
|
|
687
|
+
.stop();
|
|
688
|
+
let done = false;
|
|
689
|
+
layout = {
|
|
690
|
+
done: () => {
|
|
691
|
+
if (done) {
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
done = true;
|
|
695
|
+
layout = undefined;
|
|
696
|
+
fitAll(STEP * 2);
|
|
697
|
+
placed(new Map(nodes.map((node) => [node.id, { x: node.x, y: node.y, z: node.z }])));
|
|
698
|
+
},
|
|
699
|
+
limit: performance.now() + (options.layoutLimit ?? 10000),
|
|
700
|
+
nodes,
|
|
701
|
+
simulation,
|
|
702
|
+
};
|
|
703
|
+
return () => {
|
|
704
|
+
done = true;
|
|
705
|
+
layout = undefined;
|
|
706
|
+
};
|
|
707
|
+
},
|
|
708
|
+
shape: (place, frame) => {
|
|
709
|
+
surfaced = place === 'surface';
|
|
710
|
+
if (frame !== shapeFrame) {
|
|
711
|
+
shapeFrame = frame;
|
|
712
|
+
solidAt = Number.NaN;
|
|
713
|
+
}
|
|
714
|
+
uniforms.steep.value = surfaced && !frame ? 1 : 0;
|
|
715
|
+
},
|
|
716
|
+
signal: (from, to, colors, animate) => {
|
|
717
|
+
signals.signal(from, to, colors, animate);
|
|
718
|
+
},
|
|
719
|
+
stop: () => {
|
|
720
|
+
stopped = true;
|
|
721
|
+
cancelAnimationFrame(pending);
|
|
722
|
+
cancelAnimationFrame(hovering);
|
|
723
|
+
cancelAnimationFrame(resizing);
|
|
724
|
+
sized.disconnect();
|
|
725
|
+
canvas.removeEventListener('pointermove', hover);
|
|
726
|
+
canvas.removeEventListener('pointerleave', leave);
|
|
727
|
+
controls.dispose();
|
|
728
|
+
for (const object of [points, quiet, lit, solid]) {
|
|
729
|
+
object.geometry.dispose();
|
|
730
|
+
object.material.dispose();
|
|
731
|
+
}
|
|
732
|
+
renderer.dispose();
|
|
733
|
+
canvas.remove();
|
|
734
|
+
signals.stop();
|
|
735
|
+
groups.stop();
|
|
736
|
+
names.stop();
|
|
737
|
+
held.stop();
|
|
738
|
+
},
|
|
739
|
+
update: (changes, animate) => {
|
|
740
|
+
const kept = new Map();
|
|
741
|
+
ids.forEach((id, spot) => {
|
|
742
|
+
const node = drawnNow.get(id);
|
|
743
|
+
if (node) {
|
|
744
|
+
kept.set(id, {
|
|
745
|
+
node,
|
|
746
|
+
world: [
|
|
747
|
+
positions[spot * 3],
|
|
748
|
+
positions[spot * 3 + 1],
|
|
749
|
+
positions[spot * 3 + 2],
|
|
750
|
+
],
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
for (const id of changes.removeNodes) {
|
|
755
|
+
kept.delete(id);
|
|
756
|
+
}
|
|
757
|
+
let shifted = false;
|
|
758
|
+
const goes = new Map();
|
|
759
|
+
for (const node of changes.changeNodes) {
|
|
760
|
+
const was = drawnNow.get(node.id);
|
|
761
|
+
const now = kept.get(node.id);
|
|
762
|
+
const moves = was !== undefined && (was.x !== node.x || was.y !== node.y || was.z !== node.z);
|
|
763
|
+
shifted ||= moves;
|
|
764
|
+
if (moves) {
|
|
765
|
+
goes.set(node.id, toWorld(node));
|
|
766
|
+
}
|
|
767
|
+
kept.set(node.id, { node, world: now?.world ?? toWorld(node) });
|
|
768
|
+
}
|
|
769
|
+
for (const node of changes.addNodes) {
|
|
770
|
+
kept.set(node.id, { node, world: toWorld(node) });
|
|
771
|
+
}
|
|
772
|
+
const removed = new Set(changes.removeLinks.map((link) => `${link.source}\u0000${link.target}`));
|
|
773
|
+
const next = [
|
|
774
|
+
...links.filter((link) => !removed.has(`${link.source}\u0000${link.target}`)),
|
|
775
|
+
...changes.addLinks,
|
|
776
|
+
];
|
|
777
|
+
const entries = [...kept.values()];
|
|
778
|
+
const from = new Float32Array(entries.length * 3);
|
|
779
|
+
const to = new Float32Array(entries.length * 3);
|
|
780
|
+
entries.forEach(({ node, world }, spot) => {
|
|
781
|
+
from.set(world, spot * 3);
|
|
782
|
+
to.set(goes.get(node.id) ?? world, spot * 3);
|
|
783
|
+
});
|
|
784
|
+
load(entries.map(({ node }) => node), next, new Float32Array(animate && shifted ? from : to));
|
|
785
|
+
if (animate && shifted) {
|
|
786
|
+
morph = { from, start: performance.now(), to };
|
|
787
|
+
}
|
|
788
|
+
if (shifted) {
|
|
789
|
+
fitAll(animate ? MORPH : 0);
|
|
790
|
+
}
|
|
791
|
+
},
|
|
792
|
+
zoomBy: (direction, animate) => {
|
|
793
|
+
const distance = camera.position.distanceTo(controls.target);
|
|
794
|
+
glideTo(controls.target.clone(), direction > 0 ? distance / ZOOM : distance * ZOOM, animate ? STEP : 0);
|
|
795
|
+
},
|
|
796
|
+
};
|
|
797
|
+
};
|