tester343 commited on
Commit
c70f030
·
verified ·
1 Parent(s): 717369e

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. 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
- panels_per_page = 5
 
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>🎬 Manual Drag 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; }
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; /* Contains handles? No, let handles spill if needed, but we keep frames inside */
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; /* Highest priority */
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 ... (Same as before) */
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>🎬 Manual Drag 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,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
- // Page Layout Data Store (Default)
525
  const DEFAULT_LAYOUT = {
526
  width: 1000, height: 700, gutter: 10, tierY: 350,
527
- r1: { topX: 635, botX: 588 },
528
- r2L: { topX: 293, botX: 326 },
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<5; 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-l-top', t:0, l:0}, {id: 'h2-l-bot', t:0, l:0},
658
- {id: 'h2-r-top', t:0, l:0}, {id: 'h2-r-bot', t:0, l:0},
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
- // Draw Panels (Using simple absolute positioning + clip-path)
703
- // P1: Top Left
704
- const p1 = document.getElementById(`${pageId}-p0`);
 
 
 
 
705
  p1.style.top='0px'; p1.style.left='0px'; p1.style.width='100%'; p1.style.height = ty + 'px';
706
- p1.style.clipPath = `polygon(0 0, ${state.r1.topX - g}px 0, ${state.r1.botX - g}px ${ty}px, 0 ${ty}px)`;
707
 
708
- // P2: Top Right
709
- const p2 = document.getElementById(`${pageId}-p1`);
710
- p2.style.top='0px'; p2.style.left='0px'; p2.style.width='100%'; p2.style.height = ty + 'px';
711
- p2.style.clipPath = `polygon(${state.r1.topX + g}px 0, ${w}px 0, ${w}px ${ty}px, ${state.r1.botX + g}px ${ty}px)`;
712
 
713
- // P3: Bottom Left
714
- const p3 = document.getElementById(`${pageId}-p2`);
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(0 ${g}px, ${state.r2L.topX - g}px ${g}px, ${state.r2L.botX - g}px ${h-ty}px, 0 ${h-ty}px)`;
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-l-top', state.r2L.topX, ty);
734
- setH('h2-l-bot', state.r2L.botX, h);
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; // Use global drag flag to prevent conflicting events
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))); // Scale to 1000px base
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-l-top') state.r2L.topX = x;
759
- else if(activeLayoutHandle === 'h2-l-bot') state.r2L.botX = x;
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
- // Smart Fit: Fill container but allow freedom
804
- // Since panels are now width: 100%, we calculate against the container size
805
- const pW = 1000; // Fixed Base Width
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 (Bubbles, selection, upload etc) ---
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: 315, y: 175}, {x: 790, y: 175}, {x: 160, y: 535}, {x: 480, y: 535}, {x: 800, 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 % 5] || {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); }); }
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');