@kgdiff/scene 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +50 -2
- package/dist/build.d.ts +23 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +407 -0
- package/dist/build.js.map +1 -0
- package/dist/choreography.d.ts +98 -0
- package/dist/choreography.d.ts.map +1 -0
- package/dist/choreography.js +271 -0
- package/dist/choreography.js.map +1 -0
- package/dist/color.d.ts +42 -0
- package/dist/color.d.ts.map +1 -0
- package/dist/color.js +169 -0
- package/dist/color.js.map +1 -0
- package/dist/constants.d.ts +89 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +89 -0
- package/dist/constants.js.map +1 -0
- package/dist/easing.d.ts +28 -0
- package/dist/easing.d.ts.map +1 -0
- package/dist/easing.js +103 -0
- package/dist/easing.js.map +1 -0
- package/dist/focus.d.ts +45 -0
- package/dist/focus.d.ts.map +1 -0
- package/dist/focus.js +317 -0
- package/dist/focus.js.map +1 -0
- package/dist/frame.d.ts +13 -0
- package/dist/frame.d.ts.map +1 -0
- package/dist/frame.js +32 -0
- package/dist/frame.js.map +1 -0
- package/dist/geometry.d.ts +61 -0
- package/dist/geometry.d.ts.map +1 -0
- package/dist/geometry.js +122 -0
- package/dist/geometry.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/resolver.d.ts +9 -0
- package/dist/resolver.d.ts.map +1 -0
- package/dist/resolver.js +1621 -0
- package/dist/resolver.js.map +1 -0
- package/dist/roles.d.ts +46 -0
- package/dist/roles.d.ts.map +1 -0
- package/dist/roles.js +197 -0
- package/dist/roles.js.map +1 -0
- package/dist/styles.d.ts +116 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +391 -0
- package/dist/styles.js.map +1 -0
- package/dist/theme.d.ts +14 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +154 -0
- package/dist/theme.js.map +1 -0
- package/dist/timeline.d.ts +69 -0
- package/dist/timeline.d.ts.map +1 -0
- package/dist/timeline.js +362 -0
- package/dist/timeline.js.map +1 -0
- package/dist/types.d.ts +493 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/view.d.ts +11 -0
- package/dist/view.d.ts.map +1 -0
- package/dist/view.js +22 -0
- package/dist/view.js.map +1 -0
- package/package.json +46 -3
- package/src/build.ts +450 -0
- package/src/choreography.ts +342 -0
- package/src/color.ts +194 -0
- package/src/constants.ts +97 -0
- package/src/easing.ts +104 -0
- package/src/focus.ts +335 -0
- package/src/frame.ts +37 -0
- package/src/geometry.ts +162 -0
- package/src/index.ts +15 -0
- package/src/resolver.ts +1710 -0
- package/src/roles.ts +203 -0
- package/src/styles.ts +513 -0
- package/src/theme.ts +185 -0
- package/src/timeline.ts +391 -0
- package/src/types.ts +544 -0
- package/src/view.ts +27 -0
package/dist/resolver.js
ADDED
|
@@ -0,0 +1,1621 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createVisualResolver`: ViewState → FrameVisuals, the per-frame heart of the visual language.
|
|
3
|
+
*
|
|
4
|
+
* Construction precomputes everything static (per-layer styles, label strings, color ramps,
|
|
5
|
+
* the replay choreography, glyph specs). `resolve(view, out)` then copies styles into the
|
|
6
|
+
* reused `out` objects, animates only the elements inside active replay windows and applies
|
|
7
|
+
* the view modulators (focus, family filter, spotlight, hover, selection, LOD).
|
|
8
|
+
*
|
|
9
|
+
* Allocation guarantee: with `out`, steady-state frames allocate no objects and no arrays —
|
|
10
|
+
* element objects, nested ring / halo / label / badge objects and glyph objects are pooled per
|
|
11
|
+
* frame object, and every per-element number is read from flat typed-array snapshots. What
|
|
12
|
+
* remains is transient V8 HeapNumber boxing of doubles in code the JIT has not inlined or
|
|
13
|
+
* optimized yet: none in static frames (union / before / after, hover, selection, filters), a
|
|
14
|
+
* few boxes per *animating* element in replay (measured ≲ 1–10 KB per frame for thousands of
|
|
15
|
+
* nodes, depending on JIT tier and module format). That is short-lived young-generation garbage,
|
|
16
|
+
* collected by scavenges in well under a millisecond, and is tolerated by design.
|
|
17
|
+
*
|
|
18
|
+
* Alpha convention: `NodeVisual.alpha` / `EdgeVisual.alpha` / `GlyphVisual.alpha` apply to the
|
|
19
|
+
* mark; nested alphas (ring, halo, labels; badges have none) are relative to it.
|
|
20
|
+
*/
|
|
21
|
+
import { KgdiffError } from '@kgdiff/core';
|
|
22
|
+
import { CHOREO, choreograph, EA_FADE, EA_FOLLOW_MERGE, EA_FOLLOW_SPLIT, EA_GROW, EA_NONE, EA_RETRACT, EA_SWAP, EA_SWING, NA_ADD, NA_EMERGE, NA_FADE, NA_NONE, NA_REMOVE, NA_TRAVEL, } from './choreography.js';
|
|
23
|
+
import { rampAt } from './color.js';
|
|
24
|
+
import { DASH_DOTTED, DASH_EDGE, DASH_GHOST, EMPHASIS, LABEL, LOD, PHASE, STROKE, } from './constants.js';
|
|
25
|
+
import { backOut, bump, clamp01, easeInOut, easeStandard, lerp, segment } from './easing.js';
|
|
26
|
+
import { edgeCurvatures } from './geometry.js';
|
|
27
|
+
import { EL_BEFORE, EL_NONE, EL_UNION, edgeLayerStyles, F_IDENTITY, F_NEUTRAL, F_REMOVED, F_STRUCTURE, FAMILY_BY_CODE, familyCode, nodeLayerStyles, StyleBook, tokensFor, } from './styles.js';
|
|
28
|
+
const L_UNION = 0;
|
|
29
|
+
const L_BEFORE = 1;
|
|
30
|
+
const L_AFTER = 2;
|
|
31
|
+
const L_REPLAY = 3;
|
|
32
|
+
const SPOT_END = PHASE.spotlightEnd;
|
|
33
|
+
const SETTLE = PHASE.settleStart;
|
|
34
|
+
const TRANSFORM = SETTLE - SPOT_END;
|
|
35
|
+
const CHANGE_FAMILIES = [
|
|
36
|
+
'added',
|
|
37
|
+
'removed',
|
|
38
|
+
'content',
|
|
39
|
+
'structure',
|
|
40
|
+
'identity',
|
|
41
|
+
];
|
|
42
|
+
const NO_GROUPS = Object.freeze([]);
|
|
43
|
+
/** Glyph roles (what a glyph spec is for). */
|
|
44
|
+
const G_TRAIL = 0;
|
|
45
|
+
const G_REPLACEMENT = 1;
|
|
46
|
+
const G_GHOST = 2;
|
|
47
|
+
const G_PIVOT = 3;
|
|
48
|
+
const G_SUGGESTION = 4;
|
|
49
|
+
const newLabel = () => ({
|
|
50
|
+
text: '',
|
|
51
|
+
color: '',
|
|
52
|
+
alpha: 0,
|
|
53
|
+
size: 0,
|
|
54
|
+
weight: 400,
|
|
55
|
+
strike: false,
|
|
56
|
+
priority: 0,
|
|
57
|
+
minZoom: 0,
|
|
58
|
+
offset: 0,
|
|
59
|
+
});
|
|
60
|
+
const newNode = () => ({
|
|
61
|
+
visible: false,
|
|
62
|
+
x: 0,
|
|
63
|
+
y: 0,
|
|
64
|
+
r: 0,
|
|
65
|
+
alpha: 0,
|
|
66
|
+
family: 'neutral',
|
|
67
|
+
fill: null,
|
|
68
|
+
stroke: null,
|
|
69
|
+
strokeWidth: 0,
|
|
70
|
+
dash: null,
|
|
71
|
+
ring: null,
|
|
72
|
+
halo: null,
|
|
73
|
+
label: null,
|
|
74
|
+
secondaryLabel: null,
|
|
75
|
+
badge: null,
|
|
76
|
+
pulse: 0,
|
|
77
|
+
z: 0,
|
|
78
|
+
});
|
|
79
|
+
const newEdge = () => ({
|
|
80
|
+
visible: false,
|
|
81
|
+
x1: 0,
|
|
82
|
+
y1: 0,
|
|
83
|
+
x2: 0,
|
|
84
|
+
y2: 0,
|
|
85
|
+
r1: 0,
|
|
86
|
+
r2: 0,
|
|
87
|
+
curvature: 0,
|
|
88
|
+
color: '',
|
|
89
|
+
width: 0,
|
|
90
|
+
alpha: 0,
|
|
91
|
+
dash: null,
|
|
92
|
+
dashOffset: 0,
|
|
93
|
+
arrow: false,
|
|
94
|
+
arrowMinZoom: 0,
|
|
95
|
+
progress: 1,
|
|
96
|
+
fromTarget: false,
|
|
97
|
+
label: null,
|
|
98
|
+
z: 0,
|
|
99
|
+
});
|
|
100
|
+
const newGlyph = () => ({
|
|
101
|
+
kind: 'pivot',
|
|
102
|
+
x1: 0,
|
|
103
|
+
y1: 0,
|
|
104
|
+
x2: 0,
|
|
105
|
+
y2: 0,
|
|
106
|
+
r1: 0,
|
|
107
|
+
r2: 0,
|
|
108
|
+
color: '',
|
|
109
|
+
alpha: 0,
|
|
110
|
+
width: 0,
|
|
111
|
+
dash: null,
|
|
112
|
+
progress: 1,
|
|
113
|
+
curvature: 0,
|
|
114
|
+
label: null,
|
|
115
|
+
});
|
|
116
|
+
function hideNode(v) {
|
|
117
|
+
v.visible = false;
|
|
118
|
+
v.alpha = 0;
|
|
119
|
+
v.ring = null;
|
|
120
|
+
v.halo = null;
|
|
121
|
+
v.label = null;
|
|
122
|
+
v.secondaryLabel = null;
|
|
123
|
+
v.badge = null;
|
|
124
|
+
v.pulse = 0;
|
|
125
|
+
}
|
|
126
|
+
function hideEdge(v) {
|
|
127
|
+
v.visible = false;
|
|
128
|
+
v.alpha = 0;
|
|
129
|
+
v.label = null;
|
|
130
|
+
}
|
|
131
|
+
class SceneResolver {
|
|
132
|
+
scene;
|
|
133
|
+
t;
|
|
134
|
+
N;
|
|
135
|
+
M;
|
|
136
|
+
H;
|
|
137
|
+
// Static per-element data.
|
|
138
|
+
nStyles;
|
|
139
|
+
eStyles;
|
|
140
|
+
nFam;
|
|
141
|
+
nHunk;
|
|
142
|
+
nHunkFam;
|
|
143
|
+
eFam;
|
|
144
|
+
eHunk;
|
|
145
|
+
eHunkFam;
|
|
146
|
+
eUnionText;
|
|
147
|
+
ghostCurv;
|
|
148
|
+
fillRamp;
|
|
149
|
+
strokeRamp;
|
|
150
|
+
ringRamp;
|
|
151
|
+
c;
|
|
152
|
+
glyphs;
|
|
153
|
+
glyphLists;
|
|
154
|
+
famBit;
|
|
155
|
+
// Flat snapshots of the scene. Scene objects come in several shapes (optional fields), and
|
|
156
|
+
// reading their double fields at a megamorphic site boxes a fresh heap number per load; flat
|
|
157
|
+
// arrays keep the frame loop monomorphic and allocation-free.
|
|
158
|
+
nx;
|
|
159
|
+
ny;
|
|
160
|
+
nr;
|
|
161
|
+
nImp;
|
|
162
|
+
nFlow;
|
|
163
|
+
nLabel;
|
|
164
|
+
nLabelBefore;
|
|
165
|
+
nBadge;
|
|
166
|
+
eS;
|
|
167
|
+
eT;
|
|
168
|
+
eSB;
|
|
169
|
+
eTB;
|
|
170
|
+
eCurv;
|
|
171
|
+
eLabel;
|
|
172
|
+
eLabelBefore;
|
|
173
|
+
eUnchanged;
|
|
174
|
+
hNodes;
|
|
175
|
+
hContext;
|
|
176
|
+
hEdges;
|
|
177
|
+
hFam;
|
|
178
|
+
chapterMode;
|
|
179
|
+
chStart;
|
|
180
|
+
chEnd;
|
|
181
|
+
chHunks;
|
|
182
|
+
// Replay state.
|
|
183
|
+
wStart;
|
|
184
|
+
wEnd;
|
|
185
|
+
hState;
|
|
186
|
+
hP;
|
|
187
|
+
levelGen;
|
|
188
|
+
level;
|
|
189
|
+
edgeGen;
|
|
190
|
+
pulseGen;
|
|
191
|
+
pulseVal;
|
|
192
|
+
gen = 0;
|
|
193
|
+
env = 0;
|
|
194
|
+
// Selection state (cached per selected hunk).
|
|
195
|
+
selNode;
|
|
196
|
+
selHunk = -1;
|
|
197
|
+
// Per-frame context.
|
|
198
|
+
view = null;
|
|
199
|
+
layer = L_UNION;
|
|
200
|
+
famMask = 0;
|
|
201
|
+
reduced = false;
|
|
202
|
+
/** Set by style application: whether the drawn style is "changed" and its family code. */
|
|
203
|
+
curChanged = false;
|
|
204
|
+
curFamily = F_NEUTRAL;
|
|
205
|
+
/**
|
|
206
|
+
* Per-element doubles are handed to helper methods through these fields rather than as
|
|
207
|
+
* arguments: V8 boxes a double argument or return value at every non-inlined call, while a
|
|
208
|
+
* double field is updated in place.
|
|
209
|
+
*/
|
|
210
|
+
u = 0;
|
|
211
|
+
tau = 0;
|
|
212
|
+
time = 0;
|
|
213
|
+
/** `view.emphasis`, clamped: strength of the selection spotlight. */
|
|
214
|
+
emph = 1;
|
|
215
|
+
/** Relative alpha of labels outside the selected hunk and its context (fade out as emph → 1). */
|
|
216
|
+
restLabels = 1;
|
|
217
|
+
/** Label LOD thresholds for this frame (small scenes label more). */
|
|
218
|
+
lodChanged = LOD.changedLabel;
|
|
219
|
+
lodNeutral = LOD.neutralLabel;
|
|
220
|
+
lodImportant = LOD.importantNeutralLabel;
|
|
221
|
+
/** Visible-node count per focus object (cached: a focus is immutable). */
|
|
222
|
+
countedFocus = null;
|
|
223
|
+
focusCount = 0;
|
|
224
|
+
px = 0;
|
|
225
|
+
py = 0;
|
|
226
|
+
pools = new WeakMap();
|
|
227
|
+
constructor(scene) {
|
|
228
|
+
this.scene = scene;
|
|
229
|
+
this.t = tokensFor(scene.theme);
|
|
230
|
+
const N = scene.nodes.length;
|
|
231
|
+
const M = scene.edges.length;
|
|
232
|
+
const H = scene.hunks.length;
|
|
233
|
+
this.N = N;
|
|
234
|
+
this.M = M;
|
|
235
|
+
this.H = H;
|
|
236
|
+
const book = new StyleBook(this.t);
|
|
237
|
+
this.nStyles = scene.nodes.map((n) => nodeLayerStyles(book, n));
|
|
238
|
+
this.eStyles = scene.edges.map((e) => edgeLayerStyles(book, e));
|
|
239
|
+
const hunkFam = (h) => {
|
|
240
|
+
const hunk = h >= 0 ? scene.hunks[h] : undefined;
|
|
241
|
+
return hunk === undefined ? F_NEUTRAL : familyCode(hunk.family);
|
|
242
|
+
};
|
|
243
|
+
this.nFam = Uint8Array.from(scene.nodes, (n) => familyCode(n.family));
|
|
244
|
+
this.nHunk = Int32Array.from(scene.nodes, (n) => n.hunk);
|
|
245
|
+
this.nHunkFam = Uint8Array.from(scene.nodes, (n) => hunkFam(n.hunk));
|
|
246
|
+
this.eFam = Uint8Array.from(scene.edges, (e) => familyCode(e.family));
|
|
247
|
+
this.eHunk = Int32Array.from(scene.edges, (e) => e.hunk);
|
|
248
|
+
this.eHunkFam = Uint8Array.from(scene.edges, (e) => hunkFam(e.hunk));
|
|
249
|
+
this.eUnionText = scene.edges.map((e) => e.labelBefore === undefined ? e.label : `${e.labelBefore} → ${e.label}`);
|
|
250
|
+
this.ghostCurv = edgeCurvatures(scene.edges).ghost;
|
|
251
|
+
this.famBit = Uint8Array.from([1, 2, 4, 8, 16, 0]);
|
|
252
|
+
this.nx = Float64Array.from(scene.nodes, (n) => n.x);
|
|
253
|
+
this.ny = Float64Array.from(scene.nodes, (n) => n.y);
|
|
254
|
+
this.nr = Float64Array.from(scene.nodes, (n) => n.r);
|
|
255
|
+
this.nImp = Float64Array.from(scene.nodes, (n) => n.importance);
|
|
256
|
+
this.nFlow = Int32Array.from(scene.nodes, (n) => n.flow ?? -1);
|
|
257
|
+
this.nLabel = scene.nodes.map((n) => n.label);
|
|
258
|
+
this.nLabelBefore = scene.nodes.map((n) => n.labelBefore ?? null);
|
|
259
|
+
this.nBadge = scene.nodes.map((n) => n.badge ?? null);
|
|
260
|
+
this.eS = Int32Array.from(scene.edges, (e) => e.s);
|
|
261
|
+
this.eT = Int32Array.from(scene.edges, (e) => e.t);
|
|
262
|
+
this.eSB = Int32Array.from(scene.edges, (e) => e.sb);
|
|
263
|
+
this.eTB = Int32Array.from(scene.edges, (e) => e.tb);
|
|
264
|
+
this.eCurv = Float64Array.from(scene.edges, (e) => e.curvature);
|
|
265
|
+
this.eLabel = scene.edges.map((e) => e.label);
|
|
266
|
+
this.eLabelBefore = scene.edges.map((e) => e.labelBefore ?? null);
|
|
267
|
+
this.eUnchanged = Uint8Array.from(scene.edges, (e) => (e.status === 'unchanged' ? 1 : 0));
|
|
268
|
+
this.hNodes = scene.hunks.map((h) => Int32Array.from(h.nodes));
|
|
269
|
+
this.hContext = scene.hunks.map((h) => Int32Array.from(h.context));
|
|
270
|
+
this.hEdges = scene.hunks.map((h) => Int32Array.from(h.edges));
|
|
271
|
+
this.hFam = Uint8Array.from(scene.hunks, (h) => familyCode(h.family));
|
|
272
|
+
this.chapterMode = scene.timeline.mode === 'chapters';
|
|
273
|
+
this.chStart = Float64Array.from(scene.timeline.chapters, (c) => c.start);
|
|
274
|
+
this.chEnd = Float64Array.from(scene.timeline.chapters, (c) => c.end);
|
|
275
|
+
this.chHunks = scene.timeline.chapters.map((c) => Int32Array.from(c.hunks));
|
|
276
|
+
// Color ramps for crossfading styles (only where the colors differ).
|
|
277
|
+
const bg = this.t.background;
|
|
278
|
+
const ramps = this.t.ramps;
|
|
279
|
+
this.fillRamp = [];
|
|
280
|
+
this.strokeRamp = [];
|
|
281
|
+
this.ringRamp = [];
|
|
282
|
+
for (let i = 0; i < N; i++) {
|
|
283
|
+
const [, B, A] = this.nStyles[i];
|
|
284
|
+
const bFill = B.hidden ? null : B.fill;
|
|
285
|
+
const aFill = A.hidden ? null : A.fill;
|
|
286
|
+
// REMOVE drains the fill into the background; FADE ramps before → after.
|
|
287
|
+
this.fillRamp.push(!B.hidden && A.hidden
|
|
288
|
+
? ramps.get(bFill ?? bg, bg)
|
|
289
|
+
: bFill !== aFill && !B.hidden
|
|
290
|
+
? ramps.get(bFill ?? bg, aFill ?? bg)
|
|
291
|
+
: null);
|
|
292
|
+
this.strokeRamp.push(!B.hidden && !A.hidden && B.stroke !== A.stroke && B.stroke !== null && A.stroke !== null
|
|
293
|
+
? ramps.get(B.stroke, A.stroke)
|
|
294
|
+
: null);
|
|
295
|
+
this.ringRamp.push(B.ring !== null && A.ring !== null && B.ring !== A.ring ? ramps.get(B.ring, A.ring) : null);
|
|
296
|
+
}
|
|
297
|
+
this.c = choreograph(scene, this.nStyles, this.eStyles);
|
|
298
|
+
// Glyph specs and per-layer lists.
|
|
299
|
+
const specs = this.buildGlyphs();
|
|
300
|
+
this.glyphs = specs;
|
|
301
|
+
const union = [];
|
|
302
|
+
const after = [];
|
|
303
|
+
const replay = [];
|
|
304
|
+
specs.forEach((g, k) => {
|
|
305
|
+
union.push(k);
|
|
306
|
+
if (g.role === G_REPLACEMENT)
|
|
307
|
+
after.push(k);
|
|
308
|
+
if (g.role !== G_SUGGESTION)
|
|
309
|
+
replay.push(k);
|
|
310
|
+
});
|
|
311
|
+
this.glyphLists = [union, [], after, replay];
|
|
312
|
+
// Replay buffers.
|
|
313
|
+
this.wStart = new Float64Array(H);
|
|
314
|
+
this.wEnd = new Float64Array(H);
|
|
315
|
+
for (const w of scene.timeline.windows) {
|
|
316
|
+
if (w.hunk >= 0 && w.hunk < H) {
|
|
317
|
+
this.wStart[w.hunk] = w.start;
|
|
318
|
+
this.wEnd[w.hunk] = w.end;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
this.hState = new Uint8Array(H);
|
|
322
|
+
this.hP = new Float64Array(H);
|
|
323
|
+
this.levelGen = new Uint32Array(N);
|
|
324
|
+
this.level = new Uint8Array(N);
|
|
325
|
+
this.edgeGen = new Uint32Array(M);
|
|
326
|
+
this.pulseGen = new Uint32Array(N);
|
|
327
|
+
this.pulseVal = new Float32Array(N);
|
|
328
|
+
this.selNode = new Uint8Array(N);
|
|
329
|
+
}
|
|
330
|
+
// ─── Construction helpers ─────────────────────────────────────────────────
|
|
331
|
+
buildGlyphs() {
|
|
332
|
+
const { scene, t } = this;
|
|
333
|
+
const out = [];
|
|
334
|
+
// One literal shape for every spec keeps reads in the frame loop monomorphic.
|
|
335
|
+
const specs = {
|
|
336
|
+
push: (g) => {
|
|
337
|
+
out.push({
|
|
338
|
+
role: g.role,
|
|
339
|
+
kind: g.kind,
|
|
340
|
+
a: g.a,
|
|
341
|
+
b: g.b,
|
|
342
|
+
edge: g.edge,
|
|
343
|
+
node: g.node,
|
|
344
|
+
hunk: g.hunk,
|
|
345
|
+
family: g.family,
|
|
346
|
+
color: g.color,
|
|
347
|
+
alpha: g.alpha,
|
|
348
|
+
width: g.width,
|
|
349
|
+
dash: g.dash,
|
|
350
|
+
curvature: g.curvature,
|
|
351
|
+
clearA: g.clearA,
|
|
352
|
+
clearB: g.clearB,
|
|
353
|
+
label: g.label,
|
|
354
|
+
});
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
const arc = (role, kind, a, b, node, hunk) => ({
|
|
358
|
+
role,
|
|
359
|
+
kind,
|
|
360
|
+
a,
|
|
361
|
+
b,
|
|
362
|
+
edge: -1,
|
|
363
|
+
node,
|
|
364
|
+
hunk,
|
|
365
|
+
family: F_IDENTITY,
|
|
366
|
+
color: t.base[F_IDENTITY],
|
|
367
|
+
alpha: 0.9,
|
|
368
|
+
width: STROKE.glyph,
|
|
369
|
+
dash: null,
|
|
370
|
+
curvature: CHOREO.arc,
|
|
371
|
+
clearA: 4,
|
|
372
|
+
clearB: 4,
|
|
373
|
+
label: null,
|
|
374
|
+
});
|
|
375
|
+
for (const n of scene.nodes) {
|
|
376
|
+
if (n.flow === undefined)
|
|
377
|
+
continue;
|
|
378
|
+
if (n.role === 'merge-source')
|
|
379
|
+
specs.push(arc(G_TRAIL, 'merge-arrow', n.index, n.flow, n.index, n.hunk));
|
|
380
|
+
else if (n.role === 'split-target')
|
|
381
|
+
specs.push(arc(G_TRAIL, 'split-arrow', n.flow, n.index, n.index, n.hunk));
|
|
382
|
+
else if (n.role === 'deprecated')
|
|
383
|
+
specs.push(arc(G_REPLACEMENT, 'replacement-arrow', n.index, n.flow, n.index, n.hunk));
|
|
384
|
+
}
|
|
385
|
+
for (const e of scene.edges) {
|
|
386
|
+
if (e.role !== 'retarget' && e.role !== 'carried')
|
|
387
|
+
continue;
|
|
388
|
+
if (e.sb === e.s && e.tb === e.t)
|
|
389
|
+
continue;
|
|
390
|
+
const fam = e.role === 'retarget' ? F_REMOVED : F_IDENTITY;
|
|
391
|
+
specs.push({
|
|
392
|
+
role: G_GHOST,
|
|
393
|
+
kind: 'edge-ghost',
|
|
394
|
+
a: e.sb,
|
|
395
|
+
b: e.tb,
|
|
396
|
+
edge: e.index,
|
|
397
|
+
node: -1,
|
|
398
|
+
hunk: e.hunk,
|
|
399
|
+
family: familyCode(e.family),
|
|
400
|
+
color: t.edge[fam],
|
|
401
|
+
alpha: EMPHASIS.ghostEdge,
|
|
402
|
+
width: STROKE.changedEdge,
|
|
403
|
+
dash: DASH_EDGE,
|
|
404
|
+
curvature: this.ghostCurv[e.index],
|
|
405
|
+
clearA: 0,
|
|
406
|
+
clearB: 0,
|
|
407
|
+
label: null,
|
|
408
|
+
});
|
|
409
|
+
if (e.role !== 'retarget')
|
|
410
|
+
continue;
|
|
411
|
+
const pivot = e.s === e.sb ? e.s : e.t === e.tb ? e.t : -1;
|
|
412
|
+
if (pivot < 0)
|
|
413
|
+
continue;
|
|
414
|
+
specs.push({
|
|
415
|
+
role: G_PIVOT,
|
|
416
|
+
kind: 'pivot',
|
|
417
|
+
a: pivot,
|
|
418
|
+
b: pivot === e.s ? e.t : e.s,
|
|
419
|
+
edge: e.index,
|
|
420
|
+
node: -1,
|
|
421
|
+
hunk: e.hunk,
|
|
422
|
+
family: F_STRUCTURE,
|
|
423
|
+
color: t.base[F_STRUCTURE],
|
|
424
|
+
alpha: 1,
|
|
425
|
+
width: STROKE.pivot,
|
|
426
|
+
dash: null,
|
|
427
|
+
curvature: 0,
|
|
428
|
+
clearA: 0,
|
|
429
|
+
clearB: 0,
|
|
430
|
+
label: null,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
for (const s of scene.suggestions) {
|
|
434
|
+
specs.push({
|
|
435
|
+
role: G_SUGGESTION,
|
|
436
|
+
kind: 'suggestion',
|
|
437
|
+
a: s.before,
|
|
438
|
+
b: s.after,
|
|
439
|
+
edge: -1,
|
|
440
|
+
node: -1,
|
|
441
|
+
hunk: -1,
|
|
442
|
+
family: F_IDENTITY,
|
|
443
|
+
color: t.base[F_IDENTITY],
|
|
444
|
+
alpha: 0.85,
|
|
445
|
+
width: STROKE.suggestion,
|
|
446
|
+
dash: DASH_DOTTED,
|
|
447
|
+
curvature: 0.12,
|
|
448
|
+
clearA: 2,
|
|
449
|
+
clearB: 2,
|
|
450
|
+
label: '?',
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
return out;
|
|
454
|
+
}
|
|
455
|
+
newFrame() {
|
|
456
|
+
return { nodes: [], edges: [], glyphs: [], collapsed: NO_GROUPS, spotlight: 1 };
|
|
457
|
+
}
|
|
458
|
+
prepare(frame) {
|
|
459
|
+
let pool = this.pools.get(frame);
|
|
460
|
+
if (pool === undefined || pool.n !== this.N || pool.m !== this.M) {
|
|
461
|
+
const make = (n, f) => Array.from({ length: n }, f);
|
|
462
|
+
pool = {
|
|
463
|
+
n: this.N,
|
|
464
|
+
m: this.M,
|
|
465
|
+
rings: make(this.N, () => ({ color: '', width: 0, gap: 0, alpha: 0 })),
|
|
466
|
+
halos: make(this.N, () => ({ color: '', radius: 0, alpha: 0 })),
|
|
467
|
+
labels: make(this.N, newLabel),
|
|
468
|
+
secondaries: make(this.N, newLabel),
|
|
469
|
+
badges: make(this.N, () => ({ text: '', color: '', background: '', minZoom: 0 })),
|
|
470
|
+
edgeLabels: make(this.M, newLabel),
|
|
471
|
+
glyphs: make(this.glyphs.length, newGlyph),
|
|
472
|
+
glyphLabels: make(this.glyphs.length, newLabel),
|
|
473
|
+
};
|
|
474
|
+
this.pools.set(frame, pool);
|
|
475
|
+
}
|
|
476
|
+
if (frame.nodes.length !== this.N)
|
|
477
|
+
frame.nodes.length = this.N;
|
|
478
|
+
if (frame.edges.length !== this.M)
|
|
479
|
+
frame.edges.length = this.M;
|
|
480
|
+
for (let i = 0; i < this.N; i++)
|
|
481
|
+
if (frame.nodes[i] === undefined)
|
|
482
|
+
frame.nodes[i] = newNode();
|
|
483
|
+
for (let j = 0; j < this.M; j++)
|
|
484
|
+
if (frame.edges[j] === undefined)
|
|
485
|
+
frame.edges[j] = newEdge();
|
|
486
|
+
return pool;
|
|
487
|
+
}
|
|
488
|
+
// ─── Frame ────────────────────────────────────────────────────────────────
|
|
489
|
+
resolve(view, out) {
|
|
490
|
+
const frame = out ?? this.newFrame();
|
|
491
|
+
const pool = this.prepare(frame);
|
|
492
|
+
this.begin(view);
|
|
493
|
+
const nodes = frame.nodes;
|
|
494
|
+
for (let i = 0; i < this.N; i++)
|
|
495
|
+
this.resolveNode(i, nodes[i], pool);
|
|
496
|
+
const edges = frame.edges;
|
|
497
|
+
for (let j = 0; j < this.M; j++)
|
|
498
|
+
this.resolveEdge(j, edges[j], nodes, pool);
|
|
499
|
+
this.resolveGlyphs(frame, pool);
|
|
500
|
+
frame.collapsed = view.focus === null ? NO_GROUPS : view.focus.collapsed;
|
|
501
|
+
frame.spotlight = this.layer === L_REPLAY ? this.env : this.selHunk >= 0 ? this.emph : 0;
|
|
502
|
+
this.view = null;
|
|
503
|
+
return frame;
|
|
504
|
+
}
|
|
505
|
+
begin(view) {
|
|
506
|
+
this.view = view;
|
|
507
|
+
switch (view.layer) {
|
|
508
|
+
case 'union':
|
|
509
|
+
this.layer = L_UNION;
|
|
510
|
+
break;
|
|
511
|
+
case 'before':
|
|
512
|
+
this.layer = L_BEFORE;
|
|
513
|
+
break;
|
|
514
|
+
case 'after':
|
|
515
|
+
this.layer = L_AFTER;
|
|
516
|
+
break;
|
|
517
|
+
case 'replay':
|
|
518
|
+
this.layer = L_REPLAY;
|
|
519
|
+
break;
|
|
520
|
+
default:
|
|
521
|
+
throw new KgdiffError('invalid-input', `Unknown layer "${String(view.layer)}".`);
|
|
522
|
+
}
|
|
523
|
+
let mask = 0;
|
|
524
|
+
if (view.families.size > 0) {
|
|
525
|
+
for (let k = 0; k < CHANGE_FAMILIES.length; k++) {
|
|
526
|
+
if (view.families.has(CHANGE_FAMILIES[k]))
|
|
527
|
+
mask |= 1 << k;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
this.famMask = mask;
|
|
531
|
+
this.reduced = view.reducedMotion;
|
|
532
|
+
const e = view.emphasis;
|
|
533
|
+
this.emph = Number.isFinite(e) ? clamp01(e) : 1;
|
|
534
|
+
this.restLabels = 1 - this.emph;
|
|
535
|
+
const focus = view.focus;
|
|
536
|
+
if (focus !== null &&
|
|
537
|
+
(focus.nodes.length !== this.N ||
|
|
538
|
+
focus.edges.length !== this.M ||
|
|
539
|
+
focus.context.length !== this.N)) {
|
|
540
|
+
throw new KgdiffError('invalid-input', 'view.focus does not belong to this scene (array lengths differ); recompute it with computeFocus(scene).');
|
|
541
|
+
}
|
|
542
|
+
const visibleNodes = focus === null ? this.N : this.focusVisible(focus);
|
|
543
|
+
const small = visibleNodes <= LOD.smallScene;
|
|
544
|
+
this.lodChanged = small ? LOD.smallChangedLabel : LOD.changedLabel;
|
|
545
|
+
this.lodNeutral = small ? LOD.smallNeutralLabel : LOD.neutralLabel;
|
|
546
|
+
this.lodImportant = small ? LOD.smallNeutralLabel : LOD.importantNeutralLabel;
|
|
547
|
+
this.updateSelection(view.selectedHunk >= 0 && view.selectedHunk < this.H ? view.selectedHunk : -1);
|
|
548
|
+
this.env = 0;
|
|
549
|
+
if (this.layer === L_REPLAY) {
|
|
550
|
+
this.time = Number.isFinite(view.t) ? view.t : 0;
|
|
551
|
+
this.updateReplay();
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
focusVisible(focus) {
|
|
555
|
+
if (focus !== this.countedFocus) {
|
|
556
|
+
let n = 0;
|
|
557
|
+
const nodes = focus.nodes;
|
|
558
|
+
for (let i = 0; i < nodes.length; i++)
|
|
559
|
+
if (nodes[i] === 1)
|
|
560
|
+
n++;
|
|
561
|
+
this.countedFocus = focus;
|
|
562
|
+
this.focusCount = n;
|
|
563
|
+
}
|
|
564
|
+
return this.focusCount;
|
|
565
|
+
}
|
|
566
|
+
updateSelection(h) {
|
|
567
|
+
if (h === this.selHunk)
|
|
568
|
+
return;
|
|
569
|
+
this.selNode.fill(0);
|
|
570
|
+
if (h >= 0) {
|
|
571
|
+
const context = this.hContext[h];
|
|
572
|
+
for (let k = 0; k < context.length; k++)
|
|
573
|
+
this.selNode[context[k]] = 1;
|
|
574
|
+
const nodes = this.hNodes[h];
|
|
575
|
+
for (let k = 0; k < nodes.length; k++)
|
|
576
|
+
this.selNode[nodes[k]] = 2;
|
|
577
|
+
}
|
|
578
|
+
this.selHunk = h;
|
|
579
|
+
}
|
|
580
|
+
updateReplay() {
|
|
581
|
+
const { H, hState, hP, wStart, wEnd } = this;
|
|
582
|
+
const t = this.time;
|
|
583
|
+
let active = -1;
|
|
584
|
+
for (let h = 0; h < H; h++) {
|
|
585
|
+
const s = wStart[h];
|
|
586
|
+
const e = wEnd[h];
|
|
587
|
+
if (t < s)
|
|
588
|
+
hState[h] = 0;
|
|
589
|
+
else if (t >= e)
|
|
590
|
+
hState[h] = 2;
|
|
591
|
+
else {
|
|
592
|
+
hState[h] = 1;
|
|
593
|
+
hP[h] = (t - s) / (e - s);
|
|
594
|
+
active = h;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
this.gen = (this.gen + 1) >>> 0;
|
|
598
|
+
if (this.gen === 0) {
|
|
599
|
+
this.levelGen.fill(0);
|
|
600
|
+
this.edgeGen.fill(0);
|
|
601
|
+
this.pulseGen.fill(0);
|
|
602
|
+
this.gen = 1;
|
|
603
|
+
}
|
|
604
|
+
if (!this.chapterMode) {
|
|
605
|
+
if (active >= 0) {
|
|
606
|
+
// Ramps in over the spotlight phase and out over the settle phase.
|
|
607
|
+
const p = hP[active];
|
|
608
|
+
this.env =
|
|
609
|
+
p < SPOT_END
|
|
610
|
+
? easeStandard(p / SPOT_END)
|
|
611
|
+
: p > SETTLE
|
|
612
|
+
? easeStandard((1 - p) / (1 - SETTLE))
|
|
613
|
+
: 1;
|
|
614
|
+
this.markHunk(active);
|
|
615
|
+
}
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
// Chapters: the whole chapter is spotlighted, ramping over its first window's spotlight phase.
|
|
619
|
+
for (let c = 0; c < this.chHunks.length; c++) {
|
|
620
|
+
const start = this.chStart[c];
|
|
621
|
+
const end = this.chEnd[c];
|
|
622
|
+
if (t < start || t >= end)
|
|
623
|
+
continue;
|
|
624
|
+
const hunks = this.chHunks[c];
|
|
625
|
+
const first = hunks.length > 0 ? hunks[0] : -1;
|
|
626
|
+
const ramp = first < 0 ? 1 : SPOT_END * (wEnd[first] - wStart[first]);
|
|
627
|
+
this.env = easeStandard(clamp01(Math.min(t - start, end - t) / Math.max(ramp, 1e-9)));
|
|
628
|
+
for (let k = 0; k < hunks.length; k++)
|
|
629
|
+
this.markHunk(hunks[k]);
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
/** Emphasize a hunk for the replay spotlight and schedule its pulse. */
|
|
634
|
+
markHunk(h) {
|
|
635
|
+
const context = this.hContext[h];
|
|
636
|
+
const nodes = this.hNodes[h];
|
|
637
|
+
const edges = this.hEdges[h];
|
|
638
|
+
if (context === undefined || nodes === undefined || edges === undefined)
|
|
639
|
+
return;
|
|
640
|
+
const { gen, levelGen, level, pulseGen, pulseVal } = this;
|
|
641
|
+
const pulsing = this.hState[h] === 1 && this.hP[h] < SPOT_END && !this.reduced;
|
|
642
|
+
const pulse = pulsing ? this.hP[h] / SPOT_END : 0;
|
|
643
|
+
for (let k = 0; k < context.length; k++) {
|
|
644
|
+
const i = context[k];
|
|
645
|
+
if (levelGen[i] !== gen) {
|
|
646
|
+
levelGen[i] = gen;
|
|
647
|
+
level[i] = 1;
|
|
648
|
+
}
|
|
649
|
+
if (pulsing && this.c.hPulseContext[h] === 1) {
|
|
650
|
+
pulseGen[i] = gen;
|
|
651
|
+
pulseVal[i] = pulse;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
for (let k = 0; k < nodes.length; k++) {
|
|
655
|
+
const i = nodes[k];
|
|
656
|
+
levelGen[i] = gen;
|
|
657
|
+
level[i] = 2;
|
|
658
|
+
if (pulsing && this.c.nPulse[i] === 1) {
|
|
659
|
+
pulseGen[i] = gen;
|
|
660
|
+
pulseVal[i] = pulse;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
for (let k = 0; k < edges.length; k++)
|
|
664
|
+
this.edgeGen[edges[k]] = gen;
|
|
665
|
+
}
|
|
666
|
+
/** Spotlight level of a node: 2 emphasized hunk, 1 its context, 0 the rest. */
|
|
667
|
+
nodeLevel(i) {
|
|
668
|
+
if (this.layer === L_REPLAY)
|
|
669
|
+
return this.levelGen[i] === this.gen ? this.level[i] : 0;
|
|
670
|
+
return this.selNode[i];
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Alpha factor of the spotlight for an element at `level`: the replay spotlight ramps with its
|
|
674
|
+
* window envelope, the selection spotlight with `view.emphasis`.
|
|
675
|
+
*/
|
|
676
|
+
spotlight(level) {
|
|
677
|
+
if (this.layer === L_REPLAY) {
|
|
678
|
+
if (this.env <= 0 || level === 2)
|
|
679
|
+
return 1;
|
|
680
|
+
return lerp(1, level === 1 ? EMPHASIS.replayContext : EMPHASIS.replayRest, this.env);
|
|
681
|
+
}
|
|
682
|
+
if (this.selHunk < 0 || level === 2)
|
|
683
|
+
return 1;
|
|
684
|
+
return lerp(1, level === 1 ? EMPHASIS.selectedContext : EMPHASIS.selectedRest, this.emph);
|
|
685
|
+
}
|
|
686
|
+
/** Labels outside the selected hunk and its context fade out with the selection spotlight. */
|
|
687
|
+
fadesRestLabels(level) {
|
|
688
|
+
return this.layer !== L_REPLAY && this.selHunk >= 0 && level === 0;
|
|
689
|
+
}
|
|
690
|
+
filtered(fam, hunkFam) {
|
|
691
|
+
const mask = this.famMask;
|
|
692
|
+
return (mask !== 0 &&
|
|
693
|
+
(this.famBit[fam] & mask) === 0 &&
|
|
694
|
+
(this.famBit[hunkFam] & mask) === 0);
|
|
695
|
+
}
|
|
696
|
+
// ─── Nodes ────────────────────────────────────────────────────────────────
|
|
697
|
+
resolveNode(i, v, pool) {
|
|
698
|
+
v.x = this.nx[i];
|
|
699
|
+
v.y = this.ny[i];
|
|
700
|
+
v.r = this.nr[i];
|
|
701
|
+
v.family = FAMILY_BY_CODE[this.nFam[i]];
|
|
702
|
+
v.pulse = 0;
|
|
703
|
+
v.strokeWidth = STROKE.node;
|
|
704
|
+
const styles = this.nStyles[i];
|
|
705
|
+
let ok;
|
|
706
|
+
if (this.layer !== L_REPLAY)
|
|
707
|
+
ok = this.applyNode(v, i, styles[this.layer], pool);
|
|
708
|
+
else {
|
|
709
|
+
const h = this.nHunk[i];
|
|
710
|
+
const state = h < 0 ? 2 : this.hState[h];
|
|
711
|
+
if (state === 0)
|
|
712
|
+
ok = this.applyNode(v, i, styles[L_BEFORE], pool);
|
|
713
|
+
else if (state === 2)
|
|
714
|
+
ok = this.applyNode(v, i, styles[L_AFTER], pool);
|
|
715
|
+
else {
|
|
716
|
+
const p = this.hP[h];
|
|
717
|
+
if (p < SPOT_END)
|
|
718
|
+
ok = this.applyNode(v, i, styles[L_BEFORE], pool);
|
|
719
|
+
else if (p >= SETTLE)
|
|
720
|
+
ok = this.applyNode(v, i, styles[L_AFTER], pool);
|
|
721
|
+
else {
|
|
722
|
+
this.u = (p - SPOT_END) / TRANSFORM;
|
|
723
|
+
ok = this.animateNode(v, i, styles, pool);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
if (ok && this.pulseGen[i] === this.gen)
|
|
727
|
+
v.pulse = this.pulseVal[i];
|
|
728
|
+
}
|
|
729
|
+
if (!ok) {
|
|
730
|
+
hideNode(v);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
this.modulateNode(v, i, pool);
|
|
734
|
+
}
|
|
735
|
+
/** Copy a static style into `v` (position / radius already set). Returns visibility. */
|
|
736
|
+
applyNode(v, i, st, pool) {
|
|
737
|
+
if (st.hidden)
|
|
738
|
+
return false;
|
|
739
|
+
const t = this.t;
|
|
740
|
+
const before = this.nLabelBefore[i] ?? null;
|
|
741
|
+
v.visible = true;
|
|
742
|
+
v.alpha = 1;
|
|
743
|
+
v.family = FAMILY_BY_CODE[st.family];
|
|
744
|
+
v.fill = st.fill;
|
|
745
|
+
v.stroke = st.stroke;
|
|
746
|
+
v.dash = st.dash;
|
|
747
|
+
v.z = st.z;
|
|
748
|
+
if (st.ring !== null) {
|
|
749
|
+
const ring = pool.rings[i];
|
|
750
|
+
ring.color = st.ring;
|
|
751
|
+
ring.width = STROKE.ring;
|
|
752
|
+
ring.gap = STROKE.ringGap;
|
|
753
|
+
ring.alpha = 1;
|
|
754
|
+
v.ring = ring;
|
|
755
|
+
}
|
|
756
|
+
else
|
|
757
|
+
v.ring = null;
|
|
758
|
+
if (st.halo !== null) {
|
|
759
|
+
const halo = pool.halos[i];
|
|
760
|
+
halo.color = st.halo;
|
|
761
|
+
halo.radius = v.r * t.haloScale;
|
|
762
|
+
halo.alpha = t.haloAlpha;
|
|
763
|
+
v.halo = halo;
|
|
764
|
+
}
|
|
765
|
+
else
|
|
766
|
+
v.halo = null;
|
|
767
|
+
const label = pool.labels[i];
|
|
768
|
+
label.text = st.labelBefore && before !== null ? before : this.nLabel[i];
|
|
769
|
+
label.color = st.labelColor;
|
|
770
|
+
label.alpha = 1;
|
|
771
|
+
label.size = LABEL.size;
|
|
772
|
+
label.weight = st.labelWeight;
|
|
773
|
+
label.strike = st.strike;
|
|
774
|
+
label.offset = LABEL.offset;
|
|
775
|
+
v.label = label;
|
|
776
|
+
if (st.secondary && before !== null) {
|
|
777
|
+
const sec = pool.secondaries[i];
|
|
778
|
+
sec.text = before;
|
|
779
|
+
sec.color = t.muted;
|
|
780
|
+
sec.alpha = 1;
|
|
781
|
+
sec.size = LABEL.oldSize;
|
|
782
|
+
sec.weight = t.weightRegular;
|
|
783
|
+
sec.strike = true;
|
|
784
|
+
sec.offset = LABEL.offset + LABEL.secondaryGap;
|
|
785
|
+
v.secondaryLabel = sec;
|
|
786
|
+
}
|
|
787
|
+
else
|
|
788
|
+
v.secondaryLabel = null;
|
|
789
|
+
const badge = this.nBadge[i] ?? null;
|
|
790
|
+
if (st.badge && badge !== null)
|
|
791
|
+
this.setBadge(v, i, badge, st.family, pool);
|
|
792
|
+
else
|
|
793
|
+
v.badge = null;
|
|
794
|
+
this.curChanged = st.changed;
|
|
795
|
+
this.curFamily = st.family;
|
|
796
|
+
return true;
|
|
797
|
+
}
|
|
798
|
+
setBadge(v, i, text, family, pool) {
|
|
799
|
+
const badge = pool.badges[i];
|
|
800
|
+
badge.text = text;
|
|
801
|
+
badge.color = this.t.text[family];
|
|
802
|
+
badge.background = this.t.soft[family];
|
|
803
|
+
v.badge = badge;
|
|
804
|
+
}
|
|
805
|
+
animateNode(v, i, styles, pool) {
|
|
806
|
+
const c = this.c;
|
|
807
|
+
const u = this.u;
|
|
808
|
+
const kind = c.nKind[i];
|
|
809
|
+
const B = styles[L_BEFORE];
|
|
810
|
+
const A = styles[L_AFTER];
|
|
811
|
+
const reduced = this.reduced;
|
|
812
|
+
switch (kind) {
|
|
813
|
+
case NA_FADE:
|
|
814
|
+
return this.animFade(v, i, B, A, pool);
|
|
815
|
+
case NA_REMOVE:
|
|
816
|
+
case NA_TRAVEL: {
|
|
817
|
+
if (!this.applyNode(v, i, B, pool))
|
|
818
|
+
return false;
|
|
819
|
+
if (reduced) {
|
|
820
|
+
v.alpha = 1 - easeStandard(u);
|
|
821
|
+
return v.alpha > 0;
|
|
822
|
+
}
|
|
823
|
+
return kind === NA_REMOVE ? this.animRemove(v, i, B) : this.animTravel(v, i);
|
|
824
|
+
}
|
|
825
|
+
case NA_ADD:
|
|
826
|
+
case NA_EMERGE: {
|
|
827
|
+
if (!this.applyNode(v, i, A, pool))
|
|
828
|
+
return false;
|
|
829
|
+
if (reduced) {
|
|
830
|
+
v.alpha = easeStandard(u);
|
|
831
|
+
return v.alpha > 0;
|
|
832
|
+
}
|
|
833
|
+
return kind === NA_ADD ? this.animAdd(v, i) : this.animEmerge(v, i);
|
|
834
|
+
}
|
|
835
|
+
case NA_NONE:
|
|
836
|
+
return this.applyNode(v, i, u < 0.5 ? B : A, pool);
|
|
837
|
+
default:
|
|
838
|
+
return this.applyNode(v, i, A, pool);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
animFade(v, i, B, A, pool) {
|
|
842
|
+
const c = this.c;
|
|
843
|
+
const u = this.u;
|
|
844
|
+
const labelBefore = this.nLabelBefore[i] ?? null;
|
|
845
|
+
const badge = this.nBadge[i] ?? null;
|
|
846
|
+
const reduced = this.reduced;
|
|
847
|
+
const k = reduced
|
|
848
|
+
? easeStandard(u)
|
|
849
|
+
: easeStandard(segment(u, c.nA[i], c.nB[i]));
|
|
850
|
+
if (!this.applyNode(v, i, k < 0.5 ? B : A, pool))
|
|
851
|
+
return false;
|
|
852
|
+
const t = this.t;
|
|
853
|
+
// Colors.
|
|
854
|
+
const fillRamp = this.fillRamp[i] ?? null;
|
|
855
|
+
if (fillRamp !== null && k > 0 && k < 1)
|
|
856
|
+
v.fill = rampAt(fillRamp, k);
|
|
857
|
+
const strokeRamp = this.strokeRamp[i] ?? null;
|
|
858
|
+
if (strokeRamp !== null && k > 0 && k < 1)
|
|
859
|
+
v.stroke = rampAt(strokeRamp, k);
|
|
860
|
+
// Ring: color ramp when both have one, else fade in / out.
|
|
861
|
+
const ring = pool.rings[i];
|
|
862
|
+
if (B.ring !== null || A.ring !== null) {
|
|
863
|
+
const ringRamp = this.ringRamp[i] ?? null;
|
|
864
|
+
ring.color =
|
|
865
|
+
A.ring !== null && B.ring !== null
|
|
866
|
+
? ringRamp !== null
|
|
867
|
+
? rampAt(ringRamp, k)
|
|
868
|
+
: A.ring
|
|
869
|
+
: (A.ring ?? B.ring ?? '');
|
|
870
|
+
ring.width = STROKE.ring;
|
|
871
|
+
ring.gap = STROKE.ringGap;
|
|
872
|
+
ring.alpha = A.ring !== null && B.ring !== null ? 1 : A.ring !== null ? k : 1 - k;
|
|
873
|
+
v.ring = ring.alpha > 0 ? ring : null;
|
|
874
|
+
}
|
|
875
|
+
if (!reduced && c.nFlashA[i] >= 0 && v.ring !== null) {
|
|
876
|
+
const f = bump(segment(u, c.nFlashA[i], c.nFlashB[i]));
|
|
877
|
+
ring.width = STROKE.ring + STROKE.ring * f;
|
|
878
|
+
ring.alpha = Math.max(ring.alpha, f);
|
|
879
|
+
}
|
|
880
|
+
// Halo.
|
|
881
|
+
if (B.halo !== null || A.halo !== null) {
|
|
882
|
+
const halo = pool.halos[i];
|
|
883
|
+
halo.color = (A.halo ?? B.halo);
|
|
884
|
+
halo.radius = v.r * t.haloScale;
|
|
885
|
+
halo.alpha =
|
|
886
|
+
t.haloAlpha * (A.halo !== null && B.halo !== null ? 1 : A.halo !== null ? k : 1 - k);
|
|
887
|
+
v.halo = halo.alpha > 0 ? halo : null;
|
|
888
|
+
}
|
|
889
|
+
// Label: the old name slides up and fades while the new one rises in (crossfade in place
|
|
890
|
+
// with reduced motion).
|
|
891
|
+
if (c.nLabelSwap[i] === 1 && labelBefore !== null) {
|
|
892
|
+
const kOut = reduced ? k : easeStandard(segment(u, CHOREO.labelOut[0], CHOREO.labelOut[1]));
|
|
893
|
+
const kIn = reduced ? k : easeStandard(segment(u, CHOREO.labelIn[0], CHOREO.labelIn[1]));
|
|
894
|
+
const label = pool.labels[i];
|
|
895
|
+
label.text = this.nLabel[i];
|
|
896
|
+
label.color = A.labelColor;
|
|
897
|
+
label.weight = A.labelWeight;
|
|
898
|
+
label.strike = A.strike;
|
|
899
|
+
label.alpha = kIn;
|
|
900
|
+
label.offset = LABEL.offset + (reduced ? 0 : CHOREO.labelSlide * (1 - kIn));
|
|
901
|
+
v.label = label;
|
|
902
|
+
const old = pool.secondaries[i];
|
|
903
|
+
old.text = labelBefore;
|
|
904
|
+
old.color = B.labelColor;
|
|
905
|
+
old.size = LABEL.size;
|
|
906
|
+
old.weight = B.labelWeight;
|
|
907
|
+
old.strike = false;
|
|
908
|
+
old.alpha = 1 - kOut;
|
|
909
|
+
old.offset = LABEL.offset - (reduced ? 0 : CHOREO.labelSlide * kOut);
|
|
910
|
+
v.secondaryLabel = old.alpha > 0 ? old : null;
|
|
911
|
+
}
|
|
912
|
+
// Badge: ticks in; merge targets count their arrivals up.
|
|
913
|
+
const arrivals = c.nArrivals[i] ?? null;
|
|
914
|
+
const steps = c.nBadgeSteps[i] ?? null;
|
|
915
|
+
if (arrivals !== null && steps !== null) {
|
|
916
|
+
let count = 0;
|
|
917
|
+
if (reduced)
|
|
918
|
+
count = k >= 0.5 ? arrivals.length : 0;
|
|
919
|
+
else
|
|
920
|
+
while (count < arrivals.length && arrivals[count] <= u)
|
|
921
|
+
count++;
|
|
922
|
+
if (count > 0)
|
|
923
|
+
this.setBadge(v, i, steps[count - 1], A.family, pool);
|
|
924
|
+
else
|
|
925
|
+
v.badge = null;
|
|
926
|
+
}
|
|
927
|
+
else if (c.nBadgeAt[i] <= 1 && badge !== null) {
|
|
928
|
+
if (reduced ? k >= 0.5 : u >= c.nBadgeAt[i])
|
|
929
|
+
this.setBadge(v, i, badge, A.family, pool);
|
|
930
|
+
else
|
|
931
|
+
v.badge = null;
|
|
932
|
+
}
|
|
933
|
+
return true;
|
|
934
|
+
}
|
|
935
|
+
animRemove(v, i, B) {
|
|
936
|
+
const c = this.c;
|
|
937
|
+
const u = this.u;
|
|
938
|
+
const s = easeStandard(segment(u, c.nA[i], c.nB[i]));
|
|
939
|
+
if (s > 0) {
|
|
940
|
+
v.r = this.nr[i] * (1 - 0.4 * s);
|
|
941
|
+
const ramp = this.fillRamp[i] ?? null;
|
|
942
|
+
v.fill = s >= 1 || ramp === null ? null : rampAt(ramp, s);
|
|
943
|
+
v.dash = DASH_GHOST;
|
|
944
|
+
v.stroke = this.t.base[B.family];
|
|
945
|
+
if (v.label !== null)
|
|
946
|
+
v.label.strike = true;
|
|
947
|
+
if (v.ring !== null)
|
|
948
|
+
v.ring.alpha = 1 - s;
|
|
949
|
+
if (v.halo !== null) {
|
|
950
|
+
v.halo.alpha *= 1 - s;
|
|
951
|
+
v.halo.radius = v.r * this.t.haloScale;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
v.alpha = 1 - easeStandard(segment(u, c.nC[i], 1));
|
|
955
|
+
return v.alpha > 0;
|
|
956
|
+
}
|
|
957
|
+
/** Point at `this.tau` on the travel arc from node `from` to node `to`, into (px, py). */
|
|
958
|
+
arcPoint(from, to) {
|
|
959
|
+
const { nx, ny } = this;
|
|
960
|
+
const x0 = nx[from];
|
|
961
|
+
const y0 = ny[from];
|
|
962
|
+
const x1 = nx[to];
|
|
963
|
+
const y1 = ny[to];
|
|
964
|
+
const tau = this.tau;
|
|
965
|
+
const cx = (x0 + x1) / 2 - CHOREO.arc * (y1 - y0);
|
|
966
|
+
const cy = (y0 + y1) / 2 + CHOREO.arc * (x1 - x0);
|
|
967
|
+
const a = (1 - tau) * (1 - tau);
|
|
968
|
+
const b = 2 * (1 - tau) * tau;
|
|
969
|
+
const d = tau * tau;
|
|
970
|
+
this.px = a * x0 + b * cx + d * x1;
|
|
971
|
+
this.py = a * y0 + b * cy + d * y1;
|
|
972
|
+
}
|
|
973
|
+
animTravel(v, i) {
|
|
974
|
+
const c = this.c;
|
|
975
|
+
const u = this.u;
|
|
976
|
+
const target = this.nFlow[i];
|
|
977
|
+
if (target < 0)
|
|
978
|
+
return true;
|
|
979
|
+
const a = c.nA[i];
|
|
980
|
+
const b = c.nB[i];
|
|
981
|
+
this.tau = easeInOut(segment(u, a, b));
|
|
982
|
+
this.arcPoint(i, target);
|
|
983
|
+
v.x = this.px;
|
|
984
|
+
v.y = this.py;
|
|
985
|
+
v.z = 3;
|
|
986
|
+
v.alpha = 1 - segment(u, b, b + CHOREO.arrivalFade);
|
|
987
|
+
if (v.label !== null)
|
|
988
|
+
v.label.alpha = 1 - segment(u, (a + b) / 2, b);
|
|
989
|
+
if (v.halo !== null)
|
|
990
|
+
v.halo.radius = v.r * this.t.haloScale;
|
|
991
|
+
return v.alpha > 0;
|
|
992
|
+
}
|
|
993
|
+
animAdd(v, i) {
|
|
994
|
+
const c = this.c;
|
|
995
|
+
const u = this.u;
|
|
996
|
+
const pop = segment(u, c.nA[i], c.nB[i]);
|
|
997
|
+
v.r = pop > 0 ? this.nr[i] * backOut(pop) : 0;
|
|
998
|
+
if (pop <= 0)
|
|
999
|
+
return false;
|
|
1000
|
+
v.alpha = Math.min(1, pop * 4);
|
|
1001
|
+
if (v.halo !== null)
|
|
1002
|
+
v.halo.radius = v.r * this.t.haloScale;
|
|
1003
|
+
const labelStart = c.nC[i];
|
|
1004
|
+
const la = easeStandard(segment(u, labelStart, Math.min(1, labelStart + CHOREO.labelFadeIn)));
|
|
1005
|
+
if (v.label !== null)
|
|
1006
|
+
v.label.alpha = la;
|
|
1007
|
+
if (v.secondaryLabel !== null)
|
|
1008
|
+
v.secondaryLabel.alpha = la;
|
|
1009
|
+
if (la < 1)
|
|
1010
|
+
v.badge = null;
|
|
1011
|
+
return true;
|
|
1012
|
+
}
|
|
1013
|
+
animEmerge(v, i) {
|
|
1014
|
+
const c = this.c;
|
|
1015
|
+
const u = this.u;
|
|
1016
|
+
const source = this.nFlow[i];
|
|
1017
|
+
if (source < 0)
|
|
1018
|
+
return true;
|
|
1019
|
+
const a = c.nA[i];
|
|
1020
|
+
const b = c.nB[i];
|
|
1021
|
+
const tau = easeInOut(segment(u, a, b));
|
|
1022
|
+
this.tau = tau;
|
|
1023
|
+
this.arcPoint(source, i);
|
|
1024
|
+
v.x = this.px;
|
|
1025
|
+
v.y = this.py;
|
|
1026
|
+
v.r = this.nr[i] * (0.5 + 0.5 * tau);
|
|
1027
|
+
v.z = 3;
|
|
1028
|
+
v.alpha = segment(u, a, a + 0.1);
|
|
1029
|
+
if (v.halo !== null)
|
|
1030
|
+
v.halo.radius = v.r * this.t.haloScale;
|
|
1031
|
+
const la = easeStandard(segment(u, b - 0.05, Math.min(1, b + CHOREO.labelFadeIn)));
|
|
1032
|
+
if (v.label !== null)
|
|
1033
|
+
v.label.alpha = la;
|
|
1034
|
+
if (v.secondaryLabel !== null)
|
|
1035
|
+
v.secondaryLabel.alpha = la;
|
|
1036
|
+
if (la < 1)
|
|
1037
|
+
v.badge = null;
|
|
1038
|
+
return v.alpha > 0;
|
|
1039
|
+
}
|
|
1040
|
+
modulateNode(v, i, pool) {
|
|
1041
|
+
const view = this.view;
|
|
1042
|
+
const t = this.t;
|
|
1043
|
+
let f = 1;
|
|
1044
|
+
const focus = view.focus;
|
|
1045
|
+
if (focus !== null) {
|
|
1046
|
+
if (focus.nodes[i] !== 1) {
|
|
1047
|
+
hideNode(v);
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
if (focus.context[i] === 1)
|
|
1051
|
+
f *= EMPHASIS.focusContext;
|
|
1052
|
+
}
|
|
1053
|
+
let changed = this.curChanged;
|
|
1054
|
+
let family = this.curFamily;
|
|
1055
|
+
let neutral = false;
|
|
1056
|
+
if (this.filtered(this.nFam[i], this.nHunkFam[i])) {
|
|
1057
|
+
neutral = true;
|
|
1058
|
+
changed = false;
|
|
1059
|
+
family = F_NEUTRAL;
|
|
1060
|
+
v.family = 'neutral';
|
|
1061
|
+
if (v.fill !== null)
|
|
1062
|
+
v.fill = t.base[F_NEUTRAL];
|
|
1063
|
+
v.stroke = t.strong[F_NEUTRAL];
|
|
1064
|
+
v.ring = null;
|
|
1065
|
+
v.halo = null;
|
|
1066
|
+
v.badge = null;
|
|
1067
|
+
v.secondaryLabel = null;
|
|
1068
|
+
if (v.label !== null) {
|
|
1069
|
+
v.label.color = t.neutralLabel;
|
|
1070
|
+
v.label.weight = t.weightRegular;
|
|
1071
|
+
}
|
|
1072
|
+
f *= EMPHASIS.filtered;
|
|
1073
|
+
}
|
|
1074
|
+
const level = this.nodeLevel(i);
|
|
1075
|
+
f *= this.spotlight(level);
|
|
1076
|
+
const selLevel = this.selHunk >= 0 ? this.selNode[i] : 0;
|
|
1077
|
+
const fadeLabels = this.fadesRestLabels(selLevel);
|
|
1078
|
+
// The selected hunk's elements get halos (fading in with the emphasis).
|
|
1079
|
+
if (selLevel === 2 && !neutral && v.halo === null && this.emph > 0) {
|
|
1080
|
+
const halo = pool.halos[i];
|
|
1081
|
+
halo.color = t.base[family];
|
|
1082
|
+
halo.radius = v.r * t.haloScale;
|
|
1083
|
+
halo.alpha = t.haloAlpha * this.emph;
|
|
1084
|
+
v.halo = halo;
|
|
1085
|
+
}
|
|
1086
|
+
const hovered = view.hoveredNode === i;
|
|
1087
|
+
const picked = view.selectedNode === i;
|
|
1088
|
+
if (hovered) {
|
|
1089
|
+
v.strokeWidth = STROKE.hoveredNode;
|
|
1090
|
+
v.stroke = changed ? t.strong[family] : t.secondary;
|
|
1091
|
+
v.z = 4;
|
|
1092
|
+
}
|
|
1093
|
+
if (picked) {
|
|
1094
|
+
const ring = pool.rings[i];
|
|
1095
|
+
ring.color = t.accent;
|
|
1096
|
+
ring.width = STROKE.hoveredNode;
|
|
1097
|
+
ring.gap = STROKE.ringGap;
|
|
1098
|
+
ring.alpha = 1;
|
|
1099
|
+
v.ring = ring;
|
|
1100
|
+
v.z = 4;
|
|
1101
|
+
}
|
|
1102
|
+
v.alpha *= f;
|
|
1103
|
+
// LOD and collision priority.
|
|
1104
|
+
const imp = this.nImp[i];
|
|
1105
|
+
let minZoom = changed
|
|
1106
|
+
? this.lodChanged
|
|
1107
|
+
: imp >= LOD.importantThreshold
|
|
1108
|
+
? this.lodImportant
|
|
1109
|
+
: this.lodNeutral;
|
|
1110
|
+
let priority = changed ? 1 + imp : imp;
|
|
1111
|
+
if (hovered) {
|
|
1112
|
+
minZoom = 0;
|
|
1113
|
+
priority = 2 + imp;
|
|
1114
|
+
}
|
|
1115
|
+
if (selLevel === 2 || picked) {
|
|
1116
|
+
minZoom = 0;
|
|
1117
|
+
priority = 3 + imp;
|
|
1118
|
+
}
|
|
1119
|
+
if (fadeLabels && !hovered && !picked) {
|
|
1120
|
+
const rest = this.restLabels;
|
|
1121
|
+
if (rest <= 0) {
|
|
1122
|
+
v.label = null;
|
|
1123
|
+
v.secondaryLabel = null;
|
|
1124
|
+
}
|
|
1125
|
+
else {
|
|
1126
|
+
if (v.label !== null)
|
|
1127
|
+
v.label.alpha *= rest;
|
|
1128
|
+
if (v.secondaryLabel !== null)
|
|
1129
|
+
v.secondaryLabel.alpha *= rest;
|
|
1130
|
+
}
|
|
1131
|
+
// Badges have no alpha of their own: drop them halfway through the fade.
|
|
1132
|
+
if (rest < 0.5)
|
|
1133
|
+
v.badge = null;
|
|
1134
|
+
}
|
|
1135
|
+
// Nested alphas stay relative to v.alpha (renderers multiply).
|
|
1136
|
+
if (v.label !== null) {
|
|
1137
|
+
v.label.minZoom = minZoom;
|
|
1138
|
+
v.label.priority = priority;
|
|
1139
|
+
}
|
|
1140
|
+
if (v.secondaryLabel !== null) {
|
|
1141
|
+
v.secondaryLabel.minZoom = minZoom;
|
|
1142
|
+
v.secondaryLabel.priority = priority - 0.5;
|
|
1143
|
+
}
|
|
1144
|
+
if (v.badge !== null)
|
|
1145
|
+
v.badge.minZoom = selLevel === 2 ? 0 : LOD.badge;
|
|
1146
|
+
}
|
|
1147
|
+
// ─── Edges ────────────────────────────────────────────────────────────────
|
|
1148
|
+
resolveEdge(j, ev, nodes, pool) {
|
|
1149
|
+
const styles = this.eStyles[j];
|
|
1150
|
+
let ok;
|
|
1151
|
+
if (this.layer !== L_REPLAY)
|
|
1152
|
+
ok = this.applyEdge(ev, j, styles[this.layer], nodes, pool);
|
|
1153
|
+
else {
|
|
1154
|
+
const h = this.eHunk[j];
|
|
1155
|
+
const state = h < 0 ? 2 : this.hState[h];
|
|
1156
|
+
if (state === 0)
|
|
1157
|
+
ok = this.applyEdge(ev, j, styles[L_BEFORE], nodes, pool);
|
|
1158
|
+
else if (state === 2)
|
|
1159
|
+
ok = this.applyEdge(ev, j, styles[L_AFTER], nodes, pool);
|
|
1160
|
+
else {
|
|
1161
|
+
const p = this.hP[h];
|
|
1162
|
+
if (p < SPOT_END)
|
|
1163
|
+
ok = this.applyEdge(ev, j, styles[L_BEFORE], nodes, pool);
|
|
1164
|
+
else if (p >= SETTLE)
|
|
1165
|
+
ok = this.applyEdge(ev, j, styles[L_AFTER], nodes, pool);
|
|
1166
|
+
else {
|
|
1167
|
+
this.u = (p - SPOT_END) / TRANSFORM;
|
|
1168
|
+
ok = this.animateEdge(ev, j, styles, nodes, pool);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
if (!ok) {
|
|
1173
|
+
hideEdge(ev);
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
this.modulateEdge(ev, j, pool);
|
|
1177
|
+
}
|
|
1178
|
+
setEnds(ev, a, b) {
|
|
1179
|
+
ev.x1 = a.x;
|
|
1180
|
+
ev.y1 = a.y;
|
|
1181
|
+
ev.r1 = a.r;
|
|
1182
|
+
ev.x2 = b.x;
|
|
1183
|
+
ev.y2 = b.y;
|
|
1184
|
+
ev.r2 = b.r;
|
|
1185
|
+
}
|
|
1186
|
+
applyEdge(ev, j, st, nodes, pool) {
|
|
1187
|
+
if (st.hidden)
|
|
1188
|
+
return false;
|
|
1189
|
+
const es = this.eS[j];
|
|
1190
|
+
const et = this.eT[j];
|
|
1191
|
+
const esb = this.eSB[j];
|
|
1192
|
+
const etb = this.eTB[j];
|
|
1193
|
+
const before = st.before && (esb !== es || etb !== et);
|
|
1194
|
+
this.setEnds(ev, nodes[before ? esb : es], nodes[before ? etb : et]);
|
|
1195
|
+
ev.curvature = (before ? this.ghostCurv[j] : this.eCurv[j]);
|
|
1196
|
+
ev.visible = true;
|
|
1197
|
+
ev.color = st.color;
|
|
1198
|
+
ev.width = st.width;
|
|
1199
|
+
ev.alpha = st.alpha;
|
|
1200
|
+
ev.dash = st.dash;
|
|
1201
|
+
ev.dashOffset = 0;
|
|
1202
|
+
ev.arrow = st.arrow;
|
|
1203
|
+
ev.arrowMinZoom = st.changed ? 0 : LOD.arrow;
|
|
1204
|
+
ev.progress = 1;
|
|
1205
|
+
ev.fromTarget = false;
|
|
1206
|
+
ev.z = st.z;
|
|
1207
|
+
if (st.label === EL_NONE)
|
|
1208
|
+
ev.label = null;
|
|
1209
|
+
else {
|
|
1210
|
+
const label = pool.edgeLabels[j];
|
|
1211
|
+
label.text =
|
|
1212
|
+
st.label === EL_UNION
|
|
1213
|
+
? this.eUnionText[j]
|
|
1214
|
+
: st.label === EL_BEFORE
|
|
1215
|
+
? (this.eLabelBefore[j] ?? this.eLabel[j])
|
|
1216
|
+
: this.eLabel[j];
|
|
1217
|
+
label.color = st.labelColor;
|
|
1218
|
+
label.alpha = 1;
|
|
1219
|
+
label.size = LABEL.edgeSize;
|
|
1220
|
+
label.weight = this.t.weightRegular;
|
|
1221
|
+
label.strike = st.strike;
|
|
1222
|
+
label.offset = 0;
|
|
1223
|
+
ev.label = label;
|
|
1224
|
+
}
|
|
1225
|
+
this.curChanged = st.changed;
|
|
1226
|
+
this.curFamily = st.family;
|
|
1227
|
+
return true;
|
|
1228
|
+
}
|
|
1229
|
+
animateEdge(ev, j, styles, nodes, pool) {
|
|
1230
|
+
const c = this.c;
|
|
1231
|
+
const u = this.u;
|
|
1232
|
+
const B = styles[L_BEFORE];
|
|
1233
|
+
const A = styles[L_AFTER];
|
|
1234
|
+
const a = c.eA[j];
|
|
1235
|
+
const b = c.eB[j];
|
|
1236
|
+
const reduced = this.reduced;
|
|
1237
|
+
switch (c.eKind[j]) {
|
|
1238
|
+
case EA_GROW: {
|
|
1239
|
+
if (!this.applyEdge(ev, j, A, nodes, pool))
|
|
1240
|
+
return false;
|
|
1241
|
+
if (reduced) {
|
|
1242
|
+
ev.alpha *= easeStandard(u);
|
|
1243
|
+
return ev.alpha > 0;
|
|
1244
|
+
}
|
|
1245
|
+
const g = easeStandard(segment(u, a, b));
|
|
1246
|
+
if (g <= 0)
|
|
1247
|
+
return false;
|
|
1248
|
+
ev.progress = g;
|
|
1249
|
+
ev.fromTarget = c.eFromTarget[j] === 1;
|
|
1250
|
+
if (ev.label !== null)
|
|
1251
|
+
ev.label.alpha = easeStandard(segment(u, b - 0.05, Math.min(1, b + 0.15)));
|
|
1252
|
+
return true;
|
|
1253
|
+
}
|
|
1254
|
+
case EA_RETRACT: {
|
|
1255
|
+
if (!this.applyEdge(ev, j, B, nodes, pool))
|
|
1256
|
+
return false;
|
|
1257
|
+
if (reduced) {
|
|
1258
|
+
ev.alpha *= 1 - easeStandard(u);
|
|
1259
|
+
return ev.alpha > 0;
|
|
1260
|
+
}
|
|
1261
|
+
const g = easeStandard(segment(u, a, b));
|
|
1262
|
+
if (g >= 1)
|
|
1263
|
+
return false;
|
|
1264
|
+
ev.progress = 1 - g;
|
|
1265
|
+
ev.fromTarget = c.eFromTarget[j] === 1;
|
|
1266
|
+
if (ev.label !== null)
|
|
1267
|
+
ev.label.alpha = 1 - segment(u, a, a + 0.1);
|
|
1268
|
+
return true;
|
|
1269
|
+
}
|
|
1270
|
+
case EA_SWING: {
|
|
1271
|
+
if (!this.applyEdge(ev, j, A, nodes, pool))
|
|
1272
|
+
return false;
|
|
1273
|
+
if (reduced) {
|
|
1274
|
+
ev.alpha *= easeStandard(u);
|
|
1275
|
+
return ev.alpha > 0;
|
|
1276
|
+
}
|
|
1277
|
+
this.tau = easeInOut(segment(u, a, b));
|
|
1278
|
+
this.swing(ev, j, nodes);
|
|
1279
|
+
return true;
|
|
1280
|
+
}
|
|
1281
|
+
case EA_FOLLOW_MERGE: {
|
|
1282
|
+
if (reduced) {
|
|
1283
|
+
if (A.hidden) {
|
|
1284
|
+
if (!this.applyEdge(ev, j, B, nodes, pool))
|
|
1285
|
+
return false;
|
|
1286
|
+
ev.alpha *= 1 - easeStandard(u);
|
|
1287
|
+
}
|
|
1288
|
+
else {
|
|
1289
|
+
if (!this.applyEdge(ev, j, A, nodes, pool))
|
|
1290
|
+
return false;
|
|
1291
|
+
ev.alpha *= easeStandard(u);
|
|
1292
|
+
}
|
|
1293
|
+
return ev.alpha > 0;
|
|
1294
|
+
}
|
|
1295
|
+
if (u < b || A.hidden) {
|
|
1296
|
+
// Rides the travelling source (before form reads its animated position).
|
|
1297
|
+
if (!this.applyEdge(ev, j, B, nodes, pool))
|
|
1298
|
+
return false;
|
|
1299
|
+
ev.alpha *= 1 - segment(u, b, b + CHOREO.arrivalFade);
|
|
1300
|
+
return ev.alpha > 0;
|
|
1301
|
+
}
|
|
1302
|
+
return this.applyEdge(ev, j, A, nodes, pool);
|
|
1303
|
+
}
|
|
1304
|
+
case EA_FOLLOW_SPLIT: {
|
|
1305
|
+
if (!this.applyEdge(ev, j, A, nodes, pool))
|
|
1306
|
+
return false;
|
|
1307
|
+
if (reduced)
|
|
1308
|
+
ev.alpha *= easeStandard(u);
|
|
1309
|
+
return ev.alpha > 0;
|
|
1310
|
+
}
|
|
1311
|
+
case EA_FADE: {
|
|
1312
|
+
const k = easeStandard(segment(u, a, b));
|
|
1313
|
+
if (!this.applyEdge(ev, j, k < 0.5 ? B : A, nodes, pool))
|
|
1314
|
+
return false;
|
|
1315
|
+
if (ev.label !== null && B.label !== A.label)
|
|
1316
|
+
ev.label.alpha = Math.abs(1 - 2 * k);
|
|
1317
|
+
if (!reduced)
|
|
1318
|
+
ev.width += bump(k);
|
|
1319
|
+
return true;
|
|
1320
|
+
}
|
|
1321
|
+
case EA_SWAP: {
|
|
1322
|
+
if (!this.applyEdge(ev, j, A, nodes, pool))
|
|
1323
|
+
return false;
|
|
1324
|
+
ev.alpha *= easeStandard(reduced ? u : segment(u, a, b));
|
|
1325
|
+
return ev.alpha > 0;
|
|
1326
|
+
}
|
|
1327
|
+
case EA_NONE:
|
|
1328
|
+
return this.applyEdge(ev, j, u < 0.5 ? B : A, nodes, pool);
|
|
1329
|
+
default:
|
|
1330
|
+
return this.applyEdge(ev, j, A, nodes, pool);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
/** Swing the changed end around the pivot (polar interpolation) at progress `this.tau`. */
|
|
1334
|
+
swing(ev, j, nodes) {
|
|
1335
|
+
const s = this.tau;
|
|
1336
|
+
const pivot = this.c.ePivot[j];
|
|
1337
|
+
const fixedIsSource = pivot === this.eS[j];
|
|
1338
|
+
const from = nodes[(fixedIsSource ? this.eTB[j] : this.eSB[j])];
|
|
1339
|
+
const to = nodes[(fixedIsSource ? this.eT[j] : this.eS[j])];
|
|
1340
|
+
const p = nodes[pivot];
|
|
1341
|
+
const a0 = Math.atan2(from.y - p.y, from.x - p.x);
|
|
1342
|
+
let da = Math.atan2(to.y - p.y, to.x - p.x) - a0;
|
|
1343
|
+
if (da > Math.PI)
|
|
1344
|
+
da -= 2 * Math.PI;
|
|
1345
|
+
else if (da < -Math.PI)
|
|
1346
|
+
da += 2 * Math.PI;
|
|
1347
|
+
const rho = lerp(Math.hypot(from.x - p.x, from.y - p.y), Math.hypot(to.x - p.x, to.y - p.y), s);
|
|
1348
|
+
const angle = a0 + da * s;
|
|
1349
|
+
const x = p.x + rho * Math.cos(angle);
|
|
1350
|
+
const y = p.y + rho * Math.sin(angle);
|
|
1351
|
+
const r = lerp(from.r, to.r, s);
|
|
1352
|
+
if (fixedIsSource) {
|
|
1353
|
+
ev.x2 = x;
|
|
1354
|
+
ev.y2 = y;
|
|
1355
|
+
ev.r2 = r;
|
|
1356
|
+
}
|
|
1357
|
+
else {
|
|
1358
|
+
ev.x1 = x;
|
|
1359
|
+
ev.y1 = y;
|
|
1360
|
+
ev.r1 = r;
|
|
1361
|
+
}
|
|
1362
|
+
ev.curvature = lerp(this.ghostCurv[j], this.eCurv[j], s);
|
|
1363
|
+
}
|
|
1364
|
+
modulateEdge(ev, j, pool) {
|
|
1365
|
+
const view = this.view;
|
|
1366
|
+
const es = this.eS[j];
|
|
1367
|
+
const et = this.eT[j];
|
|
1368
|
+
const t = this.t;
|
|
1369
|
+
let f = 1;
|
|
1370
|
+
const focus = view.focus;
|
|
1371
|
+
if (focus !== null) {
|
|
1372
|
+
if (focus.edges[j] !== 1) {
|
|
1373
|
+
hideEdge(ev);
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
if (this.eUnchanged[j] === 1 && (focus.context[es] === 1 || focus.context[et] === 1)) {
|
|
1377
|
+
f *= EMPHASIS.focusContext;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
let changed = this.curChanged;
|
|
1381
|
+
if (this.filtered(this.eFam[j], this.eHunkFam[j])) {
|
|
1382
|
+
changed = false;
|
|
1383
|
+
ev.color = t.edge[F_NEUTRAL];
|
|
1384
|
+
ev.width = STROKE.neutralEdge;
|
|
1385
|
+
ev.arrowMinZoom = LOD.arrow;
|
|
1386
|
+
if (ev.label !== null)
|
|
1387
|
+
ev.label.color = t.neutralLabel;
|
|
1388
|
+
f *= EMPHASIS.filtered;
|
|
1389
|
+
}
|
|
1390
|
+
let level;
|
|
1391
|
+
if (this.layer === L_REPLAY) {
|
|
1392
|
+
level =
|
|
1393
|
+
this.edgeGen[j] === this.gen
|
|
1394
|
+
? 2
|
|
1395
|
+
: this.nodeLevel(es) >= 1 && this.nodeLevel(et) >= 1
|
|
1396
|
+
? 1
|
|
1397
|
+
: 0;
|
|
1398
|
+
}
|
|
1399
|
+
else {
|
|
1400
|
+
level =
|
|
1401
|
+
this.selHunk >= 0 && this.eHunk[j] === this.selHunk
|
|
1402
|
+
? 2
|
|
1403
|
+
: this.selNode[es] >= 1 && this.selNode[et] >= 1
|
|
1404
|
+
? 1
|
|
1405
|
+
: 0;
|
|
1406
|
+
}
|
|
1407
|
+
f *= this.spotlight(level);
|
|
1408
|
+
const selected = this.selHunk >= 0 && this.eHunk[j] === this.selHunk;
|
|
1409
|
+
const hovered = view.hoveredEdge === j;
|
|
1410
|
+
let minZoom = changed ? LOD.edgeLabel : 0;
|
|
1411
|
+
let priority = changed ? 0.9 : 0.4;
|
|
1412
|
+
if (selected) {
|
|
1413
|
+
if (ev.width < STROKE.selectedEdge)
|
|
1414
|
+
ev.width += (STROKE.selectedEdge - ev.width) * this.emph;
|
|
1415
|
+
ev.arrowMinZoom = 0;
|
|
1416
|
+
ev.z = Math.max(ev.z, 3);
|
|
1417
|
+
minZoom = 0;
|
|
1418
|
+
priority = 3;
|
|
1419
|
+
}
|
|
1420
|
+
if (hovered) {
|
|
1421
|
+
ev.width = Math.max(ev.width, STROKE.selectedEdge);
|
|
1422
|
+
ev.arrowMinZoom = 0;
|
|
1423
|
+
ev.z = 4;
|
|
1424
|
+
if (ev.label === null) {
|
|
1425
|
+
const label = pool.edgeLabels[j];
|
|
1426
|
+
label.text = this.eLabel[j];
|
|
1427
|
+
label.color = t.neutralLabel;
|
|
1428
|
+
label.alpha = 1;
|
|
1429
|
+
label.size = LABEL.edgeSize;
|
|
1430
|
+
label.weight = t.weightRegular;
|
|
1431
|
+
label.strike = false;
|
|
1432
|
+
label.offset = 0;
|
|
1433
|
+
ev.label = label;
|
|
1434
|
+
}
|
|
1435
|
+
minZoom = 0;
|
|
1436
|
+
priority = Math.max(priority, 2.5);
|
|
1437
|
+
}
|
|
1438
|
+
if (ev.label !== null && this.fadesRestLabels(level) && !hovered) {
|
|
1439
|
+
if (this.restLabels <= 0)
|
|
1440
|
+
ev.label = null;
|
|
1441
|
+
else
|
|
1442
|
+
ev.label.alpha *= this.restLabels;
|
|
1443
|
+
}
|
|
1444
|
+
ev.alpha *= f;
|
|
1445
|
+
if (ev.label !== null) {
|
|
1446
|
+
ev.label.minZoom = minZoom;
|
|
1447
|
+
ev.label.priority = priority;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
// ─── Glyphs ───────────────────────────────────────────────────────────────
|
|
1451
|
+
resolveGlyphs(frame, pool) {
|
|
1452
|
+
const out = frame.glyphs;
|
|
1453
|
+
const list = this.glyphLists[this.layer];
|
|
1454
|
+
let count = 0;
|
|
1455
|
+
for (let k = 0; k < list.length; k++) {
|
|
1456
|
+
const spec = this.glyphs[list[k]];
|
|
1457
|
+
const g = pool.glyphs[count];
|
|
1458
|
+
if (this.writeGlyph(spec, g, pool.glyphLabels[count], frame.nodes)) {
|
|
1459
|
+
if (out[count] !== g)
|
|
1460
|
+
out[count] = g;
|
|
1461
|
+
count++;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
if (out.length !== count)
|
|
1465
|
+
out.length = count;
|
|
1466
|
+
}
|
|
1467
|
+
/** Glyph line between node centers; renderers stop it at `r1` / `r2` like an edge. */
|
|
1468
|
+
glyphLine(g, spec) {
|
|
1469
|
+
const { nx, ny, nr } = this;
|
|
1470
|
+
g.x1 = nx[spec.a];
|
|
1471
|
+
g.y1 = ny[spec.a];
|
|
1472
|
+
g.x2 = nx[spec.b];
|
|
1473
|
+
g.y2 = ny[spec.b];
|
|
1474
|
+
g.r1 = nr[spec.a] + spec.clearA;
|
|
1475
|
+
g.r2 = nr[spec.b] + spec.clearB;
|
|
1476
|
+
}
|
|
1477
|
+
writeGlyph(spec, g, label, nodes) {
|
|
1478
|
+
const view = this.view;
|
|
1479
|
+
const focus = view.focus;
|
|
1480
|
+
if (focus !== null && (focus.nodes[spec.a] !== 1 || focus.nodes[spec.b] !== 1))
|
|
1481
|
+
return false;
|
|
1482
|
+
let alpha = spec.alpha;
|
|
1483
|
+
let progress = 1;
|
|
1484
|
+
let color = spec.color;
|
|
1485
|
+
let dash = spec.dash;
|
|
1486
|
+
let width = spec.width;
|
|
1487
|
+
if (this.layer === L_REPLAY) {
|
|
1488
|
+
const h = spec.hunk;
|
|
1489
|
+
if (h < 0)
|
|
1490
|
+
return false;
|
|
1491
|
+
const state = this.hState[h];
|
|
1492
|
+
const p = this.hP[h];
|
|
1493
|
+
const inTransform = state === 1 && p >= SPOT_END && p < SETTLE;
|
|
1494
|
+
const u = inTransform ? (p - SPOT_END) / TRANSFORM : 0;
|
|
1495
|
+
const c = this.c;
|
|
1496
|
+
switch (spec.role) {
|
|
1497
|
+
case G_TRAIL: {
|
|
1498
|
+
if (!inTransform || this.reduced)
|
|
1499
|
+
return false;
|
|
1500
|
+
const a = c.nA[spec.node];
|
|
1501
|
+
const b = c.nB[spec.node];
|
|
1502
|
+
progress = easeInOut(segment(u, a, b));
|
|
1503
|
+
alpha *= 0.6 * (1 - segment(u, b + 0.05, 1));
|
|
1504
|
+
if (progress <= 0 || alpha <= 0)
|
|
1505
|
+
return false;
|
|
1506
|
+
break;
|
|
1507
|
+
}
|
|
1508
|
+
case G_REPLACEMENT: {
|
|
1509
|
+
if (state === 0 || (state === 1 && p < SPOT_END))
|
|
1510
|
+
return false;
|
|
1511
|
+
if (inTransform) {
|
|
1512
|
+
if (this.reduced)
|
|
1513
|
+
alpha *= easeStandard(u);
|
|
1514
|
+
else
|
|
1515
|
+
progress = easeStandard(segment(u, 0.4, 0.9));
|
|
1516
|
+
if (progress <= 0 || alpha <= 0)
|
|
1517
|
+
return false;
|
|
1518
|
+
}
|
|
1519
|
+
break;
|
|
1520
|
+
}
|
|
1521
|
+
case G_GHOST: {
|
|
1522
|
+
if (!inTransform)
|
|
1523
|
+
return false;
|
|
1524
|
+
const kind = c.eKind[spec.edge];
|
|
1525
|
+
if (kind === EA_SWING && !this.reduced) {
|
|
1526
|
+
alpha *= 1 - easeStandard(segment(u, CHOREO.ghostFade[0], CHOREO.ghostFade[1]));
|
|
1527
|
+
}
|
|
1528
|
+
else if (kind === EA_SWAP || kind === EA_SWING || this.reduced) {
|
|
1529
|
+
// Crossfade: the old form in its before style fades out as the new one fades in.
|
|
1530
|
+
const before = this.eStyles[spec.edge][L_BEFORE];
|
|
1531
|
+
color = before.color;
|
|
1532
|
+
dash = before.dash;
|
|
1533
|
+
width = before.width;
|
|
1534
|
+
alpha =
|
|
1535
|
+
1 -
|
|
1536
|
+
easeStandard(this.reduced ? u : segment(u, c.eA[spec.edge], c.eB[spec.edge]));
|
|
1537
|
+
}
|
|
1538
|
+
else
|
|
1539
|
+
return false;
|
|
1540
|
+
if (alpha <= 0)
|
|
1541
|
+
return false;
|
|
1542
|
+
break;
|
|
1543
|
+
}
|
|
1544
|
+
case G_PIVOT: {
|
|
1545
|
+
if (!inTransform || this.reduced || c.eKind[spec.edge] !== EA_SWING)
|
|
1546
|
+
return false;
|
|
1547
|
+
alpha *= 1 - segment(u, CHOREO.pivotFade[0], CHOREO.pivotFade[1]);
|
|
1548
|
+
if (alpha <= 0)
|
|
1549
|
+
return false;
|
|
1550
|
+
break;
|
|
1551
|
+
}
|
|
1552
|
+
default:
|
|
1553
|
+
return false;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
else if (spec.role === G_SUGGESTION && !view.showSuggestions)
|
|
1557
|
+
return false;
|
|
1558
|
+
else if (spec.edge >= 0 && nodes[spec.a]?.visible !== true)
|
|
1559
|
+
return false;
|
|
1560
|
+
g.kind = spec.kind;
|
|
1561
|
+
if (spec.role === G_PIVOT) {
|
|
1562
|
+
const { nx, ny, nr } = this;
|
|
1563
|
+
const px = nx[spec.a];
|
|
1564
|
+
const py = ny[spec.a];
|
|
1565
|
+
const pr = nr[spec.a];
|
|
1566
|
+
const dx = nx[spec.b] - px;
|
|
1567
|
+
const dy = ny[spec.b] - py;
|
|
1568
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
1569
|
+
g.x1 = len > 0 ? px + (dx / len) * pr : px;
|
|
1570
|
+
g.y1 = len > 0 ? py + (dy / len) * pr : py - pr;
|
|
1571
|
+
g.x2 = g.x1;
|
|
1572
|
+
g.y2 = g.y1;
|
|
1573
|
+
g.r1 = 0;
|
|
1574
|
+
g.r2 = 0;
|
|
1575
|
+
g.curvature = 0;
|
|
1576
|
+
}
|
|
1577
|
+
else {
|
|
1578
|
+
this.glyphLine(g, spec);
|
|
1579
|
+
g.curvature = spec.curvature;
|
|
1580
|
+
}
|
|
1581
|
+
// Modulators: filter, spotlight.
|
|
1582
|
+
let f = 1;
|
|
1583
|
+
if (this.filtered(spec.family, spec.hunk >= 0 ? this.hFam[spec.hunk] : F_NEUTRAL)) {
|
|
1584
|
+
color = this.t.edge[F_NEUTRAL];
|
|
1585
|
+
f *= EMPHASIS.filtered;
|
|
1586
|
+
}
|
|
1587
|
+
const level = Math.max(this.nodeLevel(spec.a), this.nodeLevel(spec.b));
|
|
1588
|
+
f *= this.spotlight(level);
|
|
1589
|
+
g.color = color;
|
|
1590
|
+
g.alpha = alpha * f;
|
|
1591
|
+
g.width = width;
|
|
1592
|
+
g.dash = dash;
|
|
1593
|
+
g.progress = progress;
|
|
1594
|
+
const fade = this.fadesRestLabels(level);
|
|
1595
|
+
if (spec.label !== null && !(fade && this.restLabels <= 0)) {
|
|
1596
|
+
label.text = spec.label;
|
|
1597
|
+
label.color = this.t.text[F_IDENTITY];
|
|
1598
|
+
label.alpha = fade ? this.restLabels : 1;
|
|
1599
|
+
label.size = LABEL.size;
|
|
1600
|
+
label.weight = this.t.weightStrong;
|
|
1601
|
+
label.strike = false;
|
|
1602
|
+
label.priority = 1;
|
|
1603
|
+
label.minZoom = this.lodChanged;
|
|
1604
|
+
label.offset = 0;
|
|
1605
|
+
g.label = label;
|
|
1606
|
+
}
|
|
1607
|
+
else
|
|
1608
|
+
g.label = null;
|
|
1609
|
+
return g.alpha > 0;
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
/**
|
|
1613
|
+
* A resolver that turns a `ViewState` into drawable primitives for `scene`
|
|
1614
|
+
* (docs/visual-language.md). Construction is O(N + E); `resolve` is O(N + E) per frame and,
|
|
1615
|
+
* when `out` is passed back in, allocates no objects or arrays (see the module notes for the
|
|
1616
|
+
* transient number boxing V8 may still do while replay animations run).
|
|
1617
|
+
*/
|
|
1618
|
+
export function createVisualResolver(scene) {
|
|
1619
|
+
return new SceneResolver(scene);
|
|
1620
|
+
}
|
|
1621
|
+
//# sourceMappingURL=resolver.js.map
|