4.9 KiB
<DrumPad>
(no submodules)
Purpose
Velocity-sensitive drum pad. Maps a pointer or keyboard press to a
velocity 0..1 then through [min, max] so the same component can
serve as a 0..127 MIDI velocity emitter, a 0..1 normalized parameter
trigger, or anything in between. Velocity sourcing is layered:
hardware PointerEvent.pressure is preferred when the device supplies
a non-zero value (Apple Pencil, pressure-sensitive trackpads,
ChromeOS active styluses); otherwise a duration heuristic measures
press hold time and produces velocity via a caller-replaceable
function.
Features
- Two velocity sources, transparently selected: hardware pressure or press-duration heuristic.
- Live pressure tracking:
pointermoveupdates velocity continuously while pressure is supplied (useful for aftertouch-style modulation). - Keyboard activation (Space, Enter) using the duration heuristic.
- Auto-repeat keyboard suppression — held keys don't re-arm the timer.
- Pointer capture for drift-tolerant release.
- Strike-then-release pattern: caller receives
(velocity)followed immediately byminso downstream consumers can treat each press as a discrete event without bookkeeping. - Injectable
nowMsclock for test determinism. role="button"with livearia-pressed.- CSS-variable theming (
--cs-pad-idle,--cs-pad-pressed,--cs-pad-border,--cs-pad-label-color).
Usage: Basic
import { useState } from "react";
import { DrumPad } from "@suaveplan/react-control-surface";
export function KickPad() {
const [v, setV] = useState(0);
return (
<DrumPad
value={v}
onChange={setV}
min={0}
max={127}
displayLabel="KICK"
/>
);
}
Usage: Advanced
A 4×4 pad grid with a custom velocity curve and shared state:
import { useState } from "react";
import { DrumPad } from "@suaveplan/react-control-surface";
const LABELS = [
"KICK","SNARE","CLAP","RIM",
"OH","CH","RIDE","CRASH",
"TOM1","TOM2","TOM3","PERC",
"FX1","FX2","FX3","FX4",
];
// Aggressive curve: 80 ms → max velocity.
const curve = (ms: number) => Math.min(1, ms / 80) ** 0.7;
export function Grid() {
const [values, setValues] = useState(() => LABELS.map(() => 0));
return (
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(4, 80px)",
gridGap: 6,
}}
>
{LABELS.map((lbl, i) => (
<DrumPad
key={lbl}
displayLabel={lbl}
value={values[i]}
onChange={(v) => {
const next = values.slice();
next[i] = v;
setValues(next);
}}
min={0}
max={127}
velocityFromDurationMs={curve}
/>
))}
</div>
);
}
API Reference
| Prop | Type | Default |
|---|---|---|
value |
number |
required |
onChange |
(v: number) => void |
required |
min/max |
number |
required |
disabled |
boolean |
false |
label |
string |
— |
ariaLabel |
string |
— |
displayLabel |
string |
— |
widthPx |
number |
80 |
heightPx |
number |
80 |
velocityFromDurationMs |
(ms: number) => number |
min(1, ms / 200) |
nowMs |
() => number |
performance.now |
className |
string |
— |
Implementation Notes
The strike + release pattern uses queueMicrotask so consumers see
two distinct emissions in order: first the velocity at strike, then
min immediately after. This keeps downstream parameter-bound state
trivially correct (no manual reset) while preserving the strike
event for envelope retriggering. Keyboard auto-repeat suppression
checks the active-press ref before re-arming so a held key produces
a single sustained press rather than a stream of strikes. The
injectable nowMs exists exclusively so tests can run with a fake
clock and produce deterministic velocity values without relying on
real wall-clock timing.