@andespindola/brainlink 0.1.0-beta.113 → 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 CHANGED
@@ -596,6 +596,7 @@ The graph UI shows:
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
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
599
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
600
601
  - keyboard shortcuts: `+` zoom in, `-` zoom out, `0` reset fit
601
602
  - double-click on canvas zooms in at cursor position
@@ -77,6 +77,11 @@ const zoomAnimationSlowLerp = 0.18
77
77
  const zoomAnimationFastLerp = 0.36
78
78
  const zoomAnimationScaleSnap = 0.00008
79
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
80
85
  const physicsDragFrameIntervalMs = 16
81
86
  const physicsIdleFrameIntervalMs = 78
82
87
  const physicsLargeGraphIdleFrameIntervalMs = 108
@@ -1270,19 +1275,20 @@ const projectEcosystemPoint = (x, y, depth, anchor) => {
1270
1275
 
1271
1276
  const applyEcosystemDepthProjection = (clusters, edges, anchor) => {
1272
1277
  const levelIndexMap = ecosystemLevelIndexBySize()
1278
+ const effectiveAnchor = applyBloomCameraParallax(anchor)
1273
1279
  const projectedClusters = []
1274
1280
  const clusterById = new Map()
1275
1281
 
1276
1282
  for (let index = 0; index < clusters.length; index += 1) {
1277
1283
  const cluster = clusters[index]
1278
1284
  const baseDepth = ecosystemDepthForCluster(cluster, levelIndexMap)
1279
- const radialDistance = Math.hypot(cluster.x - anchor.x, cluster.y - anchor.y)
1285
+ const radialDistance = Math.hypot(cluster.x - effectiveAnchor.x, cluster.y - effectiveAnchor.y)
1280
1286
  const radialOffset = cluster.isHub ? 0 : Math.min(320, radialDistance * ecosystemDepthRadialGain)
1281
1287
  const orbitalOffset = cluster.isHub
1282
1288
  ? 0
1283
- : Math.sin(Math.atan2(cluster.y - anchor.y, cluster.x - anchor.x) * 2.2) * ecosystemDepthOrbitalMaxOffset
1289
+ : Math.sin(Math.atan2(cluster.y - effectiveAnchor.y, cluster.x - effectiveAnchor.x) * 2.2) * ecosystemDepthOrbitalMaxOffset
1284
1290
  const depth = Math.max(0, baseDepth + radialOffset + orbitalOffset)
1285
- const projected = projectEcosystemPoint(cluster.x, cluster.y, depth, anchor)
1291
+ const projected = projectEcosystemPoint(cluster.x, cluster.y, depth, effectiveAnchor)
1286
1292
  const baseOpacity = Number.isFinite(cluster.lodOpacity) ? cluster.lodOpacity : 1
1287
1293
  const depthScale = ecosystemDepthMinScale + (1 - ecosystemDepthMinScale) * projected.factor
1288
1294
  const depthOpacity = Math.max(
@@ -1614,6 +1620,91 @@ const cursorWorldPoint = () => {
1614
1620
  return screenToWorldPoint(screenX, screenY)
1615
1621
  }
1616
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
+
1617
1708
  const visibilityScaleBucket = (scale) => {
1618
1709
  const safeScale = Math.max(zoomRange.min, scale)
1619
1710
  return Math.round(safeScale * 180_000)
@@ -3208,7 +3299,7 @@ const refreshRenderNodeDepthProjection = () => {
3208
3299
  return
3209
3300
  }
3210
3301
 
3211
- const anchor = nodeProjectionAnchor()
3302
+ const anchor = applyBloomCameraParallax(nodeProjectionAnchor())
3212
3303
  let maxDistance = 1
3213
3304
  for (let index = 0; index < state.renderNodes.length; index += 1) {
3214
3305
  const node = state.renderNodes[index]
@@ -3862,7 +3953,7 @@ const zoomAtPoint = (screenX, screenY, factor, source = 'generic') => {
3862
3953
  state.zoomTransition.screenX === screenX &&
3863
3954
  state.zoomTransition.screenY === screenY
3864
3955
  ? { x: state.zoomTransition.worldX, y: state.zoomTransition.worldY }
3865
- : screenToWorldPoint(screenX, screenY)
3956
+ : resolveZoomAnchorWorldPoint(screenX, screenY, source)
3866
3957
  const worldX = worldPointAtCursor.x
3867
3958
  const worldY = worldPointAtCursor.y
3868
3959
  state.lastZoomFocus = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.113",
3
+ "version": "0.1.0-beta.114",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",