← 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": "The built-in Events & Actions machinery — for EDITING interactivity a scene already has, for features only events provide (Scroll/Collision/Trigger/DragDrop/LookAt/Follow, variables + property bindings), for sensor events bridging clicks/hovers into code, and for explicit user asks. NEW interactive behavior is code-driven by default (the `animation` skill + the spline bridge). Covers event/action types, creating and editing them (updateEvent/updateAction/updateTween/remove*), states, variables (basic + dynamic), and inspecting existing interactivity via get_objects.",
"included_files": [],
"name": "spline-3d-interactivity",
"skill_md_contents": "---\nname: spline-3d-interactivity\ndescription: \"The built-in Events & Actions machinery — for EDITING interactivity a scene already has, for features only events provide (Scroll/Collision/Trigger/DragDrop/LookAt/Follow, variables + property bindings), for sensor events bridging clicks/hovers into code, and for explicit user asks. NEW interactive behavior is code-driven by default (the `animation` skill + the spline bridge). Covers event/action types, creating and editing them (updateEvent/updateAction/updateTween/remove*), states, variables (basic + dynamic), and inspecting existing interactivity via get_objects.\"\n---\n\n# Interactivity (Events & Actions)\n\nNEW interactive BEHAVIOR is code-driven by default — logic and motion live\nin the HTML layer, driving objects through the `spline` bridge (see\n# Motion & interactivity and the `animation` skill; games have their own\nskill). This machinery is what you load when:\n\n- the user EXPLICITLY asks for events, actions, states, or variables;\n- you are EDITING interactivity a scene ALREADY has (fixes and small\n additions stay in kind — never migrate working content to code unasked);\n- a feature only events provide is genuinely needed: Scroll-driven\n narratives, physics `Collision`/`Trigger` reactions, `DragDrop`,\n `LookAt`/`Follow` trackers, `API`/`Webhook`, variables that other scene\n machinery must read;\n- you need a SENSOR event: object-targeted pointer input (click/hover on a\n specific mesh) authored on the object — with no actions — so code can\n react via `spline.addEventListener('mouseDown' | 'mouseHover', …)` in\n Play mode; the response stays in the document.\n\nAn object reacts to the world through **events**. Each event holds one or more\n**actions** that run when the event fires. The most common action is a\n`Transition` (tween to a state), but there are many more.\n\nEverything is wired on the currently selected object, so `select(...)` (or\ncreate) the object first.\n\n## Inspect before you edit\n\n`get_scene` only shows per-object COUNTS of states/events. To edit existing\ninteractivity you need real ids: call `get_objects([objectId])` — it returns\neach object's states (`{id, name, selected}`), events (`{id, type, key/steps/\ntarget/…, containers}`), every action (`{id, type, url/variableId/object/\nrunMode/…}`) and every Transition tween (`{id, state, duration, delay, easing,\nrepeat, direction}`). Then select the object and update by id.\n\n## Event types (`INTERACTION_EVENT` string values)\nPass these strings to `addEvent(type)` or to `animate`'s `trigger`:\n\n- `'Start'` — fires once when the scene loads (default).\n- `'MouseDown'`, `'MouseUp'`, `'MousePress'` — pointer on the object.\n- `'MouseHover'` — pointer enters/leaves the object.\n- `'KeyDown'`, `'KeyUp'`, `'KeyPress'` — keyboard.\n- `'Scroll'` — scroll-driven.\n- `'Collision'` — physics collision (needs physics on — see the `physics`\n skill for the simulation itself, `game-controls` for gameplay wiring).\n- `'Trigger'` — object enters a trigger zone.\n- `'LookAt'`, `'Follow'` — object tracks cursor/target.\n- `'DragDrop'` — draggable object.\n- `'GameControl'` — WASD / third-person controller (special-cased by the\n alias). OPT-IN: add one ONLY when the user explicitly asks for the built-in\n Game Controls feature — then load `game-controls` (key mapping, physics\n pairing, collision/trigger gameplay). Building a game otherwise? The\n DEFAULT is a JS game loop + HTML UI — load the `games` skill.\n- `'Conditional'` — fires while a condition (distance / state / comparison) holds.\n- `'Resize'` — responsive breakpoints.\n- `'VariableChange'` — a variable changed.\n- `'API'`, `'WEBHOOK'` — external triggers.\n- `'AIAssistantTrigger'` — the AI voice assistant matched this event's\n `description`; `'AIAssistantListener'` — tracks the assistant's audio\n spectrum (takes ONLY `'SetVariable'` actions).\n\nThe UI/docs use different names for some of these DSL strings: Trigger Area =\n`Trigger`, Game Controls = `GameControl`, Distance / State Change =\n`Conditional`.\n\n`addEvent` returns the new event id — capture it for the action call.\n\n### Per-event props (set with `updateEvent(eventId, props)`)\n- Key events: `key` — a KeyboardEvent.key value; friendly `'Space'` works,\n single letters are lowercased. E.g. `updateEvent(e, { key: 'ArrowLeft' });`\n- Mouse events: `mode: 'Object' | 'Canvas' | 'Window'`.\n- Scroll: `steps`, `trigger: 'load'|'scroll'`, `startFrom: 'enter'|'page'`,\n `enterAnchor: 'top'|'center'|'bottom'`, `startOffset`, `endAfter`.\n- Collision: `target: 'character' | 'scene'`.\n- Trigger: `triggerZone: 'box'|'sphere'`, `size`, `position`, `radius`\n (rotation is given in degrees); `target: 'all' | 'custom'` +\n `triggeringObjects: [id, …]` restrict what sets it off.\n- AIAssistantTrigger: `description` — a natural-language description of when\n the assistant should fire it.\n- LookAt / Follow: `target` (object id or `'cursor'`), damping/limit props.\n- VariableChange: `variableId`, `objectId`, `property` (e.g. 'Position X').\n- Conditional: `condition` (Distance / State / Comparison object).\n- Any event: `name`, `enabled: false` (disable without deleting).\n- `updateEvent` cannot change an event's TYPE — `removeEvent` + `addEvent`.\n\n## Action types (`ActionStateType` string values)\nPass to `addAction(eventId, actionType, container?)` (the event's type is\nresolved automatically; the legacy `addAction(eventId, eventType, actionType)`\nform also still works):\n\n- `'Transition'` — tween the object to one of its states (the workhorse). Comes\n pre-seeded with two tweens (base + latest state); tune with `updateTween`.\n- `'Animation'` — play a baked timeline/skeletal animation clip (only set its\n `object`/`clipId` when the object actually has imported animations:\n `clips:N` in the digest marks that object, and `get_objects` lists its\n clips' ids under `clips` — give every clip its own action, never reuse one\n clipId for several). Code that switches clips itself (UI buttons, a game\n loop) needs no actions at all: `spline.playAnimation` (`animation`\n skill).\n Accepts `updateAction(e, a, { interaction: 'play' | 'pause' | 'stop' })`:\n pause holds the clip where it is (the next play resumes it), stop rewinds\n it to its first frame. A pause/stop action targets `object` (omit for\n every animated object) and optionally one `clipId` of it.\n- `'TimelineAnimation'` — play a shared timeline animation.\n- `'Link'` — open a URL: `updateAction(e, a, { url: 'https://…',\n context: 'tab' | 'window' })`.\n- `'SwitchCamera'` — cut/animate to another camera\n (`{ targetCamera: cameraId }` — NOT `camera`; plus `animate: false` for a\n hard cut, `duration`, `delay`).\n- `'SceneTransition'` — go to another page (`{ target: pageId }`).\n- `'Create'` — instantiate an object/component (`{ object: objectId }`).\n- `'Destroy'` — remove objects (`{ objects: [id, …] }`).\n- `'Reset'` — Reset Scene: restart the whole runtime scene after its delay.\n It is not a selective object reset or checkpoint teleport.\n- `'SetVariable'` — write a variable: `updateAction(e, a, { variableId,\n expression: 'clicks + 1' })` (see \"Expressions\" below).\n- `'DynamicVariablePlay'` — play/pause a DYNAMIC variable (Counter/Timer/…;\n \"Variable Control\" in the UI): `updateAction(e, a, { variableId, mode:\n 'Play' | 'Pause' | 'Stop' | 'PlayPause' | 'Restart' | 'Toggle' })`. It does\n not move objects by itself — motion comes from binding that variable to a\n property or reacting to it with a `VariableChange` event.\n- `'Audio'`, `'Video'` — media. Accept\n `updateAction(e, a, { interaction: 'play' | 'pause' | 'stop' })`.\n- `'Particles'` — control a particle emitter: `updateAction(e, a,\n { emitterObjectId: '<emitter id>', emission: 'start' | 'stop' | 'pause' |\n 'reset' | 'emitToggle' | 'pauseToggle', delay: 0 })` (delay in ms).\n Emitter setup + recipes live in the `particles` skill.\n- `'Conditional'` — branch into if/else sub-actions.\n- `'ClearLocalStorage'`, `'UserAPI'` — utility / external.\n\nUI/docs names → DSL strings: Sound = `Audio`, Particles Control =\n`Particles`, Variable Control = `DynamicVariablePlay`, Open Link = `Link`,\nAPI Request = `UserAPI`.\n\n### Which actions are allowed where\n`Transition`/`Create`/`SetVariable`/`Conditional` are allowed almost\neverywhere. Notable exceptions: `'LookAt'` takes NO actions; `'Follow'`\nonly `'Create'`; `'AIAssistantListener'` only `'SetVariable'`;\n`'Trigger'` disallows `'Animation'`/`'TimelineAnimation'`;\n`'MouseHover'`/`'Scroll'`/`'DragDrop'` disallow `'Link'`;\n`'MousePress'`/`'KeyPress'` disallow `'Destroy'`;\n`'GameControl'` only Transition/Animation/TimelineAnimation/Audio/Particles/\nCreate/Conditional. An invalid combo throws an error naming the allowed list —\nread it and pick another action or event.\n\n### Action containers\nMost events keep actions in a single list — no container argument needed.\nExceptions (pass as `addAction`'s 3rd argument): `'Conditional'` events have\n`'in'` (condition becomes true, the default) and `'out'` (becomes false);\n`'DragDrop'` has `'drag'` (default) and `'drop'`; `'API'` has `'success'`\n(default) and `'error'`; `'GameControl'` has\n`'idle'`/`'move'`/`'jump'`/`'run'` (default `'move'` — see the\n`game-controls` skill). Edits and deletes by\nactionId find the container automatically. (Adding to a specific Resize\nbreakpoint isn't supported — its existing actions are still editable by id.)\n\nA `'Conditional'` ACTION has nested branches. Create and configure the gate,\nthen add children explicitly:\n```\nconst gate = addAction(eventId, 'Conditional');\nupdateAction(eventId, gate.actionId, { condition: 'score >= 3' });\nconst win = addAction(eventId, 'Transition', {\n parentActionId: gate.actionId,\n branch: 'if',\n});\n```\n`if` runs when true and `else` when false. Nested Conditional creation is\nunsupported; nested child edits/deletes are located automatically by action id.\n\n## Wiring \"on click, transition to a state\"\nThe cleanest way is `animate` with a `trigger`:\n```\nmesh('Cube', { width: 160, height: 160, depth: 160 });\nanimate(\n { rotation: { y: 90 }, color: '#22d3ee' },\n { trigger: 'MouseDown', duration: 500, easing: 'ease-in-out' }\n);\n```\n\nThe equivalent low-level flow (when you need more control):\n```\naddState('Rotated'); // new target state, now active\nrotation({ y: 90 }); // its look\ncolor('#22d3ee');\nconst e = addEvent('MouseDown'); // event id\nconst a = addAction(e, 'Transition');\nupdateTween(e, a.actionId, a.tweenId, { duration: 500, easing: 'ease-in-out' });\n```\n\n## Editing an EXISTING interaction (worked example)\n\"Make the hover animation slower\":\n1. Find the object's id in the message's `<editor-context>` snapshot (or via\n `get_scene()` if the snapshot was omitted); `get_objects([id])` → find the\n MouseHover event, its Transition action and tween ids.\n2. ```\n select(o => o.name === 'Button');\n updateTween('<eventId>', '<actionId>', '<tweenId>', { duration: 600, easing: 'ease-out' });\n ```\n3. `play()` + `take_screenshot()` to verify.\n\nDeleting: `removeTween(e, a, t)`, `removeAction(e, a)`, `removeEvent(e)`,\n`removeState('Hovered')`. A wrong id is a silent no-op — verify with\n`get_objects` afterwards if unsure.\n\n## Tweens (Transition actions)\nA Transition's tweens run in order; each tween's `state` is a state **id** of\nthe action's target object (`null` = Base State). `updateAction(e, a,\n{ object: otherId })` retargets the action and re-points its tweens at that\nobject's latest state. Add/remove steps with\n`addTween(e, a, { duration: 300, easing: 'ease-out' })` → tweenId and\n`removeTween(e, a, t)`. Tween props: `duration`(ms), `delay`(ms), `easing`\n('linear'|'ease'|'ease-in'|'ease-out'|'ease-in-out'|'cubic'|'spring' + spring\n`mass`/`stiffness`/`damping`), `loop: true`, `direction: 'alternate'`.\n\n## Hover example\n```\nanimate(\n { scale: { x: 1.15, y: 1.15, z: 1.15 } },\n { trigger: 'MouseHover', duration: 200, easing: 'ease-out' }\n);\n```\nOn hover/press/drag events the Transition action defaults to `runMode: 'Toggle'`\n(state on enter, back on leave), which is usually what you want.\n\n## Multiple actions on one event\n`addAction` adds another action to an existing event; call it again with the\nsame `eventId` to stack actions (e.g. a Transition plus a SetVariable). Each\nreturns its own `actionId`.\n\n## States\n`addState(name?)` creates a new state (and selects it, so following transform/\ncolor calls define its look). `selectState(nameOrId)` re-activates an existing\nstate to restyle it; `selectState(null)` returns to the Base State — do this\nbefore making live (non-state) edits. `renameState` / `removeState` accept a\nname or id.\n\n## Variables\nVariables are scene-level values; `get_scene()` lists them with ids (dynamic\nones carry a `kind`).\n- `setVariable('score', 0)` — create-or-update a BASIC variable (number,\n string, or boolean); returns its id.\n- `dynamicVariable(name, type, props?)` — create-or-update a SELF-UPDATING\n variable; returns its id. Types + their settings:\n - `'counter'`: `value`, `increment` (per step), `updateInterval` (ms per\n step), `hasEnd` + `endValue`, `repeat` (restart at end), `autoStart`.\n E.g. `dynamicVariable('spin', 'counter', { updateInterval: 16,\n increment: 1, hasEnd: false })` ≈ a value rising ~60/s.\n - `'random'`: `min`, `max`, `decimals`, `updateInterval`, `isStatic`\n (one value instead of re-rolling), `autoStart`.\n - `'time'` (clock): `format` ('HH:mm:ss'|'HH:mm'|'HH'|'mm'|'ss'|'year'|\n 'month'|'dayOfYear'|'dayOfMonth'|'secondOfDay'), `format12h24h`\n ('12'|'12ampm'|'24'), `timeZone` (IANA name or null = local).\n - `'timer'` (counts down) / `'stopwatch'` (counts up): `value`,\n `endValue`, `hasEnd`, `repeat`, `format` ('HH:mm:ss'|'mm:ss'|'number'|\n 'number3decimal'). With the default 'number' format value/endValue are\n plain seconds; the clock formats store them as `{ textValue: [h, m, s] }`\n / `{ textValue: [m, s] }` arrays — set `format` in the SAME call and the\n shapes are seeded for you, then your explicit value/endValue override.\n - All types also accept `persistent: true` (value survives reloads).\n- `renameVariable(nameOrId, newName)`, `removeVariable(nameOrId)`. Removing\n does NOT clean up references — actions/bindings that pointed at it read as\n 0/empty.\nTo change a variable in response to an event, add a `'SetVariable'` action and\ngive it the target and the new value's expression:\n`updateAction(e, a, { variableId, expression: 'clicks + 1' })`. To react to\none changing, use a `'VariableChange'` event (`updateEvent(e, { variableId })`).\nStart/stop a dynamic variable from an event with a `DynamicVariablePlay` action.\n\n## Expressions\nA SetVariable's `expression` (the \"To\" field) and a Conditional action's\n`condition` (the \"If\" field) take a source string:\n- Variables by NAME (`score`, or `{player hp}` / `{<id>}` in braces for\n names with symbols, or to force a name that is also a keyword); numbers;\n `+ - * / % ^ ( )`; comparisons `== != > < >= <=`; `and` / `or` / `not`\n (or `&& || !`); `true` / `false`; functions `sin cos tan asin acos atan\n atan2 sqrt abs log exp floor ceil round min max random` (`random(1, 6)`).\n- The expression is only the NEW VALUE: `'score + 1'`, never\n `'score = score + 1'` (a lone `=` is an error — compare with `==`).\n- It is evaluated as the target variable's type. For a text variable only\n quoted literals, variables and `+` apply: `'\"Score: \" + score'`. For a\n boolean one: `'not isOpen'`, `'lives > 0'`. Conditions are boolean;\n compare text with a quoted literal: `'mode == \"hard\"'`.\n- Set `variableId` in the same (or an earlier) call — the target's type\n decides how the expression is checked. Names resolve when the call applies,\n so variables created earlier in the same run_code block are fine. Unknown\n names or expressions that don't evaluate throw.\n- A raw token array (`[{ id: scoreId }, { name: '+' }, { name: '1' }]`) is\n still accepted. `get_objects` reads back `expression` / `condition` as\n tokens: variables are `{ id, name }`, everything else `{ name }`.\n\n## Binding variables to properties\n`bindVariable(property, variableNameOrId)` makes a property of the SELECTED\nobject(s) follow a variable's live value — continuous, data-driven motion\nwithin the variables system (for plain continuous motion the default is the\ncode loop — `animation` skill; reach for bindings when variables are already\nin play or other scene machinery must read the value). Bindable properties:\n`'position.x/y/z'`, `'rotation.x/y/z'`, `'scale.x/y/z'`, `'text'` (Text\nobjects display the value — scores, countdowns, clocks), and ANY numeric\nparametric-geometry parameter by its schema name — `'width'`/`'height'`/\n`'depth'`, `'cornerRadius'`, `'cornerSides'`, `'radius'`, `'sides'`,\n`'segments'`, `'innerRadius'`, `'tube'`, `'arc'`, `'hollow'`, … (the mesh\nregenerates live as the variable changes; Vector/Path geometries are\nexcluded — their shape derives from points). Numeric properties need a\nnumber-valued variable; `'text'` takes any. `unbindVariable(property)`\nfreezes the property back to the variable's current value. `get_objects`\nreports each object's `bindings` (property → variable id).\n```\n// A cube that spins forever + a live countdown readout:\nconst spin = dynamicVariable('spin', 'counter', {\n updateInterval: 16, increment: 2, hasEnd: false,\n});\nconst left = dynamicVariable('timeLeft', 'timer', { value: 30, endValue: 0 });\nselect('Cube');\nbindVariable('rotation.y', 'spin');\nselect('CountdownText');\nbindVariable('text', 'timeLeft');\n```\n\n## Verifying interactions\nStatic screenshots won't show interaction. Call `play()` to enter play mode so\nevents fire, then `take_screenshot()`. Remember `play()` is not undoable.\n"
}SHA-256 of public snapshot: 870ce0d109fb839222ae2a91429fdf7f8340024fedf06a7c69124e9d198b5fb2