by3axap commited on
Commit
31ac10d
·
verified ·
1 Parent(s): 628e6e4

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +211 -92
templates/index.html CHANGED
@@ -1,15 +1,21 @@
 
 
1
  <head>
2
  <meta charset="UTF-8">
3
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
4
  <title>CELL CLIPPER PRO</title>
5
  <style>
6
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=JetBrains+Mono&display=swap');
 
7
  :root {
8
  --bg-page: #ffffff;
9
  --text-main: #000000;
 
 
10
  --font-sans: 'Inter', sans-serif;
11
  --font-mono: 'JetBrains Mono', monospace;
12
  }
 
13
  body {
14
  margin: 0;
15
  display: flex;
@@ -19,6 +25,7 @@
19
  color: var(--text-main);
20
  overflow: hidden;
21
  }
 
22
  /* --- Sidebar --- */
23
  .sidebar {
24
  width: 340px;
@@ -32,7 +39,9 @@
32
  overflow-y: auto;
33
  flex-shrink: 0;
34
  }
 
35
  .section { margin-bottom: 40px; }
 
36
  .section-title {
37
  font-family: var(--font-mono);
38
  font-size: 10px;
@@ -42,8 +51,10 @@
42
  margin-bottom: 16px;
43
  display: block;
44
  }
 
45
  /* --- Controls --- */
46
  .control-group { margin-bottom: 16px; width: 100%; }
 
47
  .label-row {
48
  display: flex;
49
  justify-content: space-between;
@@ -52,6 +63,7 @@
52
  margin-bottom: 4px;
53
  font-family: var(--font-mono);
54
  }
 
55
  /* Input Number Styling - No underline, plain text */
56
  .num-input {
57
  width: 50px;
@@ -64,7 +76,7 @@
64
  color: var(--text-main);
65
  padding: 0;
66
  }
67
- .num-input:focus { color: blue; }
68
 
69
  /* Custom Range Slider Styling */
70
  input[type="range"] {
@@ -74,24 +86,31 @@
74
  margin: 10px 0;
75
  cursor: pointer;
76
  }
 
 
77
  input[type="range"]::-webkit-slider-runnable-track {
78
  width: 100%;
79
  height: 1px;
80
  background: #000000;
81
  border: none;
82
  }
 
 
83
  input[type="range"]::-webkit-slider-thumb {
84
  -webkit-appearance: none;
85
  height: 14px;
86
  width: 4px;
87
  background: #000000;
88
  border-radius: 0;
89
- margin-top: -6.5px;
90
  border: none;
91
  }
 
92
  input[type="range"]:focus { outline: none; }
 
93
  /* --- File Buttons --- */
94
  input[type="file"] { display: none; }
 
95
  .btn-upload {
96
  display: flex;
97
  justify-content: center;
@@ -109,14 +128,18 @@
109
  box-sizing: border-box;
110
  text-align: center;
111
  }
 
112
  .btn-upload:hover {
113
  background: #f5f5f5;
114
  }
 
 
115
  .btn-upload.filled {
116
  background: #000000;
117
  color: #ffffff;
118
  border-color: #000000;
119
  }
 
120
  .remove-link {
121
  display: block;
122
  text-align: center;
@@ -131,8 +154,10 @@
131
  width: 100%;
132
  }
133
  .remove-link:hover { color: red; }
 
134
  /* --- Action Buttons --- */
135
  .buttons-container { display: flex; gap: 12px; margin-top: auto; padding-top: 20px;}
 
136
  .btn-action {
137
  flex: 1;
138
  background: #000000;
@@ -158,22 +183,25 @@
158
  border-color: #ccc;
159
  color: #fff;
160
  }
 
161
  /* --- Preview Area --- */
162
  .preview-area {
163
  flex: 1;
164
- background-color: #ffffff; /* Default, will be overridden by JS */
165
  display: flex;
166
  align-items: center;
167
  justify-content: center;
168
  position: relative;
169
  padding: 40px;
170
  }
 
171
  canvas {
172
- box-shadow: 0 0 0 1px #f0f0f0;
173
  max-width: 100%;
174
  max-height: 100%;
175
  object-fit: contain;
176
  }
 
177
  /* Misc */
178
  select {
179
  width: 100%;
@@ -187,45 +215,80 @@
187
  border-radius: 0;
188
  -webkit-appearance: none;
189
  }
 
 
190
  input[type="checkbox"] {
191
  accent-color: black;
192
- width: 13px;
193
- height: 13px;
194
- }
195
- /* --- Custom Color Picker Styling --- */
196
- input[type="color"] {
197
- -webkit-appearance: none;
198
- border: 1px solid #000000;
199
- width: 13px;
200
- height: 13px;
201
- padding: 0;
202
- background: none;
203
- cursor: pointer;
204
- border-radius: 0;
205
- }
206
- input[type="color"]::-webkit-color-swatch-wrapper {
207
- padding: 0;
208
- }
209
- input[type="color"]::-webkit-color-swatch {
210
- border: none;
211
- border-radius: 0;
212
  }
 
213
  /* Hidden sources */
214
  #source-video, #source-image { display: none; }
215
  </style>
216
  </head>
217
  <body>
 
218
  <div class="sidebar">
219
  <div style="margin-bottom: 40px;">
220
  <h1 style="font-size: 14px; font-weight: 600; margin:0; letter-spacing: -0.02em;">CELL CLIPPER PRO</h1>
221
  </div>
 
 
 
 
 
 
 
 
222
  <div class="section">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  <div class="control-group">
224
  <div class="label-row">
225
  <span>Stagger</span>
226
  <input type="number" class="num-input" id="num-shift_stagger" value="0" step="0.01" min="0" max="1">
227
  </div>
228
  <input type="range" id="shift_stagger" min="0" max="1" step="0.01" value="0">
 
 
 
 
 
 
 
 
 
229
 
230
  <div class="control-group">
231
  <div class="label-row">
@@ -234,14 +297,17 @@
234
  </div>
235
  <input type="range" id="growth" min="0" max="2.0" step="0.05" value="0">
236
  </div>
237
- <div class="control-group" style="display:flex; justify-content: space-between; align-items: center; margin-top:24px;">
 
 
 
 
238
  <span>Invert Logic</span>
239
  </label>
240
  </div>
241
- <div style="display:flex; align-items:center; gap:8px;">
242
- <span style="font-size:11px; font-family:var(--font-mono); cursor: default;">COLOR</span>
243
- <input type="color" id="bg_color" value="#ffffff" title="Background Color">
244
- <div class="control-group" style="margin-top:16px;">
245
  <span style="font-size:10px; color:#aaa; font-family:var(--font-mono); display:block; margin-bottom:4px;">GROWTH TYPE</span>
246
  <select id="growth_type">
247
  <option value="sine">Diagonal Sine</option>
@@ -250,49 +316,58 @@
250
  </select>
251
  </div>
252
  </div>
 
 
 
253
  <button class="btn-action secondary" id="btn-dl-svg">Download SVG</button>
254
  <button class="btn-action" id="btn-dl-video" disabled>Export MP4</button>
255
  </div>
256
  <div id="status-msg" style="font-size:10px; font-family:var(--font-mono); margin-top:10px; text-align:center;"></div>
257
  </div>
258
- <div class="preview-area" id="preview-wrapper">
 
259
  <canvas id="main-canvas" width="1400" height="900"></canvas>
 
260
  <img id="source-image" src="" crossorigin="anonymous">
261
  <video id="source-video" src="" playsinline loop muted crossorigin="anonymous"></video>
262
  <canvas id="helper-canvas" style="display:none;"></canvas>
 
 
 
263
  <script>
264
  // --- STATE ---
265
  let currentMode = 'image'; // 'image' | 'video'
266
  let currentFile = null; // Храним сам объект файла
267
  let animationId = null;
268
  let customShapeLoaded = false;
269
- // Cached colored shape canvas to avoid recoloring every frame
270
- let cachedShapeCanvas = null;
271
- let lastFillColor = "#000000";
272
  // --- DOM ---
273
  const canvas = document.getElementById('main-canvas');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  }
275
  };
276
- // --- HELPER: TINT SHAPE ---
277
- function updateCachedShape(color) {
278
- if (!customShapeLoaded || !shapeImg.src) {
279
- cachedShapeCanvas = null;
280
- return;
281
- }
282
- const c = document.createElement('canvas');
283
- c.width = shapeImg.width || 100;
284
- c.height = shapeImg.height || 100;
285
- const x = c.getContext('2d');
286
- // Draw original shape
287
- x.drawImage(shapeImg, 0, 0, c.width, c.height);
288
- // Composite operation to tint non-transparent pixels
289
- x.globalCompositeOperation = 'source-in';
290
- x.fillStyle = color;
291
- x.fillRect(0, 0, c.width, c.height);
292
- cachedShapeCanvas = c;
293
- lastFillColor = color;
294
- }
295
  // --- EVENT LISTENERS ---
 
296
  // 1. Shape Upload
297
  const shapeInput = document.getElementById('shape_svg');
298
  shapeInput.addEventListener('change', (e) => {
@@ -304,33 +379,42 @@
304
  const reader = new FileReader();
305
  reader.onload = (evt) => {
306
  const blob = new Blob([evt.target.result], {type: 'image/svg+xml'});
 
 
 
307
  reader.readAsText(file);
308
  });
 
309
  document.getElementById('rm-shape').addEventListener('click', () => {
310
  shapeInput.value = '';
311
  customShapeLoaded = false;
312
  shapeImg.src = '';
313
- cachedShapeCanvas = null;
314
  setBtnState('lbl-shape', 'rm-shape', null, false);
315
  requestRender();
316
  });
 
317
  // 2. Content Upload (Unified)
318
  const contentInput = document.getElementById('content_file');
319
  contentInput.addEventListener('change', (e) => {
320
  const file = e.target.files[0];
321
  if(!file) return;
 
322
  currentFile = file; // Save for export
323
  setBtnState('lbl-content', 'rm-content', file, true);
 
324
  const url = URL.createObjectURL(file);
325
  const type = file.type;
 
326
  // Reset previous sources
327
  sourceImage.src = "";
328
  sourceVideo.src = "";
329
  sourceVideo.pause();
 
330
  if (type.startsWith('image/')) {
331
  currentMode = 'image';
332
  sourceImage.src = url;
333
  sourceImage.onload = () => requestRender();
 
334
  document.getElementById('btn-dl-video').disabled = true;
335
  document.getElementById('btn-dl-svg').disabled = false;
336
  }
@@ -338,11 +422,13 @@
338
  currentMode = 'video';
339
  sourceVideo.src = url;
340
  sourceVideo.play();
 
341
  document.getElementById('btn-dl-video').disabled = false;
342
- document.getElementById('btn-dl-svg').disabled = true;
343
  requestRender();
344
  }
345
  });
 
346
  document.getElementById('rm-content').addEventListener('click', () => {
347
  contentInput.value = '';
348
  currentFile = null;
@@ -350,64 +436,72 @@
350
  sourceVideo.src = "";
351
  sourceVideo.pause();
352
  setBtnState('lbl-content', 'rm-content', null, false);
 
353
  // Reset defaults
354
  currentMode = 'image';
355
  document.getElementById('btn-dl-video').disabled = true;
356
  document.getElementById('btn-dl-svg').disabled = false;
357
  requestRender();
358
  });
 
359
  // 3. Inputs Sync
360
  const paramIds = ['density', 'gap_x', 'gap_y', 'shift_stagger', 'base_scale', 'growth'];
361
  paramIds.forEach(id => {
362
  const slider = document.getElementById(id);
363
  const number = document.getElementById('num-' + id);
 
364
  slider.addEventListener('input', () => { number.value = slider.value; requestRender(); });
365
  number.addEventListener('input', () => { slider.value = number.value; requestRender(); });
366
  });
 
367
  document.getElementById('invert').addEventListener('change', requestRender);
368
  document.getElementById('growth_type').addEventListener('change', requestRender);
369
- // 4. Color Inputs
370
- const bgColorInput = document.getElementById('bg_color');
371
- const fillColorInput = document.getElementById('fill_color');
372
- bgColorInput.addEventListener('input', () => {
373
- // Меняем цвет фона страницы предпросмотра для визуального комфорта
374
- previewWrapper.style.backgroundColor = bgColorInput.value;
375
- requestRender();
376
- });
377
- fillColorInput.addEventListener('input', () => {
378
- const color = fillColorInput.value;
379
- if(customShapeLoaded) {
380
- updateCachedShape(color);
381
- }
382
- requestRender();
383
- });
384
  // --- RENDER LOGIC ---
385
  function requestRender() {
386
  if (currentMode === 'video' && !sourceVideo.paused) {
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  }
 
388
  function drawFrame() {
389
  const w = canvas.width;
390
- const h = canvas.height;
 
391
  // Params
392
  const density = parseInt(document.getElementById('density').value);
393
  const gapX = parseFloat(document.getElementById('gap_x').value);
394
  const gapY = parseFloat(document.getElementById('gap_y').value);
 
395
  const baseScale = parseFloat(document.getElementById('base_scale').value);
396
  const growth = parseFloat(document.getElementById('growth').value);
397
  const growthType = document.getElementById('growth_type').value;
398
  const invert = document.getElementById('invert').checked;
399
- const bgColor = bgColorInput.value;
400
- const fillColor = fillColorInput.value;
401
- // Clear with BG Color
402
- ctx.fillStyle = bgColor;
403
  ctx.fillRect(0, 0, w, h);
 
404
  // Read Pixel Data
405
  let sourceData = null;
406
- let sw = 0;
 
407
  let activeSource = (currentMode === 'video') ? sourceVideo : sourceImage;
408
  let ready = false;
409
  if(currentMode === 'image' && sourceImage.src) ready = true;
410
  if(currentMode === 'video' && sourceVideo.readyState >= 2) ready = true;
 
411
  if (ready) {
412
  helperCanvas.width = w;
413
  helperCanvas.height = h;
@@ -416,21 +510,28 @@
416
  sourceData = imgData.data;
417
  sw = w;
418
  }
 
419
  // Draw Grid
420
- ctx.fillStyle = fillColor;
 
421
  const step = w / density;
422
  const rows = Math.ceil(h / step);
423
  const cellWMax = step * (1.0 - gapX);
424
  const cellHMax = step * (1.0 - gapY);
 
425
  // Optimization: loop range
426
  const startC = -Math.floor(density * 0.5);
427
  const endC = density * 1.5;
 
428
  for (let r = 0; r < rows; r++) {
429
- const rowShift = (r * stagger * step);
 
430
  for (let c = startC; c < endC; c++) {
431
  const gridX = c * step + rowShift;
432
  const gridY = r * step;
 
433
  if (gridX < -step || gridX > w) continue;
 
434
  // 1. Brightness
435
  let isVisible = true;
436
  if (sourceData) {
@@ -440,56 +541,68 @@
440
  const rV = sourceData[idx];
441
  const gV = sourceData[idx+1];
442
  const bV = sourceData[idx+2];
443
- const bright = (rV + gV + bV) / 3 / 255.0;
 
444
  const limit = 0.5;
445
  isVisible = invert ? (bright > limit) : (bright < limit);
446
  }
 
447
  if (!isVisible) continue;
 
448
  // 2. Growth
449
  let wave = 0;
450
  if (growthType === 'sine') wave = (Math.sin(c * 0.3 + r * 0.3) + 1) / 2;
451
  else if (growthType === 'linear') wave = ((c % density) / density);
452
  else if (growthType === 'steps') wave = (r % 5) / 5.0;
 
453
  let modW = 1.0 - (wave * growth);
454
  modW = Math.max(0.01, Math.min(1.0, modW));
 
455
  const contentW = cellWMax * baseScale * modW;
456
- const contentH = cellHMax * baseScale;
 
457
  const cx = gridX + step/2;
458
  const cy = gridY + step/2;
 
459
  // Draw
460
  ctx.save();
461
- ctx.translate(cx, cy);
462
- if (customShapeLoaded && cachedShapeCanvas) {
463
- // Draw tinted shape
464
- ctx.drawImage(cachedShapeCanvas, -contentW/2, -contentH/2, contentW, contentH);
465
  } else {
466
- // Draw simple rect
467
  ctx.fillRect(-contentW/2, -contentH/2, contentW, contentH);
468
  }
469
  ctx.restore();
470
  }
471
  }
472
  }
 
473
  // --- EXPORT ---
 
474
  // Download SVG
475
  document.getElementById('btn-dl-svg').addEventListener('click', async () => {
476
  const btn = document.getElementById('btn-dl-svg');
477
  btn.innerText = "Processing...";
 
478
  const form = new FormData();
479
  paramIds.forEach(id => form.append(id, document.getElementById(id).value));
480
  form.append('growth_type', document.getElementById('growth_type').value);
481
  form.append('invert', document.getElementById('invert').checked);
482
- form.append('bg_color', document.getElementById('bg_color').value);
483
- form.append('fill_color', document.getElementById('fill_color').value);
484
  // Attach SVG Shape
485
- if(shapeInput.files[0]) form.append('shape_svg', shapeInput.files[0]);
 
486
  // Attach Image (if existing)
 
487
  if(currentFile && currentMode === 'image') {
488
  form.append('raster_image', currentFile);
489
  }
 
490
  try {
491
  const resp = await fetch('/download_svg', { method: 'POST', body: form });
492
- if (!resp.ok) throw new Error(await resp.text());
 
493
  const blob = await resp.blob();
494
  const url = window.URL.createObjectURL(blob);
495
  const a = document.createElement('a');
@@ -503,25 +616,29 @@
503
  }
504
  btn.innerText = "Download SVG";
505
  });
 
506
  // Download Video
507
  document.getElementById('btn-dl-video').addEventListener('click', async () => {
508
  const btn = document.getElementById('btn-dl-video');
509
  btn.innerText = "Rendering...";
510
  btn.disabled = true;
511
  document.getElementById('status-msg').innerText = "Processing on server. Please wait...";
 
512
  const form = new FormData();
513
  paramIds.forEach(id => form.append(id, document.getElementById(id).value));
514
  form.append('growth_type', document.getElementById('growth_type').value);
515
  form.append('invert', document.getElementById('invert').checked);
516
- form.append('bg_color', document.getElementById('bg_color').value);
517
- form.append('fill_color', document.getElementById('fill_color').value);
518
  // Attach Video
 
519
  if(currentFile && currentMode === 'video') {
520
  form.append('video_file', currentFile);
521
  }
 
522
  try {
523
  const resp = await fetch('/process_video', { method: 'POST', body: form });
524
- if (!resp.ok) throw new Error(await resp.text());
 
525
  const blob = await resp.blob();
526
  const url = window.URL.createObjectURL(blob);
527
  const a = document.createElement('a');
@@ -538,8 +655,10 @@
538
  btn.innerText = "Export MP4";
539
  btn.disabled = false;
540
  });
 
541
  // Init
542
  requestRender();
 
543
  </script>
544
  </body>
545
- </html>

 
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</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: #aaaaaa;
14
+ --border: #e0e0e0;
15
  --font-sans: 'Inter', sans-serif;
16
  --font-mono: 'JetBrains Mono', monospace;
17
  }
18
+
19
  body {
20
  margin: 0;
21
  display: flex;
 
25
  color: var(--text-main);
26
  overflow: hidden;
27
  }
28
+
29
  /* --- Sidebar --- */
30
  .sidebar {
31
  width: 340px;
 
39
  overflow-y: auto;
40
  flex-shrink: 0;
41
  }
42
+
43
  .section { margin-bottom: 40px; }
44
+
45
  .section-title {
46
  font-family: var(--font-mono);
47
  font-size: 10px;
 
51
  margin-bottom: 16px;
52
  display: block;
53
  }
54
+
55
  /* --- Controls --- */
56
  .control-group { margin-bottom: 16px; width: 100%; }
57
+
58
  .label-row {
59
  display: flex;
60
  justify-content: space-between;
 
63
  margin-bottom: 4px;
64
  font-family: var(--font-mono);
65
  }
66
+
67
  /* Input Number Styling - No underline, plain text */
68
  .num-input {
69
  width: 50px;
 
76
  color: var(--text-main);
77
  padding: 0;
78
  }
79
+ .num-input:focus { color: blue; } /* Индикация фокуса цветом, а не линией */
80
 
81
  /* Custom Range Slider Styling */
82
  input[type="range"] {
 
86
  margin: 10px 0;
87
  cursor: pointer;
88
  }
89
+
90
+ /* The Track (Thin black line) */
91
  input[type="range"]::-webkit-slider-runnable-track {
92
  width: 100%;
93
  height: 1px;
94
  background: #000000;
95
  border: none;
96
  }
97
+
98
+ /* The Thumb (Thick black line/rect) */
99
  input[type="range"]::-webkit-slider-thumb {
100
  -webkit-appearance: none;
101
  height: 14px;
102
  width: 4px;
103
  background: #000000;
104
  border-radius: 0;
105
+ margin-top: -6.5px; /* (height/2 - track_height/2) */
106
  border: none;
107
  }
108
+
109
  input[type="range"]:focus { outline: none; }
110
+
111
  /* --- File Buttons --- */
112
  input[type="file"] { display: none; }
113
+
114
  .btn-upload {
115
  display: flex;
116
  justify-content: center;
 
128
  box-sizing: border-box;
129
  text-align: center;
130
  }
131
+
132
  .btn-upload:hover {
133
  background: #f5f5f5;
134
  }
135
+
136
+ /* Active State (Black Filled) */
137
  .btn-upload.filled {
138
  background: #000000;
139
  color: #ffffff;
140
  border-color: #000000;
141
  }
142
+
143
  .remove-link {
144
  display: block;
145
  text-align: center;
 
154
  width: 100%;
155
  }
156
  .remove-link:hover { color: red; }
157
+
158
  /* --- Action Buttons --- */
159
  .buttons-container { display: flex; gap: 12px; margin-top: auto; padding-top: 20px;}
160
+
161
  .btn-action {
162
  flex: 1;
163
  background: #000000;
 
183
  border-color: #ccc;
184
  color: #fff;
185
  }
186
+
187
  /* --- Preview Area --- */
188
  .preview-area {
189
  flex: 1;
190
+ background-color: #ffffff; /* White background */
191
  display: flex;
192
  align-items: center;
193
  justify-content: center;
194
  position: relative;
195
  padding: 40px;
196
  }
197
+
198
  canvas {
199
+ box-shadow: 0 0 0 1px #f0f0f0; /* Subtle border for visibility on white */
200
  max-width: 100%;
201
  max-height: 100%;
202
  object-fit: contain;
203
  }
204
+
205
  /* Misc */
206
  select {
207
  width: 100%;
 
215
  border-radius: 0;
216
  -webkit-appearance: none;
217
  }
218
+
219
+ /* Checkbox custom styling (simple) */
220
  input[type="checkbox"] {
221
  accent-color: black;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  }
223
+
224
  /* Hidden sources */
225
  #source-video, #source-image { display: none; }
226
  </style>
227
  </head>
228
  <body>
229
+
230
  <div class="sidebar">
231
  <div style="margin-bottom: 40px;">
232
  <h1 style="font-size: 14px; font-weight: 600; margin:0; letter-spacing: -0.02em;">CELL CLIPPER PRO</h1>
233
  </div>
234
+
235
+ <div class="section">
236
+ <span class="section-title">01 / Shape Pattern</span>
237
+ <input type="file" id="shape_svg" accept=".svg">
238
+ <label for="shape_svg" class="btn-upload" id="lbl-shape">Select SVG</label>
239
+ <button id="rm-shape" class="remove-link" style="display:none;">remove</button>
240
+ </div>
241
+
242
  <div class="section">
243
+ <span class="section-title">02 / Content Source</span>
244
+ <input type="file" id="content_file" accept=".png, .jpg, .jpeg, .mp4, .mov, .webm">
245
+ <label for="content_file" class="btn-upload" id="lbl-content">Select Content</label>
246
+ <button id="rm-content" class="remove-link" style="display:none;">remove</button>
247
+ </div>
248
+
249
+ <form id="params-form">
250
+ <div class="section">
251
+ <span class="section-title">03 / Parameters</span>
252
+
253
+ <div class="control-group">
254
+ <div class="label-row">
255
+ <span>Density</span>
256
+ <input type="number" class="num-input" id="num-density" value="40" min="5" max="150">
257
+ </div>
258
+ <input type="range" id="density" min="5" max="150" value="40">
259
+ </div>
260
+
261
+ <div class="control-group">
262
+ <div class="label-row">
263
+ <span>Gap X</span>
264
+ <input type="number" class="num-input" id="num-gap_x" value="0" step="0.01" min="0" max="0.9">
265
+ </div>
266
+ <input type="range" id="gap_x" min="0" max="0.9" step="0.01" value="0">
267
+ </div>
268
+
269
+ <div class="control-group">
270
+ <div class="label-row">
271
+ <span>Gap Y</span>
272
+ <input type="number" class="num-input" id="num-gap_y" value="0" step="0.01" min="0" max="0.9">
273
+ </div>
274
+ <input type="range" id="gap_y" min="0" max="0.9" step="0.01" value="0">
275
+ </div>
276
+
277
  <div class="control-group">
278
  <div class="label-row">
279
  <span>Stagger</span>
280
  <input type="number" class="num-input" id="num-shift_stagger" value="0" step="0.01" min="0" max="1">
281
  </div>
282
  <input type="range" id="shift_stagger" min="0" max="1" step="0.01" value="0">
283
+ </div>
284
+
285
+ <div class="control-group" style="margin-top:24px;">
286
+ <div class="label-row">
287
+ <span>Scale</span>
288
+ <input type="number" class="num-input" id="num-base_scale" value="1.0" step="0.05" min="0.1" max="2.0">
289
+ </div>
290
+ <input type="range" id="base_scale" min="0.1" max="2.0" step="0.05" value="1.0">
291
+ </div>
292
 
293
  <div class="control-group">
294
  <div class="label-row">
 
297
  </div>
298
  <input type="range" id="growth" min="0" max="2.0" step="0.05" value="0">
299
  </div>
300
+
301
+ <div class="control-group">
302
+ <div class="label-row">
303
+ <label style="display:flex; align-items:center; gap:8px; cursor:pointer;">
304
+ <input type="checkbox" id="invert">
305
  <span>Invert Logic</span>
306
  </label>
307
  </div>
308
+ </div>
309
+
310
+ <div class="control-group">
 
311
  <span style="font-size:10px; color:#aaa; font-family:var(--font-mono); display:block; margin-bottom:4px;">GROWTH TYPE</span>
312
  <select id="growth_type">
313
  <option value="sine">Diagonal Sine</option>
 
316
  </select>
317
  </div>
318
  </div>
319
+ </form>
320
+
321
+ <div class="buttons-container">
322
  <button class="btn-action secondary" id="btn-dl-svg">Download SVG</button>
323
  <button class="btn-action" id="btn-dl-video" disabled>Export MP4</button>
324
  </div>
325
  <div id="status-msg" style="font-size:10px; font-family:var(--font-mono); margin-top:10px; text-align:center;"></div>
326
  </div>
327
+
328
+ <div class="preview-area">
329
  <canvas id="main-canvas" width="1400" height="900"></canvas>
330
+
331
  <img id="source-image" src="" crossorigin="anonymous">
332
  <video id="source-video" src="" playsinline loop muted crossorigin="anonymous"></video>
333
  <canvas id="helper-canvas" style="display:none;"></canvas>
334
+ <img id="shape-img" src="" style="display:none;">
335
+ </div>
336
+
337
  <script>
338
  // --- STATE ---
339
  let currentMode = 'image'; // 'image' | 'video'
340
  let currentFile = null; // Храним сам объект файла
341
  let animationId = null;
342
  let customShapeLoaded = false;
343
+
 
 
344
  // --- DOM ---
345
  const canvas = document.getElementById('main-canvas');
346
+ const ctx = canvas.getContext('2d', { alpha: false });
347
+ const helperCanvas = document.getElementById('helper-canvas');
348
+ const helperCtx = helperCanvas.getContext('2d');
349
+
350
+ const sourceImage = document.getElementById('source-image');
351
+ const sourceVideo = document.getElementById('source-video');
352
+ const shapeImg = document.getElementById('shape-img');
353
+
354
+ // --- UI HELPERS ---
355
+ const setBtnState = (labelId, rmId, file, isActive) => {
356
+ const lbl = document.getElementById(labelId);
357
+ const rm = document.getElementById(rmId);
358
+ if(isActive && file) {
359
+ lbl.classList.add('filled');
360
+ lbl.innerText = file.name;
361
+ rm.style.display = 'block';
362
+ } else {
363
+ lbl.classList.remove('filled');
364
+ lbl.innerText = labelId === 'lbl-shape' ? 'Select SVG' : 'Select Content';
365
+ rm.style.display = 'none';
366
  }
367
  };
368
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
  // --- EVENT LISTENERS ---
370
+
371
  // 1. Shape Upload
372
  const shapeInput = document.getElementById('shape_svg');
373
  shapeInput.addEventListener('change', (e) => {
 
379
  const reader = new FileReader();
380
  reader.onload = (evt) => {
381
  const blob = new Blob([evt.target.result], {type: 'image/svg+xml'});
382
+ shapeImg.src = URL.createObjectURL(blob);
383
+ shapeImg.onload = () => { customShapeLoaded = true; requestRender(); };
384
+ };
385
  reader.readAsText(file);
386
  });
387
+
388
  document.getElementById('rm-shape').addEventListener('click', () => {
389
  shapeInput.value = '';
390
  customShapeLoaded = false;
391
  shapeImg.src = '';
 
392
  setBtnState('lbl-shape', 'rm-shape', null, false);
393
  requestRender();
394
  });
395
+
396
  // 2. Content Upload (Unified)
397
  const contentInput = document.getElementById('content_file');
398
  contentInput.addEventListener('change', (e) => {
399
  const file = e.target.files[0];
400
  if(!file) return;
401
+
402
  currentFile = file; // Save for export
403
  setBtnState('lbl-content', 'rm-content', file, true);
404
+
405
  const url = URL.createObjectURL(file);
406
  const type = file.type;
407
+
408
  // Reset previous sources
409
  sourceImage.src = "";
410
  sourceVideo.src = "";
411
  sourceVideo.pause();
412
+
413
  if (type.startsWith('image/')) {
414
  currentMode = 'image';
415
  sourceImage.src = url;
416
  sourceImage.onload = () => requestRender();
417
+
418
  document.getElementById('btn-dl-video').disabled = true;
419
  document.getElementById('btn-dl-svg').disabled = false;
420
  }
 
422
  currentMode = 'video';
423
  sourceVideo.src = url;
424
  sourceVideo.play();
425
+
426
  document.getElementById('btn-dl-video').disabled = false;
427
+ document.getElementById('btn-dl-svg').disabled = true; // SVG export only for static
428
  requestRender();
429
  }
430
  });
431
+
432
  document.getElementById('rm-content').addEventListener('click', () => {
433
  contentInput.value = '';
434
  currentFile = null;
 
436
  sourceVideo.src = "";
437
  sourceVideo.pause();
438
  setBtnState('lbl-content', 'rm-content', null, false);
439
+
440
  // Reset defaults
441
  currentMode = 'image';
442
  document.getElementById('btn-dl-video').disabled = true;
443
  document.getElementById('btn-dl-svg').disabled = false;
444
  requestRender();
445
  });
446
+
447
  // 3. Inputs Sync
448
  const paramIds = ['density', 'gap_x', 'gap_y', 'shift_stagger', 'base_scale', 'growth'];
449
  paramIds.forEach(id => {
450
  const slider = document.getElementById(id);
451
  const number = document.getElementById('num-' + id);
452
+
453
  slider.addEventListener('input', () => { number.value = slider.value; requestRender(); });
454
  number.addEventListener('input', () => { slider.value = number.value; requestRender(); });
455
  });
456
+
457
  document.getElementById('invert').addEventListener('change', requestRender);
458
  document.getElementById('growth_type').addEventListener('change', requestRender);
459
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
460
  // --- RENDER LOGIC ---
461
  function requestRender() {
462
  if (currentMode === 'video' && !sourceVideo.paused) {
463
+ if (!animationId) renderLoop();
464
+ } else {
465
+ drawFrame();
466
+ }
467
+ }
468
+
469
+ function renderLoop() {
470
+ drawFrame();
471
+ if (currentMode === 'video' && !sourceVideo.paused) {
472
+ animationId = requestAnimationFrame(renderLoop);
473
+ } else {
474
+ animationId = null;
475
+ }
476
  }
477
+
478
  function drawFrame() {
479
  const w = canvas.width;
480
+ const h = canvas.height;
481
+
482
  // Params
483
  const density = parseInt(document.getElementById('density').value);
484
  const gapX = parseFloat(document.getElementById('gap_x').value);
485
  const gapY = parseFloat(document.getElementById('gap_y').value);
486
+ const stagger = parseFloat(document.getElementById('shift_stagger').value);
487
  const baseScale = parseFloat(document.getElementById('base_scale').value);
488
  const growth = parseFloat(document.getElementById('growth').value);
489
  const growthType = document.getElementById('growth_type').value;
490
  const invert = document.getElementById('invert').checked;
491
+
492
+ // Clear
493
+ ctx.fillStyle = "white";
 
494
  ctx.fillRect(0, 0, w, h);
495
+
496
  // Read Pixel Data
497
  let sourceData = null;
498
+ let sw = 0;
499
+
500
  let activeSource = (currentMode === 'video') ? sourceVideo : sourceImage;
501
  let ready = false;
502
  if(currentMode === 'image' && sourceImage.src) ready = true;
503
  if(currentMode === 'video' && sourceVideo.readyState >= 2) ready = true;
504
+
505
  if (ready) {
506
  helperCanvas.width = w;
507
  helperCanvas.height = h;
 
510
  sourceData = imgData.data;
511
  sw = w;
512
  }
513
+
514
  // Draw Grid
515
+ ctx.fillStyle = "black";
516
+
517
  const step = w / density;
518
  const rows = Math.ceil(h / step);
519
  const cellWMax = step * (1.0 - gapX);
520
  const cellHMax = step * (1.0 - gapY);
521
+
522
  // Optimization: loop range
523
  const startC = -Math.floor(density * 0.5);
524
  const endC = density * 1.5;
525
+
526
  for (let r = 0; r < rows; r++) {
527
+ const rowShift = (r * stagger * step);
528
+
529
  for (let c = startC; c < endC; c++) {
530
  const gridX = c * step + rowShift;
531
  const gridY = r * step;
532
+
533
  if (gridX < -step || gridX > w) continue;
534
+
535
  // 1. Brightness
536
  let isVisible = true;
537
  if (sourceData) {
 
541
  const rV = sourceData[idx];
542
  const gV = sourceData[idx+1];
543
  const bV = sourceData[idx+2];
544
+ const bright = (rV + gV + bV) / 3 / 255.0;
545
+
546
  const limit = 0.5;
547
  isVisible = invert ? (bright > limit) : (bright < limit);
548
  }
549
+
550
  if (!isVisible) continue;
551
+
552
  // 2. Growth
553
  let wave = 0;
554
  if (growthType === 'sine') wave = (Math.sin(c * 0.3 + r * 0.3) + 1) / 2;
555
  else if (growthType === 'linear') wave = ((c % density) / density);
556
  else if (growthType === 'steps') wave = (r % 5) / 5.0;
557
+
558
  let modW = 1.0 - (wave * growth);
559
  modW = Math.max(0.01, Math.min(1.0, modW));
560
+
561
  const contentW = cellWMax * baseScale * modW;
562
+ const contentH = cellHMax * baseScale;
563
+
564
  const cx = gridX + step/2;
565
  const cy = gridY + step/2;
566
+
567
  // Draw
568
  ctx.save();
569
+ ctx.translate(cx, cy);
570
+
571
+ if (customShapeLoaded) {
572
+ ctx.drawImage(shapeImg, -contentW/2, -contentH/2, contentW, contentH);
573
  } else {
 
574
  ctx.fillRect(-contentW/2, -contentH/2, contentW, contentH);
575
  }
576
  ctx.restore();
577
  }
578
  }
579
  }
580
+
581
  // --- EXPORT ---
582
+
583
  // Download SVG
584
  document.getElementById('btn-dl-svg').addEventListener('click', async () => {
585
  const btn = document.getElementById('btn-dl-svg');
586
  btn.innerText = "Processing...";
587
+
588
  const form = new FormData();
589
  paramIds.forEach(id => form.append(id, document.getElementById(id).value));
590
  form.append('growth_type', document.getElementById('growth_type').value);
591
  form.append('invert', document.getElementById('invert').checked);
592
+
 
593
  // Attach SVG Shape
594
+ if(shapeInput.files[0]) form.append('shape_svg', shapeInput.files[0]);
595
+
596
  // Attach Image (if existing)
597
+ // Note: Logic expects 'raster_image' key for images
598
  if(currentFile && currentMode === 'image') {
599
  form.append('raster_image', currentFile);
600
  }
601
+
602
  try {
603
  const resp = await fetch('/download_svg', { method: 'POST', body: form });
604
+ if (!resp.ok) throw new Error(await resp.text());
605
+
606
  const blob = await resp.blob();
607
  const url = window.URL.createObjectURL(blob);
608
  const a = document.createElement('a');
 
616
  }
617
  btn.innerText = "Download SVG";
618
  });
619
+
620
  // Download Video
621
  document.getElementById('btn-dl-video').addEventListener('click', async () => {
622
  const btn = document.getElementById('btn-dl-video');
623
  btn.innerText = "Rendering...";
624
  btn.disabled = true;
625
  document.getElementById('status-msg').innerText = "Processing on server. Please wait...";
626
+
627
  const form = new FormData();
628
  paramIds.forEach(id => form.append(id, document.getElementById(id).value));
629
  form.append('growth_type', document.getElementById('growth_type').value);
630
  form.append('invert', document.getElementById('invert').checked);
631
+
 
632
  // Attach Video
633
+ // Note: Logic expects 'video_file' key
634
  if(currentFile && currentMode === 'video') {
635
  form.append('video_file', currentFile);
636
  }
637
+
638
  try {
639
  const resp = await fetch('/process_video', { method: 'POST', body: form });
640
+ if (!resp.ok) throw new Error(await resp.text());
641
+
642
  const blob = await resp.blob();
643
  const url = window.URL.createObjectURL(blob);
644
  const a = document.createElement('a');
 
655
  btn.innerText = "Export MP4";
656
  btn.disabled = false;
657
  });
658
+
659
  // Init
660
  requestRender();
661
+
662
  </script>
663
  </body>
664
+ </html>