# Motion & curves

> The motion control (an easing or a spring over one value), the spring and bézier editors on their own, and the expression grapher, each driving live motion.

The page: https://ryankiley.github.io/tweakit/motion.html. Every page as Markdown: https://ryankiley.github.io/tweakit/llms.txt

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.

```js
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).

```js
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.

```js
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));
```
