npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@plcdnl/fuse-three-forcegraph

v0.0.2

Published

A high-performance GPU-accelerated force-directed graph visualization library built with Three.js. Features a modular pass-based architecture for flexible and extensible force simulations.

Readme

Fuse Three Force Graph

A high-performance GPU-accelerated force-directed graph visualization library built with Three.js. Features a modular pass-based architecture for flexible and extensible force simulations.

Features

  • GPU-Accelerated: All force calculations run on the GPU using WebGL compute shaders
  • Modular Pass Architecture: Flexible system for composing and customizing force behaviors
  • Ping-Pong Rendering: Efficient double-buffering for position and velocity updates
  • Interactive Controls: Built-in camera controls, node dragging, and hover interactions
  • Extensible: Easy to add custom force passes and visual effects

Architecture

Core Components

Engine (core/Engine.ts)

The main orchestrator that owns and coordinates all components:

  • Manages shared GPU buffers (SimulationBuffers, StaticAssets, PickBuffer)
  • Coordinates GraphStore, GraphScene, and ForceSimulation
  • Handles the render loop and user interactions

SimulationBuffers (textures/SimulationBuffers.ts)

Manages dynamic render targets updated by force simulation:

  • Position buffers (current, previous, original) for ping-pong rendering
  • Velocity buffers for force accumulation
  • Automatically sizes textures based on node count

StaticAssets (textures/StaticAssets.ts)

Manages read-only GPU textures:

  • Node radii and colors
  • Link indices and properties
  • Created once at initialization, updated only on mode changes

GraphScene (rendering/GraphScene.ts)

Manages the 3D scene and visual rendering:

  • Node and link renderers
  • Camera controls
  • Visual mode application

Force Simulation

The simulation uses a pass-based architecture where each force type is implemented as an independent pass:

BasePass (simulation/BasePass.ts)

Abstract base class for all force passes. Provides:

  • Material management
  • Uniform updates
  • Enable/disable control
  • Render execution

Built-in Force Passes

Located in simulation/passes/:

  1. VelocityCarryPass - Applies damping to previous velocity
  2. CollisionPass - Prevents node overlap
  3. ManyBodyPass - Charge repulsion between all nodes
  4. GravityPass - Pulls nodes toward center
  5. LinkPass - Spring forces between connected nodes
  6. EmbeddingsPass - Elastic pull toward original positions
  7. DragPass - Interactive node dragging
  8. IntegratePass - Updates positions from velocities

ForceSimulation (simulation/ForceSimulation.ts)

Manages and executes force passes:

// Add custom force pass
simulation.addPass('myForce', new MyCustomPass(config))

// Remove a pass
simulation.removePass('collision')

// Enable/disable a pass
simulation.setPassEnabled('gravity', false)

// Get a pass for configuration
const gravityPass = simulation.getPass('gravity')

Execution Pipeline

Each simulation step follows this sequence:

  1. Velocity Carry - Initialize velocity buffer with damped previous velocity
  2. Force Accumulation - Each enabled pass accumulates forces into velocity
    • Read current velocity
    • Compute force contribution
    • Write to previous velocity buffer
    • Swap buffers (ping-pong)
  3. Integration - Update positions using accumulated velocities
  4. Alpha Decay - Reduce simulation heat over time

Usage

Basic Setup

import { Engine } from './core/Engine'

// Create engine with a canvas element
const engine = new Engine(canvas, {
  width: window.innerWidth,
  height: window.innerHeight,
  backgroundColor: '#000000'
})

// Load graph data
const graphData = {
  nodes: [
    { id: '1', x: 0, y: 0, z: 0 },
    { id: '2', x: 10, y: 10, z: 0 }
  ],
  links: [
    { source: '1', target: '2' }
  ]
}

engine.setData(graphData)

// Start simulation and rendering
engine.start()

  GRAPH.styleRegistry.setNodeStyles({
      'root': { color: 0xE53E3E, size: 55 },
      'series': { color: 0x38A169, size: 33 },
      'artwork': { color: 0x3182CE, size: 22 },
    })

  // Get simulation config - direct access, changes take effect immediately
  const simulation = engine.getSimulation()
  const config = simulation.config

  // Create Tweakpane
  pane = new Pane()

  // Bind simulation parameters - changes to config work directly, no sync needed
  const simFolder = pane.addFolder({ title: 'Simulation' })
  simFolder.addBinding(config, 'alpha', { min: 0, max: 1 })
  simFolder.addBinding(config, 'alphaDecay', { min: 0, max: 0.1 })
  simFolder.addBinding(config, 'damping', { min: 0, max: 1 })

  // Many-body force, check uniforms that can be added in binding...
  const manyBodyFolder = pane.addFolder({ title: 'Many-Body Force' })
  manyBodyFolder.addBinding(config, 'enableManyBody')
  manyBodyFolder.addBinding(config, 'manyBodyStrength', { min: 0, max: 100 })


  // Set up attractors - each pulls specific categories
  simulation.setAttractors([
  {
    id: 'center',
    position: { x: 0, y: 0.0, z: 0 },
    categories: ['root'],  
    strength: 55.
  },])

  // Adjust global attractor strength
  simulation.config.attractorStrength = 0.03

Generating Random Data

For testing and prototyping, you can generate random graph data:

// Utility function to create random nodes and links
const createRandomData = (nodeCount: number = 10, linkCount: number = 15) => {
  const groups = ['root', 'series', 'artwork', 'character', 'location']
  
  // Generate random nodes
  const nodes = Array.from({ length: nodeCount }, (_, i) => ({
    id: (i + 1).toString(),
    group: groups[Math.floor(Math.random() * groups.length)],
    x: (Math.random() - 0.5) * 2,
    y: (Math.random() - 0.5) * 2,
    z: (Math.random() - 0.5) * 2
  }))
  
  // Generate random links
  const links = Array.from({ length: linkCount }, () => {
    const sourceId = Math.floor(Math.random() * nodeCount) + 1
    let targetId = Math.floor(Math.random() * nodeCount) + 1
    
    // Ensure source and target are different
    while (targetId === sourceId) {
      targetId = Math.floor(Math.random() * nodeCount) + 1
    }
    
    return {
      source: sourceId.toString(),
      target: targetId.toString()
    }
  })
  
  return { nodes, links }
}

// Use random data
engine.setData(createRandomData(100, 70))

Custom Force Pass

import { BasePass, type PassContext } from './simulation/BasePass'

class CustomForcePass extends BasePass {
  private strength: number = 1.0

  getName(): string {
    return 'CustomForce'
  }

  initMaterial(context: PassContext): void {
    this.material = this.createMaterial(
      vertexShader,
      fragmentShader,
      {
        uPositionsTexture: { value: null },
        uVelocityTexture: { value: null },
        uStrength: { value: this.strength },
        uAlpha: { value: 1.0 }
      }
    )
  }

  updateUniforms(context: PassContext): void {
    if (!this.material) return
    
    this.material.uniforms.uPositionsTexture.value = 
      context.simBuffers.getCurrentPositionTexture()
    this.material.uniforms.uVelocityTexture.value = 
      context.simBuffers.getCurrentVelocityTexture()
    this.material.uniforms.uAlpha.value = context.alpha
    this.material.uniforms.uStrength.value = this.strength
  }

  setStrength(strength: number): void {
    this.strength = strength
  }
}

// Add to simulation
const customPass = new CustomForcePass()
customPass.initMaterial(context)
forceSimulation.addPass('custom', customPass, 2) // position 2

Configuration

// Update force configuration
engine.getSimulation().updateConfig({
  manyBodyStrength: 100,
  enableCollision: true,
  collisionRadius: 8.0,
  gravity: 1.2,
  damping: 0.95,
  alpha: 1.0
})

// Interactive node dragging
engine.getInteractionManager().on('dragStart', ({ nodeId }) => {
  console.log('Dragging node:', nodeId)
})

// Node picking
const nodeId = engine.pickNode(mouseX, mouseY)

Project Structure

fuse-three-forcegraph/
├── assets/
│   └── glsl/              # GLSL shaders for force simulation
│       ├── force-sim/     # Force compute shaders
│       ├── lines/         # Link rendering shaders
│       └── points/        # Node rendering shaders
├── audio/                 # Audio integration (RNBO)
├── controls/              # Input handling and interactions
│   ├── InteractionManager.ts
│   ├── InputProcessor.ts
│   └── handlers/          # Click, drag, hover handlers
├── core/                  # Core engine components
│   ├── Engine.ts          # Main orchestrator
│   ├── EventEmitter.ts    # Event system
│   └── GraphStore.ts      # Graph data management
├── rendering/             # Visual rendering
│   ├── GraphScene.ts      # Scene management
│   ├── CameraController.ts
│   ├── nodes/             # Node rendering
│   └── links/             # Link rendering
├── simulation/            # Force simulation
│   ├── BasePass.ts        # Pass base class
│   ├── ForceSimulation.ts # Pass manager
│   └── passes/            # Individual force passes
├── textures/              # GPU buffer management
│   ├── SimulationBuffers.ts  # Dynamic buffers
│   ├── StaticAssets.ts       # Static textures
│   └── PickBuffer.ts         # GPU picking
├── types/                 # TypeScript definitions
└── ui/                    # UI components (tooltips, etc.)

GPU Texture Layout

Position/Velocity Buffers

  • Format: RGBA Float
  • Layout: Grid where each pixel = one node
  • Channels: (x, y, z, unused)
  • Size: Next power-of-2 square ≥ √nodeCount

Static Assets

  • Radii: Red Float (1 channel)
  • Colors: RGBA Float (4 channels)
  • Link Indices: RGBA Float (source_x, source_y, target_x, target_y)

Performance Considerations

  • All position/velocity data stays on GPU
  • Force computations use fragment shaders (parallel)
  • Ping-pong rendering avoids read-after-write hazards
  • Static assets minimize data transfer
  • Geometry uses instancing for efficient rendering

Interaction Model

The library implements a three-tiered interaction system for progressive engagement:

1. Hover

  • Trigger: Mouse cursor enters node boundary
  • Purpose: Lightweight preview and visual feedback
  • Response:
    • Node highlight/glow effect
    • Cursor change
    • Optional tooltip display
    • No layout disruption
  • Use Case: Quick scanning and exploration

2. Pop (Dwell/Long Hover)

  • Trigger: Cursor remains over node for defined duration (e.g., 500ms)
  • Purpose: Detailed information display without commitment
  • Response:
    • Expanded tooltip/info card
    • Highlight connected nodes and edges
    • Subtle camera focus adjustment
    • Audio feedback (optional)
  • Use Case: Examining node details and immediate connections

3. Click

  • Trigger: Primary mouse button click on node
  • Purpose: Full interaction and state change
  • Response:
    • Node selection/deselection
    • Full graph filtering (show only connected components)
    • Panel/sidebar updates
    • Deep-dive views
    • State persistence
  • Use Case: Focused analysis and permanent selection

Interaction Pipeline

// Hover
interactionManager.on('hover', ({ node, position }) => {
  // Immediate visual feedback
  graphScene.highlightNode(node.id, 'hover')
})

// Pop (triggered after dwell time)
interactionManager.on('pop', ({ node, dwellTime }) => {
  // Show detailed tooltip
  tooltipManager.showExpanded(node)
  // Highlight neighborhood
  graphScene.highlightNeighborhood(node.id)
})

// Click
interactionManager.on('click', ({ node }) => {
  // Full selection
  graphStore.selectNode(node.id)
  // Filter graph
  graphScene.filterToConnected(node.id)
})

This progressive disclosure pattern prevents overwhelming users while enabling deep exploration when needed.

Dependencies

  • three.js - 3D rendering engine
  • camera-controls - Camera manipulation
  • gsap - Animation and transitions