@cosmos.gl/graph 2.4.0 → 2.6.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.
Files changed (49) hide show
  1. package/.github/SECURITY.md +7 -1
  2. package/dist/config.d.ts +73 -1
  3. package/dist/index.d.ts +34 -6
  4. package/dist/index.js +4087 -3837
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.min.js +124 -44
  7. package/dist/index.min.js.map +1 -1
  8. package/dist/modules/GraphData/index.d.ts +1 -0
  9. package/dist/modules/Lines/index.d.ts +8 -0
  10. package/dist/modules/Points/index.d.ts +3 -0
  11. package/dist/modules/Store/index.d.ts +14 -2
  12. package/dist/modules/core-module.d.ts +1 -0
  13. package/dist/stories/beginners/link-hovering/data-generator.d.ts +19 -0
  14. package/dist/stories/beginners/link-hovering/index.d.ts +5 -0
  15. package/dist/stories/beginners/pinned-points/data-gen.d.ts +5 -0
  16. package/dist/stories/beginners/pinned-points/index.d.ts +5 -0
  17. package/dist/stories/beginners.stories.d.ts +2 -0
  18. package/dist/variables.d.ts +5 -2
  19. package/package.json +1 -1
  20. package/src/config.ts +95 -3
  21. package/src/index.ts +179 -32
  22. package/src/modules/GraphData/index.ts +2 -1
  23. package/src/modules/Lines/draw-curve-line.frag +12 -1
  24. package/src/modules/Lines/draw-curve-line.vert +29 -2
  25. package/src/modules/Lines/hovered-line-index.frag +27 -0
  26. package/src/modules/Lines/hovered-line-index.vert +8 -0
  27. package/src/modules/Lines/index.ts +112 -2
  28. package/src/modules/Points/index.ts +34 -0
  29. package/src/modules/Points/update-position.frag +12 -0
  30. package/src/modules/Store/index.ts +33 -2
  31. package/src/modules/core-module.ts +1 -0
  32. package/src/stories/1. welcome.mdx +11 -4
  33. package/src/stories/2. configuration.mdx +13 -3
  34. package/src/stories/3. api-reference.mdx +13 -4
  35. package/src/stories/beginners/basic-set-up/index.ts +21 -11
  36. package/src/stories/beginners/link-hovering/data-generator.ts +198 -0
  37. package/src/stories/beginners/link-hovering/index.ts +61 -0
  38. package/src/stories/beginners/link-hovering/style.css +73 -0
  39. package/src/stories/beginners/pinned-points/data-gen.ts +153 -0
  40. package/src/stories/beginners/pinned-points/index.ts +61 -0
  41. package/src/stories/beginners/quick-start.ts +3 -2
  42. package/src/stories/beginners/remove-points/config.ts +1 -1
  43. package/src/stories/beginners/remove-points/index.ts +28 -30
  44. package/src/stories/beginners.stories.ts +31 -0
  45. package/src/stories/clusters/polygon-selection/index.ts +2 -4
  46. package/src/stories/create-cosmos.ts +1 -1
  47. package/src/stories/geospatial/moscow-metro-stations/index.ts +1 -1
  48. package/src/stories/shapes/image-example/index.ts +7 -8
  49. package/src/variables.ts +5 -2
package/src/index.ts CHANGED
@@ -17,10 +17,10 @@ import { FPSMonitor } from '@/graph/modules/FPSMonitor'
17
17
  import { GraphData } from '@/graph/modules/GraphData'
18
18
  import { Lines } from '@/graph/modules/Lines'
19
19
  import { Points } from '@/graph/modules/Points'
20
- import { Store, ALPHA_MIN, MAX_POINT_SIZE, type Hovered } from '@/graph/modules/Store'
20
+ import { Store, ALPHA_MIN, MAX_POINT_SIZE, MAX_HOVER_DETECTION_DELAY, type Hovered } from '@/graph/modules/Store'
21
21
  import { Zoom } from '@/graph/modules/Zoom'
22
22
  import { Drag } from '@/graph/modules/Drag'
23
- import { defaultConfigValues, defaultScaleToZoom } from '@/graph/variables'
23
+ import { defaultConfigValues, defaultScaleToZoom, defaultGreyoutPointColor, defaultBackgroundColor } from '@/graph/variables'
24
24
  import { createWebGLErrorMessage } from './graph/utils/error-message'
25
25
 
26
26
  export class Graph {
@@ -50,12 +50,12 @@ export class Graph {
50
50
 
51
51
  private currentEvent: D3ZoomEvent<HTMLCanvasElement, undefined> | D3DragEvent<HTMLCanvasElement, undefined, Hovered> | MouseEvent | undefined
52
52
  /**
53
- * The value of `_findHoveredPointExecutionCount` is incremented by 1 on each animation frame.
54
- * When the counter reaches 2 (or more), it is reset to 0 and the `findHoveredPoint` method is executed.
53
+ * The value of `_findHoveredItemExecutionCount` is incremented by 1 on each animation frame.
54
+ * When the counter reaches MAX_HOVER_DETECTION_DELAY (default 4), it is reset to 0 and the `findHoveredPoint` or `findHoveredLine` method is executed.
55
55
  */
56
- private _findHoveredPointExecutionCount = 0
56
+ private _findHoveredItemExecutionCount = 0
57
57
  /**
58
- * If the mouse is not on the Canvas, the `findHoveredPoint` method will not be executed.
58
+ * If the mouse is not on the Canvas, the `findHoveredPoint` or `findHoveredLine` method will not be executed.
59
59
  */
60
60
  private _isMouseOnCanvas = false
61
61
  /**
@@ -123,7 +123,30 @@ export class Graph {
123
123
  this.canvasD3Selection
124
124
  .on('mouseenter.cosmos', () => { this._isMouseOnCanvas = true })
125
125
  .on('mousemove.cosmos', () => { this._isMouseOnCanvas = true })
126
- .on('mouseleave.cosmos', () => { this._isMouseOnCanvas = false })
126
+ .on('mouseleave.cosmos', (event) => {
127
+ this._isMouseOnCanvas = false
128
+ this.currentEvent = event
129
+
130
+ // Clear point hover state and trigger callback if needed
131
+ if (this.store.hoveredPoint !== undefined && this.config.onPointMouseOut) {
132
+ this.config.onPointMouseOut(event)
133
+ }
134
+
135
+ // Clear link hover state and trigger callback if needed
136
+ if (this.store.hoveredLinkIndex !== undefined && this.config.onLinkMouseOut) {
137
+ this.config.onLinkMouseOut(event)
138
+ }
139
+
140
+ // Reset right-click flag
141
+ this.isRightClickMouse = false
142
+
143
+ // Clear hover states
144
+ this.store.hoveredPoint = undefined
145
+ this.store.hoveredLinkIndex = undefined
146
+
147
+ // Update cursor style after clearing hover states
148
+ this.updateCanvasCursor()
149
+ })
127
150
  select(document)
128
151
  .on('keydown.cosmos', (event) => { if (event.code === 'Space') this.store.isSpaceKeyPressed = true })
129
152
  .on('keyup.cosmos', (event) => { if (event.code === 'Space') this.store.isSpaceKeyPressed = false })
@@ -176,18 +199,15 @@ export class Graph {
176
199
  this.clusters = new Clusters(this.reglInstance, this.config, this.store, this.graph, this.points)
177
200
 
178
201
  this.store.backgroundColor = getRgbaColor(this.config.backgroundColor)
179
- if (this.config.hoveredPointRingColor) {
180
- this.store.setHoveredPointRingColor(this.config.hoveredPointRingColor)
181
- }
182
- if (this.config.focusedPointRingColor) {
183
- this.store.setFocusedPointRingColor(this.config.focusedPointRingColor)
184
- }
202
+ this.store.setHoveredPointRingColor(this.config.hoveredPointRingColor ?? defaultConfigValues.hoveredPointRingColor)
203
+ this.store.setFocusedPointRingColor(this.config.focusedPointRingColor ?? defaultConfigValues.focusedPointRingColor)
185
204
  if (this.config.focusedPointIndex !== undefined) {
186
205
  this.store.setFocusedPoint(this.config.focusedPointIndex)
187
206
  }
188
- if (this.config.pointGreyoutColor) {
189
- this.store.setGreyoutPointColor(this.config.pointGreyoutColor)
190
- }
207
+ this.store.setGreyoutPointColor(this.config.pointGreyoutColor ?? defaultGreyoutPointColor)
208
+ this.store.setHoveredLinkColor(this.config.hoveredLinkColor ?? defaultConfigValues.hoveredLinkColor)
209
+
210
+ this.store.updateLinkHoveringEnabled(this.config)
191
211
 
192
212
  if (this.config.showFPSMonitor) this.fpsMonitor = new FPSMonitor(this.canvas)
193
213
 
@@ -227,7 +247,8 @@ export class Graph {
227
247
  if (this._isDestroyed || !this.reglInstance || !this.points || !this.lines || !this.clusters) return
228
248
  const prevConfig = { ...this.config }
229
249
  this.config.init(config)
230
- if (prevConfig.pointColor !== this.config.pointColor) {
250
+ if ((prevConfig.pointDefaultColor !== this.config.pointDefaultColor) ||
251
+ (prevConfig.pointColor !== this.config.pointColor)) {
231
252
  this.graph.updatePointColor()
232
253
  this.points.updateColor()
233
254
  }
@@ -251,15 +272,20 @@ export class Graph {
251
272
  prevConfig.curvedLinks !== this.config.curvedLinks) {
252
273
  this.lines.updateCurveLineGeometry()
253
274
  }
254
- if (prevConfig.backgroundColor !== this.config.backgroundColor) this.store.backgroundColor = getRgbaColor(this.config.backgroundColor)
275
+ if (prevConfig.backgroundColor !== this.config.backgroundColor) {
276
+ this.store.backgroundColor = getRgbaColor(this.config.backgroundColor ?? defaultBackgroundColor)
277
+ }
255
278
  if (prevConfig.hoveredPointRingColor !== this.config.hoveredPointRingColor) {
256
- this.store.setHoveredPointRingColor(this.config.hoveredPointRingColor)
279
+ this.store.setHoveredPointRingColor(this.config.hoveredPointRingColor ?? defaultConfigValues.hoveredPointRingColor)
257
280
  }
258
281
  if (prevConfig.focusedPointRingColor !== this.config.focusedPointRingColor) {
259
- this.store.setFocusedPointRingColor(this.config.focusedPointRingColor)
282
+ this.store.setFocusedPointRingColor(this.config.focusedPointRingColor ?? defaultConfigValues.focusedPointRingColor)
260
283
  }
261
284
  if (prevConfig.pointGreyoutColor !== this.config.pointGreyoutColor) {
262
- this.store.setGreyoutPointColor(this.config.pointGreyoutColor)
285
+ this.store.setGreyoutPointColor(this.config.pointGreyoutColor ?? defaultGreyoutPointColor)
286
+ }
287
+ if (prevConfig.hoveredLinkColor !== this.config.hoveredLinkColor) {
288
+ this.store.setHoveredLinkColor(this.config.hoveredLinkColor ?? defaultConfigValues.hoveredLinkColor)
263
289
  }
264
290
  if (prevConfig.focusedPointIndex !== this.config.focusedPointIndex) {
265
291
  this.store.setFocusedPoint(this.config.focusedPointIndex)
@@ -285,6 +311,12 @@ export class Graph {
285
311
  if (prevConfig.enableZoom !== this.config.enableZoom || prevConfig.enableDrag !== this.config.enableDrag) {
286
312
  this.updateZoomDragBehaviors()
287
313
  }
314
+
315
+ if (prevConfig.onLinkClick !== this.config.onLinkClick ||
316
+ prevConfig.onLinkMouseOver !== this.config.onLinkMouseOver ||
317
+ prevConfig.onLinkMouseOut !== this.config.onLinkMouseOut) {
318
+ this.store.updateLinkHoveringEnabled(this.config)
319
+ }
288
320
  }
289
321
 
290
322
  /**
@@ -309,6 +341,7 @@ export class Graph {
309
341
  this.isPointSizeUpdateNeeded = true
310
342
  this.isPointShapeUpdateNeeded = true
311
343
  this.isPointImageIndicesUpdateNeeded = true
344
+ this.isPointImageSizesUpdateNeeded = true
312
345
  this.isPointClusterUpdateNeeded = true
313
346
  this.isForceManyBodyUpdateNeeded = true
314
347
  this.isForceLinkUpdateNeeded = true
@@ -561,6 +594,29 @@ export class Graph {
561
594
  this.isPointClusterUpdateNeeded = true
562
595
  }
563
596
 
597
+ /**
598
+ * Sets which points are pinned (fixed) in position.
599
+ *
600
+ * Pinned points:
601
+ * - Do not move due to physics forces (gravity, repulsion, link forces, etc.)
602
+ * - Still participate in force calculations (other nodes are attracted to/repelled by them)
603
+ * - Can still be dragged by the user if `enableDrag` is true
604
+ *
605
+ * @param {number[] | null} pinnedIndices - Array of point indices to pin. Set to `[]` or `null` to unpin all points.
606
+ * @example
607
+ * // Pin points 0 and 5
608
+ * graph.setPinnedPoints([0, 5])
609
+ *
610
+ * // Unpin all points
611
+ * graph.setPinnedPoints([])
612
+ * graph.setPinnedPoints(null)
613
+ */
614
+ public setPinnedPoints (pinnedIndices: number[] | null): void {
615
+ if (this._isDestroyed || !this.points) return
616
+ this.graph.inputPinnedPoints = pinnedIndices && pinnedIndices.length > 0 ? pinnedIndices : undefined
617
+ this.points.updatePinnedStatus()
618
+ }
619
+
564
620
  /**
565
621
  * Renders the graph.
566
622
  *
@@ -1065,7 +1121,8 @@ export class Graph {
1065
1121
  }
1066
1122
 
1067
1123
  /**
1068
- * Pause the simulation.
1124
+ * Pause the simulation. When paused, the simulation stops running
1125
+ * and can be resumed using the unpause method.
1069
1126
  */
1070
1127
  public pause (): void {
1071
1128
  if (this._isDestroyed) return
@@ -1074,7 +1131,19 @@ export class Graph {
1074
1131
  }
1075
1132
 
1076
1133
  /**
1077
- * Restart the simulation.
1134
+ * Unpause the simulation. This method resumes a paused
1135
+ * simulation and continues its execution.
1136
+ */
1137
+ public unpause (): void {
1138
+ if (this._isDestroyed) return
1139
+ this.store.isSimulationRunning = true
1140
+ this.config.onSimulationUnpause?.()
1141
+ }
1142
+
1143
+ /**
1144
+ * Restart/Resume the simulation. This method unpauses a paused
1145
+ * simulation and resumes its execution.
1146
+ * @deprecated Use `unpause()` instead. This method will be removed in a future version.
1078
1147
  */
1079
1148
  public restart (): void {
1080
1149
  if (this._isDestroyed) return
@@ -1245,6 +1314,7 @@ export class Graph {
1245
1314
  }
1246
1315
 
1247
1316
  private frame (): void {
1317
+ if (this._isDestroyed) return
1248
1318
  const { config: { simulationGravity, simulationCenter, renderLinks, enableSimulation }, store: { alpha, isSimulationRunning } } = this
1249
1319
  if (alpha < ALPHA_MIN && isSimulationRunning) this.end()
1250
1320
  if (!this.store.pointsTextureSize) return
@@ -1252,7 +1322,9 @@ export class Graph {
1252
1322
  this.requestAnimationFrameId = window.requestAnimationFrame((now) => {
1253
1323
  this.fpsMonitor?.begin()
1254
1324
  this.resizeCanvas()
1255
- if (!this.dragInstance.isActive) this.findHoveredPoint()
1325
+ if (!this.dragInstance.isActive) {
1326
+ this.findHoveredItem()
1327
+ }
1256
1328
 
1257
1329
  if (enableSimulation) {
1258
1330
  if (this.isRightClickMouse && this.config.enableRightClickRepulsion) {
@@ -1314,11 +1386,15 @@ export class Graph {
1314
1386
  // To prevent the dragged point from suddenly jumping, run the drag function twice
1315
1387
  this.points?.drag()
1316
1388
  this.points?.drag()
1389
+ // Update tracked positions after drag, even when simulation is disabled
1390
+ this.points?.trackPoints()
1317
1391
  }
1318
1392
  this.fpsMonitor?.end(now)
1319
1393
 
1320
1394
  this.currentEvent = undefined
1321
- this.frame()
1395
+ if (!this._isDestroyed) {
1396
+ this.frame()
1397
+ }
1322
1398
  })
1323
1399
  }
1324
1400
 
@@ -1338,6 +1414,23 @@ export class Graph {
1338
1414
  this.store.hoveredPoint?.position,
1339
1415
  event
1340
1416
  )
1417
+
1418
+ if (this.store.hoveredPoint) {
1419
+ this.config.onPointClick?.(
1420
+ this.store.hoveredPoint.index,
1421
+ this.store.hoveredPoint.position,
1422
+ event
1423
+ )
1424
+ } else if (this.store.hoveredLinkIndex !== undefined) {
1425
+ this.config.onLinkClick?.(
1426
+ this.store.hoveredLinkIndex,
1427
+ event
1428
+ )
1429
+ } else {
1430
+ this.config.onBackgroundClick?.(
1431
+ event
1432
+ )
1433
+ }
1341
1434
  }
1342
1435
 
1343
1436
  private updateMousePosition (event: MouseEvent | D3DragEvent<HTMLCanvasElement, undefined, Hovered>): void {
@@ -1365,6 +1458,7 @@ export class Graph {
1365
1458
  }
1366
1459
 
1367
1460
  private resizeCanvas (forceResize = false): void {
1461
+ if (this._isDestroyed) return
1368
1462
  const prevWidth = this.canvas.width
1369
1463
  const prevHeight = this.canvas.height
1370
1464
  const w = this.canvas.clientWidth
@@ -1382,6 +1476,10 @@ export class Graph {
1382
1476
  this.canvasD3Selection
1383
1477
  ?.call(this.zoomInstance.behavior.transform, this.zoomInstance.getTransform([centerPosition], k))
1384
1478
  this.points?.updateSampledPointsGrid()
1479
+ // Only update link index FBO if link hovering is enabled
1480
+ if (this.store.isLinkHoveringEnabled) {
1481
+ this.lines?.updateLinkIndexFbo()
1482
+ }
1385
1483
  }
1386
1484
  }
1387
1485
 
@@ -1413,13 +1511,31 @@ export class Graph {
1413
1511
  }
1414
1512
  }
1415
1513
 
1416
- private findHoveredPoint (): void {
1417
- if (!this._isMouseOnCanvas || !this.reglInstance || !this.points) return
1418
- if (this._findHoveredPointExecutionCount < 2) {
1419
- this._findHoveredPointExecutionCount += 1
1514
+ private findHoveredItem (): void {
1515
+ if (this._isDestroyed || !this._isMouseOnCanvas || !this.reglInstance) return
1516
+ if (this._findHoveredItemExecutionCount < MAX_HOVER_DETECTION_DELAY) {
1517
+ this._findHoveredItemExecutionCount += 1
1420
1518
  return
1421
1519
  }
1422
- this._findHoveredPointExecutionCount = 0
1520
+ this._findHoveredItemExecutionCount = 0
1521
+ this.findHoveredPoint()
1522
+
1523
+ if (this.graph.linksNumber && this.store.isLinkHoveringEnabled) {
1524
+ this.findHoveredLine()
1525
+ } else if (this.store.hoveredLinkIndex !== undefined) {
1526
+ // Clear stale hoveredLinkIndex when there are no links
1527
+ const wasHovered = this.store.hoveredLinkIndex !== undefined
1528
+ this.store.hoveredLinkIndex = undefined
1529
+ if (wasHovered && this.config.onLinkMouseOut) {
1530
+ this.config.onLinkMouseOut(this.currentEvent)
1531
+ }
1532
+ }
1533
+
1534
+ this.updateCanvasCursor()
1535
+ }
1536
+
1537
+ private findHoveredPoint (): void {
1538
+ if (this._isDestroyed || !this.reglInstance || !this.points) return
1423
1539
  this.points.findHoveredPoint()
1424
1540
  let isMouseover = false
1425
1541
  let isMouseout = false
@@ -1447,15 +1563,46 @@ export class Graph {
1447
1563
  )
1448
1564
  }
1449
1565
  if (isMouseout) this.config.onPointMouseOut?.(this.currentEvent)
1450
- this.updateCanvasCursor()
1566
+ }
1567
+
1568
+ private findHoveredLine (): void {
1569
+ if (this._isDestroyed || !this.reglInstance || !this.lines) return
1570
+ if (this.store.hoveredPoint) {
1571
+ if (this.store.hoveredLinkIndex !== undefined) {
1572
+ this.store.hoveredLinkIndex = undefined
1573
+ this.config.onLinkMouseOut?.(this.currentEvent)
1574
+ }
1575
+ return
1576
+ }
1577
+ this.lines.findHoveredLine()
1578
+ let isMouseover = false
1579
+ let isMouseout = false
1580
+
1581
+ const pixels = readPixels(this.reglInstance, this.lines.hoveredLineIndexFbo as regl.Framebuffer2D)
1582
+ const hoveredLineIndex = pixels[0] as number
1583
+
1584
+ if (hoveredLineIndex >= 0) {
1585
+ if (this.store.hoveredLinkIndex !== hoveredLineIndex) isMouseover = true
1586
+ this.store.hoveredLinkIndex = hoveredLineIndex
1587
+ } else {
1588
+ if (this.store.hoveredLinkIndex !== undefined) isMouseout = true
1589
+ this.store.hoveredLinkIndex = undefined
1590
+ }
1591
+
1592
+ if (isMouseover && this.store.hoveredLinkIndex !== undefined) {
1593
+ this.config.onLinkMouseOver?.(this.store.hoveredLinkIndex)
1594
+ }
1595
+ if (isMouseout) this.config.onLinkMouseOut?.(this.currentEvent)
1451
1596
  }
1452
1597
 
1453
1598
  private updateCanvasCursor (): void {
1454
- const { hoveredPointCursor } = this.config
1599
+ const { hoveredPointCursor, hoveredLinkCursor } = this.config
1455
1600
  if (this.dragInstance.isActive) select(this.canvas).style('cursor', 'grabbing')
1456
1601
  else if (this.store.hoveredPoint) {
1457
1602
  if (!this.config.enableDrag || this.store.isSpaceKeyPressed) select(this.canvas).style('cursor', hoveredPointCursor)
1458
1603
  else select(this.canvas).style('cursor', 'grab')
1604
+ } else if (this.store.isLinkHoveringEnabled && this.store.hoveredLinkIndex !== undefined) {
1605
+ select(this.canvas).style('cursor', hoveredLinkCursor)
1459
1606
  } else select(this.canvas).style('cursor', null)
1460
1607
  }
1461
1608
 
@@ -27,6 +27,7 @@ export class GraphData {
27
27
  public inputPointClusters: (number | undefined)[] | undefined
28
28
  public inputClusterPositions: (number | undefined)[] | undefined
29
29
  public inputClusterStrength: Float32Array | undefined
30
+ public inputPinnedPoints: number[] | undefined
30
31
 
31
32
  public pointPositions: Float32Array | undefined
32
33
  public pointColors: Float32Array | undefined
@@ -87,7 +88,7 @@ export class GraphData {
87
88
  }
88
89
 
89
90
  // Sets point colors to default values from config if the input is missing or does not match input points number.
90
- const defaultRgba = getRgbaColor(this._config.pointColor)
91
+ const defaultRgba = getRgbaColor(this._config.pointDefaultColor ?? this._config.pointColor)
91
92
  if (this.inputPointColors === undefined || this.inputPointColors.length / 4 !== this.pointsNumber) {
92
93
  this.pointColors = new Float32Array(this.pointsNumber * 4)
93
94
  for (let i = 0; i < this.pointColors.length / 4; i++) {
@@ -6,6 +6,10 @@ varying float arrowLength;
6
6
  varying float useArrow;
7
7
  varying float smoothing;
8
8
  varying float arrowWidthFactor;
9
+ varying float linkIndex;
10
+
11
+ // renderMode: 0.0 = normal rendering, 1.0 = index buffer rendering for picking
12
+ uniform float renderMode;
9
13
 
10
14
  float map(float value, float min1, float max1, float min2, float max2) {
11
15
  return min2 + (value - min1) * (max2 - min2) / (max1 - min1);
@@ -31,5 +35,12 @@ void main() {
31
35
  opacity = linkOpacity;
32
36
  } else opacity = rgbaColor.a * smoothstep(0.5, 0.5 - smoothing, abs(pos.y));
33
37
 
34
- gl_FragColor = vec4(color, opacity);
38
+ if (renderMode > 0.0) {
39
+ if (opacity > 0.0) {
40
+ gl_FragColor = vec4(linkIndex, 0.0, 0.0, 1.0);
41
+ } else {
42
+ gl_FragColor = vec4(-1.0, 0.0, 0.0, 0.0);
43
+ }
44
+ } else gl_FragColor = vec4(color, opacity);
45
+
35
46
  }
@@ -4,13 +4,14 @@ attribute vec2 position, pointA, pointB;
4
4
  attribute vec4 color;
5
5
  attribute float width;
6
6
  attribute float arrow;
7
+ attribute float linkIndices;
7
8
 
8
9
  uniform sampler2D positionsTexture;
9
10
  uniform sampler2D pointGreyoutStatus;
10
11
  uniform mat3 transformationMatrix;
11
12
  uniform float pointsTextureSize;
12
13
  uniform float widthScale;
13
- uniform float arrowSizeScale;
14
+ uniform float linkArrowsSizeScale;
14
15
  uniform float spaceSize;
15
16
  uniform vec2 screenSize;
16
17
  uniform vec2 linkVisibilityDistanceRange;
@@ -22,6 +23,11 @@ uniform float curvedLinkControlPointDistance;
22
23
  uniform float curvedLinkSegments;
23
24
  uniform bool scaleLinksOnZoom;
24
25
  uniform float maxPointSize;
26
+ // renderMode: 0.0 = normal rendering, 1.0 = index buffer rendering for picking
27
+ uniform float renderMode;
28
+ uniform float hoveredLinkIndex;
29
+ uniform vec4 hoveredLinkColor;
30
+ uniform float hoveredLinkWidthIncrease;
25
31
 
26
32
  varying vec4 rgbaColor;
27
33
  varying vec2 pos;
@@ -29,6 +35,7 @@ varying float arrowLength;
29
35
  varying float useArrow;
30
36
  varying float smoothing;
31
37
  varying float arrowWidthFactor;
38
+ varying float linkIndex;
32
39
 
33
40
  float map(float value, float min1, float max1, float min2, float max2) {
34
41
  return min2 + (value - min1) * (max2 - min2) / (max1 - min1);
@@ -73,6 +80,7 @@ float calculateArrowWidth(float arrowWidth) {
73
80
 
74
81
  void main() {
75
82
  pos = position;
83
+ linkIndex = linkIndices;
76
84
 
77
85
  vec2 pointTexturePosA = (pointA + 0.5) / pointsTextureSize;
78
86
  vec2 pointTexturePosB = (pointB + 0.5) / pointsTextureSize;
@@ -102,7 +110,7 @@ void main() {
102
110
  float k = 2.0;
103
111
  // Arrow width is proportionally larger than the line width
104
112
  float arrowWidth = linkWidth * k;
105
- arrowWidth *= arrowSizeScale;
113
+ arrowWidth *= linkArrowsSizeScale;
106
114
 
107
115
  // Ensure arrow width difference is non-negative to prevent unwanted changes to link width
108
116
  float arrowWidthDifference = max(0.0, arrowWidth - linkWidth);
@@ -124,10 +132,22 @@ void main() {
124
132
 
125
133
  // Calculate final link width in pixels with smoothing
126
134
  float linkWidthPx = calculateLinkWidth(linkWidth);
135
+
136
+ if (renderMode > 0.0) {
137
+ // Add 5 pixels padding for better hover detection
138
+ linkWidthPx += 5.0 / transformationMatrix[0][0];
139
+ } else {
140
+ // Add pixel increase if this is the hovered link
141
+ if (hoveredLinkIndex == linkIndex) {
142
+ linkWidthPx += hoveredLinkWidthIncrease / transformationMatrix[0][0];
143
+ }
144
+ }
127
145
  float smoothingPx = 0.5 / transformationMatrix[0][0];
128
146
  smoothing = smoothingPx / linkWidthPx;
129
147
  linkWidthPx += smoothingPx;
130
148
 
149
+
150
+
131
151
  // Calculate final color with opacity based on link distance
132
152
  vec3 rgbColor = color.rgb;
133
153
  // Adjust opacity based on link distance
@@ -141,6 +161,13 @@ void main() {
141
161
  // Pass final color to fragment shader
142
162
  rgbaColor = vec4(rgbColor, opacity);
143
163
 
164
+ // Apply hover color if this is the hovered link and hover color is defined
165
+ if (hoveredLinkIndex == linkIndex && hoveredLinkColor.a > -0.5) {
166
+ // Keep existing RGB values but multiply opacity with hover color opacity
167
+ rgbaColor.rgb = hoveredLinkColor.rgb;
168
+ rgbaColor.a *= hoveredLinkColor.a;
169
+ }
170
+
144
171
  // Calculate position on the curved path
145
172
  float t = position.x;
146
173
  float w = curvedWeight;
@@ -0,0 +1,27 @@
1
+ precision highp float;
2
+
3
+ uniform sampler2D linkIndexTexture;
4
+ uniform vec2 mousePosition;
5
+ uniform vec2 screenSize;
6
+
7
+ varying vec2 vTexCoord;
8
+
9
+ void main() {
10
+ // Convert mouse position to texture coordinates
11
+ vec2 texCoord = mousePosition / screenSize;
12
+
13
+ // Read the link index from the linkIndexFbo texture at mouse position
14
+ vec4 linkIndexData = texture2D(linkIndexTexture, texCoord);
15
+
16
+ // Extract the link index (stored in the red channel)
17
+ float linkIndex = linkIndexData.r;
18
+
19
+ // Check if there's a valid link at this position (alpha > 0)
20
+ if (linkIndexData.a > 0.0 && linkIndex >= 0.0) {
21
+ // Output the link index
22
+ gl_FragColor = vec4(linkIndex, 0.0, 0.0, 1.0);
23
+ } else {
24
+ // No link at this position, output -1 to indicate no hover
25
+ gl_FragColor = vec4(-1.0, 0.0, 0.0, 0.0);
26
+ }
27
+ }
@@ -0,0 +1,8 @@
1
+ attribute vec2 position;
2
+
3
+ varying vec2 vTexCoord;
4
+
5
+ void main() {
6
+ vTexCoord = position * 0.5 + 0.5;
7
+ gl_Position = vec4(position, 0.0, 1.0);
8
+ }