@yggy-tech/graphraum 0.35.0 → 0.36.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/dist/index.d.ts +124 -1
- package/dist/index.js +758 -15
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -80,6 +80,32 @@ interface GraphraumSetDataOptions {
|
|
|
80
80
|
*/
|
|
81
81
|
fit?: boolean;
|
|
82
82
|
}
|
|
83
|
+
interface GraphraumCameraPoint3d {
|
|
84
|
+
x: number;
|
|
85
|
+
y: number;
|
|
86
|
+
z: number;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* A 2D view: where the camera looks and how much of the world it shows from top to bottom. The
|
|
90
|
+
* visible width follows the canvas aspect, so a state read on one canvas size fits another.
|
|
91
|
+
*/
|
|
92
|
+
interface GraphraumCameraState2d {
|
|
93
|
+
center: {
|
|
94
|
+
x: number;
|
|
95
|
+
y: number;
|
|
96
|
+
};
|
|
97
|
+
mode: "2d";
|
|
98
|
+
/** World units from the top to the bottom edge of the canvas, with the current zoom applied. */
|
|
99
|
+
visibleHeight: number;
|
|
100
|
+
}
|
|
101
|
+
/** A 3D view: the camera position and the point it orbits and looks at. */
|
|
102
|
+
interface GraphraumCameraState3d {
|
|
103
|
+
mode: "3d";
|
|
104
|
+
position: GraphraumCameraPoint3d;
|
|
105
|
+
target: GraphraumCameraPoint3d;
|
|
106
|
+
}
|
|
107
|
+
/** The camera pose as plain JSON, for saving a view and restoring it with `setCameraState()`. */
|
|
108
|
+
type GraphraumCameraState = GraphraumCameraState2d | GraphraumCameraState3d;
|
|
83
109
|
interface GraphraumDataPatch<NodeAttributes = undefined, EdgeAttributes = undefined> {
|
|
84
110
|
addedNodes?: readonly GraphraumNode<NodeAttributes>[];
|
|
85
111
|
addedEdges?: readonly GraphraumEdge<EdgeAttributes>[];
|
|
@@ -240,8 +266,60 @@ type GraphraumPickHit = {
|
|
|
240
266
|
kind: "node";
|
|
241
267
|
id: string;
|
|
242
268
|
};
|
|
269
|
+
/** `linear`, cubic `easeIn` / `easeOut` / `easeInOut`, or a function from progress 0..1 to eased 0..1. */
|
|
270
|
+
type GraphraumEasing = "easeIn" | "easeInOut" | "easeOut" | "linear" | ((t: number) => number);
|
|
271
|
+
/** How depth of field animates focus moves and blur fades. */
|
|
272
|
+
interface GraphraumDepthOfFieldTransition {
|
|
273
|
+
/** Default 400; 0 jumps. `prefers-reduced-motion: reduce` always jumps. */
|
|
274
|
+
durationMs?: number;
|
|
275
|
+
/** Default `"easeInOut"`. */
|
|
276
|
+
easing?: GraphraumEasing;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Opt-in 3D depth of field. The focal plane follows the selection, so selected nodes stay sharp and
|
|
280
|
+
* the rest of the graph blurs with its distance from them, in front and behind, like a thin lens:
|
|
281
|
+
* the closer the camera is to the focus, the shallower the depth of field. A no-op in 2D. See
|
|
282
|
+
* `Graphraum.setDepthOfField`.
|
|
283
|
+
*/
|
|
284
|
+
interface GraphraumDepthOfFieldOptions {
|
|
285
|
+
/** Default `false`. While disabled no extra passes or render targets exist. Toggling fades the blur. */
|
|
286
|
+
enabled?: boolean;
|
|
287
|
+
/**
|
|
288
|
+
* `"selection"` (default) focuses the mean position of the selected nodes and fades the blur out
|
|
289
|
+
* when nothing is selected. A number fixes the focal plane at that camera distance in world units.
|
|
290
|
+
*/
|
|
291
|
+
focus?: "selection" | number;
|
|
292
|
+
/**
|
|
293
|
+
* Aperture, 0..1, default 0.5. A surface blurs by `strength * 24 * |d - f| / d` CSS pixels, where
|
|
294
|
+
* `d` is its camera distance and `f` the focal distance, clamped to `maxBlur`.
|
|
295
|
+
*/
|
|
296
|
+
strength?: number;
|
|
297
|
+
/** Largest blur radius in CSS pixels. Default 12. */
|
|
298
|
+
maxBlur?: number;
|
|
299
|
+
/** Blur radii below this many CSS pixels render sharp, which widens the in-focus band. Default 0. */
|
|
300
|
+
minBlur?: number;
|
|
301
|
+
/** Camera-to-focus distance in world units up to which `strength` applies fully. Default 0. */
|
|
302
|
+
nearDistance?: number;
|
|
303
|
+
/** Camera-to-focus distance in world units at which the blur has faded out. Default `Infinity` (no fade). */
|
|
304
|
+
farDistance?: number;
|
|
305
|
+
/** Animation for focus moves between selections and for blur fades. */
|
|
306
|
+
transition?: GraphraumDepthOfFieldTransition;
|
|
307
|
+
}
|
|
308
|
+
/** Options for one `Graphraum.setDepthOfField` call. */
|
|
309
|
+
interface GraphraumDepthOfFieldUpdate {
|
|
310
|
+
/** Default `true`: fade the blur to the new strength over the transition. `false` jumps. */
|
|
311
|
+
animate?: boolean;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* `off`: disabled. `inactive`: enabled with nothing to blur (2D mode, or no selection with
|
|
315
|
+
* `focus: "selection"`), so frames render directly. `active`: the last frame ran the blur passes.
|
|
316
|
+
* `unsupported`: enabled, but the WebGL context cannot run it, so frames render directly.
|
|
317
|
+
*/
|
|
318
|
+
type GraphraumDepthOfFieldStatus = "active" | "inactive" | "off" | "unsupported";
|
|
243
319
|
interface GraphraumOptions<NodeAttributes = undefined, EdgeAttributes = undefined> {
|
|
244
320
|
antialias?: boolean;
|
|
321
|
+
/** Opt-in 3D depth of field; off by default. Change it later with `setDepthOfField`. */
|
|
322
|
+
depthOfField?: GraphraumDepthOfFieldOptions;
|
|
245
323
|
maxVisibleEdges?: number;
|
|
246
324
|
maxVisibleNodes?: number;
|
|
247
325
|
maxPixelRatio?: number;
|
|
@@ -274,6 +352,7 @@ interface GraphraumDiagnostics {
|
|
|
274
352
|
aggregatedNodeClusters: number;
|
|
275
353
|
autoOrbit: GraphraumAutoOrbitStatus;
|
|
276
354
|
cpuFrameMilliseconds: number;
|
|
355
|
+
depthOfField: GraphraumDepthOfFieldStatus;
|
|
277
356
|
gpuFrameMilliseconds: number | null;
|
|
278
357
|
gpuDrawCalls: number;
|
|
279
358
|
gpuGeometries: number;
|
|
@@ -532,12 +611,21 @@ declare class Graphraum<NodeAttributes = undefined, EdgeAttributes = undefined>
|
|
|
532
611
|
/** True only during the orbit's own `controls.update(dt)`; see `handleViewChange`. */
|
|
533
612
|
private orbitUpdating;
|
|
534
613
|
private destroyed;
|
|
614
|
+
private canvasWidth;
|
|
615
|
+
private canvasHeight;
|
|
535
616
|
private cpuFrameMilliseconds;
|
|
536
617
|
private gpuFrameMilliseconds;
|
|
537
618
|
private gpuTimer;
|
|
538
619
|
private readonly viewListeners;
|
|
539
620
|
private readonly focusListeners;
|
|
540
621
|
private lastHoveredNodeId;
|
|
622
|
+
private readonly antialias;
|
|
623
|
+
private depthOfField;
|
|
624
|
+
private readonly depthOfFieldFocus;
|
|
625
|
+
/** Strength actually applied: fades toward `strength` while enabled and toward 0 when disabled. */
|
|
626
|
+
private readonly depthOfFieldStrength;
|
|
627
|
+
private depthOfFieldPass;
|
|
628
|
+
private depthOfFieldStatus;
|
|
541
629
|
constructor(container: HTMLElement, options?: GraphraumOptions<NodeAttributes, EdgeAttributes>);
|
|
542
630
|
/** Replaces the whole graph. Fits the camera unless `options.fit` is `false`. */
|
|
543
631
|
setData(data: GraphraumData<NodeAttributes, EdgeAttributes>, options?: GraphraumSetDataOptions): void;
|
|
@@ -596,6 +684,19 @@ declare class Graphraum<NodeAttributes = undefined, EdgeAttributes = undefined>
|
|
|
596
684
|
* before enabling it. Throws in 2D mode; `setMode("2d")` turns it off. Does nothing after `destroy()`.
|
|
597
685
|
*/
|
|
598
686
|
setAutoOrbit(options: GraphraumAutoOrbitOptions | false): void;
|
|
687
|
+
/**
|
|
688
|
+
* Updates 3D depth of field; omitted fields keep their current values. Enabling, disabling and a
|
|
689
|
+
* new `strength` fade the blur over `transition` unless `update.animate` is `false`; a change
|
|
690
|
+
* during a fade continues from the blur shown last. With `focus: "selection"` the focal plane
|
|
691
|
+
* follows the mean position of the selected nodes, re-measured from the camera every frame, and
|
|
692
|
+
* moves over `transition` when the selection changes; with nothing selected the blur fades out.
|
|
693
|
+
* `prefers-reduced-motion: reduce` jumps. Resolves `true` when the blur reaches the new strength
|
|
694
|
+
* and `false` when a later call or `destroy()` interrupts the fade. While it blurs each frame adds
|
|
695
|
+
* three draw calls; disabled, or with nothing to blur, it costs nothing. A no-op in 2D. Picking
|
|
696
|
+
* and DOM overlays are unaffected. Throws for invalid values.
|
|
697
|
+
*/
|
|
698
|
+
setDepthOfField(options: GraphraumDepthOfFieldOptions, update?: GraphraumDepthOfFieldUpdate): Promise<boolean>;
|
|
699
|
+
private depthOfFieldTargetStrength;
|
|
599
700
|
getNodePresentation(nodeId: string): CompiledGraphraumPresentation | null;
|
|
600
701
|
/** Returns canvas-relative screen coordinates for a node, or null when the node does not exist. */
|
|
601
702
|
getNodeScreenPosition(nodeId: string): GraphraumScreenPosition | null;
|
|
@@ -644,6 +745,20 @@ declare class Graphraum<NodeAttributes = undefined, EdgeAttributes = undefined>
|
|
|
644
745
|
* reduce` or a zero duration it jumps. Throws for an invalid duration or distance scale.
|
|
645
746
|
*/
|
|
646
747
|
flyTo(options: GraphraumFlyToOptions): Promise<boolean>;
|
|
748
|
+
/**
|
|
749
|
+
* The camera pose as plain JSON. 2D reports the centre and the world height the canvas shows; 3D
|
|
750
|
+
* reports the camera position and the point it orbits. Pass it to `setCameraState()` to return
|
|
751
|
+
* to the same view, for example after reloading a saved one.
|
|
752
|
+
*/
|
|
753
|
+
getCameraState(): GraphraumCameraState;
|
|
754
|
+
/**
|
|
755
|
+
* Puts the camera on a pose from `getCameraState()` at once, without fitting or animating, and
|
|
756
|
+
* cancels a running `flyTo`. Throws for a state of the other mode (switch with `setMode()` first)
|
|
757
|
+
* or with a non-finite number. 2D keeps the saved height and takes the width from the canvas, so a
|
|
758
|
+
* view saved on one canvas size fits another. 3D derives the clip planes from the pose so the whole
|
|
759
|
+
* graph stays in view. Apply it after the canvas has its size: a 2D resize to a new size refits.
|
|
760
|
+
*/
|
|
761
|
+
setCameraState(state: GraphraumCameraState): void;
|
|
647
762
|
/** Nodes whose id is in `nodeIds`, in graph order; every node when `nodeIds` is omitted. */
|
|
648
763
|
private nodesWithIds;
|
|
649
764
|
/** The closest front-view camera that frames `nodes` (issue #124); applies the container aspect. */
|
|
@@ -673,6 +788,14 @@ declare class Graphraum<NodeAttributes = undefined, EdgeAttributes = undefined>
|
|
|
673
788
|
/** Renders a pending frame now instead of on the next animation frame. */
|
|
674
789
|
private flushRender;
|
|
675
790
|
private renderFrame;
|
|
791
|
+
/**
|
|
792
|
+
* Decides whether this frame blurs and records why in the diagnostics status. Returns null to
|
|
793
|
+
* render directly. Requests another frame while a fade or focus move runs; both end on their own.
|
|
794
|
+
*/
|
|
795
|
+
private prepareDepthOfField;
|
|
796
|
+
/** Mean world position of the selected nodes, read every frame so a running layout keeps them in focus. */
|
|
797
|
+
private selectionFocusPoint;
|
|
798
|
+
private disposeDepthOfFieldPass;
|
|
676
799
|
private stopAutoOrbit;
|
|
677
800
|
/**
|
|
678
801
|
* One orbit frame. `autoRotate` is on only for this `update(dt)` call: OrbitControls also calls
|
|
@@ -860,4 +983,4 @@ declare function isTransparentGraphraumBackground(background: GraphraumBackgroun
|
|
|
860
983
|
/** Defines typed, serializable graph visuals without coupling application data to the render loop. */
|
|
861
984
|
declare function defineVisuals<NodeAttributes = undefined, EdgeAttributes = undefined>(visuals: GraphraumVisualMapper<NodeAttributes, EdgeAttributes>): GraphraumVisualMapper<NodeAttributes, EdgeAttributes>;
|
|
862
985
|
//#endregion
|
|
863
|
-
export { type CompiledGraphraumPresentation, DEFAULT_ENDPOINT_ATTACH, DEFAULT_ENDPOINT_CLEARANCE, DEFAULT_FORCE_SETTINGS, type EndpointAttach, type EndpointOutline, type ForceLayoutRequest, type ForceSettings, type GraphologyEdgeAttributes, type GraphologyNodeAttributes, type GraphologyRenderer, Graphraum, type GraphraumAutoOrbitOptions, type GraphraumAutoOrbitStatus, type GraphraumBackground, type GraphraumColor, type GraphraumData, type GraphraumDataPatch, type GraphraumDiagnostics, type GraphraumEdge, type GraphraumEdgeEncoding, type GraphraumEdgeMarker, type GraphraumEdgeMarkerEnd, type GraphraumEdgePath, type GraphraumEdgeState, type GraphraumEdgeStyle, type GraphraumEdgeVisual, type GraphraumFlyToOptions, type GraphraumLabelCandidate, type GraphraumLayoutPositions, type GraphraumMode, type GraphraumNode, type GraphraumNodeEncoding, type GraphraumNodeGeometry, type GraphraumNodeShape, type GraphraumNodeState, type GraphraumNodeUpdate, type GraphraumNodeVisual, type GraphraumOptions, GraphraumOverlay, type GraphraumOverlayNode, type GraphraumOverlayOptions, type GraphraumPatternBackground, type GraphraumPickHit, type GraphraumPosition, type GraphraumPresentation, type GraphraumPresentationAction, type GraphraumPresentationProperty, type GraphraumPropertyValue, type GraphraumScreenPosition, type GraphraumSetDataOptions, type GraphraumTheme, type GraphraumThemeName, type GraphraumVisualMapper, type LabelBudgetCandidate, type RichNodePolicy, type SelectBudgetedLabelIdsInput, type SelectFocusLabelIdsInput, attachPointOnOutline, bindGraphology, boundOverlayIds, computeClusteredForcePositions, computeForcePositions, createForceSimulation, defineVisuals, forceIterationCount, graphraumEdgePaths, graphraumNodeShapes, graphraumTheme, graphraumThemeDark, graphraumThemeLight, graphraumThemes, isTransparentGraphraumBackground, normalizeGraphraumBackground, orderFocusNodeIds, resolveGraphraumTheme, selectBudgetedLabelIds, selectFocusLabelIds, selectRichNodeIds, trimEdgeEndpoints };
|
|
986
|
+
export { type CompiledGraphraumPresentation, DEFAULT_ENDPOINT_ATTACH, DEFAULT_ENDPOINT_CLEARANCE, DEFAULT_FORCE_SETTINGS, type EndpointAttach, type EndpointOutline, type ForceLayoutRequest, type ForceSettings, type GraphologyEdgeAttributes, type GraphologyNodeAttributes, type GraphologyRenderer, Graphraum, type GraphraumAutoOrbitOptions, type GraphraumAutoOrbitStatus, type GraphraumBackground, type GraphraumCameraPoint3d, type GraphraumCameraState, type GraphraumCameraState2d, type GraphraumCameraState3d, type GraphraumColor, type GraphraumData, type GraphraumDataPatch, type GraphraumDepthOfFieldOptions, type GraphraumDepthOfFieldStatus, type GraphraumDepthOfFieldTransition, type GraphraumDepthOfFieldUpdate, type GraphraumDiagnostics, type GraphraumEasing, type GraphraumEdge, type GraphraumEdgeEncoding, type GraphraumEdgeMarker, type GraphraumEdgeMarkerEnd, type GraphraumEdgePath, type GraphraumEdgeState, type GraphraumEdgeStyle, type GraphraumEdgeVisual, type GraphraumFlyToOptions, type GraphraumLabelCandidate, type GraphraumLayoutPositions, type GraphraumMode, type GraphraumNode, type GraphraumNodeEncoding, type GraphraumNodeGeometry, type GraphraumNodeShape, type GraphraumNodeState, type GraphraumNodeUpdate, type GraphraumNodeVisual, type GraphraumOptions, GraphraumOverlay, type GraphraumOverlayNode, type GraphraumOverlayOptions, type GraphraumPatternBackground, type GraphraumPickHit, type GraphraumPosition, type GraphraumPresentation, type GraphraumPresentationAction, type GraphraumPresentationProperty, type GraphraumPropertyValue, type GraphraumScreenPosition, type GraphraumSetDataOptions, type GraphraumTheme, type GraphraumThemeName, type GraphraumVisualMapper, type LabelBudgetCandidate, type RichNodePolicy, type SelectBudgetedLabelIdsInput, type SelectFocusLabelIdsInput, attachPointOnOutline, bindGraphology, boundOverlayIds, computeClusteredForcePositions, computeForcePositions, createForceSimulation, defineVisuals, forceIterationCount, graphraumEdgePaths, graphraumNodeShapes, graphraumTheme, graphraumThemeDark, graphraumThemeLight, graphraumThemes, isTransparentGraphraumBackground, normalizeGraphraumBackground, orderFocusNodeIds, resolveGraphraumTheme, selectBudgetedLabelIds, selectFocusLabelIds, selectRichNodeIds, trimEdgeEndpoints };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AdditiveBlending, Box3, CanvasTexture, Color, InstancedBufferAttribute, InstancedMesh, MOUSE, Matrix4, MeshBasicMaterial, NormalBlending, OrthographicCamera, PerspectiveCamera, Plane, PlaneGeometry, Raycaster, RepeatWrapping, SRGBColorSpace, Scene, ShaderMaterial, SphereGeometry, TOUCH, Texture, Vector2, Vector3, WebGLRenderer } from "three";
|
|
1
|
+
import { AdditiveBlending, Box3, BufferGeometry, CanvasTexture, Color, DepthTexture, Float32BufferAttribute, GLSL3, InstancedBufferAttribute, InstancedMesh, MOUSE, Matrix4, Mesh, MeshBasicMaterial, NoBlending, NormalBlending, OrthographicCamera, PerspectiveCamera, Plane, PlaneGeometry, Raycaster, RepeatWrapping, SRGBColorSpace, Scene, ShaderMaterial, SphereGeometry, TOUCH, Texture, UnsignedByteType, Vector2, Vector3, WebGLRenderTarget, WebGLRenderer } from "three";
|
|
2
2
|
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
|
3
3
|
//#region src/node-axes.ts
|
|
4
4
|
function assertPositiveFinite(value, axis, nodeId) {
|
|
@@ -904,6 +904,63 @@ function interpolateScale(from, to, t) {
|
|
|
904
904
|
return from * (to / from) ** t;
|
|
905
905
|
}
|
|
906
906
|
//#endregion
|
|
907
|
+
//#region src/camera-state.ts
|
|
908
|
+
/**
|
|
909
|
+
* Checks a camera state before it touches the camera: an object of the graph's mode with finite
|
|
910
|
+
* numbers, a positive visible height in 2D, and a position apart from the target in 3D. The state
|
|
911
|
+
* is usually parsed JSON, so nothing about its shape is taken on trust.
|
|
912
|
+
*/
|
|
913
|
+
function validateCameraState(state, mode) {
|
|
914
|
+
if (typeof state !== "object" || state === null) throw new Error("A camera state must be an object returned by getCameraState().");
|
|
915
|
+
const stateMode = state.mode;
|
|
916
|
+
if (stateMode !== "2d" && stateMode !== "3d") throw new Error(`A camera state needs mode "2d" or "3d"; received ${JSON.stringify(stateMode)}.`);
|
|
917
|
+
if (state.mode !== mode) throw new Error(`Cannot apply a ${state.mode} camera state to a graph in ${mode} mode; call setMode("${state.mode}") first.`);
|
|
918
|
+
if (state.mode === "2d") {
|
|
919
|
+
const x = finite(state.center?.x, "center.x");
|
|
920
|
+
const y = finite(state.center?.y, "center.y");
|
|
921
|
+
const visibleHeight = finite(state.visibleHeight, "visibleHeight");
|
|
922
|
+
if (visibleHeight <= 0) throw new Error(`Camera state visibleHeight must be above 0; received ${visibleHeight}.`);
|
|
923
|
+
return {
|
|
924
|
+
center: {
|
|
925
|
+
x,
|
|
926
|
+
y
|
|
927
|
+
},
|
|
928
|
+
mode: "2d",
|
|
929
|
+
visibleHeight
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
const position = point3d(state.position, "position");
|
|
933
|
+
const target = point3d(state.target, "target");
|
|
934
|
+
if (position.x === target.x && position.y === target.y && position.z === target.z) throw new Error("Camera state position and target must differ.");
|
|
935
|
+
return {
|
|
936
|
+
mode: "3d",
|
|
937
|
+
position,
|
|
938
|
+
target
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
function point3d(value, name) {
|
|
942
|
+
return {
|
|
943
|
+
x: finite(value?.x, `${name}.x`),
|
|
944
|
+
y: finite(value?.y, `${name}.y`),
|
|
945
|
+
z: finite(value?.z, `${name}.z`)
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
function finite(value, name) {
|
|
949
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`Camera state ${name} must be a finite number; received ${String(value)}.`);
|
|
950
|
+
return value;
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Clip planes for a perspective camera `distance` from its target with the graph reaching
|
|
954
|
+
* `graphRadius` around it: the far plane holds the whole graph with the slack a fit leaves for
|
|
955
|
+
* zooming out, and the near plane stays close enough not to cut nodes in front of the camera.
|
|
956
|
+
*/
|
|
957
|
+
function perspectiveClipRange(distance, graphRadius) {
|
|
958
|
+
return {
|
|
959
|
+
far: Math.max(distance * 10, (distance + graphRadius) * 1.1),
|
|
960
|
+
near: Math.min(Math.max(distance / 1e4, .1), distance / 2)
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
//#endregion
|
|
907
964
|
//#region src/theme.ts
|
|
908
965
|
/** Dark canvas: Ink Black, Turf Green, Porcelain. */
|
|
909
966
|
const graphraumThemeDark = Object.freeze({
|
|
@@ -1203,6 +1260,501 @@ function isAppendOnlyDataPatch(patch) {
|
|
|
1203
1260
|
return (patch.removedNodeIds?.length ?? 0) === 0 && (patch.removedEdgeIds?.length ?? 0) === 0;
|
|
1204
1261
|
}
|
|
1205
1262
|
//#endregion
|
|
1263
|
+
//#region src/depth-of-field.ts
|
|
1264
|
+
const DEPTH_OF_FIELD_DEFAULTS = {
|
|
1265
|
+
enabled: false,
|
|
1266
|
+
farDistance: Number.POSITIVE_INFINITY,
|
|
1267
|
+
focus: "selection",
|
|
1268
|
+
maxBlur: 12,
|
|
1269
|
+
minBlur: 0,
|
|
1270
|
+
nearDistance: 0,
|
|
1271
|
+
strength: .5,
|
|
1272
|
+
transition: {
|
|
1273
|
+
durationMs: 400,
|
|
1274
|
+
easing: "easeInOut"
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1277
|
+
const EASINGS = {
|
|
1278
|
+
easeIn: (t) => t * t * t,
|
|
1279
|
+
easeInOut: easeInOutCubic,
|
|
1280
|
+
easeOut: (t) => 1 - (1 - t) ** 3,
|
|
1281
|
+
linear: (t) => t
|
|
1282
|
+
};
|
|
1283
|
+
function resolveEasing(easing) {
|
|
1284
|
+
return typeof easing === "function" ? easing : EASINGS[easing];
|
|
1285
|
+
}
|
|
1286
|
+
function isEasing(value) {
|
|
1287
|
+
return typeof value === "function" || typeof value === "string" && Object.hasOwn(EASINGS, value);
|
|
1288
|
+
}
|
|
1289
|
+
function isNonNegativeNumber(value) {
|
|
1290
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
1291
|
+
}
|
|
1292
|
+
/** Validates `options` and merges them onto `base`. Throws an actionable error for invalid values. */
|
|
1293
|
+
function resolveDepthOfFieldOptions(options, base = DEPTH_OF_FIELD_DEFAULTS) {
|
|
1294
|
+
if (typeof options !== "object" || options === null) throw new Error("Depth of field options must be an object, e.g. { enabled: true, strength: 0.5 }.");
|
|
1295
|
+
if (options.transition !== void 0 && (typeof options.transition !== "object" || options.transition === null)) throw new Error("Depth of field transition must be an object, e.g. { durationMs: 400, easing: \"easeInOut\" }.");
|
|
1296
|
+
const resolved = {
|
|
1297
|
+
enabled: options.enabled ?? base.enabled,
|
|
1298
|
+
farDistance: options.farDistance ?? base.farDistance,
|
|
1299
|
+
focus: options.focus ?? base.focus,
|
|
1300
|
+
maxBlur: options.maxBlur ?? base.maxBlur,
|
|
1301
|
+
minBlur: options.minBlur ?? base.minBlur,
|
|
1302
|
+
nearDistance: options.nearDistance ?? base.nearDistance,
|
|
1303
|
+
strength: options.strength ?? base.strength,
|
|
1304
|
+
transition: {
|
|
1305
|
+
durationMs: options.transition?.durationMs ?? base.transition.durationMs,
|
|
1306
|
+
easing: options.transition?.easing ?? base.transition.easing
|
|
1307
|
+
}
|
|
1308
|
+
};
|
|
1309
|
+
const { enabled, farDistance, focus, maxBlur, minBlur, nearDistance, strength, transition } = resolved;
|
|
1310
|
+
if (typeof enabled !== "boolean") throw new Error(`Depth of field enabled must be a boolean; received ${String(enabled)}.`);
|
|
1311
|
+
if (!isNonNegativeNumber(strength) || strength > 1) throw new Error(`Depth of field strength must be a number from 0 to 1; received ${String(strength)}.`);
|
|
1312
|
+
if (focus !== "selection" && (typeof focus !== "number" || !Number.isFinite(focus) || focus <= 0)) throw new Error(`Depth of field focus must be "selection" or a camera distance above 0 in world units; received ${String(focus)}.`);
|
|
1313
|
+
if (!isNonNegativeNumber(maxBlur) || !isNonNegativeNumber(minBlur) || minBlur > maxBlur) throw new Error(`Depth of field minBlur and maxBlur must be non-negative CSS pixel radii with minBlur <= maxBlur; received ${String(minBlur)} and ${String(maxBlur)}.`);
|
|
1314
|
+
if (!isNonNegativeNumber(nearDistance) || typeof farDistance !== "number" || Number.isNaN(farDistance) || farDistance <= nearDistance) throw new Error(`Depth of field nearDistance must be a non-negative world distance below farDistance; received ${String(nearDistance)} and ${String(farDistance)}.`);
|
|
1315
|
+
if (!isNonNegativeNumber(transition.durationMs)) throw new Error(`Depth of field transition durationMs must be a non-negative finite number of milliseconds; received ${String(transition.durationMs)}.`);
|
|
1316
|
+
if (!isEasing(transition.easing)) throw new Error(`Depth of field transition easing must be "linear", "easeIn", "easeOut", "easeInOut" or a function; received ${String(transition.easing)}.`);
|
|
1317
|
+
return resolved;
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1320
|
+
* How much of `strength` applies when the focus is `focusDistance` from the camera: 1 up to
|
|
1321
|
+
* `nearDistance`, easing to 0 at `farDistance`, so a zoomed-out overview stays sharp.
|
|
1322
|
+
*/
|
|
1323
|
+
function distanceFade(focusDistance, nearDistance, farDistance) {
|
|
1324
|
+
if (focusDistance <= nearDistance) return 1;
|
|
1325
|
+
if (focusDistance >= farDistance) return 0;
|
|
1326
|
+
const t = (focusDistance - nearDistance) / (farDistance - nearDistance);
|
|
1327
|
+
return 1 - t * t * (3 - 2 * t);
|
|
1328
|
+
}
|
|
1329
|
+
/** Distance in front of the camera along its view axis, from a column-major view matrix (`matrixWorldInverse`). */
|
|
1330
|
+
function viewDistance(point, viewMatrix) {
|
|
1331
|
+
const m = (index) => viewMatrix[index] ?? 0;
|
|
1332
|
+
return -(m(2) * point.x + m(6) * point.y + m(10) * point.z + m(14));
|
|
1333
|
+
}
|
|
1334
|
+
/** Mean position of `points`, or null when there are none. */
|
|
1335
|
+
function meanPoint(points) {
|
|
1336
|
+
let count = 0;
|
|
1337
|
+
const sum = {
|
|
1338
|
+
x: 0,
|
|
1339
|
+
y: 0,
|
|
1340
|
+
z: 0
|
|
1341
|
+
};
|
|
1342
|
+
for (const point of points) {
|
|
1343
|
+
count += 1;
|
|
1344
|
+
sum.x += point.x;
|
|
1345
|
+
sum.y += point.y;
|
|
1346
|
+
sum.z += point.z;
|
|
1347
|
+
}
|
|
1348
|
+
return count === 0 ? null : {
|
|
1349
|
+
x: sum.x / count,
|
|
1350
|
+
y: sum.y / count,
|
|
1351
|
+
z: sum.z / count
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* A scalar animated toward a target. Retargeting starts from the value shown last, so an interrupted
|
|
1356
|
+
* transition continues smoothly instead of restarting. Each retarget returns a promise that resolves
|
|
1357
|
+
* `true` when the value arrives and `false` when another retarget or `cancel` replaces it.
|
|
1358
|
+
*/
|
|
1359
|
+
var Tween = class {
|
|
1360
|
+
from;
|
|
1361
|
+
to;
|
|
1362
|
+
value;
|
|
1363
|
+
startMs = null;
|
|
1364
|
+
durationMs = 0;
|
|
1365
|
+
easing = EASINGS.linear;
|
|
1366
|
+
settle = null;
|
|
1367
|
+
constructor(value) {
|
|
1368
|
+
this.from = value;
|
|
1369
|
+
this.to = value;
|
|
1370
|
+
this.value = value;
|
|
1371
|
+
}
|
|
1372
|
+
get target() {
|
|
1373
|
+
return this.to;
|
|
1374
|
+
}
|
|
1375
|
+
get settled() {
|
|
1376
|
+
return this.settle === null && this.value === this.to;
|
|
1377
|
+
}
|
|
1378
|
+
retarget(to, durationMs, easing) {
|
|
1379
|
+
this.settle?.(false);
|
|
1380
|
+
this.settle = null;
|
|
1381
|
+
this.from = this.value;
|
|
1382
|
+
this.to = to;
|
|
1383
|
+
this.startMs = null;
|
|
1384
|
+
this.durationMs = durationMs;
|
|
1385
|
+
this.easing = easing;
|
|
1386
|
+
if (durationMs === 0 || this.from === to) {
|
|
1387
|
+
this.value = to;
|
|
1388
|
+
return Promise.resolve(true);
|
|
1389
|
+
}
|
|
1390
|
+
return new Promise((resolve) => {
|
|
1391
|
+
this.settle = resolve;
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
/** Value at `nowMs`; the first sample after a retarget starts its clock. */
|
|
1395
|
+
sample(nowMs) {
|
|
1396
|
+
if (this.settle === null) return this.value;
|
|
1397
|
+
this.startMs ??= nowMs;
|
|
1398
|
+
const progress = flightProgress(nowMs - this.startMs, this.durationMs);
|
|
1399
|
+
this.value = progress >= 1 ? this.to : this.from + (this.to - this.from) * this.easing(progress);
|
|
1400
|
+
if (progress >= 1) this.finish(true);
|
|
1401
|
+
return this.value;
|
|
1402
|
+
}
|
|
1403
|
+
/** Arrives at the target now, e.g. for reduced motion or when nothing renders the frames. */
|
|
1404
|
+
jump() {
|
|
1405
|
+
this.value = this.to;
|
|
1406
|
+
this.finish(true);
|
|
1407
|
+
}
|
|
1408
|
+
cancel() {
|
|
1409
|
+
this.finish(false);
|
|
1410
|
+
}
|
|
1411
|
+
finish(arrived) {
|
|
1412
|
+
const settle = this.settle;
|
|
1413
|
+
this.settle = null;
|
|
1414
|
+
settle?.(arrived);
|
|
1415
|
+
}
|
|
1416
|
+
};
|
|
1417
|
+
/**
|
|
1418
|
+
* Animates the focus between selections. The focus is a world point rather than a depth, so its
|
|
1419
|
+
* distance is re-measured from the camera every frame and the selected nodes stay sharp while the
|
|
1420
|
+
* camera orbits, pans or zooms; only a selection change animates. With no target the blur fades out
|
|
1421
|
+
* around the last focus point. A retarget mid-transition continues from the focus shown last.
|
|
1422
|
+
*/
|
|
1423
|
+
var DepthOfFieldFocus = class {
|
|
1424
|
+
transition = null;
|
|
1425
|
+
lastAmount = 0;
|
|
1426
|
+
lastPoint = null;
|
|
1427
|
+
/** Starts a transition from the focus shown in the last frame, e.g. after a selection change. */
|
|
1428
|
+
retarget(durationMs, easing) {
|
|
1429
|
+
this.transition = {
|
|
1430
|
+
durationMs,
|
|
1431
|
+
easing,
|
|
1432
|
+
fromAmount: this.lastAmount,
|
|
1433
|
+
fromPoint: this.lastAmount > 0 ? this.lastPoint : null,
|
|
1434
|
+
startMs: null
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
/** Focus for the frame at `nowMs`. `jump` (reduced motion) skips the animation. */
|
|
1438
|
+
evaluate(nowMs, target, jump) {
|
|
1439
|
+
const transition = this.transition;
|
|
1440
|
+
const toAmount = target ? 1 : 0;
|
|
1441
|
+
let eased = 1;
|
|
1442
|
+
let progress = 1;
|
|
1443
|
+
if (transition && !jump && transition.durationMs > 0) {
|
|
1444
|
+
transition.startMs ??= nowMs;
|
|
1445
|
+
progress = flightProgress(nowMs - transition.startMs, transition.durationMs);
|
|
1446
|
+
eased = progress >= 1 ? 1 : transition.easing(progress);
|
|
1447
|
+
}
|
|
1448
|
+
const fromAmount = transition?.fromAmount ?? toAmount;
|
|
1449
|
+
const fromPoint = transition?.fromPoint ?? null;
|
|
1450
|
+
const amount = fromAmount + (toAmount - fromAmount) * eased;
|
|
1451
|
+
const point = target ? fromPoint ? lerpPoint(fromPoint, target, eased) : target : fromPoint ?? this.lastPoint;
|
|
1452
|
+
const settled = progress >= 1;
|
|
1453
|
+
if (settled) this.transition = null;
|
|
1454
|
+
this.lastAmount = amount;
|
|
1455
|
+
this.lastPoint = point;
|
|
1456
|
+
return {
|
|
1457
|
+
amount,
|
|
1458
|
+
point,
|
|
1459
|
+
settled
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
};
|
|
1463
|
+
function lerpPoint(from, to, t) {
|
|
1464
|
+
return {
|
|
1465
|
+
x: from.x + (to.x - from.x) * t,
|
|
1466
|
+
y: from.y + (to.y - from.y) * t,
|
|
1467
|
+
z: from.z + (to.z - from.z) * t
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
//#endregion
|
|
1471
|
+
//#region src/shader-output.ts
|
|
1472
|
+
/**
|
|
1473
|
+
* Encodes `gl_FragColor.rgb` from the linear working space into the renderer's output color space:
|
|
1474
|
+
* sRGB on the canvas (so a `#8a9096` theme color draws as `#8a9096`), unchanged on linear render
|
|
1475
|
+
* targets. Alpha is left untouched, so anti-aliased coverage and blending are unaffected.
|
|
1476
|
+
*
|
|
1477
|
+
* Append once to every display fragment shader, after its final `gl_FragColor` assignment.
|
|
1478
|
+
* Requires a `ShaderMaterial` (not `RawShaderMaterial`), whose prefix defines `linearToOutputTexel`.
|
|
1479
|
+
*/
|
|
1480
|
+
const OUTPUT_COLOR_SPACE_FRAGMENT = "#include <colorspace_fragment>";
|
|
1481
|
+
//#endregion
|
|
1482
|
+
//#region src/depth-of-field-pass.ts
|
|
1483
|
+
/** Layer the edge mesh joins so the edge depth pass can draw it alone. Picking raycasts layer 0. */
|
|
1484
|
+
const EDGE_DEPTH_LAYER = 1;
|
|
1485
|
+
/** Disk taps around the centre of each half-resolution blur texel. */
|
|
1486
|
+
const BLUR_TAPS = 48;
|
|
1487
|
+
const vertexShader$3 = `
|
|
1488
|
+
varying vec2 vUv;
|
|
1489
|
+
|
|
1490
|
+
void main() {
|
|
1491
|
+
vUv = uv;
|
|
1492
|
+
gl_Position = vec4(position.xy, 0.0, 1.0);
|
|
1493
|
+
}
|
|
1494
|
+
`;
|
|
1495
|
+
const blurRadiusChunk = `
|
|
1496
|
+
#include <packing>
|
|
1497
|
+
uniform sampler2D nodeDepth;
|
|
1498
|
+
uniform sampler2D edgeDepth;
|
|
1499
|
+
uniform ivec2 depthSize;
|
|
1500
|
+
uniform float cameraNear;
|
|
1501
|
+
uniform float cameraFar;
|
|
1502
|
+
uniform float focusDistance;
|
|
1503
|
+
uniform float aperture;
|
|
1504
|
+
uniform float minBlur;
|
|
1505
|
+
uniform float maxBlur;
|
|
1506
|
+
|
|
1507
|
+
float surfaceDistance(ivec2 pixel) {
|
|
1508
|
+
pixel = clamp(pixel, ivec2(0), depthSize - 1);
|
|
1509
|
+
float depth = texelFetch(nodeDepth, pixel, 0).x;
|
|
1510
|
+
if (depth >= 1.0) depth = texelFetch(edgeDepth, pixel, 0).x;
|
|
1511
|
+
return -perspectiveDepthToViewZ(depth, cameraNear, cameraFar);
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
float radiusAt(float surface) {
|
|
1515
|
+
float radius = aperture * abs(surface - focusDistance) / max(surface, 1e-6);
|
|
1516
|
+
return radius < minBlur ? 0.0 : min(radius, maxBlur);
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
float blurRadius(ivec2 pixel) {
|
|
1520
|
+
return radiusAt(surfaceDistance(pixel));
|
|
1521
|
+
}
|
|
1522
|
+
`;
|
|
1523
|
+
const blurFragmentShader = `
|
|
1524
|
+
${blurRadiusChunk}
|
|
1525
|
+
uniform sampler2D colorTexture;
|
|
1526
|
+
layout(location = 0) out highp vec4 blurColor;
|
|
1527
|
+
layout(location = 1) out highp vec4 nearColor;
|
|
1528
|
+
|
|
1529
|
+
const float GOLDEN_ANGLE = 2.39996323;
|
|
1530
|
+
|
|
1531
|
+
void main() {
|
|
1532
|
+
ivec2 base = ivec2(gl_FragCoord.xy) * 2;
|
|
1533
|
+
float centreSurface = surfaceDistance(base);
|
|
1534
|
+
vec4 sum = texelFetch(colorTexture, base, 0);
|
|
1535
|
+
float total = 1.0;
|
|
1536
|
+
vec4 near = vec4(0.0);
|
|
1537
|
+
// Interleaved gradient noise turns the spiral's fixed tap pattern into fine noise that the
|
|
1538
|
+
// composite's bilinear upsample smooths, instead of hatching along blurred edges.
|
|
1539
|
+
float rotation = 6.28318531 * fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715))));
|
|
1540
|
+
for (int index = 0; index < ${BLUR_TAPS}; index++) {
|
|
1541
|
+
float distanceFromCentre = sqrt((float(index) + 0.5) / ${BLUR_TAPS}.0) * maxBlur;
|
|
1542
|
+
float angle = float(index) * GOLDEN_ANGLE + rotation;
|
|
1543
|
+
ivec2 pixel = clamp(base + ivec2(floor(vec2(cos(angle), sin(angle)) * distanceFromCentre + 0.5)), ivec2(0), depthSize - 1);
|
|
1544
|
+
float surface = surfaceDistance(pixel);
|
|
1545
|
+
float radius = radiusAt(surface);
|
|
1546
|
+
float weight = clamp(radius - distanceFromCentre + 1.0, 0.0, 1.0);
|
|
1547
|
+
vec4 color = texelFetch(colorTexture, pixel, 0);
|
|
1548
|
+
sum += color * weight;
|
|
1549
|
+
total += weight;
|
|
1550
|
+
if (surface < focusDistance && surface < centreSurface) {
|
|
1551
|
+
near += color * weight * (maxBlur * maxBlur / max(radius * radius, 1.0));
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
blurColor = sum / total;
|
|
1555
|
+
near /= ${BLUR_TAPS}.0;
|
|
1556
|
+
nearColor = near.a > 1.0 ? near / near.a : near;
|
|
1557
|
+
}
|
|
1558
|
+
`;
|
|
1559
|
+
const compositeFragmentShader = `
|
|
1560
|
+
${blurRadiusChunk}
|
|
1561
|
+
uniform sampler2D sharpTexture;
|
|
1562
|
+
uniform sampler2D blurTexture;
|
|
1563
|
+
uniform sampler2D nearTexture;
|
|
1564
|
+
varying vec2 vUv;
|
|
1565
|
+
|
|
1566
|
+
void main() {
|
|
1567
|
+
ivec2 pixel = ivec2(gl_FragCoord.xy);
|
|
1568
|
+
vec4 near = texture2D(nearTexture, vUv);
|
|
1569
|
+
vec4 sharp = texelFetch(sharpTexture, pixel, 0);
|
|
1570
|
+
sharp = near + sharp * (1.0 - near.a);
|
|
1571
|
+
vec4 blurred = texture2D(blurTexture, vUv);
|
|
1572
|
+
gl_FragColor = mix(sharp, blurred, smoothstep(0.5, 2.0, blurRadius(pixel)));
|
|
1573
|
+
${OUTPUT_COLOR_SPACE_FRAGMENT}
|
|
1574
|
+
}
|
|
1575
|
+
`;
|
|
1576
|
+
function blurUniforms() {
|
|
1577
|
+
return {
|
|
1578
|
+
cameraFar: { value: 1 },
|
|
1579
|
+
aperture: { value: 0 },
|
|
1580
|
+
cameraNear: { value: .1 },
|
|
1581
|
+
depthSize: { value: new Vector2() },
|
|
1582
|
+
edgeDepth: { value: null },
|
|
1583
|
+
focusDistance: { value: 1 },
|
|
1584
|
+
maxBlur: { value: 0 },
|
|
1585
|
+
minBlur: { value: 0 },
|
|
1586
|
+
nodeDepth: { value: null }
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
function fullscreenMaterial(fragmentShader, uniforms, glslVersion = null) {
|
|
1590
|
+
return new ShaderMaterial({
|
|
1591
|
+
blending: NoBlending,
|
|
1592
|
+
depthTest: false,
|
|
1593
|
+
depthWrite: false,
|
|
1594
|
+
fragmentShader,
|
|
1595
|
+
glslVersion,
|
|
1596
|
+
uniforms,
|
|
1597
|
+
vertexShader: vertexShader$3
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
/** One triangle that covers the viewport. */
|
|
1601
|
+
function fullscreenTriangle() {
|
|
1602
|
+
const geometry = new BufferGeometry();
|
|
1603
|
+
geometry.setAttribute("position", new Float32BufferAttribute([
|
|
1604
|
+
-1,
|
|
1605
|
+
-1,
|
|
1606
|
+
0,
|
|
1607
|
+
3,
|
|
1608
|
+
-1,
|
|
1609
|
+
0,
|
|
1610
|
+
-1,
|
|
1611
|
+
3,
|
|
1612
|
+
0
|
|
1613
|
+
], 3));
|
|
1614
|
+
geometry.setAttribute("uv", new Float32BufferAttribute([
|
|
1615
|
+
0,
|
|
1616
|
+
0,
|
|
1617
|
+
2,
|
|
1618
|
+
0,
|
|
1619
|
+
0,
|
|
1620
|
+
2
|
|
1621
|
+
], 2));
|
|
1622
|
+
return geometry;
|
|
1623
|
+
}
|
|
1624
|
+
/**
|
|
1625
|
+
* Renders the scene into a target, the edges into a depth-only target, blurs at half resolution and
|
|
1626
|
+
* composites onto the canvas: three extra draw calls per frame while it runs.
|
|
1627
|
+
* Created only while depth of field has something to blur; `dispose` releases every GPU resource.
|
|
1628
|
+
*/
|
|
1629
|
+
var DepthOfFieldPass = class {
|
|
1630
|
+
sceneTarget;
|
|
1631
|
+
edgeDepthTarget;
|
|
1632
|
+
blurTarget;
|
|
1633
|
+
blurMaterial = fullscreenMaterial(blurFragmentShader, {
|
|
1634
|
+
...blurUniforms(),
|
|
1635
|
+
colorTexture: { value: null }
|
|
1636
|
+
}, GLSL3);
|
|
1637
|
+
compositeMaterial = fullscreenMaterial(compositeFragmentShader, {
|
|
1638
|
+
...blurUniforms(),
|
|
1639
|
+
blurTexture: { value: null },
|
|
1640
|
+
nearTexture: { value: null },
|
|
1641
|
+
sharpTexture: { value: null }
|
|
1642
|
+
});
|
|
1643
|
+
quad = new Mesh(fullscreenTriangle(), this.blurMaterial);
|
|
1644
|
+
quadScene = new Scene().add(this.quad);
|
|
1645
|
+
quadCamera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
|
1646
|
+
size = new Vector2();
|
|
1647
|
+
/** Depth of field needs WebGL 2 for depth textures and sRGB render targets. */
|
|
1648
|
+
static isSupported(renderer) {
|
|
1649
|
+
return typeof WebGL2RenderingContext !== "undefined" && renderer.getContext() instanceof WebGL2RenderingContext;
|
|
1650
|
+
}
|
|
1651
|
+
constructor(antialias) {
|
|
1652
|
+
const color = {
|
|
1653
|
+
colorSpace: SRGBColorSpace,
|
|
1654
|
+
type: UnsignedByteType
|
|
1655
|
+
};
|
|
1656
|
+
this.sceneTarget = new WebGLRenderTarget(1, 1, {
|
|
1657
|
+
...color,
|
|
1658
|
+
depthTexture: new DepthTexture(1, 1),
|
|
1659
|
+
samples: antialias ? 4 : 0
|
|
1660
|
+
});
|
|
1661
|
+
this.edgeDepthTarget = new WebGLRenderTarget(1, 1, { depthTexture: new DepthTexture(1, 1) });
|
|
1662
|
+
this.blurTarget = new WebGLRenderTarget(1, 1, {
|
|
1663
|
+
...color,
|
|
1664
|
+
count: 2
|
|
1665
|
+
});
|
|
1666
|
+
this.quad.frustumCulled = false;
|
|
1667
|
+
}
|
|
1668
|
+
render(renderer, scene, camera, edgeMesh, frame) {
|
|
1669
|
+
const { x: width, y: height } = renderer.getDrawingBufferSize(this.size);
|
|
1670
|
+
this.setSize(width, height);
|
|
1671
|
+
const previousTarget = renderer.getRenderTarget();
|
|
1672
|
+
renderer.setRenderTarget(this.sceneTarget);
|
|
1673
|
+
renderer.render(scene, camera);
|
|
1674
|
+
this.renderEdgeDepth(renderer, scene, camera, edgeMesh);
|
|
1675
|
+
const shared = {
|
|
1676
|
+
aperture: frame.aperturePixels,
|
|
1677
|
+
cameraFar: camera.far,
|
|
1678
|
+
cameraNear: camera.near,
|
|
1679
|
+
depthSize: this.size,
|
|
1680
|
+
edgeDepth: this.edgeDepthTarget.depthTexture,
|
|
1681
|
+
focusDistance: frame.focusDistance,
|
|
1682
|
+
maxBlur: frame.maxBlurPixels,
|
|
1683
|
+
minBlur: frame.minBlurPixels,
|
|
1684
|
+
nodeDepth: this.sceneTarget.depthTexture
|
|
1685
|
+
};
|
|
1686
|
+
this.quad.material = this.blurMaterial;
|
|
1687
|
+
this.setUniforms(this.blurMaterial, {
|
|
1688
|
+
...shared,
|
|
1689
|
+
colorTexture: this.sceneTarget.texture
|
|
1690
|
+
});
|
|
1691
|
+
this.drawQuad(renderer, this.blurTarget);
|
|
1692
|
+
this.quad.material = this.compositeMaterial;
|
|
1693
|
+
this.setUniforms(this.compositeMaterial, {
|
|
1694
|
+
...shared,
|
|
1695
|
+
blurTexture: this.blurTarget.textures[0],
|
|
1696
|
+
nearTexture: this.blurTarget.textures[1],
|
|
1697
|
+
sharpTexture: this.sceneTarget.texture
|
|
1698
|
+
});
|
|
1699
|
+
this.drawQuad(renderer, previousTarget);
|
|
1700
|
+
}
|
|
1701
|
+
dispose() {
|
|
1702
|
+
this.sceneTarget.depthTexture?.dispose();
|
|
1703
|
+
this.sceneTarget.dispose();
|
|
1704
|
+
this.edgeDepthTarget.depthTexture?.dispose();
|
|
1705
|
+
this.edgeDepthTarget.dispose();
|
|
1706
|
+
this.blurTarget.dispose();
|
|
1707
|
+
this.blurMaterial.dispose();
|
|
1708
|
+
this.compositeMaterial.dispose();
|
|
1709
|
+
this.quad.geometry.dispose();
|
|
1710
|
+
}
|
|
1711
|
+
/** Draws the edges alone with depth writes on and colour writes off, then restores their material. */
|
|
1712
|
+
renderEdgeDepth(renderer, scene, camera, edgeMesh) {
|
|
1713
|
+
renderer.setRenderTarget(this.edgeDepthTarget);
|
|
1714
|
+
if (!edgeMesh || edgeMesh.count === 0 || Array.isArray(edgeMesh.material)) {
|
|
1715
|
+
renderer.clear();
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
const material = edgeMesh.material;
|
|
1719
|
+
const { colorWrite, depthTest, depthWrite } = material;
|
|
1720
|
+
const layerMask = camera.layers.mask;
|
|
1721
|
+
const background = scene.background;
|
|
1722
|
+
edgeMesh.layers.enable(EDGE_DEPTH_LAYER);
|
|
1723
|
+
material.colorWrite = false;
|
|
1724
|
+
material.depthTest = true;
|
|
1725
|
+
material.depthWrite = true;
|
|
1726
|
+
camera.layers.set(EDGE_DEPTH_LAYER);
|
|
1727
|
+
scene.background = null;
|
|
1728
|
+
try {
|
|
1729
|
+
renderer.render(scene, camera);
|
|
1730
|
+
} finally {
|
|
1731
|
+
material.colorWrite = colorWrite;
|
|
1732
|
+
material.depthTest = depthTest;
|
|
1733
|
+
material.depthWrite = depthWrite;
|
|
1734
|
+
camera.layers.mask = layerMask;
|
|
1735
|
+
scene.background = background;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
drawQuad(renderer, target) {
|
|
1739
|
+
renderer.setRenderTarget(target);
|
|
1740
|
+
renderer.render(this.quadScene, this.quadCamera);
|
|
1741
|
+
}
|
|
1742
|
+
setUniforms(material, values) {
|
|
1743
|
+
for (const [name, value] of Object.entries(values)) {
|
|
1744
|
+
const uniform = material.uniforms[name];
|
|
1745
|
+
if (uniform) uniform.value = value;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
setSize(width, height) {
|
|
1749
|
+
if (this.sceneTarget.width === width && this.sceneTarget.height === height) return;
|
|
1750
|
+
this.sceneTarget.setSize(width, height);
|
|
1751
|
+
this.edgeDepthTarget.setSize(width, height);
|
|
1752
|
+
const halfWidth = Math.max(1, Math.ceil(width / 2));
|
|
1753
|
+
const halfHeight = Math.max(1, Math.ceil(height / 2));
|
|
1754
|
+
this.blurTarget.setSize(halfWidth, halfHeight);
|
|
1755
|
+
}
|
|
1756
|
+
};
|
|
1757
|
+
//#endregion
|
|
1206
1758
|
//#region src/edge-paths.ts
|
|
1207
1759
|
const AUTO_OFFSET_FRACTION = .18;
|
|
1208
1760
|
function segmentCountForPath(path, tier) {
|
|
@@ -1492,17 +2044,6 @@ function pickClosestEdgeIndex(point, segments) {
|
|
|
1492
2044
|
return nearestEdgeIndex;
|
|
1493
2045
|
}
|
|
1494
2046
|
//#endregion
|
|
1495
|
-
//#region src/shader-output.ts
|
|
1496
|
-
/**
|
|
1497
|
-
* Encodes `gl_FragColor.rgb` from the linear working space into the renderer's output color space:
|
|
1498
|
-
* sRGB on the canvas (so a `#8a9096` theme color draws as `#8a9096`), unchanged on linear render
|
|
1499
|
-
* targets. Alpha is left untouched, so anti-aliased coverage and blending are unaffected.
|
|
1500
|
-
*
|
|
1501
|
-
* Append once to every display fragment shader, after its final `gl_FragColor` assignment.
|
|
1502
|
-
* Requires a `ShaderMaterial` (not `RawShaderMaterial`), whose prefix defines `linearToOutputTexel`.
|
|
1503
|
-
*/
|
|
1504
|
-
const OUTPUT_COLOR_SPACE_FRAGMENT = "#include <colorspace_fragment>";
|
|
1505
|
-
//#endregion
|
|
1506
2047
|
//#region src/edge-rendering.ts
|
|
1507
2048
|
const vertexShader$2 = `
|
|
1508
2049
|
attribute float instanceKind;
|
|
@@ -2756,6 +3297,8 @@ function prefersReducedMotion() {
|
|
|
2756
3297
|
function disposeMaterial(material) {
|
|
2757
3298
|
for (const item of Array.isArray(material) ? material : [material]) item.dispose();
|
|
2758
3299
|
}
|
|
3300
|
+
/** One warning per page when a context cannot run depth of field; diagnostics report it per instance. */
|
|
3301
|
+
let warnedDepthOfFieldUnsupported = false;
|
|
2759
3302
|
/** Canvas input that cancels a running `flyTo`, so people take over the camera at once. */
|
|
2760
3303
|
const FLIGHT_CANCEL_EVENTS = [
|
|
2761
3304
|
"keydown",
|
|
@@ -2836,12 +3379,21 @@ var Graphraum = class {
|
|
|
2836
3379
|
/** True only during the orbit's own `controls.update(dt)`; see `handleViewChange`. */
|
|
2837
3380
|
orbitUpdating = false;
|
|
2838
3381
|
destroyed = false;
|
|
3382
|
+
canvasWidth = 0;
|
|
3383
|
+
canvasHeight = 0;
|
|
2839
3384
|
cpuFrameMilliseconds = 0;
|
|
2840
3385
|
gpuFrameMilliseconds = null;
|
|
2841
3386
|
gpuTimer = null;
|
|
2842
3387
|
viewListeners = /* @__PURE__ */ new Set();
|
|
2843
3388
|
focusListeners = /* @__PURE__ */ new Set();
|
|
2844
3389
|
lastHoveredNodeId = null;
|
|
3390
|
+
antialias;
|
|
3391
|
+
depthOfField = DEPTH_OF_FIELD_DEFAULTS;
|
|
3392
|
+
depthOfFieldFocus = new DepthOfFieldFocus();
|
|
3393
|
+
/** Strength actually applied: fades toward `strength` while enabled and toward 0 when disabled. */
|
|
3394
|
+
depthOfFieldStrength = new Tween(0);
|
|
3395
|
+
depthOfFieldPass = null;
|
|
3396
|
+
depthOfFieldStatus = "off";
|
|
2845
3397
|
constructor(container, options = {}) {
|
|
2846
3398
|
this.container = container;
|
|
2847
3399
|
this.mode = options.mode ?? "2d";
|
|
@@ -2854,8 +3406,13 @@ var Graphraum = class {
|
|
|
2854
3406
|
if (!Number.isSafeInteger(this.maxVisibleNodes) || this.maxVisibleNodes < 1) throw new Error("Maximum visible nodes must be a positive integer.");
|
|
2855
3407
|
if (!Number.isFinite(this.viewportOverscan) || this.viewportOverscan < 0) throw new Error("Viewport overscan must be a non-negative finite number.");
|
|
2856
3408
|
this.theme = resolveGraphraumTheme(options.theme);
|
|
3409
|
+
if (options.depthOfField !== void 0) {
|
|
3410
|
+
this.depthOfField = resolveDepthOfFieldOptions(options.depthOfField);
|
|
3411
|
+
this.depthOfFieldStrength.retarget(this.depthOfFieldTargetStrength(), 0, resolveEasing("linear"));
|
|
3412
|
+
}
|
|
3413
|
+
this.antialias = options.antialias ?? false;
|
|
2857
3414
|
this.renderer = new WebGLRenderer({
|
|
2858
|
-
antialias:
|
|
3415
|
+
antialias: this.antialias,
|
|
2859
3416
|
alpha: true
|
|
2860
3417
|
});
|
|
2861
3418
|
this.applyBackground();
|
|
@@ -3312,6 +3869,10 @@ var Graphraum = class {
|
|
|
3312
3869
|
const changedNodeIds = new Set([...currentSelection, ...nextSelection].filter((nodeId) => currentSelection.has(nodeId) !== nextSelection.has(nodeId)));
|
|
3313
3870
|
if (changedNodeIds.size === 0) return;
|
|
3314
3871
|
this.setNodeStateIds(state, nextSelection);
|
|
3872
|
+
if (state === "selected" && this.depthOfField.enabled) {
|
|
3873
|
+
const { durationMs, easing } = this.depthOfField.transition;
|
|
3874
|
+
this.depthOfFieldFocus.retarget(durationMs, resolveEasing(easing));
|
|
3875
|
+
}
|
|
3315
3876
|
this.applySelectionColors(changedNodeIds);
|
|
3316
3877
|
this.requestRender();
|
|
3317
3878
|
if (state === "selected" || state === "hovered") this.notifyFocusChange();
|
|
@@ -3376,6 +3937,33 @@ var Graphraum = class {
|
|
|
3376
3937
|
if (this.autoOrbit && this.autoOrbit.status !== "off") this.autoOrbit.setResumeAfterMs(resumeAfterMs);
|
|
3377
3938
|
else this.autoOrbit = new AutoOrbitLoop(this.renderer.domElement, this.controls, resumeAfterMs, this.stepAutoOrbit);
|
|
3378
3939
|
}
|
|
3940
|
+
/**
|
|
3941
|
+
* Updates 3D depth of field; omitted fields keep their current values. Enabling, disabling and a
|
|
3942
|
+
* new `strength` fade the blur over `transition` unless `update.animate` is `false`; a change
|
|
3943
|
+
* during a fade continues from the blur shown last. With `focus: "selection"` the focal plane
|
|
3944
|
+
* follows the mean position of the selected nodes, re-measured from the camera every frame, and
|
|
3945
|
+
* moves over `transition` when the selection changes; with nothing selected the blur fades out.
|
|
3946
|
+
* `prefers-reduced-motion: reduce` jumps. Resolves `true` when the blur reaches the new strength
|
|
3947
|
+
* and `false` when a later call or `destroy()` interrupts the fade. While it blurs each frame adds
|
|
3948
|
+
* three draw calls; disabled, or with nothing to blur, it costs nothing. A no-op in 2D. Picking
|
|
3949
|
+
* and DOM overlays are unaffected. Throws for invalid values.
|
|
3950
|
+
*/
|
|
3951
|
+
setDepthOfField(options, update = {}) {
|
|
3952
|
+
const animate = update.animate ?? true;
|
|
3953
|
+
if (typeof animate !== "boolean") throw new Error(`setDepthOfField option animate must be a boolean; received ${String(animate)}.`);
|
|
3954
|
+
const previous = this.depthOfField;
|
|
3955
|
+
this.depthOfField = resolveDepthOfFieldOptions(options, previous);
|
|
3956
|
+
if (this.destroyed) return Promise.resolve(false);
|
|
3957
|
+
const { durationMs, easing } = this.depthOfField.transition;
|
|
3958
|
+
if (this.depthOfField.enabled && !previous.enabled) this.depthOfFieldFocus.retarget(0, resolveEasing(easing));
|
|
3959
|
+
const fadeMs = animate && !prefersReducedMotion() ? durationMs : 0;
|
|
3960
|
+
const done = this.depthOfFieldStrength.retarget(this.depthOfFieldTargetStrength(), fadeMs, resolveEasing(easing));
|
|
3961
|
+
this.requestRender();
|
|
3962
|
+
return done;
|
|
3963
|
+
}
|
|
3964
|
+
depthOfFieldTargetStrength() {
|
|
3965
|
+
return this.depthOfField.enabled ? this.depthOfField.strength : 0;
|
|
3966
|
+
}
|
|
3379
3967
|
getNodePresentation(nodeId) {
|
|
3380
3968
|
return this.nodePresentations.get(nodeId) ?? null;
|
|
3381
3969
|
}
|
|
@@ -3470,6 +4058,7 @@ var Graphraum = class {
|
|
|
3470
4058
|
aggregatedNodeClusters: this.densityLodActive ? this.visibleNodeCount : 0,
|
|
3471
4059
|
autoOrbit: this.autoOrbit?.status ?? "off",
|
|
3472
4060
|
cpuFrameMilliseconds: this.cpuFrameMilliseconds,
|
|
4061
|
+
depthOfField: this.depthOfFieldStatus,
|
|
3473
4062
|
gpuFrameMilliseconds: this.gpuFrameMilliseconds,
|
|
3474
4063
|
gpuDrawCalls: this.renderer.info.render.calls,
|
|
3475
4064
|
gpuGeometries: this.renderer.info.memory.geometries,
|
|
@@ -3578,6 +4167,71 @@ var Graphraum = class {
|
|
|
3578
4167
|
this.requestRender();
|
|
3579
4168
|
});
|
|
3580
4169
|
}
|
|
4170
|
+
/**
|
|
4171
|
+
* The camera pose as plain JSON. 2D reports the centre and the world height the canvas shows; 3D
|
|
4172
|
+
* reports the camera position and the point it orbits. Pass it to `setCameraState()` to return
|
|
4173
|
+
* to the same view, for example after reloading a saved one.
|
|
4174
|
+
*/
|
|
4175
|
+
getCameraState() {
|
|
4176
|
+
const camera = this.camera;
|
|
4177
|
+
if (camera instanceof OrthographicCamera) return {
|
|
4178
|
+
center: {
|
|
4179
|
+
x: camera.position.x,
|
|
4180
|
+
y: camera.position.y
|
|
4181
|
+
},
|
|
4182
|
+
mode: "2d",
|
|
4183
|
+
visibleHeight: (camera.top - camera.bottom) / camera.zoom
|
|
4184
|
+
};
|
|
4185
|
+
const { target } = this.controls;
|
|
4186
|
+
return {
|
|
4187
|
+
mode: "3d",
|
|
4188
|
+
position: {
|
|
4189
|
+
x: camera.position.x,
|
|
4190
|
+
y: camera.position.y,
|
|
4191
|
+
z: camera.position.z
|
|
4192
|
+
},
|
|
4193
|
+
target: {
|
|
4194
|
+
x: target.x,
|
|
4195
|
+
y: target.y,
|
|
4196
|
+
z: target.z
|
|
4197
|
+
}
|
|
4198
|
+
};
|
|
4199
|
+
}
|
|
4200
|
+
/**
|
|
4201
|
+
* Puts the camera on a pose from `getCameraState()` at once, without fitting or animating, and
|
|
4202
|
+
* cancels a running `flyTo`. Throws for a state of the other mode (switch with `setMode()` first)
|
|
4203
|
+
* or with a non-finite number. 2D keeps the saved height and takes the width from the canvas, so a
|
|
4204
|
+
* view saved on one canvas size fits another. 3D derives the clip planes from the pose so the whole
|
|
4205
|
+
* graph stays in view. Apply it after the canvas has its size: a 2D resize to a new size refits.
|
|
4206
|
+
*/
|
|
4207
|
+
setCameraState(state) {
|
|
4208
|
+
const next = validateCameraState(state, this.mode);
|
|
4209
|
+
this.cancelFlight();
|
|
4210
|
+
const camera = this.camera;
|
|
4211
|
+
const width = Math.max(this.container.clientWidth, 1);
|
|
4212
|
+
const height = Math.max(this.container.clientHeight, 1);
|
|
4213
|
+
if (next.mode === "2d" && camera instanceof OrthographicCamera) {
|
|
4214
|
+
setOrthographicFrustum(camera, width / height, next.visibleHeight);
|
|
4215
|
+
camera.position.set(next.center.x, next.center.y, camera.position.z);
|
|
4216
|
+
this.controls.target.set(next.center.x, next.center.y, this.controls.target.z);
|
|
4217
|
+
camera.lookAt(this.controls.target);
|
|
4218
|
+
camera.updateProjectionMatrix();
|
|
4219
|
+
this.controls.update();
|
|
4220
|
+
this.materializeViewport();
|
|
4221
|
+
} else if (next.mode === "3d" && camera instanceof PerspectiveCamera) {
|
|
4222
|
+
const target = new Vector3(next.target.x, next.target.y, next.target.z);
|
|
4223
|
+
camera.position.set(next.position.x, next.position.y, next.position.z);
|
|
4224
|
+
this.controls.target.copy(target);
|
|
4225
|
+
camera.lookAt(target);
|
|
4226
|
+
applyPerspectiveContainerAspect(camera, width, height);
|
|
4227
|
+
const clip = perspectiveClipRange(camera.position.distanceTo(target), this.graphRadiusAround(target));
|
|
4228
|
+
camera.near = clip.near;
|
|
4229
|
+
camera.far = clip.far;
|
|
4230
|
+
camera.updateProjectionMatrix();
|
|
4231
|
+
this.controls.update();
|
|
4232
|
+
}
|
|
4233
|
+
this.requestRender();
|
|
4234
|
+
}
|
|
3581
4235
|
/** Nodes whose id is in `nodeIds`, in graph order; every node when `nodeIds` is omitted. */
|
|
3582
4236
|
nodesWithIds(nodeIds) {
|
|
3583
4237
|
if (!nodeIds) return this.data.nodes;
|
|
@@ -3725,11 +4379,14 @@ var Graphraum = class {
|
|
|
3725
4379
|
resize() {
|
|
3726
4380
|
const width = Math.max(this.container.clientWidth, 1);
|
|
3727
4381
|
const height = Math.max(this.container.clientHeight, 1);
|
|
4382
|
+
const sizeChanged = this.canvasWidth !== width || this.canvasHeight !== height;
|
|
4383
|
+
this.canvasWidth = width;
|
|
4384
|
+
this.canvasHeight = height;
|
|
3728
4385
|
this.renderer.setSize(width, height, false);
|
|
3729
4386
|
if (this.camera instanceof PerspectiveCamera) {
|
|
3730
4387
|
applyPerspectiveContainerAspect(this.camera, width, height);
|
|
3731
4388
|
this.camera.updateProjectionMatrix();
|
|
3732
|
-
} else if (this.camera instanceof OrthographicCamera) {
|
|
4389
|
+
} else if (this.camera instanceof OrthographicCamera && sizeChanged) {
|
|
3733
4390
|
this.fitView();
|
|
3734
4391
|
return;
|
|
3735
4392
|
}
|
|
@@ -3749,6 +4406,8 @@ var Graphraum = class {
|
|
|
3749
4406
|
if (this.frameRequest !== null) cancelAnimationFrame(this.frameRequest);
|
|
3750
4407
|
if (this.gpuTimer) for (const query of this.gpuTimer.pending) this.gpuTimer.context.deleteQuery(query);
|
|
3751
4408
|
this.disposeBackgroundTexture();
|
|
4409
|
+
this.depthOfFieldStrength.cancel();
|
|
4410
|
+
this.disposeDepthOfFieldPass();
|
|
3752
4411
|
this.disposeGraphObjects();
|
|
3753
4412
|
this.renderer.dispose();
|
|
3754
4413
|
this.renderer.domElement.remove();
|
|
@@ -3774,7 +4433,16 @@ var Graphraum = class {
|
|
|
3774
4433
|
const query = timer?.context.createQuery() ?? null;
|
|
3775
4434
|
if (timer && query) timer.context.beginQuery(timer.extension.TIME_ELAPSED_EXT, query);
|
|
3776
4435
|
const startedAt = performance.now();
|
|
3777
|
-
this.
|
|
4436
|
+
const depthOfField = this.prepareDepthOfField(startedAt);
|
|
4437
|
+
if (depthOfField && this.depthOfFieldPass && this.camera instanceof PerspectiveCamera) {
|
|
4438
|
+
this.renderer.info.autoReset = false;
|
|
4439
|
+
this.renderer.info.reset();
|
|
4440
|
+
try {
|
|
4441
|
+
this.depthOfFieldPass.render(this.renderer, this.scene, this.camera, this.edgeMesh, depthOfField);
|
|
4442
|
+
} finally {
|
|
4443
|
+
this.renderer.info.autoReset = true;
|
|
4444
|
+
}
|
|
4445
|
+
} else this.renderer.render(this.scene, this.camera);
|
|
3778
4446
|
this.cpuFrameMilliseconds = performance.now() - startedAt;
|
|
3779
4447
|
if (timer && query) {
|
|
3780
4448
|
timer.context.endQuery(timer.extension.TIME_ELAPSED_EXT);
|
|
@@ -3782,6 +4450,81 @@ var Graphraum = class {
|
|
|
3782
4450
|
}
|
|
3783
4451
|
for (const listener of this.viewListeners) listener();
|
|
3784
4452
|
}
|
|
4453
|
+
/**
|
|
4454
|
+
* Decides whether this frame blurs and records why in the diagnostics status. Returns null to
|
|
4455
|
+
* render directly. Requests another frame while a fade or focus move runs; both end on their own.
|
|
4456
|
+
*/
|
|
4457
|
+
prepareDepthOfField(nowMs) {
|
|
4458
|
+
const options = this.depthOfField;
|
|
4459
|
+
const strengthTween = this.depthOfFieldStrength;
|
|
4460
|
+
if (!options.enabled && strengthTween.settled) {
|
|
4461
|
+
this.depthOfFieldStatus = "off";
|
|
4462
|
+
this.disposeDepthOfFieldPass();
|
|
4463
|
+
return null;
|
|
4464
|
+
}
|
|
4465
|
+
if (!(this.camera instanceof PerspectiveCamera)) {
|
|
4466
|
+
strengthTween.jump();
|
|
4467
|
+
this.depthOfFieldStatus = options.enabled ? "inactive" : "off";
|
|
4468
|
+
this.disposeDepthOfFieldPass();
|
|
4469
|
+
return null;
|
|
4470
|
+
}
|
|
4471
|
+
if (!DepthOfFieldPass.isSupported(this.renderer)) {
|
|
4472
|
+
strengthTween.jump();
|
|
4473
|
+
this.depthOfFieldStatus = options.enabled ? "unsupported" : "off";
|
|
4474
|
+
if (options.enabled && !warnedDepthOfFieldUnsupported) {
|
|
4475
|
+
warnedDepthOfFieldUnsupported = true;
|
|
4476
|
+
console.warn("graphraum: depth of field needs a WebGL 2 context and is turned off for this canvas. getDiagnostics().depthOfField reports \"unsupported\".");
|
|
4477
|
+
}
|
|
4478
|
+
return null;
|
|
4479
|
+
}
|
|
4480
|
+
const strength = strengthTween.sample(nowMs);
|
|
4481
|
+
if (!strengthTween.settled) this.requestRender();
|
|
4482
|
+
this.camera.updateMatrixWorld();
|
|
4483
|
+
let focusDistance = typeof options.focus === "number" ? options.focus : null;
|
|
4484
|
+
let amount = 1;
|
|
4485
|
+
if (options.focus === "selection") {
|
|
4486
|
+
const focus = this.depthOfFieldFocus.evaluate(nowMs, this.selectionFocusPoint(), prefersReducedMotion());
|
|
4487
|
+
if (!focus.settled) this.requestRender();
|
|
4488
|
+
amount = focus.amount;
|
|
4489
|
+
if (focus.point) focusDistance = viewDistance(focus.point, this.camera.matrixWorldInverse.elements);
|
|
4490
|
+
}
|
|
4491
|
+
if (focusDistance === null) {
|
|
4492
|
+
this.depthOfFieldStatus = options.enabled ? "inactive" : "off";
|
|
4493
|
+
return null;
|
|
4494
|
+
}
|
|
4495
|
+
focusDistance = Math.max(focusDistance, this.camera.near);
|
|
4496
|
+
const pixelRatio = this.renderer.getPixelRatio();
|
|
4497
|
+
const aperturePixels = strength * amount * distanceFade(focusDistance, options.nearDistance, options.farDistance) * 24 * pixelRatio;
|
|
4498
|
+
const maxBlurPixels = options.maxBlur * pixelRatio;
|
|
4499
|
+
if (aperturePixels < .25 || maxBlurPixels < .25) {
|
|
4500
|
+
this.depthOfFieldStatus = options.enabled ? "inactive" : "off";
|
|
4501
|
+
return null;
|
|
4502
|
+
}
|
|
4503
|
+
this.depthOfFieldPass ??= new DepthOfFieldPass(this.antialias);
|
|
4504
|
+
this.depthOfFieldStatus = "active";
|
|
4505
|
+
return {
|
|
4506
|
+
aperturePixels,
|
|
4507
|
+
focusDistance,
|
|
4508
|
+
maxBlurPixels,
|
|
4509
|
+
minBlurPixels: options.minBlur * pixelRatio
|
|
4510
|
+
};
|
|
4511
|
+
}
|
|
4512
|
+
/** Mean world position of the selected nodes, read every frame so a running layout keeps them in focus. */
|
|
4513
|
+
selectionFocusPoint() {
|
|
4514
|
+
return meanPoint([...this.selectedNodeIds].flatMap((id) => {
|
|
4515
|
+
const index = this.nodeIndices.get(id);
|
|
4516
|
+
const node = index === void 0 ? void 0 : this.data.nodes[index];
|
|
4517
|
+
return node ? [{
|
|
4518
|
+
x: node.position.x,
|
|
4519
|
+
y: node.position.y,
|
|
4520
|
+
z: node.position.z ?? 0
|
|
4521
|
+
}] : [];
|
|
4522
|
+
}));
|
|
4523
|
+
}
|
|
4524
|
+
disposeDepthOfFieldPass() {
|
|
4525
|
+
this.depthOfFieldPass?.dispose();
|
|
4526
|
+
this.depthOfFieldPass = null;
|
|
4527
|
+
}
|
|
3785
4528
|
stopAutoOrbit() {
|
|
3786
4529
|
this.autoOrbit?.dispose();
|
|
3787
4530
|
this.autoOrbit = null;
|