Boka73's picture
Initial DeepSite commit
eeb0a40 verified
raw
history blame
27.7 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CineFlow Studio - Free Image to Video Generator</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/@ffmpeg/ffmpeg@0.12.7/dist/umd/ffmpeg.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@ffmpeg/util@0.12.1/dist/umd/index.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
body {
font-family: 'Inter', sans-serif;
background: #0a0a0a;
}
.glass-panel {
background: rgba(255, 255, 255, 0.03);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.gradient-text {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.drop-zone {
background: repeating-linear-gradient(
45deg,
rgba(99, 102, 241, 0.05),
rgba(99, 102, 241, 0.05) 10px,
rgba(99, 102, 241, 0.1) 10px,
rgba(99, 102, 241, 0.1) 20px
);
border: 2px dashed rgba(99, 102, 241, 0.3);
transition: all 0.3s ease;
}
.drop-zone.drag-over {
background: repeating-linear-gradient(
45deg,
rgba(99, 102, 241, 0.15),
rgba(99, 102, 241, 0.15) 10px,
rgba(99, 102, 241, 0.25) 10px,
rgba(99, 102, 241, 0.25) 20px
);
border-color: #6366f1;
transform: scale(1.02);
}
.timeline-item {
transition: all 0.2s ease;
}
.timeline-item:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
}
.progress-bar {
background: linear-gradient(90deg, #6366f1, #8b5cf6, #a855f7);
background-size: 200% 100%;
animation: gradient-shift 2s ease infinite;
}
@keyframes gradient-shift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.preview-container {
aspect-ratio: 16/9;
background: linear-gradient(135deg, #1a1a1a, #2d2d2d);
}
input[type="range"] {
-webkit-appearance: none;
appearance: none;
background: transparent;
cursor: pointer;
}
input[type="range"]::-webkit-slider-track {
background: rgba(255,255,255,0.1);
height: 6px;
border-radius: 3px;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
background: #6366f1;
height: 16px;
width: 16px;
border-radius: 50%;
margin-top: -5px;
transition: all 0.2s;
}
input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.2);
box-shadow: 0 0 20px rgba(99, 102, 241, 0.5);
}
.frame-preview {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
}
</style>
</head>
<body class="text-white min-h-screen overflow-x-hidden">
<!-- Header -->
<header class="glass-panel sticky top-0 z-50 border-b border-white/10">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center">
<i data-lucide="film" class="w-5 h-5 text-white"></i>
</div>
<div>
<h1 class="text-xl font-bold tracking-tight">CineFlow <span class="gradient-text">Studio</span></h1>
<p class="text-xs text-gray-400">Client-side Video Generator</p>
</div>
</div>
<div class="flex items-center gap-4">
<span class="hidden sm:flex items-center gap-2 text-xs text-gray-400 bg-white/5 px-3 py-1.5 rounded-full">
<i data-lucide="cpu" class="w-3 h-3"></i>
<span id="cpu-status">Ready</span>
</span>
<button onclick="resetAll()" class="text-sm text-gray-400 hover:text-white transition-colors">
Reset
</button>
</div>
</div>
</header>
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="grid lg:grid-cols-3 gap-8">
<!-- Left Panel: Upload & Timeline -->
<div class="lg:col-span-2 space-y-6">
<!-- Upload Zone -->
<div class="glass-panel rounded-2xl p-6">
<div id="dropZone" class="drop-zone rounded-xl p-12 text-center cursor-pointer">
<input type="file" id="fileInput" multiple accept="image/*" class="hidden">
<div class="space-y-4">
<div class="w-16 h-16 mx-auto bg-indigo-500/20 rounded-2xl flex items-center justify-center">
<i data-lucide="upload-cloud" class="w-8 h-8 text-indigo-400"></i>
</div>
<div>
<p class="text-lg font-medium text-white">Drop images here or click to browse</p>
<p class="text-sm text-gray-400 mt-1">Supports JPG, PNG, WebP • Unlimited files • Private & Secure</p>
</div>
<button onclick="document.getElementById('fileInput').click()" class="inline-flex items-center gap-2 bg-indigo-600 hover:bg-indigo-700 text-white px-6 py-2.5 rounded-lg font-medium transition-all hover:scale-105">
<i data-lucide="plus" class="w-4 h-4"></i>
Select Images
</button>
</div>
</div>
</div>
<!-- Timeline -->
<div class="glass-panel rounded-2xl p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold flex items-center gap-2">
<i data-lucide="layers" class="w-5 h-5 text-indigo-400"></i>
Timeline
<span id="frameCount" class="text-xs bg-white/10 px-2 py-0.5 rounded-full text-gray-400">0 frames</span>
</h3>
<div class="flex gap-2">
<button onclick="clearAll()" class="text-xs text-red-400 hover:text-red-300 flex items-center gap-1">
<i data-lucide="trash-2" class="w-3 h-3"></i>
Clear
</button>
</div>
</div>
<div id="timeline" class="space-y-2 max-h-96 overflow-y-auto pr-2">
<div class="text-center py-12 text-gray-500">
<i data-lucide="image-off" class="w-12 h-12 mx-auto mb-3 opacity-20"></i>
<p>No images added yet</p>
</div>
</div>
</div>
</div>
<!-- Right Panel: Settings & Preview -->
<div class="space-y-6">
<!-- Preview Window -->
<div class="glass-panel rounded-2xl p-6">
<h3 class="text-lg font-semibold mb-4 flex items-center gap-2">
<i data-lucide="play-circle" class="w-5 h-5 text-indigo-400"></i>
Preview
</h3>
<div class="preview-container rounded-xl overflow-hidden relative bg-black">
<img id="previewImage" src="" alt="Preview" class="w-full h-full object-contain hidden">
<div id="previewPlaceholder" class="absolute inset-0 flex items-center justify-center text-gray-500">
<div class="text-center">
<i data-lucide="video" class="w-12 h-12 mx-auto mb-2 opacity-20"></i>
<p class="text-sm">Video preview will appear here</p>
</div>
</div>
<div id="previewOverlay" class="absolute inset-0 bg-black/50 hidden items-center justify-center">
<span class="text-2xl font-bold" id="previewCounter">1/10</span>
</div>
</div>
</div>
<!-- Settings -->
<div class="glass-panel rounded-2xl p-6 space-y-6">
<h3 class="text-lg font-semibold flex items-center gap-2">
<i data-lucide="settings" class="w-5 h-5 text-indigo-400"></i>
Export Settings
</h3>
<div class="space-y-4">
<div>
<label class="flex justify-between text-sm font-medium mb-2">
<span>Duration per image</span>
<span id="durationValue" class="text-indigo-400">2s</span>
</label>
<input type="range" id="duration" min="0.5" max="10" step="0.5" value="2" class="w-full">
</div>
<div>
<label class="flex justify-between text-sm font-medium mb-2">
<span>Frame Rate (FPS)</span>
<span id="fpsValue" class="text-indigo-400">30fps</span>
</label>
<input type="range" id="fps" min="15" max="60" step="15" value="30" class="w-full">
</div>
<div>
<label class="flex justify-between text-sm font-medium mb-2">
<span>Resolution</span>
<span id="resolutionValue" class="text-indigo-400">1920×1080</span>
</label>
<select id="resolution" class="w-full bg-black/30 border border-white/10 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-indigo-500">
<option value="3840x2160">4K Ultra HD (3840×2160)</option>
<option value="1920x1080" selected>Full HD (1920×1080)</option>
<option value="1280x720">HD (1280×720)</option>
<option value="854x480">480p (854×480)</option>
<option value="640x360">360p (640×360)</option>
</select>
</div>
<div class="flex items-center justify-between py-2">
<span class="text-sm font-medium">Transition Effect</span>
<select id="transition" class="bg-black/30 border border-white/10 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-indigo-500">
<option value="none">Cut (None)</option>
<option value="fade">Fade</option>
<option value="slide">Slide</option>
</select>
</div>
</div>
<button id="generateBtn" onclick="generateVideo()" class="w-full bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500 text-white font-semibold py-4 rounded-xl transition-all hover:scale-[1.02] active:scale-[0.98] flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed">
<i data-lucide="render" class="w-5 h-5"></i>
Generate Video
</button>
<div id="progressContainer" class="hidden space-y-2">
<div class="flex justify-between text-xs text-gray-400">
<span id="progressText">Initializing...</span>
<span id="progressPercent">0%</span>
</div>
<div class="h-2 bg-white/10 rounded-full overflow-hidden">
<div id="progressBar" class="progress-bar h-full w-0 rounded-full transition-all duration-300"></div>
</div>
</div>
<div id="downloadContainer" class="hidden">
<a id="downloadLink" href="#" download="cineflow-video.mp4" class="flex items-center justify-center gap-2 w-full bg-green-600 hover:bg-green-500 text-white font-semibold py-3 rounded-xl transition-all">
<i data-lucide="download" class="w-5 h-5"></i>
Download Video
</a>
</div>
</div>
<!-- Info -->
<div class="glass-panel rounded-2xl p-4 text-xs text-gray-400 space-y-2">
<div class="flex items-start gap-2">
<i data-lucide="shield-check" class="w-4 h-4 text-green-400 mt-0.5"></i>
<span>All processing happens locally in your browser. No data is uploaded to any server.</span>
</div>
<div class="flex items-start gap-2">
<i data-lucide="infinity" class="w-4 h-4 text-indigo-400 mt-0.5"></i>
<span>Unlimited generations. Limited only by your device's memory.</span>
</div>
</div>
</div>
</div>
</main>
<script>
// Initialize Lucide icons
lucide.createIcons();
// State
let images = [];
let ffmpeg = null;
let isGenerating = false;
// DOM Elements
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('fileInput');
const timeline = document.getElementById('timeline');
const frameCount = document.getElementById('frameCount');
const generateBtn = document.getElementById('generateBtn');
const progressContainer = document.getElementById('progressContainer');
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
const progressPercent = document.getElementById('progressPercent');
const downloadContainer = document.getElementById('downloadContainer');
const downloadLink = document.getElementById('downloadLink');
// Event Listeners
dropZone.addEventListener('click', () => fileInput.click());
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('drag-over');
});
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('drag-over');
handleFiles(e.dataTransfer.files);
});
fileInput.addEventListener('change', (e) => handleFiles(e.target.files));
document.getElementById('duration').addEventListener('input', (e) => {
document.getElementById('durationValue').textContent = e.target.value + 's';
});
document.getElementById('fps').addEventListener('input', (e) => {
document.getElementById('fpsValue').textContent = e.target.value + 'fps';
});
document.getElementById('resolution').addEventListener('change', (e) => {
document.getElementById('resolutionValue').textContent = e.target.value.split('x').join('×');
});
function handleFiles(files) {
const validFiles = Array.from(files).filter(file => file.type.startsWith('image/'));
validFiles.forEach(file => {
const reader = new FileReader();
reader.onload = (e) => {
images.push({
id: Date.now() + Math.random(),
src: e.target.result,
name: file.name,
type: file.type
});
updateTimeline();
};
reader.readAsDataURL(file);
});
}
function updateTimeline() {
frameCount.textContent = `${images.length} frame${images.length !== 1 ? 's' : ''}`;
if (images.length === 0) {
timeline.innerHTML = `
<div class="text-center py-12 text-gray-500">
<i data-lucide="image-off" class="w-12 h-12 mx-auto mb-3 opacity-20"></i>
<p>No images added yet</p>
</div>
`;
lucide.createIcons();
return;
}
timeline.innerHTML = images.map((img, index) => `
<div class="timeline-item flex items-center gap-3 bg-white/5 rounded-lg p-2 border border-white/5" id="frame-${img.id}">
<div class="relative w-20 h-12 bg-black rounded overflow-hidden flex-shrink-0">
<img src="${img.src}" class="w-full h-full object-cover frame-preview">
<div class="absolute inset-0 bg-black/30 flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity cursor-pointer" onclick="previewImage(${index})">
<i data-lucide="eye" class="w-4 h-4 text-white"></i>
</div>
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium truncate">${img.name}</p>
<p class="text-xs text-gray-500">Frame ${index + 1}</p>
</div>
<div class="flex gap-1">
<button onclick="moveImage(${index}, -1)" class="p-1.5 hover:bg-white/10 rounded transition-colors ${index === 0 ? 'opacity-30 cursor-not-allowed' : ''}" ${index === 0 ? 'disabled' : ''}>
<i data-lucide="chevron-up" class="w-4 h-4"></i>
</button>
<button onclick="moveImage(${index}, 1)" class="p-1.5 hover:bg-white/10 rounded transition-colors ${index === images.length - 1 ? 'opacity-30 cursor-not-allowed' : ''}" ${index === images.length - 1 ? 'disabled' : ''}>
<i data-lucide="chevron-down" class="w-4 h-4"></i>
</button>
<button onclick="removeImage(${index})" class="p-1.5 hover:bg-red-500/20 text-red-400 rounded transition-colors">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
</div>
`).join('');
lucide.createIcons();
}
function removeImage(index) {
images.splice(index, 1);
updateTimeline();
}
function moveImage(index, direction) {
const newIndex = index + direction;
if (newIndex < 0 || newIndex >= images.length) return;
const temp = images[index];
images[index] = images[newIndex];
images[newIndex] = temp;
updateTimeline();
}
function previewImage(index) {
const img = document.getElementById('previewImage');
const placeholder = document.getElementById('previewPlaceholder');
img.src = images[index].src;
img.classList.remove('hidden');
placeholder.classList.add('hidden');
}
function clearAll() {
images = [];
updateTimeline();
document.getElementById('previewImage').classList.add('hidden');
document.getElementById('previewPlaceholder').classList.remove('hidden');
}
function resetAll() {
clearAll();
document.getElementById('downloadContainer').classList.add('hidden');
progressContainer.classList.add('hidden');
}
async function loadFFmpeg() {
if (ffmpeg) return;
const { FFmpeg } = FFmpegWASM;
ffmpeg = new FFmpeg();
progressText.textContent = 'Loading FFmpeg...';
await ffmpeg.load();
ffmpeg.on('log', ({ message }) => {
console.log('FFmpeg:', message);
});
}
async function generateVideo() {
if (images.length === 0) {
alert('Please add at least one image');
return;
}
if (isGenerating) return;
try {
isGenerating = true;
generateBtn.disabled = true;
generateBtn.innerHTML = '<i data-lucide="loader-2" class="w-5 h-5 animate-spin"></i> Processing...';
lucide.createIcons();
progressContainer.classList.remove('hidden');
downloadContainer.classList.add('hidden');
// Load FFmpeg if not loaded
if (!ffmpeg) {
progressText.textContent = 'Loading video engine...';
await loadFFmpeg();
}
const duration = parseFloat(document.getElementById('duration').value);
const fps = parseInt(document.getElementById('fps').value);
const resolution = document.getElementById('resolution').value.split('x');
const width = parseInt(resolution[0]);
const height = parseInt(resolution[1]);
progressText.textContent = 'Preparing images...';
// Process images
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
let frameCount = 0;
const totalFrames = Math.ceil(images.length * duration * fps);
for (let i = 0; i < images.length; i++) {
const img = new Image();
img.src = images[i].src;
await new Promise(resolve => {
img.onload = resolve;
});
// Calculate frames for this image
const framesForThisImage = Math.floor(duration * fps);
for (let f = 0; f < framesForThisImage; f++) {
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, width, height);
// Scale image to fit while maintaining aspect ratio
const scale = Math.min(width / img.width, height / img.height);
const x = (width - img.width * scale) / 2;
const y = (height - img.height * scale) / 2;
ctx.drawImage(img, x, y, img.width * scale, img.height * scale);
// Convert to blob and write to FFmpeg
const blob = await new Promise(resolve => {
canvas.toBlob(resolve, 'image/jpeg', 0.95);
});
const buffer = await blob.arrayBuffer();
const frameName = `frame_${String(frameCount).padStart(6, '0')}.jpg`;
await ffmpeg.writeFile(frameName, new Uint8Array(buffer));
frameCount++;
// Update progress
const percent = Math.round((frameCount / totalFrames) * 90);
progressBar.style.width = percent + '%';
progressPercent.textContent = percent + '%';
progressText.textContent = `Processing frame ${frameCount}/${totalFrames}...`;
}
}
progressText.textContent = 'Encoding video...';
progressBar.style.width = '95%';
// Generate video
await ffmpeg.exec([
'-framerate', fps.toString(),
'-pattern_type', 'glob',
'-i', 'frame_*.jpg',
'-c:v', 'libx264',
'-pix_fmt', 'yuv420p',
'-preset', 'ultrafast',
'-crf', '23',
'output.mp4'
]);
progressText.textContent = 'Finalizing...';
progressBar.style.width = '100%';
// Read output file
const data = await ffmpeg.readFile('output.mp4');
const videoBlob = new Blob([data.buffer], { type: 'video/mp4' });
const url = URL.createObjectURL(videoBlob);
// Setup download
downloadLink.href = url;
downloadContainer.classList.remove('hidden');
// Cleanup
for (let i = 0; i < frameCount; i++) {
try {
await ffmpeg.deleteFile(`frame_${String(i).padStart(6, '0')}.jpg`);
} catch (e) {}
}
progressText.textContent = 'Complete!';
} catch (error) {
console.error('Error:', error);
alert('Error generating video: ' + error.message);
progressText.textContent = 'Error occurred';
} finally {
isGenerating = false;
generateBtn.disabled = false;
generateBtn.innerHTML = '<i data-lucide="render" class="w-5 h-5"></i> Generate Video';
lucide.createIcons();
}
}
</script>
<script src="https://deepsite.hf.co/deepsite-badge.js"></script>
</body>
</html>