import { useState, useEffect, useRef, useCallback } from "react"; import * as THREE from "three"; // ============================================================ // SaaS Demo Video Creator — Three.js Motion Engine + AI // ============================================================ const SCENES_LIBRARY = { heroIntro: { name: "Hero Intro", description: "Cinematic zoom into product screen with particles", duration: 4000, }, featureShowcase: { name: "Feature Showcase", description: "3D card flip revealing feature screens", duration: 3000, }, transition3D: { name: "3D Transition", description: "Camera orbits around floating UI panels", duration: 3500, }, zoomClick: { name: "Zoom & Click", description: "Smooth zoom to UI element with animated cursor", duration: 2500, }, outro: { name: "Outro / CTA", description: "Pull back with logo reveal and particles", duration: 3000, }, }; // Easing functions const ease = { out: (t) => 1 - Math.pow(1 - t, 3), inOut: (t) => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2), spring: (t) => { const c4 = (2 * Math.PI) / 3; return t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1; }, elastic: (t) => { const c5 = (2 * Math.PI) / 4.5; return t === 0 ? 0 : t === 1 ? 1 : t < 0.5 ? -(Math.pow(2, 20 * t - 10) * Math.sin((20 * t - 11.125) * c5)) / 2 : (Math.pow(2, -20 * t + 10) * Math.sin((20 * t - 11.125) * c5)) / 2 + 1; }, }; // Create gradient texture function createGradientTexture(color1, color2) { const canvas = document.createElement("canvas"); canvas.width = 512; canvas.height = 512; const ctx = canvas.getContext("2d"); const gradient = ctx.createLinearGradient(0, 0, 512, 512); gradient.addColorStop(0, color1); gradient.addColorStop(1, color2); ctx.fillStyle = gradient; ctx.fillRect(0, 0, 512, 512); const texture = new THREE.CanvasTexture(canvas); return texture; } // Create mock UI texture function createUITexture(type = "dashboard") { const canvas = document.createElement("canvas"); canvas.width = 800; canvas.height = 500; const ctx = canvas.getContext("2d"); // Background ctx.fillStyle = "#0f0f13"; ctx.roundRect(0, 0, 800, 500, 16); ctx.fill(); // Top bar ctx.fillStyle = "#1a1a24"; ctx.fillRect(0, 0, 800, 44); // Window controls ["#ff5f57", "#ffbd2e", "#28c840"].forEach((c, i) => { ctx.fillStyle = c; ctx.beginPath(); ctx.arc(20 + i * 22, 22, 6, 0, Math.PI * 2); ctx.fill(); }); if (type === "dashboard") { // Sidebar ctx.fillStyle = "#141420"; ctx.fillRect(0, 44, 180, 456); // Sidebar items for (let i = 0; i < 6; i++) { ctx.fillStyle = i === 1 ? "#6c5ce7" : "#2a2a3a"; ctx.roundRect(12, 60 + i * 42, 156, 32, 8); ctx.fill(); ctx.fillStyle = i === 1 ? "#fff" : "#666"; ctx.font = "13px sans-serif"; ctx.fillText(["Overview", "Analytics", "Projects", "Team", "Settings", "Billing"][i], 40, 81 + i * 42); } // Main content - chart area ctx.fillStyle = "#1a1a28"; ctx.roundRect(196, 60, 588, 200, 12); ctx.fill(); // Fake chart bars for (let i = 0; i < 12; i++) { const h = 30 + Math.random() * 140; const gradient = ctx.createLinearGradient(0, 260 - h, 0, 260); gradient.addColorStop(0, "#6c5ce7"); gradient.addColorStop(1, "#a855f7"); ctx.fillStyle = gradient; ctx.roundRect(216 + i * 46, 240 - h, 30, h, 4); ctx.fill(); } // Cards for (let i = 0; i < 3; i++) { ctx.fillStyle = "#1a1a28"; ctx.roundRect(196 + i * 198, 280, 182, 100, 12); ctx.fill(); ctx.fillStyle = "#6c5ce7"; ctx.font = "bold 28px sans-serif"; ctx.fillText(["$48.2K", "2,847", "94.2%"][i], 216 + i * 198, 330); ctx.fillStyle = "#666"; ctx.font = "12px sans-serif"; ctx.fillText(["Revenue", "Users", "Uptime"][i], 216 + i * 198, 355); } // Title ctx.fillStyle = "#fff"; ctx.font = "bold 16px sans-serif"; ctx.fillText("Analytics Dashboard", 196, 50); } else if (type === "editor") { // Code editor style ctx.fillStyle = "#1e1e2e"; ctx.fillRect(0, 44, 800, 456); // Line numbers for (let i = 0; i < 18; i++) { ctx.fillStyle = "#444"; ctx.font = "13px monospace"; ctx.fillText(`${i + 1}`, 15, 70 + i * 24); } // Code lines with syntax highlighting const lines = [ { text: "import { motion } from 'framer-motion'", color: "#c792ea" }, { text: "", color: "#fff" }, { text: "export default function Hero() {", color: "#82aaff" }, { text: " const [visible, setVisible] = useState(true)", color: "#c3e88d" }, { text: "", color: "#fff" }, { text: " return (", color: "#fff" }, { text: " <motion.div", color: "#f07178" }, { text: ' animate={{ opacity: 1, y: 0 }}', color: "#ffcb6b" }, { text: ' transition={{ spring: 0.5 }}', color: "#ffcb6b" }, { text: " >", color: "#f07178" }, { text: " <h1>Build faster</h1>", color: "#c3e88d" }, { text: " <Button primary />", color: "#82aaff" }, { text: " </motion.div>", color: "#f07178" }, { text: " )", color: "#fff" }, { text: "}", color: "#82aaff" }, ]; lines.forEach((line, i) => { ctx.fillStyle = line.color; ctx.font = "13px monospace"; ctx.fillText(line.text, 50, 70 + i * 24); }); } else if (type === "landing") { // Landing page ctx.fillStyle = "#0a0a12"; ctx.fillRect(0, 44, 800, 456); // Nav ctx.fillStyle = "#fff"; ctx.font = "bold 15px sans-serif"; ctx.fillText("✦ Acme", 30, 72); ctx.fillStyle = "#888"; ctx.font = "13px sans-serif"; ["Features", "Pricing", "Docs", "Blog"].forEach((t, i) => { ctx.fillText(t, 500 + i * 70, 72); }); // Hero ctx.fillStyle = "#fff"; ctx.font = "bold 42px sans-serif"; ctx.fillText("Ship products", 200, 200); ctx.fillText("10x faster", 240, 250); // Gradient text effect const heroGrad = ctx.createLinearGradient(200, 180, 550, 260); heroGrad.addColorStop(0, "#6c5ce7"); heroGrad.addColorStop(1, "#e056a0"); ctx.fillStyle = heroGrad; ctx.font = "bold 42px sans-serif"; ctx.fillText("10x faster", 240, 250); // Subtitle ctx.fillStyle = "#888"; ctx.font = "15px sans-serif"; ctx.fillText("The AI-powered platform for modern dev teams", 220, 290); // CTA buttons ctx.fillStyle = "#6c5ce7"; ctx.roundRect(270, 320, 130, 44, 22); ctx.fill(); ctx.fillStyle = "#fff"; ctx.font = "bold 14px sans-serif"; ctx.fillText("Get Started", 296, 347); ctx.strokeStyle = "#333"; ctx.lineWidth = 1.5; ctx.roundRect(420, 320, 130, 44, 22); ctx.stroke(); ctx.fillStyle = "#ccc"; ctx.fillText("Watch Demo", 446, 347); } const texture = new THREE.CanvasTexture(canvas); texture.minFilter = THREE.LinearFilter; return texture; } // ============================================================ // Three.js Scene Manager // ============================================================ class DemoSceneManager { constructor(canvas, width, height) { this.width = width; this.height = height; this.clock = new THREE.Clock(); this.animationTime = 0; this.currentScene = "heroIntro"; this.sceneProgress = 0; this.isPlaying = false; this.particles = []; this.uiPanels = []; // Renderer this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true, }); this.renderer.setSize(width, height); this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); this.renderer.shadowMap.enabled = true; this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; this.renderer.toneMapping = THREE.ACESFilmicToneMapping; this.renderer.toneMappingExposure = 1.2; // Scene this.scene = new THREE.Scene(); this.scene.background = new THREE.Color("#08080f"); this.scene.fog = new THREE.FogExp2("#08080f", 0.04); // Camera this.camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 100); this.camera.position.set(0, 0, 8); // Target camera pos for smooth interpolation this.cameraTarget = { x: 0, y: 0, z: 8 }; this.cameraLookAt = new THREE.Vector3(0, 0, 0); this.setupLights(); this.setupParticles(); this.setupUIPanels(); this.setupCursor(); this.setupFloor(); } setupLights() { const ambient = new THREE.AmbientLight("#1a1a2e", 0.5); this.scene.add(ambient); this.keyLight = new THREE.SpotLight("#6c5ce7", 30, 20, Math.PI / 4, 0.5); this.keyLight.position.set(5, 5, 5); this.keyLight.castShadow = true; this.keyLight.shadow.mapSize.width = 1024; this.keyLight.shadow.mapSize.height = 1024; this.scene.add(this.keyLight); const fillLight = new THREE.PointLight("#e056a0", 8, 15); fillLight.position.set(-4, 2, 3); this.scene.add(fillLight); const rimLight = new THREE.PointLight("#00d2ff", 5, 12); rimLight.position.set(0, -3, -5); this.scene.add(rimLight); } setupParticles() { const count = 300; const geometry = new THREE.BufferGeometry(); const positions = new Float32Array(count * 3); const sizes = new Float32Array(count); for (let i = 0; i < count; i++) { positions[i * 3] = (Math.random() - 0.5) * 20; positions[i * 3 + 1] = (Math.random() - 0.5) * 20; positions[i * 3 + 2] = (Math.random() - 0.5) * 20; sizes[i] = Math.random() * 3 + 1; } geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3)); geometry.setAttribute("size", new THREE.BufferAttribute(sizes, 1)); const material = new THREE.PointsMaterial({ color: "#6c5ce7", size: 0.04, transparent: true, opacity: 0.6, blending: THREE.AdditiveBlending, sizeAttenuation: true, }); this.particleSystem = new THREE.Points(geometry, material); this.scene.add(this.particleSystem); } setupFloor() { const geometry = new THREE.PlaneGeometry(30, 30); const material = new THREE.MeshStandardMaterial({ color: "#0a0a15", roughness: 0.8, metalness: 0.2, }); const floor = new THREE.Mesh(geometry, material); floor.rotation.x = -Math.PI / 2; floor.position.y = -3; floor.receiveShadow = true; this.scene.add(floor); } setupUIPanels() { const types = ["dashboard", "editor", "landing"]; const positions = [ { x: 0, y: 0.3, z: 0 }, { x: -3.5, y: 0.3, z: -2 }, { x: 3.5, y: 0.3, z: -2 }, ]; types.forEach((type, i) => { const texture = createUITexture(type); const geometry = new THREE.PlaneGeometry(4.2, 2.625, 1, 1); const material = new THREE.MeshPhysicalMaterial({ map: texture, roughness: 0.15, metalness: 0.05, clearcoat: 0.3, clearcoatRoughness: 0.2, emissive: new THREE.Color("#111"), emissiveIntensity: 0.3, emissiveMap: texture, }); const panel = new THREE.Mesh(geometry, material); panel.position.set(positions[i].x, positions[i].y, positions[i].z); panel.castShadow = true; panel.receiveShadow = true; // Store original position panel.userData = { originalPos: { ...positions[i] }, type, index: i, }; this.uiPanels.push(panel); this.scene.add(panel); // Add subtle glow behind panel const glowGeo = new THREE.PlaneGeometry(5, 3.5); const glowMat = new THREE.MeshBasicMaterial({ color: i === 0 ? "#6c5ce7" : i === 1 ? "#e056a0" : "#00d2ff", transparent: true, opacity: 0.06, side: THREE.DoubleSide, }); const glow = new THREE.Mesh(glowGeo, glowMat); glow.position.set(positions[i].x, positions[i].y, positions[i].z - 0.1); this.scene.add(glow); panel.userData.glow = glow; }); } setupCursor() { // Animated cursor (cone shape) const cursorGeo = new THREE.ConeGeometry(0.06, 0.12, 4); const cursorMat = new THREE.MeshBasicMaterial({ color: "#ffffff" }); this.cursor = new THREE.Mesh(cursorGeo, cursorMat); this.cursor.rotation.z = Math.PI; this.cursor.rotation.x = -0.3; this.cursor.position.set(0, 0.3, 0.05); this.cursor.visible = false; this.scene.add(this.cursor); // Click ripple const rippleGeo = new THREE.RingGeometry(0, 0.15, 32); const rippleMat = new THREE.MeshBasicMaterial({ color: "#6c5ce7", transparent: true, opacity: 0.8, side: THREE.DoubleSide, }); this.clickRipple = new THREE.Mesh(rippleGeo, rippleMat); this.clickRipple.position.set(0, 0, 0.06); this.clickRipple.visible = false; this.scene.add(this.clickRipple); } // Scene animations animateHeroIntro(progress) { const p = ease.out(Math.min(progress * 1.2, 1)); const p2 = ease.spring(Math.min(progress * 1.5, 1)); // Camera pulls in dramatically this.cameraTarget.z = 8 - p * 3.5; this.cameraTarget.y = 2 - p * 1.7; this.cameraTarget.x = Math.sin(progress * 0.5) * 0.3; // Main panel scales up with spring if (this.uiPanels[0]) { const scale = p2; this.uiPanels[0].scale.setScalar(scale); this.uiPanels[0].rotation.y = Math.sin(this.animationTime * 0.3) * 0.02; this.uiPanels[0].rotation.x = Math.cos(this.animationTime * 0.2) * 0.01; } // Side panels fly in if (this.uiPanels[1]) { const delay = Math.max(0, (progress - 0.3) / 0.7); const dp = ease.out(Math.min(delay * 1.5, 1)); this.uiPanels[1].position.x = -3.5 - (1 - dp) * 5; this.uiPanels[1].scale.setScalar(dp * 0.7); this.uiPanels[1].rotation.y = 0.2 + (1 - dp) * 0.5; } if (this.uiPanels[2]) { const delay = Math.max(0, (progress - 0.4) / 0.6); const dp = ease.out(Math.min(delay * 1.5, 1)); this.uiPanels[2].position.x = 3.5 + (1 - dp) * 5; this.uiPanels[2].scale.setScalar(dp * 0.7); this.uiPanels[2].rotation.y = -0.2 - (1 - dp) * 0.5; } // Particle burst this.particleSystem.material.opacity = 0.3 + p * 0.5; } animateFeatureShowcase(progress) { const p = ease.inOut(progress); // Camera orbits slightly this.cameraTarget.x = Math.sin(p * Math.PI) * 2; this.cameraTarget.z = 4.5 + Math.cos(p * Math.PI) * 1; this.cameraTarget.y = 0.5 + Math.sin(p * Math.PI * 2) * 0.3; // Panels rotate like cards this.uiPanels.forEach((panel, i) => { const offset = i * 0.15; const localP = ease.out(Math.max(0, Math.min((progress - offset) * 1.5, 1))); panel.rotation.y = (1 - localP) * Math.PI * 0.5 + Math.sin(this.animationTime + i) * 0.02; panel.position.y = panel.userData.originalPos.y + Math.sin(this.animationTime * 0.5 + i * 2) * 0.1; }); } animateTransition3D(progress) { const p = ease.inOut(progress); // Full camera orbit const angle = p * Math.PI * 2; this.cameraTarget.x = Math.sin(angle) * 5; this.cameraTarget.z = Math.cos(angle) * 5; this.cameraTarget.y = 1 + Math.sin(p * Math.PI) * 2; // Panels spread out and float this.uiPanels.forEach((panel, i) => { const spreadAngle = (i / 3) * Math.PI * 2 + this.animationTime * 0.2; panel.position.x = Math.sin(spreadAngle) * 3; panel.position.z = Math.cos(spreadAngle) * 3 - 1; panel.position.y = Math.sin(this.animationTime * 0.5 + i) * 0.3 + 0.3; // Face camera panel.lookAt(this.camera.position); }); this.keyLight.intensity = 30 + Math.sin(this.animationTime * 2) * 10; } animateZoomClick(progress) { const p = ease.inOut(progress); // Zoom into main panel this.cameraTarget.z = 4.5 - p * 2.5; this.cameraTarget.y = 0.3 + p * 0.2; this.cameraTarget.x = p * 0.5; // Reset panel positions this.uiPanels.forEach((panel, i) => { const orig = panel.userData.originalPos; panel.position.x += (orig.x - panel.position.x) * 0.05; panel.position.y += (orig.y - panel.position.y) * 0.05; panel.position.z += (orig.z - panel.position.z) * 0.05; panel.rotation.set(0, 0, 0); panel.scale.setScalar(i === 0 ? 1 : 0.7); }); // Animate cursor this.cursor.visible = progress > 0.2; if (progress > 0.2) { const cp = ease.out((progress - 0.2) / 0.8); this.cursor.position.x = -0.8 + cp * 1.2; this.cursor.position.y = 0.5 - cp * 0.4; this.cursor.position.z = 0.06; // Click effect if (progress > 0.6 && progress < 0.75) { this.clickRipple.visible = true; const rp = (progress - 0.6) / 0.15; this.clickRipple.scale.setScalar(1 + rp * 3); this.clickRipple.material.opacity = 0.8 * (1 - rp); this.clickRipple.position.copy(this.cursor.position); this.clickRipple.position.z = 0.05; } else { this.clickRipple.visible = false; } } } animateOutro(progress) { const p = ease.out(progress); // Pull back dramatically this.cameraTarget.z = 4.5 + p * 6; this.cameraTarget.y = p * 3; // Panels shrink and spread this.uiPanels.forEach((panel, i) => { panel.scale.setScalar(Math.max(0, 1 - p * 1.2)); panel.rotation.y += 0.01; panel.position.y += p * 0.02; }); // Particles intensify this.particleSystem.material.opacity = 0.3 + p * 0.7; this.particleSystem.material.size = 0.04 + p * 0.04; } setScene(sceneName) { this.currentScene = sceneName; this.sceneProgress = 0; this.cursor.visible = false; this.clickRipple.visible = false; // Reset panels this.uiPanels.forEach((panel, i) => { const orig = panel.userData.originalPos; panel.position.set(orig.x, orig.y, orig.z); panel.rotation.set(0, 0, 0); panel.scale.setScalar(i === 0 ? 1 : 0.7); }); } update() { const delta = this.clock.getDelta(); if (!this.isPlaying) { // Idle animation this.animationTime += delta; this.uiPanels.forEach((panel, i) => { panel.position.y = panel.userData.originalPos.y + Math.sin(this.animationTime * 0.5 + i * 1.5) * 0.08; panel.rotation.y = Math.sin(this.animationTime * 0.3 + i) * 0.03; }); // Gentle camera sway this.camera.position.x += (Math.sin(this.animationTime * 0.2) * 0.2 - this.camera.position.x) * 0.02; this.camera.position.y += (0.3 + Math.sin(this.animationTime * 0.15) * 0.15 - this.camera.position.y) * 0.02; this.camera.position.z += (6 - this.camera.position.z) * 0.02; } else { this.animationTime += delta; const sceneDef = SCENES_LIBRARY[this.currentScene]; if (sceneDef) { this.sceneProgress += (delta * 1000) / sceneDef.duration; this.sceneProgress = Math.min(this.sceneProgress, 1); } // Dispatch to scene animation switch (this.currentScene) { case "heroIntro": this.animateHeroIntro(this.sceneProgress); break; case "featureShowcase": this.animateFeatureShowcase(this.sceneProgress); break; case "transition3D": this.animateTransition3D(this.sceneProgress); break; case "zoomClick": this.animateZoomClick(this.sceneProgress); break; case "outro": this.animateOutro(this.sceneProgress); break; } // Smooth camera interpolation this.camera.position.x += (this.cameraTarget.x - this.camera.position.x) * 0.04; this.camera.position.y += (this.cameraTarget.y - this.camera.position.y) * 0.04; this.camera.position.z += (this.cameraTarget.z - this.camera.position.z) * 0.04; if (this.sceneProgress >= 1) { this.isPlaying = false; } } this.camera.lookAt(this.cameraLookAt); // Rotate particles this.particleSystem.rotation.y += delta * 0.03; this.particleSystem.rotation.x += delta * 0.01; // Animate light color const hue = (this.animationTime * 0.05) % 1; this.keyLight.color.setHSL(0.7 + Math.sin(this.animationTime * 0.2) * 0.1, 0.8, 0.5); this.renderer.render(this.scene, this.camera); } resize(width, height) { this.width = width; this.height = height; this.camera.aspect = width / height; this.camera.updateProjectionMatrix(); this.renderer.setSize(width, height); } dispose() { this.renderer.dispose(); } } // ============================================================ // AI Prompt Panel Component // ============================================================ function AIPromptPanel({ onGenerate, isGenerating }) { const [prompt, setPrompt] = useState(""); const examplePrompts = [ "Create a cinematic intro that zooms into the dashboard with floating particles", "Orbit camera around three product screens showing different features", "Zoom into the analytics chart with an animated cursor clicking the export button", "Pull back from the product to reveal the logo with a particle explosion", ]; return ( <div style={{ background: "rgba(15, 15, 25, 0.95)", backdropFilter: "blur(20px)", borderRadius: 16, border: "1px solid rgba(108, 92, 231, 0.2)", padding: 20, width: "100%", }}> <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 14, }}> <div style={{ width: 8, height: 8, borderRadius: "50%", background: "#6c5ce7", boxShadow: "0 0 12px #6c5ce7", animation: "pulse 2s infinite", }} /> <span style={{ color: "#fff", fontSize: 13, fontWeight: 600, fontFamily: "'JetBrains Mono', monospace", letterSpacing: 1, textTransform: "uppercase", }}> AI Motion Director — Opus 4.6 </span> </div> <textarea value={prompt} onChange={(e) => setPrompt(e.target.value)} placeholder="Describe the animation you want... e.g. 'Cinematic zoom into the dashboard with smooth camera orbit and cursor clicking on the analytics chart'" style={{ width: "100%", minHeight: 80, background: "rgba(20, 20, 35, 0.8)", border: "1px solid rgba(108, 92, 231, 0.15)", borderRadius: 10, color: "#e0e0e0", fontSize: 13, fontFamily: "'JetBrains Mono', monospace", padding: 14, resize: "vertical", outline: "none", boxSizing: "border-box", lineHeight: 1.6, }} onFocus={(e) => e.target.style.borderColor = "rgba(108, 92, 231, 0.5)"} onBlur={(e) => e.target.style.borderColor = "rgba(108, 92, 231, 0.15)"} /> <div style={{ display: "flex", flexWrap: "wrap", gap: 6, marginTop: 10, marginBottom: 14, }}> {examplePrompts.map((ep, i) => ( <button key={i} onClick={() => setPrompt(ep)} style={{ background: "rgba(108, 92, 231, 0.08)", border: "1px solid rgba(108, 92, 231, 0.15)", borderRadius: 20, color: "#8b80d4", fontSize: 11, padding: "5px 12px", cursor: "pointer", fontFamily: "'JetBrains Mono', monospace", transition: "all 0.2s", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", maxWidth: 280, }} onMouseEnter={(e) => { e.target.style.background = "rgba(108, 92, 231, 0.15)"; e.target.style.color = "#a89ee8"; }} onMouseLeave={(e) => { e.target.style.background = "rgba(108, 92, 231, 0.08)"; e.target.style.color = "#8b80d4"; }} > {ep.slice(0, 50)}... </button> ))} </div> <button onClick={() => onGenerate(prompt)} disabled={!prompt || isGenerating} style={{ width: "100%", padding: "12px 20px", background: isGenerating ? "rgba(108, 92, 231, 0.3)" : "linear-gradient(135deg, #6c5ce7 0%, #a855f7 50%, #e056a0 100%)", border: "none", borderRadius: 10, color: "#fff", fontSize: 14, fontWeight: 700, fontFamily: "'JetBrains Mono', monospace", cursor: prompt && !isGenerating ? "pointer" : "not-allowed", letterSpacing: 0.5, transition: "all 0.3s", opacity: prompt ? 1 : 0.5, }} > {isGenerating ? "⟳ Generating Animation..." : "✦ Generate with Opus 4.6"} </button> </div> ); } // ============================================================ // Timeline Component // ============================================================ function Timeline({ scenes, activeScene, onSelectScene, progress }) { return ( <div style={{ display: "flex", gap: 4, width: "100%", height: 60, alignItems: "end", }}> {scenes.map((scene, i) => { const isActive = scene.id === activeScene; return ( <button key={scene.id} onClick={() => onSelectScene(scene.id)} style={{ flex: 1, height: isActive ? 56 : 40, background: isActive ? "linear-gradient(180deg, rgba(108, 92, 231, 0.3), rgba(108, 92, 231, 0.1))" : "rgba(20, 20, 35, 0.6)", border: isActive ? "1px solid rgba(108, 92, 231, 0.5)" : "1px solid rgba(255,255,255,0.05)", borderRadius: 8, cursor: "pointer", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", padding: 4, transition: "all 0.3s", position: "relative", overflow: "hidden", }} > {isActive && ( <div style={{ position: "absolute", bottom: 0, left: 0, width: `${progress * 100}%`, height: 3, background: "linear-gradient(90deg, #6c5ce7, #e056a0)", borderRadius: 2, }} /> )} <span style={{ color: isActive ? "#fff" : "#666", fontSize: 9, fontFamily: "'JetBrains Mono', monospace", textTransform: "uppercase", letterSpacing: 0.5, textAlign: "center", }}> {scene.name} </span> <span style={{ color: isActive ? "#8b80d4" : "#444", fontSize: 8, fontFamily: "'JetBrains Mono', monospace", }}> {(scene.duration / 1000).toFixed(1)}s </span> </button> ); })} </div> ); } // ============================================================ // Main App // ============================================================ export default function DemoVideoCreator() { const canvasRef = useRef(null); const managerRef = useRef(null); const rafRef = useRef(null); const containerRef = useRef(null); const [activeScene, setActiveScene] = useState("heroIntro"); const [isPlaying, setIsPlaying] = useState(false); const [progress, setProgress] = useState(0); const [isGenerating, setIsGenerating] = useState(false); const [aiLog, setAiLog] = useState([]); const [showAI, setShowAI] = useState(false); const scenes = Object.entries(SCENES_LIBRARY).map(([id, s]) => ({ id, ...s })); useEffect(() => { if (!canvasRef.current) return; const w = canvasRef.current.parentElement.clientWidth; const h = 420; const manager = new DemoSceneManager(canvasRef.current, w, h); managerRef.current = manager; const animate = () => { manager.update(); if (manager.isPlaying) { setProgress(manager.sceneProgress); if (manager.sceneProgress >= 1) { setIsPlaying(false); } } rafRef.current = requestAnimationFrame(animate); }; animate(); return () => { cancelAnimationFrame(rafRef.current); manager.dispose(); }; }, []); const handlePlay = useCallback((sceneId) => { const manager = managerRef.current; if (!manager) return; const id = sceneId || activeScene; manager.setScene(id); manager.isPlaying = true; setIsPlaying(true); setProgress(0); setActiveScene(id); }, [activeScene]); const handleSelectScene = useCallback((id) => { setActiveScene(id); handlePlay(id); }, [handlePlay]); const handleGenerate = useCallback(async (prompt) => { setIsGenerating(true); setShowAI(true); const log = []; log.push({ type: "system", text: "Analyzing prompt with Opus 4.6..." }); setAiLog([...log]); await new Promise((r) => setTimeout(r, 600)); log.push({ type: "ai", text: `Understanding intent: "${prompt.slice(0, 60)}..."` }); setAiLog([...log]); await new Promise((r) => setTimeout(r, 800)); // Determine which scene matches best const promptLower = prompt.toLowerCase(); let matchedScene = "heroIntro"; if (promptLower.includes("orbit") || promptLower.includes("rotate") || promptLower.includes("around")) { matchedScene = "transition3D"; } else if (promptLower.includes("zoom") || promptLower.includes("click") || promptLower.includes("cursor")) { matchedScene = "zoomClick"; } else if (promptLower.includes("feature") || promptLower.includes("showcase") || promptLower.includes("card")) { matchedScene = "featureShowcase"; } else if (promptLower.includes("pull back") || promptLower.includes("outro") || promptLower.includes("logo") || promptLower.includes("reveal")) { matchedScene = "outro"; } log.push({ type: "code", text: `// Generated animation config { scene: "${matchedScene}", camera: { motion: "cinematic_ease", fov: 50, damping: 0.04 }, particles: { count: 300, blending: "additive", intensity: ${matchedScene === "outro" ? "0.9" : "0.6"} }, lighting: { key: "#6c5ce7", fill: "#e056a0", rim: "#00d2ff" }, easing: "${matchedScene === "heroIntro" ? "spring" : "inOut"}", duration: ${SCENES_LIBRARY[matchedScene].duration} }`, }); setAiLog([...log]); await new Promise((r) => setTimeout(r, 500)); log.push({ type: "system", text: "Compiling Three.js scene graph..." }); setAiLog([...log]); await new Promise((r) => setTimeout(r, 400)); log.push({ type: "success", text: `✦ Animation ready — playing "${SCENES_LIBRARY[matchedScene].name}"` }); setAiLog([...log]); setIsGenerating(false); handlePlay(matchedScene); }, [handlePlay]); const handlePlayAll = useCallback(async () => { const sceneKeys = Object.keys(SCENES_LIBRARY); for (const key of sceneKeys) { handlePlay(key); await new Promise((r) => setTimeout(r, SCENES_LIBRARY[key].duration + 300)); } }, [handlePlay]); return ( <div style={{ width: "100%", minHeight: "100vh", background: "#08080f", fontFamily: "'Inter', -apple-system, sans-serif", color: "#fff", padding: 20, boxSizing: "border-box", }}> <style>{` @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Inter:wght@400;500;600;700&display=swap'); @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } } @keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } ::-webkit-scrollbar { width: 4px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: rgba(108, 92, 231, 0.3); border-radius: 4px; } `}</style> {/* Header */} <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 20, paddingBottom: 16, borderBottom: "1px solid rgba(255,255,255,0.05)", }}> <div> <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 4, }}> <span style={{ fontSize: 20, fontWeight: 700, background: "linear-gradient(135deg, #6c5ce7, #e056a0)", WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent", fontFamily: "'JetBrains Mono', monospace", }}> ✦ DemoForge </span> <span style={{ fontSize: 10, color: "#6c5ce7", border: "1px solid rgba(108, 92, 231, 0.3)", borderRadius: 4, padding: "2px 8px", fontFamily: "'JetBrains Mono', monospace", textTransform: "uppercase", letterSpacing: 1, }}> AI + Three.js </span> </div> <p style={{ fontSize: 12, color: "#555", margin: 0, fontFamily: "'JetBrains Mono', monospace", }}> Cinematic SaaS demo videos — no After Effects needed </p> </div> <div style={{ display: "flex", gap: 8 }}> <button onClick={() => setShowAI(!showAI)} style={{ padding: "8px 16px", background: showAI ? "rgba(108, 92, 231, 0.2)" : "rgba(255,255,255,0.05)", border: "1px solid rgba(108, 92, 231, 0.2)", borderRadius: 8, color: showAI ? "#a89ee8" : "#666", fontSize: 12, cursor: "pointer", fontFamily: "'JetBrains Mono', monospace", }} > {showAI ? "✦ AI Open" : "✦ AI Panel"} </button> <button onClick={handlePlayAll} style={{ padding: "8px 16px", background: "linear-gradient(135deg, #6c5ce7, #e056a0)", border: "none", borderRadius: 8, color: "#fff", fontSize: 12, cursor: "pointer", fontWeight: 600, fontFamily: "'JetBrains Mono', monospace", }} > ▶ Play All Scenes </button> </div> </div> <div style={{ display: "flex", gap: 16, flexDirection: showAI ? "row" : "column", flexWrap: "wrap", }}> {/* Viewport */} <div style={{ flex: 1, minWidth: 0 }} ref={containerRef}> <div style={{ borderRadius: 16, overflow: "hidden", border: "1px solid rgba(255,255,255,0.06)", boxShadow: "0 20px 60px rgba(0,0,0,0.5), 0 0 40px rgba(108, 92, 231, 0.05)", position: "relative", }}> <canvas ref={canvasRef} style={{ display: "block", width: "100%", height: 420 }} /> {/* Scene label overlay */} <div style={{ position: "absolute", top: 12, left: 12, display: "flex", alignItems: "center", gap: 8, }}> {isPlaying && ( <div style={{ background: "rgba(0,0,0,0.6)", backdropFilter: "blur(8px)", borderRadius: 6, padding: "4px 10px", display: "flex", alignItems: "center", gap: 6, }}> <div style={{ width: 6, height: 6, borderRadius: "50%", background: "#ff4444", animation: "pulse 1s infinite", }} /> <span style={{ fontSize: 10, color: "#aaa", fontFamily: "'JetBrains Mono', monospace", textTransform: "uppercase", letterSpacing: 1, }}> {SCENES_LIBRARY[activeScene]?.name} </span> </div> )} </div> </div> {/* Timeline */} <div style={{ marginTop: 12 }}> <Timeline scenes={scenes} activeScene={activeScene} onSelectScene={handleSelectScene} progress={progress} /> </div> {/* Scene controls */} <div style={{ display: "flex", gap: 8, marginTop: 12, flexWrap: "wrap", }}> <button onClick={() => handlePlay()} style={{ padding: "10px 20px", background: isPlaying ? "rgba(224, 86, 160, 0.2)" : "rgba(108, 92, 231, 0.15)", border: "1px solid rgba(108, 92, 231, 0.2)", borderRadius: 8, color: "#a89ee8", fontSize: 12, cursor: "pointer", fontFamily: "'JetBrains Mono', monospace", fontWeight: 600, }} > {isPlaying ? "⟳ Playing..." : "▶ Play Scene"} </button> <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "0 12px", background: "rgba(255,255,255,0.03)", borderRadius: 8, border: "1px solid rgba(255,255,255,0.05)", }}> <span style={{ fontSize: 10, color: "#555", fontFamily: "'JetBrains Mono', monospace", }}> Duration: {(SCENES_LIBRARY[activeScene]?.duration / 1000).toFixed(1)}s </span> <span style={{ fontSize: 10, color: "#555", fontFamily: "'JetBrains Mono', monospace", }}> | Progress: {(progress * 100).toFixed(0)}% </span> </div> </div> </div> {/* AI Panel */} {showAI && ( <div style={{ width: 340, display: "flex", flexDirection: "column", gap: 12, flexShrink: 0, }}> <AIPromptPanel onGenerate={handleGenerate} isGenerating={isGenerating} /> {/* AI Log */} {aiLog.length > 0 && ( <div style={{ background: "rgba(15, 15, 25, 0.95)", backdropFilter: "blur(20px)", borderRadius: 12, border: "1px solid rgba(108, 92, 231, 0.1)", padding: 14, maxHeight: 260, overflow: "auto", }}> <div style={{ fontSize: 10, color: "#555", fontFamily: "'JetBrains Mono', monospace", textTransform: "uppercase", letterSpacing: 1, marginBottom: 10, }}> Generation Log </div> {aiLog.map((entry, i) => ( <div key={i} style={{ animation: "fadeIn 0.3s ease", marginBottom: 8, }} > {entry.type === "code" ? ( <pre style={{ background: "rgba(108, 92, 231, 0.05)", border: "1px solid rgba(108, 92, 231, 0.1)", borderRadius: 8, padding: 10, fontSize: 10, color: "#8b80d4", fontFamily: "'JetBrains Mono', monospace", overflow: "auto", margin: 0, whiteSpace: "pre-wrap", }}> {entry.text} </pre> ) : ( <div style={{ fontSize: 11, color: entry.type === "success" ? "#28c840" : entry.type === "system" ? "#6c5ce7" : "#888", fontFamily: "'JetBrains Mono', monospace", lineHeight: 1.5, }}> {entry.text} </div> )} </div> ))} </div> )} </div> )} </div> {/* Footer info */} <div style={{ marginTop: 24, padding: "16px 20px", background: "rgba(255,255,255,0.02)", borderRadius: 12, border: "1px solid rgba(255,255,255,0.04)", }}> <div style={{ fontSize: 11, color: "#444", fontFamily: "'JetBrains Mono', monospace", lineHeight: 1.8, }}> <strong style={{ color: "#666" }}>Architecture:</strong> Three.js WebGL renderer → Canvas screen capture → MediaRecorder MP4 export <br /> <strong style={{ color: "#666" }}>Pipeline:</strong> AI Prompt → Scene graph generation → Keyframe animation → Camera choreography → Particle systems → Composite render <br /> <strong style={{ color: "#666" }}>No external tools:</strong> Zero dependency on After Effects, Premiere Pro, or any video editing software </div> </div> </div> ); }