From lll-animation
This skill should be used when the user is working on rolling cube animation, edge-pivoting, isometric cube movement, transform-origin animation, Object3D reparenting for rotation, grid-based cube wandering, window boundary enforcement, direction selection logic, cursor following, or weighted random direction bias — even if they don't explicitly mention "rolling cube." Covers both CSS transform-origin and Three.js Object3D pivot approaches, GSAP animation chaining, cumulative rotation tracking, grid snapping, boundary checking, and cursor-biased direction selection.
How this skill is triggered — by the user, by Claude, or both
Slash command
/lll-animation:isometric-rolling-cubeThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
A cube "rolls" by tipping 90 degrees over one of its bottom edges, translating one grid cell in that direction. This is fundamentally different from rotating around the centre — the pivot point must be at the contact edge, not the object's origin.
A cube "rolls" by tipping 90 degrees over one of its bottom edges, translating one grid cell in that direction. This is fundamentally different from rotating around the centre — the pivot point must be at the contact edge, not the object's origin.
This skill covers the technique for both CSS (Phase 1) and Three.js (Phase 2) implementations, with GSAP driving the animation in both cases.
When invoked without a specific topic, prompt for context:
Which aspect of rolling animation are you working on?
Topic Covers Rolling / pivot CSS transform-origin or Three.js Object3D pivot technique Directions The 4 isometric roll directions and data tables Boundaries Window boundary enforcement and direction filtering Cursor Cursor-following bias and weighted random selection Snapping Grid snapping and floating-point drift prevention Reparenting Three.js attach()vsadd()and the pivot cycleOr describe what you need help with.
When a specific topic is identified, jump directly to the relevant section. For CSS cube construction (faces, preserve-3d, isometric projection), delegate to the phase1-css-cube skill. For GSAP API specifics, delegate to the gsap-expert skill.
In isometric view, the cube moves diagonally on screen. Each screen direction maps to a world-axis movement:
| Screen Direction | World Direction (XZ) | Grid Delta |
|---|---|---|
| Top-Right | +X | (1, 0) |
| Bottom-Right | +Z | (0, 1) |
| Bottom-Left | -X | (-1, 0) |
| Top-Left | -Z | (0, -1) |
For a unit cube (side length S), each direction has a corresponding rotation axis, pivot offset, and translation vector.
For a cube centred at (gx, S/2, gz):
| Direction | Translation | Pivot Offset from Centre | Rotation Axis | Angle |
|---|---|---|---|---|
| +X | (S, 0, 0) | (+S/2, -S/2, 0) | (0, 0, -1) | PI/2 |
| -X | (-S, 0, 0) | (-S/2, -S/2, 0) | (0, 0, +1) | PI/2 |
| +Z | (0, 0, S) | (0, -S/2, +S/2) | (+1, 0, 0) | PI/2 |
| -Z | (0, 0, -S) | (0, -S/2, -S/2) | (-1, 0, 0) | PI/2 |
The rotation axis can be derived: cross(direction, downVector) where downVector = (0, -1, 0).
The pivot offset is always: half a cube-width toward the roll direction on the ground axis, plus half a cube-width downward (to the bottom edge).
Full transform-origin values and translation vectors for all 4 directions: see references/direction-data.md.
Key principle: transform-origin must be a 3D value (three components) targeting the specific bottom edge for each direction. A 2D value places the pivot at Z=0, which is wrong for the Bottom-Right and Top-Left directions (those need a non-zero Z component).
Each roll is a simple 90-degree rotation animated with GSAP. The key is the reset step after each roll — this keeps each animation starting from a clean state.
For each roll:
After a roll completes, the cube has rotated 90 degrees. Absorb this into the base state:
left/top or translate)Without this reset, each successive roll compounds on the previous rotation and the animation breaks after 2-3 rolls. The reset is what makes the entire approach work — each roll starts from a clean, predictable state.
Because each roll resets to a clean state, no complex rotation tracking is needed. The logical grid position (gridX, gridY) is the only state to maintain between rolls — update it in the onComplete callback after each reset.
Three.js has no transform-origin equivalent. Instead, create a temporary pivot Object3D at the rolling edge and make the cube a child of it. Rotating the pivot then rotates the cube around that edge.
The full cycle for one roll:
Step 1 — Create pivot at the rolling edge:
Position a new Object3D at the cube's bottom edge in the roll direction.
Step 2 — Attach cube to pivot:
Use pivot.attach(cube) — this preserves the cube's world-space position while making it a child of the pivot. The cube does not visually move.
Step 3 — Animate pivot rotation: Use GSAP to rotate the pivot 90 degrees around the appropriate axis. The cube, as a child, orbits around the pivot point.
Step 4 — Detach cube from pivot:
Use scene.attach(cube) — this moves the cube back to the scene's children while preserving its current world position and rotation.
Step 5 — Clean up: Remove the pivot from the scene. Snap the cube's position to exact grid coordinates.
attach() Not add()This is the single most important API distinction in the entire implementation.
Object3D.add(child) — reparents the child but keeps its local transform unchanged. Since the local transform is now interpreted relative to the new parent, the child's world position changes. The cube would visually jump.Object3D.attach(child) — reparents the child and recalculates its local transform so that its world position stays the same. Internally it computes: newLocal = inverse(newParent.worldMatrix) * oldParent.worldMatrix * oldLocal.Use attach() for both directions: cube-to-pivot and pivot-back-to-scene.
After each roll's reparenting cleanup, reset the cube's rotation to clean values. Since each roll is exactly 90 degrees, all rotation components should be exact multiples of PI/2.
Recommended approach: Reset Euler angles to clean values after each roll in the onComplete callback. Round each rotation component to the nearest multiple of PI/2. This keeps every roll starting from a predictable state — no cumulative drift, no gimbal lock issues.
Optional advanced approach: Use quaternions (Quaternion.premultiply()) to track cumulative rotation if you need precise face-orientation tracking (e.g., knowing which face is on top). This is not needed for the workshop's rolling behaviour.
Each roll is an independent animation. Since directions are chosen randomly, you cannot pre-build a long timeline. Instead, chain rolls dynamically:
onComplete callback, clean up the pivot/transform state, then call the roll function again (possibly after a delay)Use gsap.delayedCall(pauseDuration, rollNext) to insert pauses between rolls — this is cleaner than adding empty delays to timelines.
Target the sub-object, not the mesh itself:
gsap.to(mesh.position, { x: ..., z: ... })gsap.to(pivot.rotation, { z: angle })Do NOT use dot-path syntax on the mesh: gsap.to(mesh, { "rotation.x": angle }) — this does not work with Three.js objects.
"power2.inOut" gives a natural roll-and-settle feel"power1.out" (GSAP default) also works well for a lighter landingAdd a natural bounce at each landing with yoyo: true:
// Runs alongside the roll animation — plays up then automatically reverses
gsap.to(positionContainer, {
y: -jumpHeight,
duration: rollDuration / 2,
yoyo: true,
repeat: 1,
ease: "power2.out"
})
yoyo: true with repeat: 1 plays the tween forward then automatically reverses — the position container rises then falls back, producing the characteristic up-then-down bounce at each landing.
GSAP tweens values but does not trigger Three.js renders. Three options:
requestAnimationFrame that calls renderer.render() every frame. Simple, always works. (spec preferred — the boilerplate provides this)renderer.render() inside each tween's onUpdate. Renders only during animation but misses static scene updates.gsap.ticker.add(() => renderer.render(scene, camera)). Synchronises rendering with GSAP's update cycle. Good middle ground.Floating-point arithmetic causes gradual drift after many rolls. After each roll completes (in the onComplete callback), snap the cube's position to exact grid coordinates:
gsap.set(mesh.position, { x: Math.round(mesh.position.x), z: Math.round(mesh.position.z) }) — using gsap.set() rather than direct assignment keeps GSAP's internal state consistentgridX * cellSize, gridY * cellSize and clear any transform residueTrack position as integer grid coordinates (gridX, gridY) alongside the rendered position. Update the grid coordinates after each roll, and snap the rendered position to match.
Before each roll, filter valid directions against grid bounds. See references/boundary-algorithm.md for the full direction filtering algorithm, CSS viewport-pixel bounds approach, and Three.js grid-coordinate bounds approach.
Weighted direction bias toward cursor position. See references/boundary-algorithm.md for the full bias algorithm, CSS screen-space approach, and Three.js Vector3.unproject() conversion for world-space cursor coordinates.
| Pitfall | Symptom | Fix |
|---|---|---|
| Pivot at centre, not edge | Cube spins in place | Position pivot at bottom edge using offset formula |
Using add() instead of attach() | Cube jumps when reparented | Always use attach() for both directions |
| Euler angle accumulation | Unexpected rotation after 3-4 rolls | Reset rotation to clean multiples of PI/2 after each roll |
| No grid snap | Cube drifts off grid over time | gsap.set() to round position in onComplete |
| Pre-built timeline for random directions | Can't randomise; timeline is fixed | Use onComplete chaining, one roll per timeline |
GSAP directional shortcuts (_cw, _short) | Ignored or breaks on Three.js objects | These only work on DOM targets — use raw angle values |
| No render loop | Scene doesn't update visually | Add gsap.ticker.add(() => renderer.render(...)) |
| Modifying boilerplate camera/scene | Breaks grid alignment | The starter repo's camera and grid are pre-configured — don't touch them |
phase1-css-cube — CSS cube construction (Desandro 6-div approach, face transforms, preserve-3d, isometric projection)workshop-guide — Task progression, acceptance criteria, phase transitionsgsap-expert — Full GSAP API reference, timeline patterns, ScrollTriggerthreejs-fundamentals — Scene setup, Object3D hierarchy, coordinate system, math utilitiesnpx claudepluginhub simon-tanna/lll-animation-plugin --plugin lll-animationCreates 3D scenes, interactive experiences, and visual effects using Three.js. Handles WebGL rendering, animations, and 3D visualizations.
Creates CSS/JS-based 3D animations including perspective transforms, flip cards, cube rotations, parallax depth, and tilt-on-hover effects without WebGL. For React/TSX components.
Builds interactive 3D web scenes with Three.js using WebGL/WebGPU. Guides on scenes, cameras, renderers, geometries, materials, meshes, lights, animations, and OrbitControls.