agent-animate
Version:
AI-powered cinematic animations from workflow transcripts - Jony Ive precision meets Hans Zimmer timing
668 lines (571 loc) β’ 26.3 kB
JavaScript
/**
* 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')
};
}
}