4.2 KiB
<JogWheel> — endless rotary with velocity + inertia
(no submodules)
Purpose
<JogWheel> is an endless rotary control inspired by DJ-deck jog
wheels. Unlike a knob, it does not have an absolute value — instead
each pointer-drag event emits a delta in radians via onChange,
which the caller integrates into whatever parameter the wheel
controls (scrub position, tempo bend, parameter trim, etc.).
The wheel reads pointer motion tangentially around its centre: motion
that curls clockwise around the hub produces positive deltas, motion
curling counter-clockwise produces negative deltas. On release, if
the angular velocity exceeds inertiaThreshold, the wheel continues
to spin under exponential friction (inertiaFriction) and emits
deltas via requestAnimationFrame until the velocity falls below a
small cut-off.
Features
- Tangential pointer-motion integration via 2D cross product
- Pointer-capture so drags continuing off the wheel still register
- Configurable sensitivity (
radiansPerPixel) - Inertial spin after release, with caller-tunable threshold + friction
- Tab focus + arrow-key navigation (±0.1 rad per arrow press)
role="slider"with descriptivearia-valuetextdata-spinningattribute for E2E and CSS visual feedback
Usage: Basic
import { useState } from "react";
import { JogWheel } from "@suaveplan/react-control-surface";
function Scrubber() {
const [pos, setPos] = useState(0);
return (
<JogWheel onChange={(d) => setPos((p) => p + d)} label="Scrub" />
);
}
Usage: Advanced
import { JogWheel } from "@suaveplan/react-control-surface";
function PitchBend({ engine }: { engine: { bend: (cents: number) => void } }) {
return (
<JogWheel
onChange={(delta) => engine.bend(delta * 60)}
radiansPerPixel={0.04}
inertiaThreshold={2}
inertiaFriction={3.5}
size={180}
label="Pitch bend"
/>
);
}
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
onChange |
(deltaRadians: number) => void |
req | Called per drag step + inertia tick. |
radiansPerPixel |
number |
0.02 |
Sensitivity (≈1.15 deg / pixel by default). |
inertiaThreshold |
number |
4 |
Min release velocity (rad/s) to start spinning. |
inertiaFriction |
number |
2.5 |
Exponential decay coefficient (per second). |
size |
number |
120 |
Render size in CSS pixels (the wheel is square). |
disabled |
boolean |
false |
When true, ignores all input. |
label |
string |
— | Visible + accessible label. |
ariaLabel |
string |
derived | Override the accessible label. |
className |
string |
— | Appended to rcs-jog-wheel. |
The exported decayVelocity(velocity, friction, dtMs) helper is the
pure exponential-decay primitive used internally.
Implementation Notes
The tangential pixel motion is computed as (rx * dy - ry * dx) / radius, i.e. the 2D cross product of the radius vector and the
motion vector divided by the radius magnitude. This produces a signed
scalar that is positive for clockwise motion (in standard
screen-coordinates where Y points down) and negative for
counter-clockwise. We guard against radius-zero starts by skipping
motion samples that occur within 1 pixel of the centre.
Inertia is driven by a separate useEffect that runs only while
spinning is true. The effect captures the current inertiaFriction
and emits a delta per frame: step = velocity * dt. Once the absolute
velocity falls below 0.05 rad/s, the effect clears its own spinning
flag and unmounts the RAF loop.