first commit
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
# `<JogWheel>` — endless rotary with velocity + inertia
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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 descriptive `aria-valuetext`
|
||||
- `data-spinning` attribute for E2E and CSS visual feedback
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
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
|
||||
|
||||
```tsx
|
||||
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.
|
||||
Reference in New Issue
Block a user