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