{"id":46302,"plugin_id":"plugin_asdk_app_6acd235a70a48191812b3ba9c471b4ac","kind":"skill","collection_source":"plugin_package","comparison_source":null,"observed_at":"2026-10-24T13:29:11.426Z","digest":"71528387d9f01d0df4e060a3cac4c523325447086b84e779fcf85c5ce5338caf","against":null,"payload":{"description":"HTML content over the 3D canvas (set_html_content) — the DEFAULT home of motion and behavior code (animation loops, interaction logic, game loops; headless script-only documents are fine) as well as web UI. The full injected `spline` bridge API: direct object control (updateObject), runtime creation (createObject/spawnObject/setInstanceTransforms), imported-model animation clips (getAnimations/playAnimation/pauseAnimation/stopAnimation), custom three.js/TSL shader materials (createCustomMaterial), queries, variables/events, console — plus the transparent-background + pointer-passthrough rules and Play-mode caveats. Load BEFORE writing any HTML document.","included_files":[],"name":"spline-3d-html-content","skill_md_contents":"---\nname: spline-3d-html-content\ndescription: \"HTML content over the 3D canvas (set_html_content) — the DEFAULT home of motion and behavior code (animation loops, interaction logic, game loops; headless script-only documents are fine) as well as web UI. The full injected `spline` bridge API: direct object control (updateObject), runtime creation (createObject/spawnObject/setInstanceTransforms), imported-model animation clips (getAnimations/playAnimation/pauseAnimation/stopAnimation), custom three.js/TSL shader materials (createCustomMaterial), queries, variables/events, console — plus the transparent-background + pointer-passthrough rules and Play-mode caveats. Load BEFORE writing any HTML document.\"\n---\n\n# HTML content over the 3D canvas\n\n`set_html_content` attaches ONE complete HTML document to the file. The editor\nrenders it in a sandboxed iframe layered exactly over the 3D canvas — the user\nsees your markup composited on top of the live scene (Preview tab) and the\nsource in the Code tab, with a Console strip capturing your `console.*` output\nand errors. The document persists with the file, is undoable, and syncs to\ncollaborators.\n\nThis layer is the DEFAULT home of the file's MOTION and BEHAVIOR code —\nanimation loops, interaction logic, game loops (# Motion & interactivity;\nthe `animation` and `games` skills build on the API below). A document may\nbe HEADLESS — one `<script>` and nothing visible: leave html/body unstyled\nso the page stays transparent and every pointer event passes through to the\nviewport. Web UI (HUDs, dashboards, panels, forms, controls) is the other\nhalf of its job.\n\nThe HTML SHIPS WITH EXPORTS: public URLs, `<spline-viewer>` embeds, and code\nexports that run the Spline runtime all present the same HTML + 3D\ncomposition automatically (the same `spline` bridge, with the full event\nsystem live since exports always run in play mode). Only the three.js /\nreact-three-fiber exports — which don't use the Spline engine — omit it.\n\n## The environment\n\n- The document is standalone: your own CSS/JS goes inline, in the document.\n- EXTERNAL LIBRARIES from a CDN DO work — the frame is sandboxed\n  (`allow-scripts`, opaque origin, no access to the editor's DOM or cookies)\n  but not cut off from the network, so `<script src=\"https://…\">` and remote\n  stylesheets load normally. Reach for a chart/animation/UI library when it\n  genuinely helps; prefer inline code for small things, since every remote\n  script is another thing that can fail to load at export time.\n- The scene is NOT a three.js scene your script can reach into and traverse.\n  Scene-level work — fracturing, physics, geometry, particles — is BUILT\n  natively (CSG/booleans, particles, events) and DRIVEN through `spline.*`.\n  The one three.js seam is `createCustomMaterial` (below), and it takes\n  Spline's OWN bundled three namespace — a three.js you loaded from a CDN\n  cannot compile there. So never tell the user external libraries are\n  impossible; explain this split instead.\n- KEEP THE PAGE BACKGROUND TRANSPARENT (don't set a background on html/body)\n  — the 3D scene shows through everywhere you don't paint. Style your own\n  panels/cards, not the page.\n- Pointer events that hit the page background (html/body, or any element\n  inside one marked `data-spline-passthrough`) pass through to the 3D\n  viewport automatically — orbiting, selecting, and scene interactions keep\n  working between your UI elements. Events on your own elements stay yours.\n- A `spline` object is injected before your scripts run (also\n  `window.spline`). All methods return Promises; `await spline.ready` first.\n\n## The `spline` API\n\nReads:\n- `spline.getVariable(name)` / `spline.getVariables()` — current values.\n- `spline.findObjectByName(name)` / `spline.findObjectById(id)` /\n  `spline.getAllObjects()` — snapshots: `{id, name, type, visible,\n  position:{x,y,z}, rotation:{x,y,z} (degrees), scale:{x,y,z}}`. Snapshots\n  carry NO geometry — vertex data reads through `getGeometry` below.\n- `spline.getGeometry(nameOrId, {space?})` — a mesh's LIVE geometry, the\n  read mirror of the vertex write channel: resolves `{id, name, geometry?,\n  space, vertexCount, triangleCount, indexed, bounds: {min, max, size},\n  vertices: Float32Array, indices?: Uint32Array, normals?: Float32Array,\n  uvs?: Float32Array}` (or undefined + a console warning when the object\n  is missing or has no geometry — only meshes do). Works on every mesh\n  kind: parametric primitives (their real tessellation), custom meshes,\n  Text glyphs, Boolean results, generated models, and spawned copies\n  (their template's geometry). `space: 'local'` (default) is the mesh's\n  own frame — exactly what the vertex write channel takes back, so read →\n  modify → `updateObject` round-trips (fetch the base topology ONCE, keep\n  it, rewrite per frame); `space: 'world'` runs vertices (and normals)\n  through the world matrix — e.g. sample a terrain's surface heights to\n  place or move things along the ground.\n\nWrites:\n- `spline.updateObject(nameOrId, {position?, rotation?, scale?, visible?,\n  geometry?, material?})` — DIRECT object control; THIS IS THE DEFAULT\n  INTEGRATION POINT. Transforms in degrees, partial axes ok. `geometry`\n  overrides any parametric-geometry parameter by schema name and regenerates\n  the mesh live: `{geometry: {cornerRadius: 20}}`, `{geometry: {radius: 80,\n  segments: 12}}`, and on Text objects `{geometry: {text: 'Score: 42'}}`.\n  CUSTOM MESHES additionally take raw data — the VERTEX CHANNEL:\n  `{geometry: {vertices, normals?, uvs?, indices?}}` (flat arrays; pass\n  `Float32Array` for per-frame writes). Matching topology updates the live\n  GPU buffers in place, so rewriting a custom mesh's vertices every frame\n  is cheap — keep vertex/index counts fixed across frames (grow by MOVING\n  vertices, not adding them); ≤60k vertices. The base topology doesn't\n  need recomputing in script: `getGeometry` (Reads above) returns it.\n  `material` writes the mesh's material live: `{material: {color:\n  '#ff4040'}}` (the color layer — CSS hex/name or {r,g,b,a?} 0..1),\n  `{material: {alpha: 60}}` (material alpha 0..100), and\n  `{material: {layers: [{type: 'fresnel', intensity: 2}]}}` for any layer's\n  numeric/vector/color properties, addressed by `type`, `index`, or `id`\n  (layer `alpha` is 0..100; shader-structural props like `noiseType` are\n  skipped). Works in edit AND play mode, at 60fps (requestAnimationFrame) or\n  from input handlers. Transient/runtime-only: never saved, never touches\n  undo history, resets on reload — for persistent scene changes use\n  run_code.\n- `spline.spawnObject(templateNameOrId, {name?, position?, rotation?,\n  scale?, visible?})` — create a RUNTIME-ONLY copy of a template object\n  (projectiles, dynamic props, growing counts). The copy renders through\n  its template (same geometry/material — restyle the TEMPLATE to restyle\n  every copy), is never saved, and cannot be selected or raycast. TRS\n  defaults to the template's (rotation in degrees); `visible` defaults\n  true even when the template is hidden, so a hidden template works as a\n  mold. Resolves to the copy's snapshot — drive it by `id` with\n  `updateObject` (transforms + visible only). Budget: 500 live copies.\n- `spline.destroyObject(idOrName)` — remove a spawned copy (frees its\n  budget slot). Spawned copies only; hide document objects with\n  `updateObject(..., {visible: false})` instead.\n- `spline.setInstanceTransforms(nameOrId, transforms | null)` — per-copy\n  transform writes on a CLONER, indexed by copy: each entry\n  `{position?, rotation?, scale?}` (partial axes, degrees), `null` entry =\n  hand that copy back to the layout, `null` instead of the array = clear\n  all. One call moves every copy — THE channel for swarms, crowds, leaves,\n  debris; overrides survive the cloner's own re-layouts until cleared.\n  Resolves to the copy count (-1: not a cloner). `getAllObjects`/\n  `findObjectByName` snapshots report a cloner's `instanceCount`.\n- `spline.createObject(type, options?)` — spawn a NEW object at runtime;\n  resolves to its snapshot. `type`: a shape name ('Cube', 'Sphere',\n  'Torus', …, 'Text' — 'Geometry' suffix optional), 'CustomMesh' (raw\n  vertex geometry — see below), 'Group', or a light ('PointLight',\n  'SpotLight', 'DirectionalLight'). Options are one flat bag:\n  placement (`position`/`rotation` (degrees)/`scale`, `parent`,\n  `name`, `visible`), `material` (a shared material NAME from the file,\n  a css color, `{color, roughness, metalness, reflectivity, alpha}`, or a\n  custom shader material from `createCustomMaterial` — see below),\n  `color` shorthand, and type-specific parameters (`width`,\n  `cornerRadius`, `text`, `fontSize`, `intensity`, …). Anything else\n  (paths, vectors, booleans, particles…) is rejected with a clear error —\n  clone an existing object instead. Transient/runtime-only: never saved; in\n  the editor these are Play-mode spawns removed on reset.\n- `spline.createObject('CustomMesh', {vertices, indices?, normals?, uvs?,\n  …options})` — build a mesh from raw geometry data, the runtime twin of\n  run_code's `customMesh`: `vertices` is a flat `[x,y,z, x,y,z, …]`\n  array (number[] or Float32Array, max 60000 vertices, used at REAL\n  world-unit size), `indices` are triangle vertex indices (omit for a\n  non-indexed, faceted-look mesh — there is no flatShading switch at\n  runtime), `normals`/`uvs` optional (normals computed when omitted).\n  The same winding safety net as run_code applies — inverted closed solids\n  are flipped, open surfaces render double-sided — and the same\n  counter-clockwise-from-outside winding rules (incl. the XZ-plane gotcha)\n  from the core guidance apply. COMPUTE the arrays in script loops; this is\n  how a runtime generator makes terrain, rocks, and organic shapes the\n  primitives can't. After creation, MORPH it per frame through\n  `updateObject`'s vertex channel (same topology — see above): create\n  once, then rewrite `vertices` each frame for waves, breathing surfaces,\n  melting forms.\n- `spline.createCustomMaterial(factory)` — build a CUSTOM three.js/TSL\n  shader material and use it via `createObject`'s `material` option. NOT\n  the same thing as run_code's `createMaterial('Name')` (that saves a\n  Spline layered material as a shared asset) — this one escapes the layer\n  system entirely for looks the layers can't express (procedural shader\n  patterns, custom lighting response, animated surface math). The factory\n  receives Spline's bundled three namespace — build ONLY from it, never\n  from an imported/CDN three (a separate three cannot compile here):\n  ```js\n  const lava = await spline.createCustomMaterial((three) => {\n    const { tsl } = three; // Fn, uniform, vec3, texture, wgslFn, time, uv, positionLocal, …\n    const m = new three.MeshStandardNodeMaterial();\n    m.colorNode = tsl.mix(tsl.vec3(0.8, 0.1, 0), tsl.vec3(1, 0.8, 0),\n      tsl.sin(tsl.time.mul(2)).mul(0.5).add(0.5));\n    return m;\n  });\n  await spline.createObject('Sphere', { material: lava });     // reusable —\n  await spline.createObject('Torus',  { material: lava, position: [200,0,0] });\n  ```\n  To sample an image the SCENE ALREADY SHOWS, do NOT try to load it —\n  the namespace ships no `TextureLoader`, and a document image has no URL\n  (one embedded on a material layer is not even a shared asset). Ask the\n  scene for the live texture instead:\n  ```js\n  const src = await spline.getTexture('Wall');   // { texture, width, height, layer }\n  const mat = await spline.createCustomMaterial((three) => {\n    const m = new three.MeshStandardNodeMaterial();\n    m.colorNode = three.tsl.texture(src.texture, three.tsl.uv());\n    return m;\n  });\n  ```\n  `getTexture` takes the first texture/matcap layer, or `{ layer }` to pick\n  one by type/uuid, and returns null when the object has none. Same\n  inline-only constraint as `createCustomMaterial`.\n  Namespace: `tsl` (the full TSL function object), the NodeMaterial\n  classes (`MeshStandardNodeMaterial`, `MeshPhysicalNodeMaterial`,\n  `MeshBasicNodeMaterial`, …), and `Color`/vectors/texture classes/side\n  + blending constants. Drive live changes through `tsl.uniform(...)`\n  handles (`u.value = x` — no per-frame bridge calls) or shader-side\n  `tsl.time` (zero JS per frame). Scene lights and shadows apply; Spline's\n  environment reflections don't. WebGPU only (the default backend), and\n  INLINE HTML-content mode only: works in the editor Preview/Play and web\n  exports; SANDBOXED surfaces — `<spline-viewer>` embeds and the editor's\n  legacy in-canvas play overlay — REJECT the call (their transport cannot\n  carry functions or materials). That rejection is EXPECTED there, not a\n  bug in your code — but unhandled it lands as a Console-strip error, so\n  `try/catch` the call and fall back to a layered material (createObject's\n  normal `material` bag / `updateObject`'s material channel) so the\n  scene still reads; don't hang a scene's core look on it if the user\n  targets viewer embeds. Prefer Spline layered materials (createObject's\n  normal `material` bag / shared material names) whenever they can express\n  the look — custom shaders are for what layers can't do.\n- `spline.setMaterial(nameOrId, materialOrFactory)` — replace an EXISTING\n  mesh's material with a custom three.js material: a `createCustomMaterial`\n  result (reusable across meshes), a built-in three instance, or the factory\n  itself (`spline.setMaterial('Cube', (three) => …)`). The replacement is\n  pinned (states/transitions no longer restyle the mesh) and permanent for\n  the session (no unset); resolves to the applied material. Same constraints\n  AND rejection-handling rule as createCustomMaterial: WebGPU + inline mode\n  only — sandboxed surfaces reject it, so `try/catch` with a layered\n  fallback. For ordinary layered tweaks (color, alpha, layer props) keep\n  using `updateObject`'s `material` channel — setMaterial is for swapping\n  to shader looks the layers can't express.\n- `spline.cloneObject(nameOrId, options?)` — deep-clone an EXISTING object\n  (subtree included, any type — imported models too); resolves to the\n  clone's snapshot. Options: `name`, `parent`, `position`,\n  `rotation` (degrees), `scale`, `visible`.\n- `spline.removeObject(nameOrId)` — remove an object; resolves false when\n  none matched. In the editor preview only code-created objects can be\n  removed (file objects throw); exported scenes can remove anything.\n- `spline.setVariable(name, value)` / `spline.setVariables({...})` — set\n  file variables by NAME. ONLY for scenes that already use variables (or\n  when the user asks for them): values update live bindings immediately, in\n  edit and play mode, without touching undo history. The variable must\n  already exist (create with run_code `setVariable(...)` first). Don't\n  create variables + bindings just to wire an HTML control to an object —\n  `spline.updateObject` does that directly with no scene changes.\n- IMPORTED MODEL ANIMATIONS — a GLB/FBX's own clips (skeletal or node\n  animations; `clips:N` in the scene digest, ids/names under `clips` in\n  `get_objects`) play straight from code, no events or trigger objects:\n  - `spline.getAnimations(nameOrId?)` — resolves to\n    `[{id, name, duration, object: {id, name}}]` (`duration` in seconds):\n    the named object's clips (naming the model's group finds the animated\n    object inside it), or every clip in the scene when omitted.\n  - `spline.playAnimation(nameOrId, clip?, {loop?, pingpong?, fade?,\n    speed?})` — play one clip by name or id (the first when omitted; the\n    importer prefixes names with the file name, '<file>_<clip>', and the\n    clip's own name works too). It replaces whatever that object was\n    playing: `fade: 0.3` cross-fades over 0.3 s, else it cuts. `loop`:\n    `true` (default) forever, `false` once — then it HOLDS the last\n    frame — or a play count. Resolves to the clip; time a one-shot's\n    follow-up from its `duration / speed` (e.g. a one-shot, then back to\n    the loop).\n  - `spline.pauseAnimation(nameOrId?, clip?)` holds clips where they are\n    (`playAnimation` resumes them); `spline.stopAnimation(nameOrId?,\n    clip?)` stops them on their first frame. Omit both to pause/stop every\n    animated object.\n  Clips play where the scene runs — Play mode, the Preview tab, exports.\n  An import's own `Start` event plays the first clip a beat after the\n  document boots — remove it (`removeEvent`) when code owns the clips.\n  Don't ALSO drive the model's bones/parts with `updateObject` while a\n  clip plays — the clip owns them.\n- `spline.emitEvent(eventName, objectNameOrId)` /\n  `spline.emitEventReverse(...)` — trigger events the scene ALREADY has\n  authored ('mouseDown', 'keyDown', 'start', …). Interactions only RUN in\n  Play mode; in edit mode the call warns in the console instead. Don't\n  author new events/states just for HTML wiring.\n- `spline.requestRender()` — force a redraw (rarely needed; writes do it).\n\nEvents:\n- `spline.addEventListener('variableChange', ({name, value}) => {...})` —\n  fires in every mode when any variable changes (your writes, the Variables\n  panel, SetVariable actions...). Poll-based (~100ms).\n- `spline.addEventListener('mouseDown'|'mouseUp'|'mouseHover'|'keyDown'|\n  'keyUp'|'start'|'scroll'|'collision', (e) => {...})` — authored scene\n  events as they fire; `e.target` is `{id, name}`. Play mode only (the\n  interaction system is off while editing). This is how object-targeted\n  pointer input reaches code: author the event on the object as a SENSOR\n  (it needs no actions — the bridge relays the firing itself) and put the\n  response here (`animation` skill's triggered-phase pattern).\n- `spline.removeEventListener(name, cb)` to detach.\n\n## Patterns that work well\n\n- **HTML → 3D (default: direct)**: call `spline.updateObject` straight from\n  input handlers or requestAnimationFrame —\n  `spline.updateObject('Globe', {rotation: {y: t}})`,\n  `spline.updateObject('Card', {geometry: {cornerRadius: v}})`,\n  `spline.updateObject('Cube', {material: {color: picker.value}})`,\n  `spline.updateObject('Score', {geometry: {text: String(points)}})`. No\n  variables, no bindings, no states — the scene stays exactly as authored.\n- **Truly procedural content (ship the generator, not one output)**: a\n  \"procedural\"/\"generative\"/\"different every time\" ask means the user\n  wants objects CREATED BY CODE AT RUNTIME. Write the generation algorithm\n  here, built on `spline.createObject` / `spline.cloneObject`, and it\n  re-runs on every load, Preview, and export — a script-only document with\n  no visible UI is valid; the generator IS the app. Seed an rng (vary the\n  seed per load for fresh results, fix it for stable ones), expose the\n  knobs as consts — or wire them to UI controls with a Regenerate button\n  (`spline.removeObject` the last batch, rebuild). A run_code loop would\n  bake ONE frozen output into the tree — that's authoring, not procedural\n  behavior. The same verbs cover one-object-per-datum live data and any\n  DISTINCT generated object; repeated copies of ONE template (bullets,\n  obstacles, collectibles) are `spawnObject`'s job instead — cheaper,\n  they render through the template. Cap live counts and remove what\n  leaves play so long sessions don't accumulate\n  objects. `createObject` builds primitives/Text/Group/lights with full\n  geometry params AND raw vertex meshes ('CustomMesh' — compute\n  vertices/indices in script; runtime terrain, rocks, and organic shapes\n  are fully possible); for complex looks (imported models, tuned material\n  stacks, particles) author ONE template in the scene with run_code and\n  `cloneObject` it with per-instance variation. What stays authored with\n  run_code: the tuned DSL generators (`terrain()`/`ocean()`/`tree()`\n  algorithms aren't callable from the bridge — hand-roll the vertex math\n  or clone an authored result), decals, particles, paths/vectors.\n- **HTML → 3D (only when asked / already wired)**: if the user wants\n  variables (persistence across the file's logic, Variables panel control,\n  scene actions reading the value) or the scene already binds properties to\n  variables, drive those with `spline.setVariable` — creating them with\n  run_code `setVariable(...)` + `bindVariable(...)` first (see the\n  interactivity skill). Don't introduce this layer on your own initiative.\n- **3D → HTML**: poll object snapshots (`findObjectByName`) or — in scenes\n  that use variables — `spline.addEventListener('variableChange', ...)` to\n  mirror scene state into your UI; object click/hover reactions via the\n  spline event listeners in Play mode.\n- **Layout**: position UI with fixed/absolute CSS against the full viewport —\n  the iframe covers the canvas exactly. Keep interactive HTML compact so the\n  3D stays reachable; large decorative regions should carry\n  `data-spline-passthrough` so they don't swallow scene input.\n- **Motion & animation**: THE default home of every animation — a headless\n  rAF loop computing poses from elapsed time and writing them with\n  `updateObject` (growth, orbits, ambient life, triggered reactions). The\n  `animation` skill carries the recipes (clock, easing, stagger,\n  entrance → idle) — load it for any motion work.\n- **Games**: this layer is the DEFAULT home of a game's loop, keyboard input,\n  procedural WebAudio sound and start/HUD/game-over UI — the `games` skill\n  builds on the API above (load it for the game shell, chase-camera,\n  movement and sound recipes).\n- **Anything driven per-frame must move SMOOTHLY** — the `games` skill's\n  smooth-motion rules apply to every interactive scene, not just games:\n  integrate with a fixed timestep (never the raw frame delta) and write\n  low-passed values; never bolt a camera to a moving object\n  (`position = target − offset` buzzes — separate updateObject writes can\n  land a frame apart; ease toward the target and aim AT it); throttle\n  continuously-updating DOM text to ~10 Hz.\n- Debug with `console.log` — the user sees it in the Console strip.\n\n## Rules\n\n- DEFAULT TO DIRECT CONTROL: wire HTML to the scene with\n  `spline.updateObject` (and reads). Do NOT create variables, bindings,\n  states, or events just to connect an HTML control — that adds scene\n  complexity the user didn't ask for. Reach for variables/events only when\n  the user requests them, the scene already uses them, or something outside\n  the HTML (actions, states, exports reading variables) must see the value.\n  (The one authored-event exception: an action-less SENSOR event that\n  bridges object clicks/hovers into code — see the listener note above.)\n- ONE document per file. `set_html_content` creates or fully rewrites it;\n  to CHANGE an existing document, `get_html_content` to read the current\n  source, then use `edit_html_content` with ONE `edits` batch carrying\n  every change you can already spell out (exact `old_str` → `new_str`\n  find-and-replace each, `replace_all` for repeated strings) instead of\n  resending the whole document — or one call per edit.\n- Empty string to `set_html_content` removes the HTML content entirely.\n- Don't duplicate the 3D scene in HTML (no WebGL canvases of your own) — the\n  scene is behind you; you're the UI layer.\n- If you do use variables, they must exist in the file: create/verify them\n  with run_code in the same response before shipping HTML that uses them.\n- `updateObject` is transient (runtime state, not saved). That's right for\n  interactive controls; if the user wants a control that permanently edits\n  the file, say so and apply the final value with run_code instead.\n- Test-drive interactions mentally against edit mode (updateObject +\n  variables work; authored events need Play) and say so to the user when a\n  piece of the HTML app only comes alive in Play mode.\n\n## Minimal example (direct control — no variables)\n\n```html\n<!doctype html>\n<html>\n<head>\n<style>\n  /* page background stays transparent */\n  .hud { position: fixed; top: 16px; left: 16px; padding: 12px 16px;\n         background: rgba(20,20,24,.85); color: #fff; border-radius: 12px;\n         font: 13px/1.4 system-ui; }\n  input[type=range] { width: 160px; }\n</style>\n</head>\n<body>\n  <div class=\"hud\">\n    <div>Spin: <span id=\"sv\">20</span>°/s &nbsp; Rounding: <span id=\"rv\">0</span></div>\n    <input id=\"s\" type=\"range\" min=\"0\" max=\"180\" step=\"1\" value=\"20\">\n    <input id=\"r\" type=\"range\" min=\"0\" max=\"45\" step=\"1\" value=\"0\">\n  </div>\n  <script>\n    (async () => {\n      await spline.ready;\n      const s = document.getElementById('s'), r = document.getElementById('r');\n      // Continuous motion: drive the transform directly every frame.\n      let angle = 0, last = performance.now();\n      (function tick(now) {\n        angle += ((now - last) / 1000) * Number(s.value); last = now;\n        spline.updateObject('Cube', { rotation: { y: angle } });\n        requestAnimationFrame(tick);\n      })(last);\n      s.addEventListener('input', () => { sv.textContent = s.value; });\n      // Shape control: geometry parameters regenerate the mesh live.\n      r.addEventListener('input', () => {\n        rv.textContent = r.value;\n        spline.updateObject('Cube', { geometry: { cornerRadius: Number(r.value) } });\n      });\n    })();\n  </script>\n</body>\n</html>\n```\n"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}