import { MathUtils } from "three"; import { RobotAnimationConfig } from "@/lib/types"; // Define the interface for the Urdf viewer element export interface UrdfViewerElement extends HTMLElement { setJointValue: (joint: string, value: number) => void; } /** * Generalized animation function for any robot * @param viewer The Urdf viewer element * @param config Configuration for the robot's joint animations * @returns A cleanup function to cancel the animation */ export function animateRobot( viewer: UrdfViewerElement, config: RobotAnimationConfig ): () => void { let animationFrameId: number | null = null; let isRunning = true; const speedMultiplier = config.speedMultiplier || 1; const animate = () => { if (!isRunning) return; const time = Date.now() / 300; // Base time unit try { // Process each joint configuration for (const joint of config.joints) { // Calculate the animation ratio (0-1) based on the animation type let ratio = 0; const adjustedTime = time * joint.speed * speedMultiplier + joint.offset; switch (joint.type) { case "sine": // Sine wave oscillation mapped to 0-1 ratio = (Math.sin(adjustedTime) + 1) / 2; break; case "linear": // Saw tooth pattern (0 to 1 repeated) ratio = (adjustedTime % (2 * Math.PI)) / (2 * Math.PI); break; case "constant": // Constant value (using max) ratio = 1; break; default: // Use custom easing if provided if (joint.customEasing) { ratio = joint.customEasing(adjustedTime); } } // Calculate the joint value based on min/max and the ratio let value = MathUtils.lerp(joint.min, joint.max, ratio); // Convert from degrees to radians if specified if (joint.isDegrees) { value = (value * Math.PI) / 180; } // Set the joint value, catching errors for non-existent joints try { viewer.setJointValue(joint.name, value); } catch (e) { // Silently ignore if the joint doesn't exist } } } catch (err) { console.error("Error in robot animation:", err); } // Continue the animation loop animationFrameId = requestAnimationFrame(animate); }; // Start the animation animationFrameId = requestAnimationFrame(animate); // Return cleanup function return () => { isRunning = false; if (animationFrameId) { cancelAnimationFrame(animationFrameId); animationFrameId = null; } }; } /** * Animates a hexapod robot (like T12) with walking motion * @param viewer The Urdf viewer element * @returns A cleanup function to cancel the animation */ export function animateHexapodRobot(viewer: UrdfViewerElement): () => void { let animationFrameId: number | null = null; let isRunning = true; const animate = () => { // Don't continue animation if we've been told to stop if (!isRunning) return; // Animate the legs (for T12 robot) const time = Date.now() / 3e2; try { for (let i = 1; i <= 6; i++) { const offset = (i * Math.PI) / 3; const ratio = Math.max(0, Math.sin(time + offset)); // For a hexapod robot like T12 if (typeof viewer.setJointValue === "function") { // Hip joints viewer.setJointValue( `HP${i}`, (MathUtils.lerp(30, 0, ratio) * Math.PI) / 180 ); // Knee joints viewer.setJointValue( `KP${i}`, (MathUtils.lerp(90, 150, ratio) * Math.PI) / 180 ); // Ankle joints viewer.setJointValue( `AP${i}`, (MathUtils.lerp(-30, -60, ratio) * Math.PI) / 180 ); // Check if these joints exist before setting values try { // Tire/Contact joints viewer.setJointValue(`TC${i}A`, MathUtils.lerp(0, 0.065, ratio)); viewer.setJointValue(`TC${i}B`, MathUtils.lerp(0, 0.065, ratio)); // Wheel rotation viewer.setJointValue(`W${i}`, performance.now() * 0.001); } catch (e) { // Silently ignore if those joints don't exist } } } } catch (err) { console.error("Error in animation:", err); } // Continue the animation loop animationFrameId = requestAnimationFrame(animate); }; // Start the animation animationFrameId = requestAnimationFrame(animate); // Return cleanup function return () => { // Mark animation as stopped but DO NOT reset joint positions isRunning = false; if (animationFrameId) { cancelAnimationFrame(animationFrameId); animationFrameId = null; } }; } // Example: Walking animation for Cassie robot export const cassieWalkingConfig: RobotAnimationConfig = { speedMultiplier: 0.5, // Adjust overall speed joints: [ // Left leg { name: "hip_abduction_left", type: "sine", min: -0.1, // Small side-to-side movement max: 0.1, speed: 1, offset: 0, isDegrees: false, // Already in radians }, { name: "hip_rotation_left", // Assuming this joint exists type: "sine", min: -0.2, max: 0.2, speed: 1, offset: Math.PI / 2, // 90 degrees out of phase isDegrees: false, }, { name: "hip_flexion_left", // Assuming this joint exists type: "sine", min: -0.3, max: 0.6, speed: 1, offset: 0, isDegrees: false, }, { name: "knee_joint_left", // Assuming this joint exists type: "sine", min: 0.2, max: 1.4, speed: 1, offset: Math.PI / 2, // 90 degrees phase shifted from hip isDegrees: false, }, { name: "ankle_joint_left", // Assuming this joint exists type: "sine", min: -0.4, max: 0.1, speed: 1, offset: Math.PI, // 180 degrees out of phase with hip isDegrees: false, }, { name: "toe_joint_left", // Assuming this joint exists type: "sine", min: -0.2, max: 0.2, speed: 1, offset: Math.PI * 1.5, // 270 degrees phase isDegrees: false, }, // Right leg (with appropriate phase shift to alternate with left leg) { name: "hip_abduction_right", // Assuming this joint exists type: "sine", min: -0.1, max: 0.1, speed: 1, offset: Math.PI, // 180 degrees out of phase with left side isDegrees: false, }, { name: "hip_rotation_right", // Assuming this joint exists type: "sine", min: -0.2, max: 0.2, speed: 1, offset: Math.PI + Math.PI / 2, // 180 + 90 degrees phase isDegrees: false, }, { name: "hip_flexion_right", // Assuming this joint exists type: "sine", min: -0.3, max: 0.6, speed: 1, offset: Math.PI, // 180 degrees out of phase with left hip isDegrees: false, }, { name: "knee_joint_right", // Assuming this joint exists type: "sine", min: 0.2, max: 1.4, speed: 1, offset: Math.PI + Math.PI / 2, // 180 + 90 degrees phase isDegrees: false, }, { name: "ankle_joint_right", // Assuming this joint exists type: "sine", min: -0.4, max: 0.1, speed: 1, offset: 0, // 180 + 180 degrees = 360 = 0 isDegrees: false, }, { name: "toe_joint_right", // Assuming this joint exists type: "sine", min: -0.2, max: 0.2, speed: 1, offset: Math.PI / 2, // 180 + 270 = 450 degrees = 90 degrees isDegrees: false, }, ], };