← SplineCONTENT HISTORY
WHAT CHANGED
Update to Spline
Snapshot Oct 24, 2026 · 13:29 UTC · version 1.2.0
Collection source: downloaded plugin package.
First saved snapshot
No earlier snapshot is available to establish a change.
Save Spline and check future changes in Following.
Up to 10 plugins free · no card required.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"description": "DEFAULT recipe for ALL animation/motion/interactivity asks (\"animate …\", \"make it grow/bloom/orbit/spin\", hover/click behavior, ambient life): scene by run_code with stable names + a pivot at every motion origin; the motion as plain JS in the HTML layer driving objects per frame via the spline bridge — one rAF clock, poses from elapsed time f(t), easing + stagger, entrance → idle handoff, recipes, perf budget. The built-in States & Transitions feature is OPT-IN only (load state-transitions when the user explicitly asks).",
"included_files": [],
"name": "spline-3d-animation",
"skill_md_contents": "---\nname: spline-3d-animation\ndescription: \"DEFAULT recipe for ALL animation/motion/interactivity asks (\\\"animate …\\\", \\\"make it grow/bloom/orbit/spin\\\", hover/click behavior, ambient life): scene by run_code with stable names + a pivot at every motion origin; the motion as plain JS in the HTML layer driving objects per frame via the spline bridge — one rAF clock, poses from elapsed time f(t), easing + stagger, entrance → idle handoff, recipes, perf budget. The built-in States & Transitions feature is OPT-IN only (load state-transitions when the user explicitly asks).\"\n---\n\n# Animation & motion (code-driven)\n\nMotion is CODE-DRIVEN BY DEFAULT: the scene — objects, pivots, materials,\nlights — is authored with `run_code`, and ALL motion and behavior —\nentrances, growth, orbits, ambient life, reactions to input — is plain\nJavaScript in the file's HTML layer (`set_html_content`), computing poses\nfrom elapsed time and writing them through the `spline` bridge\n(`spline.updateObject`) every frame. This generalizes the `games` skill's\nloop architecture to every moving scene, and the document ships with\nexports (public URLs, viewer embeds, runtime code exports), where the same\nbridge is injected — the motion IS the file's runtime behavior.\n\nDo NOT reach for the built-in states machinery unless asked:\n\n- States & Transitions (`animate()`, addState/addEvent/addAction/\n updateTween, the timeline) is OPT-IN: use it ONLY when the user\n explicitly asks — they name states, transitions, the timeline, or the\n States panel. Then load `state-transitions` and follow it instead.\n- Two things must stay on the engine's machinery: PHYSICS-COUPLED\n kinematic motion (a positioned body pushes dynamic bodies only when a\n STATE animates it — bridge writes are teleports the simulation cannot\n see; `physics` skill), and SCROLL-DRIVEN sequences (the bridge never\n relays scroll — that is the Scroll event's job; `interactivity` skill).\n Decorative physics on objects the code never moves is fine.\n- Interactivity a scene ALREADY runs on states/events gets its fixes and\n small additions in kind (`get_objects` → update by id — `interactivity`\n skill); NEW motion beside it is still code.\n\nLoad `html-content` alongside this skill for the full bridge API and\ndocument rules (transparent background, passthrough, one document per\nfile).\n\nWhy this is the default: code motion is PROCEDURAL. Every pose derives\nfrom time, so it is eased and staggered per element, channels compose\n(grow + sway + drift on one object), a replay is one reset of the clock,\nand all the rules live in one debuggable place. A pair of hand-authored\nstates with a tween between them reads as mechanical next to it.\n\n## Layer 1 — build the scene FOR animation (run_code)\n\nMotion quality is decided at build time, before any code runs:\n\n- **Stable, unique names** for everything code will drive ('Part 0'…,\n 'Wing L', 'Moon 1'…). The document looks names up ONCE at\n boot and drives objects BY ID thereafter.\n- **STRUCTURE IS GENERATED — a part per moving element.** Whatever the\n subject, decompose it by what MOVES and generate that decomposition in\n code (see # Structure is generated below). The procedural generators\n (`tree()`, `rock()`) are baked wholes — right for scenery, wrong for a\n subject that must articulate or grow.\n- **A pivot at every moving part's motion origin.** Objects rotate and\n scale around their own origin, so put the origin where the motion\n starts: a tree grows from its BASE, a door swings on its HINGE, a wheel\n spins on its AXLE, a petal opens from its STEM end. Two ways:\n - the procedural generators already do it — `tree()`/`rock()`/\n `building()` bases sit at y = 0 of their position, so scaling the\n handle's group from ~0 grows it out of the ground;\n - for assemblies, `add('Empty')` positioned AT the motion origin,\n build the parts in place, then `moveInto` them (NEXT call — the\n selection rule) and drive the EMPTY. One pivot per moving part —\n the same rule as procedural-generation's creature rigs.\n - a single center-origin primitive can grow from its base without a\n pivot by compensating in code: write `scale.y = s` AND\n `position.y = baseY + (height * s) / 2` in the same frame.\n- **Bridge transforms are ABSOLUTE and LOCAL** (rotations in DEGREES).\n `updateObject` SETS values — its `scale` is not the DSL's multiply —\n and reads/writes are in the parent's frame, so snapshot each driven\n object's initial transform at boot and compute against that.\n- **One writer per property.** An object the document drives must not\n ALSO carry states/bindings writing the same property — they fight.\n Compose all of a frame's channels in JS and write once.\n\n## Layer 2 — the motion document (set_html_content)\n\nONE complete HTML document per file. For pure animation it is HEADLESS —\na `<script>` and nothing visible: leave html/body unstyled and\ntransparent; with no UI elements every pointer event passes through to\nthe viewport, so the user can still orbit. Add UI only when the request\ncalls for it (controls, a replay button, a HUD). If the file may already\nHAVE a document (`get_html_content` to check), MERGE your motion in with\n`edit_html_content` — never clobber a game or HUD with a full rewrite.\n\nNon-negotiables, shared with the `games` loop:\n\n- **One rAF loop, one clock.** Start it once at boot; every pose derives\n from a single elapsed time.\n- **STATE FROM TIME — `pose = f(t)`, never `pose += delta`.** Compute\n each frame's pose from elapsed seconds: deterministic, staggerable per\n element, hitch-proof, and replayable (restart = reset the clock).\n Accumulate (+=) only when integrating INPUT or a simulation — that is\n the games skill's fixed-timestep territory, not an animation's.\n- **Boot reads, then cached ids**: `await spline.ready`, look each name\n up ONCE, keep `{ id, p: initial position }`. A null lookup logs to the\n console and aborts cleanly — a throw inside the tick silently freezes\n ALL motion.\n- **Easing + progress helpers** — inline these; motion is always eased:\n\n```js\nconst clamp01 = (x) => Math.min(1, Math.max(0, x));\n// phase progress: 0..1 over dur seconds, starting at delay\nconst prog = (t, delay, dur) => clamp01((t - delay) / dur);\nconst easeOutCubic = (p) => 1 - Math.pow(1 - p, 3);\nconst easeInOutSine = (p) => 0.5 - Math.cos(Math.PI * p) / 2;\n// overshoot (\"pop\") — the organic entrance\nconst easeOutBack = (p) => {\n const c = 1.70158; const q = p - 1;\n return 1 + (c + 1) * q * q * q + c * q * q;\n};\n```\n\n- **Stagger per element.** Element i gets its own delay (`i * 0.18`) and\n its own phase/duration jitter — simultaneous identical motion is the\n mechanical look the code path exists to avoid.\n- **Entrance → idle handoff.** A growth/entrance settles into gentle\n perpetual life (sway, breathe, drift), never a hard stop: ramp the idle\n amplitude in with its own `prog(...)` as the entrance ends.\n\n## Structure is generated (a part per moving element)\n\nAn animated subject is DECOMPOSED BY MOTION: one named object per element\nthat moves independently. The part list comes from asking \"when THIS\nsubject moves / grows / reacts, what moves separately?\" — never from a\nfixed recipe; different subjects decompose differently. Generate that\ndecomposition in code (bounded loops/queues/recursion in `run_code`,\nseeded randomness for organic variation) at the fidelity the ask implies,\nand create every part at its authored FINAL pose — Edit mode shows the\nfinished subject; the document takes it through its motion.\n\n- The driver must KNOW the structure — each part's role, geometry and\n timing. Two general techniques:\n - DETERMINISTIC REGENERATION: derive the part list from one seeded\n routine and run the IDENTICAL routine in both places (same seed, same\n call order ⇒ the same arrays; any edit edits both copies). Right when\n parts carry rich data the scene snapshot can't express — directions,\n lengths, per-part timing.\n - BOOT READS: name parts by convention ('Part 0'…), read their authored\n transforms at boot (`getAllObjects` + prefix filter) and derive the\n motion from those. Right when the authored pose is all the driver\n needs.\n- TIMING IS PART OF THE STRUCTURE: give each part its own window or phase\n on ONE normalized timeline (`t = clamp01(elapsed / DURATION)`).\n Entrances run in structural order — supporting parts before the parts\n that depend on them, dependents opening BEFORE their support fully\n finishes: the overlap is what reads organic; a strict one-after-another\n sequence reads mechanical.\n- DIRECTIONAL PARTS: a +Y-built part is aimed along a unit direction `d`\n with XYZ Euler degrees —\n `{ x: Math.atan2(d.z, d.y) * 180 / Math.PI, y: 0,\n z: Math.asin(-d.x) * 180 / Math.PI }` — the tool for any structure\n whose parts point somewhere (limbs, spokes, streams, rays). A part\n grows along its own axis by scaling while re-centering:\n `scale.y = g` plus `position = start + dir * (len * g / 2)`.\n- GRANULARITY = MOTION, NOT GEOMETRY. A part is an independently MOVING\n element, not a geometric segment: a hero subject usually decomposes\n into ~5–20 parts, and each part's BODY is ONE rich procedural\n `customMesh` — compute the whole limb/blade/petal cluster, offshoots\n and detail included, into a single mesh (up to 60k vertices), pivoted\n at its motion origin. The object list should read like the subject's\n ANATOMY, never like its polygons — one object per geometric segment\n floods the scene tree the user has to live in. When even the anatomy\n parts would multiply (per-branch growth, a crowd, a wave field), drop\n a LEVEL instead of adding objects: the vertex channel and the\n instancing channel below animate arbitrary internal detail through ONE\n scene object. Write only what CHANGED each frame: zero a part once\n before its window opens, stop writing it once it settles, rotate slow\n ambient writes across frames.\n- THE CHANNEL MAP — every level of motion through one object or less:\n - WHOLE-OBJECT: transforms/visibility (`updateObject`) — pivots,\n orbits, travel; numeric geometry params; `displace`/`noise` layer\n `movement` per frame = shader-space wobble/sway/shimmer for free.\n - INSIDE ONE MESH — the VERTEX CHANNEL: a `customMesh` accepts\n `updateObject(id, { geometry: { vertices, normals?, uvs?,\n indices? } })` per frame (flat arrays; pass `Float32Array` —\n matching topology updates the live GPU buffers in place). THE way to\n grow/deform structure without objects: keep the generator in the\n document, rebuild the vertex array at progress t, write it — a whole\n branching tree growing per limb is ONE mesh (≤60k vertices). Keep\n the topology (vertex/index counts) FIXED across frames — grow by\n moving vertices (collapsed→extended), not by adding them — so every\n frame takes the cheap in-place path.\n - MANY COPIES — the INSTANCING CHANNEL: make the template a cloner\n (`clone({...})`), then `spline.setInstanceTransforms(name,\n [{position, rotation, scale}, …])` writes every copy's transform in\n ONE call, indexed by copy (partial axes; degrees; a null entry — or\n null instead of the array — hands copies back to the layout;\n overrides survive re-layouts). THE way to drive swarms, crowds,\n leaves, debris: hundreds of moving copies, one scene object.\n - DYNAMIC COUNTS — RUNTIME SPAWNING: `spline.spawnObject(template,\n { name?, position?, rotation?, scale?, visible? })` creates a\n runtime-only copy (never saved, not selectable/raycastable; renders\n through its template) and resolves to its snapshot — drive it by id,\n remove with `spline.destroyObject(id)`. THE way to emit objects the\n scene can't pre-place (projectiles, spawned pickups, growing counts);\n budget 500 live copies — reuse or destroy, and hide the template\n with `visible: false` when it is only a mold. Copies restyle\n through their TEMPLATE (shared look), never individually.\n\nThe generic wiring, whatever the subject:\n\n```js\nawait spline.ready;\nconst parts = []; // { id, …per-part data }\nfor (let i = 0; i < COUNT; i++) { // from regeneration or boot reads\n const o = await spline.findObjectByName('Part ' + i);\n if (!o) { console.warn('missing Part ' + i); return; }\n parts.push({ id: o.id, p: o.position });\n}\nconst t0 = performance.now();\n(function tick(now) {\n const t = Math.min(1, (now - t0) / 1000 / DURATION);\n for (let i = 0; i < parts.length; i++) {\n // eased progress of part i's own window; skip when unchanged;\n // ONE composed updateObject(parts[i].id, …) from t, i and part data\n }\n requestAnimationFrame(tick);\n})(t0);\n```\n\n## Ambient life (phase offsets)\n\nScenes that should just FEEL alive (drifting clouds, bobbing boats,\nflickering fireflies) get the same loop with no entrance: per element i,\ncombine small channels with per-element frequency and phase —\n`y = p.y + Math.sin(t * (0.5 + i * 0.13) + i * 2.4) * 12`, a slow drift\non x/z, a fraction of a degree of roll. Never one synchronized bob on a\nwhole group, and keep amplitudes SMALL (a few units, 1–4°) — ambient means\nfelt, not watched. Collect the elements at boot by name prefix\n(`getAllObjects()` then filter) so a scatter of 20 clouds is one array.\n\n## Orbits, spins & travel\n\n- Endless spin: `rotation.y = (t * 24) % 360` (absolute; % keeps numbers\n finite).\n- A REAL orbit is position math, not a spin:\n `x = cx + Math.cos(t * w + phase) * r; z = cz + Math.sin(t * w + phase) * r`\n — with per-body radius/speed/phase (moons, electrons, drones). Add\n `rotation.y = -(t * w + phase) * 180 / Math.PI` to keep a face inward.\n- Travel along a route: precompute waypoints in JS, index by\n `u = (t / lapSeconds) % 1`, and interpolate — same shape as the games\n skill's racing line (steady speed needs arc-length-ish spacing:\n keep waypoints roughly evenly spaced).\n\n## Triggered phases & input (code-driven interactivity)\n\nA one-shot reaction is STILL f(t): record when it started, then compute\nfrom that moment.\n\n```js\nlet burstAt = -1; // set on input\nconst b = burstAt < 0 ? 0 : prog(t, burstAt, 0.6); // in the tick\n```\n\n- Page-level input (keyboard, pointer anywhere, UI controls): plain DOM\n handlers in the document — buttons/sliders drive parameters directly\n (`html-content` skill). Games-grade input (held-key sets, focus/pause)\n is the `games` skill.\n- Click/hover on a SPECIFIC OBJECT: author that event on the object as a\n SENSOR — it needs NO actions — and respond in code. run_code:\n `select('Bloom'); addEvent('MouseDown');` — document:\n `spline.addEventListener('mouseDown', (e) => { if (e.target.name === 'Bloom') burstAt = t; })`.\n Sensor events fire in Play mode (and exports) — say so to the user.\n- Replay/restart when asked: `t0 = performance.now()` (and re-zero any\n trigger timestamps) wired to a button or key.\n\n## Imported model animations (baked clips)\n\nAn IMPORTED model (GLB/FBX) may carry its own authored clips — skeletal or\nnode animations baked into the file. Those are already eased and\nhand-keyed: PLAY them, never rebuild them with pose writes.\n`clips:N` in the scene digest flags the object carrying them;\n`get_objects` lists each clip's `id`, `name` and `duration`. The\ndocument drives them through the bridge (`html-content` skill):\n\n```js\nawait spline.ready;\nconst MODEL = '<model name or id>';\nconst clips = await spline.getAnimations(MODEL); // the real names: never guess\nconst base = clips[0].name;\nawait spline.playAnimation(MODEL, base); // loops by default\ndocument.querySelector('#next').onclick = () =>\n spline.playAnimation(MODEL, clips[1].name, { fade: 0.3 }); // cross-fade\ndocument.querySelector('#once').onclick = async () => {\n const clip = await spline.playAnimation(MODEL, clips[2].name, { loop: false, fade: 0.15 });\n if (clip) setTimeout(() => spline.playAnimation(MODEL, base, { fade: 0.3 }),\n clip.duration * 1000); // one-shot, then back\n};\n// spline.pauseAnimation(MODEL) holds the pose (play resumes);\n// spline.stopAnimation(MODEL) rests it on the first frame.\n```\n\n- An import ships a `Start` event whose Animation action plays the\n model's first clip. When the document drives the clips, REMOVE that event\n first (`get_objects` for its id → `removeEvent` in run_code) — it\n fires a beat after the document boots and overrides its first\n `playAnimation`. One owner per model's clips.\n- One clip per object at a time: playing another replaces it (`fade`\n blends the two). Clip names come prefixed with the file name\n ('<file>_<clip>'); the clip's own name resolves too.\n- Moving the model AROUND (travel, turning to face a direction) is still\n the loop's job — drive its top-level group with `updateObject` while\n the clip animates the limbs, never the clip's own bones.\n- Use an Animation ACTION (`interactivity` skill) instead only when the\n user asks for it on an event in the States/Events panel.\n\n## Material & geometry motion\n\n- Material motion rides the same loop: `spline.updateObject(id,\n { material: { layers: [{ type: 'gradient', angle: 120 + Math.sin(t * 0.7) * 40 }] } })`\n — flowing water, shimmer, pulsing emissive color. Hero surfaces only.\n- Geometry parameters animate too (`{ geometry: { cornerRadius: v } }`,\n a Text object's `{ geometry: { text: label } }`) — each write\n REGENERATES the mesh, so keep it to one or two hero objects, and\n throttle text to ~10 Hz. Note a Lathe's size does not respond to\n geometry tweens (profile-driven) — scale it instead.\n\n## Performance budget\n\n- ONE `updateObject` per animated object per frame, fire-and-forget (no\n await in the tick). A few dozen writes per frame is comfortable (brief\n peaks near sixty are fine); hundreds is not — past a few dozen movers,\n switch channel: `setInstanceTransforms` moves every cloner copy in ONE\n message, and the vertex channel moves a whole mesh's structure in one.\n Otherwise write only what CHANGED\n this frame (track per-part zeroed/settled flags), drive GROUP\n pivots instead of leaves, thin the animated set, or update the slow\n layer every 3rd frame.\n- Never write DOM text every frame (~10 Hz for readouts — games rule).\n- Bridge writes are transient (never saved, reset on reload) — exactly\n right for motion; the authored scene stays clean.\n\n## Verify\n\n- `play()`, screenshot, then screenshot AGAIN — the two captures land a\n beat apart, so poses MUST differ between them; identical frames mean\n the loop isn't running (Console strip shows the error — read it).\n `stop()` when done so the user isn't left in play mode.\n- Screenshots capture the 3D canvas only — HTML UI never appears in\n them; verify documents through the Console strip and by telling the\n user what they should see.\n- Tell the user where the motion runs: Play mode, the Preview tab, and\n every export. The Edit tab unmounts the document so the scene stays\n editable — the still scene there is expected, not a bug.\n- Entrance pacing: a whole entrance inside ~2–4 s unless asked otherwise;\n idle amplitudes small. If the screenshotted mid-growth pose looks like\n scenery collapsing (parts at odd scales), the pivots/compensation are\n wrong — fix Layer 1, don't mask it with timing.\n\nGotchas:\n- `updateObject` transforms are ABSOLUTE, LOCAL, and in DEGREES — read\n the boot snapshot, write computed values; a DSL-style \"multiply\" scale\n assumption doubles the object every frame.\n- A thrown error inside the tick freezes ALL motion silently — guard\n lookups, read the Console strip.\n- Exact scale 0 can degenerate — clamp to 0.001.\n- Two writers on one property (a state/binding + the loop) fight —\n one owner per property, channels composed in JS.\n- Don't drive objects a physics simulation owns (dynamic bodies), and\n don't expect driven motion to push them — kinematic pushing is the\n states carve-out (`physics` skill).\n- One document per file: merge into an existing document with\n `edit_html_content`; `set_html_content` replaces the whole thing.\n"
}SHA-256 of public snapshot: adf482b20208fa3c62febad6f9f01b42ef56b9e5a763f8c0cc6f3f4c14ae53d0