Motion & curves
These controls edit plain values that feed straight into your own animation code: a
transition's timing, a spring's parameters, a CSS easing curve, a function of x.
All of them are lazy-loaded heavy controls.
Motion
{ type: "motion" } is one control for a transition's timing: an
easing curve with a duration, or a spring tuned by feel (duration + bounce), switched
inside the popover over one value. Either way the param carries the CSS it resolves
to, duration in ms and an easing string
(cubic-bezier(…), or a sampled linear(…) for the spring), so
one transition works in both modes. The spring's fields read in ms and %,
but the value keeps visualDuration in seconds and bounce as
−1 to 1, as the snippet writes them; a stiffness/damping/mass config is accepted and
mapped to the nearest pair. Copy and Paste at the foot of the popover move a whole
motion to another transition, switching it to easing or spring as the copy is. Tap
anywhere on the stage and the box goes there with the motion; make the spring
bouncier, or switch to an easing.
const stage = target.querySelector(".mo-stage"), box = target.querySelector(".mo-box");
const panel = tweaks("Motion", {
move: { type: "motion", visualDuration: 0.45, bounce: 0.3 }, // or { type: "motion", curve: "ease-out", duration: 240 }
});
mount.append(panel.el);
const apply = (p) => { box.style.transition = `transform ${p.move.duration}ms ${p.move.easing}`; };
panel.on(apply);
panel.ready.then(() => apply(panel.params));
stage.addEventListener("pointerdown", (e) => {
const r = stage.getBoundingClientRect();
const x = Math.max(0, Math.min(r.width - 40, e.clientX - r.left - 20)); // the box's left edge, kept inside the stage
box.style.transform = `translateX(${x}px)`;
});mount is the panel's slot inside the stage; target is the demo surface it controls. In your own page you'd just document.body.append(panel.el).
Spring and bézier on their own
When the motion isn't a transition, use the editors inside Motion alone.
{ type: "spring" } has two modes, physics (stiffness / damping / mass) and
time (duration + bounce), and resolves to { stiffness, damping, mass }
either way (plus the time pair when tuned by feel) for a simulation that reads them
every frame (the ball here is a tiny integrator). { type: "cubicbezier" }
is the bare four-number curve for anything that takes a timing function on its own
(the dot's keyframe animation).
const ball = target.querySelector(".pc-ball"), dot = target.querySelector(".pc-dot");
let x = 0, vel = 0, dest = 0, raf = 0;
const panel = tweaks("Pieces", {
spring: { type: "spring", stiffness: 220, damping: 18, mass: 1 },
send: { type: "button", label: "Send the ball", action: () => { dest = dest ? 0 : 1; go(); } },
curve: { type: "cubicbezier", value: [0.25, 0.1, 0.25, 1] },
seconds: { type: "slider", value: 1.2, min: 0.2, max: 3, step: 0.1, unit: "s" },
});
mount.append(panel.el);
const go = () => {
cancelAnimationFrame(raf);
const tick = () => {
const { stiffness, damping, mass } = panel.params.spring;
vel += ((-stiffness * (x - dest) - damping * vel) / mass) / 60;
x += vel / 60;
ball.style.left = `calc(8px + ${x} * (100% - 46px))`;
if (Math.abs(vel) + Math.abs(x - dest) > 0.0005) raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
};
const apply = (p) => {
dot.style.animationTimingFunction = `cubic-bezier(${p.curve.join(", ")})`;
dot.style.animationDuration = `${p.seconds}s`;
};
panel.on(apply);
panel.ready.then(() => apply(panel.params));Plot
An expression grapher with a safe evaluator: no eval, just a
small parser over x, the usual math functions and constants, and calculator
shorthand such as 2x and sin x. The
param is the expression string itself; type into the field to regraph. Pass
fn instead to graph one of your own functions (read-only), or
editable: false to drop the field and keep a fixed expression, and
xMin/xMax/yMin/yMax to frame it and
samples for the resolution.
params.wave = "sin(x) * exp(-x / 6)"const readout = target.querySelector(".plt-readout");
const panel = tweaks("Plot", {
wave: { type: "plot", expr: "sin(x) * exp(-x / 6)", xMin: 0, xMax: 12 },
});
mount.append(panel.el);
const apply = (p) => {
readout.textContent = `params.wave = ${JSON.stringify(p.wave)}`;
};
panel.on(apply);
panel.ready.then(() => apply(panel.params));