How to Implement 1:1 Pointer Tracking with Velocity Calculation for Gestures
Use setPointerCapture to track pointer movement outside element bounds, maintain a rolling history of recent positions, and compute release velocity by dividing position delta by elapsed time before passing it to a spring animation.
Smooth, physics-driven gestures require continuous 1:1 pointer tracking and accurate velocity extraction at release. The emilkowalski/skills repository documents a battle-tested pattern for implementing this in web applications, combining pointer capture APIs with spring-based animations to create interfaces that feel truly responsive and alive.
Core Concepts from the Skills Repository
The implementation rests on four interconnected principles documented across the repository's skill files:
| Concept | Implementation Source |
|---|---|
Pointer capture with setPointerCapture() to receive events beyond element bounds |
skills/apple-design/SKILL.md |
| Position history buffer for velocity calculation | skills/apple-design/SKILL.md |
| Spring animation with initial velocity for momentum continuation | skills/apple-design/SKILL.md |
| Canonical spring configuration for gesture-driven motion | skills/review-animations/STANDARDS.md |
Step-by-Step Implementation
1: Capture the Pointer on Gesture Start
Call setPointerCapture() immediately in your pointerdown handler. This ensures the element continues receiving pointermove events even if the cursor leaves its bounds—a critical requirement for 1:1 tracking that doesn't break when users drag quickly.
element.addEventListener('pointerdown', (e) => {
element.setPointerCapture(e.pointerId);
// Initialize tracking state...
});
According to skills/apple-design/SKILL.md, this pattern is essential for drag-to-dismiss and flick gestures where the user needs freedom of movement without the gesture terminating prematurely【apple‑design SKILL.md†L42-L47】.
2: Maintain a Rolling Position History
Store timestamped positions in a fixed-length buffer during pointermove. The repository recommends keeping approximately 5 recent samples to balance noise reduction with responsiveness【apple‑design SKILL.md†L43-L46】.
const HISTORY_SIZE = 5;
const history = []; // { t: timestamp, x: number, y: number }
element.addEventListener('pointermove', (e) => {
history.push({ t: e.timeStamp, x: e.clientX, y: e.clientY });
if (history.length > HISTORY_SIZE) history.shift();
// Apply 1:1 transform...
element.style.transform = `translate(${e.clientX}px, ${e.clientY}px)`;
});
3: Calculate Velocity at Gesture End
Compute velocity by fitting a linear slope through your history buffer. The simplest reliable method: divide the position delta between first and last samples by their time difference.
element.addEventListener('pointerup', () => {
const [first, last] = [history[0], history[history.length - 1]];
const dt = (last.t - first.t) / 1000; // seconds
const dx = last.x - first.x;
const dy = last.y - first.y;
const vx = dx / dt; // pixels per second
const vy = dy / dt;
});
4: Launch Spring with Extracted Velocity
Pass the computed velocity to your spring animation. The repository specifies a canonical configuration for gesture-driven springs: { type: 'spring', duration: 0.5, bounce: 0.2 }【STANDARDS.md†L67-L70】.
import { animate } from 'motion';
animate(
element,
{ x: targetX, y: targetY },
{
type: 'spring',
duration: 0.5,
bounce: 0.2,
velocity: { x: vx, y: vy } // Momentum continuation
}
);
Complete Vanilla JavaScript Implementation
This self-contained example implements the full pipeline with the Motion library:
import { animate } from "https://cdn.skypack.dev/motion";
const el = document.querySelector(".draggable");
let history = null;
// ---------- Gesture start ----------
el.addEventListener("pointerdown", (e) => {
el.setPointerCapture(e.pointerId);
history = [{ t: e.timeStamp, x: e.clientX, y: e.clientY }];
});
// ---------- Track movement ----------
el.addEventListener("pointermove", (e) => {
if (!history) return;
history.push({ t: e.timeStamp, x: e.clientX, y: e.clientY });
if (history.length > 5) history.shift();
// 1:1 pointer tracking
el.style.transform = `translate(${e.clientX}px, ${e.clientY}px)`;
});
// ---------- Gesture end with velocity calculation ----------
el.addEventListener("pointerup", (e) => {
if (!history) return;
const [prev, last] = [history[0], history[history.length - 1]];
const dt = (last.t - prev.t) / 1000;
const dx = last.x - prev.x;
const dy = last.y - prev.y;
const vx = dx / dt;
const vy = dy / dt;
// Spring with momentum continuation
animate(
el,
{ x: 0, y: 0 }, // Snap-back target
{
type: "spring",
bounce: 0.2,
duration: 0.5,
velocity: { x: vx, y: vy },
}
);
history = null;
});
React Hook Implementation
For React applications, encapsulate the pattern in a reusable hook with Framer Motion:
import { useRef } from "react";
import { motion, useMotionValue, animate } from "framer-motion";
export function usePointerGesture() {
const x = useMotionValue(0);
const y = useMotionValue(0);
const history = useRef<Array<{t:number;x:number;y:number}>>([]);
const start = (e: React.PointerEvent) => {
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
history.current = [{ t: e.timeStamp, x: e.clientX, y: e.clientY }];
};
const move = (e: React.PointerEvent) => {
history.current.push({ t: e.timeStamp, x: e.clientX, y: e.clientY });
if (history.current.length > 5) history.current.shift();
x.set(e.clientX);
y.set(e.clientY);
};
const end = () => {
const h = history.current;
if (h.length < 2) return;
const dt = (h[h.length - 1].t - h[0].t) / 1000;
const vx = (h[h.length - 1].x - h[0].x) / dt;
const vy = (h[h.length - 1].y - h[0].y) / dt;
animate(y, 0, {
type: "spring",
bounce: 0.2,
duration: 0.5,
velocity: vy,
});
};
return { x, y, start, move, end };
}
export default function DraggableBox() {
const { x, y, start, move, end } = usePointerGesture();
return (
<motion.div
style={{ x, y, touchAction: "none", position: "absolute" }}
onPointerDown={start}
onPointerMove={move}
onPointerUp={end}
onPointerCancel={end}
/>
);
}
Why Velocity-Preserving Springs Matter
The repository emphasizes interruptibility as a core principle for gesture-driven interfaces【apple‑design SKILL.md†L33-L35】. When a spring animation receives the release velocity:
- No jarring stops — motion continues naturally from the finger's speed
- Reversible gestures — users can interrupt and reverse direction smoothly
- iOS-native feel — matches "additive animations" in Apple's interface patterns【apple‑design SKILL.md†L62-L66】
Key Source Files
| File | Purpose |
|---|---|
skills/apple-design/SKILL.md |
Pointer capture, velocity history, spring integration |
skills/review-animations/STANDARDS.md |
Canonical spring configs: duration: 0.5, bounce: 0.2 |
skills/animation-vocabulary/SKILL.md |
Spring terminology: stiffness, damping, critical damping, momentum scenarios |
skills/pick-ui-library/SKILL.md |
Motion/Framer Motion library recommendations |
Summary
- Capture with
setPointerCapture()to track beyond element bounds - Buffer the last 5 positions with timestamps during movement
- Calculate velocity as Δposition/Δtime from history samples
- Animate with springs using the repository's standard config:
duration: 0.5, bounce: 0.2 - Preserve momentum by passing computed velocity to the spring's
velocityoption
Frequently Asked Questions
How large should the position history buffer be?
Five samples provides optimal balance. Fewer samples amplify noise from input jitter; more samples add latency and may capture outdated momentum. The skills/apple-design/SKILL.md documentation specifically references this approximate window for velocity calculation【apple‑design SKILL.md†L43-L46】.
Can I use requestAnimationFrame instead of pointer events for tracking?
Pointer events via setPointerCapture are required for reliable 1:1 tracking that continues outside element bounds. requestAnimationFrame polling loses pointer lock and cannot capture movement beyond the original element's geometry. The repository explicitly recommends pointer capture APIs for this reason【apple‑design SKILL.md†L42-L47】.
What spring parameters work best for heavy vs. light elements?
Use the canonical config (bounce: 0.2, duration: 0.5) as your baseline. Adjust bounce higher (0.3-0.4) for lighter feel, lower (0.0-0.1) for heavier, more substantial motion. The skills/animation-vocabulary/SKILL.md file defines spring taxonomy for tuning these values to specific momentum scenarios【animation‑vocabulary SKILL.md†L126-L131】.
How do I handle pointercancel events?
Treat pointercancel identically to pointerup: compute velocity from available history and launch the spring. This ensures the gesture completes gracefully if the browser interrupts the pointer stream (system gesture, touch-action conflict, etc.).
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →