Curve
A curve through points in time, for automation, envelopes, fades and tempo, on a timeline or on its own, drawn on canvas once and moved by CSS, and editable with a hook.
- Volume
- Pan
- Steps
Anatomy
import { scales, type CurvePoint } from '@addstack/daw-ui';
import { Curve } from '@addstack/daw-ui/react';
const points: CurvePoint[] = [
{ at: 0, value: -Infinity, shape: -0.6 },
{ at: 1, value: 0 },
];
<Curve.Root points={points} min={-Infinity} max={6} scale={scales.decibel} aria-label="Volume" className="h-12 w-64">
<Curve.Fill className="text-orange-500/20" />
<Curve.Line thickness={1.5} className="text-orange-500" />
</Curve.Root>A curve goes through points, each at a time in seconds with a value: the automation of a parameter, an envelope, a fade, a tempo that changes. It is named for what it draws, like a waveform or notes: what the curve means is your application's. Before the first point and after the last, it holds their value.
A point's shape says how the curve goes on to the next point:
shape | |
|---|---|
"linear" (default) | A straight line. |
"hold" | The point's value until the next point, then a jump: steps, as for a stepped modulation. Two points at the same time jump too. |
| A number from −1 to 1 | A tension that bends it: above 0 it stays near the point's value and moves late, below 0 it moves early, as the curved segments of DAWs. |
Range
Values go bottom to top from min to max, placed with scale as a knob or a fader of that range would place them: scales.decibel for volume, scales.log for frequency. A curve in decibels on the range of your volume fader goes where the fader goes, and a straight segment is a steady move of the fader, not of the decibels. The same rule gives the value at a time:
import { createRange, curveValue, scales } from '@addstack/daw-ui';
const volume = createRange({ min: -Infinity, max: 6, scale: scales.decibel });
curveValue(points, 0.5, volume); // the value halfway through the fade, as the audio engine should play itcurveValue(points, time, range) takes points sorted by time; without a range, segments go straight in value.
Where it lies
- On its own, as in the demo, a curve is its own time axis:
offset…offset + durationacross its width, up to its last point by default. - On a timeline, outside a region, it lies on the timeline itself, from its second 0, for ever: an automation lane. The Arrangement block has one under its bass track.
- In a region, it shows the part the region shows, from its
offsetfor itsduration, as clip automation or a fade does.
The same holds for waveforms and notes.
Line and fill
Curve.Line strokes the curve, thickness CSS pixels wide, in its CSS color. Curve.Fill fills the area between the curve and origin, the root's min by default, or the middle for pan and pitch bend (origin={0} on a range from −1 to 1). Render the fill before the line, to lie under it.
Editing
Give the root an editing engine from useCurveEditing, and its points can be moved, added, removed and bent. The engine is yours, as the principles (section 6) have it: its options are your rules, its callbacks report what the user does, and the points stay in your state.
const [points, setPoints] = useState(initialPoints);
const editing = useCurveEditing({ snap: { time: grid }, onGestureEnd: setPoints });
<Curve.Root points={points} editing={editing} aria-label={t('volume')}>
<Curve.Fill />
<Curve.Line />
<Curve.Dots />
<Curve.Bend className="size-2 rotate-45 border" />
<Curve.Handle aria-label={t('point')} className="size-3 rounded-full bg-white data-selected:bg-orange-400" />
</Curve.Root>- Dots and handles.
Curve.Dotsdraws every point in the tiles, which costs nothing to scroll. ACurve.Handleis an element only where one is needed: at the point the pointer is near, at the selected points, and at the one that takes the focus from Tab. A lane of 2000 points has a handful of handles, however many points it shows.Curve.Bendshows in the middle of the segment the pointer is near. - A gesture shows without rendering. The points, their handles and the stretch of the curve a move changes are drawn again as the pointer moves, and nothing renders.
onPointsChangereports each step; when the gesture ends, the curve returns to itspoints, so keep the changes, inonGestureEndor as they come. Dragging a point of a lane of 2000 next to 32 waveforms takes 4 ms per frame. - Moves hold together. A drag moves the selection. No point passes a neighbour that does not move, no value leaves the range, and the most constrained point stops them all. Moves snap to the finest lines of
snap.timeat least 12 px apart, and to multiples ofsnap.value; Shift does not snap. - Your rules.
lockfixes axes of points: a point locked in both has no handle.constrainis a function from the points a move makes to the points it may make, applied at every step, andcanAddandcanRemoveturn the double-clicks off. The Envelope block is an ADSR made with them.
| Input | Effect |
|---|---|
| Drag a point | Moves it, and the selection with it. |
| Drag the middle of a segment | Bends the segment through the pointer. |
| Double-click the curve / a point / the middle of a segment | Adds a point / removes it / makes the segment straight. |
| Shift while dragging | Does not snap. |
| Press, Cmd/Ctrl+press, press away from the points | Selects alone, adds or takes out, clears. |
| Tab | Into the curve: one tab stop for all its points. |
| Left and Right | The previous or next point. |
| Up and Down (Shift: finer) | Moves the value, by snap.value or a hundredth of the range. |
| Cmd/Ctrl+Left and Right | Moves the point in time, by a snap step (Shift: a pixel). |
| Alt+Up and Down | Bends the segment after the point. |
| Delete, Space | Removes; selects (with Cmd/Ctrl: adds or takes out). |
Editable, the root is a group, and each handle a slider of its point's value, whose aria-valuetext says its time and value in format.time and format.value. Bend handles are hidden from assistive technology: Alt with Up and Down does what they do.
Options of useCurveEditing
| Option | |
|---|---|
snap | { time?: TimeGrid, value?: number }: the grid moves snap to in time, the step in value. |
lock | Axes points do not move on, by index ({ 0: "both", 1: "value" }), or as a function of the index and the point. |
constrain | (points) => points: your rule, applied at every step of a gesture. Keep the number of points and their order. |
canAdd, canRemove | Whether double-clicks and Delete add and remove points. Both true by default. |
format | { time?, value? }: the formats handles announce with. |
onPointsChange(points, { reason, event }) | Each step of a gesture: reason is "drag", "keyboard", "add", "remove" or "bend". |
onGestureStart(), onGestureEnd(points) | Around each gesture: one undo step. |
onSelectedChange(indexes) | The selection a press or a key makes. |
The engine is the same object on every render. Read editing.selected, and call editing.select(indexes) or editing.selectAll() on it, from a toolbar for instance.
How it stays fast
A curve is drawn like a waveform, in canvas tiles placed in time with the timeline's CSS variables: playback and scrolling draw nothing drawn already, a zoom stretches the tiles and draws them sharp when it rests. A tile finds its first point by a binary search and traces only the points it shows: straight and held segments as lines between their points, bent ones through a point every two device pixels. An automation lane of 2000 bent points adds about half a millisecond per frame to zooming 32 waveforms.
A new points array draws the curve again: keep the same one between renders while the points stay the same.
API reference
Root
Renders a <div> with role="img", position: relative and overflow: hidden. On its own, it is its own time axis, with --timeline-start and --timeline-scale.
Prop
Type
Line
The curve as a line, drawn on canvas in the CSS color of this element. Renders a <div> that fills the root.
Prop
Type
Fill
The area between the curve and origin, drawn on canvas in the CSS color of this element. Renders a <div> that fills the root.
Prop
Type
Dots
A dot at every point, drawn on canvas in the CSS color of this element. Renders a <div> that fills the root.
Prop
Type
Handle
The handle of a point, with editing: rendered for each point that needs one, centred on it. Renders a <div> with role="slider": give it a size and a look, and an aria-label.
Prop
Type
| Data attribute | |
|---|---|
data-selected | While its point is selected. |
data-dragging | While it is dragged. |
Bend
The handle in the middle of a segment, with editing, rendered for the segment the pointer is near. Renders a <div> hidden from assistive technology, with data-dragging while it is dragged.
Prop
Type
Notes
The notes of a MIDI clip or a pattern, in a region of a timeline or on their own, drawn on canvas once and moved by CSS.
Arrangement
Tracks of audio and MIDI clips and a lane of automation on a timeline, with their headers beside it, composed from Timeline, Region, Waveform, Notes, Curve and Toggle.