first commit
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
# `<DrumPad>`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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
|
||||
|
||||
```tsx
|
||||
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:
|
||||
|
||||
```tsx
|
||||
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.
|
||||
Reference in New Issue
Block a user