first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -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.