Update templates/index.html
Browse files- 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:
|
| 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">
|
| 191 |
-
<button class="btn-action btn-download-mp4" id="btn-dl-video" disabled>
|
|
|
|
| 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"
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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`);
|