Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>FaceSwap Studio - AI Video Processor</title> | |
| <!-- Importing FontAwesome --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <!-- Google Fonts --> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --bg-dark: #0f0f13; | |
| --bg-panel: #16161a; | |
| --bg-input: #1e1e24; | |
| --primary: #6366f1; | |
| --primary-hover: #4f46e5; | |
| --accent: #ec4899; | |
| --text-main: #ffffff; | |
| --text-muted: #a1a1aa; | |
| --border: #2d2d3a; | |
| --glass: rgba(22, 22, 26, 0.8); | |
| --gradient: linear-gradient(135deg, #6366f1 0%, #ec4899 100%); | |
| --shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.5); | |
| --glow: 0 0 20px rgba(99, 66, 241, 0.3); | |
| } | |
| * { | |
| box-sizing: box-sizing; | |
| margin: 0; | |
| padding: 0; | |
| font-family: 'Inter', sans-serif; | |
| } | |
| body { | |
| background-color: var(--bg-dark); | |
| color: var(--text-main); | |
| height: 100vh; | |
| overflow-x: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| font-size: 14px; | |
| } | |
| /* --- Header --- */ | |
| header { | |
| height: 64px; | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 0 2rem; | |
| background: var(--bg-panel); | |
| z-index: 100; | |
| box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); | |
| } | |
| .logo { | |
| font-weight: 700; | |
| font-size: 1.2rem; | |
| letter-spacing: -0.5px; | |
| background: var(--gradient); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .anycoder-link { | |
| font-size: 0.8rem; | |
| color: var(--text-muted); | |
| text-decoration: none; | |
| border: 1px solid var(--border); | |
| padding: 6px 14px; | |
| border-radius: 6px; | |
| transition: all 0.3s ease; | |
| font-weight: 500; | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| background: rgba(15, 15, 19, 0.5); | |
| } | |
| .anycoder-link:hover { | |
| border-color: var(--primary); | |
| color: var(--primary); | |
| background: rgba(99, 66, 241, 0.1); | |
| } | |
| /* --- Main Layout --- */ | |
| main { | |
| display: grid; | |
| grid-template-columns: 340px 1fr; | |
| height: calc(100vh - 64px); | |
| overflow: hidden; | |
| } | |
| /* --- Sidebar (Inputs) --- */ | |
| .sidebar { | |
| background: var(--bg-panel); | |
| border-right: 1px solid var(--border); | |
| padding: 24px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 24px; | |
| overflow-y: auto; | |
| } | |
| .input-card { | |
| background: var(--bg-input); | |
| border: 1px dashed var(--border); | |
| border-radius: 12px; | |
| padding: 20px; | |
| text-align: center; | |
| transition: 0.3s; | |
| cursor: pointer; | |
| position: relative; | |
| min-height: 160px; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| overflow: hidden; | |
| } | |
| .input-card:hover { | |
| border-color: var(--primary); | |
| background: rgba(99, 66, 241, 0.05); | |
| } | |
| .input-card.active { | |
| border-color: var(--accent); | |
| border-style: solid; | |
| box-shadow: 0 0 15px rgba(236, 72, 153, 0.15); | |
| } | |
| .input-icon { | |
| font-size: 2.5rem; | |
| color: var(--text-muted); | |
| margin-bottom: 12px; | |
| transition: 0.3s; | |
| } | |
| .input-card:hover .input-icon { | |
| color: var(--primary); | |
| transform: scale(1.1); | |
| } | |
| .input-text { | |
| font-size: 0.9rem; | |
| color: var(--text-muted); | |
| } | |
| .preview-img { | |
| width: 100%; | |
| height: 100px; | |
| border-radius: 8px; | |
| display: none; | |
| margin-top: 10px; | |
| object-fit: cover; | |
| border: 1px solid var(--border); | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.3); | |
| } | |
| .file-info { | |
| margin-top: 8px; | |
| font-size: 0.75rem; | |
| color: var(--primary); | |
| font-weight: 600; | |
| } | |
| .settings-group { | |
| margin-top: 20px; | |
| background: rgba(0,0,0,0.2); | |
| padding: 15px; | |
| border-radius: 8px; | |
| border: 1px solid var(--border); | |
| } | |
| .setting-item { | |
| margin-bottom: 10px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| font-size: 0.8rem; | |
| color: var(--text-muted); | |
| } | |
| .toggle-switch { | |
| width: 32px; | |
| height: 16px; | |
| background: var(--border); | |
| border-radius: 8px; | |
| position: relative; | |
| cursor: pointer; | |
| transition: 0.3s; | |
| } | |
| .toggle-switch.active { | |
| background: var(--primary); | |
| } | |
| .toggle-switch::after { | |
| content: ''; | |
| position: absolute; | |
| top: 2px; | |
| left: 2px; | |
| width: 10px; | |
| height: 10px; | |
| background: white; | |
| border-radius: 50%; | |
| transition: 0.3s; | |
| } | |
| .toggle-switch.active::after { | |
| left: 18px; | |
| } | |
| /* --- Workspace (Video & Timeline) --- */ | |
| .workspace { | |
| display: flex; | |
| flex-direction: column; | |
| padding: 24px; | |
| gap: 20px; | |
| position: relative; | |
| background: radial-gradient(circle at center, #1a1a24 0%, #0f0f13 100%); | |
| } | |
| /* Video Container */ | |
| .video-stage { | |
| flex-grow: 1; | |
| background: #000; | |
| border-radius: 16px; | |
| position: relative; | |
| overflow: hidden; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| border: 1px solid var(--border); | |
| box-shadow: var(--shadow); | |
| } | |
| video, | |
| canvas { | |
| max-width: 100%; | |
| max-height: 100%; | |
| object-fit: contain; | |
| border-radius: 4px; | |
| } | |
| /* Overlay Controls */ | |
| .video-overlay { | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| right: 0; | |
| background: linear-gradient(to top, rgba(0, 0, 0, 0.9), transparent); | |
| padding: 20px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| pointer-events: none; | |
| } | |
| .status-badge { | |
| background: rgba(15, 15, 19, 0.8); | |
| padding: 6px 12px; | |
| border-radius: 4px; | |
| font-size: 0.75rem; | |
| border: 1px solid var(--border); | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| backdrop-filter: blur(4px); | |
| } | |
| .status-badge.processing { | |
| border-color: var(--accent); | |
| color: var(--accent); | |
| animation: pulse 1.5s infinite; | |
| } | |
| .time-display { | |
| font-family: monospace; | |
| color: var(--text-muted); | |
| font-size: 0.9rem; | |
| } | |
| /* Timeline Section */ | |
| .timeline-panel { | |
| height: 240px; | |
| background: var(--bg-panel); | |
| border-radius: 12px; | |
| border: 1px solid var(--border); | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| .timeline-header { | |
| padding: 12px 20px; | |
| border-bottom: 1px solid var(--border); | |
| font-size: 0.8rem; | |
| color: var(--text-muted); | |
| display: flex; | |
| justify-content: space-between; | |
| background: rgba(15, 15, 19, 0.3); | |
| align-items: center; | |
| } | |
| .timeline-scroll-area { | |
| flex-grow: 1; | |
| display: flex; | |
| overflow-x: auto; | |
| padding: 15px; | |
| gap: 8px; | |
| scrollbar-width: thin; | |
| scrollbar-color: var(--primary) var(--bg-dark); | |
| align-items: center; | |
| background: #0f0f13; | |
| } | |
| /* Custom Scrollbar */ | |
| .timeline-scroll-area::-webkit-scrollbar { | |
| height: 8px; | |
| } | |
| .timeline-scroll-area::-webkit-scrollbar-thumb { | |
| background: var(--border); | |
| border-radius: 4px; | |
| } | |
| .frame-thumb { | |
| min-width: 70px; | |
| height: 90px; | |
| background: #000; | |
| border-radius: 6px; | |
| flex-shrink: 0; | |
| position: relative; | |
| border: 2px solid transparent; | |
| cursor: pointer; | |
| transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); | |
| overflow: hidden; | |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); | |
| } | |
| .frame-thumb:hover { | |
| transform: translateY(-4px); | |
| z-index: 2; | |
| border-color: var(--text-muted); | |
| } | |
| .frame-thumb.active { | |
| border-color: var(--primary); | |
| transform: translateY(-4px) scale(1.05); | |
| box-shadow: 0 0 15px rgba(99, 66, 241, 0.4); | |
| z-index: 3; | |
| } | |
| .frame-thumb img { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| } | |
| .frame-overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: rgba(99, 66, 241, 0.3); | |
| display: none; | |
| } | |
| .frame-thumb.processed .frame-overlay { | |
| display: block; | |
| } | |
| .frame-number { | |
| position: absolute; | |
| bottom: 4px; | |
| right: 4px; | |
| font-size: 0.6rem; | |
| background: rgba(0, 0, 0, 0.7); | |
| color: white; | |
| padding: 1px 4px; | |
| border-radius: 2px; | |
| pointer-events: none; | |
| } | |
| /* Action Bar */ | |
| .action-bar { | |
| margin-top: 15px; | |
| display: flex; | |
| justify-content: flex-end; | |
| gap: 12px; | |
| } | |
| .btn { | |
| padding: 10px 24px; | |
| border-radius: 8px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| border: none; | |
| transition: 0.2s; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-size: 0.9rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| } | |
| .btn-primary { | |
| background: var(--gradient); | |
| color: white; | |
| box-shadow: 0 4px 15px rgba(99, 66, 241, 0.4); | |
| } | |
| .btn-primary:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 25px rgba(99, 66, 241, 0.6); | |
| } | |
| .btn-secondary { | |
| background: transparent; | |
| border: 1px solid var(--border); | |
| color: var(--text-muted); | |
| } | |
| .btn-secondary:hover { | |
| border-color: var(--text-main); | |
| color: var(--text-main); | |
| } | |
| /* --- Animations --- */ | |
| @keyframes pulse { | |
| 0% { opacity: 0.6; } | |
| 50% { opacity: 1; } | |
| 100% { opacity: 0.6; } | |
| } | |
| /* --- Responsive --- */ | |
| @media (max-width: 900px) { | |
| main { | |
| grid-template-columns: 1fr; | |
| height: auto; | |
| overflow: auto; | |
| } | |
| .sidebar { | |
| border-right: none; | |
| border-bottom: 1px solid var(--border); | |
| padding: 20px; | |
| } | |
| .workspace { | |
| height: auto; | |
| } | |
| .timeline-panel { | |
| height: 200px; | |
| } | |
| } | |
| /* Hidden inputs */ | |
| #source-input, #target-input { display: none; } | |
| /* Progress Bar Overlay */ | |
| .progress-overlay { | |
| position: absolute; | |
| top: 0; left: 0; width: 100%; height: 100%; | |
| background: rgba(15, 15, 19, 0.9); | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 200; | |
| backdrop-filter: blur(8px); | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity 0.3s ease; | |
| } | |
| .progress-overlay.visible { | |
| opacity: 1; | |
| pointer-events: all; | |
| } | |
| .progress-track { | |
| width: 300px; | |
| height: 6px; | |
| background: var(--border); | |
| border-radius: 3px; | |
| overflow: hidden; | |
| margin-top: 15px; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: var(--gradient); | |
| width: 0%; | |
| transition: width 0.2s linear; | |
| } | |
| .loader-text { | |
| font-size: 1.5rem; | |
| font-weight: 700; | |
| margin-bottom: 10px; | |
| background: var(--gradient); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="logo"> | |
| <i class="fa-solid fa-layer-group"></i> FaceSwap Studio | |
| </div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link"> | |
| Built with anycoder <i class="fa-solid fa-external-link-alt"></i> | |
| </a> | |
| </header> | |
| <main> | |
| <!-- Sidebar: Inputs --> | |
| <div class="sidebar"> | |
| <!-- Source Image Input --> | |
| <div class="input-card" id="source-zone" onclick="document.getElementById('source-input').click()"> | |
| <input type="file" id="source-input" accept="image/*"> | |
| <i class="fa-solid fa-user input-icon"></i> | |
| <div class="input-text"> | |
| <strong>Source Face</strong><br> | |
| Upload reference image | |
| </div> | |
| <img id="source-preview" class="preview-img" alt="Source Face"> | |
| <div id="source-info" class="file-info"></div> | |
| </div> | |
| <!-- Target Video Input --> | |
| <div class="input-card" id="target-zone" onclick="document.getElementById('target-input').click()"> | |
| <input type="file" id="target-input" accept="video/*"> | |
| <i class="fa-solid fa-video input-icon"></i> | |
| <div class="input-text"> | |
| <strong>Target Video</strong><br> | |
| Upload video to process | |
| </div> | |
| <div id="target-info" class="file-info"></div> | |
| </div> | |
| <div class="settings-group"> | |
| <div class="setting-item"> | |
| <span>Enhance Lighting</span> | |
| <div class="toggle-switch active"></div> | |
| </div> | |
| <div class="setting-item"> | |
| <span>Smooth Blending</span> | |
| <div class="toggle-switch active"></div> | |
| </div> | |
| <div class="setting-item"> | |
| <span>Face Tracking</span> | |
| <div class="toggle-switch active"></div> | |
| </div> | |
| </div> | |
| <div style="margin-top: 20px; font-size: 0.85rem; color: var(--text-muted); line-height: 1.6; background: rgba(0,0,0,0.2); padding: 15px; border-radius: 8px;"> | |
| <p><i class="fa-solid fa-circle-info"></i> <strong>Workflow:</strong></p> | |
| <p>1. Upload a clear face image.</p> | |
| <p>2. Upload a target video.</p> | |
| <p>3. Click "Start Swap" to process.</p> | |
| </div> | |
| </div> | |
| <!-- Workspace --> | |
| <div class="workspace"> | |
| <!-- Video Player / Canvas Output --> | |
| <div class="video-stage"> | |
| <video id="main-video" muted loop playsinline></video> | |
| <!-- Canvas for processing preview --> | |
| <canvas id="process-canvas" style="display:none;"></canvas> | |
| <div class="video-overlay"> | |
| <div class="status-badge" id="processing-status"> | |
| <i class="fa-solid fa-circle-notch"></i> Ready | |
| </div> | |
| <div class="time-display"> | |
| <span id="current-time">00:00</span> / <span id="total-time">00:00</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Timeline / Frame Visualizer --> | |
| <div class="timeline-panel"> | |
| <div class="timeline-header"> | |
| <span><i class="fa-solid fa-bars"></i> Frame Timeline</span> | |
| <span id="frame-count">0 Frames</span> | |
| </div> | |
| <div class="timeline-scroll-area" id="timeline-track"> | |
| <!-- Frames will be injected here via JS --> | |
| <div style="display:flex; align-items:center; justify-content:center; width:100%; color: var(--text-muted); font-style: italic;"> | |
| Upload a video to generate timeline | |
| </div> | |
| </div> | |
| <div class="action-bar"> | |
| <button class="btn btn-secondary" onclick="resetApp()"> | |
| <i class="fa-solid fa-rotate-left"></i> Reset | |
| </button> | |
| <button class="btn btn-primary" onclick="startSwapProcess()"> | |
| <i class="fa-solid fa-magic"></i> Start Face Swap | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- Processing Overlay --> | |
| <div class="progress-overlay" id="progress-overlay"> | |
| <div class="loader-text">Processing Frames...</div> | |
| <div class="progress-track"> | |
| <div class="progress-fill" id="progress-fill"></div> | |
| </div> | |
| <div style="margin-top: 10px; color: var(--text-muted); font-size: 0.9rem;"> | |
| Applying AI transformations... | |
| </div> | |
| </div> | |
| <script> | |
| // --- DOM Elements --- | |
| const sourceInput = document.getElementById('source-input'); | |
| const targetInput = document.getElementById('target-input'); | |
| const sourcePreview = document.getElementById('source-preview'); | |
| const sourceZone = document.getElementById('source-zone'); | |
| const sourceInfo = document.getElementById('source-info'); | |
| const targetInfo = document.getElementById('target-info'); | |
| const mainVideo = document.getElementById('main-video'); | |
| const processCanvas = document.getElementById('process-canvas'); | |
| const timelineTrack = document.getElementById('timeline-track'); | |
| const frameCountLabel = document.getElementById('frame-count'); | |
| const processingStatus = document.getElementById('processing-status'); | |
| const currentTimeLabel = document.getElementById('current-time'); | |
| const totalTimeLabel = document.getElementById('total-time'); | |
| const progressOverlay = document.getElementById('progress-overlay'); | |
| const progressFill = document.getElementById('progress-fill'); | |
| let sourceImageObj = new Image(); | |
| let isProcessing = false; | |
| let extractedFrames = []; | |
| let videoDuration = 0; | |
| // --- Event Listeners --- | |
| // Source Image Upload | |
| sourceInput.addEventListener('change', (e) => { | |
| const file = e.target.files[0]; | |
| if (file) { | |
| const reader = new FileReader(); | |
| reader.onload = (event) => { | |
| sourceImageObj.src = event.target.result; | |
| sourcePreview.src = event.target.result; | |
| sourcePreview.style.display = 'block'; | |
| sourceZone.classList.add('active'); | |
| sourceInfo.innerText = file.name; | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| }); | |
| // Target Video Upload | |
| targetInput.addEventListener('change', (e) => { | |
| const file = e.target.files[0]; | |
| if (file) { | |
| const url = URL.createObjectURL(file); | |
| mainVideo.src = url; | |
| targetInfo.innerText = file.name; | |
| mainVideo.onloadeddata = () => { | |
| videoDuration = mainVideo.duration; | |
| totalTimeLabel.innerText = formatTime(videoDuration); | |
| generateTimelineThumbnails(); | |
| }; | |
| } | |
| }); | |
| // Video Time Update | |
| mainVideo.addEventListener('timeupdate', () => { | |
| currentTimeLabel.innerText = formatTime(mainVideo.currentTime); | |
| highlightCurrentFrame(); | |
| }); | |
| // --- Functions --- | |
| function formatTime(seconds) { | |
| if(isNaN(seconds)) return "00:00"; | |
| const m = Math.floor(seconds / 60); | |
| const s = Math.floor(seconds % 60); | |
| return `${m}:${s < 10 ? '0' : ''}${s}`; | |
| } | |
| // Generate Timeline Thumbnails | |
| function generateTimelineThumbnails() { | |
| timelineTrack.innerHTML = ''; | |
| extractedFrames = []; | |
| const duration = mainVideo.duration; | |
| const interval = 0.5; // Generate thumb every 0.5s | |
| const totalFrames = Math.floor(duration / interval); | |
| frameCountLabel.innerText = `${totalFrames} Frames`; | |
| // We use a temporary canvas to grab frames from the video | |
| const tempCanvas = document.createElement('canvas'); | |
| tempCanvas.width = 70; | |
| tempCanvas.height = 90; | |
| const ctx = tempCanvas.getContext('2d'); | |
| for (let i = 0; i < totalFrames; i++) { | |
| const thumb = document.createElement('div'); | |
| thumb.className = 'frame-thumb'; | |
| thumb.dataset.index = i; | |
| thumb.dataset.time = i * interval; | |
| // Capture frame logic | |
| mainVideo.currentTime = i * interval; | |
| mainVideo.onseeked = () => { | |
| ctx.drawImage(mainVideo, 0, 0, 70, 90, 0, 0, 70, 90); | |
| thumb.style.backgroundImage = `url(${tempCanvas.toDataURL()})`; | |
| thumb.style.backgroundSize = 'cover'; | |
| }; | |
| const frameNum = document.createElement('div'); | |
| frameNum.className = 'frame-number'; | |
| frameNum.innerText = i; | |
| thumb.appendChild(frameNum); | |
| thumb.addEventListener('click', () => { | |
| mainVideo.currentTime = i * interval; | |
| mainVideo.play(); | |
| }); | |
| timelineTrack.appendChild(thumb); | |
| extractedFrames.push(thumb); | |
| } | |
| } | |
| function highlightCurrentFrame() { | |
| const currentSec = mainVideo.currentTime; | |
| const thumbIndex = Math.floor(currentSec / 0.5); | |
| extractedFrames.forEach(t => t.classList.remove('active')); | |
| if(extractedFrames[thumbIndex]) { | |
| extractedFrames[thumbIndex].classList.add('active'); | |
| } | |
| } | |
| // --- The "Swap" Logic (Simulation) --- | |
| // Since we cannot run heavy Python/DeepLearning in a single HTML file, | |
| // we simulate the UI experience of processing frames and perform a basic overlay. | |
| function startSwapProcess() { | |
| if (!sourceImageObj.src || !mainVideo.src) { | |
| alert("Please upload both a Source Face and a Target Video."); | |
| return; | |
| } | |
| if (isProcessing) return; | |
| isProcessing = true; | |
| // Show Progress Overlay | |
| progressOverlay.classList.add('visible'); | |
| processingStatus.classList.add('processing'); | |
| processingStatus.innerHTML = `<i class="fa-solid fa-circle-notch fa-spin"></i> Processing...`; | |
| mainVideo.pause(); | |
| // Hide video, show canvas for "processing" | |
| mainVideo.style.display = 'none'; | |
| processCanvas.style.display = 'block'; | |
| // Setup canvas dimensions (using fixed size for demo stability) | |
| processCanvas.width = 640; | |
| processCanvas.height = 360; | |
| const ctx = processCanvas.getContext('2d'); | |
| let frameIndex = 0; | |
| const totalFrames = extractedFrames.length || 100; // Default fallback if empty | |
| const speed = 50; // ms per frame (fast for demo) | |
| // Animation Loop | |
| const interval = setInterval(() => { | |
| // 1. Clear | |
| ctx.clearRect(0, 0, processCanvas.width, processCanvas.height); | |
| // 2. Draw "Video Frame" (Simulated background) | |
| ctx.fillStyle = '#1a1a24'; | |
| ctx.fillRect(0, 0, processCanvas.width, processCanvas.height); | |
| // 3. Draw "Swapped Face" (The Source Image) | |
| if(sourceImageObj) { | |
| const imgSize = 150; | |
| const x = (processCanvas.width / 2) - (imgSize / 2); | |
| const y = (processCanvas.height / 2) - (imgSize / 2); | |
| // Simulate "AI Tracking" by slightly moving the face based on sine wave | |
| const offset = Math.sin(frameIndex * 0.1) * 10; | |
| ctx.drawImage(sourceImageObj, x + offset, y + offset, imgSize, imgSize); | |
| // Add a "Scanline" effect to look like processing | |
| ctx.fillStyle = `rgba(99, 66, 241, 0.2)`; | |
| ctx.fillRect(0, frameIndex % processCanvas.height, processCanvas.width, 2); | |
| } | |
| // Update Timeline UI | |
| if(extractedFrames[frameIndex]) { | |
| extractedFrames[frameIndex].classList.add('processed'); | |
| extractedFrames[frameIndex].scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); | |
| } | |
| frameIndex++; | |
| // Update UI progress | |
| const progress = Math.min((frameIndex / totalFrames) * 100, 100); | |
| progressFill.style.width = `${progress}%`; | |
| processingStatus.innerHTML = `<i class="fa-solid fa-circle-notch fa-spin"></i> Processing ${Math.floor(progress)}%`; | |
| if (frameIndex >= totalFrames) { | |
| clearInterval(interval); | |
| finishProcessing(); | |
| } | |
| }, speed); | |
| } | |
| function finishProcessing() { | |
| isProcessing = false; | |
| processingStatus.classList.remove('processing'); | |
| processingStatus.innerHTML = `<i class="fa-solid fa-check"></i> Completed`; | |
| processingStatus.style.borderColor = "#4ade80"; | |
| processingStatus.style.color = "#4ade80"; | |
| // Hide overlay | |
| progressOverlay.classList.remove('visible'); | |
| // Reset view | |
| setTimeout(() => { | |
| processCanvas.style.display = 'none'; | |
| mainVideo.style.display = 'block'; | |
| mainVideo.play(); | |
| processingStatus.style.borderColor = "var(--border)"; | |
| processingStatus.style.color = "var(--text-main)"; | |
| processingStatus.innerHTML = `<i class="fa-solid fa-circle"></i> Ready`; | |
| // Reset progress bar | |
| progressFill.style.width = '0%'; | |
| }, 1500); | |
| } | |
| function resetApp() { | |
| location.reload(); | |
| } | |
| </script> | |
| </body> | |
| </html> |