CurveEditor

5 parts

A 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

Loading demo…

Anatomy

Import the parts and compose them. Each part forwards attributes to its underlying element.

vue
import {
  CurveEditorRoot,
  CurveEditorCurve,
  CurveEditorGrid,
  CurveEditorHandle,
  CurveEditorPoint,
} from '@robonen/primitives/canvas/curve-editor';

<CurveEditorRoot>
  <CurveEditorCurve />
  <CurveEditorGrid />
  <CurveEditorHandle />
  <CurveEditorPoint />
</CurveEditorRoot>

API Reference

CurveEditorRoot

Root

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

PropTypeDescription
defaultValue?CurveEditorAnchor[]Uncontrolled initial anchors. Seeds the curve when v-model is absent.
interpolation?CurveEditorInterpolationHow 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?booleanKeep x single-valued: neighbour-clamp anchors so they can't cross in x. Easing / tone curves require a function of x.
fixedEndpoints?booleanLock the first and last anchor in x (only y is editable).
channel?CurveEditorChannelTags 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?numberKeyboard step for x/y nudges.
largeStep?numberLarge keyboard step (Shift+Arrow / Page keys).
samples?numberSample count for the rendered polyline / LUT.
disabled?booleanDisable all interaction.
dir?CurveEditorDirectionWriting direction (inherited from ConfigProvider when omitted).
modelValue?CurveEditorAnchor[] | nullTwo-way bound value (v-model).
Emits
EventPayload
update:modelValue[value: CurveEditorAnchor[] | null]

CurveEditorCurve

Curve

The 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

Grid

The 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

Handle

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

PropTypeDescription
anchorCurveEditorAnchorThe anchor whose tangent this handle controls.
sideCurveEditorHandleSideWhich tangent ('in' = incoming, 'out' = outgoing).

CurveEditorPoint

Point

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

PropTypeDescription
anchorCurveEditorAnchorThe anchor this point renders.
valueText?(x: number, y: number) => stringOverride the announced aria-valuetext. Receives the anchor's x and y in domain space.