Files
skills/docs/ui/react/react-control-surface/src/controls/drum-pad/drum-pad.md
2026-05-29 08:20:56 +01:00

4.9 KiB
Raw Blame History

<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: pointermove updates 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 by min so downstream consumers can treat each press as a discrete event without bookkeeping.
  • Injectable nowMs clock for test determinism.
  • role="button" with live aria-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.