@fusion.dev/helix-graph 0.125.0 → 0.127.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 +84 -35
- package/dist/connect-graph/arrange.d.ts +4 -2
- package/dist/connect-graph/arrange.js +5 -22
- package/dist/connect-graph/connect-graph.d.ts +18 -6
- package/dist/connect-graph/connect-graph.js +76 -32
- package/dist/connect-graph/diff.d.ts +7 -0
- package/dist/connect-graph/looks.d.ts +8 -0
- package/dist/connect-graph/looks.js +6 -3
- package/dist/connect-graph/seed.d.ts +9 -0
- package/dist/connect-graph/seed.js +21 -15
- package/dist/connect-graph/stencil.d.ts +11 -1
- package/dist/connect-graph/stencil.js +109 -11
- package/dist/cosmos/cosmos-driver.d.ts +5 -4
- package/dist/cosmos/cosmos-driver.js +122 -39
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -2
- package/dist/model/index.d.ts +1 -0
- package/dist/model/index.js +1 -0
- package/dist/model/model.d.ts +16 -0
- package/dist/model/model.js +50 -0
- package/dist/overlay/overlay.d.ts +14 -8
- package/dist/overlay/overlay.js +64 -62
- package/dist/shapes/flat.d.ts +3 -2
- package/dist/shapes/flat.js +59 -6
- package/dist/shapes/index.d.ts +3 -4
- package/dist/shapes/index.js +3 -4
- package/dist/{connect-graph → shapes}/shape-field.d.ts +12 -3
- package/dist/{connect-graph → shapes}/shape-field.js +4 -2
- package/dist/shapes/solids.d.ts +3 -3
- package/dist/shapes/solids.js +25 -9
- package/dist/{three → shapes}/surface.d.ts +1 -1
- package/dist/shapes/triangles.d.ts +12 -3
- package/dist/shapes/triangles.js +83 -2
- package/dist/sigma/sigma-driver.d.ts +33 -17
- package/dist/sigma/sigma-driver.js +69 -17
- package/dist/three/three-driver.d.ts +12 -3
- package/dist/three/three-driver.js +376 -201
- package/package.json +9 -5
- package/dist/shapes/model.d.ts +0 -12
- package/dist/shapes/model.js +0 -31
- /package/dist/{three → shapes}/surface.js +0 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { shapeFromTriangles } from '../shapes/triangles.js';
|
|
2
|
+
export const shapeFromModel = async (source, options = {}) => {
|
|
3
|
+
const which = typeof source === 'string' ? `The model ${source}` : 'The model given as bytes';
|
|
4
|
+
const [{ GLTFLoader }, { Mesh, Vector3 }] = await Promise.all([
|
|
5
|
+
import('three/addons/loaders/GLTFLoader.js'),
|
|
6
|
+
import('three'),
|
|
7
|
+
]).catch((cause) => {
|
|
8
|
+
throw new Error(`${which} could not be read, because three.js could not be loaded. Install three alongside @fusion.dev/helix-graph.`, { cause });
|
|
9
|
+
});
|
|
10
|
+
const loader = new GLTFLoader();
|
|
11
|
+
const model = await (typeof source === 'string'
|
|
12
|
+
? loader.loadAsync(source)
|
|
13
|
+
: loader.parseAsync(source, '')).catch((cause) => {
|
|
14
|
+
throw new Error(`${which} could not be read as a glTF model: ${String(cause)}`, { cause });
|
|
15
|
+
});
|
|
16
|
+
model.scene.updateMatrixWorld(true);
|
|
17
|
+
const corners = [];
|
|
18
|
+
const names = [];
|
|
19
|
+
const ofTriangle = [];
|
|
20
|
+
const point = new Vector3();
|
|
21
|
+
model.scene.traverse((object) => {
|
|
22
|
+
if (!(object instanceof Mesh)) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const geometry = object.geometry;
|
|
26
|
+
const place = geometry.getAttribute('position');
|
|
27
|
+
if (!place) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const name = object.name || geometry.name || '';
|
|
31
|
+
let part = names.indexOf(name);
|
|
32
|
+
if (name && part < 0) {
|
|
33
|
+
part = names.push(name) - 1;
|
|
34
|
+
}
|
|
35
|
+
const index = geometry.getIndex();
|
|
36
|
+
const count = index ? index.count : place.count;
|
|
37
|
+
for (let at = 0; at < count; at++) {
|
|
38
|
+
point.fromBufferAttribute(place, index ? index.getX(at) : at).applyMatrix4(object.matrixWorld);
|
|
39
|
+
corners.push(point.x, point.y, point.z);
|
|
40
|
+
}
|
|
41
|
+
for (let triangle = 0; triangle < Math.floor(count / 3); triangle++) {
|
|
42
|
+
ofTriangle.push(part);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
const named = names.length > 1 && !ofTriangle.includes(-1);
|
|
46
|
+
return shapeFromTriangles(corners, null, {
|
|
47
|
+
...options,
|
|
48
|
+
...(named ? { regions: { names, ofTriangle } } : {}),
|
|
49
|
+
});
|
|
50
|
+
};
|
|
@@ -7,11 +7,6 @@ export type OnScreen = (id: string) => {
|
|
|
7
7
|
x: number;
|
|
8
8
|
y: number;
|
|
9
9
|
} | undefined;
|
|
10
|
-
/** A point and how to bend toward it: the control point of a curve on screen. */
|
|
11
|
-
export type Bend = (from: string, to: string) => {
|
|
12
|
-
x: number;
|
|
13
|
-
y: number;
|
|
14
|
-
} | undefined;
|
|
15
10
|
/** Whether an `rgb()` color is light, which a light ink says the ground is dark. */
|
|
16
11
|
export declare const isLight: (color: string) => boolean;
|
|
17
12
|
/** Eases in and out, so a signal leaves and arrives gently. */
|
|
@@ -23,13 +18,19 @@ export interface Box {
|
|
|
23
18
|
right: number;
|
|
24
19
|
top: number;
|
|
25
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* How many pixels a layer has for each of the page's: the screen's own, and
|
|
23
|
+
* never fewer than two, so on a screen of one a name's edges and a signal's
|
|
24
|
+
* tail are drawn twice as finely and smoothed down. At most three.
|
|
25
|
+
*/
|
|
26
|
+
export declare const overlayDensity: () => number;
|
|
26
27
|
/**
|
|
27
28
|
* **Signals are drawn on a canvas of the driver's own**, over the renderer's,
|
|
28
29
|
* so a signal never makes the renderer redraw the graph: ten signals a second
|
|
29
30
|
* over ten thousand notes draw ten dots. It draws only while something is in
|
|
30
31
|
* flight, and takes no pointer, so a press reaches the note under it.
|
|
31
32
|
*/
|
|
32
|
-
export declare const signalsOver: (container: HTMLElement, onScreen: OnScreen
|
|
33
|
+
export declare const signalsOver: (container: HTMLElement, onScreen: OnScreen) => {
|
|
33
34
|
look: (looks: GraphLooks) => void;
|
|
34
35
|
signal: (from: string, to: string, colors: {
|
|
35
36
|
from: string;
|
|
@@ -50,14 +51,18 @@ export interface Named {
|
|
|
50
51
|
* order given.
|
|
51
52
|
*/
|
|
52
53
|
export declare const namesOver: (container: HTMLElement, onScreen: OnScreen) => {
|
|
54
|
+
/** The boxes the names last drawn take. */
|
|
55
|
+
boxes: () => readonly Box[];
|
|
53
56
|
draw: (looks: GraphLooks | undefined, named: readonly Named[]) => void;
|
|
54
57
|
stop: () => void;
|
|
55
58
|
};
|
|
56
59
|
/**
|
|
57
60
|
* **Group names are drawn on a canvas of their own**, under the signals', and
|
|
58
61
|
* drawn again whenever the driver says the view moved, so they follow it.
|
|
62
|
+
* `avoid` says where notes' names are drawn, which a group's name keeps
|
|
63
|
+
* clear of: moved a line, or left out where no line is clear.
|
|
59
64
|
*/
|
|
60
|
-
export declare const groupsOver: (container: HTMLElement, onScreen: OnScreen) => {
|
|
65
|
+
export declare const groupsOver: (container: HTMLElement, onScreen: OnScreen, avoid?: () => readonly Box[]) => {
|
|
61
66
|
draw: () => void;
|
|
62
67
|
look: (next: GraphLooks) => void;
|
|
63
68
|
set: (next: readonly GraphGroup[]) => void;
|
|
@@ -69,7 +74,8 @@ export declare const groupsOver: (container: HTMLElement, onScreen: OnScreen) =>
|
|
|
69
74
|
* frame's buttons stopped working with them. A listener on the container in
|
|
70
75
|
* the capture phase runs before the renderer's, and keeps from it what the
|
|
71
76
|
* page has: the plain wheel and one finger while cooperative, and every
|
|
72
|
-
* gesture while a waiting surface is off
|
|
77
|
+
* gesture while a waiting surface is off, a double click among them, which
|
|
78
|
+
* zooms where a renderer zooms on one. A single click still selects a note.
|
|
73
79
|
*/
|
|
74
80
|
export declare const gesturesHeld: (container: HTMLElement) => {
|
|
75
81
|
/**
|
package/dist/overlay/overlay.js
CHANGED
|
@@ -1,55 +1,28 @@
|
|
|
1
|
-
import { mixRgb } from '../connect-graph/looks.js';
|
|
1
|
+
import { mixRgb, rgbOf } from '../connect-graph/looks.js';
|
|
2
2
|
const SPEED = 340;
|
|
3
3
|
const QUICKEST = 450;
|
|
4
4
|
const SLOWEST = 1400;
|
|
5
5
|
const ARRIVAL = 520;
|
|
6
6
|
const FLYING = 400;
|
|
7
7
|
const withAlpha = (color, alpha) => {
|
|
8
|
-
const [red
|
|
8
|
+
const [red, green, blue] = rgbOf(color);
|
|
9
9
|
return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
|
|
10
10
|
};
|
|
11
11
|
const TAIL = 120;
|
|
12
12
|
const PIECES = 14;
|
|
13
|
-
const BOW = 0.16;
|
|
14
|
-
const BOW_MOST = 90;
|
|
15
13
|
const SPARK = 0.22;
|
|
16
|
-
const on = (a,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
};
|
|
22
|
-
};
|
|
23
|
-
const bowed = (from, to) => {
|
|
24
|
-
const dx = to.x - from.x;
|
|
25
|
-
const dy = to.y - from.y;
|
|
26
|
-
const length = Math.hypot(dx, dy) || 1;
|
|
27
|
-
const bow = Math.min(length * BOW, BOW_MOST) * 2;
|
|
28
|
-
return {
|
|
29
|
-
x: (from.x + to.x) / 2 - (dy / length) * bow,
|
|
30
|
-
y: (from.y + to.y) / 2 + (dx / length) * bow,
|
|
31
|
-
};
|
|
32
|
-
};
|
|
33
|
-
const bounded = (from, to, toward) => {
|
|
34
|
-
const middle = { x: (from.x + to.x) / 2, y: (from.y + to.y) / 2 };
|
|
35
|
-
const far = Math.hypot(toward.x - middle.x, toward.y - middle.y);
|
|
36
|
-
const most = Math.max(Math.hypot(to.x - from.x, to.y - from.y) * 0.9, 12);
|
|
37
|
-
if (far <= most) {
|
|
38
|
-
return toward;
|
|
39
|
-
}
|
|
40
|
-
return {
|
|
41
|
-
x: middle.x + ((toward.x - middle.x) * most) / far,
|
|
42
|
-
y: middle.y + ((toward.y - middle.y) * most) / far,
|
|
43
|
-
};
|
|
44
|
-
};
|
|
45
|
-
const trace = (context, a, c, b) => {
|
|
14
|
+
const on = (a, b, t) => ({
|
|
15
|
+
x: a.x + (b.x - a.x) * t,
|
|
16
|
+
y: a.y + (b.y - a.y) * t,
|
|
17
|
+
});
|
|
18
|
+
const trace = (context, a, b) => {
|
|
46
19
|
context.beginPath();
|
|
47
20
|
context.moveTo(a.x, a.y);
|
|
48
|
-
context.
|
|
21
|
+
context.lineTo(b.x, b.y);
|
|
49
22
|
};
|
|
50
|
-
const comet = (context, leaves, arrives, from,
|
|
23
|
+
const comet = (context, leaves, arrives, from, to, along, through, glowing) => {
|
|
51
24
|
const strength = glowing ? 1.25 : 1;
|
|
52
|
-
const length = Math.hypot(
|
|
25
|
+
const length = Math.hypot(to.x - from.x, to.y - from.y) || 1;
|
|
53
26
|
const color = mixRgb(leaves, arrives, along);
|
|
54
27
|
context.lineCap = 'round';
|
|
55
28
|
const line = context.createLinearGradient(from.x, from.y, to.x, to.y);
|
|
@@ -57,15 +30,15 @@ const comet = (context, leaves, arrives, from, bend, to, along, through, glowing
|
|
|
57
30
|
line.addColorStop(1, withAlpha(arrives, 0.16 * Math.sin(Math.PI * through)));
|
|
58
31
|
context.strokeStyle = line;
|
|
59
32
|
context.lineWidth = 1;
|
|
60
|
-
trace(context, from,
|
|
33
|
+
trace(context, from, to);
|
|
61
34
|
context.stroke();
|
|
62
35
|
const reach = Math.min(TAIL / length, along);
|
|
63
36
|
context.lineCap = 'butt';
|
|
64
|
-
let behind = on(from,
|
|
37
|
+
let behind = on(from, to, along - reach);
|
|
65
38
|
for (let piece = 1; piece <= PIECES; piece++) {
|
|
66
39
|
const share = piece / PIECES;
|
|
67
40
|
const t = along - reach * (1 - share);
|
|
68
|
-
const ahead = on(from,
|
|
41
|
+
const ahead = on(from, to, t);
|
|
69
42
|
context.strokeStyle = withAlpha(mixRgb(leaves, arrives, t), (glowing ? 0.85 : 1) * share * share);
|
|
70
43
|
context.lineWidth = (0.4 + 2.2 * share) * strength;
|
|
71
44
|
context.beginPath();
|
|
@@ -74,7 +47,7 @@ const comet = (context, leaves, arrives, from, bend, to, along, through, glowing
|
|
|
74
47
|
context.stroke();
|
|
75
48
|
behind = ahead;
|
|
76
49
|
}
|
|
77
|
-
const head = on(from,
|
|
50
|
+
const head = on(from, to, along);
|
|
78
51
|
const bloom = 9 * strength;
|
|
79
52
|
const light = context.createRadialGradient(head.x, head.y, 0, head.x, head.y, bloom);
|
|
80
53
|
light.addColorStop(0, glowing ? 'rgba(255, 255, 255, 0.95)' : withAlpha(color, 1));
|
|
@@ -109,24 +82,28 @@ const arrive = (context, color, at, gone) => {
|
|
|
109
82
|
context.stroke();
|
|
110
83
|
};
|
|
111
84
|
export const isLight = (color) => {
|
|
112
|
-
const [red
|
|
85
|
+
const [red, green, blue] = rgbOf(color, 0);
|
|
113
86
|
return (0.2126 * red + 0.7152 * green + 0.0722 * blue) / 255 > 0.5;
|
|
114
87
|
};
|
|
115
88
|
export const ease = (at) => (at < 0.5 ? 2 * at * at : 1 - (-2 * at + 2) ** 2 / 2);
|
|
116
|
-
const
|
|
89
|
+
const SAMPLED = 96;
|
|
90
|
+
const nameGroups = (context, groups, looks, at, avoid) => {
|
|
117
91
|
context.font = `650 11px ${looks.font}`;
|
|
118
92
|
context.letterSpacing = '0.14em';
|
|
119
93
|
context.textAlign = 'center';
|
|
120
94
|
context.textBaseline = 'middle';
|
|
121
95
|
context.shadowColor = looks.ground;
|
|
122
|
-
const taken = [];
|
|
96
|
+
const taken = [...avoid];
|
|
123
97
|
for (const group of groups) {
|
|
124
98
|
let x = 0;
|
|
125
99
|
let y = 0;
|
|
126
100
|
let count = 0;
|
|
127
101
|
const points = [];
|
|
128
|
-
|
|
129
|
-
|
|
102
|
+
const step = Math.max(1, Math.floor(group.ids.length / SAMPLED));
|
|
103
|
+
let asked = 0;
|
|
104
|
+
for (let index = 0; index < group.ids.length; index += step) {
|
|
105
|
+
asked += 1;
|
|
106
|
+
const point = at(group.ids[index]);
|
|
130
107
|
if (point) {
|
|
131
108
|
x += point.x;
|
|
132
109
|
y += point.y;
|
|
@@ -134,7 +111,7 @@ const nameGroups = (context, groups, looks, at) => {
|
|
|
134
111
|
points.push(point);
|
|
135
112
|
}
|
|
136
113
|
}
|
|
137
|
-
if (count < Math.max(2,
|
|
114
|
+
if (count < Math.max(2, asked * 0.3)) {
|
|
138
115
|
continue;
|
|
139
116
|
}
|
|
140
117
|
x /= count;
|
|
@@ -186,12 +163,13 @@ const layer = (container, kind) => {
|
|
|
186
163
|
}
|
|
187
164
|
return canvas;
|
|
188
165
|
};
|
|
166
|
+
export const overlayDensity = () => Math.min(Math.max(window.devicePixelRatio || 1, 2), 3);
|
|
189
167
|
const fresh = (canvas, container) => {
|
|
190
168
|
const context = canvas.getContext('2d');
|
|
191
169
|
if (!context) {
|
|
192
170
|
return null;
|
|
193
171
|
}
|
|
194
|
-
const ratio =
|
|
172
|
+
const ratio = overlayDensity();
|
|
195
173
|
const width = Math.round(container.clientWidth * ratio);
|
|
196
174
|
const height = Math.round(container.clientHeight * ratio);
|
|
197
175
|
if (canvas.width !== width || canvas.height !== height) {
|
|
@@ -202,14 +180,14 @@ const fresh = (canvas, container) => {
|
|
|
202
180
|
context.clearRect(0, 0, width, height);
|
|
203
181
|
return context;
|
|
204
182
|
};
|
|
205
|
-
export const signalsOver = (container, onScreen
|
|
183
|
+
export const signalsOver = (container, onScreen) => {
|
|
206
184
|
let overlay;
|
|
207
185
|
let flights = [];
|
|
208
186
|
let flying = 0;
|
|
209
187
|
let ink = 'rgb(128, 128, 128)';
|
|
210
188
|
let glowing = false;
|
|
211
189
|
const fly = (now) => {
|
|
212
|
-
const context = overlay ? fresh(overlay, container) : null;
|
|
190
|
+
const context = overlay && container.isConnected ? fresh(overlay, container) : null;
|
|
213
191
|
if (!context) {
|
|
214
192
|
flights = [];
|
|
215
193
|
return;
|
|
@@ -221,17 +199,16 @@ export const signalsOver = (container, onScreen, bend) => {
|
|
|
221
199
|
if (!from || !to) {
|
|
222
200
|
return false;
|
|
223
201
|
}
|
|
224
|
-
flight.crossing ??= Math.min(Math.max((
|
|
202
|
+
flight.crossing ??= Math.min(Math.max((Math.hypot(to.x - from.x, to.y - from.y) / SPEED) * 1000, QUICKEST), SLOWEST);
|
|
225
203
|
const crossing = flight.crossing;
|
|
226
204
|
const gone = (now - flight.started) / crossing;
|
|
227
205
|
if (gone >= 1 + ARRIVAL / crossing) {
|
|
228
206
|
return false;
|
|
229
207
|
}
|
|
230
|
-
const toward = bounded(from, to, bend?.(flight.from, flight.to) ?? bowed(from, to));
|
|
231
208
|
context.save();
|
|
232
209
|
if (gone < 1) {
|
|
233
210
|
if (flight.animate) {
|
|
234
|
-
comet(context, flight.leaves, flight.arrives, from,
|
|
211
|
+
comet(context, flight.leaves, flight.arrives, from, to, ease(gone), gone, glowing);
|
|
235
212
|
if (gone < SPARK) {
|
|
236
213
|
spark(context, flight.leaves, from, gone / SPARK);
|
|
237
214
|
}
|
|
@@ -243,7 +220,7 @@ export const signalsOver = (container, onScreen, bend) => {
|
|
|
243
220
|
context.globalAlpha = 1 - gone;
|
|
244
221
|
context.strokeStyle = line;
|
|
245
222
|
context.lineWidth = 2;
|
|
246
|
-
trace(context, from,
|
|
223
|
+
trace(context, from, to);
|
|
247
224
|
context.stroke();
|
|
248
225
|
}
|
|
249
226
|
}
|
|
@@ -261,6 +238,9 @@ export const signalsOver = (container, onScreen, bend) => {
|
|
|
261
238
|
glowing = isLight(looks.ink);
|
|
262
239
|
},
|
|
263
240
|
signal: (from, to, colors, animate) => {
|
|
241
|
+
if (!container.isConnected) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
264
244
|
overlay ??= layer(container, 'signals');
|
|
265
245
|
flights.push({
|
|
266
246
|
animate,
|
|
@@ -288,13 +268,15 @@ export const signalsOver = (container, onScreen, bend) => {
|
|
|
288
268
|
};
|
|
289
269
|
export const namesOver = (container, onScreen) => {
|
|
290
270
|
const canvas = layer(container, 'names');
|
|
271
|
+
let taken = [];
|
|
291
272
|
return {
|
|
273
|
+
boxes: () => taken,
|
|
292
274
|
draw: (looks, named) => {
|
|
293
275
|
const context = looks ? fresh(canvas, container) : null;
|
|
294
276
|
if (!context || !looks) {
|
|
295
277
|
return;
|
|
296
278
|
}
|
|
297
|
-
|
|
279
|
+
taken = [];
|
|
298
280
|
for (const { id, name, strong } of named) {
|
|
299
281
|
const at = onScreen(id);
|
|
300
282
|
if (!at || looks.node(id).hidden) {
|
|
@@ -330,14 +312,14 @@ export const namesOver = (container, onScreen) => {
|
|
|
330
312
|
},
|
|
331
313
|
};
|
|
332
314
|
};
|
|
333
|
-
export const groupsOver = (container, onScreen) => {
|
|
315
|
+
export const groupsOver = (container, onScreen, avoid = () => []) => {
|
|
334
316
|
let canvas;
|
|
335
317
|
let groups = [];
|
|
336
318
|
let looks;
|
|
337
319
|
const draw = () => {
|
|
338
320
|
const context = canvas && looks ? fresh(canvas, container) : null;
|
|
339
321
|
if (context && looks) {
|
|
340
|
-
nameGroups(context, groups, looks, onScreen);
|
|
322
|
+
nameGroups(context, groups, looks, onScreen, avoid());
|
|
341
323
|
}
|
|
342
324
|
};
|
|
343
325
|
return {
|
|
@@ -357,14 +339,29 @@ export const groupsOver = (container, onScreen) => {
|
|
|
357
339
|
},
|
|
358
340
|
};
|
|
359
341
|
};
|
|
342
|
+
const GESTURES = 'data-helix-graph-gestures';
|
|
343
|
+
const touchRule = (container) => {
|
|
344
|
+
const root = container.getRootNode();
|
|
345
|
+
const holder = root instanceof ShadowRoot ? root : document.head;
|
|
346
|
+
if (!holder || holder.querySelector(`style[${GESTURES}]`)) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const style = document.createElement('style');
|
|
350
|
+
style.setAttribute(GESTURES, '');
|
|
351
|
+
style.textContent = `[${GESTURES}="cooperative"], [${GESTURES}="cooperative"] * { touch-action: pan-x pan-y !important; }
|
|
352
|
+
[${GESTURES}="off"], [${GESTURES}="off"] * { touch-action: auto !important; }`;
|
|
353
|
+
holder.append(style);
|
|
354
|
+
};
|
|
360
355
|
export const gesturesHeld = (container) => {
|
|
361
356
|
let mode = 'free';
|
|
362
|
-
const touchAction = container.style.touchAction;
|
|
363
357
|
const hold = (event) => {
|
|
364
358
|
if (mode === 'free') {
|
|
365
359
|
return;
|
|
366
360
|
}
|
|
367
361
|
if (mode === 'off') {
|
|
362
|
+
if (event.type === 'click' && event.detail < 2) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
368
365
|
event.stopImmediatePropagation();
|
|
369
366
|
return;
|
|
370
367
|
}
|
|
@@ -383,21 +380,26 @@ export const gesturesHeld = (container) => {
|
|
|
383
380
|
}
|
|
384
381
|
}
|
|
385
382
|
};
|
|
386
|
-
const held = ['wheel', 'touchstart', 'touchmove', 'mousedown'];
|
|
383
|
+
const held = ['wheel', 'touchstart', 'touchmove', 'mousedown', 'click', 'dblclick'];
|
|
387
384
|
for (const type of held) {
|
|
388
385
|
container.addEventListener(type, hold, { capture: true, passive: false });
|
|
389
386
|
}
|
|
390
387
|
return {
|
|
391
388
|
set: (next) => {
|
|
392
389
|
mode = next;
|
|
393
|
-
|
|
394
|
-
|
|
390
|
+
if (next === 'free') {
|
|
391
|
+
container.removeAttribute(GESTURES);
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
touchRule(container);
|
|
395
|
+
container.setAttribute(GESTURES, next);
|
|
396
|
+
}
|
|
395
397
|
},
|
|
396
398
|
stop: () => {
|
|
397
399
|
for (const type of held) {
|
|
398
400
|
container.removeEventListener(type, hold, { capture: true });
|
|
399
401
|
}
|
|
400
|
-
container.
|
|
402
|
+
container.removeAttribute(GESTURES);
|
|
401
403
|
},
|
|
402
404
|
};
|
|
403
405
|
};
|
package/dist/shapes/flat.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { type ShapeField } from '
|
|
1
|
+
import { type ShapeField } from './shape-field.js';
|
|
2
2
|
/**
|
|
3
3
|
* A flat field from an SVG: an `<svg>` element, or its markup. Its box is
|
|
4
4
|
* its `viewBox`, and its shape every path, rectangle, circle, ellipse and
|
|
5
|
-
* polygon in it, together, their holes cut by the even-odd rule.
|
|
5
|
+
* polygon in it, together, their holes cut by the even-odd rule. Each shape
|
|
6
|
+
* with an `id`, or in a group with one, is a named part.
|
|
6
7
|
*
|
|
7
8
|
* Transforms on the shapes are not read: draw the outline in the `viewBox`'s
|
|
8
9
|
* own units, as an exported icon or logo is.
|
package/dist/shapes/flat.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FLAT, fieldFromCells, fieldFromPath
|
|
1
|
+
import { FLAT, fieldFromCells, fieldFromPath } from './shape-field.js';
|
|
2
2
|
const pathOf = (element) => {
|
|
3
3
|
const number = (name) => Number(element.getAttribute(name) ?? 0);
|
|
4
4
|
switch (element.localName) {
|
|
@@ -27,18 +27,71 @@ export const shapeFromSvg = (svg, resolution = FLAT) => {
|
|
|
27
27
|
const element = typeof svg === 'string'
|
|
28
28
|
? new DOMParser().parseFromString(svg, 'image/svg+xml').documentElement
|
|
29
29
|
: svg;
|
|
30
|
-
const [, , width = 0, height = 0] = (element.getAttribute('viewBox') ?? '')
|
|
30
|
+
const [left = 0, top = 0, width = 0, height = 0] = (element.getAttribute('viewBox') ?? '')
|
|
31
31
|
.trim()
|
|
32
32
|
.split(/[\s,]+/)
|
|
33
33
|
.map(Number);
|
|
34
|
+
const origin = { x: Number.isFinite(left) ? left : 0, y: Number.isFinite(top) ? top : 0 };
|
|
34
35
|
const box = {
|
|
35
36
|
height: height || Number(element.getAttribute('height') ?? 0),
|
|
36
37
|
width: width || Number(element.getAttribute('width') ?? 0),
|
|
37
38
|
};
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
const shapes = [...element.querySelectorAll('path, rect, circle, ellipse, polygon')];
|
|
40
|
+
const path = shapes.map(pathOf).join(' ');
|
|
41
|
+
const field = fieldFromPath(path, box.width || 1, box.height || 1, resolution, origin);
|
|
42
|
+
const named = shapes.flatMap((shape) => {
|
|
43
|
+
const name = shape.closest('[id]')?.getAttribute('id');
|
|
44
|
+
return name && shape.closest('[id]') !== element ? [{ name, path: pathOf(shape) }] : [];
|
|
45
|
+
});
|
|
46
|
+
return named.length > 0
|
|
47
|
+
? { ...field, regionOf: partsOf(named, field, resolution, origin) }
|
|
48
|
+
: field;
|
|
49
|
+
};
|
|
50
|
+
const partsOf = (named, box, resolution, origin) => {
|
|
51
|
+
const cell = Math.max(box.width, box.height, 1) / resolution;
|
|
52
|
+
const nx = Math.max(1, Math.ceil(box.width / cell));
|
|
53
|
+
const ny = Math.max(1, Math.ceil(box.height / cell));
|
|
54
|
+
const canvas = document.createElement('canvas');
|
|
55
|
+
canvas.width = nx;
|
|
56
|
+
canvas.height = ny;
|
|
57
|
+
const context = canvas.getContext('2d', { willReadFrequently: true });
|
|
58
|
+
const labels = new Uint16Array(nx * ny);
|
|
59
|
+
if (context) {
|
|
60
|
+
context.scale(1 / cell, 1 / cell);
|
|
61
|
+
context.translate(-origin.x, -origin.y);
|
|
62
|
+
named.forEach(({ path }, at) => {
|
|
63
|
+
const label = at + 1;
|
|
64
|
+
context.fillStyle = `rgb(${label & 255}, ${(label >> 8) & 255}, 0)`;
|
|
65
|
+
context.fill(new Path2D(path), 'evenodd');
|
|
66
|
+
});
|
|
67
|
+
const pixels = context.getImageData(0, 0, nx, ny).data;
|
|
68
|
+
for (let at = 0; at < labels.length; at++) {
|
|
69
|
+
if ((pixels[at * 4 + 3] ?? 0) === 255 && (pixels[at * 4 + 2] ?? 0) === 0) {
|
|
70
|
+
const label = (pixels[at * 4] ?? 0) + ((pixels[at * 4 + 1] ?? 0) << 8);
|
|
71
|
+
labels[at] = label <= named.length ? label : 0;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return (x, y) => {
|
|
76
|
+
const column = Math.floor(x / cell);
|
|
77
|
+
const row = Math.floor(y / cell);
|
|
78
|
+
for (let ring = 0; ring <= 3; ring++) {
|
|
79
|
+
for (let dy = -ring; dy <= ring; dy++) {
|
|
80
|
+
for (let dx = -ring; dx <= ring; dx++) {
|
|
81
|
+
const c = column + dx;
|
|
82
|
+
const r = row + dy;
|
|
83
|
+
if (c < 0 || r < 0 || c >= nx || r >= ny) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const label = labels[r * nx + c] ?? 0;
|
|
87
|
+
if (label > 0) {
|
|
88
|
+
return named[label - 1]?.name;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
};
|
|
42
95
|
};
|
|
43
96
|
export const shapeFromImage = (image, { resolution = FLAT, threshold = 0.5 } = {}) => {
|
|
44
97
|
const size = (value) => typeof value === 'number' ? value : value.baseVal.value;
|
package/dist/shapes/index.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
export { fieldFromInside as shapeFromInside, fieldFromPath as shapeFromPath, inflate, type ShapeField, } from '../connect-graph/shape-field.js';
|
|
2
|
-
export { surfaceOf } from '../three/surface.js';
|
|
3
1
|
export { type ImageOptions, shapeFromImage, shapeFromSvg } from './flat.js';
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
2
|
+
export { fieldFromInside as shapeFromInside, fieldFromPath as shapeFromPath, inflate, type ShapeField, } from './shape-field.js';
|
|
3
|
+
export { cuboid, onSphere, sphere, torus } from './solids.js';
|
|
4
|
+
export { surfaceOf } from './surface.js';
|
|
6
5
|
export { shapeFromTriangles, type TriangleOptions } from './triangles.js';
|
package/dist/shapes/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
export { fieldFromInside as shapeFromInside, fieldFromPath as shapeFromPath, inflate, } from '../connect-graph/shape-field.js';
|
|
2
|
-
export { surfaceOf } from '../three/surface.js';
|
|
3
1
|
export { shapeFromImage, shapeFromSvg } from './flat.js';
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
2
|
+
export { fieldFromInside as shapeFromInside, fieldFromPath as shapeFromPath, inflate, } from './shape-field.js';
|
|
3
|
+
export { cuboid, onSphere, sphere, torus } from './solids.js';
|
|
4
|
+
export { surfaceOf } from './surface.js';
|
|
6
5
|
export { shapeFromTriangles } from './triangles.js';
|
|
@@ -24,12 +24,17 @@ export interface ShapeField {
|
|
|
24
24
|
corners: Float32Array;
|
|
25
25
|
triangles: Uint32Array;
|
|
26
26
|
} | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Which named part of the shape a point is in, where the shape has named
|
|
29
|
+
* parts: the part nearest it, or undefined where it has none.
|
|
30
|
+
*/
|
|
31
|
+
regionOf?(x: number, y: number, z?: number): string | undefined;
|
|
27
32
|
/** How wide the box is. */
|
|
28
33
|
readonly width: number;
|
|
29
34
|
}
|
|
30
35
|
/** How many cells a field has along its longer side, flat and solid. */
|
|
31
36
|
export declare const FLAT = 512;
|
|
32
|
-
export declare const SOLID =
|
|
37
|
+
export declare const SOLID = 144;
|
|
33
38
|
/**
|
|
34
39
|
* A field from a grid of cells marked inside, `cell` box units a cell, x
|
|
35
40
|
* fastest and z slowest: the distance to the nearest outside cell, taken
|
|
@@ -53,9 +58,13 @@ export declare const fieldFromInside: (inside: (x: number, y: number, z: number)
|
|
|
53
58
|
/**
|
|
54
59
|
* A flat field from an SVG path in a box `width` by `height`, its holes cut by
|
|
55
60
|
* the even-odd rule. Drawn once to a canvas and read off it, which is what
|
|
56
|
-
* lets a detailed outline become a field in a moment.
|
|
61
|
+
* lets a detailed outline become a field in a moment. A box that starts
|
|
62
|
+
* somewhere other than 0, as an SVG `viewBox` can, is given as `origin`.
|
|
57
63
|
*/
|
|
58
|
-
export declare const fieldFromPath: (path: string, width: number, height: number, resolution?: number
|
|
64
|
+
export declare const fieldFromPath: (path: string, width: number, height: number, resolution?: number, origin?: {
|
|
65
|
+
x: number;
|
|
66
|
+
y: number;
|
|
67
|
+
}) => ShapeField;
|
|
59
68
|
/**
|
|
60
69
|
* A solid puffed out of a flat shape, as a cushion is: as thick as `depth`
|
|
61
70
|
* where the shape is widest, rounding to nothing at its edge, and cut
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const FLAT = 512;
|
|
2
|
-
export const SOLID =
|
|
2
|
+
export const SOLID = 144;
|
|
3
3
|
const FAR = 1e20;
|
|
4
4
|
const line = (values, count, out) => {
|
|
5
5
|
const hull = new Int32Array(count);
|
|
@@ -126,7 +126,7 @@ export const fieldFromInside = (inside, box, resolution = box.depth ? SOLID : FL
|
|
|
126
126
|
width: box.width,
|
|
127
127
|
});
|
|
128
128
|
};
|
|
129
|
-
export const fieldFromPath = (path, width, height, resolution = FLAT) => {
|
|
129
|
+
export const fieldFromPath = (path, width, height, resolution = FLAT, origin = { x: 0, y: 0 }) => {
|
|
130
130
|
const cell = Math.max(width, height, 1) / resolution;
|
|
131
131
|
const nx = Math.max(1, Math.ceil(width / cell));
|
|
132
132
|
const ny = Math.max(1, Math.ceil(height / cell));
|
|
@@ -137,6 +137,7 @@ export const fieldFromPath = (path, width, height, resolution = FLAT) => {
|
|
|
137
137
|
const cells = new Uint8Array(nx * ny);
|
|
138
138
|
if (context) {
|
|
139
139
|
context.scale(1 / cell, 1 / cell);
|
|
140
|
+
context.translate(-origin.x, -origin.y);
|
|
140
141
|
context.fill(new Path2D(path), 'evenodd');
|
|
141
142
|
const alpha = context.getImageData(0, 0, nx, ny).data;
|
|
142
143
|
for (let at = 0; at < cells.length; at++) {
|
|
@@ -168,5 +169,6 @@ export const inflate = (flat, { depth, roundness = 1 }) => {
|
|
|
168
169
|
},
|
|
169
170
|
height: flat.height,
|
|
170
171
|
width: flat.width,
|
|
172
|
+
...(flat.regionOf ? { regionOf: (x, y) => flat.regionOf?.(x, y) } : {}),
|
|
171
173
|
};
|
|
172
174
|
};
|
package/dist/shapes/solids.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ShapeField } from '
|
|
1
|
+
import type { ShapeField } from './shape-field.js';
|
|
2
2
|
/** A ball `radius` across from its middle. */
|
|
3
3
|
export declare const sphere: ({ radius }?: {
|
|
4
4
|
radius?: number;
|
|
@@ -8,8 +8,8 @@ export declare const torus: ({ radius, tube, }?: {
|
|
|
8
8
|
radius?: number;
|
|
9
9
|
tube?: number;
|
|
10
10
|
}) => ShapeField;
|
|
11
|
-
/** A
|
|
12
|
-
export declare const
|
|
11
|
+
/** A cuboid `width` by `height` by `depth`, its edges rounded `round` in. */
|
|
12
|
+
export declare const cuboid: ({ depth, height, round, width, }?: {
|
|
13
13
|
depth?: number;
|
|
14
14
|
height?: number;
|
|
15
15
|
round?: number;
|
package/dist/shapes/solids.js
CHANGED
|
@@ -13,7 +13,7 @@ export const torus = ({ radius = 50, tube = 18, } = {}) => {
|
|
|
13
13
|
width: reach * 2,
|
|
14
14
|
};
|
|
15
15
|
};
|
|
16
|
-
export const
|
|
16
|
+
export const cuboid = ({ depth = 100, height = 100, round = 0, width = 100, } = {}) => ({
|
|
17
17
|
depth,
|
|
18
18
|
distance: (x, y, z = depth / 2) => {
|
|
19
19
|
const qx = Math.abs(x - width / 2) - width / 2 + round;
|
|
@@ -28,20 +28,36 @@ export const box = ({ depth = 100, height = 100, round = 0, width = 100, } = {})
|
|
|
28
28
|
});
|
|
29
29
|
export const onSphere = (flat, { radius = 50, thickness = radius * 0.08 } = {}) => {
|
|
30
30
|
const along = (Math.PI * 2 * radius) / Math.max(flat.width, 1e-9);
|
|
31
|
+
const onMap = (x, y, z) => {
|
|
32
|
+
const dx = x - radius;
|
|
33
|
+
const dy = y - radius;
|
|
34
|
+
const dz = z - radius;
|
|
35
|
+
const out = Math.hypot(dx, dy, dz);
|
|
36
|
+
const longitude = Math.atan2(dx, dz);
|
|
37
|
+
const latitude = Math.acos(Math.min(Math.max(dy / (out || 1), -1), 1));
|
|
38
|
+
return {
|
|
39
|
+
out,
|
|
40
|
+
u: ((longitude + Math.PI) / (Math.PI * 2)) * flat.width,
|
|
41
|
+
v: (latitude / Math.PI) * flat.height,
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
const regionOf = flat.regionOf;
|
|
31
45
|
return {
|
|
32
46
|
depth: radius * 2,
|
|
33
47
|
distance: (x, y, z = radius) => {
|
|
34
|
-
const
|
|
35
|
-
const dy = y - radius;
|
|
36
|
-
const dz = z - radius;
|
|
37
|
-
const out = Math.hypot(dx, dy, dz);
|
|
48
|
+
const { out, u, v } = onMap(x, y, z);
|
|
38
49
|
const shell = Math.abs(out - (radius - thickness / 2)) - thickness / 2;
|
|
39
|
-
|
|
40
|
-
const latitude = Math.acos(Math.min(Math.max(dy / (out || 1), -1), 1));
|
|
41
|
-
const map = flat.distance(((longitude + Math.PI) / (Math.PI * 2)) * flat.width, (latitude / Math.PI) * flat.height);
|
|
42
|
-
return Math.max(shell, map * along);
|
|
50
|
+
return Math.max(shell, flat.distance(u, v) * along);
|
|
43
51
|
},
|
|
44
52
|
height: radius * 2,
|
|
45
53
|
width: radius * 2,
|
|
54
|
+
...(regionOf
|
|
55
|
+
? {
|
|
56
|
+
regionOf: (x, y, z = radius) => {
|
|
57
|
+
const { u, v } = onMap(x, y, z);
|
|
58
|
+
return regionOf(u, v);
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
: {}),
|
|
46
62
|
};
|
|
47
63
|
};
|