by3axap commited on
Commit
d69429c
·
verified ·
1 Parent(s): 1061221

Create templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +602 -0
templates/index.html ADDED
@@ -0,0 +1,602 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ru">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>CELL CLIPPER PRO / V3.0</title>
7
+ <style>
8
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=JetBrains+Mono&display=swap');
9
+
10
+ :root {
11
+ --bg-page: #ffffff;
12
+ --text-main: #000000;
13
+ --text-muted: #888888;
14
+ --border: #e0e0e0;
15
+ --active: #000000;
16
+ --font-sans: 'Inter', sans-serif;
17
+ --font-mono: 'JetBrains Mono', monospace;
18
+ }
19
+
20
+ body {
21
+ margin: 0;
22
+ display: flex;
23
+ height: 100vh;
24
+ font-family: var(--font-sans);
25
+ background-color: var(--bg-page);
26
+ color: var(--text-main);
27
+ overflow: hidden;
28
+ }
29
+
30
+ /* --- Sidebar --- */
31
+ .sidebar {
32
+ width: 380px;
33
+ background: #fafafa;
34
+ border-right: 1px solid var(--border);
35
+ display: flex;
36
+ flex-direction: column;
37
+ padding: 24px;
38
+ box-sizing: border-box;
39
+ z-index: 10;
40
+ overflow-y: auto;
41
+ flex-shrink: 0;
42
+ }
43
+
44
+ .section { margin-bottom: 24px; border-bottom: 1px solid var(--border); padding-bottom: 16px; }
45
+ .section:last-child { border: none; }
46
+
47
+ .section-title {
48
+ font-family: var(--font-mono);
49
+ font-size: 10px;
50
+ color: var(--text-muted);
51
+ text-transform: uppercase;
52
+ letter-spacing: 0.1em;
53
+ margin-bottom: 12px;
54
+ display: block;
55
+ font-weight: 600;
56
+ }
57
+
58
+ /* --- Controls --- */
59
+ .control-group { margin-bottom: 12px; width: 100%; }
60
+
61
+ .label-row {
62
+ display: flex;
63
+ justify-content: space-between;
64
+ align-items: center;
65
+ font-size: 11px;
66
+ margin-bottom: 6px;
67
+ font-family: var(--font-mono);
68
+ }
69
+
70
+ /* Number Input styling */
71
+ .num-input {
72
+ width: 50px;
73
+ border: none;
74
+ border-bottom: 1px solid var(--text-muted);
75
+ background: transparent;
76
+ font-family: var(--font-mono);
77
+ font-size: 11px;
78
+ text-align: right;
79
+ outline: none;
80
+ padding: 2px 0;
81
+ }
82
+ .num-input:focus { border-bottom: 1px solid black; }
83
+
84
+ input[type="range"] {
85
+ width: 100%;
86
+ -webkit-appearance: none;
87
+ background: transparent;
88
+ margin: 8px 0;
89
+ }
90
+ input[type="range"]::-webkit-slider-runnable-track {
91
+ width: 100%; height: 2px; background: #ddd;
92
+ }
93
+ input[type="range"]::-webkit-slider-thumb {
94
+ -webkit-appearance: none; height: 12px; width: 12px;
95
+ background: black; border-radius: 50%; margin-top: -5px; cursor: pointer;
96
+ }
97
+
98
+ /* --- File Inputs --- */
99
+ input[type="file"] { display: none; }
100
+ .file-upload-label {
101
+ display: block;
102
+ border: 1px dashed var(--text-muted);
103
+ padding: 12px;
104
+ text-align: center;
105
+ cursor: pointer;
106
+ font-size: 11px;
107
+ font-family: var(--font-mono);
108
+ text-transform: uppercase;
109
+ transition: all 0.2s;
110
+ background: white;
111
+ color: var(--text-muted);
112
+ }
113
+ .file-upload-label:hover { border-color: black; color: black; }
114
+ .file-upload-label.active { border-style: solid; border-color: black; color: black; background: #f0f0f0; }
115
+
116
+ /* --- Tabs for Media --- */
117
+ .media-tabs { display: flex; gap: 10px; margin-bottom: 10px; }
118
+ .tab-btn {
119
+ flex: 1; border: 1px solid var(--border); background: white;
120
+ padding: 8px; font-size: 10px; font-family: var(--font-mono); cursor: pointer;
121
+ }
122
+ .tab-btn.active { background: black; color: white; border-color: black; }
123
+
124
+ /* --- Buttons --- */
125
+ .buttons-container { display: flex; gap: 10px; margin-top: auto; }
126
+ .btn-action {
127
+ flex: 1;
128
+ background: var(--text-main);
129
+ color: white;
130
+ border: none;
131
+ padding: 14px;
132
+ font-family: var(--font-mono);
133
+ font-size: 11px;
134
+ text-transform: uppercase;
135
+ cursor: pointer;
136
+ transition: opacity 0.2s;
137
+ }
138
+ .btn-action:hover { opacity: 0.8; }
139
+ .btn-action.secondary { background: white; color: black; border: 1px solid black; }
140
+ .btn-action:disabled { opacity: 0.5; cursor: not-allowed; }
141
+
142
+ /* --- Preview Area --- */
143
+ .preview-area {
144
+ flex: 1;
145
+ background-color: #f0f0f0;
146
+ display: flex;
147
+ align-items: center;
148
+ justify-content: center;
149
+ position: relative;
150
+ padding: 40px;
151
+ }
152
+
153
+ canvas {
154
+ background: white;
155
+ box-shadow: 0 20px 50px rgba(0,0,0,0.1);
156
+ max-width: 100%;
157
+ max-height: 100%;
158
+ object-fit: contain;
159
+ }
160
+
161
+ /* Hidden media sources */
162
+ #source-video, #source-image { display: none; }
163
+ </style>
164
+ </head>
165
+ <body>
166
+
167
+ <div class="sidebar">
168
+ <div style="margin-bottom: 20px;">
169
+ <h1 style="font-size: 14px; font-weight: 600; letter-spacing: -0.02em; margin:0;">CELL CLIPPER PRO</h1>
170
+ <span style="font-size: 10px; color: #888; font-family: var(--font-mono);">REALTIME PROCESSING</span>
171
+ </div>
172
+
173
+ <div class="section">
174
+ <span class="section-title">01 / Shape (SVG)</span>
175
+ <input type="file" id="shape_svg" accept=".svg">
176
+ <label for="shape_svg" class="file-upload-label" id="lbl-shape">Default Square</label>
177
+ </div>
178
+
179
+ <div class="section">
180
+ <span class="section-title">02 / Source Media</span>
181
+
182
+ <div class="media-tabs">
183
+ <button class="tab-btn active" onclick="setMode('image')">IMAGE</button>
184
+ <button class="tab-btn" onclick="setMode('video')">VIDEO</button>
185
+ </div>
186
+
187
+ <div id="upload-image-box">
188
+ <input type="file" id="raster_image" accept=".png, .jpg, .jpeg">
189
+ <label for="raster_image" class="file-upload-label" id="lbl-raster">Upload Image</label>
190
+ </div>
191
+
192
+ <div id="upload-video-box" style="display:none;">
193
+ <input type="file" id="video_file" accept=".mp4, .mov, .webm">
194
+ <label for="video_file" class="file-upload-label" id="lbl-video">Upload Video (MP4)</label>
195
+ </div>
196
+ </div>
197
+
198
+ <form id="params-form">
199
+ <div class="section">
200
+ <span class="section-title">03 / Grid Layout</span>
201
+
202
+ <div class="control-group">
203
+ <div class="label-row">
204
+ <span>Density</span>
205
+ <input type="number" class="num-input" id="num-density" value="40" min="5" max="150">
206
+ </div>
207
+ <input type="range" id="density" min="5" max="150" value="40">
208
+ </div>
209
+
210
+ <div class="control-group">
211
+ <div class="label-row">
212
+ <span>Gap X</span>
213
+ <input type="number" class="num-input" id="num-gap_x" value="0" step="0.01" min="0" max="0.9">
214
+ </div>
215
+ <input type="range" id="gap_x" min="0" max="0.9" step="0.01" value="0">
216
+ </div>
217
+
218
+ <div class="control-group">
219
+ <div class="label-row">
220
+ <span>Gap Y</span>
221
+ <input type="number" class="num-input" id="num-gap_y" value="0" step="0.01" min="0" max="0.9">
222
+ </div>
223
+ <input type="range" id="gap_y" min="0" max="0.9" step="0.01" value="0">
224
+ </div>
225
+
226
+ <div class="control-group">
227
+ <div class="label-row">
228
+ <span>Stagger</span>
229
+ <input type="number" class="num-input" id="num-shift_stagger" value="0" step="0.01" min="0" max="1">
230
+ </div>
231
+ <input type="range" id="shift_stagger" min="0" max="1" step="0.01" value="0">
232
+ </div>
233
+ </div>
234
+
235
+ <div class="section">
236
+ <span class="section-title">04 / Transformation</span>
237
+
238
+ <div class="control-group">
239
+ <div class="label-row">
240
+ <span>Base Scale</span>
241
+ <input type="number" class="num-input" id="num-base_scale" value="1.0" step="0.05" min="0.1" max="2.0">
242
+ </div>
243
+ <input type="range" id="base_scale" min="0.1" max="2.0" step="0.05" value="1.0">
244
+ </div>
245
+
246
+ <div class="control-group">
247
+ <div class="label-row">
248
+ <span>Growth Factor</span>
249
+ <input type="number" class="num-input" id="num-growth" value="0" step="0.05" min="0" max="2.0">
250
+ </div>
251
+ <input type="range" id="growth" min="0" max="2.0" step="0.05" value="0">
252
+ </div>
253
+
254
+ <div class="control-group">
255
+ <div class="label-row">
256
+ <label style="display:flex; align-items:center; gap:5px; cursor:pointer;">
257
+ <input type="checkbox" id="invert">
258
+ <span>Invert Logic</span>
259
+ </label>
260
+ </div>
261
+ </div>
262
+
263
+ <div class="control-group">
264
+ <span style="font-size:10px; color:#888; font-family:var(--font-mono);">GROWTH PATTERN</span>
265
+ <select id="growth_type" style="width:100%; margin-top:5px; padding:5px; font-family:var(--font-mono);">
266
+ <option value="sine">Diagonal Sine</option>
267
+ <option value="linear">Linear Gradient</option>
268
+ <option value="steps">Steps</option>
269
+ </select>
270
+ </div>
271
+ </div>
272
+ </form>
273
+
274
+ <div class="buttons-container">
275
+ <button class="btn-action secondary" id="btn-dl-svg">SVG Image</button>
276
+ <button class="btn-action" id="btn-dl-video" disabled>Render MP4</button>
277
+ </div>
278
+ <div id="status-msg" style="font-size:10px; font-family:var(--font-mono); margin-top:10px; color:blue;"></div>
279
+ </div>
280
+
281
+ <div class="preview-area">
282
+ <canvas id="main-canvas" width="1400" height="900"></canvas>
283
+
284
+ <img id="source-image" src="" crossorigin="anonymous">
285
+ <video id="source-video" src="" playsinline loop muted crossorigin="anonymous"></video>
286
+
287
+ <canvas id="helper-canvas" style="display:none;"></canvas>
288
+ <img id="shape-img" src="" style="display:none;">
289
+ </div>
290
+
291
+ <script>
292
+ // --- STATE ---
293
+ let currentMode = 'image'; // 'image' or 'video'
294
+ let isVideoPlaying = false;
295
+ let animationId = null;
296
+ let customShapeLoaded = false;
297
+
298
+ // --- DOM ELEMENTS ---
299
+ const canvas = document.getElementById('main-canvas');
300
+ const ctx = canvas.getContext('2d', { alpha: false }); // Optimize
301
+ const helperCanvas = document.getElementById('helper-canvas');
302
+ const helperCtx = helperCanvas.getContext('2d');
303
+
304
+ const sourceImage = document.getElementById('source-image');
305
+ const sourceVideo = document.getElementById('source-video');
306
+ const shapeImg = document.getElementById('shape-img');
307
+
308
+ // --- INPUTS SYNC ---
309
+ const paramIds = ['density', 'gap_x', 'gap_y', 'shift_stagger', 'base_scale', 'growth'];
310
+
311
+ paramIds.forEach(id => {
312
+ const slider = document.getElementById(id);
313
+ const number = document.getElementById('num-' + id);
314
+
315
+ slider.addEventListener('input', () => { number.value = slider.value; requestRender(); });
316
+ number.addEventListener('input', () => { slider.value = number.value; requestRender(); });
317
+ });
318
+
319
+ document.getElementById('invert').addEventListener('change', requestRender);
320
+ document.getElementById('growth_type').addEventListener('change', requestRender);
321
+
322
+ // --- MODE SWITCH ---
323
+ window.setMode = (mode) => {
324
+ currentMode = mode;
325
+ document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
326
+ if(mode === 'image') {
327
+ document.querySelectorAll('.tab-btn')[0].classList.add('active');
328
+ document.getElementById('upload-image-box').style.display = 'block';
329
+ document.getElementById('upload-video-box').style.display = 'none';
330
+ document.getElementById('btn-dl-video').disabled = true;
331
+ document.getElementById('btn-dl-svg').disabled = false;
332
+ sourceVideo.pause();
333
+ } else {
334
+ document.querySelectorAll('.tab-btn')[1].classList.add('active');
335
+ document.getElementById('upload-image-box').style.display = 'none';
336
+ document.getElementById('upload-video-box').style.display = 'block';
337
+ document.getElementById('btn-dl-video').disabled = false;
338
+ document.getElementById('btn-dl-svg').disabled = true; // SVG from video not supported frame-by-frame here
339
+ if(sourceVideo.src) sourceVideo.play();
340
+ }
341
+ requestRender();
342
+ };
343
+
344
+ // --- FILE HANDLERS ---
345
+
346
+ // 1. Shape SVG
347
+ document.getElementById('shape_svg').addEventListener('change', (e) => {
348
+ const file = e.target.files[0];
349
+ if(!file) return;
350
+ document.getElementById('lbl-shape').innerText = file.name;
351
+ document.getElementById('lbl-shape').classList.add('active');
352
+
353
+ const reader = new FileReader();
354
+ reader.onload = (evt) => {
355
+ // Convert SVG to Data URL for Canvas drawing
356
+ const svgStr = evt.target.result;
357
+ const blob = new Blob([svgStr], {type: 'image/svg+xml'});
358
+ const url = URL.createObjectURL(blob);
359
+ shapeImg.onload = () => { customShapeLoaded = true; requestRender(); };
360
+ shapeImg.src = url;
361
+ };
362
+ reader.readAsText(file);
363
+ });
364
+
365
+ // 2. Raster Image
366
+ document.getElementById('raster_image').addEventListener('change', (e) => {
367
+ const file = e.target.files[0];
368
+ if(!file) return;
369
+ document.getElementById('lbl-raster').innerText = file.name;
370
+ document.getElementById('lbl-raster').classList.add('active');
371
+ const url = URL.createObjectURL(file);
372
+ sourceImage.onload = () => { requestRender(); };
373
+ sourceImage.src = url;
374
+ });
375
+
376
+ // 3. Video
377
+ document.getElementById('video_file').addEventListener('change', (e) => {
378
+ const file = e.target.files[0];
379
+ if(!file) return;
380
+ document.getElementById('lbl-video').innerText = file.name;
381
+ document.getElementById('lbl-video').classList.add('active');
382
+ const url = URL.createObjectURL(file);
383
+ sourceVideo.src = url;
384
+ sourceVideo.play();
385
+ requestRender(); // Start loop
386
+ });
387
+
388
+ // --- RENDER LOOP ---
389
+ function requestRender() {
390
+ if (currentMode === 'video' && !sourceVideo.paused) {
391
+ if (!animationId) {
392
+ renderLoop();
393
+ }
394
+ } else {
395
+ // Static render
396
+ drawFrame();
397
+ }
398
+ }
399
+
400
+ function renderLoop() {
401
+ drawFrame();
402
+ if (currentMode === 'video' && !sourceVideo.paused) {
403
+ animationId = requestAnimationFrame(renderLoop);
404
+ } else {
405
+ animationId = null;
406
+ }
407
+ }
408
+
409
+ // --- CORE LOGIC (JS version of Python logic) ---
410
+ function drawFrame() {
411
+ const w = canvas.width;
412
+ const h = canvas.height;
413
+
414
+ // Params
415
+ const density = parseInt(document.getElementById('density').value);
416
+ const gapX = parseFloat(document.getElementById('gap_x').value);
417
+ const gapY = parseFloat(document.getElementById('gap_y').value);
418
+ const stagger = parseFloat(document.getElementById('shift_stagger').value);
419
+ const baseScale = parseFloat(document.getElementById('base_scale').value);
420
+ const growth = parseFloat(document.getElementById('growth').value);
421
+ const growthType = document.getElementById('growth_type').value;
422
+ const invert = document.getElementById('invert').checked;
423
+
424
+ // Clear
425
+ ctx.fillStyle = "white";
426
+ ctx.fillRect(0, 0, w, h);
427
+
428
+ // Prepare Source Data (Pixel reading)
429
+ let sourceData = null;
430
+ let sw = 0, sh = 0;
431
+
432
+ // Draw source to helper canvas to read pixels
433
+ let activeSource = (currentMode === 'video') ? sourceVideo : sourceImage;
434
+
435
+ if (activeSource && ((activeSource.tagName === 'IMG' && activeSource.src) || (activeSource.tagName === 'VIDEO' && activeSource.readyState >= 2))) {
436
+ helperCanvas.width = w;
437
+ helperCanvas.height = h;
438
+ // Scale object fit cover
439
+ // Simple fit: stretch for now to match python logic
440
+ helperCtx.drawImage(activeSource, 0, 0, w, h);
441
+
442
+ // Get all pixels at once (Optimization)
443
+ const imgData = helperCtx.getImageData(0, 0, w, h);
444
+ sourceData = imgData.data; // R,G,B,A array
445
+ sw = w;
446
+ }
447
+
448
+ // Draw Grid
449
+ ctx.fillStyle = "black";
450
+
451
+ const step = w / density;
452
+ const rows = Math.ceil(h / step);
453
+ const cellWMax = step * (1.0 - gapX);
454
+ const cellHMax = step * (1.0 - gapY);
455
+
456
+ for (let r = 0; r < rows; r++) {
457
+ const rowShift = (r * stagger * step);
458
+ // Optimization range
459
+ for (let c = -Math.floor(density/2); c < density * 1.5; c++) {
460
+ const gridX = c * step + rowShift;
461
+ const gridY = r * step;
462
+
463
+ if (gridX < -step || gridX > w) continue;
464
+
465
+ // 1. Visibility / Mask Logic
466
+ let brightness = 0; // Black
467
+ let isVisible = true;
468
+
469
+ if (sourceData) {
470
+ const sx = Math.max(0, Math.min(Math.floor(gridX + step/2), w - 1));
471
+ const sy = Math.max(0, Math.min(Math.floor(gridY + step/2), h - 1));
472
+
473
+ const idx = (sy * sw + sx) * 4;
474
+ const rVal = sourceData[idx];
475
+ const gVal = sourceData[idx+1];
476
+ const bVal = sourceData[idx+2];
477
+
478
+ // Simple grayscale
479
+ brightness = (rVal + gVal + bVal) / 3 / 255.0;
480
+
481
+ const limit = 0.5;
482
+ isVisible = invert ? (brightness > limit) : (brightness < limit);
483
+ }
484
+
485
+ if (!isVisible) continue;
486
+
487
+ // 2. Growth Logic
488
+ let wave = 0;
489
+ if (growthType === 'sine') {
490
+ wave = (Math.sin(c * 0.3 + r * 0.3) + 1) / 2;
491
+ } else if (growthType === 'linear') {
492
+ wave = ((c % density) / density);
493
+ } else if (growthType === 'steps') {
494
+ wave = (r % 5) / 5.0;
495
+ }
496
+
497
+ let modW = 1.0 - (wave * growth);
498
+ modW = Math.max(0.01, Math.min(1.0, modW));
499
+
500
+ const contentW = cellWMax * baseScale * modW;
501
+ const contentH = cellHMax * baseScale;
502
+
503
+ const cx = gridX + step/2;
504
+ const cy = gridY + step/2;
505
+
506
+ // Draw Shape
507
+ ctx.save();
508
+ ctx.translate(cx, cy);
509
+
510
+ if (customShapeLoaded) {
511
+ // Draw SVG image
512
+ // Needs scaling calc to match contentW/H
513
+ // Assuming square SVG
514
+ ctx.drawImage(shapeImg, -contentW/2, -contentH/2, contentW, contentH);
515
+ } else {
516
+ // Default Rect
517
+ ctx.fillRect(-contentW/2, -contentH/2, contentW, contentH);
518
+ }
519
+ ctx.restore();
520
+ }
521
+ }
522
+ }
523
+
524
+ // --- EXPORT HANDLERS ---
525
+
526
+ // 1. Download SVG (via Python for correctness or JS)
527
+ // Using Python to keep SVG logic clean and downloadable
528
+ document.getElementById('btn-dl-svg').addEventListener('click', async () => {
529
+ const form = new FormData();
530
+ // Append all inputs
531
+ paramIds.forEach(id => form.append(id, document.getElementById(id).value));
532
+ form.append('growth_type', document.getElementById('growth_type').value);
533
+ form.append('invert', document.getElementById('invert').checked);
534
+
535
+ const shp = document.getElementById('shape_svg').files[0];
536
+ if(shp) form.append('shape_svg', shp);
537
+
538
+ const rast = document.getElementById('raster_image').files[0];
539
+ if(rast) form.append('raster_image', rast);
540
+
541
+ const btn = document.getElementById('btn-dl-svg');
542
+ btn.innerText = "Generating...";
543
+
544
+ const res = await fetch('/download_svg', { method: 'POST', body: form });
545
+ if(res.ok) {
546
+ const blob = await res.blob();
547
+ const url = URL.createObjectURL(blob);
548
+ const a = document.createElement('a');
549
+ a.href = url;
550
+ a.download = 'pattern.svg';
551
+ a.click();
552
+ } else {
553
+ alert("Error generating SVG");
554
+ }
555
+ btn.innerText = "SVG IMAGE";
556
+ });
557
+
558
+ // 2. Download Video (via Python)
559
+ document.getElementById('btn-dl-video').addEventListener('click', async () => {
560
+ const form = new FormData();
561
+ paramIds.forEach(id => form.append(id, document.getElementById(id).value));
562
+ form.append('growth_type', document.getElementById('growth_type').value);
563
+ form.append('invert', document.getElementById('invert').checked);
564
+
565
+ const vid = document.getElementById('video_file').files[0];
566
+ if(!vid) { alert("Please upload a video first"); return; }
567
+ form.append('video_file', vid);
568
+
569
+ const btn = document.getElementById('btn-dl-video');
570
+ btn.innerText = "Rendering (Wait)...";
571
+ btn.disabled = true;
572
+ document.getElementById('status-msg').innerText = "Processing video on server. This may take a minute...";
573
+
574
+ try {
575
+ const res = await fetch('/process_video', { method: 'POST', body: form });
576
+ if(res.ok) {
577
+ const blob = await res.blob();
578
+ const url = URL.createObjectURL(blob);
579
+ const a = document.createElement('a');
580
+ a.href = url;
581
+ a.download = 'processed_video.mp4';
582
+ a.click();
583
+ document.getElementById('status-msg').innerText = "Done!";
584
+ } else {
585
+ const txt = await res.text();
586
+ alert("Error: " + txt);
587
+ document.getElementById('status-msg').innerText = "Error.";
588
+ }
589
+ } catch(e) {
590
+ console.error(e);
591
+ alert("Network error");
592
+ }
593
+ btn.innerText = "RENDER MP4";
594
+ btn.disabled = false;
595
+ });
596
+
597
+ // Init
598
+ requestRender();
599
+ </script>
600
+
601
+ </body>
602
+ </html>