CurveEditor
5 partsA headless control-point curve editor: a draggable set of anchors defining a
single-valued y = f(x) curve. It backs both animation easing curves (an
ease over normalized time) and photo tone curves (per-RGB-channel output
remapping), and is the shared engine reused by Levels (gamma) and the future
KeyframeTrack.
The root owns the anchor array (controlled via v-model, uncontrolled via
defaultValue), builds value↔pixel projections for both axes (useScale,
y-axis value-up), and exposes the live evaluator: sample(x) → y and
toLUT(size) for applying the curve to pixels. With monotonicX (the
default) anchors are neighbour-clamped so they can never cross in x — easing
and tone curves both require a function of x. fixedEndpoints locks the first
and last anchor in x. The interpolation mode selects monotone (default),
linear, catmull-rom, or per-anchor bezier handles.
Provides context to CurveEditorGrid, CurveEditorCurve, CurveEditorPoint,
and CurveEditorHandle. The channel prop only tags which curve is being
edited (for styling / the #channel slot); consumers render their own RGB
tabs.
Demo
Anatomy
Import the parts and compose them. Each part forwards attributes to its underlying element.
import {
CurveEditorRoot,
CurveEditorCurve,
CurveEditorGrid,
CurveEditorHandle,
CurveEditorPoint,
} from '@robonen/primitives/canvas/curve-editor';
<CurveEditorRoot>
<CurveEditorCurve />
<CurveEditorGrid />
<CurveEditorHandle />
<CurveEditorPoint />
</CurveEditorRoot>API Reference
CurveEditorRoot
RootA headless control-point curve editor: a draggable set of anchors defining a single-valued `y = f(x)` curve. It backs both **animation easing curves** (an ease over normalized time) and **photo tone curves** (per-RGB-channel output remapping), and is the shared engine reused by Levels (gamma) and the future KeyframeTrack. The root owns the anchor array (controlled via `v-model`, uncontrolled via `defaultValue`), builds value↔pixel projections for both axes (`useScale`, y-axis value-up), and exposes the live evaluator: `sample(x) → y` and `toLUT(size)` for applying the curve to pixels. With `monotonicX` (the default) anchors are neighbour-clamped so they can never cross in x — easing and tone curves both require a function of x. `fixedEndpoints` locks the first and last anchor in x. The `interpolation` mode selects monotone (default), linear, catmull-rom, or per-anchor bezier handles. Provides context to `CurveEditorGrid`, `CurveEditorCurve`, `CurveEditorPoint`, and `CurveEditorHandle`. The `channel` prop only tags which curve is being edited (for styling / the `#channel` slot); consumers render their own RGB tabs.
| Prop | Type | Description |
|---|---|---|
defaultValue? | CurveEditorAnchor[] | Uncontrolled initial anchors. Seeds the curve when v-model is absent. |
interpolation? | CurveEditorInterpolation | How the curve is interpolated between anchors. |
domainX? | readonly [number, number] | Input (x) domain [min, max]. |
domainY? | readonly [number, number] | Output (y) domain [min, max]. |
monotonicX? | boolean | Keep x single-valued: neighbour-clamp anchors so they can't cross in x. Easing / tone curves require a function of x. |
fixedEndpoints? | boolean | Lock the first and last anchor in x (only y is editable). |
channel? | CurveEditorChannel | Tags which curve is being edited (composite 'value' or per-channel
'r'/'g'/'b'). Purely cosmetic — exposed for styling / the #channel
slot; consumers render their own channel tabs. |
step? | number | Keyboard step for x/y nudges. |
largeStep? | number | Large keyboard step (Shift+Arrow / Page keys). |
samples? | number | Sample count for the rendered polyline / LUT. |
disabled? | boolean | Disable all interaction. |
dir? | CurveEditorDirection | Writing direction (inherited from ConfigProvider when omitted). |
modelValue? | CurveEditorAnchor[] | null | Two-way bound value (v-model). |
| Event | Payload |
|---|---|
update:modelValue | [value: CurveEditorAnchor[] | null] |
CurveEditorCurve
CurveThe rendered curve of a `CurveEditorRoot`, an SVG `<path>` whose `d` is built by sampling `f(x)` across `domainX` (or the per-anchor bezier path in `'bezier'` mode) and projecting each sample to pixels. It is decorative (`role="presentation"` / `aria-hidden`) — the accessible controls are the `CurveEditorPoint` thumbs. The path `d` is also exposed as a slot prop for custom rendering (fills, glows).
No props or events — renders its element and forwards attributes.
CurveEditorGrid
GridThe background gridlines of a `CurveEditorRoot`, drawn from `niceTicks` on both axes. Rendered as an SVG `<g>` by default (override via `as`) and marked `aria-hidden` — it is decorative. Exposes the projected `xTicks` / `yTicks` as slot props so the consumer draws their own lines / labels, and provides a `#histogram` slot region behind the curve for tone-curve histograms.
No props or events — renders its element and forwards attributes.
CurveEditorHandle
HandleA bezier tangent handle for an anchor, rendered only in `'bezier'` interpolation. Dragging it adjusts the anchor's `inHandle` / `outHandle` tangent (relative deltas), clamped (in easing / `monotonicX` mode) so the cubic segment stays single-valued in x — the handle's x-component can't reach past the neighbouring anchor (`dx >= 0`), preventing an S-fold. It is exposed as `role="slider"` with a descriptive `aria-label`; pass `aria-hidden` to make it purely decorative. Positions itself at the tangent endpoint in pixel space.
| Prop | Type | Description |
|---|---|---|
anchor | CurveEditorAnchor | The anchor whose tangent this handle controls. |
side | CurveEditorHandleSide | Which tangent ('in' = incoming, 'out' = outgoing). |
CurveEditorPoint
PointOne anchor handle of a `CurveEditorRoot`, rendered as `role="slider"`. A 2D control whose single `aria-valuenow` (the output `y`) can't carry both axes, so `aria-valuetext` announces the pair as `"input {x}, output {y}"`. `aria-valuemin`/`max` describe the output (`y`) domain. Anchors share one tab-stop (roving focus): Tab moves focus between them, the arrow keys nudge the focused anchor. Left/Right nudge `x` by `step` (neighbour- and domain-clamped; no-op for fixed endpoints), Up/Down nudge `y` (Up = +y), Shift+Arrow uses the large step, PageUp/PageDown jump `y`, Home/End move `x` to the domain min/max. Enter adds an anchor at the midpoint to the next anchor; Delete/Backspace removes the focused anchor (never an endpoint). Double-click also adds, drag moves the anchor (2D, clamped). Exposes the anchor and its pixel position as slot props.
| Prop | Type | Description |
|---|---|---|
anchor | CurveEditorAnchor | The anchor this point renders. |
valueText? | (x: number, y: number) => string | Override the announced aria-valuetext. Receives the anchor's x and y
in domain space. |