UNPKG

ngraph.svg

Version:

SVG-based graph visualization library with adaptive rendering

156 lines (132 loc) 3.43 kB
/** * Kinetic animation for smooth momentum-based scrolling. * Tracks velocity during pan and continues movement after release. */ export default function createKineticAnimation(options = {}) { const { friction = 0.92, minVelocity = 0.5, onUpdate = () => {} } = options; let velocityX = 0; let velocityY = 0; let lastTime = 0; let rafId = null; let isTracking = false; // Track recent movements for velocity calculation const movements = []; const maxMovements = 5; function recordMovement(dx, dy) { const now = performance.now(); movements.push({ dx, dy, time: now }); // Keep only recent movements while (movements.length > maxMovements) { movements.shift(); } } function calculateVelocity() { if (movements.length < 2) { return { vx: 0, vy: 0 }; } // Calculate average velocity from recent movements let totalDx = 0; let totalDy = 0; let totalTime = 0; for (let i = 1; i < movements.length; i++) { const dt = movements[i].time - movements[i - 1].time; if (dt > 0) { totalDx += movements[i].dx; totalDy += movements[i].dy; totalTime += dt; } } if (totalTime === 0) { return { vx: 0, vy: 0 }; } // Velocity in pixels per millisecond return { vx: (totalDx / totalTime) * 16, // Scale to approx per-frame velocity vy: (totalDy / totalTime) * 16 }; } function animate() { rafId = null; const now = performance.now(); const dt = Math.min(now - lastTime, 32); // Cap at ~30fps minimum lastTime = now; // Frame-rate independent friction and movement const dtScale = dt / 16; const frictionFactor = Math.pow(friction, dtScale); velocityX *= frictionFactor; velocityY *= frictionFactor; // Stop if velocity is too low const speed = Math.sqrt(velocityX * velocityX + velocityY * velocityY); if (speed < minVelocity) { velocityX = 0; velocityY = 0; return; } // Apply movement scaled by time onUpdate(velocityX * dtScale, velocityY * dtScale); // Continue animation rafId = requestAnimationFrame(animate); } return { /** * Start tracking movements */ startTracking() { isTracking = true; movements.length = 0; this.stop(); }, /** * Record a movement during pan */ track(dx, dy) { if (isTracking) { recordMovement(dx, dy); } }, /** * Stop tracking and start kinetic animation */ release() { isTracking = false; const { vx, vy } = calculateVelocity(); velocityX = vx; velocityY = vy; movements.length = 0; // Start animation if there's enough velocity const speed = Math.sqrt(velocityX * velocityX + velocityY * velocityY); if (speed >= minVelocity) { lastTime = performance.now(); rafId = requestAnimationFrame(animate); } }, /** * Stop any ongoing animation */ stop() { if (rafId !== null) { cancelAnimationFrame(rafId); rafId = null; } velocityX = 0; velocityY = 0; }, /** * Check if animation is running */ isAnimating() { return rafId !== null; }, /** * Dispose of the animation */ dispose() { this.stop(); movements.length = 0; } }; }