Beta
Curve Editor <mv-curve-editor>
Editor für Easing-Kurven: Zieh zwei Griffe oder verschieb sie per Tastatur, mit Live-Koordinaten und Überschwingen für Antizipation und Bounce. Kurvenfamilien wechseln zwischen in, out und in-out, ein Spring-Preset wird als linear() berechnet, und eine Bahn zeigt die Bewegung neben einer linearen Referenz. Formularfähig.
| Kategorie | Formulare |
|---|---|
| Typ | Web Component (<mv-curve-editor>) |
| Status | Beta |
| Keywords | easing, cubic-bezier, linear(), spring, curve, animation, inspector, form-associated, editor |
When to use
- A motion or animation tool lets users design a cubic-bezier or spring easing visually
- An inspector needs easing presets by family and direction plus exact x1/y1/x2/y2 values with a live preview
Avoid when
- The easing is a fixed design decision that end users never change; hardcode it instead
- Users only pick among a few named easings → use Segmented instead
Installation
node scripts/add.mjs curve-editor --out ./src/marvelousKI-Agent mit dem MCP-Server von Marvelous UI: install_components({ slugs: ["curve-editor"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Kopierte Dateien (inklusive Abhängigkeiten): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/form.js, core/motion.js, components/curve-editor/curve-editor.js, components/curve-editor/curve-editor.css.
Verwendung
Referenz-Markup zum Starten und Anpassen mit Attributen, data-* und CSS-Variablen:
<div id="curve-demo">
<style>
#curve-demo { display: flex; flex-wrap: wrap; gap: 1.5rem; align-items: center; justify-content: center; width: 100%; }
#curve-demo .cd-col { display: grid; gap: .6rem; width: 17rem; }
#curve-demo .cd-stage { position: relative; height: 15rem; overflow: hidden; border-radius: var(--mv-radius-xl); border: 1px solid var(--mv-border); background: radial-gradient(circle at 1px 1px, color-mix(in oklch, var(--mv-fg) 12%, transparent) 1px, transparent 0) 0 0 / 12px 12px, var(--mv-bg); }
#curve-demo .cd-toast { position: absolute; inset: auto 1rem 1rem; display: flex; align-items: center; gap: .7rem; padding: .7rem .8rem; border: 1px solid var(--mv-border); border-radius: var(--mv-radius-lg); background: var(--mv-surface-raised); box-shadow: var(--mv-shadow-lg); font-size: .8rem; }
#curve-demo .cd-toast i { flex: none; width: 2rem; height: 2rem; border-radius: .6rem; background: linear-gradient(135deg, oklch(0.75 0.15 160), oklch(0.6 0.16 200)); }
#curve-demo .cd-toast b { display: block; font-weight: 600; }
#curve-demo .cd-toast small { color: var(--mv-fg-muted); }
#curve-demo .cd-code { margin: 0; padding: .55rem .7rem; max-height: 3.2rem; overflow: hidden; border: 1px solid var(--mv-border); border-radius: var(--mv-radius-md); background: var(--mv-bg-muted); color: var(--mv-fg-muted); font: 500 .66rem/1.5 var(--mv-font-mono); word-break: break-all; }
#curve-demo .cd-panel { display: grid; gap: .6rem; width: 16.5rem; padding: .75rem; border: 1px solid var(--mv-border); border-radius: var(--mv-radius-lg); background: var(--mv-surface); box-shadow: var(--mv-shadow-sm); }
#curve-demo .cd-head { display: flex; justify-content: space-between; align-items: baseline; margin: 0; color: var(--mv-fg-subtle); font: 600 .7rem var(--mv-font-sans); letter-spacing: .06em; text-transform: uppercase; }
#curve-demo .cd-dur { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: .6rem; font-size: .75rem; color: var(--mv-fg-muted); }
#curve-demo .cd-dur output { font: 500 .7rem var(--mv-font-mono); font-variant-numeric: tabular-nums; color: var(--mv-fg); min-width: 3.4rem; text-align: end; }
</style>
<div class="cd-col">
<div class="cd-stage">
<div class="cd-toast" id="curve-demo-toast"><i></i><span><b>Export complete</b><small>final-cut.mp4 · 42 MB</small></span></div>
</div>
<p class="cd-code" id="curve-demo-out"></p>
</div>
<div class="cd-panel" role="group" aria-label="Inspector">
<p class="cd-head">Toast enter</p>
<label class="cd-dur"><span>Duration</span><input type="range" class="mv-slider" id="curve-demo-dur" min="200" max="2000" step="50" value="900"><output id="curve-demo-dur-out">900 ms</output></label>
<mv-curve-editor id="curve-demo-editor" name="easing" label="Toast enter curve" value="cubic-bezier(0.34, 1.56, 0.64, 1)" duration="900"></mv-curve-editor>
</div>
</div>
<script type="module">
const $ = (id) => document.getElementById(`curve-demo-${id}`);
const ed = $("editor");
let timer = 0;
let springMs = null;
const show = () => { $("out").textContent = `transition-timing-function: ${ed.value};`; };
const play = () => {
clearTimeout(timer);
if (!ed.isConnected) return;
show();
const ms = (!ed.points && springMs) || Number($("dur").value);
$("toast").animate([{ translate: "0 140%", scale: 0.9, opacity: 0 }, { translate: "0 0", scale: 1, opacity: 1 }], { duration: ms, easing: ed.value, fill: "both" });
timer = setTimeout(play, ms + 1400);
};
ed.addEventListener("mv-input", show);
ed.addEventListener("mv-change", (e) => { springMs = e.detail.duration; play(); });
$("dur").addEventListener("input", () => { $("dur-out").textContent = `${$("dur").value} ms`; ed.setAttribute("duration", $("dur").value); });
$("dur").addEventListener("change", play);
customElements.whenDefined("mv-curve-editor").then(() => requestAnimationFrame(play));
</script>API
Attributes
| Name | Typ | Default | Description |
|---|---|---|---|
value | string | ease | cubic-bezier(…), a keyword (ease, ease-in…) or linear(…). Restored on reset. |
duration | number | 1100 | Preview duration (ms); a spring uses its settling time. |
y-min / y-max | number | -0.4 / 1.4 | Vertical range of the plot: handles can go beyond 0..1 within this limit. |
label | string | Group name. | |
no-presets / no-preview | boolean | Hides the presets or the ball. | |
name / disabled / readonly | Standard behavior. |
Properties
| Name | Typ | Description |
|---|---|---|
value | string | “cubic-bezier(0.22, 1, 0.36, 1)” or “linear(…)”. |
points | number[] | null | [x1, y1, x2, y2], null for a linear() curve. |
strings | object | Default texts, for translation (label, start, end, handle, presets, direction, in, out, in-out, family names, x1…y2, points, settles). The label attribute wins. Assignable before the element is defined. |
Methods
| Name | Description |
|---|---|
springEasing({ stiffness, damping, mass, points }) | Module export: damped spring (core spring()) sampled as linear(), with its duration. |
parseEasing(str) | Module export: parses cubic-bezier, keywords and linear(). |
Events
| Name | Description |
|---|---|
mv-input | While dragging or typing. detail: { value, points, preset, duration } (preset like "expo-out", "linear", "spring" or null; duration is the spring settling time in ms, else null). |
mv-change | Release, key press, preset, direction or committed field. |
CSS classes
| Name | Description |
|---|---|
mv-curve-editor-plot / -handle / -tip / -path / -tangent / -preview / -ball / -ghost / -fields / -field / -readout / -presets / -dirs / -dir / -families / -preset | Generated parts. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-curve-editor-width | 100% | Width (the plot is square). |
Accessibility
Named role="group". Each handle is a button with role="slider" (aria-roledescription “2D handle”) and an aria-valuetext like “x 0.42, y 0”; a coordinates badge shows the same values while it is focused, hovered or dragged. Arrow keys move by 0.01 (Shift 0.1, Alt 0.001), Page Up/Down move y by 0.1, Home/End set x to 0/1, and Escape cancels a drag. Fields are labelled native number inputs. Direction and family buttons are toggle buttons (aria-pressed). The preview is decorative (aria-hidden) and stays still with reduced motion; forced colors keep the curve, handles and pressed states visible.