Show description
draw2Video
draw2Video
draw2Video
Your Imagination, Animated.
×
Generate Image from Drawing
Your drawing will be used as a guide. Describe any extra details.
Generate Image
Image will appear here
Generate Video Storyboard
Describe a scene or action. The AI will create a 4-frame storyboard.
Generate Storyboard
draw2Video
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>draw2Video</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
overscroll-behavior: none;
}
.dot-grid {
background-image: radial-gradient(circle, #e2e8f0 1px, rgba(0, 0, 0, 0) 1px);
background-size: 20px 20px;
}
#studio-sidebar {
transition: transform 0.3s ease-in-out;
transform: translateX(100%);
box-shadow: -10px 0 20px -10px rgba(0,0,0,0.1);
}
#studio-sidebar.open {
transform: translateX(0);
}
.tool-btn.active {
background-color: #3b82f6;
color: white;
}
.loader {
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.storyboard-frame {
cursor: pointer;
border: 4px solid transparent;
transition: border-color 0.2s ease-in-out;
}
.storyboard-frame[data-selected="true"] {
border-color: #3b82f6;
}
</style>
</head>
<body class="bg-slate-200 m-0 overflow-hidden flex h-screen">
<!-- Main Content Area -->
<main class="flex-1 relative bg-slate-100 h-full">
<!-- Canvas for drawing -->
<canvas id="drawingCanvas" class="w-full h-full dot-grid cursor-crosshair"></canvas>
<!-- Drawing Toolbar -->
<div id="toolbar" class="fixed bottom-5 left-1/2 -translate-x-1/2 bg-white/80 backdrop-filter backdrop-blur-lg border border-slate-300 rounded-xl p-2 flex items-center gap-4 z-20">
<button id="penTool" class="tool-btn p-3 rounded-lg hover:bg-slate-200 transition-colors active">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg>
</button>
<button id="eraserTool" class="tool-btn p-3 rounded-lg hover:bg-slate-200 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m7 21-4.3-4.3c-1-1-1-2.5 0-3.4l9.6-9.6c1-1 2.5-1 3.4 0l5.6 5.6c1 1 1 2.5 0 3.4L13 21H7Z"/><path d="M22 21H7"/><path d="m5 12 5 5"/></svg>
</button>
<div class="w-px h-8 bg-slate-300 mx-1"></div>
<input type="color" id="colorPicker" value="#000000" class="w-10 h-10 p-0 border-none bg-transparent rounded-lg cursor-pointer appearance-none">
</div>
<!-- AI Studio Toggle Button -->
<button id="studio-toggle" class="fixed top-4 right-4 bg-blue-500 hover:bg-blue-600 text-white rounded-full p-3 shadow-lg z-20 transition-transform hover:scale-110">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 5 4 4"/><path d="M12 21a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3Z"/><path d="M18 12h3"/><path d="M21 9V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h6"/><path d="M3 10h4"/><path d="M3 14h4"/></svg>
</button>
</main>
<!-- AI Studio Sidebar -->
<aside id="studio-sidebar" class="w-96 bg-white/90 backdrop-filter backdrop-blur-lg border-l border-slate-300 flex flex-col h-screen absolute right-0 top-0 z-30">
<div class="p-4 border-b border-slate-300 flex justify-between items-center">
<div>
<h2 class="text-xl font-bold text-slate-800">draw2Video</h2>
<p class="text-sm text-slate-500">Your Imagination, Animated.</p>
</div>
<button id="studio-close" class="text-slate-500 hover:text-slate-800 text-2xl font-bold">×</button>
</div>
<div class="flex-1 overflow-y-auto p-4 space-y-6">
<!-- Image Generation Section -->
<div>
<h3 class="font-semibold text-slate-700 mb-2">Generate Image from Drawing</h3>
<p class="text-sm text-slate-500 mb-2">Your drawing will be used as a guide. Describe any extra details.</p>
<textarea id="image-prompt" class="w-full p-2 border border-slate-300 rounded-md" rows="3" placeholder="e.g., a photorealistic cat, fantasy style, vibrant colors..."></textarea>
<button id="generate-image-btn" class="w-full mt-2 bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded-lg transition-colors">Generate Image</button>
<div id="image-output" class="mt-4 bg-slate-100 rounded-md aspect-square flex items-center justify-center">
<p class="text-slate-400">Image will appear here</p>
</div>
</div>
<!-- Video Generation Section -->
<div>
<h3 class="font-semibold text-slate-700 mb-2">Generate Video Storyboard</h3>
<p class="text-sm text-slate-500 mb-2">Describe a scene or action. The AI will create a 4-frame storyboard.</p>
<textarea id="video-prompt" class="w-full p-2 border border-slate-300 rounded-md" rows="3" placeholder="e.g., a car driving through a neon city at night..."></textarea>
<button id="generate-storyboard-btn" class="w-full mt-2 bg-purple-500 hover:bg-purple-600 text-white font-bold py-2 px-4 rounded-lg transition-colors">Generate Storyboard</button>
<div id="storyboard-output" class="mt-4 grid grid-cols-2 gap-2">
<!-- Storyboard frames will be injected here -->
</div>
<div id="video-generation-controls" class="mt-4">
<!-- This will be populated dynamically -->
</div>
</div>
</div>
</aside>
<script type="module">
// --- Canvas & Drawing Setup ---
const canvas = document.getElementById('drawingCanvas');
const ctx = canvas.getContext('2d');
let isDrawing = false;
let tool = 'pen';
let strokeColor = '#000000';
function resizeCanvas() {
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
}
function getCoords(e) {
const rect = canvas.getBoundingClientRect();
const touch = e.touches ? e.touches[0] : e;
return { x: touch.clientX - rect.left, y: touch.clientY - rect.top };
}
function startDrawing(e) { e.preventDefault(); isDrawing = true; const { x, y } = getCoords(e); ctx.beginPath(); ctx.moveTo(x, y); }
function draw(e) { if (!isDrawing) return; e.preventDefault(); const { x, y } = getCoords(e); ctx.lineTo(x, y); ctx.strokeStyle = tool === 'pen' ? strokeColor : '#f1f5f9'; ctx.lineWidth = tool === 'pen' ? 5 : 30; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.stroke(); }
function stopDrawing() { isDrawing = false; ctx.closePath(); }
// --- UI Element Listeners ---
document.getElementById('penTool').addEventListener('click', () => { tool = 'pen'; document.getElementById('penTool').classList.add('active'); document.getElementById('eraserTool').classList.remove('active'); });
document.getElementById('eraserTool').addEventListener('click', () => { tool = 'eraser'; document.getElementById('eraserTool').classList.add('active'); document.getElementById('penTool').classList.remove('active'); });
document.getElementById('colorPicker').addEventListener('input', (e) => { strokeColor = e.target.value; });
const studioSidebar = document.getElementById('studio-sidebar');
document.getElementById('studio-toggle').addEventListener('click', () => studioSidebar.classList.add('open'));
document.getElementById('studio-close').addEventListener('click', () => studioSidebar.classList.remove('open'));
// --- AI Studio API Logic ---
const imageOutput = document.getElementById('image-output');
const storyboardOutput = document.getElementById('storyboard-output');
const videoGenerationControls = document.getElementById('video-generation-controls');
const generateImageBtn = document.getElementById('generate-image-btn');
const generateStoryboardBtn = document.getElementById('generate-storyboard-btn');
let selectedFrames = [];
const apiKey = ""; // Leave blank, will be handled by environment
function showLoader(element) { element.innerHTML = '<div class="loader mx-auto"></div>'; }
async function generateImageWithImagen(prompt, outputElement) {
const payload = { instances: [{ prompt: prompt }], parameters: { "sampleCount": 1 } };
try {
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/imagen-3.0-generate-002:predict?key=${apiKey}`;
const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
if (!response.ok) throw new Error(`API Error: ${response.status}`);
const result = await response.json();
if (result.predictions && result.predictions[0].bytesBase64Encoded) {
const imageUrl = `data:image/png;base64,${result.predictions[0].bytesBase64Encoded}`;
outputElement.innerHTML = `<img src="${imageUrl}" class="w-full h-full object-cover rounded-md">`;
return { prompt, imageUrl };
} else {
outputElement.innerHTML = `<p class="text-red-500 p-4">Error: Could not generate image from prompt.</p>`;
return null;
}
} catch (error) {
console.error('Error calling Imagen API:', error);
outputElement.innerHTML = `<p class="text-red-500 p-4">Error: Imagen API call failed.</p>`;
return null;
}
}
generateImageBtn.addEventListener('click', async () => {
const promptText = document.getElementById('image-prompt').value;
if (!promptText) { alert("Please enter a prompt to describe the image."); return; }
showLoader(imageOutput);
const drawingDataUrl = canvas.toDataURL('image/png');
const base64ImageData = drawingDataUrl.split(',')[1];
const payload = {
contents: [{ role: "user", parts: [ { text: `Based on the user's drawing and their prompt, create a new, detailed, single-sentence text-to-image prompt to generate a beautiful image. User's prompt: "${promptText}"` }, { inlineData: { mimeType: "image/png", data: base64ImageData } } ] }]
};
try {
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`;
const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
if (!response.ok) throw new Error(`Gemini API Error: ${response.status}`);
const result = await response.json();
if (result.candidates && result.candidates[0].content.parts[0].text) {
const imagePrompt = result.candidates[0].content.parts[0].text;
await generateImageWithImagen(imagePrompt, imageOutput);
} else { throw new Error("Gemini did not return a valid prompt."); }
} catch (error) {
console.error('Error in image generation flow:', error);
imageOutput.innerHTML = `<p class="text-red-500 p-4">Error: ${error.message}</p>`;
}
});
generateStoryboardBtn.addEventListener('click', async () => {
const promptText = document.getElementById('video-prompt').value;
if (!promptText) { alert("Please enter a prompt to describe the video scene."); return; }
storyboardOutput.innerHTML = '';
videoGenerationControls.innerHTML = '';
selectedFrames = [];
showLoader(storyboardOutput);
const storyboardPrompt = `Based on the user's request for a video scene, create 4 distinct text-to-image prompts that would form a cohesive storyboard. The user's request is: "${promptText}". Return ONLY a JSON array of 4 strings. Example: ["prompt 1", "prompt 2", "prompt 3", "prompt 4"]`;
const payload = { contents: [{ role: "user", parts: [{ text: storyboardPrompt }] }], generationConfig: { responseMimeType: "application/json" } };
try {
const geminiApiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`;
const response = await fetch(geminiApiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
const result = await response.json();
if (result.candidates && result.candidates[0].content.parts[0].text) {
const prompts = JSON.parse(result.candidates[0].content.parts[0].text);
storyboardOutput.innerHTML = '';
for (const prompt of prompts) {
const frameDiv = document.createElement('div');
frameDiv.className = 'storyboard-frame bg-slate-100 rounded-md aspect-square flex items-center justify-center';
frameDiv.dataset.selected = "false";
storyboardOutput.appendChild(frameDiv);
showLoader(frameDiv);
const generatedData = await generateImageWithImagen(prompt, frameDiv);
if(generatedData) {
frameDiv.dataset.prompt = generatedData.prompt;
frameDiv.dataset.imageUrl = generatedData.imageUrl;
frameDiv.addEventListener('click', () => toggleFrameSelection(frameDiv));
}
}
} else { throw new Error("Could not generate storyboard prompts."); }
} catch (error) {
console.error('Error generating storyboard:', error);
storyboardOutput.innerHTML = `<p class="text-red-500 p-4">Error: Storyboard generation failed.</p>`;
}
});
function toggleFrameSelection(frameDiv) {
const isSelected = frameDiv.dataset.selected === 'true';
if (isSelected) {
frameDiv.dataset.selected = 'false';
selectedFrames = selectedFrames.filter(f => f.prompt !== frameDiv.dataset.prompt);
} else {
if (selectedFrames.length < 2) {
frameDiv.dataset.selected = 'true';
selectedFrames.push({ prompt: frameDiv.dataset.prompt, imageUrl: frameDiv.dataset.imageUrl });
}
}
updateVideoControls();
}
function updateVideoControls() {
videoGenerationControls.innerHTML = '';
if (selectedFrames.length === 2) {
const btn = document.createElement('button');
btn.id = 'generate-video-from-selection-btn';
btn.className = 'w-full mt-2 bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded-lg transition-colors';
btn.textContent = 'Generate Video from Selection';
btn.onclick = generateVideoFromFrames;
videoGenerationControls.appendChild(btn);
}
}
async function generateVideoFromFrames() {
if (selectedFrames.length !== 2) return;
const [frame1, frame2] = selectedFrames;
videoGenerationControls.innerHTML = '';
const videoResultDiv = document.createElement('div');
videoResultDiv.className = 'mt-4 space-y-2';
const title = document.createElement('h4');
title.className = 'font-semibold text-slate-700';
title.textContent = 'Generating Video...';
videoResultDiv.appendChild(title);
const loaderContainer = document.createElement('div');
loaderContainer.className = 'bg-slate-100 rounded-md aspect-video flex flex-col items-center justify-center p-4';
showLoader(loaderContainer);
const loaderText = document.createElement('p');
loaderText.className = 'text-sm text-slate-500 mt-2';
loaderText.textContent = 'Sending frames to video model...';
loaderContainer.appendChild(loaderText);
videoResultDiv.appendChild(loaderContainer);
videoGenerationControls.appendChild(videoResultDiv);
console.log("Simulating video generation request with:");
console.log("Start Frame:", frame1.prompt);
console.log("End Frame:", frame2.prompt);
setTimeout(() => {
title.textContent = 'Video Generation Complete (Simulation)';
loaderContainer.innerHTML = `
<div class="relative w-full h-full bg-black rounded-md">
<img src="${frame1.imageUrl}" class="w-full h-full object-cover rounded-md opacity-80" alt="Video thumbnail">
<div class="absolute inset-0 flex items-center justify-center">
<svg class="w-16 h-16 text-white/80" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd"></path></svg>
</div>
<p class="absolute bottom-2 left-2 text-white text-xs bg-black/50 px-2 py-1 rounded">This is a simulated video output.</p>
</div>
`;
}, 5000);
}
// --- Initialization ---
window.addEventListener('load', resizeCanvas);
window.addEventListener('resize', resizeCanvas);
canvas.addEventListener('mousedown', startDrawing);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', stopDrawing);
canvas.addEventListener('mouseout', stopDrawing);
canvas.addEventListener('touchstart', startDrawing, { passive: false });
canvas.addEventListener('touchmove', draw, { passive: false });
canvas.addEventListener('touchend', stopDrawing);
</script>
</body>
</html>