NSTiwari commited on
Commit
b2099d7
·
verified ·
1 Parent(s): df28460

Upload index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +279 -0
templates/index.html ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Sketch2Video with Gemini & Veo 3</title>
7
+ <style>
8
+ body { font-family: sans-serif; display: flex; flex-direction: column; align-items: center; padding: 20px; background-color: #f4f4f4; min-height: 100vh; box-sizing: border-box; }
9
+ h1 { color: #333; text-align: center; margin-bottom: 25px; }
10
+ h2 { margin-top: 0; margin-bottom: 15px; text-align: center; font-size: 1.2em; color: #444; }
11
+ .attribution { text-align: center; font-size: 1.2em; color: #667; margin-top: -15px; margin-bottom: 25px; }
12
+ .attribution .heart { display: inline-block; vertical-align: middle; }
13
+ .attribution .dev-logo-img { display: inline-block; width: 1.5em; height: auto; vertical-align: middle; margin: 0 0.2em; position: relative; top: -0.1em; }
14
+ .main-container { display: flex; justify-content: space-around; align-items: center; width: 95%; max-width: 1100px; gap: 30px; margin-top: 20px; flex-wrap: wrap; }
15
+ .sketch-area, .output-container { display: flex; flex-direction: column; align-items: center; width: auto; flex-basis: 512px; flex-grow: 0; box-sizing: border-box; }
16
+ .canvas-wrapper, .output-video-wrapper {
17
+ width: 100%;
18
+ aspect-ratio: 16 / 9;
19
+ background-color: #e9e9e9;
20
+ display: flex;
21
+ align-items: center;
22
+ justify-content: center;
23
+ border: 1px solid #ccc;
24
+ box-shadow: 2px 2px 5px rgba(0,0,0,0.05);
25
+ overflow: hidden;
26
+ }
27
+ canvas {
28
+ display: block;
29
+ background-color: #fff;
30
+ width: 100%;
31
+ height: 100%;
32
+ cursor: crosshair;
33
+ touch-action: none;
34
+ }
35
+ canvas.erase-mode { cursor: cell; }
36
+ .output-video-wrapper video { width: 100%; height: 100%; display: block; }
37
+ .output-video-wrapper .placeholder, .output-video-wrapper .output-error {
38
+ width: 100%;
39
+ height: 100%;
40
+ background-color: #fff;
41
+ color: #888;
42
+ text-align: center;
43
+ padding: 20px;
44
+ display: flex;
45
+ align-items: center;
46
+ justify-content: center;
47
+ box-sizing: border-box;
48
+ }
49
+ .output-video-wrapper .output-error { color: red; font-weight: bold; word-break: break-word; }
50
+ .canvas-controls-container { display: flex; justify-content: center; align-items: center; gap: 15px; margin-top: 25px; margin-bottom: 15px; flex-wrap: wrap; padding: 10px; background-color: #e9e9e9; border-radius: 5px; width: auto; max-width: 90%; }
51
+ .canvas-controls-container button { padding: 8px 12px; font-size: 0.9em; cursor: pointer; border: 1px solid #ccc; border-radius: 4px; background-color: #f8f8f8; }
52
+ .canvas-controls-container button:hover { background-color: #e0e0e0; border-color: #bbb; }
53
+ .canvas-controls-container button.active-tool { background-color: #d0e7ff; border-color: #007bff; font-weight: bold; }
54
+ label { font-size: 0.9em; color: #555; }
55
+ #colorPicker { width: 40px; height: 30px; border: 1px solid #ccc; padding: 2px; border-radius: 4px; cursor: pointer; background-color: #fff; vertical-align: middle; }
56
+
57
+ /* --- NEW CSS: A small addition for better alignment of the new slider --- */
58
+ .thickness-control { display: flex; align-items: center; gap: 5px; }
59
+ .thickness-control input[type="range"] { vertical-align: middle; }
60
+
61
+ .prompt-container { display: flex; flex-direction: column; align-items: center; width: 100%; max-width: 800px; margin-bottom: 15px; }
62
+ #promptInput { width: 100%; padding: 8px 10px; border: 1px solid #000000; border-radius: 8px; font-size: 1.2em; min-height: 40px; resize: vertical; line-height: 1.4; box-sizing: border-box; }
63
+ .action-buttons { display: flex; justify-content: center; gap: 15px; width: 100%; margin-top: 0; }
64
+ .action-buttons button { padding: 12px 20px; font-size: 1.1em; cursor: pointer; border: none; border-radius: 5px; color: white; transition: background-color 0.2s; min-width: 120px; }
65
+ .action-buttons button:disabled { background-color: #cccccc; cursor: not-allowed; }
66
+ #generateBtn { background-color: #007bff; }
67
+ #generateBtn:hover:not(:disabled) { background-color: #0056b3; }
68
+ #clearBtn { background-color: #dc3545; }
69
+ #clearBtn:hover:not(:disabled) { background-color: #c82333; }
70
+ .status-area { margin-top: 20px; font-weight: bold; min-height: 1.2em; text-align: center; width: 100%; padding-bottom: 20px; }
71
+ .status-area.error { color: red; }
72
+ .status-area.loading { color: #0056b3; }
73
+ </style>
74
+ </head>
75
+ <body>
76
+ <h1>Sketch2Video with Gemini & Veo 3</h1>
77
+ <h3 class="attribution">
78
+ Made with <span class="heart">🧡</span> by
79
+ <img src="{{ url_for('static', filename='images/dev-logo.png') }}" alt="Google Developers logo" class="dev-logo-img">
80
+ Nitin Tiwari
81
+ </h2>
82
+
83
+ <div class="main-container">
84
+ <div class="sketch-area">
85
+ <h2>Your Sketch</h2>
86
+ <div class="canvas-wrapper">
87
+ <canvas id="sketchCanvas" width="512" height="288"></canvas>
88
+ </div>
89
+ </div>
90
+ <div class="output-container">
91
+ <h2>Generated Video</h2>
92
+ <div id="outputWrapper" class="output-video-wrapper">
93
+ <video id="outputVideo" controls autoplay muted loop playsinline style="display: none;"></video>
94
+ <div id="outputPlaceholder" class="placeholder">Video will appear here...</div>
95
+ <div id="outputError" class="output-error" style="display: none;"></div>
96
+ </div>
97
+ </div>
98
+ </div>
99
+
100
+ <div class="canvas-controls-container">
101
+ <button id="drawBtn" class="active-tool">Draw</button>
102
+ <button id="eraseBtn">Erase</button>
103
+ <label for="colorPicker">Color:</label>
104
+ <input type="color" id="colorPicker" value="#000000">
105
+
106
+ <!-- +++ NEW HTML: Brush thickness slider +++ -->
107
+ <div class="thickness-control">
108
+ <label for="thicknessSlider">Thickness:</label>
109
+ <input type="range" id="thicknessSlider" min="1" max="50" value="3">
110
+ <span id="thicknessValue">3</span>
111
+ </div>
112
+ <!-- +++ END OF NEW HTML +++ -->
113
+
114
+ </div>
115
+ <div class="prompt-container">
116
+ <textarea id="promptInput" rows="1" placeholder="Describe the video animation (e.g., camera zooms in, car drives away)"></textarea>
117
+ </div>
118
+ <div class="action-buttons">
119
+ <button id="clearBtn">Clear Sketch & Prompt</button>
120
+ <button id="generateBtn">Generate Video</button>
121
+ </div>
122
+ <div id="status" class="status-area"></div>
123
+ <script>
124
+ const promptInput = document.getElementById('promptInput');
125
+ const canvas = document.getElementById('sketchCanvas');
126
+ const ctx = canvas.getContext('2d');
127
+ const clearBtn = document.getElementById('clearBtn');
128
+ const generateBtn = document.getElementById('generateBtn');
129
+ const eraseBtn = document.getElementById('eraseBtn');
130
+ const drawBtn = document.getElementById('drawBtn');
131
+ const colorPicker = document.getElementById('colorPicker');
132
+
133
+ // +++ NEW JS: Get the new slider elements +++
134
+ const thicknessSlider = document.getElementById('thicknessSlider');
135
+ const thicknessValue = document.getElementById('thicknessValue');
136
+
137
+ const outputWrapper = document.getElementById('outputWrapper');
138
+ const outputVideo = document.getElementById('outputVideo');
139
+ const outputPlaceholder = document.getElementById('outputPlaceholder');
140
+ const outputError = document.getElementById('outputError');
141
+ const statusDiv = document.getElementById('status');
142
+ let isDrawing = false;
143
+ let lastX = 0;
144
+ let lastY = 0;
145
+ let currentMode = 'draw';
146
+ let drawColor = '#000000';
147
+
148
+ // +++ MODIFIED JS: Change drawLineWidth from const to let so it can be updated +++
149
+ let drawLineWidth = 3;
150
+ const eraseLineWidth = 15;
151
+
152
+ function initializeCanvas() {
153
+ ctx.fillStyle = "white";
154
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
155
+ drawColor = '#000000';
156
+ colorPicker.value = '#000000';
157
+
158
+ // +++ NEW JS: Reset the slider to its default state +++
159
+ drawLineWidth = 3;
160
+ thicknessSlider.value = 3;
161
+ thicknessValue.textContent = 3;
162
+
163
+ setDrawMode();
164
+ outputVideo.style.display = 'none';
165
+ outputVideo.src = '';
166
+ outputPlaceholder.style.display = 'flex';
167
+ outputPlaceholder.textContent = 'Video will appear here...';
168
+ outputError.style.display = 'none';
169
+ statusDiv.textContent = '';
170
+ statusDiv.className = 'status-area';
171
+ generateBtn.disabled = false;
172
+ clearBtn.disabled = false;
173
+ eraseBtn.disabled = false;
174
+ drawBtn.disabled = false;
175
+ colorPicker.disabled = false;
176
+ promptInput.value = '';
177
+ promptInput.disabled = false;
178
+ }
179
+
180
+ function setDrawMode() { currentMode = 'draw'; ctx.globalCompositeOperation = 'source-over'; ctx.strokeStyle = drawColor; ctx.lineWidth = drawLineWidth; canvas.classList.remove('erase-mode'); drawBtn.classList.add('active-tool'); eraseBtn.classList.remove('active-tool'); }
181
+ function setEraseMode() { currentMode = 'erase'; ctx.globalCompositeOperation = 'destination-out'; ctx.lineWidth = eraseLineWidth; canvas.classList.add('erase-mode'); eraseBtn.classList.add('active-tool'); drawBtn.classList.remove('active-tool'); }
182
+
183
+ function getPos(evt) {
184
+ const rect = canvas.getBoundingClientRect();
185
+ const scaleX = canvas.width / rect.width;
186
+ const scaleY = canvas.height / rect.height;
187
+ const clientX = evt.touches ? evt.touches[0].clientX : evt.clientX;
188
+ const clientY = evt.touches ? evt.touches[0].clientY : evt.clientY;
189
+ return {
190
+ x: (clientX - rect.left) * scaleX,
191
+ y: (clientY - rect.top) * scaleY
192
+ };
193
+ }
194
+
195
+ function startDrawing(e) { if (e.touches) e.preventDefault(); isDrawing = true; const pos = getPos(e); [lastX, lastY] = [pos.x, pos.y]; ctx.beginPath(); ctx.moveTo(lastX, lastY); ctx.fillStyle = (currentMode === 'draw') ? ctx.strokeStyle : 'rgba(0,0,0,1)'; ctx.arc(lastX, lastY, ctx.lineWidth / 2, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.moveTo(lastX, lastY); }
196
+ function draw(e) { if (!isDrawing) return; if (e.touches) e.preventDefault(); const pos = getPos(e); ctx.lineTo(pos.x, pos.y); ctx.stroke(); [lastX, lastY] = [pos.x, pos.y]; }
197
+ function stopDrawing() { if (isDrawing) { isDrawing = false; ctx.beginPath(); } }
198
+
199
+ canvas.addEventListener('mousedown', startDrawing);
200
+ canvas.addEventListener('mousemove', draw);
201
+ canvas.addEventListener('mouseup', stopDrawing);
202
+ canvas.addEventListener('mouseout', stopDrawing);
203
+ canvas.addEventListener('touchstart', startDrawing, { passive: false });
204
+ canvas.addEventListener('touchmove', draw, { passive: false });
205
+ canvas.addEventListener('touchend', stopDrawing);
206
+ canvas.addEventListener('touchcancel', stopDrawing);
207
+
208
+ clearBtn.addEventListener('click', () => { initializeCanvas(); });
209
+ drawBtn.addEventListener('click', setDrawMode);
210
+ eraseBtn.addEventListener('click', setEraseMode);
211
+ colorPicker.addEventListener('input', (event) => { drawColor = event.target.value; if (currentMode === 'draw') { ctx.strokeStyle = drawColor; } setDrawMode(); });
212
+
213
+ // +++ NEW JS: Event listener for the thickness slider +++
214
+ thicknessSlider.addEventListener('input', (event) => {
215
+ // Update the variable, the display number, and the context
216
+ drawLineWidth = event.target.value;
217
+ thicknessValue.textContent = drawLineWidth;
218
+ ctx.lineWidth = drawLineWidth;
219
+ // For good UX, switch back to draw mode when changing thickness
220
+ setDrawMode();
221
+ });
222
+
223
+ // The generateBtn logic is unchanged and correct
224
+ generateBtn.addEventListener('click', async () => {
225
+ generateBtn.disabled = true; clearBtn.disabled = true; eraseBtn.disabled = true;
226
+ drawBtn.disabled = true; colorPicker.disabled = true; promptInput.disabled = true;
227
+ statusDiv.textContent = 'Generating video... this may take a few minutes.';
228
+ statusDiv.className = 'status-area loading';
229
+ outputVideo.style.display = 'none';
230
+ outputError.style.display = 'none';
231
+ outputPlaceholder.style.display = 'flex';
232
+ outputPlaceholder.textContent = 'Generating...';
233
+ const imageDataUrl = canvas.toDataURL('image/png');
234
+ const userPrompt = promptInput.value.trim();
235
+ const payload = { image_data: imageDataUrl };
236
+ if (userPrompt) { payload.prompt = userPrompt; }
237
+ try {
238
+ const response = await fetch('/generate', {
239
+ method: 'POST',
240
+ headers: { 'Content-Type': 'application/json', },
241
+ body: JSON.stringify(payload),
242
+ });
243
+ const result = await response.json();
244
+ if (response.ok && result.generated_video_url) {
245
+ outputVideo.src = result.generated_video_url;
246
+ outputVideo.load();
247
+ outputVideo.style.display = 'block';
248
+ outputPlaceholder.style.display = 'none';
249
+ outputError.style.display = 'none';
250
+ statusDiv.textContent = 'Video generated successfully!';
251
+ statusDiv.className = 'status-area';
252
+ } else {
253
+ const errorMsg = result.error || response.statusText || `Failed with status ${response.status}`;
254
+ console.error('Generation failed:', errorMsg);
255
+ statusDiv.textContent = `Error: ${errorMsg}`;
256
+ statusDiv.className = 'status-area error';
257
+ outputError.textContent = `Generation Failed: ${errorMsg}`;
258
+ outputError.style.display = 'flex';
259
+ outputPlaceholder.style.display = 'none';
260
+ outputVideo.style.display = 'none';
261
+ }
262
+ } catch (error) {
263
+ console.error('Network or fetch error:', error);
264
+ const errorText = `Network error or failed to fetch: ${error.message || String(error)}`;
265
+ statusDiv.textContent = `Error: ${errorText}`;
266
+ statusDiv.className = 'status-area error';
267
+ outputError.textContent = `Error: ${errorText}`;
268
+ outputError.style.display = 'flex';
269
+ outputPlaceholder.style.display = 'none';
270
+ outputVideo.style.display = 'none';
271
+ } finally {
272
+ generateBtn.disabled = false; clearBtn.disabled = false; eraseBtn.disabled = false;
273
+ drawBtn.disabled = false; colorPicker.disabled = false; promptInput.disabled = false;
274
+ }
275
+ });
276
+ window.onload = initializeCanvas;
277
+ </script>
278
+ </body>
279
+ </html>