Curves, shapes and orientations
Curves map one value to another, or animate one over time. Your code samples them, and the panel edits them in place:
import { gui, curve, easing } from '@haxiomic/devui';
const anim = gui.folder('Anim').params({
bounce: curve([[0, 0], [0.5, 1], [1, 0]], 'loop'), // keys [x, y], smooth between them
falloff: curve([[0, 1, 'linear'], [4, 0]]), // any domain: seconds, metres…
ease: { value: curve(easing('easeInOutCubic')), view: 'easing' },
});
anim.bounce.sample(t); // the value at t (and the editor shows a playhead at t)
anim.falloff.lut(64); // Float32Array over the keys' domain
anim.ease.cubicBezier(); // 'cubic-bezier(0.65, 0, 0.35, 1)'
- Keys are smooth (automatic tangents that don't overshoot), bezier (your own handles), linear or step. Outside the keys a curve holds, loops, ping-pongs or continues.
- The row shows the curve's shape, with a dot where your app last sampled it. Click it to edit.
- The editor:
- Keys: double-click to add one, drag to move it (Shift snaps), Delete to remove it. Drag a box to select several.
- Handles: drag one to shape the curve (Alt moves the two apart).
- Toolbar: set the interpolation, apply an easing preset, or choose what happens outside.
- The view: as every graph (see gestures): scroll to zoom (Alt: the values alone; Alt + Shift: the time alone), Shift + scroll to pan along the time, and Alt-drag, the middle button or two fingers to pan. Focused, + and − zoom and 0 fits, as fit does;
[and]pick a key, the arrows move it (Shift faster, Alt slower), andI(or Insert) adds one halfway to the next. - Its own window: window opens the editor in a resizable window. Dock that at the bottom for an animation strip.
- Easings (
view: 'easing') are one segment from (0,0) to (1,1), shaped by its two handles, like CSS. The row shows the four numbers. - Production builds:
curve()works in the noop build too. three.js:curveTexture(anim.falloff)is a 1D half-float lookup texture that updates as you edit.
Your own curves. A list of points or a CSS easing your code already has is edited in place, and your code samples it as it did:
const motion = gui.folder('Motion').params({
falloff: [[0, 1], [0.25, 0.85], [1, 0]], // [x, y] or { x, y }, x rising
easing: 'ease-in-out', // or 'cubic-bezier(…)'
});
dot.style.animationTimingFunction = motion.easing;
- Points join as your code joins them, so the editor shows that and can't change it: straight lines unless you say otherwise. Say
curve: { interp: 'step' }, or'smooth'where your code does as devui's smooth does (a monotone cubic), and what it gives outside them withcurve: { extrapolate: 'loop' }. A point you add is shaped as the last, and each keeps its array or object. - A CSS easing: a
cubic-bezier(…)anywhere, and a keyword (ease-in) on a value named like an easing (easing,ease,…TimingFunction). It stays its keyword while it still is one.
Springs. A spring config your app already has, for whichever library animates with it, is a spring's row: how it moves, its damping ratio and how long it takes to settle. Click it to tune it:
const motion = gui.folder('Motion').params({
pop: { type: 'spring', stiffness: 260, damping: 14 }, // Motion
drawer: { tension: 170, friction: 26 }, // react-spring
fade: { duration_s: 0.4, bounce: 0.5 }, // physics-animator
});
animate(el, { x: 100 }, motion.pop); // as before
- Each library's own: Motion's
{ stiffness, damping, mass },{ duration, bounce }and{ visualDuration, bounce }; react-spring's{ tension, friction, mass }and{ frequency, damping }; physics-animator's{ strength, damping },{ duration_s, bounce },{ duration_s }and{ halfLife_s }. Each is read as its library reads it and edited in its own terms, and the rest of the object (type: 'spring') stays as it is. - The editor: drag sideways for slower or faster, and up and down for more or less bounce, keeping what the config says its speed is (its stiffness, or its duration). A config with no bounce (
halfLife_s) is only slower or faster. Its numbers are fields, and double-click resets. Copy as gives the same spring for Motion, react-spring, physics-animator, GSAP or CSS. - The primitives are
@haxiomic/devui/spring, with nothing else of devui's in it, so they work the same in production. For GSAP, which has no springs, and CSS, as easings:
import { physicsOf, spring, stepSpring } from '@haxiomic/devui/spring';
const s = spring({ duration: 0.6, bounce: 0.3 }); // from any library's config
gsap.to(el, { x: 100, ...s.gsap() }); // { duration, ease }
const { duration, easing } = s.css(); // a CSS linear() easing
el.style.transition = `transform ${duration}s ${easing}`;
stepSpring(physicsOf(config)!, state, target, dt); // or step any config yourself, exactly
- What it costs: the row, its editor and the spring maths load the first time a spring shows. Your app's bundle has only the configs' keys.
Orientations, directions, pads and knobs:
view: 'orientation': rotations on a sphere, with a small car in the middle that has an obvious front (headlights, a bonnet), back (red tail-lights) and top, so you can see what the rotation does. Its own axes are drawn too (x red, y green, z blue).- What it takes: a quaternion
[x, y, z, w](or{x, y, z, w}), or Euler angles[x, y, z]in radians (unit: 'deg'for degrees) in any order:orientation: { order: 'YXZ' }. three'sEulerandQuaternion(soobject.rotationand.quaternion) get it, and so do unit 4-vectors namedrotation,orientationorquat…. - Gestures: drag on the sphere to turn it (the point under the pointer stays under it). Drag one of the rings over it to turn about that axis alone, or the ring around it to turn about the view. The rings are the model's own axes (local, the default): they turn with it, so its x ring pitches it however it's turned, and the ring about its forward axis rolls it. world makes them the world's x, y and z (the horizon is y's); the choice is kept, for every row. A ring seen edge-on lies along the axis lines, so it fades to a faint dashed line until you point at it; the middle of the sphere is always a free turn, so it's taken towards its ends. Shift snaps a ring's turn to 15° steps from where you took it (on a free drag, the angles themselves); Alt keeps a free drag to one of the rings' axes, the one nearest your drag. Double-click resets.
- No gimbal lock: a drag turns the rotation itself, never its Euler angles. Euler angles stay continuous as you turn past 90° and 180°, even straight through the top, where two of them turn about the same axis (they keep the split they had); a quaternion doesn't flip sign.
- The rest: look from three-quarters, the front, the top or the side; axes shows or hides its own axes; identity; snap 90° (the nearest right angles).
- The row shows the car at its orientation, then the angles (in degrees) or the components. Typing a quaternion component rescales the others, so it stays unit length.
orientation: { forward: '-z' }says which way the object faces: three's objects face +Z (the default), cameras and lights −Z (set for you by the three inspector).
- What it takes: a quaternion
view: 'direction'(inferred for unit vectors named like directions): the same sphere, with an arrow from its centre along the vector. Drag on the sphere to point it, with Shift to snap to the axes and diagonals. Double-click goes through to the other side.view: 'pad'on a 2D vector: an XY pad over itsmin/max. Shift snaps, and double-click resets.view: 'knob'on a number (inferred for one in an angle unit): a dial beside its field (which has all the number gestures). Click the dial for a big knob. Press or drag anywhere on it and its tip goes where you point, at any distance from the centre: where the pointer is sets the value, whatever way it got there. Ctrl (⌘ on macOS) rounds it (a range's tidy steps, an angle's 15°). For finer changes, drag the number in the middle, or use the arrow keys.- What it shows: a range is a 270° sweep from the lower left, its ends and default marked. Pointing into the gap at the bottom gives the nearer end; a value typed past the range shows at its end, in orange, and says so. An angle (
unit: 'rad','deg'or'°') is the angle as on a clock or a compass: 0° at the top, rising clockwise (as every knob does), and a range a whole turn wide (−180° to 180°) wraps round, 180° at the bottom. A number without a range turns on without ends, a turn about the size of its value when you opened it (it says how much). - The rest: the number in the middle is a field (click to type). Double-click the knob to reset it. Arrow keys step it (Shift faster, Alt slower), Page Up and Down ten steps, Home and End go to the ends.
- What it shows: a range is a 270° sweep from the lower left, its ends and default marked. Pointing into the gap at the bottom gives the nearer end; a value typed past the range shows at its end, in orange, and says so. An angle (