Spaces:
Running
Running
| // ============================= | |
| // PornFlux I2V β Main Script | |
| // ============================= | |
| document.addEventListener('DOMContentLoaded', () => { | |
| lucide.createIcons(); | |
| // βββ State βββ | |
| const state = { | |
| duration: 5, | |
| resolution: '1080', | |
| count: 1, | |
| motion: 50, | |
| camera: 'pan', | |
| seed: '', | |
| negPrompt: '', | |
| strength: 70, | |
| audio: 'auto', | |
| model: 'wan25-pro', | |
| style: 'cinematic', | |
| aspect: '16:9', | |
| lighting: 'natural', | |
| fps: '30', | |
| nsfw: 75, | |
| faceEnhance: true, | |
| detailUpscale: false, | |
| uploadedImage: null, | |
| isGenerating: false, | |
| }; | |
| // βββ Surprise Prompts (explicit, bilingual) βββ | |
| const surprisePrompts = [ | |
| "Close-up POV of passionate missionary sex, slow deep thrusts, sweaty skin glistening, intimate eye contact, cinematic lighting", | |
| "POV fellation, caméra subjective, lèvres humides, regard séducteur vers la caméra, mouvement lent et sensuel", | |
| "Doggy style from behind, POV rear angle, rhythmic hip slapping, arched back, hands gripping the sheets, moaning", | |
| "Levrette vue de derrière, angled bas, frappes rythmiques, dos cambré, mains agrippant les draps, gémissements", | |
| "Cowgirl riding, close-up hip movement, slow grinding, breasts bouncing, hair swaying, erotic expression", | |
| "Premier plan, chatte sur le visage, chevauchement lent, regards intenses, Γ©clairage bougie, ambiance intime", | |
| "Erotic lap dance, slow striptease reveal, sensual hip grinding, intimate contact, dim bedroom lighting", | |
| "Sperme facial, finission explosive, gros plan, gouttelettes lentes, rΓ©action de plaisir, ralenti cinΓ©matique", | |
| "Threesome on silk sheets, multiple bodies intertwined, passionate kissing, switching positions, warm golden light", | |
| "Trio lesbien, baisers passionnΓ©s, caresses intimes, corps enlacΓ©s sur des draps en soie, Γ©clairage doux", | |
| "Shower sex scene, water cascading, steamy glass, pressed against tiles, wet hair, intense pounding", | |
| "Sous la douche, eau coulant sur la peau, carrelage, cheveux mouillΓ©s, pΓ©nΓ©tration intense, buΓ©e", | |
| "Sensual massage turning explicit, oil on skin, hands exploring every inch, erotic build-up, soft moaning", | |
| "Massage sensuelεζΓ©rotique, huile sur la peau, mains explorant chaque centimΓ¨tre, escalation du dΓ©sir", | |
| "Bound in silk restraints, submission, blindfolded pleasure, teasing, slow anticipation, fetish lighting", | |
| "Lingerie tease, stockings and garters, slow reveal, high heels, mirror reflection, boudoir aesthetic", | |
| "DΓ©shabillagePin-up, bas et porte-jarretelles, lente rΓ©vΓ©lation, talons hauts, reflets dans le miroir", | |
| "Outdoor sex on the beach at sunset, sand, crashing waves, passionate, wild, golden hour lighting", | |
| "Public restroom quickie, urgency, skirt hiked up, biting lip to stay quiet, fluorescent light flickering", | |
| "Sextape aesthetic, amateur handheld camera feel, real girlfriend experience, genuine pleasure, authentic audio", | |
| ]; | |
| let promptIndex = 0; | |
| // βββ Prompt Gallery Data βββ | |
| const promptGalleryData = [ | |
| { | |
| category: 'Cinematic', | |
| color: 'flux', | |
| prompt: 'Cinematic slow motion, dramatic backlighting, sensual movement, film grain, noir aesthetic', | |
| image: 'http://static.photos/aerial/320x240/10', | |
| }, | |
| { | |
| category: 'Sensual', | |
| color: 'violet', | |
| prompt: 'Soft candlelight, silk and skin, intimate boudoir, warm color grading, slow pan', | |
| image: 'http://static.photos/minimal/320x240/22', | |
| }, | |
| { | |
| category: 'Artistic', | |
| color: 'flux', | |
| prompt: 'Nu artistique, Γ©clairage de galerie, poses inspirΓ©es de la sculpture classique, rotation lente', | |
| image: 'http://static.photos/abstract/320x240/15', | |
| }, | |
| { | |
| category: 'Romantic', | |
| color: 'violet', | |
| prompt: 'Romance au coucher de soleil, lumière dorée, regard tendre, mouvement délicat, ambiance chaude', | |
| image: 'http://static.photos/nature/320x240/33', | |
| }, | |
| { | |
| category: 'Dark & Moody', | |
| color: 'flux', | |
| prompt: 'Dark neon-lit room, slow reveal, shadows and highlights, urban night aesthetic, moody atmosphere', | |
| image: 'http://static.photos/technology/320x240/7', | |
| }, | |
| { | |
| category: 'Glamour', | |
| color: 'violet', | |
| prompt: 'Glamour photo shoot motion, professional lighting, beauty close-ups, editorial style, slow dolly', | |
| image: 'http://static.photos/workspace/320x240/44', | |
| }, | |
| ]; | |
| // βββ Video Gallery Data βββ | |
| const videoGalleryData = [ | |
| { image: 'http://static.photos/nature/640x360/1', label: 'Doggy POV' }, | |
| { image: 'http://static.photos/abstract/640x360/3', label: 'BJ Close-up' }, | |
| { image: 'http://static.photos/minimal/640x360/5', label: 'Cowgirl' }, | |
| { image: 'http://static.photos/aerial/640x360/7', label: 'Missionary' }, | |
| { image: 'http://static.photos/technology/640x360/9', label: 'Threesome' }, | |
| { image: 'http://static.photos/workspace/640x360/11', label: 'Striptease' }, | |
| { image: 'http://static.photos/nature/640x360/13', label: 'Facial' }, | |
| { image: 'http://static.photos/abstract/640x360/15', label: 'Shower Sex' }, | |
| ]; | |
| // βββ DOM Elements βββ | |
| const uploadArea = document.getElementById('uploadArea'); | |
| const uploadPlaceholder = document.getElementById('uploadPlaceholder'); | |
| const uploadPreview = document.getElementById('uploadPreview'); | |
| const previewImage = document.getElementById('previewImage'); | |
| const removeImageBtn = document.getElementById('removeImageBtn'); | |
| const imageInfo = document.getElementById('imageInfo'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const promptInput = document.getElementById('promptInput'); | |
| const charCount = document.getElementById('charCount'); | |
| const surpriseBtn = document.getElementById('surpriseBtn'); | |
| const generateBtn = document.getElementById('generateBtn'); | |
| const loadingState = document.getElementById('loadingState'); | |
| const progressBar = document.getElementById('progressBar'); | |
| const progressPercent = document.getElementById('progressPercent'); | |
| const progressETA = document.getElementById('progressETA'); | |
| const loadingText = document.getElementById('loadingText'); | |
| const loadingSubtext = document.getElementById('loadingSubtext'); | |
| const resultsArea = document.getElementById('resultsArea'); | |
| const resultsGrid = document.getElementById('resultsGrid'); | |
| const clearResultsBtn = document.getElementById('clearResultsBtn'); | |
| const advancedToggle = document.getElementById('advancedToggle'); | |
| const advancedPanel = document.getElementById('advancedPanel'); | |
| const advancedChevron = document.getElementById('advancedChevron'); | |
| const motionSlider = document.getElementById('motionSlider'); | |
| const motionVal = document.getElementById('motionVal'); | |
| const strengthSlider = document.getElementById('strengthSlider'); | |
| const strengthVal = document.getElementById('strengthVal'); | |
| const seedInput = document.getElementById('seedInput'); | |
| const randomSeedBtn = document.getElementById('randomSeedBtn'); | |
| const enhanceBtn = document.getElementById('enhanceBtn'); | |
| const nsfwSlider = document.getElementById('nsfwSlider'); | |
| const nsfwVal = document.getElementById('nsfwVal'); | |
| const faceEnhance = document.getElementById('faceEnhance'); | |
| const detailUpscale = document.getElementById('detailUpscale'); | |
| const mobileMenuBtn = document.getElementById('mobileMenuBtn'); | |
| const mobileMenu = document.getElementById('mobileMenu'); | |
| const header = document.getElementById('header'); | |
| // ββ Enhance Prompt Keywords βββ | |
| const enhanceKeywords = [ | |
| '8K ultra HD', 'hyperrealistic', 'detailed skin texture', 'cinematic depth of field', | |
| 'soft ambient lighting', 'wet skin glistening', 'realistic body physics', | |
| 'natural movement', 'steady camera', 'professional color grading', | |
| 'film grain', 'moody shadows', 'realistic anatomy', 'explicit detail', | |
| 'photorealistic rendering', 'smooth motion', 'natural body proportions', | |
| ]; | |
| // βββ Initialize βββ | |
| init(); | |
| function init() { | |
| renderPromptGallery(); | |
| renderVideoGallery(); | |
| setupButtonGroups(); | |
| setupSliders(); | |
| setupUpload(); | |
| setupSurprise(); | |
| setupEnhance(); | |
| setupPromptTags(); | |
| setupAdvanced(); | |
| setupGenerate(); | |
| setupHeader(); | |
| setupMobileMenu(); | |
| setupNavLinks(); | |
| setupClearResults(); | |
| } | |
| // βββ Prompt Gallery βββ | |
| function renderPromptGallery() { | |
| const container = document.getElementById('promptGallery'); | |
| container.innerHTML = promptGalleryData.map((item, i) => ` | |
| <div class="prompt-card rounded-xl sm:rounded-2xl bg-dark-900/60 border border-dark-700 overflow-hidden group" data-prompt="${encodeURIComponent(item.prompt)}"> | |
| <div class="aspect-video relative overflow-hidden"> | |
| <img src="${item.image}" alt="${item.category}" class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700" loading="lazy"> | |
| <div class="absolute inset-0 bg-gradient-to-t from-dark-950/80 via-transparent to-transparent"></div> | |
| <div class="absolute top-3 left-3"> | |
| <span class="px-2 py-1 rounded-md text-[10px] font-bold uppercase tracking-wider ${item.color === 'flux' ? 'bg-flux-500/20 text-flux-400' : 'bg-violet-500/20 text-violet-400'}">${item.category}</span> | |
| </div> | |
| </div> | |
| <div class="p-4"> | |
| <p class="text-sm text-dark-300 line-clamp-2 leading-relaxed">${item.prompt}</p> | |
| <div class="mt-3 flex items-center gap-2 text-xs text-flux-400 opacity-0 group-hover:opacity-100 transition-opacity"> | |
| <i data-lucide="arrow-right" class="w-3 h-3"></i> | |
| <span>Use this prompt</span> | |
| </div> | |
| </div> | |
| </div> | |
| `).join(''); | |
| // Click to fill prompt | |
| container.querySelectorAll('.prompt-card').forEach(card => { | |
| card.addEventListener('click', () => { | |
| promptInput.value = decodeURIComponent(card.dataset.prompt); | |
| updateCharCount(); | |
| document.getElementById('generator').scrollIntoView({ behavior: 'smooth' }); | |
| showToast('Prompt loaded!', 'success'); | |
| }); | |
| }); | |
| lucide.createIcons(); | |
| } | |
| // βββ Video Gallery βββ | |
| function renderVideoGallery() { | |
| const container = document.getElementById('videoGallery'); | |
| container.innerHTML = videoGalleryData.map((item, i) => ` | |
| <div class="gallery-card aspect-video"> | |
| <img src="${item.image}" alt="${item.label}" class="w-full h-full object-cover" loading="lazy"> | |
| <div class="gallery-overlay"> | |
| <div class="flex items-center gap-2"> | |
| <div class="w-8 h-8 rounded-full bg-flux-500/90 flex items-center justify-center"> | |
| <i data-lucide="play" class="w-4 h-4 text-white fill-white"></i> | |
| </div> | |
| <span class="text-sm font-semibold text-white">${item.label}</span> | |
| </div> | |
| </div> | |
| </div> | |
| `).join(''); | |
| lucide.createIcons(); | |
| } | |
| // βββ Button Groups βββ | |
| function setupButtonGroups() { | |
| // Duration | |
| document.querySelectorAll('#durationBtns .duration-btn').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| document.querySelectorAll('#durationBtns .duration-btn').forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| state.duration = parseInt(btn.dataset.value); | |
| }); | |
| }); | |
| // Resolution | |
| document.querySelectorAll('#resolutionBtns .resolution-btn').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| document.querySelectorAll('#resolutionBtns .resolution-btn').forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| state.resolution = btn.dataset.value; | |
| }); | |
| }); | |
| // Count | |
| document.querySelectorAll('#countBtns .count-btn').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| document.querySelectorAll('#countBtns .count-btn').forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| state.count = parseInt(btn.dataset.value); | |
| }); | |
| }); | |
| // Camera | |
| document.querySelectorAll('#cameraBtns .camera-btn').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| document.querySelectorAll('#cameraBtns .camera-btn').forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| state.camera = btn.dataset.value; | |
| }); | |
| }); | |
| // Audio | |
| document.querySelectorAll('#audioBtns .audio-btn').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| document.querySelectorAll('#audioBtns .audio-btn').forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| state.audio = btn.dataset.value; | |
| }); | |
| }); | |
| // Model | |
| document.querySelectorAll('#modelBtns .model-btn').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| document.querySelectorAll('#modelBtns .model-btn').forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| state.model = btn.dataset.value; | |
| }); | |
| }); | |
| // Style | |
| document.querySelectorAll('#styleBtns .style-btn').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| document.querySelectorAll('#styleBtns .style-btn').forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| state.style = btn.dataset.value; | |
| }); | |
| }); | |
| // Aspect Ratio | |
| document.querySelectorAll('#aspectBtns .aspect-btn').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| document.querySelectorAll('#aspectBtns .aspect-btn').forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| state.aspect = btn.dataset.value; | |
| }); | |
| }); | |
| // Lighting | |
| document.querySelectorAll('#lightingBtns .lighting-btn').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| document.querySelectorAll('#lightingBtns .lighting-btn').forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| state.lighting = btn.dataset.value; | |
| }); | |
| }); | |
| // FPS | |
| document.querySelectorAll('#fpsBtns .fps-btn').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| document.querySelectorAll('#fpsBtns .fps-btn').forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| state.fps = btn.dataset.value; | |
| }); | |
| }); | |
| } | |
| // βββ Sliders βββ | |
| function setupSliders() { | |
| motionSlider.addEventListener('input', () => { | |
| state.motion = parseInt(motionSlider.value); | |
| motionVal.textContent = state.motion + '%'; | |
| }); | |
| strengthSlider.addEventListener('input', () => { | |
| state.strength = parseInt(strengthSlider.value); | |
| strengthVal.textContent = state.strength + '%'; | |
| }); | |
| if (nsfwSlider) { | |
| nsfwSlider.addEventListener('input', () => { | |
| state.nsfw = parseInt(nsfwSlider.value); | |
| nsfwVal.textContent = state.nsfw + '%'; | |
| }); | |
| } | |
| randomSeedBtn.addEventListener('click', () => { | |
| seedInput.value = Math.floor(Math.random() * 999999999); | |
| state.seed = seedInput.value; | |
| }); | |
| if (faceEnhance) { | |
| faceEnhance.addEventListener('change', () => { | |
| state.faceEnhance = faceEnhance.checked; | |
| }); | |
| } | |
| if (detailUpscale) { | |
| detailUpscale.addEventListener('change', () => { | |
| state.detailUpscale = detailUpscale.checked; | |
| }); | |
| } | |
| } | |
| // βββ Upload βββ | |
| function setupUpload() { | |
| // Click to upload | |
| uploadArea.addEventListener('click', (e) => { | |
| if (e.target.closest('#removeImageBtn')) return; | |
| if (!state.uploadedImage) fileInput.click(); | |
| }); | |
| // Drag & Drop | |
| uploadArea.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| uploadArea.classList.add('dragover'); | |
| }); | |
| uploadArea.addEventListener('dragleave', () => { | |
| uploadArea.classList.remove('dragover'); | |
| }); | |
| uploadArea.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| uploadArea.classList.remove('dragover'); | |
| const file = e.dataTransfer.files[0]; | |
| if (file) handleFile(file); | |
| }); | |
| // File input change | |
| fileInput.addEventListener('change', (e) => { | |
| const file = e.target.files[0]; | |
| if (file) handleFile(file); | |
| }); | |
| // Remove image | |
| removeImageBtn.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| state.uploadedImage = null; | |
| uploadPreview.classList.add('hidden'); | |
| uploadPlaceholder.classList.remove('hidden'); | |
| fileInput.value = ''; | |
| }); | |
| } | |
| function handleFile(file) { | |
| const validTypes = ['image/png', 'image/jpeg', 'image/webp']; | |
| if (!validTypes.includes(file.type)) { | |
| showToast('Please upload a PNG, JPG, or WebP image.', 'error'); | |
| return; | |
| } | |
| const maxSize = 20 * 1024 * 1024; // 20MB | |
| if (file.size > maxSize) { | |
| showToast('Image must be under 20MB.', 'error'); | |
| return; | |
| } | |
| state.uploadedImage = file; | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| previewImage.src = e.target.result; | |
| uploadPlaceholder.classList.add('hidden'); | |
| uploadPreview.classList.remove('hidden'); | |
| // Get image dimensions | |
| const img = new Image(); | |
| img.onload = () => { | |
| imageInfo.textContent = `${file.name} Β· ${img.width}Γ${img.height} Β· ${(file.size / 1024).toFixed(0)}KB`; | |
| lucide.createIcons(); | |
| }; | |
| img.src = e.target.result; | |
| }; | |
| reader.readAsDataURL(file); | |
| showToast('Image uploaded successfully!', 'success'); | |
| } | |
| // ββ Surprise Me βββ | |
| function setupSurprise() { | |
| surpriseBtn.addEventListener('click', () => { | |
| const prompt = surprisePrompts[promptIndex % surprisePrompts.length]; | |
| promptInput.value = prompt; | |
| promptIndex++; | |
| updateCharCount(); | |
| showToast('π² Random filthy prompt loaded!', 'success'); | |
| }); | |
| } | |
| // ββ Enhance Prompt βββ | |
| function setupEnhance() { | |
| if (enhanceBtn) { | |
| enhanceBtn.addEventListener('click', () => { | |
| if (!promptInput.value.trim()) { | |
| showToast('Enter a prompt first to enhance it!', 'error'); | |
| promptInput.focus(); | |
| return; | |
| } | |
| // Add random enhance keywords | |
| const shuffled = enhanceKeywords.sort(() => 0.5 - Math.random()); | |
| const numKeywords = 3 + Math.floor(Math.random() * 3); | |
| const keywords = shuffled.slice(0, numKeywords).join(', '); | |
| promptInput.value = promptInput.value.trim() + ', ' + keywords; | |
| updateCharCount(); | |
| showToast('π₯ Prompt enhanced with detail keywords!', 'success'); | |
| }); | |
| } | |
| } | |
| // ββ Prompt Tags βββ | |
| function setupPromptTags() { | |
| document.querySelectorAll('.prompt-tag').forEach(tag => { | |
| tag.addEventListener('click', () => { | |
| const additionalText = tag.dataset.prompt; | |
| if (promptInput.value.trim()) { | |
| promptInput.value = promptInput.value.trim() + ', ' + additionalText; | |
| } else { | |
| promptInput.value = additionalText; | |
| } | |
| updateCharCount(); | |
| showToast('β Tag added to prompt', 'success'); | |
| }); | |
| }); | |
| } | |
| // βββ Character Count βββ | |
| promptInput.addEventListener('input', updateCharCount); | |
| function updateCharCount() { | |
| const len = promptInput.value.length; | |
| charCount.textContent = `${len} / 2000`; | |
| if (len > 1800) charCount.classList.add('text-flux-400'); | |
| else charCount.classList.remove('text-flux-400'); | |
| } | |
| // βββ Advanced Panel βββ | |
| function setupAdvanced() { | |
| advancedToggle.addEventListener('click', () => { | |
| const isOpen = !advancedPanel.classList.contains('hidden'); | |
| if (isOpen) { | |
| advancedPanel.classList.add('hidden'); | |
| advancedChevron.style.transform = 'rotate(0deg)'; | |
| } else { | |
| advancedPanel.classList.remove('hidden'); | |
| advancedChevron.style.transform = 'rotate(180deg)'; | |
| lucide.createIcons(); | |
| } | |
| }); | |
| } | |
| // βββ Generate βββ | |
| function setupGenerate() { | |
| generateBtn.addEventListener('click', startGeneration); | |
| } | |
| async function startGeneration() { | |
| if (state.isGenerating) return; | |
| if (!state.uploadedImage) { | |
| showToast('Please upload an image first!', 'error'); | |
| uploadArea.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| return; | |
| } | |
| if (!promptInput.value.trim()) { | |
| showToast('Please enter a prompt!', 'error'); | |
| promptInput.focus(); | |
| return; | |
| } | |
| state.isGenerating = true; | |
| // Show loading | |
| loadingState.classList.remove('hidden'); | |
| resultsArea.classList.add('hidden'); | |
| generateBtn.classList.add('generate-loading'); | |
| generateBtn.innerHTML = ` | |
| <svg class="animate-spin w-5 h-5 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg> | |
| Generating... | |
| `; | |
| const steps = [ | |
| { text: 'Analyzing source image...', sub: 'Extracting visual features', pct: 10 }, | |
| { text: 'Processing prompt...', sub: 'Interpreting motion instructions', pct: 25 }, | |
| { text: 'Initializing Wan 2.5 engine...', sub: 'Loading uncensored model weights', pct: 35 }, | |
| { text: 'Generating video frames...', sub: 'Rendering keyframes', pct: 55 }, | |
| { text: 'Adding motion dynamics...', sub: `Camera: ${state.camera} | Style: ${state.style} | NSFW: ${state.nsfw}%`, pct: 70 }, | |
| { text: 'Rendering audio track...', sub: `Audio mode: ${state.audio} | Lighting: ${state.lighting}`, pct: 82 }, | |
| { text: 'Post-processing...', sub: `Upscaling to ${getResLabel()} | ${state.fps}fps`, pct: 92 }, | |
| { text: 'Finalizing...', sub: 'Almost there!', pct: 98 }, | |
| ]; | |
| let stepIndex = 0; | |
| const loadingInterval = setInterval(() => { | |
| if (stepIndex < steps.length) { | |
| const step = steps[stepIndex]; | |
| loadingText.textContent = step.text; | |
| loadingSubtext.textContent = step.sub; | |
| progressBar.style.width = step.pct + '%'; | |
| progressPercent.textContent = step.pct + '%'; | |
| const etaMin = state.duration > 10 ? '1-2 min' : '~30s'; | |
| progressETA.textContent = `ETA: ${etaMin}`; | |
| stepIndex++; | |
| } | |
| }, 1200); | |
| // Final progress | |
| setTimeout(() => { | |
| clearInterval(loadingInterval); | |
| progressBar.style.width = '100%'; | |
| progressPercent.textContent = '100%'; | |
| loadingText.textContent = 'Complete!'; | |
| loadingSubtext.textContent = 'Your video is ready'; | |
| setTimeout(() => { | |
| state.isGenerating = false; | |
| loadingState.classList.add('hidden'); | |
| generateBtn.classList.remove('generate-loading'); | |
| generateBtn.innerHTML = ` | |
| <i data-lucide="wand-2" class="w-5 h-5 sm:w-6 sm:h-6 relative z-10"></i> | |
| <span class="relative z-10">GENERATE VIDEO</span> | |
| `; | |
| lucide.createIcons(); | |
| showResults(); | |
| }, 600); | |
| }, steps.length * 1200 + 500); | |
| } | |
| function getResLabel() { | |
| const map = { '720': '720p', '1080': '1080p', '4k': '4K' }; | |
| return map[state.resolution] || '1080p'; | |
| } | |
| function showResults() { | |
| resultsArea.classList.remove('hidden'); | |
| resultsGrid.innerHTML = ''; | |
| const count = state.count; | |
| for (let i = 0; i < count; i++) { | |
| const seed = state.seed || Math.floor(Math.random() * 999999); | |
| const imgSeed = Math.floor(Math.random() * 999); | |
| const card = document.createElement('div'); | |
| card.className = 'video-result-card animate-fadeIn'; | |
| card.style.animationDelay = `${i * 0.1}s`; | |
| card.innerHTML = ` | |
| <div class="aspect-video relative bg-dark-950 overflow-hidden group"> | |
| <img src="http://static.photos/nature/640x360/${imgSeed}" alt="Generated Video ${i + 1}" class="w-full h-full object-cover"> | |
| <div class="absolute inset-0 bg-dark-950/30 group-hover:bg-dark-950/10 transition-all duration-300 flex items-center justify-center"> | |
| <div class="w-14 h-14 rounded-full bg-flux-500/90 flex items-center justify-center shadow-lg shadow-flux-500/30 cursor-pointer hover:scale-110 transition-transform"> | |
| <i data-lucide="play" class="w-6 h-6 text-white fill-white ml-1"></i> | |
| </div> | |
| </div> | |
| <div class="absolute top-2 right-2 px-2 py-1 rounded bg-dark-900/80 backdrop-blur text-[10px] font-mono text-dark-300"> | |
| ${state.duration}s β’ ${getResLabel()} | |
| </div> | |
| <div class="absolute bottom-2 left-2 flex items-center gap-1"> | |
| <div class="w-2 h-2 rounded-full bg-green-400 animate-pulse"></div> | |
| <span class="text-[10px] text-dark-300 font-medium">AI Generated</span> | |
| </div> | |
| </div> | |
| <div class="p-3 flex items-center justify-between"> | |
| <div class="text-xs text-dark-500 font-mono">Seed: ${seed}</div> | |
| <div class="flex items-center gap-2"> | |
| <button class="p-2 rounded-lg bg-dark-850 border border-dark-700 hover:border-flux-500/40 hover:bg-flux-500/10 transition-all" title="Download"> | |
| <i data-lucide="download" class="w-3.5 h-3.5 text-dark-400"></i> | |
| </button> | |
| <button class="p-2 rounded-lg bg-dark-850 border border-dark-700 hover:border-violet-500/40 hover:bg-violet-500/10 transition-all" title="Regenerate with variation"> | |
| <i data-lucide="refresh-cw" class="w-3.5 h-3.5 text-dark-400"></i> | |
| </button> | |
| <button class="p-2 rounded-lg bg-dark-850 border border-dark-700 hover:border-flux-500/40 hover:bg-flux-500/10 transition-all" title="Share"> | |
| <i data-lucide="share-2" class="w-3.5 h-3.5 text-dark-400"></i> | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| resultsGrid.appendChild(card); | |
| } | |
| lucide.createIcons(); | |
| resultsArea.scrollIntoView({ behavior: 'smooth', block: 'start' }); | |
| showToast(`${count} video${count > 1 ? 's' : ''} generated successfully!`, 'success'); | |
| } | |
| // βββ Clear Results βββ | |
| function setupClearResults() { | |
| clearResultsBtn.addEventListener('click', () => { | |
| resultsArea.classList.add('hidden'); | |
| resultsGrid.innerHTML = ''; | |
| }); | |
| } | |
| // βββ Header Scroll βββ | |
| function setupHeader() { | |
| let lastScroll = 0; | |
| window.addEventListener('scroll', () => { | |
| const scrollY = window.scrollY; | |
| if (scrollY > 50) { | |
| header.classList.add('header-scrolled'); | |
| } else { | |
| header.classList.remove('header-scrolled'); | |
| } | |
| lastScroll = scrollY; | |
| }); | |
| } | |
| // βββ Mobile Menu βββ | |
| function setupMobileMenu() { | |
| mobileMenuBtn.addEventListener('click', () => { | |
| mobileMenu.classList.toggle('hidden'); | |
| }); | |
| // Close on link click | |
| mobileMenu.querySelectorAll('a').forEach(link => { | |
| link.addEventListener('click', () => { | |
| mobileMenu.classList.add('hidden'); | |
| }); | |
| }); | |
| } | |
| // βββ Nav Links βββ | |
| function setupNavLinks() { | |
| const sections = ['home', 'generator', 'examples', 'about']; | |
| const navLinks = document.querySelectorAll('.nav-link'); | |
| window.addEventListener('scroll', () => { | |
| let current = ''; | |
| sections.forEach(id => { | |
| const section = document.getElementById(id); | |
| if (section) { | |
| const sectionTop = section.offsetTop - 100; | |
| if (window.scrollY >= sectionTop) current = id; | |
| } | |
| }); | |
| navLinks.forEach(link => { | |
| const href = link.getAttribute('href').slice(1); | |
| if (href === current) link.classList.add('active'); | |
| else link.classList.remove('active'); | |
| }); | |
| }); | |
| } | |
| // βββ Toast Notifications βββ | |
| function showToast(message, type = 'info') { | |
| const container = document.getElementById('toastContainer'); | |
| const toast = document.createElement('div'); | |
| const colors = { | |
| success: 'border-green-500/50 bg-green-500/10 text-green-400', | |
| error: 'border-flux-500/50 bg-flux-500/10 text-flux-400', | |
| info: 'border-violet-500/50 bg-violet-500/10 text-violet-400', | |
| }; | |
| const icons = { | |
| success: 'check-circle', | |
| error: 'alert-circle', | |
| info: 'info', | |
| }; | |
| toast.className = `toast-enter flex items-center gap-2 px-4 py-3 rounded-xl border backdrop-blur-xl text-sm font-medium shadow-lg ${colors[type] || colors.info}`; | |
| toast.innerHTML = `<i data-lucide="${icons[type] || 'info'}" class="w-4 h-4 shrink-0"></i><span>${message}</span>`; | |
| container.appendChild(toast); | |
| lucide.createIcons(); | |
| setTimeout(() => { | |
| toast.classList.remove('toast-enter'); | |
| toast.classList.add('toast-exit'); | |
| setTimeout(() => toast.remove(), 300); | |
| }, 3000); | |
| } | |
| }); |