by3axap commited on
Commit
ad0c4cd
·
verified ·
1 Parent(s): ef9045e

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +104 -13
templates/index.html CHANGED
@@ -74,8 +74,8 @@
74
  transition: background 0.2s, border-color 0.2s;
75
  text-align: center;
76
  }
77
- .btn-upload:hover { background: var(--color-hover); }
78
 
 
79
  .btn-upload.active-pink { background: var(--color-pink); border-color: var(--color-pink); color: #000; }
80
  .btn-upload.active-lime { background: var(--color-lime); border-color: var(--color-lime); color: #000; }
81
 
@@ -110,11 +110,12 @@
110
  input[type="color"]::-webkit-color-swatch { border: 1px solid rgba(0,0,0,0.2); border-radius: 0; }
111
 
112
  /* --- Action Buttons --- */
113
- .action-buttons { display: flex; gap: 15px; margin-top: auto; }
114
  .btn-action { flex: 1; padding: 16px 0; font-family: var(--font-mono); font-size: 12px; border: none; cursor: pointer; transition: opacity 0.2s; text-align: center; }
115
  .btn-action:hover { opacity: 0.8; }
116
  .btn-download-svg { background: #000000; color: #ffffff; }
117
  .btn-download-mp4 { background: var(--color-btn-gray); color: #ffffff; }
 
118
  .btn-action:disabled { opacity: 0.4; cursor: not-allowed; }
119
 
120
  /* --- Preview Area --- */
@@ -126,14 +127,14 @@
126
  justify-content: center;
127
  padding: 20px;
128
  height: 100vh;
129
- overflow: hidden; /* Запрет скролла */
130
  }
131
 
132
  #main-canvas {
133
- box-shadow: 0 10px 40px rgba(0,0,0,0.05);
134
  max-width: 100%;
135
  max-height: 100%;
136
- object-fit: contain; /* Пропорциональное вписывание */
137
  background: #fff;
138
  }
139
 
@@ -154,6 +155,7 @@
154
  <span class="section-title">02 / CONTENT</span>
155
  <input type="file" id="content_file" accept=".png, .jpg, .jpeg, .mp4, .mov">
156
  <label for="content_file" class="btn-upload" id="lbl-content">Select Content</label>
 
157
  </div>
158
 
159
  <div class="section">
@@ -187,8 +189,9 @@
187
  </div>
188
 
189
  <div class="action-buttons">
190
- <button class="btn-action btn-download-svg" id="btn-dl-svg">Download SVG</button>
191
- <button class="btn-action btn-download-mp4" id="btn-dl-video" disabled>Download MP4</button>
 
192
  </div>
193
  <div id="status-msg" style="font-size:10px; font-family:var(--font-mono); text-align:center;"></div>
194
  </div>
@@ -197,7 +200,7 @@
197
  <canvas id="main-canvas" width="1400" height="900"></canvas>
198
 
199
  <img id="source-image" src="" crossorigin="anonymous">
200
- <video id="source-video" src="" playsinline loop muted crossorigin="anonymous"></video>
201
  <canvas id="helper-canvas" style="display:none;"></canvas>
202
  </div>
203
 
@@ -207,6 +210,12 @@
207
  let currentFile = null;
208
  let animationId = null;
209
 
 
 
 
 
 
 
210
  const canvas = document.getElementById('main-canvas');
211
  const ctx = canvas.getContext('2d', { alpha: false });
212
  const helperCanvas = document.getElementById('helper-canvas');
@@ -218,9 +227,11 @@
218
 
219
  const btnShape = document.getElementById('lbl-shape');
220
  const btnContent = document.getElementById('lbl-content');
 
221
  const patternsContainer = document.getElementById('patterns-list');
222
  const btnDlSvg = document.getElementById('btn-dl-svg');
223
  const btnDlVideo = document.getElementById('btn-dl-video');
 
224
 
225
  // --- Авто-размер холста по пропорциям контента ---
226
  function updateCanvasSize() {
@@ -301,11 +312,50 @@
301
  p.cache = off;
302
  }
303
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
  // --- Content ---
305
  document.getElementById('content_file').addEventListener('change', (e) => {
306
  const file = e.target.files[0];
307
  if(!file) return;
308
 
 
 
 
309
  currentFile = file;
310
  const url = URL.createObjectURL(file);
311
 
@@ -360,7 +410,7 @@
360
 
361
  // --- Engine ---
362
  function requestRender() {
363
- if (currentMode === 'video' && !sourceVideo.paused) {
364
  if (!animationId) renderLoop();
365
  } else {
366
  drawFrame();
@@ -369,7 +419,7 @@
369
 
370
  function renderLoop() {
371
  drawFrame();
372
- if (currentMode === 'video' && !sourceVideo.paused) {
373
  animationId = requestAnimationFrame(renderLoop);
374
  } else {
375
  animationId = null;
@@ -393,7 +443,12 @@
393
  let sourceData = null;
394
  let activeSource = currentMode === 'video' ? sourceVideo : sourceImage;
395
 
396
- if (activeSource.src && (activeSource.readyState >= 2 || activeSource.complete)) {
 
 
 
 
 
397
  const step = w / density;
398
  const rows = Math.ceil(h / step);
399
  helperCanvas.width = density;
@@ -417,6 +472,7 @@
417
  const idx = (r * density + c) * 4;
418
  bright = (sourceData[idx] + sourceData[idx+1] + sourceData[idx+2]) / 3 / 255.0;
419
  }
 
420
  if (invert) bright = 1.0 - bright;
421
  if (bright > 0.96) continue;
422
 
@@ -438,6 +494,43 @@
438
  }
439
  }
440
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
441
  // --- Helpers for Blob ---
442
  async function getPatternBlobs() {
443
  const blobs = [];
@@ -466,7 +559,6 @@
466
  form.append('invert', document.getElementById('invert').checked);
467
  form.append('bg_color', bgColorInput.value);
468
 
469
- // Передаем исходные размеры
470
  form.append('canvas_w', canvas.width);
471
  form.append('canvas_h', canvas.height);
472
 
@@ -491,7 +583,6 @@
491
  btnDlVideo.disabled = true;
492
  const form = new FormData();
493
 
494
- // Отправляем закэшированные цветные PNG паттерны для видео
495
  const pBlobs = await getPatternBlobs();
496
  pBlobs.forEach((b, i) => {
497
  form.append('pattern_pngs', b, `pattern_${i}.png`);
 
74
  transition: background 0.2s, border-color 0.2s;
75
  text-align: center;
76
  }
 
77
 
78
+ .btn-upload:hover { background: var(--color-hover); }
79
  .btn-upload.active-pink { background: var(--color-pink); border-color: var(--color-pink); color: #000; }
80
  .btn-upload.active-lime { background: var(--color-lime); border-color: var(--color-lime); color: #000; }
81
 
 
110
  input[type="color"]::-webkit-color-swatch { border: 1px solid rgba(0,0,0,0.2); border-radius: 0; }
111
 
112
  /* --- Action Buttons --- */
113
+ .action-buttons { display: flex; gap: 10px; margin-top: auto; flex-wrap: wrap; }
114
  .btn-action { flex: 1; padding: 16px 0; font-family: var(--font-mono); font-size: 12px; border: none; cursor: pointer; transition: opacity 0.2s; text-align: center; }
115
  .btn-action:hover { opacity: 0.8; }
116
  .btn-download-svg { background: #000000; color: #ffffff; }
117
  .btn-download-mp4 { background: var(--color-btn-gray); color: #ffffff; }
118
+ .btn-record { background: #FF0000; color: #ffffff; }
119
  .btn-action:disabled { opacity: 0.4; cursor: not-allowed; }
120
 
121
  /* --- Preview Area --- */
 
127
  justify-content: center;
128
  padding: 20px;
129
  height: 100vh;
130
+ overflow: hidden;
131
  }
132
 
133
  #main-canvas {
134
+ box-shadow: 0 10px 40px rgba(0,0,0,0.05);
135
  max-width: 100%;
136
  max-height: 100%;
137
+ object-fit: contain;
138
  background: #fff;
139
  }
140
 
 
155
  <span class="section-title">02 / CONTENT</span>
156
  <input type="file" id="content_file" accept=".png, .jpg, .jpeg, .mp4, .mov">
157
  <label for="content_file" class="btn-upload" id="lbl-content">Select Content</label>
158
+ <button class="btn-upload" id="btn-webcam">Use Webcam 📷</button>
159
  </div>
160
 
161
  <div class="section">
 
189
  </div>
190
 
191
  <div class="action-buttons">
192
+ <button class="btn-action btn-download-svg" id="btn-dl-svg">SVG</button>
193
+ <button class="btn-action btn-download-mp4" id="btn-dl-video" disabled>MP4</button>
194
+ <button class="btn-action btn-record" id="btn-record">Record 🔴</button>
195
  </div>
196
  <div id="status-msg" style="font-size:10px; font-family:var(--font-mono); text-align:center;"></div>
197
  </div>
 
200
  <canvas id="main-canvas" width="1400" height="900"></canvas>
201
 
202
  <img id="source-image" src="" crossorigin="anonymous">
203
+ <video id="source-video" playsinline autoplay loop muted crossorigin="anonymous"></video>
204
  <canvas id="helper-canvas" style="display:none;"></canvas>
205
  </div>
206
 
 
210
  let currentFile = null;
211
  let animationId = null;
212
 
213
+ // Переменные для вебкамеры и записи
214
+ let webcamStream = null;
215
+ let mediaRecorder = null;
216
+ let recordedChunks = [];
217
+ let isRecording = false;
218
+
219
  const canvas = document.getElementById('main-canvas');
220
  const ctx = canvas.getContext('2d', { alpha: false });
221
  const helperCanvas = document.getElementById('helper-canvas');
 
227
 
228
  const btnShape = document.getElementById('lbl-shape');
229
  const btnContent = document.getElementById('lbl-content');
230
+ const btnWebcam = document.getElementById('btn-webcam');
231
  const patternsContainer = document.getElementById('patterns-list');
232
  const btnDlSvg = document.getElementById('btn-dl-svg');
233
  const btnDlVideo = document.getElementById('btn-dl-video');
234
+ const btnRecord = document.getElementById('btn-record');
235
 
236
  // --- Авто-размер холста по пропорциям контента ---
237
  function updateCanvasSize() {
 
312
  p.cache = off;
313
  }
314
 
315
+ // --- Сброс вебкамеры ---
316
+ function stopWebcam() {
317
+ if (webcamStream) {
318
+ webcamStream.getTracks().forEach(track => track.stop());
319
+ webcamStream = null;
320
+ }
321
+ btnWebcam.classList.remove('active-lime');
322
+ btnWebcam.innerText = "Use Webcam 📷";
323
+ }
324
+
325
+ // --- Управление вебкамерой ---
326
+ btnWebcam.addEventListener('click', async () => {
327
+ try {
328
+ stopWebcam();
329
+
330
+ webcamStream = await navigator.mediaDevices.getUserMedia({ video: { width: 1280, height: 720 } });
331
+
332
+ sourceVideo.srcObject = webcamStream;
333
+ sourceVideo.play();
334
+
335
+ currentMode = 'video';
336
+ currentFile = null;
337
+
338
+ // Настройка кнопок
339
+ btnContent.classList.remove('active-lime');
340
+ btnContent.innerText = "Select Content";
341
+ btnWebcam.classList.add('active-lime');
342
+ btnWebcam.innerText = "Webcam Active 🟢";
343
+ btnDlVideo.disabled = true; // Экспорт через бэкенд не нужен для вебкамеры
344
+
345
+ requestRender();
346
+ } catch (err) {
347
+ alert("Не удалось получить доступ к камере: " + err.message);
348
+ }
349
+ });
350
+
351
  // --- Content ---
352
  document.getElementById('content_file').addEventListener('change', (e) => {
353
  const file = e.target.files[0];
354
  if(!file) return;
355
 
356
+ stopWebcam(); // Выключаем камеру, если она была включена
357
+ sourceVideo.srcObject = null; // Убираем стрим из тега video
358
+
359
  currentFile = file;
360
  const url = URL.createObjectURL(file);
361
 
 
410
 
411
  // --- Engine ---
412
  function requestRender() {
413
+ if (currentMode === 'video' && (!sourceVideo.paused || webcamStream)) {
414
  if (!animationId) renderLoop();
415
  } else {
416
  drawFrame();
 
419
 
420
  function renderLoop() {
421
  drawFrame();
422
+ if (currentMode === 'video' && (!sourceVideo.paused || webcamStream)) {
423
  animationId = requestAnimationFrame(renderLoop);
424
  } else {
425
  animationId = null;
 
443
  let sourceData = null;
444
  let activeSource = currentMode === 'video' ? sourceVideo : sourceImage;
445
 
446
+ // Проверка готовности для видео-файлов или веб-камеры
447
+ const isReady = activeSource.srcObject
448
+ ? activeSource.readyState >= 2 // Для стрима веб-камеры
449
+ : activeSource.src && (activeSource.readyState >= 2 || activeSource.complete); // Для файлов
450
+
451
+ if (isReady) {
452
  const step = w / density;
453
  const rows = Math.ceil(h / step);
454
  helperCanvas.width = density;
 
472
  const idx = (r * density + c) * 4;
473
  bright = (sourceData[idx] + sourceData[idx+1] + sourceData[idx+2]) / 3 / 255.0;
474
  }
475
+
476
  if (invert) bright = 1.0 - bright;
477
  if (bright > 0.96) continue;
478
 
 
494
  }
495
  }
496
 
497
+ // --- Запись видео в реальном времени (Webcam или Canvas) ---
498
+ btnRecord.addEventListener('click', () => {
499
+ if (!isRecording) {
500
+ // Начинаем запись
501
+ const stream = canvas.captureStream(30); // Захват холста 30 кадров в секунду
502
+ mediaRecorder = new MediaRecorder(stream, { mimeType: 'video/webm' });
503
+ recordedChunks = [];
504
+
505
+ mediaRecorder.ondataavailable = e => {
506
+ if (e.data.size > 0) recordedChunks.push(e.data);
507
+ };
508
+
509
+ mediaRecorder.onstop = () => {
510
+ const blob = new Blob(recordedChunks, { type: 'video/webm' });
511
+ const url = URL.createObjectURL(blob);
512
+ const a = document.createElement('a');
513
+ a.href = url;
514
+ a.download = 'cell_clipper_record.webm';
515
+ a.click();
516
+ URL.revokeObjectURL(url);
517
+ };
518
+
519
+ mediaRecorder.start();
520
+ isRecording = true;
521
+ btnRecord.innerText = "Stop ⏹";
522
+ btnRecord.style.background = "#000";
523
+ document.getElementById('status-msg').innerText = "RECORDING IN PROGRESS...";
524
+ } else {
525
+ // Останавливаем запись
526
+ mediaRecorder.stop();
527
+ isRecording = false;
528
+ btnRecord.innerText = "Record 🔴";
529
+ btnRecord.style.background = "#FF0000";
530
+ document.getElementById('status-msg').innerText = "RECORDING SAVED";
531
+ }
532
+ });
533
+
534
  // --- Helpers for Blob ---
535
  async function getPatternBlobs() {
536
  const blobs = [];
 
559
  form.append('invert', document.getElementById('invert').checked);
560
  form.append('bg_color', bgColorInput.value);
561
 
 
562
  form.append('canvas_w', canvas.width);
563
  form.append('canvas_h', canvas.height);
564
 
 
583
  btnDlVideo.disabled = true;
584
  const form = new FormData();
585
 
 
586
  const pBlobs = await getPatternBlobs();
587
  pBlobs.forEach((b, i) => {
588
  form.append('pattern_pngs', b, `pattern_${i}.png`);