@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
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { fieldFromInside, fieldFromPath } from './shape-field.js';
|
|
2
|
+
import { surfaceWeb } from './surface-web.js';
|
|
3
|
+
const PROBE = { flat: 64, solid: 32 };
|
|
4
|
+
const SHELL = 0.04;
|
|
3
5
|
const WAVE = 0.07;
|
|
4
6
|
const seeded = (seed) => {
|
|
5
7
|
let state = seed >>> 0 || 1;
|
|
@@ -8,57 +10,60 @@ const seeded = (seed) => {
|
|
|
8
10
|
return state / 4294967296;
|
|
9
11
|
};
|
|
10
12
|
};
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
const fields = new WeakMap();
|
|
14
|
+
export const fieldOf = (stencil) => {
|
|
15
|
+
if ('distance' in stencil) {
|
|
16
|
+
return stencil;
|
|
14
17
|
}
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const canvas = document.createElement('canvas');
|
|
19
|
-
canvas.width = width;
|
|
20
|
-
canvas.height = height;
|
|
21
|
-
const context = canvas.getContext('2d', { willReadFrequently: true });
|
|
22
|
-
if (!context) {
|
|
23
|
-
return () => true;
|
|
18
|
+
const made = fields.get(stencil);
|
|
19
|
+
if (made) {
|
|
20
|
+
return made;
|
|
24
21
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const row = Math.floor(y * scale);
|
|
31
|
-
return (column >= 0 &&
|
|
32
|
-
row >= 0 &&
|
|
33
|
-
column < width &&
|
|
34
|
-
row < height &&
|
|
35
|
-
(alpha[(row * width + column) * 4 + 3] ?? 0) > 127);
|
|
36
|
-
};
|
|
22
|
+
const field = 'path' in stencil
|
|
23
|
+
? fieldFromPath(stencil.path, stencil.width, stencil.height)
|
|
24
|
+
: fieldFromInside((x, y) => stencil.inside(x, y), stencil);
|
|
25
|
+
fields.set(stencil, field);
|
|
26
|
+
return field;
|
|
37
27
|
};
|
|
38
|
-
export const placesIn = (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
28
|
+
export const placesIn = (field, count, { place = 'fill', shell = SHELL } = {}) => {
|
|
29
|
+
const solid = field.depth > 0;
|
|
30
|
+
const { depth, height, width } = field;
|
|
31
|
+
const thickness = shell * Math.max(width, height, depth);
|
|
32
|
+
const takes = (x, y, z) => {
|
|
33
|
+
const distance = field.distance(x, y, z);
|
|
34
|
+
return distance < 0 && (place === 'fill' || distance > -thickness);
|
|
35
|
+
};
|
|
36
|
+
const probe = solid ? PROBE.solid : PROBE.flat;
|
|
37
|
+
let taken = 0;
|
|
38
|
+
for (let layer = 0; layer < (solid ? probe : 1); layer++) {
|
|
39
|
+
for (let row = 0; row < probe; row++) {
|
|
40
|
+
for (let column = 0; column < probe; column++) {
|
|
41
|
+
if (takes(((column + 0.5) / probe) * width, ((row + 0.5) / probe) * height, solid ? ((layer + 0.5) / probe) * depth : 0)) {
|
|
42
|
+
taken += 1;
|
|
43
|
+
}
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
-
if (count === 0 ||
|
|
47
|
+
if (count === 0 || taken === 0) {
|
|
48
48
|
return [];
|
|
49
49
|
}
|
|
50
50
|
const random = seeded(count);
|
|
51
|
-
|
|
51
|
+
const room = (taken / probe ** (solid ? 3 : 2)) * width * height * (solid ? depth : 1);
|
|
52
|
+
let step = (room / count) ** (1 / (solid ? 3 : 2));
|
|
52
53
|
for (let tries = 0; tries < 8; tries++) {
|
|
53
54
|
const places = [];
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
const layers = solid ? Math.ceil(depth / step) : 1;
|
|
56
|
+
for (let layer = 0; layer < layers; layer++) {
|
|
57
|
+
for (let y = step / 2; y < height; y += step) {
|
|
58
|
+
for (let x = step / 2; x < width; x += step) {
|
|
59
|
+
const place = {
|
|
60
|
+
x: x + (random() - 0.5) * step * 0.8,
|
|
61
|
+
y: y + (random() - 0.5) * step * 0.8,
|
|
62
|
+
...(solid ? { z: (layer + 0.5 + (random() - 0.5) * 0.8) * step } : {}),
|
|
63
|
+
};
|
|
64
|
+
if (takes(place.x, place.y, place.z ?? 0)) {
|
|
65
|
+
places.push(place);
|
|
66
|
+
}
|
|
62
67
|
}
|
|
63
68
|
}
|
|
64
69
|
}
|
|
@@ -69,53 +74,129 @@ export const placesIn = (inside, width, height, count) => {
|
|
|
69
74
|
}
|
|
70
75
|
return places.slice(0, count);
|
|
71
76
|
}
|
|
72
|
-
step *=
|
|
77
|
+
step *= (places.length / count) ** (1 / (solid ? 3 : 2)) * 0.97 || 0.5;
|
|
73
78
|
}
|
|
74
79
|
return [];
|
|
75
80
|
};
|
|
76
|
-
|
|
81
|
+
const nested = (items, keys) => {
|
|
82
|
+
const [key, ...rest] = keys;
|
|
83
|
+
if (!key) {
|
|
84
|
+
return items;
|
|
85
|
+
}
|
|
86
|
+
const sorted = [...items].sort((one, other) => key(one) - key(other));
|
|
87
|
+
if (rest.length === 0) {
|
|
88
|
+
return sorted;
|
|
89
|
+
}
|
|
90
|
+
const slice = Math.ceil(items.length ** (rest.length / keys.length));
|
|
91
|
+
const out = [];
|
|
92
|
+
for (let from = 0; from < sorted.length; from += slice) {
|
|
93
|
+
out.push(...nested(sorted.slice(from, from + slice), rest));
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
};
|
|
97
|
+
export const fitToStencil = (positions, stencil) => fitInStencil(positions, stencil).positions;
|
|
98
|
+
export const fitInStencil = (positions, stencil) => {
|
|
77
99
|
const notes = [...positions].map(([id, at]) => ({ at, id }));
|
|
78
100
|
if (notes.length === 0) {
|
|
79
|
-
return new Map();
|
|
101
|
+
return { positions: new Map() };
|
|
80
102
|
}
|
|
81
|
-
const
|
|
103
|
+
const field = fieldOf(stencil);
|
|
104
|
+
const places = placesIn(field, notes.length, {
|
|
105
|
+
place: stencil.place ?? 'fill',
|
|
106
|
+
shell: stencil.shell ?? SHELL,
|
|
107
|
+
});
|
|
82
108
|
if (places.length < notes.length) {
|
|
83
|
-
return new Map(positions);
|
|
109
|
+
return { positions: new Map(positions) };
|
|
84
110
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
111
|
+
const solid = field.depth > 0;
|
|
112
|
+
const thickness = (stencil.place ?? 'fill') === 'surface'
|
|
113
|
+
? (stencil.shell ?? SHELL) * Math.max(field.width, field.height, field.depth)
|
|
114
|
+
: 0;
|
|
115
|
+
const low = {
|
|
116
|
+
x: Number.POSITIVE_INFINITY,
|
|
117
|
+
y: Number.POSITIVE_INFINITY,
|
|
118
|
+
z: Number.POSITIVE_INFINITY,
|
|
119
|
+
};
|
|
120
|
+
const high = {
|
|
121
|
+
x: Number.NEGATIVE_INFINITY,
|
|
122
|
+
y: Number.NEGATIVE_INFINITY,
|
|
123
|
+
z: Number.NEGATIVE_INFINITY,
|
|
124
|
+
};
|
|
89
125
|
for (const { at } of notes) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
126
|
+
for (const axis of ['x', 'y', 'z']) {
|
|
127
|
+
const value = at[axis] ?? 0;
|
|
128
|
+
low[axis] = Math.min(low[axis], value);
|
|
129
|
+
high[axis] = Math.max(high[axis], value);
|
|
130
|
+
}
|
|
94
131
|
}
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
132
|
+
const { depth, height, width } = field;
|
|
133
|
+
const scale = Math.max(high.x - low.x, high.y - low.y, solid ? high.z - low.z : 0, 1) /
|
|
134
|
+
Math.max(width, height, depth);
|
|
135
|
+
const middle = { x: (low.x + high.x) / 2, y: (low.y + high.y) / 2, z: (low.z + high.z) / 2 };
|
|
136
|
+
const spots = places.map((place) => {
|
|
137
|
+
const z = place.z ?? depth / 2;
|
|
138
|
+
return {
|
|
139
|
+
across: place.x + WAVE * width * Math.sin((place.y / height) * Math.PI * 3 + 1.3),
|
|
140
|
+
at: {
|
|
141
|
+
x: middle.x + (place.x - width / 2) * scale,
|
|
142
|
+
y: middle.y - (place.y - height / 2) * scale,
|
|
143
|
+
...(solid ? { z: middle.z + (z - depth / 2) * scale } : {}),
|
|
144
|
+
},
|
|
145
|
+
down: -place.y + WAVE * height * Math.sin((place.x / width) * Math.PI * 3 + 0.4),
|
|
146
|
+
};
|
|
147
|
+
});
|
|
109
148
|
const fitted = new Map();
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
149
|
+
if (solid) {
|
|
150
|
+
const widest = (points) => {
|
|
151
|
+
let best = 'x';
|
|
152
|
+
let spread = Number.NEGATIVE_INFINITY;
|
|
153
|
+
for (const axis of ['x', 'y', 'z']) {
|
|
154
|
+
let low = Number.POSITIVE_INFINITY;
|
|
155
|
+
let high = Number.NEGATIVE_INFINITY;
|
|
156
|
+
for (const point of points) {
|
|
157
|
+
const value = point[axis] ?? 0;
|
|
158
|
+
low = Math.min(low, value);
|
|
159
|
+
high = Math.max(high, value);
|
|
160
|
+
}
|
|
161
|
+
if (high - low > spread) {
|
|
162
|
+
spread = high - low;
|
|
163
|
+
best = axis;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return best;
|
|
167
|
+
};
|
|
168
|
+
const targets = spots.map((spot) => spot.at);
|
|
169
|
+
const web = (stencil.place ?? 'fill') === 'surface' ? surfaceWeb(targets) : undefined;
|
|
170
|
+
const halveTargets = (places, first) => {
|
|
171
|
+
if (web && places.length > 2) {
|
|
172
|
+
return web.halve(places, first);
|
|
173
|
+
}
|
|
174
|
+
const axis = widest(places.map((at) => targets[at]));
|
|
175
|
+
const ordered = [...places].sort((one, other) => (targets[one][axis] ?? 0) - (targets[other][axis] ?? 0));
|
|
176
|
+
return [ordered.slice(0, first), ordered.slice(first)];
|
|
177
|
+
};
|
|
178
|
+
const halve = (ours, theirs) => {
|
|
179
|
+
if (ours.length === 1) {
|
|
180
|
+
fitted.set(ours[0].id, targets[theirs[0]]);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (ours.length === 0) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const mine = widest(ours.map((note) => note.at));
|
|
187
|
+
ours.sort((one, other) => (one.at[mine] ?? 0) - (other.at[mine] ?? 0));
|
|
188
|
+
const half = Math.floor(ours.length / 2);
|
|
189
|
+
const [near, rest] = halveTargets(theirs, half);
|
|
190
|
+
halve(ours.slice(0, half), near);
|
|
191
|
+
halve(ours.slice(half), rest);
|
|
192
|
+
};
|
|
193
|
+
halve([...notes], targets.map((_, at) => at));
|
|
194
|
+
return { frame: { field, middle, scale, shell: thickness }, positions: fitted };
|
|
119
195
|
}
|
|
120
|
-
|
|
196
|
+
const ours = nested(notes, [(note) => note.at.x, (note) => note.at.y]);
|
|
197
|
+
const theirs = nested(spots, [(spot) => spot.across, (spot) => spot.down]);
|
|
198
|
+
ours.forEach((note, at) => {
|
|
199
|
+
fitted.set(note.id, theirs[at].at);
|
|
200
|
+
});
|
|
201
|
+
return { frame: { field, middle, scale, shell: thickness }, positions: fitted };
|
|
121
202
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { GraphPoint } from './seed.js';
|
|
2
|
+
/**
|
|
3
|
+
* Places on a surface, joined to their near neighbors, and split in halves
|
|
4
|
+
* by how far apart they are across the surface, not through the air.
|
|
5
|
+
*
|
|
6
|
+
* **Half a surface measured across it is one piece of it**: a stretch of a
|
|
7
|
+
* knot's tube, or one lobe of a brain. Half of it by position can be its
|
|
8
|
+
* front and its back, and half of it by direction from its middle can be
|
|
9
|
+
* three passes of a tube that crosses itself.
|
|
10
|
+
*/
|
|
11
|
+
export declare const surfaceWeb: (points: readonly GraphPoint[]) => {
|
|
12
|
+
/**
|
|
13
|
+
* A set of places in two halves, `first` as many as the first: the
|
|
14
|
+
* places nearer one end of the set, across the surface, and the rest.
|
|
15
|
+
* The ends are two places about as far apart across it as any.
|
|
16
|
+
*/
|
|
17
|
+
halve: (places: readonly number[], first: number) => [number[], number[]];
|
|
18
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export const surfaceWeb = (points) => {
|
|
2
|
+
const count = points.length;
|
|
3
|
+
const x = (at) => points[at].x;
|
|
4
|
+
const y = (at) => points[at].y;
|
|
5
|
+
const z = (at) => points[at].z ?? 0;
|
|
6
|
+
const far = (one, other) => Math.hypot(x(one) - x(other), y(one) - y(other), z(one) - z(other));
|
|
7
|
+
const sample = Math.min(count, 256);
|
|
8
|
+
const nearest = [];
|
|
9
|
+
for (let step = 0; step < sample; step++) {
|
|
10
|
+
const one = Math.floor((step * count) / sample);
|
|
11
|
+
let best = Number.POSITIVE_INFINITY;
|
|
12
|
+
for (let other = 0; other < count; other++) {
|
|
13
|
+
if (other !== one) {
|
|
14
|
+
best = Math.min(best, far(one, other));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (Number.isFinite(best)) {
|
|
18
|
+
nearest.push(best);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
nearest.sort((one, other) => one - other);
|
|
22
|
+
const reach = (nearest[Math.floor(nearest.length / 2)] ?? 1) * 2.2;
|
|
23
|
+
const cells = new Map();
|
|
24
|
+
const cellOf = (at) => [
|
|
25
|
+
Math.floor(x(at) / reach),
|
|
26
|
+
Math.floor(y(at) / reach),
|
|
27
|
+
Math.floor(z(at) / reach),
|
|
28
|
+
];
|
|
29
|
+
for (let at = 0; at < count; at++) {
|
|
30
|
+
const key = cellOf(at).join(',');
|
|
31
|
+
const list = cells.get(key);
|
|
32
|
+
if (list) {
|
|
33
|
+
list.push(at);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
cells.set(key, [at]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const neighbors = Array.from({ length: count }, () => []);
|
|
40
|
+
for (let at = 0; at < count; at++) {
|
|
41
|
+
const [cx, cy, cz] = cellOf(at);
|
|
42
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
43
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
44
|
+
for (let dz = -1; dz <= 1; dz++) {
|
|
45
|
+
for (const other of cells.get(`${cx + dx},${cy + dy},${cz + dz}`) ?? []) {
|
|
46
|
+
if (other > at && far(at, other) <= reach) {
|
|
47
|
+
;
|
|
48
|
+
neighbors[at].push(other);
|
|
49
|
+
neighbors[other].push(at);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const member = new Int32Array(count).fill(-1);
|
|
57
|
+
let round = 0;
|
|
58
|
+
const steps = (from, out) => {
|
|
59
|
+
out.fill(-1);
|
|
60
|
+
const queue = [from];
|
|
61
|
+
out[from] = 0;
|
|
62
|
+
let last = from;
|
|
63
|
+
for (let head = 0; head < queue.length; head++) {
|
|
64
|
+
const at = queue[head];
|
|
65
|
+
last = at;
|
|
66
|
+
for (const next of neighbors[at]) {
|
|
67
|
+
if (member[next] === round && out[next] === -1) {
|
|
68
|
+
out[next] = out[at] + 1;
|
|
69
|
+
queue.push(next);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return last;
|
|
74
|
+
};
|
|
75
|
+
const fromA = new Int32Array(count);
|
|
76
|
+
const fromB = new Int32Array(count);
|
|
77
|
+
return {
|
|
78
|
+
halve: (places, first) => {
|
|
79
|
+
round += 1;
|
|
80
|
+
for (const at of places) {
|
|
81
|
+
member[at] = round;
|
|
82
|
+
}
|
|
83
|
+
const a = steps(places[0], fromA);
|
|
84
|
+
const b = steps(a, fromA);
|
|
85
|
+
steps(b, fromB);
|
|
86
|
+
const unreached = places.length * 4;
|
|
87
|
+
const key = (at) => {
|
|
88
|
+
const one = fromA[at];
|
|
89
|
+
const other = fromB[at];
|
|
90
|
+
if (one < 0 || other < 0) {
|
|
91
|
+
return unreached + far(at, a) - far(at, b);
|
|
92
|
+
}
|
|
93
|
+
return one - other;
|
|
94
|
+
};
|
|
95
|
+
const ordered = [...places].sort((one, other) => key(one) - key(other));
|
|
96
|
+
return [ordered.slice(0, first), ordered.slice(first)];
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { gesturesHeld, groupsOver, signalsOver } from '../overlay/overlay.js';
|
|
1
|
+
import { gesturesHeld, groupsOver, namesOver, signalsOver, } from '../overlay/overlay.js';
|
|
2
2
|
const SHAPES = { circle: 0, diamond: 3, ring: 0, square: 1 };
|
|
3
3
|
const STEP = 250;
|
|
4
4
|
const FAINT = 0.16;
|
|
@@ -81,61 +81,13 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
81
81
|
const held = gesturesHeld(container);
|
|
82
82
|
const signals = signalsOver(container, onScreen);
|
|
83
83
|
const groups = groupsOver(container, onScreen);
|
|
84
|
-
const names =
|
|
85
|
-
names.setAttribute('aria-hidden', 'true');
|
|
86
|
-
names.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;pointer-events:none';
|
|
87
|
-
container.append(names);
|
|
84
|
+
const names = namesOver(container, onScreen);
|
|
88
85
|
const drawNames = () => {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const width = Math.round(container.clientWidth * ratio);
|
|
95
|
-
const height = Math.round(container.clientHeight * ratio);
|
|
96
|
-
if (names.width !== width || names.height !== height) {
|
|
97
|
-
names.width = width;
|
|
98
|
-
names.height = height;
|
|
99
|
-
}
|
|
100
|
-
context.setTransform(ratio, 0, 0, ratio, 0, 0);
|
|
101
|
-
context.clearRect(0, 0, width, height);
|
|
102
|
-
const taken = [];
|
|
103
|
-
const shown = [selected, hovered === undefined ? undefined : ids[hovered], ...near];
|
|
104
|
-
for (const id of shown) {
|
|
105
|
-
if (id === undefined) {
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
const at = onScreen(id);
|
|
109
|
-
const label = looks.node(id);
|
|
110
|
-
if (!at || label.hidden) {
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
const name = drawnNow.get(id)?.label ?? id;
|
|
114
|
-
const strong = id === selected || id === ids[hovered ?? -1];
|
|
115
|
-
context.font = `${strong ? 600 : 500} ${strong ? 13 : 12}px ${looks.font}`;
|
|
116
|
-
const x = at.x + at.radius + 4;
|
|
117
|
-
const y = at.y + 4;
|
|
118
|
-
const box = {
|
|
119
|
-
bottom: y + 4,
|
|
120
|
-
left: x - 2,
|
|
121
|
-
right: x + context.measureText(name).width + 2,
|
|
122
|
-
top: y - 13,
|
|
123
|
-
};
|
|
124
|
-
const free = taken.every((other) => box.right < other.left ||
|
|
125
|
-
box.left > other.right ||
|
|
126
|
-
box.bottom < other.top ||
|
|
127
|
-
box.top > other.bottom);
|
|
128
|
-
if (!free && !strong) {
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
taken.push(box);
|
|
132
|
-
context.lineJoin = 'round';
|
|
133
|
-
context.lineWidth = 3;
|
|
134
|
-
context.strokeStyle = looks.ground;
|
|
135
|
-
context.strokeText(name, x, y);
|
|
136
|
-
context.fillStyle = strong ? looks.ink : looks.quiet;
|
|
137
|
-
context.fillText(name, x, y);
|
|
138
|
-
}
|
|
86
|
+
const lit = hovered === undefined ? undefined : ids[hovered];
|
|
87
|
+
const shown = [...new Set([selected, lit, ...near])].flatMap((id) => id === undefined
|
|
88
|
+
? []
|
|
89
|
+
: [{ id, name: drawnNow.get(id)?.label ?? id, strong: id === selected || id === lit }]);
|
|
90
|
+
names.draw(looks, shown);
|
|
139
91
|
};
|
|
140
92
|
const redraw = () => {
|
|
141
93
|
drawNames();
|
|
@@ -418,7 +370,7 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
418
370
|
});
|
|
419
371
|
signals.stop();
|
|
420
372
|
groups.stop();
|
|
421
|
-
names.
|
|
373
|
+
names.stop();
|
|
422
374
|
held.stop();
|
|
423
375
|
},
|
|
424
376
|
update: (changes, animate) => {
|
package/dist/index.d.ts
CHANGED
|
@@ -2,4 +2,4 @@ export { type ConnectGraphOptions, connectGraph, type DrawnLink, type DrawnNode,
|
|
|
2
2
|
export { diffGraph } from './connect-graph/diff.js';
|
|
3
3
|
export { type GraphColors, type GraphLooks, graphColorsFrom, type LinkLook, type NodeLook, } from './connect-graph/looks.js';
|
|
4
4
|
export { type GraphPoint, seedPositions } from './connect-graph/seed.js';
|
|
5
|
-
export { fitToStencil, type GraphStencil } from './connect-graph/stencil.js';
|
|
5
|
+
export { fitToStencil, type GraphStencil, type StencilFrame, } from './connect-graph/stencil.js';
|
package/dist/index.js
CHANGED
|
@@ -2,4 +2,4 @@ export { connectGraph, } from './connect-graph/connect-graph.js';
|
|
|
2
2
|
export { diffGraph } from './connect-graph/diff.js';
|
|
3
3
|
export { graphColorsFrom, } from './connect-graph/looks.js';
|
|
4
4
|
export { seedPositions } from './connect-graph/seed.js';
|
|
5
|
-
export { fitToStencil } from './connect-graph/stencil.js';
|
|
5
|
+
export { fitToStencil, } from './connect-graph/stencil.js';
|
|
@@ -7,6 +7,8 @@ export type OnScreen = (id: string) => {
|
|
|
7
7
|
x: number;
|
|
8
8
|
y: number;
|
|
9
9
|
} | undefined;
|
|
10
|
+
/** Whether an `rgb()` color is light, which a light ink says the ground is dark. */
|
|
11
|
+
export declare const isLight: (color: string) => boolean;
|
|
10
12
|
/** Eases in and out, so a signal leaves and arrives gently. */
|
|
11
13
|
export declare const ease: (at: number) => number;
|
|
12
14
|
/** A name's box on screen, in pixels. */
|
|
@@ -30,6 +32,22 @@ export declare const signalsOver: (container: HTMLElement, onScreen: OnScreen) =
|
|
|
30
32
|
} | undefined, animate: boolean) => void;
|
|
31
33
|
stop: () => void;
|
|
32
34
|
};
|
|
35
|
+
/** A note to name over the drawing: its name, and whether it is the one the reader is on. */
|
|
36
|
+
export interface Named {
|
|
37
|
+
id: string;
|
|
38
|
+
name: string;
|
|
39
|
+
strong: boolean;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* **Names on a canvas of their own**, for a renderer that draws none: each
|
|
43
|
+
* note given, beside it, ringed in the ground's color so it reads over the
|
|
44
|
+
* links. The strong ones always, and the rest where there is room, in the
|
|
45
|
+
* order given.
|
|
46
|
+
*/
|
|
47
|
+
export declare const namesOver: (container: HTMLElement, onScreen: OnScreen) => {
|
|
48
|
+
draw: (looks: GraphLooks | undefined, named: readonly Named[]) => void;
|
|
49
|
+
stop: () => void;
|
|
50
|
+
};
|
|
33
51
|
/**
|
|
34
52
|
* **Group names are drawn on a canvas of their own**, under the signals', and
|
|
35
53
|
* drawn again whenever the driver says the view moved, so they follow it.
|
package/dist/overlay/overlay.js
CHANGED
|
@@ -65,7 +65,7 @@ const arrive = (context, color, at, gone) => {
|
|
|
65
65
|
context.arc(at.x, at.y, reach, 0, Math.PI * 2);
|
|
66
66
|
context.fill();
|
|
67
67
|
};
|
|
68
|
-
const isLight = (color) => {
|
|
68
|
+
export const isLight = (color) => {
|
|
69
69
|
const [red = 0, green = 0, blue = 0] = color.match(/[\d.]+/g)?.map(Number) ?? [];
|
|
70
70
|
return (0.2126 * red + 0.7152 * green + 0.0722 * blue) / 255 > 0.5;
|
|
71
71
|
};
|
|
@@ -76,6 +76,7 @@ const nameGroups = (context, groups, looks, at) => {
|
|
|
76
76
|
context.textAlign = 'center';
|
|
77
77
|
context.textBaseline = 'middle';
|
|
78
78
|
context.shadowColor = looks.ground;
|
|
79
|
+
const taken = [];
|
|
79
80
|
for (const group of groups) {
|
|
80
81
|
let x = 0;
|
|
81
82
|
let y = 0;
|
|
@@ -100,6 +101,23 @@ const nameGroups = (context, groups, looks, at) => {
|
|
|
100
101
|
continue;
|
|
101
102
|
}
|
|
102
103
|
const name = group.name.toLocaleUpperCase();
|
|
104
|
+
const half = context.measureText(name).width / 2 + 4;
|
|
105
|
+
const boxAt = (middle) => ({
|
|
106
|
+
bottom: middle + 9,
|
|
107
|
+
left: x - half,
|
|
108
|
+
right: x + half,
|
|
109
|
+
top: middle - 9,
|
|
110
|
+
});
|
|
111
|
+
const clear = (box) => taken.every((other) => box.right < other.left ||
|
|
112
|
+
box.left > other.right ||
|
|
113
|
+
box.bottom < other.top ||
|
|
114
|
+
box.top > other.bottom);
|
|
115
|
+
const line = [0, 18, -18].map((step) => y + step).find((middle) => clear(boxAt(middle)));
|
|
116
|
+
if (line === undefined) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
y = line;
|
|
120
|
+
taken.push(boxAt(y));
|
|
103
121
|
context.globalAlpha = group.faded ? 0.3 : 0.92;
|
|
104
122
|
context.fillStyle = mixRgb(group.color, looks.ink, 0.6);
|
|
105
123
|
context.shadowBlur = 10;
|
|
@@ -223,6 +241,50 @@ export const signalsOver = (container, onScreen) => {
|
|
|
223
241
|
},
|
|
224
242
|
};
|
|
225
243
|
};
|
|
244
|
+
export const namesOver = (container, onScreen) => {
|
|
245
|
+
const canvas = layer(container, 'names');
|
|
246
|
+
return {
|
|
247
|
+
draw: (looks, named) => {
|
|
248
|
+
const context = looks ? fresh(canvas, container) : null;
|
|
249
|
+
if (!context || !looks) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const taken = [];
|
|
253
|
+
for (const { id, name, strong } of named) {
|
|
254
|
+
const at = onScreen(id);
|
|
255
|
+
if (!at || looks.node(id).hidden) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
context.font = `${strong ? 600 : 500} ${strong ? 13 : 12}px ${looks.font}`;
|
|
259
|
+
const x = at.x + at.radius + 4;
|
|
260
|
+
const y = at.y + 4;
|
|
261
|
+
const box = {
|
|
262
|
+
bottom: y + 4,
|
|
263
|
+
left: x - 2,
|
|
264
|
+
right: x + context.measureText(name).width + 2,
|
|
265
|
+
top: y - 13,
|
|
266
|
+
};
|
|
267
|
+
const free = taken.every((other) => box.right < other.left ||
|
|
268
|
+
box.left > other.right ||
|
|
269
|
+
box.bottom < other.top ||
|
|
270
|
+
box.top > other.bottom);
|
|
271
|
+
if (!free && !strong) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
taken.push(box);
|
|
275
|
+
context.lineJoin = 'round';
|
|
276
|
+
context.lineWidth = 3;
|
|
277
|
+
context.strokeStyle = looks.ground;
|
|
278
|
+
context.strokeText(name, x, y);
|
|
279
|
+
context.fillStyle = strong ? looks.ink : looks.quiet;
|
|
280
|
+
context.fillText(name, x, y);
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
stop: () => {
|
|
284
|
+
canvas.remove();
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
};
|
|
226
288
|
export const groupsOver = (container, onScreen) => {
|
|
227
289
|
let canvas;
|
|
228
290
|
let groups = [];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type ShapeField } from '../connect-graph/shape-field.js';
|
|
2
|
+
/**
|
|
3
|
+
* A flat field from an SVG: an `<svg>` element, or its markup. Its box is
|
|
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.
|
|
6
|
+
*
|
|
7
|
+
* Transforms on the shapes are not read: draw the outline in the `viewBox`'s
|
|
8
|
+
* own units, as an exported icon or logo is.
|
|
9
|
+
*/
|
|
10
|
+
export declare const shapeFromSvg: (svg: SVGSVGElement | string, resolution?: number) => ShapeField;
|
|
11
|
+
/** How an image is read as a shape. */
|
|
12
|
+
export interface ImageOptions {
|
|
13
|
+
/** How opaque a pixel is to be inside, from 0 to 1. 0.5 by default. */
|
|
14
|
+
threshold?: number | undefined;
|
|
15
|
+
/** How many cells the field has along the image's longer side. 512 by default. */
|
|
16
|
+
resolution?: number | undefined;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A flat field from an image's opaque pixels: a logo, a silhouette, or a
|
|
20
|
+
* mask drawn for the purpose. Its box is the image's size in pixels.
|
|
21
|
+
*
|
|
22
|
+
* An image from another site is read only where that site allows it, as a
|
|
23
|
+
* canvas reads any image.
|
|
24
|
+
*/
|
|
25
|
+
export declare const shapeFromImage: (image: CanvasImageSource & {
|
|
26
|
+
height: number | SVGAnimatedLength;
|
|
27
|
+
width: number | SVGAnimatedLength;
|
|
28
|
+
}, { resolution, threshold }?: ImageOptions) => ShapeField;
|