@andespindola/brainlink 0.1.0-beta.112 → 0.1.0-beta.114
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/application/frontend/client-js.js +259 -52
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -595,6 +595,8 @@ The graph UI shows:
|
|
|
595
595
|
- realtime refresh while `--watch` is enabled
|
|
596
596
|
- graph controls for zoom in, zoom out, fit visible nodes and reset-to-fit-all
|
|
597
597
|
- wheel zoom (including `cmd+scroll` and `ctrl+scroll`) anchored to cursor position for faster navigation in large graphs
|
|
598
|
+
- continuous target-scale interpolation for wheel/button zoom to avoid abrupt LOD jumps while keeping cursor-anchored focus
|
|
599
|
+
- bloom-like navigation profile: wheel zoom now biases focus toward hovered/selected/hub nodes and applies subtle camera-anchor parallax so depth remains stable while zooming and panning
|
|
598
600
|
- zoom-out floor for large and massive graphs, plus reset macro floor tied to hub-neighbor distance and first-level cluster spacing, with a tighter initial camera so the first graph appears as a closer cell instead of a distant mesh
|
|
599
601
|
- keyboard shortcuts: `+` zoom in, `-` zoom out, `0` reset fit
|
|
600
602
|
- double-click on canvas zooms in at cursor position
|
|
@@ -13,8 +13,8 @@ const macroGalaxyZoomThreshold = 0.012
|
|
|
13
13
|
const macroGalaxyEnterHysteresis = 0.86
|
|
14
14
|
const macroGalaxyExitHysteresis = 1.24
|
|
15
15
|
const galaxyDiscoveryEnabled = false
|
|
16
|
-
const massiveAutoFitMacroScale = 0.
|
|
17
|
-
const defaultMacroScale = 0.
|
|
16
|
+
const massiveAutoFitMacroScale = 0.018
|
|
17
|
+
const defaultMacroScale = 0.018
|
|
18
18
|
const clusterCellPixelSize = 64
|
|
19
19
|
const minNodePixelRadius = 2.3
|
|
20
20
|
const viewportPaddingPx = 280
|
|
@@ -28,32 +28,38 @@ const ecosystemHubEdgeLimit = 120
|
|
|
28
28
|
const ecosystemSiblingEdgeLimit = 180
|
|
29
29
|
const ecosystemClusterScaleThreshold = 0.78
|
|
30
30
|
const massiveEcosystemClusterScaleThreshold = 4.2
|
|
31
|
+
const ecosystemClusterEnterHysteresis = 0.94
|
|
32
|
+
const ecosystemClusterExitHysteresis = 1.1
|
|
31
33
|
const ecosystemSubgraphScaleThreshold = 0.18
|
|
32
34
|
const ecosystemMicroScaleThreshold = 0.08
|
|
33
35
|
const ecosystemFocusedParentLimit = 2
|
|
34
36
|
const ecosystemDepthNear = 80
|
|
35
37
|
const ecosystemDepthFar = 2600
|
|
36
|
-
const ecosystemDepthPerspective =
|
|
37
|
-
const ecosystemDepthTiltY = 0.
|
|
38
|
-
const ecosystemDepthYaw = 0.
|
|
39
|
-
const ecosystemDepthPitch = 0.
|
|
40
|
-
const ecosystemDepthRadialGain = 0.
|
|
38
|
+
const ecosystemDepthPerspective = 560
|
|
39
|
+
const ecosystemDepthTiltY = 0.3
|
|
40
|
+
const ecosystemDepthYaw = 0.3
|
|
41
|
+
const ecosystemDepthPitch = 0.24
|
|
42
|
+
const ecosystemDepthRadialGain = 0.13
|
|
41
43
|
const ecosystemDepthOrbitalMaxOffset = 160
|
|
42
|
-
const ecosystemDepthMinScale = 0.
|
|
43
|
-
const ecosystemDepthOpacityFloor = 0.
|
|
44
|
+
const ecosystemDepthMinScale = 0.2
|
|
45
|
+
const ecosystemDepthOpacityFloor = 0.16
|
|
44
46
|
const graphDepthNear = 40
|
|
45
|
-
const graphDepthFar =
|
|
46
|
-
const graphDepthPerspective =
|
|
47
|
-
const graphDepthYaw = 0.
|
|
48
|
-
const graphDepthPitch = 0.
|
|
49
|
-
const graphDepthRadialGain = 0.
|
|
50
|
-
const graphDepthMinScale = 0.
|
|
51
|
-
const graphDepthOpacityFloor = 0.
|
|
52
|
-
const graphDepthEdgeOpacityFloor = 0.
|
|
47
|
+
const graphDepthFar = 1320
|
|
48
|
+
const graphDepthPerspective = 430
|
|
49
|
+
const graphDepthYaw = 0.42
|
|
50
|
+
const graphDepthPitch = 0.3
|
|
51
|
+
const graphDepthRadialGain = 0.24
|
|
52
|
+
const graphDepthMinScale = 0.34
|
|
53
|
+
const graphDepthOpacityFloor = 0.22
|
|
54
|
+
const graphDepthEdgeOpacityFloor = 0.12
|
|
53
55
|
const graphDepthProjectionNodeThreshold = 40
|
|
54
|
-
const graphDepthProjectionNodeCap =
|
|
55
|
-
const graphDepthProjectionMinScale = 0.
|
|
56
|
-
const graphDepthProjectionMaxScale = 1.
|
|
56
|
+
const graphDepthProjectionNodeCap = 2600
|
|
57
|
+
const graphDepthProjectionMinScale = 0.03
|
|
58
|
+
const graphDepthProjectionMaxScale = 1.7
|
|
59
|
+
const graphDepthProjectionEnterMinScale = graphDepthProjectionMinScale * 1.08
|
|
60
|
+
const graphDepthProjectionExitMinScale = graphDepthProjectionMinScale * 0.88
|
|
61
|
+
const graphDepthProjectionEnterMaxScale = graphDepthProjectionMaxScale * 0.92
|
|
62
|
+
const graphDepthProjectionExitMaxScale = graphDepthProjectionMaxScale * 1.16
|
|
57
63
|
const zoomRecoveryGuardMs = 4200
|
|
58
64
|
const zoomCapTargetViewportShare = 0.72
|
|
59
65
|
const meshEdgeScaleThreshold = 0.09
|
|
@@ -65,6 +71,17 @@ const dragSettleRounds = 3
|
|
|
65
71
|
const wheelZoomExponent = 0.0009
|
|
66
72
|
const wheelZoomExponentCap = 0.035
|
|
67
73
|
const wheelZoomModifierBoost = 1.08
|
|
74
|
+
const wheelZoomInputFloorCap = 0.976
|
|
75
|
+
const wheelZoomInputCeilCap = 1.024
|
|
76
|
+
const zoomAnimationSlowLerp = 0.18
|
|
77
|
+
const zoomAnimationFastLerp = 0.36
|
|
78
|
+
const zoomAnimationScaleSnap = 0.00008
|
|
79
|
+
const zoomAnimationPositionSnap = 0.14
|
|
80
|
+
const bloomZoomFocusMaxScreenDistance = 260
|
|
81
|
+
const bloomZoomFocusFarStrength = 0.66
|
|
82
|
+
const bloomZoomFocusNearStrength = 0.34
|
|
83
|
+
const bloomCameraParallaxNearStrength = 0.2
|
|
84
|
+
const bloomCameraParallaxFarStrength = 0.06
|
|
68
85
|
const physicsDragFrameIntervalMs = 16
|
|
69
86
|
const physicsIdleFrameIntervalMs = 78
|
|
70
87
|
const physicsLargeGraphIdleFrameIntervalMs = 108
|
|
@@ -123,7 +140,18 @@ const state = {
|
|
|
123
140
|
lastHoverHitAt: 0,
|
|
124
141
|
lastManualZoomAt: 0,
|
|
125
142
|
lastZoomFocus: { x: 0, y: 0, at: 0 },
|
|
126
|
-
macroViewActive: false
|
|
143
|
+
macroViewActive: false,
|
|
144
|
+
ecosystemViewActive: false,
|
|
145
|
+
depthProjectionActive: false,
|
|
146
|
+
zoomTransition: {
|
|
147
|
+
active: false,
|
|
148
|
+
source: 'generic',
|
|
149
|
+
screenX: 0,
|
|
150
|
+
screenY: 0,
|
|
151
|
+
worldX: 0,
|
|
152
|
+
worldY: 0,
|
|
153
|
+
targetScale: 1
|
|
154
|
+
}
|
|
127
155
|
}
|
|
128
156
|
|
|
129
157
|
const byId = id => document.getElementById(id)
|
|
@@ -1247,19 +1275,20 @@ const projectEcosystemPoint = (x, y, depth, anchor) => {
|
|
|
1247
1275
|
|
|
1248
1276
|
const applyEcosystemDepthProjection = (clusters, edges, anchor) => {
|
|
1249
1277
|
const levelIndexMap = ecosystemLevelIndexBySize()
|
|
1278
|
+
const effectiveAnchor = applyBloomCameraParallax(anchor)
|
|
1250
1279
|
const projectedClusters = []
|
|
1251
1280
|
const clusterById = new Map()
|
|
1252
1281
|
|
|
1253
1282
|
for (let index = 0; index < clusters.length; index += 1) {
|
|
1254
1283
|
const cluster = clusters[index]
|
|
1255
1284
|
const baseDepth = ecosystemDepthForCluster(cluster, levelIndexMap)
|
|
1256
|
-
const radialDistance = Math.hypot(cluster.x -
|
|
1285
|
+
const radialDistance = Math.hypot(cluster.x - effectiveAnchor.x, cluster.y - effectiveAnchor.y)
|
|
1257
1286
|
const radialOffset = cluster.isHub ? 0 : Math.min(320, radialDistance * ecosystemDepthRadialGain)
|
|
1258
1287
|
const orbitalOffset = cluster.isHub
|
|
1259
1288
|
? 0
|
|
1260
|
-
: Math.sin(Math.atan2(cluster.y -
|
|
1289
|
+
: Math.sin(Math.atan2(cluster.y - effectiveAnchor.y, cluster.x - effectiveAnchor.x) * 2.2) * ecosystemDepthOrbitalMaxOffset
|
|
1261
1290
|
const depth = Math.max(0, baseDepth + radialOffset + orbitalOffset)
|
|
1262
|
-
const projected = projectEcosystemPoint(cluster.x, cluster.y, depth,
|
|
1291
|
+
const projected = projectEcosystemPoint(cluster.x, cluster.y, depth, effectiveAnchor)
|
|
1263
1292
|
const baseOpacity = Number.isFinite(cluster.lodOpacity) ? cluster.lodOpacity : 1
|
|
1264
1293
|
const depthScale = ecosystemDepthMinScale + (1 - ecosystemDepthMinScale) * projected.factor
|
|
1265
1294
|
const depthOpacity = Math.max(
|
|
@@ -1591,12 +1620,107 @@ const cursorWorldPoint = () => {
|
|
|
1591
1620
|
return screenToWorldPoint(screenX, screenY)
|
|
1592
1621
|
}
|
|
1593
1622
|
|
|
1623
|
+
const bloomZoomFocusStrength = (scale) => {
|
|
1624
|
+
if (scale <= 0.08) return bloomZoomFocusFarStrength
|
|
1625
|
+
if (scale <= 0.2) return 0.58
|
|
1626
|
+
if (scale <= 0.45) return 0.5
|
|
1627
|
+
if (scale <= 0.9) return 0.42
|
|
1628
|
+
return bloomZoomFocusNearStrength
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
const bloomCameraParallaxStrength = (scale) => {
|
|
1632
|
+
if (scale <= 0.08) return bloomCameraParallaxNearStrength
|
|
1633
|
+
if (scale <= 0.2) return 0.16
|
|
1634
|
+
if (scale <= 0.45) return 0.12
|
|
1635
|
+
return bloomCameraParallaxFarStrength
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
const screenPointForWorld = (worldX, worldY) => ({
|
|
1639
|
+
x: worldX * state.transform.scale + state.transform.x,
|
|
1640
|
+
y: worldY * state.transform.scale + state.transform.y
|
|
1641
|
+
})
|
|
1642
|
+
|
|
1643
|
+
const bloomZoomFocusCandidate = (screenX, screenY) => {
|
|
1644
|
+
const candidates = [state.hovered, state.selected, state.primaryHub].filter(Boolean)
|
|
1645
|
+
if (candidates.length === 0) {
|
|
1646
|
+
return null
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
let bestNode = null
|
|
1650
|
+
let bestDistance = Number.POSITIVE_INFINITY
|
|
1651
|
+
for (let index = 0; index < candidates.length; index += 1) {
|
|
1652
|
+
const node = candidates[index]
|
|
1653
|
+
const point = screenPointForWorld(nodeRenderX(node), nodeRenderY(node))
|
|
1654
|
+
const distance = Math.hypot(point.x - screenX, point.y - screenY)
|
|
1655
|
+
if (distance < bestDistance) {
|
|
1656
|
+
bestDistance = distance
|
|
1657
|
+
bestNode = node
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
if (!bestNode) {
|
|
1662
|
+
return null
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
const scale = state.transform.scale
|
|
1666
|
+
const allowDistance = scale <= 0.16 ? bloomZoomFocusMaxScreenDistance * 1.7 : bloomZoomFocusMaxScreenDistance
|
|
1667
|
+
if (bestDistance > allowDistance) {
|
|
1668
|
+
return null
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
return {
|
|
1672
|
+
x: nodeRenderX(bestNode),
|
|
1673
|
+
y: nodeRenderY(bestNode)
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
const resolveZoomAnchorWorldPoint = (screenX, screenY, source) => {
|
|
1678
|
+
const cursorPoint = screenToWorldPoint(screenX, screenY)
|
|
1679
|
+
if (source !== 'wheel') {
|
|
1680
|
+
return cursorPoint
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
const focusCandidate = bloomZoomFocusCandidate(screenX, screenY)
|
|
1684
|
+
if (!focusCandidate) {
|
|
1685
|
+
return cursorPoint
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
const strength = bloomZoomFocusStrength(state.transform.scale)
|
|
1689
|
+
return {
|
|
1690
|
+
x: cursorPoint.x + (focusCandidate.x - cursorPoint.x) * strength,
|
|
1691
|
+
y: cursorPoint.y + (focusCandidate.y - cursorPoint.y) * strength
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
const applyBloomCameraParallax = (anchor) => {
|
|
1696
|
+
const cursorPoint = cursorWorldPoint()
|
|
1697
|
+
if (!cursorPoint) {
|
|
1698
|
+
return anchor
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
const strength = bloomCameraParallaxStrength(state.transform.scale)
|
|
1702
|
+
return {
|
|
1703
|
+
x: anchor.x + (cursorPoint.x - anchor.x) * strength,
|
|
1704
|
+
y: anchor.y + (cursorPoint.y - anchor.y) * strength
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1594
1708
|
const visibilityScaleBucket = (scale) => {
|
|
1595
1709
|
const safeScale = Math.max(zoomRange.min, scale)
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1710
|
+
return Math.round(safeScale * 180_000)
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
const shouldRenderEcosystemClusterView = (nodeCount, scale) => {
|
|
1714
|
+
const baseThreshold = nodeCount > massiveGraphNodeThreshold
|
|
1715
|
+
? massiveEcosystemClusterScaleThreshold
|
|
1716
|
+
: ecosystemClusterScaleThreshold
|
|
1717
|
+
const enterThreshold = baseThreshold * ecosystemClusterEnterHysteresis
|
|
1718
|
+
const exitThreshold = baseThreshold * ecosystemClusterExitHysteresis
|
|
1719
|
+
const shouldRender = state.ecosystemViewActive
|
|
1720
|
+
? scale <= exitThreshold
|
|
1721
|
+
: scale <= enterThreshold
|
|
1722
|
+
state.ecosystemViewActive = shouldRender
|
|
1723
|
+
return shouldRender
|
|
1600
1724
|
}
|
|
1601
1725
|
|
|
1602
1726
|
const shouldRenderMacroGalaxyView = () => {
|
|
@@ -2462,7 +2586,7 @@ const currentZoomMax = () => {
|
|
|
2462
2586
|
}
|
|
2463
2587
|
|
|
2464
2588
|
const zoomFloorByNodeCount = (nodeCount) => {
|
|
2465
|
-
if (nodeCount > massiveGraphNodeThreshold) return 0.
|
|
2589
|
+
if (nodeCount > massiveGraphNodeThreshold) return 0.018
|
|
2466
2590
|
if (nodeCount > largeGraphNodeThreshold) return 0.0032
|
|
2467
2591
|
if (nodeCount > ecosystemActivationNodeThreshold) return 0.001
|
|
2468
2592
|
return zoomRange.min
|
|
@@ -2483,6 +2607,52 @@ const clampTransformCoordinate = value => {
|
|
|
2483
2607
|
return value
|
|
2484
2608
|
}
|
|
2485
2609
|
|
|
2610
|
+
const clearZoomTransition = () => {
|
|
2611
|
+
state.zoomTransition.active = false
|
|
2612
|
+
state.zoomTransition.targetScale = state.transform.scale
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
const zoomTransitionLerp = (delta, source) => {
|
|
2616
|
+
const normalizedDelta = Math.max(0, Math.min(1, delta / 32))
|
|
2617
|
+
const base = zoomAnimationSlowLerp + (zoomAnimationFastLerp - zoomAnimationSlowLerp) * normalizedDelta
|
|
2618
|
+
const sourceBoost = source === 'wheel' ? 1 : 1.25
|
|
2619
|
+
return Math.max(0.08, Math.min(0.45, base * sourceBoost))
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
const applyZoomTransition = (delta) => {
|
|
2623
|
+
if (!state.zoomTransition.active) {
|
|
2624
|
+
return
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
const targetScale = clampScale(state.zoomTransition.targetScale)
|
|
2628
|
+
const scaleDelta = targetScale - state.transform.scale
|
|
2629
|
+
const targetX = clampTransformCoordinate(state.zoomTransition.screenX - state.zoomTransition.worldX * targetScale)
|
|
2630
|
+
const targetY = clampTransformCoordinate(state.zoomTransition.screenY - state.zoomTransition.worldY * targetScale)
|
|
2631
|
+
const scaleSettled = Math.abs(scaleDelta) <= zoomAnimationScaleSnap
|
|
2632
|
+
|
|
2633
|
+
if (scaleSettled) {
|
|
2634
|
+
state.transform.scale = targetScale
|
|
2635
|
+
state.transform.x = targetX
|
|
2636
|
+
state.transform.y = targetY
|
|
2637
|
+
clearZoomTransition()
|
|
2638
|
+
return
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
const lerp = zoomTransitionLerp(delta, state.zoomTransition.source)
|
|
2642
|
+
const nextScale = clampScale(state.transform.scale + scaleDelta * lerp)
|
|
2643
|
+
state.transform.scale = nextScale
|
|
2644
|
+
state.transform.x = clampTransformCoordinate(state.zoomTransition.screenX - state.zoomTransition.worldX * nextScale)
|
|
2645
|
+
state.transform.y = clampTransformCoordinate(state.zoomTransition.screenY - state.zoomTransition.worldY * nextScale)
|
|
2646
|
+
const settledX = Math.abs(targetX - state.transform.x) <= zoomAnimationPositionSnap
|
|
2647
|
+
const settledY = Math.abs(targetY - state.transform.y) <= zoomAnimationPositionSnap
|
|
2648
|
+
if (Math.abs(targetScale - nextScale) <= zoomAnimationScaleSnap && settledX && settledY) {
|
|
2649
|
+
state.transform.scale = targetScale
|
|
2650
|
+
state.transform.x = targetX
|
|
2651
|
+
state.transform.y = targetY
|
|
2652
|
+
clearZoomTransition()
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2486
2656
|
const graphBounds = nodes => {
|
|
2487
2657
|
if (nodes.length === 0) return null
|
|
2488
2658
|
let minX = Number.POSITIVE_INFINITY
|
|
@@ -2537,7 +2707,7 @@ const macroFaceToFaceScale = (nodeCount, hubDistance) => {
|
|
|
2537
2707
|
|
|
2538
2708
|
const rect = canvas.getBoundingClientRect()
|
|
2539
2709
|
const viewportReference = Math.max(320, Math.min(rect.width, rect.height))
|
|
2540
|
-
const share = nodeCount > massiveGraphNodeThreshold ? 0.
|
|
2710
|
+
const share = nodeCount > massiveGraphNodeThreshold ? 0.2 : 0.17
|
|
2541
2711
|
const targetPx = Math.max(24, viewportReference * share)
|
|
2542
2712
|
return targetPx / hubDistance
|
|
2543
2713
|
}
|
|
@@ -2576,7 +2746,7 @@ const macroEcosystemFaceScale = (nodeCount) => {
|
|
|
2576
2746
|
|
|
2577
2747
|
const rect = canvas.getBoundingClientRect()
|
|
2578
2748
|
const viewportReference = Math.max(320, Math.min(rect.width, rect.height))
|
|
2579
|
-
const targetShare = nodeCount > massiveGraphNodeThreshold ? 0.
|
|
2749
|
+
const targetShare = nodeCount > massiveGraphNodeThreshold ? 0.28 : 0.24
|
|
2580
2750
|
const targetPx = Math.max(30, viewportReference * targetShare)
|
|
2581
2751
|
return targetPx / nearestDistance
|
|
2582
2752
|
}
|
|
@@ -2589,6 +2759,7 @@ const fitView = (options = { useFiltered: true, macro: false, preferHubCenter: t
|
|
|
2589
2759
|
|
|
2590
2760
|
if (!bounds) {
|
|
2591
2761
|
state.transform = { x: width / 2, y: height / 2, scale: 1 }
|
|
2762
|
+
clearZoomTransition()
|
|
2592
2763
|
state.offscreenFrameCount = 0
|
|
2593
2764
|
state.recoveringViewport = false
|
|
2594
2765
|
markRenderDirty()
|
|
@@ -2638,6 +2809,7 @@ const fitView = (options = { useFiltered: true, macro: false, preferHubCenter: t
|
|
|
2638
2809
|
y: clampTransformCoordinate(height / 2 - centerY * resolvedScale),
|
|
2639
2810
|
scale: clampScale(resolvedScale)
|
|
2640
2811
|
}
|
|
2812
|
+
clearZoomTransition()
|
|
2641
2813
|
state.offscreenFrameCount = 0
|
|
2642
2814
|
state.recoveringViewport = false
|
|
2643
2815
|
markRenderDirty()
|
|
@@ -2662,6 +2834,7 @@ const focusPrimaryHub = () => {
|
|
|
2662
2834
|
y: clampTransformCoordinate(height / 2 - hub.y * targetScale),
|
|
2663
2835
|
scale: targetScale
|
|
2664
2836
|
}
|
|
2837
|
+
clearZoomTransition()
|
|
2665
2838
|
state.offscreenFrameCount = 0
|
|
2666
2839
|
markRenderDirty()
|
|
2667
2840
|
}
|
|
@@ -3068,13 +3241,25 @@ const clusterOpacity = cluster =>
|
|
|
3068
3241
|
const clusterDepth = cluster => Number.isFinite(cluster.depth) ? cluster.depth : ecosystemDepthNear
|
|
3069
3242
|
const clusterDepthScale = cluster => Number.isFinite(cluster.depthScale) ? cluster.depthScale : 1
|
|
3070
3243
|
|
|
3071
|
-
const shouldProjectRenderNodesInDepth = () =>
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3244
|
+
const shouldProjectRenderNodesInDepth = () => {
|
|
3245
|
+
const withinNodeCountWindow =
|
|
3246
|
+
state.renderClusters.length === 0 &&
|
|
3247
|
+
state.renderNodes.length >= graphDepthProjectionNodeThreshold &&
|
|
3248
|
+
state.renderNodes.length <= graphDepthProjectionNodeCap &&
|
|
3249
|
+
!state.pointer.down
|
|
3250
|
+
|
|
3251
|
+
if (!withinNodeCountWindow) {
|
|
3252
|
+
state.depthProjectionActive = false
|
|
3253
|
+
return false
|
|
3254
|
+
}
|
|
3255
|
+
|
|
3256
|
+
const scale = state.transform.scale
|
|
3257
|
+
const shouldProject = state.depthProjectionActive
|
|
3258
|
+
? scale >= graphDepthProjectionExitMinScale && scale <= graphDepthProjectionExitMaxScale
|
|
3259
|
+
: scale >= graphDepthProjectionEnterMinScale && scale <= graphDepthProjectionEnterMaxScale
|
|
3260
|
+
state.depthProjectionActive = shouldProject
|
|
3261
|
+
return shouldProject
|
|
3262
|
+
}
|
|
3078
3263
|
|
|
3079
3264
|
const nodeProjectionAnchor = () => {
|
|
3080
3265
|
const hub = state.primaryHub
|
|
@@ -3114,7 +3299,7 @@ const refreshRenderNodeDepthProjection = () => {
|
|
|
3114
3299
|
return
|
|
3115
3300
|
}
|
|
3116
3301
|
|
|
3117
|
-
const anchor = nodeProjectionAnchor()
|
|
3302
|
+
const anchor = applyBloomCameraParallax(nodeProjectionAnchor())
|
|
3118
3303
|
let maxDistance = 1
|
|
3119
3304
|
for (let index = 0; index < state.renderNodes.length; index += 1) {
|
|
3120
3305
|
const node = state.renderNodes[index]
|
|
@@ -3312,12 +3497,9 @@ const computeRenderVisibility = () => {
|
|
|
3312
3497
|
return
|
|
3313
3498
|
}
|
|
3314
3499
|
|
|
3315
|
-
const ecosystemScaleThreshold = state.visibleNodes.length > massiveGraphNodeThreshold
|
|
3316
|
-
? massiveEcosystemClusterScaleThreshold
|
|
3317
|
-
: ecosystemClusterScaleThreshold
|
|
3318
3500
|
if (
|
|
3319
3501
|
state.ecosystemExpansionLevels.length > 0 &&
|
|
3320
|
-
state.transform.scale
|
|
3502
|
+
shouldRenderEcosystemClusterView(state.visibleNodes.length, state.transform.scale) &&
|
|
3321
3503
|
state.ecosystemClusters.length > 0
|
|
3322
3504
|
) {
|
|
3323
3505
|
const clusters = selectHierarchicalEcosystemClusters(viewport)
|
|
@@ -3331,6 +3513,7 @@ const computeRenderVisibility = () => {
|
|
|
3331
3513
|
state.renderEdges = []
|
|
3332
3514
|
return
|
|
3333
3515
|
}
|
|
3516
|
+
state.ecosystemViewActive = false
|
|
3334
3517
|
|
|
3335
3518
|
if (state.visibleNodes.length <= 2000) {
|
|
3336
3519
|
state.renderNodes = state.visibleNodes
|
|
@@ -3509,7 +3692,8 @@ const render = now => {
|
|
|
3509
3692
|
const isInteracting =
|
|
3510
3693
|
state.pointer.down ||
|
|
3511
3694
|
state.renderVisibilityDirty ||
|
|
3512
|
-
state.recoveringViewport
|
|
3695
|
+
state.recoveringViewport ||
|
|
3696
|
+
state.zoomTransition.active
|
|
3513
3697
|
const minFrameIntervalMs = isInteracting ? 16 : backgroundFrameIntervalMs
|
|
3514
3698
|
if (delta < minFrameIntervalMs) {
|
|
3515
3699
|
requestAnimationFrame(render)
|
|
@@ -3523,6 +3707,7 @@ const render = now => {
|
|
|
3523
3707
|
if (!hasValidTransform()) {
|
|
3524
3708
|
resetView()
|
|
3525
3709
|
}
|
|
3710
|
+
applyZoomTransition(delta)
|
|
3526
3711
|
ctx.clearRect(0, 0, width, height)
|
|
3527
3712
|
webGlRenderer?.clear(width, height)
|
|
3528
3713
|
if (state.nodes.length === 0) {
|
|
@@ -3752,12 +3937,23 @@ const selectNodeById = id => {
|
|
|
3752
3937
|
|
|
3753
3938
|
const zoomAtPoint = (screenX, screenY, factor, source = 'generic') => {
|
|
3754
3939
|
state.lastManualZoomAt = performance.now()
|
|
3755
|
-
const
|
|
3756
|
-
|
|
3757
|
-
|
|
3940
|
+
const baseScale = state.zoomTransition.active
|
|
3941
|
+
? state.zoomTransition.targetScale
|
|
3942
|
+
: state.transform.scale
|
|
3943
|
+
const boundedFactor = source === 'wheel'
|
|
3944
|
+
? Math.max(wheelZoomInputFloorCap, Math.min(wheelZoomInputCeilCap, factor))
|
|
3945
|
+
: factor
|
|
3946
|
+
const nextScale = clampScale(baseScale * boundedFactor)
|
|
3947
|
+
if (nextScale === baseScale && !state.zoomTransition.active) {
|
|
3758
3948
|
return
|
|
3759
3949
|
}
|
|
3760
|
-
const worldPointAtCursor =
|
|
3950
|
+
const worldPointAtCursor =
|
|
3951
|
+
state.zoomTransition.active &&
|
|
3952
|
+
state.zoomTransition.source === source &&
|
|
3953
|
+
state.zoomTransition.screenX === screenX &&
|
|
3954
|
+
state.zoomTransition.screenY === screenY
|
|
3955
|
+
? { x: state.zoomTransition.worldX, y: state.zoomTransition.worldY }
|
|
3956
|
+
: resolveZoomAnchorWorldPoint(screenX, screenY, source)
|
|
3761
3957
|
const worldX = worldPointAtCursor.x
|
|
3762
3958
|
const worldY = worldPointAtCursor.y
|
|
3763
3959
|
state.lastZoomFocus = {
|
|
@@ -3765,9 +3961,15 @@ const zoomAtPoint = (screenX, screenY, factor, source = 'generic') => {
|
|
|
3765
3961
|
y: worldY,
|
|
3766
3962
|
at: performance.now()
|
|
3767
3963
|
}
|
|
3768
|
-
state.
|
|
3769
|
-
|
|
3770
|
-
|
|
3964
|
+
state.zoomTransition = {
|
|
3965
|
+
active: true,
|
|
3966
|
+
source,
|
|
3967
|
+
screenX,
|
|
3968
|
+
screenY,
|
|
3969
|
+
worldX,
|
|
3970
|
+
worldY,
|
|
3971
|
+
targetScale: nextScale
|
|
3972
|
+
}
|
|
3771
3973
|
state.offscreenFrameCount = 0
|
|
3772
3974
|
markRenderDirty()
|
|
3773
3975
|
}
|
|
@@ -3797,7 +3999,11 @@ const wheelZoomFactor = event => {
|
|
|
3797
3999
|
-exponentCap,
|
|
3798
4000
|
Math.min(exponentCap, -normalizedDelta * sensitivity)
|
|
3799
4001
|
)
|
|
3800
|
-
|
|
4002
|
+
const factor = Math.exp(exponent)
|
|
4003
|
+
if (factor > 1) {
|
|
4004
|
+
return Math.min(factor, wheelZoomInputCeilCap)
|
|
4005
|
+
}
|
|
4006
|
+
return Math.max(factor, wheelZoomInputFloorCap)
|
|
3801
4007
|
}
|
|
3802
4008
|
|
|
3803
4009
|
const handleWheelZoom = event => {
|
|
@@ -3881,6 +4087,7 @@ const bindEvents = () => {
|
|
|
3881
4087
|
zoomAtPoint(cursorX, cursorY, 1.055)
|
|
3882
4088
|
})
|
|
3883
4089
|
canvas.addEventListener('pointerdown', event => {
|
|
4090
|
+
clearZoomTransition()
|
|
3884
4091
|
const point = worldPoint(event)
|
|
3885
4092
|
const node = hitNode(point)
|
|
3886
4093
|
state.pointer = { x: event.clientX, y: event.clientY, down: true, dragNode: node, moved: false }
|
package/package.json
CHANGED