UNPKG

agent-animate

Version:

AI-powered cinematic animations from workflow transcripts - Jony Ive precision meets Hans Zimmer timing

668 lines (571 loc) β€’ 26.3 kB
/** * Agent Animate Web - Main Application Class * Orchestrates the entire animation system using GSAP */ class AgentAnimate { constructor() { this.parser = new ArchitectureParser(); this.sceneManager = new SceneManager(); this.timeline = new CinematicTimeline(); this.responsiveManager = new ResponsiveManager(); this.isPlaying = false; this.currentScene = 0; this.totalDuration = 15; this.masterTimeline = null; this.initializeUI(); this.setupEventListeners(); // Initialize workflow parser for transcript support this.workflowParser = null; // Will be initialized when WorkflowParser is available // Initialize AI scene creation agent this.sceneAgent = null; // Will be initialized when SceneCreationAgent is available } initializeUI() { // Setup UI elements this.canvas = document.getElementById('animation-canvas'); this.container = document.getElementById('animation-container'); this.connectionsContainer = document.getElementById('connections-container'); this.packetsContainer = document.getElementById('packets-container'); this.sceneTitle = document.getElementById('scene-title'); this.sceneSubtitle = document.getElementById('scene-subtitle'); this.progressFill = document.getElementById('progress-fill'); this.timeDisplay = document.getElementById('time-display'); this.sceneDisplay = document.getElementById('scene-display'); this.playBtn = document.getElementById('play-btn'); // Set initial state this.updateUI(); } setupEventListeners() { // GSAP timeline callbacks this.onTimelineUpdate = () => { if (this.masterTimeline) { const progress = this.masterTimeline.progress() * 100; const currentTime = this.masterTimeline.time(); this.progressFill.style.width = `${progress}%`; this.timeDisplay.textContent = `${currentTime.toFixed(1)}s`; // Update scene display const scene = this.sceneManager.getCurrentScene(currentTime); if (scene) { this.currentScene = scene.index; this.sceneDisplay.textContent = `${scene.index + 1}/${this.sceneManager.scenes.length}`; } } }; } async createAnimation(prompt, duration = 15) { try { this.showLoading(true); this.totalDuration = duration; // Parse the prompt into architecture console.log(`🎬 Parsing prompt: "${prompt}"`); const architecture = await this.parser.parse(prompt); console.log(`πŸ“Š Architecture parsed:`, { components: architecture.components.length, connections: architecture.connections.length }); // Create scenes this.sceneManager.createScenes(architecture, duration); console.log(`🎭 Created ${this.sceneManager.scenes.length} scenes`); // Layout components responsively this.responsiveManager.layoutComponents(architecture.components); // Clear previous animation this.clearCanvas(); // Create visual elements this.createComponents(architecture.components); this.createConnections(architecture.connections); // Ensure components and connections are positioned and initially visible console.log('πŸ”§ Setting up components for animation'); architecture.components.forEach(comp => { const element = document.getElementById(`component-${comp.id}`); if (element) { // Set initial visible state instead of waiting for timeline gsap.set(element, { opacity: 1, scale: 1, x: 0, y: 0 }); console.log(`βœ… Component ${comp.id} visible at (${comp.x}, ${comp.y})`); } else { console.log(`❌ Component ${comp.id} element not found`); } }); // Make connections visible and update their paths console.log('πŸ”§ Setting up connections'); architecture.connections.forEach((conn, index) => { const element = document.getElementById(`connection-${index}`); if (element) { gsap.set(element, { opacity: 1 }); // Find components and update connection path const fromComponent = architecture.components.find(c => c.id === conn.fromId); const toComponent = architecture.components.find(c => c.id === conn.toId); if (fromComponent && toComponent) { const fromElement = document.getElementById(`component-${fromComponent.id}`); const toElement = document.getElementById(`component-${toComponent.id}`); if (fromElement && toElement) { const fromPoint = Component.getConnectionPoint(fromElement, 'right'); const toPoint = Component.getConnectionPoint(toElement, 'left'); Connection.updatePath(element, fromPoint, toPoint); console.log(`βœ… Connection ${index} path updated`); } } console.log(`βœ… Connection ${index} visible`); } else { console.log(`❌ Connection ${index} element not found`); } }); // Build enhanced timeline with component animations this.masterTimeline = gsap.timeline({ paused: true }); // Start with components hidden, then animate them in gsap.set(architecture.components.map(comp => `#component-${comp.id}`), { opacity: 0, scale: 0.3 }); // Animate title this.masterTimeline .to([this.sceneTitle, this.sceneSubtitle], { opacity: 1, y: 0, duration: 0.8, ease: "power2.out" }) .to({}, { duration: 1.0 }) // Hold title .to([this.sceneTitle, this.sceneSubtitle], { opacity: 0.3, duration: 0.5 }); // Animate components in groups with enhanced effects const componentGroups = this.groupComponentsByType(architecture.components); let currentTime = 2.0; const groupNames = ['clients', 'edge', 'services', 'data', 'ops']; groupNames.forEach((groupName, groupIndex) => { const group = componentGroups[groupName]; if (group && group.length > 0) { const groupElements = group.map(comp => `#component-${comp.id}`); // Enhanced entrance with different effects per group const entranceEffects = { clients: { ease: "cubic-bezier(0.34, 1.56, 0.64, 1)", y: 30 }, edge: { ease: "back.out(1.4)", rotationY: 15 }, services: { ease: "elastic.out(0.8, 0.4)", x: -20 }, data: { ease: "cubic-bezier(0.175, 0.885, 0.32, 1.275)", scale: 0.8 }, ops: { ease: "expo.out", y: -15, rotationX: 10 } }; const effect = entranceEffects[groupName] || {}; this.masterTimeline .fromTo(groupElements, { opacity: 0, scale: 0.3, y: effect.y || 0, x: effect.x || 0, rotationY: effect.rotationY || 0 }, { opacity: 1, scale: 1, y: 0, x: 0, rotationY: 0, duration: 1.4, ease: effect.ease, stagger: { each: 0.25, from: "start" } }, currentTime) // Add subtle glow effect on entrance .to(groupElements, { filter: "drop-shadow(0 0 8px rgba(255,255,255,0.3))", duration: 0.3, ease: "power2.out", stagger: 0.1 }, currentTime + 0.5) .to(groupElements, { filter: "drop-shadow(0 0 0px rgba(255,255,255,0))", duration: 0.8, ease: "power2.out", stagger: 0.1 }, currentTime + 1.0); currentTime += 2.0; } }); // Animate connections with draw-on effect if (architecture.connections.length > 0) { // First set up stroke-dasharray for draw-on effect architecture.connections.forEach((conn, index) => { const element = document.getElementById(`connection-${index}`); if (element) { const path = element.querySelector('path'); if (path) { const pathLength = path.getTotalLength(); path.style.strokeDasharray = pathLength; path.style.strokeDashoffset = pathLength; } } }); // Animate connections drawing on this.masterTimeline .to('.connection', { opacity: 1, duration: 0.2, stagger: 0.15 }, currentTime) .to('.connection path', { strokeDashoffset: 0, duration: 1.2, ease: "cubic-bezier(0.4, 0, 0.2, 1)", stagger: 0.15 }, currentTime + 0.1) // Add arrow pulse effect .to('.connection path', { filter: "drop-shadow(0 0 4px rgba(255,255,255,0.6))", duration: 0.3, ease: "power2.out", stagger: 0.1 }, currentTime + 0.8) .to('.connection path', { filter: "drop-shadow(0 0 0px rgba(255,255,255,0))", duration: 0.5, ease: "power2.out" }, currentTime + 1.5); } // Fill rest of timeline this.masterTimeline.to({}, { duration: Math.max(0, duration - this.masterTimeline.duration()) }); // Setup timeline callbacks this.masterTimeline.eventCallback('onUpdate', this.onTimelineUpdate); this.masterTimeline.eventCallback('onComplete', () => { this.isPlaying = false; this.updateUI(); }); // Update duration display document.getElementById('duration-display').textContent = `${duration}.0s`; this.showLoading(false); // Set scene title this.sceneTitle.textContent = 'OAuth Authentication Flow'; this.sceneSubtitle.textContent = 'System Architecture Overview'; gsap.set([this.sceneTitle, this.sceneSubtitle], { opacity: 1 }); console.log('βœ… Animation setup complete - components should be visible'); } catch (error) { console.error('Animation creation failed:', error); this.showLoading(false); } } createComponents(components) { components.forEach((comp, index) => { const element = Component.createElement(comp, index); this.container.appendChild(element); }); } createConnections(connections) { connections.forEach((conn, index) => { const element = Connection.createElement(conn, index); this.connectionsContainer.appendChild(element); }); } clearCanvas() { this.container.innerHTML = ''; this.connectionsContainer.innerHTML = ''; this.packetsContainer.innerHTML = ''; } play() { if (this.masterTimeline) { this.masterTimeline.play(); this.isPlaying = true; this.updateUI(); console.log('▢️ Timeline playing'); } } pause() { if (this.masterTimeline) { this.masterTimeline.pause(); this.isPlaying = false; this.updateUI(); console.log('⏸️ Timeline paused'); } } togglePlayback() { if (this.isPlaying) { this.pause(); } else { this.play(); } } restart() { if (this.masterTimeline) { this.masterTimeline.restart(); this.isPlaying = true; this.currentScene = 0; this.updateUI(); console.log('πŸ”„ Timeline restarted'); } } nextScene() { if (this.masterTimeline && this.currentScene < this.sceneManager.scenes.length - 1) { const nextScene = this.sceneManager.scenes[this.currentScene + 1]; this.masterTimeline.seek(nextScene.startTime); this.updateUI(); } } previousScene() { if (this.masterTimeline && this.currentScene > 0) { const prevScene = this.sceneManager.scenes[this.currentScene - 1]; this.masterTimeline.seek(prevScene.startTime); this.updateUI(); } } handleResize() { // Responsive handling this.responsiveManager.handleResize(); // Update SVG viewBox if needed const rect = this.canvas.getBoundingClientRect(); const aspectRatio = rect.width / rect.height; if (aspectRatio < 16/9) { // Portrait or square - adjust viewBox const newHeight = 1920 / aspectRatio; this.canvas.setAttribute('viewBox', `0 0 1920 ${newHeight}`); } else { // Landscape - standard viewBox this.canvas.setAttribute('viewBox', '0 0 1920 1080'); } } updateUI() { // Update play button if (this.playBtn) { this.playBtn.textContent = this.isPlaying ? '⏸' : 'β–Ά'; } // Update scene display if (this.sceneDisplay) { this.sceneDisplay.textContent = `${this.currentScene + 1}/${this.sceneManager.scenes.length || 5}`; } } showLoading(show) { const loading = document.getElementById('loading'); if (loading) { loading.style.display = show ? 'block' : 'none'; } } // Helper methods groupComponentsByType(components) { return { clients: components.filter(c => c.type === 'client'), edge: components.filter(c => ['api', 'loadbalancer', 'cdn'].includes(c.type)), services: components.filter(c => ['service', 'function'].includes(c.type)), data: components.filter(c => ['database', 'cache', 'queue', 'storage', 'search'].includes(c.type)), ops: components.filter(c => ['monitoring', 'security'].includes(c.type)) }; } // Export functionality exportAsVideo() { // This would integrate with a service to render the animation as video console.log('Video export not implemented yet'); } exportAsGIF() { // This would capture frames and create a GIF console.log('GIF export not implemented yet'); } // Embed functionality getEmbedCode() { const prompt = document.getElementById('prompt-input').value; const duration = this.totalDuration; return `<iframe src="https://agent-animate.com/embed?prompt=${encodeURIComponent(prompt)}&duration=${duration}" width="800" height="450" frameborder="0"></iframe>`; } // Workflow-specific methods for transcript visualization async createAnimationFromArchitecture(architecture, duration = 15) { try { this.showLoading(true); this.totalDuration = duration; console.log(`🎬 Creating animation from parsed architecture:`, { components: architecture.components.length, connections: architecture.connections.length, platform: architecture.metadata.platform }); // Create scenes based on architecture type if (architecture.metadata.source === 'video_transcript') { this.sceneManager.createWorkflowScenes(architecture, duration); } else { this.sceneManager.createScenes(architecture, duration); } console.log(`🎭 Created ${this.sceneManager.scenes.length} scenes`); // Layout components using responsive manager this.responsiveManager.layoutComponents(architecture.components); // Clear previous animation this.clearCanvas(); // Create visual elements this.createComponents(architecture.components); this.createConnections(architecture.connections); // Set up components as hidden initially (timeline will reveal them) console.log('πŸ”§ Setting up workflow components for animation'); architecture.components.forEach(comp => { const element = document.getElementById(`component-${comp.id}`); if (element) { gsap.set(element, { opacity: 0, scale: 0.3, x: 0, y: 0 }); console.log(`βœ… Workflow component ${comp.id} hidden and ready at (${comp.x}, ${comp.y})`); } }); // Set up connections architecture.connections.forEach((conn, index) => { const element = document.getElementById(`connection-${index}`); if (element) { const fromComponent = architecture.components.find(c => c.id === conn.fromId); const toComponent = architecture.components.find(c => c.id === conn.toId); if (fromComponent && toComponent) { const fromElement = document.getElementById(`component-${fromComponent.id}`); const toElement = document.getElementById(`component-${toComponent.id}`); if (fromElement && toElement) { const fromPoint = Component.getConnectionPoint(fromElement, 'right'); const toPoint = Component.getConnectionPoint(toElement, 'left'); Connection.updatePath(element, fromPoint, toPoint); } } } }); // Build workflow-specific timeline this.masterTimeline = this.createWorkflowTimeline(architecture, duration); // Setup timeline callbacks this.masterTimeline.eventCallback('onUpdate', this.onTimelineUpdate); this.masterTimeline.eventCallback('onComplete', () => { this.isPlaying = false; this.updateUI(); }); // Update UI const durationDisplay = document.getElementById('duration-display'); if (durationDisplay) { durationDisplay.textContent = `${duration}.0s`; } this.showLoading(false); // Set scene title from metadata (but keep hidden initially) if (architecture.metadata.narrative && architecture.metadata.narrative.title) { if (this.sceneTitle) { this.sceneTitle.textContent = architecture.metadata.narrative.title; } if (this.sceneSubtitle) { this.sceneSubtitle.textContent = architecture.metadata.narrative.description || 'Workflow Visualization'; } } // Initialize titles as hidden - the timeline will reveal them if (this.sceneTitle && this.sceneSubtitle) { gsap.set([this.sceneTitle, this.sceneSubtitle], { opacity: 0, y: 20 }); } console.log('βœ… Workflow animation setup complete'); // Auto-start the workflow animation this.play(); } catch (error) { console.error('Workflow animation creation failed:', error); this.showLoading(false); throw error; } } createWorkflowTimeline(architecture, duration) { const timeline = gsap.timeline({ paused: true }); // Group components by workflow hierarchy const workflowGroups = this.groupWorkflowComponents(architecture.components); let currentTime = 1.0; // Animate title first (if elements exist) if (this.sceneTitle && this.sceneSubtitle) { // Set initial state gsap.set([this.sceneTitle, this.sceneSubtitle], { opacity: 0, y: 20 }); timeline .to([this.sceneTitle, this.sceneSubtitle], { opacity: 1, y: 0, duration: 1.0, ease: "power2.out" }) .to({}, { duration: 2.0 }) // Hold title longer .to([this.sceneTitle, this.sceneSubtitle], { opacity: 0, y: -10, duration: 0.8, ease: "power2.in" }); } currentTime = 4.0; // Increased to account for longer title sequence // Animate workflow components in logical order const groupOrder = ['platform', 'integrations', 'ui', 'workflow']; groupOrder.forEach((groupName, groupIndex) => { const group = workflowGroups[groupName]; if (group && group.length > 0) { const groupElements = group.map(comp => `#component-${comp.id}`); // Set initial hidden state gsap.set(groupElements, { opacity: 0, scale: 0.3 }); // Animate group entrance timeline.to(groupElements, { opacity: 1, scale: 1, duration: 1.2, ease: "back.out(1.4)", stagger: { each: 0.25, from: "start" } }, currentTime); // Add glow effect for workflow components if (groupName === 'integrations' || groupName === 'workflow') { timeline .to(groupElements, { filter: "drop-shadow(0 0 10px rgba(100, 210, 255, 0.4))", duration: 0.4, ease: "power2.out", stagger: 0.1 }, currentTime + 0.5) .to(groupElements, { filter: "drop-shadow(0 0 0px rgba(100, 210, 255, 0))", duration: 0.8, ease: "power2.out" }, currentTime + 1.2); } currentTime += 2.2; } }); // Animate connections after components if (architecture.connections.length > 0) { // Set up stroke-dasharray for draw-on effect architecture.connections.forEach((conn, index) => { const element = document.getElementById(`connection-${index}`); if (element) { const path = element.querySelector('path'); if (path) { const pathLength = path.getTotalLength(); path.style.strokeDasharray = pathLength; path.style.strokeDashoffset = pathLength; } } }); timeline .to('.connection', { opacity: 1, duration: 0.3, stagger: 0.2 }, currentTime) .to('.connection path', { strokeDashoffset: 0, duration: 1.5, ease: "power2.out", stagger: 0.2 }, currentTime + 0.2); currentTime += 2.0; } // Fill rest of timeline timeline.to({}, { duration: Math.max(0, duration - timeline.duration()) }); return timeline; } groupWorkflowComponents(components) { return { platform: components.filter(c => c.category === 'platform' || c.type === 'designer'), integrations: components.filter(c => c.category === 'integration' || ['service', 'database', 'api'].includes(c.type)), ui: components.filter(c => c.category === 'ui' || c.type === 'interface'), workflow: components.filter(c => c.category === 'workflow' || c.type === 'workflow') }; } }