by3axap commited on
Commit
da270b2
·
verified ·
1 Parent(s): b0019de

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +171 -233
templates/index.html CHANGED
@@ -1,13 +1,17 @@
 
 
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
-
8
  :root {
9
  --bg-page: #ffffff;
10
  --text-main: #000000;
 
 
11
  --font-sans: 'Inter', sans-serif;
12
  --font-mono: 'JetBrains Mono', monospace;
13
  }
@@ -29,14 +33,14 @@
29
  border-right: 1px solid var(--border);
30
  display: flex;
31
  flex-direction: column;
32
- padding: 32px;
33
  box-sizing: border-box;
34
  z-index: 10;
35
  overflow-y: auto;
36
  flex-shrink: 0;
37
  }
38
 
39
- .section { margin-bottom: 40px; }
40
 
41
  .section-title {
42
  font-family: var(--font-mono);
@@ -51,12 +55,6 @@
51
  /* --- Controls --- */
52
  .control-group { margin-bottom: 16px; width: 100%; }
53
 
54
-
55
-
56
-
57
-
58
-
59
-
60
  .label-row {
61
  display: flex;
62
  justify-content: space-between;
@@ -66,7 +64,6 @@
66
  font-family: var(--font-mono);
67
  }
68
 
69
- /* Input Number Styling - No underline, plain text */
70
  .num-input {
71
  width: 50px;
72
  border: none;
@@ -85,17 +82,15 @@
85
  width: 100%;
86
  -webkit-appearance: none;
87
  background: transparent;
88
- margin: 10px 0;
89
  cursor: pointer;
90
  }
91
-
92
  input[type="range"]::-webkit-slider-runnable-track {
93
  width: 100%;
94
  height: 1px;
95
  background: #000000;
96
  border: none;
97
  }
98
-
99
  input[type="range"]::-webkit-slider-thumb {
100
  -webkit-appearance: none;
101
  height: 14px;
@@ -105,7 +100,6 @@
105
  margin-top: -6.5px;
106
  border: none;
107
  }
108
-
109
  input[type="range"]:focus { outline: none; }
110
 
111
  /* --- File Buttons --- */
@@ -128,11 +122,7 @@
128
  box-sizing: border-box;
129
  text-align: center;
130
  }
131
-
132
- .btn-upload:hover {
133
- background: #f5f5f5;
134
- }
135
-
136
  .btn-upload.filled {
137
  background: #000000;
138
  color: #ffffff;
@@ -140,7 +130,7 @@
140
  }
141
 
142
  .remove-link {
143
- display: block;
144
  text-align: center;
145
  font-family: var(--font-mono);
146
  font-size: 10px;
@@ -155,12 +145,8 @@
155
  .remove-link:hover { color: red; }
156
 
157
  /* --- Action Buttons --- */
158
- .buttons-container { display: flex; gap: 12px; margin-top: auto; padding-top: 20px;}
159
-
160
-
161
 
162
-
163
-
164
  .btn-action {
165
  flex: 1;
166
  background: #000000;
@@ -190,22 +176,36 @@
190
  /* --- Preview Area --- */
191
  .preview-area {
192
  flex: 1;
193
- background-color: #ffffff; /* Default, will be overridden by JS */
194
  display: flex;
195
  align-items: center;
196
  justify-content: center;
197
  position: relative;
198
  padding: 40px;
 
199
  }
200
-
201
  canvas {
202
- box-shadow: 0 0 0 1px #f0f0f0;
203
  max-width: 100%;
204
  max-height: 100%;
205
  object-fit: contain;
206
  }
207
 
208
- /* Misc */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  select {
210
  width: 100%;
211
  padding: 5px 0;
@@ -223,81 +223,126 @@
223
  accent-color: black;
224
  width: 13px;
225
  height: 13px;
 
 
226
  }
227
 
228
- /* --- Custom Color Picker Styling --- */
229
  input[type="color"] {
230
  -webkit-appearance: none;
231
  border: 1px solid #000000;
232
- width: 13px;
233
- height: 13px;
234
  padding: 0;
235
  background: none;
236
  cursor: pointer;
237
  border-radius: 0;
238
  }
239
- input[type="color"]::-webkit-color-swatch-wrapper {
240
- padding: 0;
241
- }
242
- input[type="color"]::-webkit-color-swatch {
243
- border: none;
244
- border-radius: 0;
245
- }
246
 
247
- /* Hidden sources */
248
  #source-video, #source-image { display: none; }
249
  </style>
250
  </head>
251
  <body>
252
 
253
  <div class="sidebar">
254
- <div style="margin-bottom: 40px;">
255
  <h1 style="font-size: 14px; font-weight: 600; margin:0; letter-spacing: -0.02em;">CELL CLIPPER PRO</h1>
256
  </div>
257
 
 
258
  <div class="section">
 
 
 
 
 
259
 
260
- <div class="control-group">
261
- <div class="label-row">
262
- <span>Stagger</span>
263
- <input type="number" class="num-input" id="num-shift_stagger" value="0" step="0.01" min="0" max="1">
264
- </div>
265
- <input type="range" id="shift_stagger" min="0" max="1" step="0.01" value="0">
266
-
267
- <div class="control-group">
268
- <div class="label-row">
269
- <span>Growth</span>
270
- <input type="number" class="num-input" id="num-growth" value="0" step="0.05" min="0" max="2.0">
271
- </div>
272
- <input type="range" id="growth" min="0" max="2.0" step="0.05" value="0">
273
-
274
-
275
-
276
 
 
 
 
 
 
 
 
 
 
 
 
277
 
 
 
 
 
 
 
 
278
 
 
 
 
 
 
 
 
279
 
 
 
 
 
 
 
 
280
 
 
 
 
 
 
 
 
281
 
 
 
 
 
282
  </div>
 
 
283
 
284
- <div class="control-group" style="display:flex; justify-content: space-between; align-items: center; margin-top:24px;">
285
- <span>Invert Logic</span>
286
- </label>
287
- </div>
288
-
289
- <div style="display:flex; align-items:center; gap:8px;">
290
- <span style="font-size:11px; font-family:var(--font-mono); cursor: default;">COLOR</span>
291
- <input type="color" id="bg_color" value="#ffffff" title="Background Color">
292
- <div class="control-group" style="margin-top:16px;">
293
- <span style="font-size:10px; color:#aaa; font-family:var(--font-mono); display:block; margin-bottom:4px;">GROWTH TYPE</span>
294
- <select id="growth_type">
295
- <option value="sine">Diagonal Sine</option>
296
- <option value="linear">Linear Gradient</option>
297
- <option value="steps">Steps</option>
298
- </select>
299
  </div>
300
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
301
  <button class="btn-action secondary" id="btn-dl-svg">Download SVG</button>
302
  <button class="btn-action" id="btn-dl-video" disabled>Export MP4</button>
303
  </div>
@@ -306,78 +351,80 @@
306
 
307
  <div class="preview-area" id="preview-wrapper">
308
  <canvas id="main-canvas" width="1400" height="900"></canvas>
309
-
310
  <img id="source-image" src="" crossorigin="anonymous">
311
  <video id="source-video" src="" playsinline loop muted crossorigin="anonymous"></video>
312
  <canvas id="helper-canvas" style="display:none;"></canvas>
 
313
 
314
  <script>
315
  // --- STATE ---
316
- let currentMode = 'image'; // 'image' | 'video'
317
- let currentFile = null; // Храним сам объект файла
318
- let animationId = null;
319
  let customShapeLoaded = false;
320
-
321
- // Cached colored shape canvas to avoid recoloring every frame
322
  let cachedShapeCanvas = null;
323
- let lastFillColor = "#000000";
324
 
325
  // --- DOM ---
326
  const canvas = document.getElementById('main-canvas');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
  }
328
- };
329
 
330
- // --- HELPER: TINT SHAPE ---
331
  function updateCachedShape(color) {
332
  if (!customShapeLoaded || !shapeImg.src) {
333
  cachedShapeCanvas = null;
334
  return;
335
  }
336
-
337
  const c = document.createElement('canvas');
338
  c.width = shapeImg.width || 100;
339
  c.height = shapeImg.height || 100;
340
  const x = c.getContext('2d');
341
-
342
- // Draw original shape
343
  x.drawImage(shapeImg, 0, 0, c.width, c.height);
344
-
345
- // Composite operation to tint non-transparent pixels
346
  x.globalCompositeOperation = 'source-in';
347
  x.fillStyle = color;
348
  x.fillRect(0, 0, c.width, c.height);
349
-
350
  cachedShapeCanvas = c;
351
- lastFillColor = color;
352
  }
353
 
354
  // --- EVENT LISTENERS ---
355
-
356
-
357
-
358
-
359
-
360
-
361
-
362
-
363
-
364
-
365
-
366
-
367
-
368
-
369
  // 1. Shape Upload
370
  const shapeInput = document.getElementById('shape_svg');
371
  shapeInput.addEventListener('change', (e) => {
372
  const file = e.target.files[0];
373
  if(!file) return;
374
-
375
  setBtnState('lbl-shape', 'rm-shape', file, true);
376
-
377
  const reader = new FileReader();
378
  reader.onload = (evt) => {
379
- const blob = new Blob([evt.target.result], {type: 'image/svg+xml'});
380
- reader.readAsText(file);
 
 
 
 
 
 
381
  });
382
 
383
  document.getElementById('rm-shape').addEventListener('click', () => {
@@ -389,19 +436,17 @@
389
  requestRender();
390
  });
391
 
392
- // 2. Content Upload (Unified)
393
  const contentInput = document.getElementById('content_file');
394
  contentInput.addEventListener('change', (e) => {
395
  const file = e.target.files[0];
396
  if(!file) return;
397
 
398
- currentFile = file; // Save for export
399
  setBtnState('lbl-content', 'rm-content', file, true);
400
-
401
  const url = URL.createObjectURL(file);
402
  const type = file.type;
403
 
404
- // Reset previous sources
405
  sourceImage.src = "";
406
  sourceVideo.src = "";
407
  sourceVideo.pause();
@@ -410,17 +455,14 @@
410
  currentMode = 'image';
411
  sourceImage.src = url;
412
  sourceImage.onload = () => requestRender();
413
-
414
  document.getElementById('btn-dl-video').disabled = true;
415
  document.getElementById('btn-dl-svg').disabled = false;
416
- }
417
- else if (type.startsWith('video/')) {
418
  currentMode = 'video';
419
  sourceVideo.src = url;
420
  sourceVideo.play();
421
-
422
  document.getElementById('btn-dl-video').disabled = false;
423
- document.getElementById('btn-dl-svg').disabled = true;
424
  requestRender();
425
  }
426
  });
@@ -432,8 +474,6 @@
432
  sourceVideo.src = "";
433
  sourceVideo.pause();
434
  setBtnState('lbl-content', 'rm-content', null, false);
435
-
436
- // Reset defaults
437
  currentMode = 'image';
438
  document.getElementById('btn-dl-video').disabled = true;
439
  document.getElementById('btn-dl-svg').disabled = false;
@@ -445,7 +485,6 @@
445
  paramIds.forEach(id => {
446
  const slider = document.getElementById(id);
447
  const number = document.getElementById('num-' + id);
448
-
449
  slider.addEventListener('input', () => { number.value = slider.value; requestRender(); });
450
  number.addEventListener('input', () => { slider.value = number.value; requestRender(); });
451
  });
@@ -454,37 +493,32 @@
454
  document.getElementById('growth_type').addEventListener('change', requestRender);
455
 
456
  // 4. Color Inputs
457
- const bgColorInput = document.getElementById('bg_color');
458
- const fillColorInput = document.getElementById('fill_color');
459
-
460
  bgColorInput.addEventListener('input', () => {
461
- // Меняем цвет фона страницы предпросмотра для визуального комфорта
462
  previewWrapper.style.backgroundColor = bgColorInput.value;
463
  requestRender();
464
  });
465
 
466
  fillColorInput.addEventListener('input', () => {
467
- const color = fillColorInput.value;
468
- if(customShapeLoaded) {
469
- updateCachedShape(color);
470
- }
471
  requestRender();
472
  });
473
 
474
-
475
  // --- RENDER LOGIC ---
476
  function requestRender() {
 
477
  if (currentMode === 'video' && !sourceVideo.paused) {
 
 
478
  }
479
 
480
  function drawFrame() {
481
  const w = canvas.width;
482
  const h = canvas.height;
483
-
484
- // Params
485
  const density = parseInt(document.getElementById('density').value);
486
  const gapX = parseFloat(document.getElementById('gap_x').value);
487
  const gapY = parseFloat(document.getElementById('gap_y').value);
 
488
  const baseScale = parseFloat(document.getElementById('base_scale').value);
489
  const growth = parseFloat(document.getElementById('growth').value);
490
  const growthType = document.getElementById('growth_type').value;
@@ -493,14 +527,13 @@
493
  const bgColor = bgColorInput.value;
494
  const fillColor = fillColorInput.value;
495
 
496
- // Clear with BG Color
497
  ctx.fillStyle = bgColor;
498
  ctx.fillRect(0, 0, w, h);
499
 
500
- // Read Pixel Data
501
  let sourceData = null;
502
  let sw = 0;
503
-
504
  let activeSource = (currentMode === 'video') ? sourceVideo : sourceImage;
505
  let ready = false;
506
  if(currentMode === 'image' && sourceImage.src) ready = true;
@@ -517,26 +550,24 @@
517
 
518
  // Draw Grid
519
  ctx.fillStyle = fillColor;
520
-
521
  const step = w / density;
522
  const rows = Math.ceil(h / step);
523
  const cellWMax = step * (1.0 - gapX);
524
  const cellHMax = step * (1.0 - gapY);
525
 
526
- // Optimization: loop range
527
  const startC = -Math.floor(density * 0.5);
528
  const endC = density * 1.5;
529
 
530
  for (let r = 0; r < rows; r++) {
531
  const rowShift = (r * stagger * step);
532
-
533
  for (let c = startC; c < endC; c++) {
534
  const gridX = c * step + rowShift;
535
  const gridY = r * step;
536
 
537
- if (gridX < -step || gridX > w) continue;
538
 
539
- // 1. Brightness
540
  let isVisible = true;
541
  if (sourceData) {
542
  const sx = Math.max(0, Math.min(Math.floor(gridX + step/2), w - 1));
@@ -546,18 +577,13 @@
546
  const gV = sourceData[idx+1];
547
  const bV = sourceData[idx+2];
548
  const bright = (rV + gV + bV) / 3 / 255.0;
549
-
550
  const limit = 0.5;
551
  isVisible = invert ? (bright > limit) : (bright < limit);
552
  }
553
 
554
  if (!isVisible) continue;
555
 
556
- // 2. Growth
557
-
558
-
559
-
560
-
561
  let wave = 0;
562
  if (growthType === 'sine') wave = (Math.sin(c * 0.3 + r * 0.3) + 1) / 2;
563
  else if (growthType === 'linear') wave = ((c % density) / density);
@@ -566,24 +592,17 @@
566
  let modW = 1.0 - (wave * growth);
567
  modW = Math.max(0.01, Math.min(1.0, modW));
568
 
569
-
570
-
571
-
572
  const contentW = cellWMax * baseScale * modW;
573
  const contentH = cellHMax * baseScale;
574
-
575
  const cx = gridX + step/2;
576
  const cy = gridY + step/2;
577
 
578
- // Draw
579
  ctx.save();
580
  ctx.translate(cx, cy);
581
-
582
  if (customShapeLoaded && cachedShapeCanvas) {
583
- // Draw tinted shape
584
  ctx.drawImage(cachedShapeCanvas, -contentW/2, -contentH/2, contentW, contentH);
585
  } else {
586
- // Draw simple rect
587
  ctx.fillRect(-contentW/2, -contentH/2, contentW, contentH);
588
  }
589
  ctx.restore();
@@ -591,89 +610,8 @@
591
  }
592
  }
593
 
594
- // --- EXPORT ---
595
-
596
- // Download SVG
597
- document.getElementById('btn-dl-svg').addEventListener('click', async () => {
598
- const btn = document.getElementById('btn-dl-svg');
599
- btn.innerText = "Processing...";
600
-
601
- const form = new FormData();
602
- paramIds.forEach(id => form.append(id, document.getElementById(id).value));
603
- form.append('growth_type', document.getElementById('growth_type').value);
604
- form.append('invert', document.getElementById('invert').checked);
605
- form.append('bg_color', document.getElementById('bg_color').value);
606
- form.append('fill_color', document.getElementById('fill_color').value);
607
-
608
- // Attach SVG Shape
609
- if(shapeInput.files[0]) form.append('shape_svg', shapeInput.files[0]);
610
-
611
- // Attach Image (if existing)
612
- if(currentFile && currentMode === 'image') {
613
- form.append('raster_image', currentFile);
614
- }
615
-
616
- try {
617
- const resp = await fetch('/download_svg', { method: 'POST', body: form });
618
- if (!resp.ok) throw new Error(await resp.text());
619
-
620
- const blob = await resp.blob();
621
- const url = window.URL.createObjectURL(blob);
622
- const a = document.createElement('a');
623
- a.href = url;
624
- a.download = "pattern.svg";
625
- document.body.appendChild(a);
626
- a.click();
627
- a.remove();
628
- } catch (e) {
629
- alert("Error: " + e.message);
630
- }
631
- btn.innerText = "Download SVG";
632
- });
633
-
634
- // Download Video
635
- document.getElementById('btn-dl-video').addEventListener('click', async () => {
636
- const btn = document.getElementById('btn-dl-video');
637
- btn.innerText = "Rendering...";
638
- btn.disabled = true;
639
- document.getElementById('status-msg').innerText = "Processing on server. Please wait...";
640
-
641
- const form = new FormData();
642
- paramIds.forEach(id => form.append(id, document.getElementById(id).value));
643
- form.append('growth_type', document.getElementById('growth_type').value);
644
- form.append('invert', document.getElementById('invert').checked);
645
- form.append('bg_color', document.getElementById('bg_color').value);
646
- form.append('fill_color', document.getElementById('fill_color').value);
647
-
648
- // Attach Video
649
- if(currentFile && currentMode === 'video') {
650
- form.append('video_file', currentFile);
651
- }
652
-
653
- try {
654
- const resp = await fetch('/process_video', { method: 'POST', body: form });
655
- if (!resp.ok) throw new Error(await resp.text());
656
-
657
- const blob = await resp.blob();
658
- const url = window.URL.createObjectURL(blob);
659
- const a = document.createElement('a');
660
- a.href = url;
661
- a.download = "render.mp4";
662
- document.body.appendChild(a);
663
- a.click();
664
- a.remove();
665
- document.getElementById('status-msg').innerText = "";
666
- } catch (e) {
667
- alert("Error: " + e.message);
668
- document.getElementById('status-msg').innerText = "Error occurred.";
669
- }
670
- btn.innerText = "Export MP4";
671
- btn.disabled = false;
672
- });
673
-
674
- // Init
675
  requestRender();
676
-
677
  </script>
678
  </body>
679
  </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: #888888;
14
+ --border: #e0e0e0;
15
  --font-sans: 'Inter', sans-serif;
16
  --font-mono: 'JetBrains Mono', monospace;
17
  }
 
33
  border-right: 1px solid var(--border);
34
  display: flex;
35
  flex-direction: column;
36
+ padding: 32px;
37
  box-sizing: border-box;
38
  z-index: 10;
39
  overflow-y: auto;
40
  flex-shrink: 0;
41
  }
42
 
43
+ .section { margin-bottom: 32px; }
44
 
45
  .section-title {
46
  font-family: var(--font-mono);
 
55
  /* --- Controls --- */
56
  .control-group { margin-bottom: 16px; width: 100%; }
57
 
 
 
 
 
 
 
58
  .label-row {
59
  display: flex;
60
  justify-content: space-between;
 
64
  font-family: var(--font-mono);
65
  }
66
 
 
67
  .num-input {
68
  width: 50px;
69
  border: none;
 
82
  width: 100%;
83
  -webkit-appearance: none;
84
  background: transparent;
85
+ margin: 8px 0;
86
  cursor: pointer;
87
  }
 
88
  input[type="range"]::-webkit-slider-runnable-track {
89
  width: 100%;
90
  height: 1px;
91
  background: #000000;
92
  border: none;
93
  }
 
94
  input[type="range"]::-webkit-slider-thumb {
95
  -webkit-appearance: none;
96
  height: 14px;
 
100
  margin-top: -6.5px;
101
  border: none;
102
  }
 
103
  input[type="range"]:focus { outline: none; }
104
 
105
  /* --- File Buttons --- */
 
122
  box-sizing: border-box;
123
  text-align: center;
124
  }
125
+ .btn-upload:hover { background: #f5f5f5; }
 
 
 
 
126
  .btn-upload.filled {
127
  background: #000000;
128
  color: #ffffff;
 
130
  }
131
 
132
  .remove-link {
133
+ display: none;
134
  text-align: center;
135
  font-family: var(--font-mono);
136
  font-size: 10px;
 
145
  .remove-link:hover { color: red; }
146
 
147
  /* --- Action Buttons --- */
148
+ .buttons-container { display: flex; gap: 12px; margin-top: auto; padding-top: 20px; }
 
 
149
 
 
 
150
  .btn-action {
151
  flex: 1;
152
  background: #000000;
 
176
  /* --- Preview Area --- */
177
  .preview-area {
178
  flex: 1;
179
+ background-color: #ffffff;
180
  display: flex;
181
  align-items: center;
182
  justify-content: center;
183
  position: relative;
184
  padding: 40px;
185
+ box-sizing: border-box;
186
  }
 
187
  canvas {
188
+ box-shadow: 0 0 0 1px var(--border);
189
  max-width: 100%;
190
  max-height: 100%;
191
  object-fit: contain;
192
  }
193
 
194
+ /* Controls Row (Invert & Color) */
195
+ .options-row {
196
+ display: flex;
197
+ justify-content: space-between;
198
+ align-items: center;
199
+ margin-top: 24px;
200
+ font-family: var(--font-mono);
201
+ font-size: 11px;
202
+ }
203
+ .color-pickers {
204
+ display: flex;
205
+ align-items: center;
206
+ gap: 6px;
207
+ }
208
+
209
  select {
210
  width: 100%;
211
  padding: 5px 0;
 
223
  accent-color: black;
224
  width: 13px;
225
  height: 13px;
226
+ margin: 0 6px 0 0;
227
+ cursor: pointer;
228
  }
229
 
 
230
  input[type="color"] {
231
  -webkit-appearance: none;
232
  border: 1px solid #000000;
233
+ width: 14px;
234
+ height: 14px;
235
  padding: 0;
236
  background: none;
237
  cursor: pointer;
238
  border-radius: 0;
239
  }
240
+ input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
241
+ input[type="color"]::-webkit-color-swatch { border: none; border-radius: 0; }
 
 
 
 
 
242
 
 
243
  #source-video, #source-image { display: none; }
244
  </style>
245
  </head>
246
  <body>
247
 
248
  <div class="sidebar">
249
+ <div style="margin-bottom: 32px;">
250
  <h1 style="font-size: 14px; font-weight: 600; margin:0; letter-spacing: -0.02em;">CELL CLIPPER PRO</h1>
251
  </div>
252
 
253
+ <!-- 01. Shape Pattern -->
254
  <div class="section">
255
+ <span class="section-title">01 / Shape Pattern</span>
256
+ <input type="file" id="shape_svg" accept=".svg">
257
+ <label for="shape_svg" class="btn-upload" id="lbl-shape">Select SVG</label>
258
+ <button type="button" class="remove-link" id="rm-shape">Remove SVG</button>
259
+ </div>
260
 
261
+ <!-- 02. Content Source -->
262
+ <div class="section">
263
+ <span class="section-title">02 / Content Source</span>
264
+ <input type="file" id="content_file" accept="image/*,video/*">
265
+ <label for="content_file" class="btn-upload" id="lbl-content">Select Content</label>
266
+ <button type="button" class="remove-link" id="rm-content">Remove Content</button>
267
+ </div>
 
 
 
 
 
 
 
 
 
268
 
269
+ <!-- 03. Parameters -->
270
+ <div class="section">
271
+ <span class="section-title">03 / Parameters</span>
272
+
273
+ <div class="control-group">
274
+ <div class="label-row">
275
+ <span>Density</span>
276
+ <input type="number" class="num-input" id="num-density" value="24" min="5" max="100">
277
+ </div>
278
+ <input type="range" id="density" min="5" max="100" value="24">
279
+ </div>
280
 
281
+ <div class="control-group">
282
+ <div class="label-row">
283
+ <span>Gap X</span>
284
+ <input type="number" class="num-input" id="num-gap_x" value="0.2" step="0.01" min="0" max="0.9">
285
+ </div>
286
+ <input type="range" id="gap_x" min="0" max="0.9" step="0.01" value="0.2">
287
+ </div>
288
 
289
+ <div class="control-group">
290
+ <div class="label-row">
291
+ <span>Gap Y</span>
292
+ <input type="number" class="num-input" id="num-gap_y" value="0" step="0.01" min="0" max="0.9">
293
+ </div>
294
+ <input type="range" id="gap_y" min="0" max="0.9" step="0.01" value="0">
295
+ </div>
296
 
297
+ <div class="control-group">
298
+ <div class="label-row">
299
+ <span>Stagger</span>
300
+ <input type="number" class="num-input" id="num-shift_stagger" value="0.34" step="0.01" min="0" max="1">
301
+ </div>
302
+ <input type="range" id="shift_stagger" min="0" max="1" step="0.01" value="0.34">
303
+ </div>
304
 
305
+ <div class="control-group">
306
+ <div class="label-row">
307
+ <span>Scale</span>
308
+ <input type="number" class="num-input" id="num-base_scale" value="1.0" step="0.05" min="0.1" max="2.0">
309
+ </div>
310
+ <input type="range" id="base_scale" min="0.1" max="2.0" step="0.05" value="1.0">
311
+ </div>
312
 
313
+ <div class="control-group">
314
+ <div class="label-row">
315
+ <span>Growth</span>
316
+ <input type="number" class="num-input" id="num-growth" value="0.9" step="0.05" min="0" max="2.0">
317
  </div>
318
+ <input type="range" id="growth" min="0" max="2.0" step="0.05" value="0.9">
319
+ </div>
320
 
321
+ <div class="options-row">
322
+ <label style="display:flex; align-items:center; cursor:pointer;">
323
+ <input type="checkbox" id="invert">
324
+ <span>Invert Logic</span>
325
+ </label>
326
+
327
+ <div class="color-pickers">
328
+ <span>COLOR</span>
329
+ <input type="color" id="bg_color" value="#ffffff" title="Background Color">
330
+ <input type="color" id="fill_color" value="#000000" title="Pattern Color">
 
 
 
 
 
331
  </div>
332
  </div>
333
+
334
+ <div class="control-group" style="margin-top:20px;">
335
+ <span style="font-size:10px; color:var(--text-muted); font-family:var(--font-mono); display:block; margin-bottom:6px;">GROWTH TYPE</span>
336
+ <select id="growth_type">
337
+ <option value="sine">Diagonal Sine</option>
338
+ <option value="linear">Linear Gradient</option>
339
+ <option value="steps">Steps</option>
340
+ </select>
341
+ </div>
342
+ </div>
343
+
344
+ <!-- Actions -->
345
+ <div class="buttons-container">
346
  <button class="btn-action secondary" id="btn-dl-svg">Download SVG</button>
347
  <button class="btn-action" id="btn-dl-video" disabled>Export MP4</button>
348
  </div>
 
351
 
352
  <div class="preview-area" id="preview-wrapper">
353
  <canvas id="main-canvas" width="1400" height="900"></canvas>
 
354
  <img id="source-image" src="" crossorigin="anonymous">
355
  <video id="source-video" src="" playsinline loop muted crossorigin="anonymous"></video>
356
  <canvas id="helper-canvas" style="display:none;"></canvas>
357
+ </div>
358
 
359
  <script>
360
  // --- STATE ---
361
+ let currentMode = 'image';
362
+ let currentFile = null;
 
363
  let customShapeLoaded = false;
364
+ let shapeImg = new Image();
 
365
  let cachedShapeCanvas = null;
 
366
 
367
  // --- DOM ---
368
  const canvas = document.getElementById('main-canvas');
369
+ const ctx = canvas.getContext('2d');
370
+ const helperCanvas = document.getElementById('helper-canvas');
371
+ const helperCtx = helperCanvas.getContext('2d');
372
+ const previewWrapper = document.getElementById('preview-wrapper');
373
+ const sourceImage = document.getElementById('source-image');
374
+ const sourceVideo = document.getElementById('source-video');
375
+
376
+ const bgColorInput = document.getElementById('bg_color');
377
+ const fillColorInput = document.getElementById('fill_color');
378
+
379
+ function setBtnState(lblId, rmId, file, isFilled) {
380
+ const lbl = document.getElementById(lblId);
381
+ const rm = document.getElementById(rmId);
382
+ if (isFilled && file) {
383
+ lbl.classList.add('filled');
384
+ lbl.innerText = file.name;
385
+ rm.style.display = 'block';
386
+ } else {
387
+ lbl.classList.remove('filled');
388
+ lbl.innerText = lblId === 'lbl-shape' ? 'Select SVG' : 'Select Content';
389
+ rm.style.display = 'none';
390
  }
391
+ }
392
 
393
+ // --- TINT SHAPE ---
394
  function updateCachedShape(color) {
395
  if (!customShapeLoaded || !shapeImg.src) {
396
  cachedShapeCanvas = null;
397
  return;
398
  }
 
399
  const c = document.createElement('canvas');
400
  c.width = shapeImg.width || 100;
401
  c.height = shapeImg.height || 100;
402
  const x = c.getContext('2d');
 
 
403
  x.drawImage(shapeImg, 0, 0, c.width, c.height);
 
 
404
  x.globalCompositeOperation = 'source-in';
405
  x.fillStyle = color;
406
  x.fillRect(0, 0, c.width, c.height);
 
407
  cachedShapeCanvas = c;
 
408
  }
409
 
410
  // --- EVENT LISTENERS ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411
  // 1. Shape Upload
412
  const shapeInput = document.getElementById('shape_svg');
413
  shapeInput.addEventListener('change', (e) => {
414
  const file = e.target.files[0];
415
  if(!file) return;
 
416
  setBtnState('lbl-shape', 'rm-shape', file, true);
417
+
418
  const reader = new FileReader();
419
  reader.onload = (evt) => {
420
+ shapeImg.src = evt.target.result;
421
+ shapeImg.onload = () => {
422
+ customShapeLoaded = true;
423
+ updateCachedShape(fillColorInput.value);
424
+ requestRender();
425
+ };
426
+ };
427
+ reader.readAsDataURL(file);
428
  });
429
 
430
  document.getElementById('rm-shape').addEventListener('click', () => {
 
436
  requestRender();
437
  });
438
 
439
+ // 2. Content Upload
440
  const contentInput = document.getElementById('content_file');
441
  contentInput.addEventListener('change', (e) => {
442
  const file = e.target.files[0];
443
  if(!file) return;
444
 
445
+ currentFile = file;
446
  setBtnState('lbl-content', 'rm-content', file, true);
 
447
  const url = URL.createObjectURL(file);
448
  const type = file.type;
449
 
 
450
  sourceImage.src = "";
451
  sourceVideo.src = "";
452
  sourceVideo.pause();
 
455
  currentMode = 'image';
456
  sourceImage.src = url;
457
  sourceImage.onload = () => requestRender();
 
458
  document.getElementById('btn-dl-video').disabled = true;
459
  document.getElementById('btn-dl-svg').disabled = false;
460
+ } else if (type.startsWith('video/')) {
 
461
  currentMode = 'video';
462
  sourceVideo.src = url;
463
  sourceVideo.play();
 
464
  document.getElementById('btn-dl-video').disabled = false;
465
+ document.getElementById('btn-dl-svg').disabled = true;
466
  requestRender();
467
  }
468
  });
 
474
  sourceVideo.src = "";
475
  sourceVideo.pause();
476
  setBtnState('lbl-content', 'rm-content', null, false);
 
 
477
  currentMode = 'image';
478
  document.getElementById('btn-dl-video').disabled = true;
479
  document.getElementById('btn-dl-svg').disabled = false;
 
485
  paramIds.forEach(id => {
486
  const slider = document.getElementById(id);
487
  const number = document.getElementById('num-' + id);
 
488
  slider.addEventListener('input', () => { number.value = slider.value; requestRender(); });
489
  number.addEventListener('input', () => { slider.value = number.value; requestRender(); });
490
  });
 
493
  document.getElementById('growth_type').addEventListener('change', requestRender);
494
 
495
  // 4. Color Inputs
 
 
 
496
  bgColorInput.addEventListener('input', () => {
 
497
  previewWrapper.style.backgroundColor = bgColorInput.value;
498
  requestRender();
499
  });
500
 
501
  fillColorInput.addEventListener('input', () => {
502
+ if(customShapeLoaded) updateCachedShape(fillColorInput.value);
 
 
 
503
  requestRender();
504
  });
505
 
 
506
  // --- RENDER LOGIC ---
507
  function requestRender() {
508
+ drawFrame();
509
  if (currentMode === 'video' && !sourceVideo.paused) {
510
+ requestAnimationFrame(requestRender);
511
+ }
512
  }
513
 
514
  function drawFrame() {
515
  const w = canvas.width;
516
  const h = canvas.height;
517
+
 
518
  const density = parseInt(document.getElementById('density').value);
519
  const gapX = parseFloat(document.getElementById('gap_x').value);
520
  const gapY = parseFloat(document.getElementById('gap_y').value);
521
+ const stagger = parseFloat(document.getElementById('shift_stagger').value);
522
  const baseScale = parseFloat(document.getElementById('base_scale').value);
523
  const growth = parseFloat(document.getElementById('growth').value);
524
  const growthType = document.getElementById('growth_type').value;
 
527
  const bgColor = bgColorInput.value;
528
  const fillColor = fillColorInput.value;
529
 
530
+ // Clear Background
531
  ctx.fillStyle = bgColor;
532
  ctx.fillRect(0, 0, w, h);
533
 
534
+ // Read Pixel Data if content loaded
535
  let sourceData = null;
536
  let sw = 0;
 
537
  let activeSource = (currentMode === 'video') ? sourceVideo : sourceImage;
538
  let ready = false;
539
  if(currentMode === 'image' && sourceImage.src) ready = true;
 
550
 
551
  // Draw Grid
552
  ctx.fillStyle = fillColor;
 
553
  const step = w / density;
554
  const rows = Math.ceil(h / step);
555
  const cellWMax = step * (1.0 - gapX);
556
  const cellHMax = step * (1.0 - gapY);
557
 
 
558
  const startC = -Math.floor(density * 0.5);
559
  const endC = density * 1.5;
560
 
561
  for (let r = 0; r < rows; r++) {
562
  const rowShift = (r * stagger * step);
563
+
564
  for (let c = startC; c < endC; c++) {
565
  const gridX = c * step + rowShift;
566
  const gridY = r * step;
567
 
568
+ if (gridX < -step || gridX > w + step) continue;
569
 
570
+ // Brightness Logic
571
  let isVisible = true;
572
  if (sourceData) {
573
  const sx = Math.max(0, Math.min(Math.floor(gridX + step/2), w - 1));
 
577
  const gV = sourceData[idx+1];
578
  const bV = sourceData[idx+2];
579
  const bright = (rV + gV + bV) / 3 / 255.0;
 
580
  const limit = 0.5;
581
  isVisible = invert ? (bright > limit) : (bright < limit);
582
  }
583
 
584
  if (!isVisible) continue;
585
 
586
+ // Growth Calc
 
 
 
 
587
  let wave = 0;
588
  if (growthType === 'sine') wave = (Math.sin(c * 0.3 + r * 0.3) + 1) / 2;
589
  else if (growthType === 'linear') wave = ((c % density) / density);
 
592
  let modW = 1.0 - (wave * growth);
593
  modW = Math.max(0.01, Math.min(1.0, modW));
594
 
 
 
 
595
  const contentW = cellWMax * baseScale * modW;
596
  const contentH = cellHMax * baseScale;
 
597
  const cx = gridX + step/2;
598
  const cy = gridY + step/2;
599
 
 
600
  ctx.save();
601
  ctx.translate(cx, cy);
602
+
603
  if (customShapeLoaded && cachedShapeCanvas) {
 
604
  ctx.drawImage(cachedShapeCanvas, -contentW/2, -contentH/2, contentW, contentH);
605
  } else {
 
606
  ctx.fillRect(-contentW/2, -contentH/2, contentW, contentH);
607
  }
608
  ctx.restore();
 
610
  }
611
  }
612
 
613
+ // Init Render
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
614
  requestRender();
 
615
  </script>
616
  </body>
617
  </html>