generateAnimationCode
Create animation code for 3D objects in Spline scenes to rotate, move, scale, or change colors with customizable duration, easing, and looping options.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| sceneId | Yes | Scene ID | |
| objectId | Yes | Object ID | |
| animationType | Yes | Animation type | |
| duration | No | Animation duration (ms) | |
| easing | No | Animation easing | easeInOut |
| loop | No | Whether to loop the animation | |
| params | No | Animation-specific parameters |
Implementation Reference
- src/tools/runtime-tools.js:73-122 (registration)Registration of the 'generateAnimationCode' tool including schema definition and handler function that delegates to runtimeManagerserver.tool( 'generateAnimationCode', { sceneId: z.string().min(1).describe('Scene ID'), objectId: z.string().min(1).describe('Object ID'), animationType: z.enum(['rotate', 'move', 'scale', 'color']).describe('Animation type'), duration: z.number().int().min(100).default(1000).describe('Animation duration (ms)'), easing: z.enum(['linear', 'easeIn', 'easeOut', 'easeInOut']).default('easeInOut') .describe('Animation easing'), loop: z.boolean().default(false).describe('Whether to loop the animation'), params: z.record(z.any()).optional().describe('Animation-specific parameters'), }, async ({ sceneId, objectId, animationType, duration, easing, loop, params }) => { try { const animationParams = { animationType, duration, easing, loop, ...(params && params), }; const code = runtimeManager.generateObjectInteractionCode( sceneId, objectId, 'animation', animationParams ); return { content: [ { type: 'text', text: code } ] }; } catch (error) { return { content: [ { type: 'text', text: `Error generating animation code: ${error.message}` } ], isError: true }; } } );
- src/tools/runtime-tools.js:85-121 (handler)The MCP tool handler that processes inputs and calls runtimeManager.generateObjectInteractionCode to produce animation codeasync ({ sceneId, objectId, animationType, duration, easing, loop, params }) => { try { const animationParams = { animationType, duration, easing, loop, ...(params && params), }; const code = runtimeManager.generateObjectInteractionCode( sceneId, objectId, 'animation', animationParams ); return { content: [ { type: 'text', text: code } ] }; } catch (error) { return { content: [ { type: 'text', text: `Error generating animation code: ${error.message}` } ], isError: true }; } }
- src/tools/runtime-tools.js:76-84 (schema)Zod schema defining input parameters for the generateAnimationCode toolsceneId: z.string().min(1).describe('Scene ID'), objectId: z.string().min(1).describe('Object ID'), animationType: z.enum(['rotate', 'move', 'scale', 'color']).describe('Animation type'), duration: z.number().int().min(100).default(1000).describe('Animation duration (ms)'), easing: z.enum(['linear', 'easeIn', 'easeOut', 'easeInOut']).default('easeInOut') .describe('Animation easing'), loop: z.boolean().default(false).describe('Whether to loop the animation'), params: z.record(z.any()).optional().describe('Animation-specific parameters'), },
- src/utils/runtime-manager.js:453-547 (helper)Core helper function in RuntimeManager that generates the specific animation code based on animationType, duration, easing, and loop parameterscase 'animation': const { animationType = 'rotate', duration = 1000, easing = 'easeInOut', loop = false } = params; let animationCode = ''; if (animationType === 'rotate') { animationCode = ` // Rotate animation const startRotation = { ...obj.rotation }; const targetRotation = { x: startRotation.x + Math.PI * 2, // Full 360° rotation y: startRotation.y, z: startRotation.z }; obj.rotation.x = startRotation.x + (targetRotation.x - startRotation.x) * progress;`; } else if (animationType === 'move') { animationCode = ` // Move animation const startPosition = { ...obj.position }; const targetPosition = { x: startPosition.x, y: startPosition.y + 1, // Move up by 1 unit z: startPosition.z }; obj.position.x = startPosition.x + (targetPosition.x - startPosition.x) * progress; obj.position.y = startPosition.y + (targetPosition.y - startPosition.y) * progress; obj.position.z = startPosition.z + (targetPosition.z - startPosition.z) * progress;`; } else if (animationType === 'scale') { animationCode = ` // Scale animation const startScale = { ...obj.scale }; const targetScale = { x: startScale.x * 1.5, // Scale up by 50% y: startScale.y * 1.5, z: startScale.z * 1.5 }; obj.scale.x = startScale.x + (targetScale.x - startScale.x) * progress; obj.scale.y = startScale.y + (targetScale.y - startScale.y) * progress; obj.scale.z = startScale.z + (targetScale.z - startScale.z) * progress;`; } let easingFunction = ''; if (easing === 'linear') { easingFunction = 'const ease = progress;'; } else if (easing === 'easeIn') { easingFunction = 'const ease = progress * progress;'; } else if (easing === 'easeOut') { easingFunction = 'const ease = 1 - Math.pow(1 - progress, 2);'; } else { // easeInOut easingFunction = 'const ease = progress < 0.5 ? 2 * progress * progress : 1 - Math.pow(-2 * progress + 2, 2) / 2;'; } codeSnippet = ` // Animate object const obj = spline.findObjectById('${objectId}'); let startTime = null; const duration = ${duration}; // milliseconds let animationFrame; function animate(timestamp) { if (!startTime) startTime = timestamp; const elapsed = timestamp - startTime; let progress = Math.min(elapsed / duration, 1); // Apply easing ${easingFunction} progress = ease; ${animationCode} if (progress < 1) { animationFrame = requestAnimationFrame(animate); } else if (${loop}) { // Reset for looping startTime = null; animationFrame = requestAnimationFrame(animate); } } // Start animation animationFrame = requestAnimationFrame(animate); // To stop animation early: // cancelAnimationFrame(animationFrame); `; break;
- src/utils/runtime-manager.js:212-567 (helper)Full generateObjectInteractionCode method called by the tool handler, with switch case for 'animation' containing the animation logicgenerateObjectInteractionCode(sceneId, objectId, action, params = {}) { const sceneUrl = `https://prod.spline.design/${sceneId}/scene.splinecode`; let codeSnippet = ''; switch (action) { case 'move': const { x = 0, y = 0, z = 0 } = params; codeSnippet = ` // Move object const obj = spline.findObjectById('${objectId}'); obj.position.x = ${x}; obj.position.y = ${y}; obj.position.z = ${z}; // Animate movement smoothly (alternative approach) let startTime = null; const duration = 1000; // 1 second const startPos = { ...obj.position }; const targetPos = { x: ${x}, y: ${y}, z: ${z} }; function animateMove(timestamp) { if (!startTime) startTime = timestamp; const elapsed = timestamp - startTime; const progress = Math.min(elapsed / duration, 1); // Use easing function (ease-out-cubic) const easeOut = 1 - Math.pow(1 - progress, 3); obj.position.x = startPos.x + (targetPos.x - startPos.x) * easeOut; obj.position.y = startPos.y + (targetPos.y - startPos.y) * easeOut; obj.position.z = startPos.z + (targetPos.z - startPos.z) * easeOut; if (progress < 1) { requestAnimationFrame(animateMove); } } // Uncomment to use animation instead of direct positioning // requestAnimationFrame(animateMove); `; break; case 'rotate': const { rotX = 0, rotY = 0, rotZ = 0 } = params; codeSnippet = ` // Rotate object const obj = spline.findObjectById('${objectId}'); obj.rotation.x = ${rotX} * Math.PI / 180; // Convert degrees to radians obj.rotation.y = ${rotY} * Math.PI / 180; obj.rotation.z = ${rotZ} * Math.PI / 180; // Animate rotation smoothly (alternative approach) let startTime = null; const duration = 1000; // 1 second const startRot = { ...obj.rotation }; const targetRot = { x: ${rotX} * Math.PI / 180, y: ${rotY} * Math.PI / 180, z: ${rotZ} * Math.PI / 180 }; function animateRotation(timestamp) { if (!startTime) startTime = timestamp; const elapsed = timestamp - startTime; const progress = Math.min(elapsed / duration, 1); // Use easing function (ease-in-out) const easeInOut = progress < 0.5 ? 2 * progress * progress : 1 - Math.pow(-2 * progress + 2, 2) / 2; obj.rotation.x = startRot.x + (targetRot.x - startRot.x) * easeInOut; obj.rotation.y = startRot.y + (targetRot.y - startRot.y) * easeInOut; obj.rotation.z = startRot.z + (targetRot.z - startRot.z) * easeInOut; if (progress < 1) { requestAnimationFrame(animateRotation); } } // Uncomment to use animation instead of direct rotation // requestAnimationFrame(animateRotation); `; break; case 'scale': const { scaleX = 1, scaleY = 1, scaleZ = 1 } = params; codeSnippet = ` // Scale object const obj = spline.findObjectById('${objectId}'); obj.scale.x = ${scaleX}; obj.scale.y = ${scaleY}; obj.scale.z = ${scaleZ}; `; break; case 'color': const { color = '#ffffff' } = params; codeSnippet = ` // Change object color const obj = spline.findObjectById('${objectId}'); if (obj.material) { obj.material.color.set('${color}'); } // For more complex color operations // You can also change emissive color if (obj.material && obj.material.emissive) { obj.material.emissive.set('#000000'); obj.material.emissiveIntensity = 0.5; } // Or use RGB values // obj.material.color.setRGB(1, 0, 0); // Red `; break; case 'visibility': const { visible = true } = params; codeSnippet = ` // Change object visibility const obj = spline.findObjectById('${objectId}'); obj.visible = ${visible}; // To fade out an object gradually function fadeOut(obj, duration = 1000) { const startOpacity = 1; const endOpacity = 0; let startTime = null; // Store the original material const originalMaterial = obj.material.clone(); // Make sure the material is set to be transparent obj.material.transparent = true; function animate(timestamp) { if (!startTime) startTime = timestamp; const elapsed = timestamp - startTime; const progress = Math.min(elapsed / duration, 1); obj.material.opacity = startOpacity + (endOpacity - startOpacity) * progress; if (progress < 1) { requestAnimationFrame(animate); } else { obj.visible = false; // Restore original opacity to the material obj.material.opacity = startOpacity; } } requestAnimationFrame(animate); } // To fade in an object gradually function fadeIn(obj, duration = 1000) { const startOpacity = 0; const endOpacity = 1; let startTime = null; // Make sure the material is set to be transparent and object is visible obj.material.transparent = true; obj.material.opacity = startOpacity; obj.visible = true; function animate(timestamp) { if (!startTime) startTime = timestamp; const elapsed = timestamp - startTime; const progress = Math.min(elapsed / duration, 1); obj.material.opacity = startOpacity + (endOpacity - startOpacity) * progress; if (progress < 1) { requestAnimationFrame(animate); } else { // If material wasn't originally transparent, we could reset here // obj.material.transparent = false; } } requestAnimationFrame(animate); } // Uncomment to use fading // ${visible ? 'fadeIn(obj, 1000);' : 'fadeOut(obj, 1000);'} `; break; case 'emitEvent': const { eventName = 'mouseDown' } = params; codeSnippet = ` // Emit event on object const obj = spline.findObjectById('${objectId}'); obj.emitEvent('${eventName}'); // Listen for events on this object spline.addEventListener('${eventName}', (e) => { if (e.target.id === '${objectId}') { console.log('Event ${eventName} triggered on object'); // Handle the event } }); // Available events: // - mouseDown // - mouseUp // - mouseHover (mouseOver) // - mouseOut // - keyDown // - keyUp // - collision `; break; case 'material': const { materialId = '', materialParams = {} } = params; codeSnippet = ` // Change object material const obj = spline.findObjectById('${objectId}'); // Option 1: Apply an existing material by finding it const material = spline.findMaterialById('${materialId}'); if (material) { obj.material = material; } // Option 2: Create a new material // Note: This is a simplified example - Spline materials are more complex const newMaterial = new THREE.MeshStandardMaterial({ color: ${materialParams.color ? `'${materialParams.color}'` : "'#ffffff'"}, roughness: ${materialParams.roughness || 0.5}, metalness: ${materialParams.metalness || 0}, transparent: ${materialParams.transparent || false}, opacity: ${materialParams.opacity || 1} }); // To apply the new material: // obj.material = newMaterial; `; break; case 'animation': const { animationType = 'rotate', duration = 1000, easing = 'easeInOut', loop = false } = params; let animationCode = ''; if (animationType === 'rotate') { animationCode = ` // Rotate animation const startRotation = { ...obj.rotation }; const targetRotation = { x: startRotation.x + Math.PI * 2, // Full 360° rotation y: startRotation.y, z: startRotation.z }; obj.rotation.x = startRotation.x + (targetRotation.x - startRotation.x) * progress;`; } else if (animationType === 'move') { animationCode = ` // Move animation const startPosition = { ...obj.position }; const targetPosition = { x: startPosition.x, y: startPosition.y + 1, // Move up by 1 unit z: startPosition.z }; obj.position.x = startPosition.x + (targetPosition.x - startPosition.x) * progress; obj.position.y = startPosition.y + (targetPosition.y - startPosition.y) * progress; obj.position.z = startPosition.z + (targetPosition.z - startPosition.z) * progress;`; } else if (animationType === 'scale') { animationCode = ` // Scale animation const startScale = { ...obj.scale }; const targetScale = { x: startScale.x * 1.5, // Scale up by 50% y: startScale.y * 1.5, z: startScale.z * 1.5 }; obj.scale.x = startScale.x + (targetScale.x - startScale.x) * progress; obj.scale.y = startScale.y + (targetScale.y - startScale.y) * progress; obj.scale.z = startScale.z + (targetScale.z - startScale.z) * progress;`; } let easingFunction = ''; if (easing === 'linear') { easingFunction = 'const ease = progress;'; } else if (easing === 'easeIn') { easingFunction = 'const ease = progress * progress;'; } else if (easing === 'easeOut') { easingFunction = 'const ease = 1 - Math.pow(1 - progress, 2);'; } else { // easeInOut easingFunction = 'const ease = progress < 0.5 ? 2 * progress * progress : 1 - Math.pow(-2 * progress + 2, 2) / 2;'; } codeSnippet = ` // Animate object const obj = spline.findObjectById('${objectId}'); let startTime = null; const duration = ${duration}; // milliseconds let animationFrame; function animate(timestamp) { if (!startTime) startTime = timestamp; const elapsed = timestamp - startTime; let progress = Math.min(elapsed / duration, 1); // Apply easing ${easingFunction} progress = ease; ${animationCode} if (progress < 1) { animationFrame = requestAnimationFrame(animate); } else if (${loop}) { // Reset for looping startTime = null; animationFrame = requestAnimationFrame(animate); } } // Start animation animationFrame = requestAnimationFrame(animate); // To stop animation early: // cancelAnimationFrame(animationFrame); `; break; default: throw new Error(`Unsupported action: ${action}`); } return ` import { Application } from '@splinetool/runtime'; // Create a new Application instance const canvas = document.getElementById('canvas3d'); const spline = new Application(canvas); // Load the scene spline.load('${sceneUrl}').then(() => { console.log('Scene loaded successfully'); ${codeSnippet} }); `; }