@fusion.dev/helix-graph 0.120.0 → 0.121.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 +141 -8
- package/dist/connect-graph/arrange.d.ts +18 -0
- package/dist/connect-graph/arrange.js +92 -0
- package/dist/connect-graph/connect-graph.d.ts +40 -5
- package/dist/connect-graph/connect-graph.js +81 -18
- package/dist/connect-graph/diff.d.ts +1 -1
- package/dist/connect-graph/looks.d.ts +29 -3
- package/dist/connect-graph/looks.js +24 -6
- package/dist/connect-graph/seed.js +11 -2
- package/dist/connect-graph/stencil.d.ts +34 -0
- package/dist/connect-graph/stencil.js +121 -0
- package/dist/cosmos/cosmos-driver.d.ts +67 -0
- package/dist/cosmos/cosmos-driver.js +467 -0
- package/dist/cosmos/index.d.ts +1 -0
- package/dist/cosmos/index.js +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/overlay/overlay.d.ts +59 -0
- package/dist/overlay/overlay.js +296 -0
- package/dist/sigma/node-program.d.ts +71 -0
- package/dist/sigma/node-program.js +170 -0
- package/dist/sigma/sigma-driver.d.ts +31 -3
- package/dist/sigma/sigma-driver.js +125 -140
- package/package.json +25 -6
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { mixRgb } from '../connect-graph/looks.js';
|
|
2
|
+
const SPEED = 340;
|
|
3
|
+
const QUICKEST = 450;
|
|
4
|
+
const SLOWEST = 1400;
|
|
5
|
+
const ARRIVAL = 380;
|
|
6
|
+
const FLYING = 400;
|
|
7
|
+
const withAlpha = (color, alpha) => {
|
|
8
|
+
const [red = 128, green = 128, blue = 128] = color.match(/[\d.]+/g)?.map(Number) ?? [];
|
|
9
|
+
return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
|
|
10
|
+
};
|
|
11
|
+
const TAIL = 110;
|
|
12
|
+
const pulse = (context, leaves, arrives, from, to, along, through, glowing) => {
|
|
13
|
+
const strength = glowing ? 1.3 : 1;
|
|
14
|
+
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
|
+
context.lineCap = 'round';
|
|
20
|
+
context.globalAlpha = 0.14 * Math.sin(Math.PI * through);
|
|
21
|
+
const line = context.createLinearGradient(from.x, from.y, to.x, to.y);
|
|
22
|
+
line.addColorStop(0, leaves);
|
|
23
|
+
line.addColorStop(1, arrives);
|
|
24
|
+
context.strokeStyle = line;
|
|
25
|
+
context.lineWidth = 1;
|
|
26
|
+
context.beginPath();
|
|
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;
|
|
47
|
+
context.stroke();
|
|
48
|
+
const reach = 5 * strength;
|
|
49
|
+
const light = context.createRadialGradient(head.x, head.y, 0, head.x, head.y, reach);
|
|
50
|
+
light.addColorStop(0, 'rgba(255, 255, 255, 0.9)');
|
|
51
|
+
light.addColorStop(0.4, withAlpha(color, 0.75));
|
|
52
|
+
light.addColorStop(1, withAlpha(color, 0));
|
|
53
|
+
context.fillStyle = light;
|
|
54
|
+
context.beginPath();
|
|
55
|
+
context.arc(head.x, head.y, reach, 0, Math.PI * 2);
|
|
56
|
+
context.fill();
|
|
57
|
+
};
|
|
58
|
+
const arrive = (context, color, at, gone) => {
|
|
59
|
+
const reach = at.radius + 6;
|
|
60
|
+
const glow = context.createRadialGradient(at.x, at.y, at.radius * 0.4, at.x, at.y, reach);
|
|
61
|
+
glow.addColorStop(0, withAlpha(color, 0.5 * (1 - gone) ** 2));
|
|
62
|
+
glow.addColorStop(1, withAlpha(color, 0));
|
|
63
|
+
context.fillStyle = glow;
|
|
64
|
+
context.beginPath();
|
|
65
|
+
context.arc(at.x, at.y, reach, 0, Math.PI * 2);
|
|
66
|
+
context.fill();
|
|
67
|
+
};
|
|
68
|
+
const isLight = (color) => {
|
|
69
|
+
const [red = 0, green = 0, blue = 0] = color.match(/[\d.]+/g)?.map(Number) ?? [];
|
|
70
|
+
return (0.2126 * red + 0.7152 * green + 0.0722 * blue) / 255 > 0.5;
|
|
71
|
+
};
|
|
72
|
+
export const ease = (at) => (at < 0.5 ? 2 * at * at : 1 - (-2 * at + 2) ** 2 / 2);
|
|
73
|
+
const nameGroups = (context, groups, looks, at) => {
|
|
74
|
+
context.font = `650 11px ${looks.font}`;
|
|
75
|
+
context.letterSpacing = '0.14em';
|
|
76
|
+
context.textAlign = 'center';
|
|
77
|
+
context.textBaseline = 'middle';
|
|
78
|
+
context.shadowColor = looks.ground;
|
|
79
|
+
for (const group of groups) {
|
|
80
|
+
let x = 0;
|
|
81
|
+
let y = 0;
|
|
82
|
+
let count = 0;
|
|
83
|
+
const points = [];
|
|
84
|
+
for (const id of group.ids) {
|
|
85
|
+
const point = at(id);
|
|
86
|
+
if (point) {
|
|
87
|
+
x += point.x;
|
|
88
|
+
y += point.y;
|
|
89
|
+
count += 1;
|
|
90
|
+
points.push(point);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (count < 2) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
x /= count;
|
|
97
|
+
y /= count;
|
|
98
|
+
const spread = points.reduce((sum, point) => sum + Math.hypot(point.x - x, point.y - y), 0) / count;
|
|
99
|
+
if (spread < 12) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const name = group.name.toLocaleUpperCase();
|
|
103
|
+
context.globalAlpha = group.faded ? 0.3 : 0.92;
|
|
104
|
+
context.fillStyle = mixRgb(group.color, looks.ink, 0.6);
|
|
105
|
+
context.shadowBlur = 10;
|
|
106
|
+
context.fillText(name, x, y);
|
|
107
|
+
context.shadowBlur = 3;
|
|
108
|
+
context.fillText(name, x, y);
|
|
109
|
+
}
|
|
110
|
+
context.shadowBlur = 0;
|
|
111
|
+
context.letterSpacing = '0px';
|
|
112
|
+
context.globalAlpha = 1;
|
|
113
|
+
};
|
|
114
|
+
const layer = (container, kind) => {
|
|
115
|
+
const canvas = document.createElement('canvas');
|
|
116
|
+
canvas.setAttribute('aria-hidden', 'true');
|
|
117
|
+
canvas.dataset.helixGraphLayer = kind;
|
|
118
|
+
canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;pointer-events:none';
|
|
119
|
+
const signals = container.querySelector('canvas[data-helix-graph-layer="signals"]');
|
|
120
|
+
if (kind === 'groups' && signals) {
|
|
121
|
+
container.insertBefore(canvas, signals);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
container.append(canvas);
|
|
125
|
+
}
|
|
126
|
+
return canvas;
|
|
127
|
+
};
|
|
128
|
+
const fresh = (canvas, container) => {
|
|
129
|
+
const context = canvas.getContext('2d');
|
|
130
|
+
if (!context) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const ratio = window.devicePixelRatio || 1;
|
|
134
|
+
const width = Math.round(container.clientWidth * ratio);
|
|
135
|
+
const height = Math.round(container.clientHeight * ratio);
|
|
136
|
+
if (canvas.width !== width || canvas.height !== height) {
|
|
137
|
+
canvas.width = width;
|
|
138
|
+
canvas.height = height;
|
|
139
|
+
}
|
|
140
|
+
context.setTransform(ratio, 0, 0, ratio, 0, 0);
|
|
141
|
+
context.clearRect(0, 0, width, height);
|
|
142
|
+
return context;
|
|
143
|
+
};
|
|
144
|
+
export const signalsOver = (container, onScreen) => {
|
|
145
|
+
let overlay;
|
|
146
|
+
let flights = [];
|
|
147
|
+
let flying = 0;
|
|
148
|
+
let ink = 'rgb(128, 128, 128)';
|
|
149
|
+
let glowing = false;
|
|
150
|
+
const fly = (now) => {
|
|
151
|
+
const context = overlay ? fresh(overlay, container) : null;
|
|
152
|
+
if (!context) {
|
|
153
|
+
flights = [];
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
context.globalCompositeOperation = glowing ? 'lighter' : 'source-over';
|
|
157
|
+
flights = flights.filter((flight) => {
|
|
158
|
+
const from = onScreen(flight.from);
|
|
159
|
+
const to = onScreen(flight.to);
|
|
160
|
+
if (!from || !to) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
flight.crossing ??= Math.min(Math.max((Math.hypot(to.x - from.x, to.y - from.y) / SPEED) * 1000, QUICKEST), SLOWEST);
|
|
164
|
+
const crossing = flight.crossing;
|
|
165
|
+
const gone = (now - flight.started) / crossing;
|
|
166
|
+
if (gone >= 1 + ARRIVAL / crossing) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
context.save();
|
|
170
|
+
if (gone < 1) {
|
|
171
|
+
if (flight.animate) {
|
|
172
|
+
pulse(context, flight.leaves, flight.arrives, from, to, ease(gone), gone, glowing);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
const line = context.createLinearGradient(from.x, from.y, to.x, to.y);
|
|
176
|
+
line.addColorStop(0, flight.leaves);
|
|
177
|
+
line.addColorStop(1, flight.arrives);
|
|
178
|
+
context.globalAlpha = 1 - gone;
|
|
179
|
+
context.strokeStyle = line;
|
|
180
|
+
context.lineWidth = 2;
|
|
181
|
+
context.beginPath();
|
|
182
|
+
context.moveTo(from.x, from.y);
|
|
183
|
+
context.lineTo(to.x, to.y);
|
|
184
|
+
context.stroke();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
else if (flight.animate) {
|
|
188
|
+
arrive(context, flight.arrives, to, (gone - 1) / (ARRIVAL / crossing));
|
|
189
|
+
}
|
|
190
|
+
context.restore();
|
|
191
|
+
return true;
|
|
192
|
+
});
|
|
193
|
+
flying = flights.length > 0 ? requestAnimationFrame(fly) : 0;
|
|
194
|
+
};
|
|
195
|
+
return {
|
|
196
|
+
look: (looks) => {
|
|
197
|
+
ink = looks.ink;
|
|
198
|
+
glowing = isLight(looks.ink);
|
|
199
|
+
},
|
|
200
|
+
signal: (from, to, colors, animate) => {
|
|
201
|
+
overlay ??= layer(container, 'signals');
|
|
202
|
+
flights.push({
|
|
203
|
+
animate,
|
|
204
|
+
arrives: colors?.to ?? ink,
|
|
205
|
+
from,
|
|
206
|
+
leaves: colors?.from ?? ink,
|
|
207
|
+
started: performance.now(),
|
|
208
|
+
to,
|
|
209
|
+
});
|
|
210
|
+
if (flights.length > FLYING) {
|
|
211
|
+
flights = flights.slice(-FLYING);
|
|
212
|
+
}
|
|
213
|
+
if (!flying) {
|
|
214
|
+
flying = requestAnimationFrame(fly);
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
stop: () => {
|
|
218
|
+
cancelAnimationFrame(flying);
|
|
219
|
+
flying = 0;
|
|
220
|
+
flights = [];
|
|
221
|
+
overlay?.remove();
|
|
222
|
+
overlay = undefined;
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
export const groupsOver = (container, onScreen) => {
|
|
227
|
+
let canvas;
|
|
228
|
+
let groups = [];
|
|
229
|
+
let looks;
|
|
230
|
+
const draw = () => {
|
|
231
|
+
const context = canvas && looks ? fresh(canvas, container) : null;
|
|
232
|
+
if (context && looks) {
|
|
233
|
+
nameGroups(context, groups, looks, onScreen);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
return {
|
|
237
|
+
draw,
|
|
238
|
+
look: (next) => {
|
|
239
|
+
looks = next;
|
|
240
|
+
draw();
|
|
241
|
+
},
|
|
242
|
+
set: (next) => {
|
|
243
|
+
canvas ??= layer(container, 'groups');
|
|
244
|
+
groups = next;
|
|
245
|
+
draw();
|
|
246
|
+
},
|
|
247
|
+
stop: () => {
|
|
248
|
+
canvas?.remove();
|
|
249
|
+
canvas = undefined;
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
};
|
|
253
|
+
export const gesturesHeld = (container) => {
|
|
254
|
+
let mode = 'free';
|
|
255
|
+
const touchAction = container.style.touchAction;
|
|
256
|
+
const hold = (event) => {
|
|
257
|
+
if (mode === 'free') {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (mode === 'off') {
|
|
261
|
+
event.stopImmediatePropagation();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (event instanceof WheelEvent) {
|
|
265
|
+
if (!(event.ctrlKey || event.metaKey)) {
|
|
266
|
+
event.stopImmediatePropagation();
|
|
267
|
+
}
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (typeof TouchEvent !== 'undefined' && event instanceof TouchEvent) {
|
|
271
|
+
if (event.touches.length < 2) {
|
|
272
|
+
event.stopImmediatePropagation();
|
|
273
|
+
}
|
|
274
|
+
else if (event.type === 'touchstart' && event.cancelable) {
|
|
275
|
+
event.preventDefault();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
const held = ['wheel', 'touchstart', 'touchmove', 'mousedown'];
|
|
280
|
+
for (const type of held) {
|
|
281
|
+
container.addEventListener(type, hold, { capture: true, passive: false });
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
set: (next) => {
|
|
285
|
+
mode = next;
|
|
286
|
+
container.style.touchAction =
|
|
287
|
+
next === 'free' ? touchAction : next === 'cooperative' ? 'pan-x pan-y' : 'auto';
|
|
288
|
+
},
|
|
289
|
+
stop: () => {
|
|
290
|
+
for (const type of held) {
|
|
291
|
+
container.removeEventListener(type, hold, { capture: true });
|
|
292
|
+
}
|
|
293
|
+
container.style.touchAction = touchAction;
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { NodeProgram } from 'sigma/rendering';
|
|
2
|
+
import type { NodeDisplayData, RenderParams } from 'sigma/types';
|
|
3
|
+
/** The shapes a note is drawn in, numbered as the shader reads them. */
|
|
4
|
+
export declare const SHAPES: {
|
|
5
|
+
readonly circle: 0;
|
|
6
|
+
readonly diamond: 2;
|
|
7
|
+
readonly ring: 3;
|
|
8
|
+
readonly square: 1;
|
|
9
|
+
};
|
|
10
|
+
/** How heavily a note is drawn, numbered as the shader reads them. */
|
|
11
|
+
export declare const EMPHASES: {
|
|
12
|
+
readonly outlined: 1;
|
|
13
|
+
readonly solid: 0;
|
|
14
|
+
readonly subtle: 2;
|
|
15
|
+
};
|
|
16
|
+
/** What a note is made of, numbered as the shader reads them. */
|
|
17
|
+
export declare const MATERIALS: {
|
|
18
|
+
readonly flat: 0;
|
|
19
|
+
readonly frosted: 2;
|
|
20
|
+
readonly glass: 1;
|
|
21
|
+
};
|
|
22
|
+
/** What the node program reads off a note, beyond what Sigma gives every note. */
|
|
23
|
+
export interface HelixNodeData {
|
|
24
|
+
/** `EMPHASES`. */
|
|
25
|
+
emphasisCode?: number;
|
|
26
|
+
/** How strongly the note glows, from 0 to 1: the selected note's halo. */
|
|
27
|
+
glow?: number;
|
|
28
|
+
/** `MATERIALS`. */
|
|
29
|
+
materialCode?: number;
|
|
30
|
+
/** `SHAPES`. */
|
|
31
|
+
shapeCode?: number;
|
|
32
|
+
}
|
|
33
|
+
declare const UNIFORMS: readonly ['u_sizeRatio', 'u_correctionRatio', 'u_matrix'];
|
|
34
|
+
/**
|
|
35
|
+
* Draws a note in its shape, its emphasis and its material, with a glow where
|
|
36
|
+
* it is selected: one triangle a note, as Sigma's own circle is, cut to the
|
|
37
|
+
* shape where it is drawn. Pressing reads the shape and not the glow, so a
|
|
38
|
+
* press beside a note is not a press on it.
|
|
39
|
+
*/
|
|
40
|
+
export declare class HelixNodeProgram extends NodeProgram<(typeof UNIFORMS)[number]> {
|
|
41
|
+
getDefinition(): {
|
|
42
|
+
ATTRIBUTES: ({
|
|
43
|
+
name: string;
|
|
44
|
+
size: number;
|
|
45
|
+
type: 5126;
|
|
46
|
+
normalized?: never;
|
|
47
|
+
} | {
|
|
48
|
+
name: string;
|
|
49
|
+
normalized: boolean;
|
|
50
|
+
size: number;
|
|
51
|
+
type: 5121;
|
|
52
|
+
})[];
|
|
53
|
+
CONSTANT_ATTRIBUTES: {
|
|
54
|
+
name: string;
|
|
55
|
+
size: number;
|
|
56
|
+
type: 5126;
|
|
57
|
+
}[];
|
|
58
|
+
CONSTANT_DATA: number[][];
|
|
59
|
+
FRAGMENT_SHADER_SOURCE: string;
|
|
60
|
+
METHOD: 4;
|
|
61
|
+
UNIFORMS: readonly ["u_sizeRatio", "u_correctionRatio", "u_matrix"];
|
|
62
|
+
VERTEX_SHADER_SOURCE: string;
|
|
63
|
+
VERTICES: number;
|
|
64
|
+
};
|
|
65
|
+
processVisibleItem(nodeIndex: number, startIndex: number, data: NodeDisplayData & HelixNodeData): void;
|
|
66
|
+
setUniforms(params: RenderParams, { gl, uniformLocations, }: {
|
|
67
|
+
gl: WebGLRenderingContext;
|
|
68
|
+
uniformLocations: Record<string, WebGLUniformLocation>;
|
|
69
|
+
}): void;
|
|
70
|
+
}
|
|
71
|
+
export {};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { NodeProgram } from 'sigma/rendering';
|
|
2
|
+
import { floatColor } from 'sigma/utils';
|
|
3
|
+
export const SHAPES = { circle: 0, diamond: 2, ring: 3, square: 1 };
|
|
4
|
+
export const EMPHASES = { outlined: 1, solid: 0, subtle: 2 };
|
|
5
|
+
export const MATERIALS = { flat: 0, frosted: 2, glass: 1 };
|
|
6
|
+
const REACH = 2.6;
|
|
7
|
+
const VERTEX = `
|
|
8
|
+
attribute vec4 a_id;
|
|
9
|
+
attribute vec4 a_color;
|
|
10
|
+
attribute vec2 a_position;
|
|
11
|
+
attribute float a_size;
|
|
12
|
+
attribute float a_shape;
|
|
13
|
+
attribute float a_emphasis;
|
|
14
|
+
attribute float a_material;
|
|
15
|
+
attribute float a_glow;
|
|
16
|
+
attribute float a_angle;
|
|
17
|
+
|
|
18
|
+
uniform mat3 u_matrix;
|
|
19
|
+
uniform float u_sizeRatio;
|
|
20
|
+
uniform float u_correctionRatio;
|
|
21
|
+
|
|
22
|
+
varying vec4 v_color;
|
|
23
|
+
varying vec2 v_diffVector;
|
|
24
|
+
varying float v_radius;
|
|
25
|
+
varying float v_shape;
|
|
26
|
+
varying float v_emphasis;
|
|
27
|
+
varying float v_material;
|
|
28
|
+
varying float v_glow;
|
|
29
|
+
|
|
30
|
+
const float bias = 255.0 / 254.0;
|
|
31
|
+
|
|
32
|
+
void main() {
|
|
33
|
+
float size = a_size * u_correctionRatio / u_sizeRatio * 4.0;
|
|
34
|
+
vec2 diffVector = size * ${REACH.toFixed(2)} * vec2(cos(a_angle), sin(a_angle));
|
|
35
|
+
gl_Position = vec4((u_matrix * vec3(a_position + diffVector, 1)).xy, 0, 1);
|
|
36
|
+
v_diffVector = diffVector;
|
|
37
|
+
v_radius = size / 2.0;
|
|
38
|
+
v_shape = a_shape;
|
|
39
|
+
v_emphasis = a_emphasis;
|
|
40
|
+
v_material = a_material;
|
|
41
|
+
v_glow = a_glow;
|
|
42
|
+
#ifdef PICKING_MODE
|
|
43
|
+
v_color = a_id;
|
|
44
|
+
#else
|
|
45
|
+
v_color = a_color;
|
|
46
|
+
#endif
|
|
47
|
+
v_color.a *= bias;
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
50
|
+
const FRAGMENT = `
|
|
51
|
+
precision highp float;
|
|
52
|
+
|
|
53
|
+
varying vec4 v_color;
|
|
54
|
+
varying vec2 v_diffVector;
|
|
55
|
+
varying float v_radius;
|
|
56
|
+
varying float v_shape;
|
|
57
|
+
varying float v_emphasis;
|
|
58
|
+
varying float v_material;
|
|
59
|
+
varying float v_glow;
|
|
60
|
+
|
|
61
|
+
uniform float u_correctionRatio;
|
|
62
|
+
|
|
63
|
+
/* How far a point is outside the note's shape, in radii: below zero inside. */
|
|
64
|
+
float outside(vec2 p, float shape) {
|
|
65
|
+
if (shape > 0.5 && shape < 1.5) {
|
|
66
|
+
vec2 q = abs(p) - vec2(0.7);
|
|
67
|
+
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - 0.18;
|
|
68
|
+
}
|
|
69
|
+
if (shape > 1.5 && shape < 2.5) {
|
|
70
|
+
return (abs(p.x) + abs(p.y)) * 0.7071 - 0.86;
|
|
71
|
+
}
|
|
72
|
+
return length(p) - 1.0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
void main(void) {
|
|
76
|
+
vec2 p = v_diffVector / v_radius;
|
|
77
|
+
float d = outside(p, v_shape);
|
|
78
|
+
|
|
79
|
+
#ifdef PICKING_MODE
|
|
80
|
+
if (d > 0.0) discard;
|
|
81
|
+
gl_FragColor = v_color;
|
|
82
|
+
#else
|
|
83
|
+
float aa = u_correctionRatio * 2.0 / v_radius;
|
|
84
|
+
float inside = 1.0 - smoothstep(-aa, 0.0, d);
|
|
85
|
+
|
|
86
|
+
/* A ring is a circle drawn outlined, whatever its emphasis says. */
|
|
87
|
+
float emphasis = v_shape > 2.5 ? 1.0 : v_emphasis;
|
|
88
|
+
float edge = smoothstep(-0.32 - aa, -0.32, d);
|
|
89
|
+
float fill = emphasis > 1.5 ? mix(0.3, 1.0, edge) : emphasis > 0.5 ? edge : 1.0;
|
|
90
|
+
|
|
91
|
+
vec3 color = v_color.rgb;
|
|
92
|
+
float alpha = 1.0;
|
|
93
|
+
/* How large the note is drawn, in pixels: a highlight that suits a hub is
|
|
94
|
+
a speck of noise on a leaf, so the detail grows in with the size. */
|
|
95
|
+
float drawn = smoothstep(4.0, 14.0, v_radius / u_correctionRatio);
|
|
96
|
+
/* Lit from above on the left, as every surface here is. */
|
|
97
|
+
float lit = clamp(0.5 + 0.5 * dot(normalize(vec2(-0.45, 0.9)), p), 0.0, 1.0);
|
|
98
|
+
/* A fine rim a shade darker, so notes that touch stay two. */
|
|
99
|
+
float rim = smoothstep(-0.2 - aa, -0.2, d);
|
|
100
|
+
if (v_material > 0.5 && v_material < 1.5) {
|
|
101
|
+
/* Glass: shaded like a sphere, a highlight, and light at its rim. */
|
|
102
|
+
color *= mix(0.72, 1.12, lit);
|
|
103
|
+
float highlight = exp(-pow(length(p - vec2(-0.32, 0.38)) / 0.26, 2.0));
|
|
104
|
+
color = mix(color, vec3(1.0), highlight * mix(0.25, 0.6, drawn));
|
|
105
|
+
color = mix(color, vec3(1.0), rim * 0.22 * drawn);
|
|
106
|
+
alpha = 0.94;
|
|
107
|
+
} else if (v_material > 1.5) {
|
|
108
|
+
/* Frosted: a milky body, a soft fall of light, and no sharp highlight. */
|
|
109
|
+
color = mix(color * mix(0.86, 1.06, lit), vec3(1.0), 0.16);
|
|
110
|
+
color = mix(color, vec3(1.0), rim * 0.14);
|
|
111
|
+
alpha = 0.86;
|
|
112
|
+
} else {
|
|
113
|
+
color = mix(color, color * 0.72, rim * 0.6);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
float body = inside * fill * alpha * v_color.a;
|
|
117
|
+
/* The glow, outside the shape: a crisp ring just clear of the edge and a
|
|
118
|
+
soft light falling away beyond it, both in the note's own color. */
|
|
119
|
+
float reach = ${(REACH - 1).toFixed(2)};
|
|
120
|
+
float ring = 1.0 - smoothstep(0.0, 0.07 + aa, abs(d - 0.32));
|
|
121
|
+
float falloff = exp(-d * 2.4) * (1.0 - smoothstep(reach * 0.8, reach, d));
|
|
122
|
+
float halo = v_glow * step(0.0, d) * clamp(ring * 0.95 + falloff * 0.55, 0.0, 1.0) * v_color.a;
|
|
123
|
+
/* Premultiplied, as Sigma blends: the body over its glow. */
|
|
124
|
+
gl_FragColor = vec4(color * body + v_color.rgb * halo * (1.0 - body), body + halo * (1.0 - body));
|
|
125
|
+
#endif
|
|
126
|
+
}
|
|
127
|
+
`;
|
|
128
|
+
const { FLOAT, UNSIGNED_BYTE } = WebGLRenderingContext;
|
|
129
|
+
const UNIFORMS = ['u_sizeRatio', 'u_correctionRatio', 'u_matrix'];
|
|
130
|
+
export class HelixNodeProgram extends NodeProgram {
|
|
131
|
+
getDefinition() {
|
|
132
|
+
return {
|
|
133
|
+
ATTRIBUTES: [
|
|
134
|
+
{ name: 'a_position', size: 2, type: FLOAT },
|
|
135
|
+
{ name: 'a_size', size: 1, type: FLOAT },
|
|
136
|
+
{ name: 'a_color', normalized: true, size: 4, type: UNSIGNED_BYTE },
|
|
137
|
+
{ name: 'a_id', normalized: true, size: 4, type: UNSIGNED_BYTE },
|
|
138
|
+
{ name: 'a_shape', size: 1, type: FLOAT },
|
|
139
|
+
{ name: 'a_emphasis', size: 1, type: FLOAT },
|
|
140
|
+
{ name: 'a_material', size: 1, type: FLOAT },
|
|
141
|
+
{ name: 'a_glow', size: 1, type: FLOAT },
|
|
142
|
+
],
|
|
143
|
+
CONSTANT_ATTRIBUTES: [{ name: 'a_angle', size: 1, type: FLOAT }],
|
|
144
|
+
CONSTANT_DATA: [[0], [(2 * Math.PI) / 3], [(4 * Math.PI) / 3]],
|
|
145
|
+
FRAGMENT_SHADER_SOURCE: FRAGMENT,
|
|
146
|
+
METHOD: WebGLRenderingContext.TRIANGLES,
|
|
147
|
+
UNIFORMS,
|
|
148
|
+
VERTEX_SHADER_SOURCE: VERTEX,
|
|
149
|
+
VERTICES: 3,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
processVisibleItem(nodeIndex, startIndex, data) {
|
|
153
|
+
const array = this.array;
|
|
154
|
+
let at = startIndex;
|
|
155
|
+
array[at++] = data.x;
|
|
156
|
+
array[at++] = data.y;
|
|
157
|
+
array[at++] = data.size;
|
|
158
|
+
array[at++] = floatColor(data.color);
|
|
159
|
+
array[at++] = nodeIndex;
|
|
160
|
+
array[at++] = data.shapeCode ?? SHAPES.circle;
|
|
161
|
+
array[at++] = data.emphasisCode ?? EMPHASES.solid;
|
|
162
|
+
array[at++] = data.materialCode ?? MATERIALS.flat;
|
|
163
|
+
array[at] = data.glow ?? 0;
|
|
164
|
+
}
|
|
165
|
+
setUniforms(params, { gl, uniformLocations, }) {
|
|
166
|
+
gl.uniform1f(uniformLocations.u_correctionRatio, params.correctionRatio);
|
|
167
|
+
gl.uniform1f(uniformLocations.u_sizeRatio, params.sizeRatio);
|
|
168
|
+
gl.uniformMatrix3fv(uniformLocations.u_matrix, false, params.matrix);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { GraphDriver } from '../connect-graph/connect-graph.js';
|
|
2
|
+
import type { NodeLook } from '../connect-graph/looks.js';
|
|
2
3
|
/** A graphology graph, as far as the driver calls it. */
|
|
3
4
|
export interface SigmaGraph {
|
|
4
5
|
addEdge(source: string, target: string, attributes?: Record<string, unknown>): unknown;
|
|
@@ -57,16 +58,19 @@ export interface SigmaRenderer {
|
|
|
57
58
|
x: number;
|
|
58
59
|
y: number;
|
|
59
60
|
} | undefined;
|
|
60
|
-
off(type: 'clickNode', listener: (payload: {
|
|
61
|
+
off(type: 'clickNode' | 'enterNode' | 'leaveNode', listener: (payload: {
|
|
61
62
|
node: string;
|
|
62
63
|
}) => void): unknown;
|
|
63
|
-
|
|
64
|
+
off(type: 'afterRender' | 'beforeRender', listener: () => void): unknown;
|
|
65
|
+
on(type: 'clickNode' | 'enterNode' | 'leaveNode', listener: (payload: {
|
|
64
66
|
node: string;
|
|
65
67
|
}) => void): unknown;
|
|
68
|
+
on(type: 'afterRender' | 'beforeRender', listener: () => void): unknown;
|
|
69
|
+
getSetting(name: 'nodeProgramClasses'): Record<string, unknown>;
|
|
66
70
|
refresh(): unknown;
|
|
67
71
|
resize(force?: boolean): unknown;
|
|
68
72
|
scaleSize(size?: number): number;
|
|
69
|
-
setSetting(name: 'defaultDrawNodeHover' | 'edgeReducer' | 'labelColor' | 'labelFont' | 'labelRenderedSizeThreshold' | 'nodeReducer', value: unknown): unknown;
|
|
73
|
+
setSetting(name: 'defaultDrawNodeHover' | 'edgeReducer' | 'labelColor' | 'labelFont' | 'defaultDrawNodeLabel' | 'labelGridCellSize' | 'labelRenderedSizeThreshold' | 'labelSize' | 'minEdgeThickness' | 'nodeProgramClasses' | 'nodeReducer', value: unknown): unknown;
|
|
70
74
|
viewportToFramedGraph(point: {
|
|
71
75
|
x: number;
|
|
72
76
|
y: number;
|
|
@@ -100,6 +104,30 @@ export interface SigmaDriverOptions {
|
|
|
100
104
|
* Without one, notes stay where the connector started them.
|
|
101
105
|
*/
|
|
102
106
|
layout?: ((graph: SigmaGraph) => SigmaLayout) | undefined;
|
|
107
|
+
/**
|
|
108
|
+
* How heavily a note is drawn where it does not say: `solid`, filled, by
|
|
109
|
+
* default; `outlined`, a ring; `subtle`, a tinted fill with a ring.
|
|
110
|
+
*/
|
|
111
|
+
emphasis?: NodeLook['emphasis'];
|
|
112
|
+
/**
|
|
113
|
+
* What a note is drawn as made of where it does not say: flat by default,
|
|
114
|
+
* `glass`, a shaded sphere with a highlight and a lit rim, or `frosted`, a
|
|
115
|
+
* milky body with a soft fall of light.
|
|
116
|
+
*/
|
|
117
|
+
material?: NodeLook['material'];
|
|
118
|
+
/** A note's shape where it does not say: `circle` by default, `square`, `diamond` or `ring`. */
|
|
119
|
+
shape?: NodeLook['shape'];
|
|
120
|
+
/**
|
|
121
|
+
* Whether each group's name is drawn over its notes. On by default, which
|
|
122
|
+
* is how a reader finds their way while notes are named only on hover.
|
|
123
|
+
*/
|
|
124
|
+
groupLabels?: boolean | undefined;
|
|
125
|
+
/**
|
|
126
|
+
* Which notes are named. `hover`, by default: the note under the pointer,
|
|
127
|
+
* the selected note and its neighbors. `hubs`: the busiest notes as well, at
|
|
128
|
+
* rest. `all`: every note, where its name does not cover another.
|
|
129
|
+
*/
|
|
130
|
+
labels?: 'all' | 'hover' | 'hubs' | undefined;
|
|
103
131
|
/** The longest a layout runs, in milliseconds, settled or not. 10 seconds by default, about where 10,000 notes settle. */
|
|
104
132
|
layoutLimit?: number | undefined;
|
|
105
133
|
}
|