Update app_enhanced.py
Browse files- app_enhanced.py +43 -65
app_enhanced.py
CHANGED
|
@@ -118,7 +118,8 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
|
|
| 118 |
raw_moments = [{'text': s.content, 'start': s.start.total_seconds(), 'end': s.end.total_seconds()} for s in valid_subs]
|
| 119 |
|
| 120 |
if target_pages <= 0: target_pages = 1
|
| 121 |
-
|
|
|
|
| 122 |
total_panels_needed = target_pages * panels_per_page
|
| 123 |
|
| 124 |
selected_moments = []
|
|
@@ -292,7 +293,7 @@ class EnhancedComicGenerator:
|
|
| 292 |
# 🌐 ROUTES & FULL UI
|
| 293 |
# ======================================================
|
| 294 |
INDEX_HTML = '''
|
| 295 |
-
<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>🎬
|
| 296 |
|
| 297 |
#upload-container { display: flex; flex-direction:column; justify-content: center; align-items: center; min-height: 100vh; width: 100%; padding: 20px; }
|
| 298 |
.upload-box { max-width: 500px; width: 100%; padding: 40px; background: white; border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,0.12); text-align: center; }
|
|
@@ -334,7 +335,7 @@ INDEX_HTML = '''
|
|
| 334 |
box-shadow: 0 0 50px rgba(0,0,0,0.2);
|
| 335 |
position: relative;
|
| 336 |
z-index: 1;
|
| 337 |
-
overflow: hidden;
|
| 338 |
border: 2px solid black;
|
| 339 |
}
|
| 340 |
|
|
@@ -346,7 +347,7 @@ INDEX_HTML = '''
|
|
| 346 |
border: 2px solid white;
|
| 347 |
border-radius: 50%;
|
| 348 |
cursor: ew-resize;
|
| 349 |
-
z-index: 999;
|
| 350 |
transform: translate(-50%, -50%);
|
| 351 |
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
|
| 352 |
}
|
|
@@ -368,7 +369,6 @@ INDEX_HTML = '''
|
|
| 368 |
top: 0; left: 0;
|
| 369 |
transform-origin: 0 0;
|
| 370 |
cursor: grab;
|
| 371 |
-
/* No object-fit: we use transforms for freedom */
|
| 372 |
}
|
| 373 |
.panel img.panning { cursor: grabbing; }
|
| 374 |
|
|
@@ -384,7 +384,7 @@ INDEX_HTML = '''
|
|
| 384 |
}
|
| 385 |
.speech-bubble.selected { outline: 2px dashed #4CAF50; z-index: 501; }
|
| 386 |
|
| 387 |
-
/* BUBBLE STYLES
|
| 388 |
.bubble-text { padding:0.5em; overflow-wrap:break-word; word-wrap:break-word; white-space:pre-wrap; position:relative; z-index:5; pointer-events:none; user-select:none; width:100%; height:100%; display:flex; align-items:center; justify-content:center; border-radius:inherit; }
|
| 389 |
.speech-bubble textarea { position:absolute; top:0; left:0; width:100%; height:100%; box-sizing:border-box; border:1px solid #4CAF50; background:rgba(255,255,255,0.95); text-align:center; padding:8px; z-index:102; resize:none; font:inherit; white-space:pre-wrap; }
|
| 390 |
.speech-bubble.speech { --b:3em; --h:1.8em; --t:0.6; --p:var(--tail-pos,50%); --r:1.2em; background:var(--bubble-fill-color,#4ECDC4); color:var(--bubble-text-color,#fff); padding:0; border-radius:var(--r) var(--r) min(var(--r),calc(100% - var(--p) - (1 - var(--t))*var(--b)/2)) min(var(--r),calc(var(--p) - (1 - var(--t))*var(--b)/2))/var(--r); }
|
|
@@ -413,7 +413,7 @@ INDEX_HTML = '''
|
|
| 413 |
.modal-content { background:white; padding:30px; border-radius:12px; max-width:400px; width:90%; text-align:center; }
|
| 414 |
.modal-content .code { font-size:32px; font-weight:bold; letter-spacing:4px; background:#f0f0f0; padding:15px 25px; border-radius:8px; display:inline-block; margin:15px 0; font-family:monospace; user-select:all; }
|
| 415 |
</style>
|
| 416 |
-
</head> <body> <div id="upload-container"> <div class="upload-box"> <h1>🎬
|
| 417 |
<div class="page-input-group">
|
| 418 |
<label>📚 Total Comic Pages:</label>
|
| 419 |
<input type="number" id="page-count" value="3" min="1" max="15" placeholder="e.g. 3">
|
|
@@ -438,9 +438,7 @@ INDEX_HTML = '''
|
|
| 438 |
<input type="file" id="image-uploader" style="display: none;" accept="image/*">
|
| 439 |
<div class="edit-controls">
|
| 440 |
<h4>✏️ Architect Editor</h4>
|
| 441 |
-
|
| 442 |
<button onclick="undoLastAction()" class="undo-btn">↩️ Undo</button>
|
| 443 |
-
|
| 444 |
<div class="control-group">
|
| 445 |
<label>📐 Layout Settings:</label>
|
| 446 |
<div class="slider-container">
|
|
@@ -450,7 +448,6 @@ INDEX_HTML = '''
|
|
| 450 |
</div>
|
| 451 |
<small style="color:#aaa; font-size:10px;">Drag red dots on panels to resize.</small>
|
| 452 |
</div>
|
| 453 |
-
|
| 454 |
<div class="control-group">
|
| 455 |
<label>💾 Save & Load:</label>
|
| 456 |
<button onclick="saveComic()" class="save-btn">💾 Save Comic</button>
|
|
@@ -459,7 +456,6 @@ INDEX_HTML = '''
|
|
| 459 |
<button onclick="copyCode()" style="padding:2px; width:auto; font-size:10px;">Copy</button>
|
| 460 |
</div>
|
| 461 |
</div>
|
| 462 |
-
|
| 463 |
<div class="control-group">
|
| 464 |
<label>💬 Bubble Styling:</label>
|
| 465 |
<select id="bubble-type-select" onchange="changeBubbleType(this.value)" disabled>
|
|
@@ -477,7 +473,6 @@ INDEX_HTML = '''
|
|
| 477 |
<button onclick="deleteBubble()" class="reset-btn">Delete</button>
|
| 478 |
</div>
|
| 479 |
</div>
|
| 480 |
-
|
| 481 |
<div class="control-group">
|
| 482 |
<label>🖼️ Panel Tools:</label>
|
| 483 |
<button onclick="replacePanelImage()" class="action-btn">🖼️ Replace Image</button>
|
|
@@ -486,7 +481,6 @@ INDEX_HTML = '''
|
|
| 486 |
<button onclick="adjustFrame('forward')" class="action-btn">Next Frame ➡️</button>
|
| 487 |
</div>
|
| 488 |
</div>
|
| 489 |
-
|
| 490 |
<div class="control-group">
|
| 491 |
<label>🔍 Zoom & Pan:</label>
|
| 492 |
<div class="button-grid">
|
|
@@ -494,7 +488,6 @@ INDEX_HTML = '''
|
|
| 494 |
<input type="range" id="zoom-slider" min="50" max="300" value="100" step="5" disabled oninput="handleZoom(this)">
|
| 495 |
</div>
|
| 496 |
</div>
|
| 497 |
-
|
| 498 |
<div class="control-group">
|
| 499 |
<button onclick="exportComic()" class="export-btn">📥 Export as PNG</button>
|
| 500 |
<button onclick="goBackToUpload()" class="reset-btn" style="margin-top:10px;">🏠 Home</button>
|
|
@@ -521,12 +514,11 @@ INDEX_HTML = '''
|
|
| 521 |
let startX, startY, initX, initY, panStartX, panStartY, panStartTx, panStartTy;
|
| 522 |
let activeLayoutHandle = null, activePageId = null;
|
| 523 |
|
| 524 |
-
//
|
| 525 |
const DEFAULT_LAYOUT = {
|
| 526 |
width: 1000, height: 700, gutter: 10, tierY: 350,
|
| 527 |
-
r1: { topX:
|
| 528 |
-
|
| 529 |
-
r2R: { topX: 617, botX: 666 }
|
| 530 |
};
|
| 531 |
|
| 532 |
let historyStack = [];
|
|
@@ -635,8 +627,8 @@ INDEX_HTML = '''
|
|
| 635 |
div.id = pageId;
|
| 636 |
div.dataset.layout = JSON.stringify(layout);
|
| 637 |
|
| 638 |
-
// Generate Panels
|
| 639 |
-
for(let i=0; i<
|
| 640 |
const pDiv = document.createElement('div');
|
| 641 |
pDiv.className = 'panel'; pDiv.id = `${pageId}-p${i}`;
|
| 642 |
|
|
@@ -651,12 +643,11 @@ INDEX_HTML = '''
|
|
| 651 |
div.appendChild(pDiv);
|
| 652 |
}
|
| 653 |
|
| 654 |
-
// Generate Handles
|
| 655 |
const handles = [
|
| 656 |
-
{id: 'h1-top', t:0, l:0}, {id: 'h1-bot', t:0, l:0},
|
| 657 |
-
{id: 'h2-
|
| 658 |
-
{id: '
|
| 659 |
-
{id: 'h-tier', t:0, l:0, cls: 'horiz'}
|
| 660 |
];
|
| 661 |
handles.forEach(h => {
|
| 662 |
const hDiv = document.createElement('div');
|
|
@@ -689,7 +680,7 @@ INDEX_HTML = '''
|
|
| 689 |
document.getElementById('font-select').disabled = true;
|
| 690 |
}
|
| 691 |
|
| 692 |
-
// --- LAYOUT ENGINE ---
|
| 693 |
function drawLayout(pageId) {
|
| 694 |
const pageEl = document.getElementById(pageId);
|
| 695 |
if(!pageEl) return;
|
|
@@ -699,41 +690,33 @@ INDEX_HTML = '''
|
|
| 699 |
const w = state.width;
|
| 700 |
const h = state.height;
|
| 701 |
|
| 702 |
-
//
|
| 703 |
-
|
| 704 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 705 |
p1.style.top='0px'; p1.style.left='0px'; p1.style.width='100%'; p1.style.height = ty + 'px';
|
| 706 |
-
p1.style.clipPath = `polygon(
|
| 707 |
|
| 708 |
-
// P2:
|
| 709 |
-
const p2 = document.getElementById(`${pageId}-
|
| 710 |
-
p2.style.top='
|
| 711 |
-
p2.style.clipPath = `polygon(${
|
| 712 |
|
| 713 |
-
// P3: Bottom
|
| 714 |
-
const p3 = document.getElementById(`${pageId}-
|
| 715 |
p3.style.top = ty + 'px'; p3.style.left='0px'; p3.style.width='100%'; p3.style.height = (h - ty) + 'px';
|
| 716 |
-
p3.style.clipPath = `polygon(
|
| 717 |
-
|
| 718 |
-
// P4: Bottom Center
|
| 719 |
-
const p4 = document.getElementById(`${pageId}-p3`);
|
| 720 |
-
p4.style.top = ty + 'px'; p4.style.left='0px'; p4.style.width='100%'; p4.style.height = (h - ty) + 'px';
|
| 721 |
-
p4.style.clipPath = `polygon(${state.r2L.topX + g}px ${g}px, ${state.r2R.topX - g}px ${g}px, ${state.r2R.botX - g}px ${h-ty}px, ${state.r2L.botX + g}px ${h-ty}px)`;
|
| 722 |
-
|
| 723 |
-
// P5: Bottom Right
|
| 724 |
-
const p5 = document.getElementById(`${pageId}-p4`);
|
| 725 |
-
p5.style.top = ty + 'px'; p5.style.left='0px'; p5.style.width='100%'; p5.style.height = (h - ty) + 'px';
|
| 726 |
-
p5.style.clipPath = `polygon(${state.r2R.topX + g}px ${g}px, ${w}px ${g}px, ${w}px ${h-ty}px, ${state.r2R.botX + g}px ${h-ty}px)`;
|
| 727 |
|
| 728 |
// Update Handles Position
|
| 729 |
const setH = (id, l, t) => { const el = document.getElementById(`${pageId}-${id}`); if(el) { el.style.left=l+'px'; el.style.top=t+'px'; } };
|
| 730 |
|
| 731 |
setH('h1-top', state.r1.topX, 0);
|
| 732 |
setH('h1-bot', state.r1.botX, ty);
|
| 733 |
-
setH('h2-
|
| 734 |
-
setH('h2-
|
| 735 |
-
setH('h2-r-top', state.r2R.topX, ty);
|
| 736 |
-
setH('h2-r-bot', state.r2R.botX, h);
|
| 737 |
setH('h-tier', w/2, ty);
|
| 738 |
}
|
| 739 |
|
|
@@ -741,7 +724,7 @@ INDEX_HTML = '''
|
|
| 741 |
e.stopPropagation(); e.preventDefault();
|
| 742 |
activeLayoutHandle = handleRole;
|
| 743 |
activePageId = pageId;
|
| 744 |
-
isDragging = true;
|
| 745 |
}
|
| 746 |
|
| 747 |
// --- GLOBAL MOUSE EVENTS ---
|
|
@@ -750,22 +733,19 @@ INDEX_HTML = '''
|
|
| 750 |
const pageEl = document.getElementById(activePageId);
|
| 751 |
const rect = pageEl.getBoundingClientRect();
|
| 752 |
const state = JSON.parse(pageEl.dataset.layout);
|
| 753 |
-
const x = Math.max(0, Math.min(1000, (e.clientX - rect.left) * (1000 / rect.width)));
|
| 754 |
const y = Math.max(50, Math.min(650, (e.clientY - rect.top) * (700 / rect.height)));
|
| 755 |
|
| 756 |
if(activeLayoutHandle === 'h1-top') state.r1.topX = x;
|
| 757 |
else if(activeLayoutHandle === 'h1-bot') state.r1.botX = x;
|
| 758 |
-
else if(activeLayoutHandle === 'h2-
|
| 759 |
-
else if(activeLayoutHandle === 'h2-
|
| 760 |
-
else if(activeLayoutHandle === 'h2-r-top') state.r2R.topX = x;
|
| 761 |
-
else if(activeLayoutHandle === 'h2-r-bot') state.r2R.botX = x;
|
| 762 |
else if(activeLayoutHandle === 'h-tier') state.tierY = y;
|
| 763 |
|
| 764 |
pageEl.dataset.layout = JSON.stringify(state);
|
| 765 |
drawLayout(activePageId);
|
| 766 |
}
|
| 767 |
else if(isDragging && selectedBubble) {
|
| 768 |
-
// Bubble Drag
|
| 769 |
selectedBubble.style.left = (initX + e.clientX - startX) + 'px';
|
| 770 |
selectedBubble.style.top = (initY + e.clientY - startY) + 'px';
|
| 771 |
}
|
|
@@ -800,10 +780,9 @@ INDEX_HTML = '''
|
|
| 800 |
return;
|
| 801 |
}
|
| 802 |
|
| 803 |
-
//
|
| 804 |
-
|
| 805 |
-
const
|
| 806 |
-
const pH = panel.offsetHeight; // Row Height
|
| 807 |
const iW = img.naturalWidth || img.width;
|
| 808 |
const iH = img.naturalHeight || img.height;
|
| 809 |
|
|
@@ -833,7 +812,7 @@ INDEX_HTML = '''
|
|
| 833 |
function handleZoom(el) { if(!selectedPanel) return; const img = selectedPanel.querySelector('img'); img.dataset.zoom = el.value; updateImageTransform(img); }
|
| 834 |
function resetPanelTransform() { if(!selectedPanel) return alert("Select a panel"); const img = selectedPanel.querySelector('img'); fitImageToPanel(img, selectedPanel, null); saveDraft(true); }
|
| 835 |
|
| 836 |
-
// --- STANDARD FUNCTIONS
|
| 837 |
function selectPanel(el) {
|
| 838 |
if(selectedPanel) selectedPanel.classList.remove('selected');
|
| 839 |
if(selectedBubble) { selectedBubble.classList.remove('selected'); selectedBubble = null; }
|
|
@@ -881,10 +860,9 @@ INDEX_HTML = '''
|
|
| 881 |
async function gotoTimestamp() { if(isProcessing) return; if(!selectedPanel) return alert("Select a panel"); let v = document.getElementById('timestamp-input').value.trim(); if(!v) return; if(v.includes(':')) { let p = v.split(':'); v = parseInt(p[0]) * 60 + parseFloat(p[1]); } else { v = parseFloat(v); } if(isNaN(v)) return alert("Invalid time"); const img = selectedPanel.querySelector('img'); let fname = img.src.split('/').pop().split('?')[0]; setProcessing(true); img.style.opacity = '0.5'; try { const r = await fetch(`/goto_timestamp?sid=${sid}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({filename:fname, timestamp:v}) }); const d = await r.json(); if(d.success) { img.src = `/frames/${fname}?sid=${sid}&t=${Date.now()}`; document.getElementById('timestamp-input').value = ''; } else { alert('Error: ' + d.message); } } catch(e) { console.error(e); } img.style.opacity = '1'; setProcessing(false); saveDraft(true); }
|
| 882 |
async function upload() { const f = document.getElementById('file-upload').files[0]; const pCount = document.getElementById('page-count').value; if(!f) return alert("Select a video"); sid = genUUID(); localStorage.setItem('comic_sid', sid); currentSaveCode = null; document.querySelector('.upload-box').style.display='none'; document.getElementById('loading-view').style.display='flex'; const fd = new FormData(); fd.append('file', f); fd.append('target_pages', pCount); const r = await fetch(`/uploader?sid=${sid}`, {method:'POST', body:fd}); if(r.ok) interval = setInterval(checkStatus, 2000); else { alert("Upload failed"); location.reload(); } }
|
| 883 |
async function checkStatus() { try { const r = await fetch(`/status?sid=${sid}`); const d = await r.json(); document.getElementById('status-text').innerText = d.message; if(d.progress >= 100) { clearInterval(interval); document.getElementById('upload-container').style.display='none'; document.getElementById('editor-container').style.display='block'; loadNewComic(); } else if (d.progress < 0) { clearInterval(interval); document.getElementById('status-text').textContent = "Error: " + d.message; document.querySelector('.loader').style.display = 'none'; } } catch(e) {} }
|
| 884 |
-
function loadNewComic() { fetch(`/output/pages.json?sid=${sid}`).then(r=>r.json()).then(data => { const cleanData = data.map((p, pi) => { const panels = []; const pageBubbles = []; const panelCenters = [{x:
|
| 885 |
function editBubbleText(bubble) { if (currentlyEditing) return; currentlyEditing = bubble; const textSpan = bubble.querySelector('.bubble-text'); const textarea = document.createElement('textarea'); textarea.value = textSpan.textContent; bubble.appendChild(textarea); textSpan.style.display = 'none'; textarea.focus(); const finishEditing = () => { textSpan.textContent = textarea.value; textarea.remove(); textSpan.style.display = ''; currentlyEditing = null; saveDraft(true); }; textarea.addEventListener('blur', finishEditing, { once: true }); textarea.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); textarea.blur(); } }); }
|
| 886 |
async function exportComic() { const pgs = document.querySelectorAll('.comic-page'); if(pgs.length === 0) return alert("No pages found"); if(selectedBubble) selectedBubble.classList.remove('selected'); if(selectedPanel) selectedPanel.classList.remove('selected'); alert(`Exporting ${pgs.length} page(s)...`);
|
| 887 |
-
// Hide handles for export
|
| 888 |
const handles = document.querySelectorAll('.handle'); handles.forEach(h=>h.style.display='none');
|
| 889 |
for(let i = 0; i < pgs.length; i++) { try { const u = await htmlToImage.toPng(pgs[i], { pixelRatio: 2, style: { transform: 'none' } }); const a = document.createElement('a'); a.href = u; a.download = `Comic-Page-${i+1}.png`; a.click(); } catch(err) { console.error(err); alert(`Failed to export page ${i+1}`); } }
|
| 890 |
handles.forEach(h=>h.style.display='block');
|
|
|
|
| 118 |
raw_moments = [{'text': s.content, 'start': s.start.total_seconds(), 'end': s.end.total_seconds()} for s in valid_subs]
|
| 119 |
|
| 120 |
if target_pages <= 0: target_pages = 1
|
| 121 |
+
# CHANGED TO 4 PANELS
|
| 122 |
+
panels_per_page = 4
|
| 123 |
total_panels_needed = target_pages * panels_per_page
|
| 124 |
|
| 125 |
selected_moments = []
|
|
|
|
| 293 |
# 🌐 ROUTES & FULL UI
|
| 294 |
# ======================================================
|
| 295 |
INDEX_HTML = '''
|
| 296 |
+
<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>🎬 4-Panel Comic Architect</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"></script> <link href="https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@700&family=Gloria+Hallelujah&family=Lato&display=swap" rel="stylesheet"> <style> * { box-sizing: border-box; } body { background-color: #fdf6e3; font-family: 'Lato', sans-serif; color: #3d3d3d; margin: 0; min-height: 100vh; }
|
| 297 |
|
| 298 |
#upload-container { display: flex; flex-direction:column; justify-content: center; align-items: center; min-height: 100vh; width: 100%; padding: 20px; }
|
| 299 |
.upload-box { max-width: 500px; width: 100%; padding: 40px; background: white; border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,0.12); text-align: center; }
|
|
|
|
| 335 |
box-shadow: 0 0 50px rgba(0,0,0,0.2);
|
| 336 |
position: relative;
|
| 337 |
z-index: 1;
|
| 338 |
+
overflow: hidden;
|
| 339 |
border: 2px solid black;
|
| 340 |
}
|
| 341 |
|
|
|
|
| 347 |
border: 2px solid white;
|
| 348 |
border-radius: 50%;
|
| 349 |
cursor: ew-resize;
|
| 350 |
+
z-index: 999;
|
| 351 |
transform: translate(-50%, -50%);
|
| 352 |
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
|
| 353 |
}
|
|
|
|
| 369 |
top: 0; left: 0;
|
| 370 |
transform-origin: 0 0;
|
| 371 |
cursor: grab;
|
|
|
|
| 372 |
}
|
| 373 |
.panel img.panning { cursor: grabbing; }
|
| 374 |
|
|
|
|
| 384 |
}
|
| 385 |
.speech-bubble.selected { outline: 2px dashed #4CAF50; z-index: 501; }
|
| 386 |
|
| 387 |
+
/* BUBBLE STYLES */
|
| 388 |
.bubble-text { padding:0.5em; overflow-wrap:break-word; word-wrap:break-word; white-space:pre-wrap; position:relative; z-index:5; pointer-events:none; user-select:none; width:100%; height:100%; display:flex; align-items:center; justify-content:center; border-radius:inherit; }
|
| 389 |
.speech-bubble textarea { position:absolute; top:0; left:0; width:100%; height:100%; box-sizing:border-box; border:1px solid #4CAF50; background:rgba(255,255,255,0.95); text-align:center; padding:8px; z-index:102; resize:none; font:inherit; white-space:pre-wrap; }
|
| 390 |
.speech-bubble.speech { --b:3em; --h:1.8em; --t:0.6; --p:var(--tail-pos,50%); --r:1.2em; background:var(--bubble-fill-color,#4ECDC4); color:var(--bubble-text-color,#fff); padding:0; border-radius:var(--r) var(--r) min(var(--r),calc(100% - var(--p) - (1 - var(--t))*var(--b)/2)) min(var(--r),calc(var(--p) - (1 - var(--t))*var(--b)/2))/var(--r); }
|
|
|
|
| 413 |
.modal-content { background:white; padding:30px; border-radius:12px; max-width:400px; width:90%; text-align:center; }
|
| 414 |
.modal-content .code { font-size:32px; font-weight:bold; letter-spacing:4px; background:#f0f0f0; padding:15px 25px; border-radius:8px; display:inline-block; margin:15px 0; font-family:monospace; user-select:all; }
|
| 415 |
</style>
|
| 416 |
+
</head> <body> <div id="upload-container"> <div class="upload-box"> <h1>🎬 4-Panel Comic Architect</h1> <input type="file" id="file-upload" class="file-input" onchange="document.getElementById('fn').innerText=this.files[0].name"> <label for="file-upload" class="file-label">📁 Choose Video File</label> <span id="fn" style="margin-bottom:10px; display:block; color:#666;">No file selected</span>
|
| 417 |
<div class="page-input-group">
|
| 418 |
<label>📚 Total Comic Pages:</label>
|
| 419 |
<input type="number" id="page-count" value="3" min="1" max="15" placeholder="e.g. 3">
|
|
|
|
| 438 |
<input type="file" id="image-uploader" style="display: none;" accept="image/*">
|
| 439 |
<div class="edit-controls">
|
| 440 |
<h4>✏️ Architect Editor</h4>
|
|
|
|
| 441 |
<button onclick="undoLastAction()" class="undo-btn">↩️ Undo</button>
|
|
|
|
| 442 |
<div class="control-group">
|
| 443 |
<label>📐 Layout Settings:</label>
|
| 444 |
<div class="slider-container">
|
|
|
|
| 448 |
</div>
|
| 449 |
<small style="color:#aaa; font-size:10px;">Drag red dots on panels to resize.</small>
|
| 450 |
</div>
|
|
|
|
| 451 |
<div class="control-group">
|
| 452 |
<label>💾 Save & Load:</label>
|
| 453 |
<button onclick="saveComic()" class="save-btn">💾 Save Comic</button>
|
|
|
|
| 456 |
<button onclick="copyCode()" style="padding:2px; width:auto; font-size:10px;">Copy</button>
|
| 457 |
</div>
|
| 458 |
</div>
|
|
|
|
| 459 |
<div class="control-group">
|
| 460 |
<label>💬 Bubble Styling:</label>
|
| 461 |
<select id="bubble-type-select" onchange="changeBubbleType(this.value)" disabled>
|
|
|
|
| 473 |
<button onclick="deleteBubble()" class="reset-btn">Delete</button>
|
| 474 |
</div>
|
| 475 |
</div>
|
|
|
|
| 476 |
<div class="control-group">
|
| 477 |
<label>🖼️ Panel Tools:</label>
|
| 478 |
<button onclick="replacePanelImage()" class="action-btn">🖼️ Replace Image</button>
|
|
|
|
| 481 |
<button onclick="adjustFrame('forward')" class="action-btn">Next Frame ➡️</button>
|
| 482 |
</div>
|
| 483 |
</div>
|
|
|
|
| 484 |
<div class="control-group">
|
| 485 |
<label>🔍 Zoom & Pan:</label>
|
| 486 |
<div class="button-grid">
|
|
|
|
| 488 |
<input type="range" id="zoom-slider" min="50" max="300" value="100" step="5" disabled oninput="handleZoom(this)">
|
| 489 |
</div>
|
| 490 |
</div>
|
|
|
|
| 491 |
<div class="control-group">
|
| 492 |
<button onclick="exportComic()" class="export-btn">📥 Export as PNG</button>
|
| 493 |
<button onclick="goBackToUpload()" class="reset-btn" style="margin-top:10px;">🏠 Home</button>
|
|
|
|
| 514 |
let startX, startY, initX, initY, panStartX, panStartY, panStartTx, panStartTy;
|
| 515 |
let activeLayoutHandle = null, activePageId = null;
|
| 516 |
|
| 517 |
+
// 4-PANEL DEFAULT LAYOUT
|
| 518 |
const DEFAULT_LAYOUT = {
|
| 519 |
width: 1000, height: 700, gutter: 10, tierY: 350,
|
| 520 |
+
r1: { topX: 500, botX: 500 }, // Top Row Vertical Split
|
| 521 |
+
r2: { topX: 500, botX: 500 } // Bottom Row Vertical Split
|
|
|
|
| 522 |
};
|
| 523 |
|
| 524 |
let historyStack = [];
|
|
|
|
| 627 |
div.id = pageId;
|
| 628 |
div.dataset.layout = JSON.stringify(layout);
|
| 629 |
|
| 630 |
+
// Generate 4 Panels
|
| 631 |
+
for(let i=0; i<4; i++) {
|
| 632 |
const pDiv = document.createElement('div');
|
| 633 |
pDiv.className = 'panel'; pDiv.id = `${pageId}-p${i}`;
|
| 634 |
|
|
|
|
| 643 |
div.appendChild(pDiv);
|
| 644 |
}
|
| 645 |
|
| 646 |
+
// Generate Handles (Vertical Dividers and Tier Divider)
|
| 647 |
const handles = [
|
| 648 |
+
{id: 'h1-top', t:0, l:0}, {id: 'h1-bot', t:0, l:0}, // Top Row
|
| 649 |
+
{id: 'h2-top', t:0, l:0}, {id: 'h2-bot', t:0, l:0}, // Bottom Row
|
| 650 |
+
{id: 'h-tier', t:0, l:0, cls: 'horiz'} // Horizontal Tier
|
|
|
|
| 651 |
];
|
| 652 |
handles.forEach(h => {
|
| 653 |
const hDiv = document.createElement('div');
|
|
|
|
| 680 |
document.getElementById('font-select').disabled = true;
|
| 681 |
}
|
| 682 |
|
| 683 |
+
// --- LAYOUT ENGINE (4-PANEL) ---
|
| 684 |
function drawLayout(pageId) {
|
| 685 |
const pageEl = document.getElementById(pageId);
|
| 686 |
if(!pageEl) return;
|
|
|
|
| 690 |
const w = state.width;
|
| 691 |
const h = state.height;
|
| 692 |
|
| 693 |
+
// P0: Top Left
|
| 694 |
+
const p0 = document.getElementById(`${pageId}-p0`);
|
| 695 |
+
p0.style.top='0px'; p0.style.left='0px'; p0.style.width='100%'; p0.style.height = ty + 'px';
|
| 696 |
+
p0.style.clipPath = `polygon(0 0, ${state.r1.topX - g}px 0, ${state.r1.botX - g}px ${ty}px, 0 ${ty}px)`;
|
| 697 |
+
|
| 698 |
+
// P1: Top Right
|
| 699 |
+
const p1 = document.getElementById(`${pageId}-p1`);
|
| 700 |
p1.style.top='0px'; p1.style.left='0px'; p1.style.width='100%'; p1.style.height = ty + 'px';
|
| 701 |
+
p1.style.clipPath = `polygon(${state.r1.topX + g}px 0, ${w}px 0, ${w}px ${ty}px, ${state.r1.botX + g}px ${ty}px)`;
|
| 702 |
|
| 703 |
+
// P2: Bottom Left
|
| 704 |
+
const p2 = document.getElementById(`${pageId}-p2`);
|
| 705 |
+
p2.style.top = ty + 'px'; p2.style.left='0px'; p2.style.width='100%'; p2.style.height = (h - ty) + 'px';
|
| 706 |
+
p2.style.clipPath = `polygon(0 ${g}px, ${state.r2.topX - g}px ${g}px, ${state.r2.botX - g}px ${h-ty}px, 0 ${h-ty}px)`;
|
| 707 |
|
| 708 |
+
// P3: Bottom Right
|
| 709 |
+
const p3 = document.getElementById(`${pageId}-p3`);
|
| 710 |
p3.style.top = ty + 'px'; p3.style.left='0px'; p3.style.width='100%'; p3.style.height = (h - ty) + 'px';
|
| 711 |
+
p3.style.clipPath = `polygon(${state.r2.topX + g}px ${g}px, ${w}px ${g}px, ${w}px ${h-ty}px, ${state.r2.botX + g}px ${h-ty}px)`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 712 |
|
| 713 |
// Update Handles Position
|
| 714 |
const setH = (id, l, t) => { const el = document.getElementById(`${pageId}-${id}`); if(el) { el.style.left=l+'px'; el.style.top=t+'px'; } };
|
| 715 |
|
| 716 |
setH('h1-top', state.r1.topX, 0);
|
| 717 |
setH('h1-bot', state.r1.botX, ty);
|
| 718 |
+
setH('h2-top', state.r2.topX, ty);
|
| 719 |
+
setH('h2-bot', state.r2.botX, h);
|
|
|
|
|
|
|
| 720 |
setH('h-tier', w/2, ty);
|
| 721 |
}
|
| 722 |
|
|
|
|
| 724 |
e.stopPropagation(); e.preventDefault();
|
| 725 |
activeLayoutHandle = handleRole;
|
| 726 |
activePageId = pageId;
|
| 727 |
+
isDragging = true;
|
| 728 |
}
|
| 729 |
|
| 730 |
// --- GLOBAL MOUSE EVENTS ---
|
|
|
|
| 733 |
const pageEl = document.getElementById(activePageId);
|
| 734 |
const rect = pageEl.getBoundingClientRect();
|
| 735 |
const state = JSON.parse(pageEl.dataset.layout);
|
| 736 |
+
const x = Math.max(0, Math.min(1000, (e.clientX - rect.left) * (1000 / rect.width)));
|
| 737 |
const y = Math.max(50, Math.min(650, (e.clientY - rect.top) * (700 / rect.height)));
|
| 738 |
|
| 739 |
if(activeLayoutHandle === 'h1-top') state.r1.topX = x;
|
| 740 |
else if(activeLayoutHandle === 'h1-bot') state.r1.botX = x;
|
| 741 |
+
else if(activeLayoutHandle === 'h2-top') state.r2.topX = x;
|
| 742 |
+
else if(activeLayoutHandle === 'h2-bot') state.r2.botX = x;
|
|
|
|
|
|
|
| 743 |
else if(activeLayoutHandle === 'h-tier') state.tierY = y;
|
| 744 |
|
| 745 |
pageEl.dataset.layout = JSON.stringify(state);
|
| 746 |
drawLayout(activePageId);
|
| 747 |
}
|
| 748 |
else if(isDragging && selectedBubble) {
|
|
|
|
| 749 |
selectedBubble.style.left = (initX + e.clientX - startX) + 'px';
|
| 750 |
selectedBubble.style.top = (initY + e.clientY - startY) + 'px';
|
| 751 |
}
|
|
|
|
| 780 |
return;
|
| 781 |
}
|
| 782 |
|
| 783 |
+
// Fit to 1000px width container to ensure panning works across whole row
|
| 784 |
+
const pW = 1000;
|
| 785 |
+
const pH = panel.offsetHeight;
|
|
|
|
| 786 |
const iW = img.naturalWidth || img.width;
|
| 787 |
const iH = img.naturalHeight || img.height;
|
| 788 |
|
|
|
|
| 812 |
function handleZoom(el) { if(!selectedPanel) return; const img = selectedPanel.querySelector('img'); img.dataset.zoom = el.value; updateImageTransform(img); }
|
| 813 |
function resetPanelTransform() { if(!selectedPanel) return alert("Select a panel"); const img = selectedPanel.querySelector('img'); fitImageToPanel(img, selectedPanel, null); saveDraft(true); }
|
| 814 |
|
| 815 |
+
// --- STANDARD FUNCTIONS ---
|
| 816 |
function selectPanel(el) {
|
| 817 |
if(selectedPanel) selectedPanel.classList.remove('selected');
|
| 818 |
if(selectedBubble) { selectedBubble.classList.remove('selected'); selectedBubble = null; }
|
|
|
|
| 860 |
async function gotoTimestamp() { if(isProcessing) return; if(!selectedPanel) return alert("Select a panel"); let v = document.getElementById('timestamp-input').value.trim(); if(!v) return; if(v.includes(':')) { let p = v.split(':'); v = parseInt(p[0]) * 60 + parseFloat(p[1]); } else { v = parseFloat(v); } if(isNaN(v)) return alert("Invalid time"); const img = selectedPanel.querySelector('img'); let fname = img.src.split('/').pop().split('?')[0]; setProcessing(true); img.style.opacity = '0.5'; try { const r = await fetch(`/goto_timestamp?sid=${sid}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({filename:fname, timestamp:v}) }); const d = await r.json(); if(d.success) { img.src = `/frames/${fname}?sid=${sid}&t=${Date.now()}`; document.getElementById('timestamp-input').value = ''; } else { alert('Error: ' + d.message); } } catch(e) { console.error(e); } img.style.opacity = '1'; setProcessing(false); saveDraft(true); }
|
| 861 |
async function upload() { const f = document.getElementById('file-upload').files[0]; const pCount = document.getElementById('page-count').value; if(!f) return alert("Select a video"); sid = genUUID(); localStorage.setItem('comic_sid', sid); currentSaveCode = null; document.querySelector('.upload-box').style.display='none'; document.getElementById('loading-view').style.display='flex'; const fd = new FormData(); fd.append('file', f); fd.append('target_pages', pCount); const r = await fetch(`/uploader?sid=${sid}`, {method:'POST', body:fd}); if(r.ok) interval = setInterval(checkStatus, 2000); else { alert("Upload failed"); location.reload(); } }
|
| 862 |
async function checkStatus() { try { const r = await fetch(`/status?sid=${sid}`); const d = await r.json(); document.getElementById('status-text').innerText = d.message; if(d.progress >= 100) { clearInterval(interval); document.getElementById('upload-container').style.display='none'; document.getElementById('editor-container').style.display='block'; loadNewComic(); } else if (d.progress < 0) { clearInterval(interval); document.getElementById('status-text').textContent = "Error: " + d.message; document.querySelector('.loader').style.display = 'none'; } } catch(e) {} }
|
| 863 |
+
function loadNewComic() { fetch(`/output/pages.json?sid=${sid}`).then(r=>r.json()).then(data => { const cleanData = data.map((p, pi) => { const panels = []; const pageBubbles = []; const panelCenters = [{x: 250, y: 175}, {x: 750, y: 175}, {x: 250, y: 535}, {x: 750, y: 535}]; p.panels.forEach((pan, j) => { panels.push({ src: `/frames/${pan.image}?sid=${sid}`, bubbles: [] }); if(p.bubbles && p.bubbles[j] && p.bubbles[j].dialog) { const center = panelCenters[j % 4] || {x:500, y:350}; pageBubbles.push({ text: p.bubbles[j].dialog, left: (center.x - 75) + 'px', top: (center.y - 40) + 'px', type: (p.bubbles[j].type || 'speech'), classes: `speech-bubble ${p.bubbles[j].type || 'speech'} tail-bottom` }); } }); return { panels: panels, pageBubbles: pageBubbles }; }); renderFromState(cleanData); saveDraft(true); }); }
|
| 864 |
function editBubbleText(bubble) { if (currentlyEditing) return; currentlyEditing = bubble; const textSpan = bubble.querySelector('.bubble-text'); const textarea = document.createElement('textarea'); textarea.value = textSpan.textContent; bubble.appendChild(textarea); textSpan.style.display = 'none'; textarea.focus(); const finishEditing = () => { textSpan.textContent = textarea.value; textarea.remove(); textSpan.style.display = ''; currentlyEditing = null; saveDraft(true); }; textarea.addEventListener('blur', finishEditing, { once: true }); textarea.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); textarea.blur(); } }); }
|
| 865 |
async function exportComic() { const pgs = document.querySelectorAll('.comic-page'); if(pgs.length === 0) return alert("No pages found"); if(selectedBubble) selectedBubble.classList.remove('selected'); if(selectedPanel) selectedPanel.classList.remove('selected'); alert(`Exporting ${pgs.length} page(s)...`);
|
|
|
|
| 866 |
const handles = document.querySelectorAll('.handle'); handles.forEach(h=>h.style.display='none');
|
| 867 |
for(let i = 0; i < pgs.length; i++) { try { const u = await htmlToImage.toPng(pgs[i], { pixelRatio: 2, style: { transform: 'none' } }); const a = document.createElement('a'); a.href = u; a.download = `Comic-Page-${i+1}.png`; a.click(); } catch(err) { console.error(err); alert(`Failed to export page ${i+1}`); } }
|
| 868 |
handles.forEach(h=>h.style.display='block');
|