tester343 commited on
Commit
55eb357
·
verified ·
1 Parent(s): 6b724b1

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. app_enhanced.py +107 -299
app_enhanced.py CHANGED
@@ -332,37 +332,39 @@ INDEX_HTML = '''
332
 
333
  .comic-page { background: white; width: 600px; height: 400px; box-shadow: 0 4px 10px rgba(0,0,0,0.1); position: relative; overflow: hidden; border: 2px solid #000; padding: 10px; }
334
 
335
- /* === ADJUSTABLE LAYOUTS === */
336
  .comic-grid { width: 100%; height: 100%; position: relative; background: #000; display:grid; gap: 10px; }
337
 
338
- /* Layout: Rows (Horizontal Split) - Default */
339
  .comic-grid.layout-rows { grid-template-columns: 1fr; grid-auto-rows: 1fr; }
340
-
341
- /* Layout: Columns (Vertical Split) */
342
  .comic-grid.layout-cols { grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); grid-template-rows: 1fr; }
343
-
344
- /* Layout: Grid (2x2) */
345
  .comic-grid.layout-grid { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; }
346
 
347
- /* Layout: Slanted (Designer) */
348
- .comic-grid.layout-slanted { display: block; }
349
- .comic-grid.layout-slanted .panel { position: absolute; width:100%; left:0; }
350
- /* Top Panel */
351
- .comic-grid.layout-slanted .panel:nth-child(1) { top: 0; height: 55%; clip-path: polygon(0% 0%, 100% 0%, 100% 85%, 0% 100%); z-index:2; }
352
- /* Bottom Panel */
353
- .comic-grid.layout-slanted .panel:nth-child(2) { top: 45%; height: 55%; clip-path: polygon(0% 15%, 100% 0%, 100% 100%, 0% 100%); z-index:1; }
354
- /* Hide extra panels in slanted mode */
355
- .comic-grid.layout-slanted .panel:nth-child(n+3) { display: none; }
 
 
 
 
 
 
 
 
356
 
357
 
358
  .panel { overflow: hidden; background: #eee; cursor: pointer; border: 2px solid #000; position: relative; }
359
- .layout-slanted .panel { border: none; background: transparent; }
360
  .panel.selected { z-index: 20; border-color: #2196F3; }
361
 
362
- /* Image fitting */
363
  .panel img {
364
  width: 100%; height: 100%;
365
- object-fit: contain; /* Default: Show entire image (no cut) */
366
  transition: transform 0.1s ease-out;
367
  transform-origin: center center;
368
  pointer-events: auto;
@@ -393,19 +395,15 @@ INDEX_HTML = '''
393
  .speech-bubble.selected { outline: 2px dashed #4CAF50; z-index: 100; }
394
  .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; }
395
 
396
- /* SPEECH BUBBLE CSS (Tails) */
397
  .speech-bubble.speech {
398
  --b: 3em; --h: 1.8em; --t: 0.6; --p: var(--tail-pos, 50%); --r: 1.2em;
399
- background: var(--bubble-fill-color, #4ECDC4);
400
- color: var(--bubble-text-color, #fff);
401
- padding: 0;
402
  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);
403
  }
404
  .speech-bubble.speech:before {
405
  content: ""; position: absolute; width: var(--b); height: var(--h);
406
  background: inherit; border-bottom-left-radius: 100%; pointer-events: none; z-index: 1;
407
- -webkit-mask: radial-gradient(calc(var(--t)*100%) 105% at 100% 0,#0000 99%,#000 101%);
408
- mask: radial-gradient(calc(var(--t)*100%) 105% at 100% 0,#0000 99%,#000 101%);
409
  }
410
 
411
  .speech-bubble.speech.tail-bottom:before { top: 100%; left: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); }
@@ -415,7 +413,6 @@ INDEX_HTML = '''
415
  .speech-bubble.speech.tail-right { border-radius: var(--r); }
416
  .speech-bubble.speech.tail-right:before { left: 100%; top: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); transform: rotate(-90deg); transform-origin: top left; }
417
 
418
- /* Thought/Reaction Styles */
419
  .speech-bubble.thought { background: white; border: 2px dashed #555; color: #333; border-radius: 50%; }
420
  .speech-bubble.thought::before { display:none; }
421
  .thought-dot { position: absolute; background-color: white; border: 2px solid #555; border-radius: 50%; z-index: -1; }
@@ -498,7 +495,7 @@ INDEX_HTML = '''
498
  <option value="layout-rows">Rows (Horizontal)</option>
499
  <option value="layout-cols">Cols (Vertical)</option>
500
  <option value="layout-grid">Grid (2x2)</option>
501
- <option value="layout-slanted">Slanted (Designer)</option>
502
  </select>
503
  </div>
504
 
@@ -589,10 +586,11 @@ INDEX_HTML = '''
589
  let currentSaveCode = null;
590
  let isProcessing = false;
591
  let interval, selectedBubble = null, selectedPanel = null;
592
- let isDragging = false, isResizing = false, isPanning = false;
593
  let startX, startY, initX, initY, panStartX, panStartY, panStartTx, panStartTy;
594
  let resizeHandle, originalWidth, originalHeight, originalMouseX, originalMouseY;
595
  let currentlyEditing = null;
 
596
 
597
  // UNDO SYSTEM
598
  let historyStack = [];
@@ -673,12 +671,15 @@ INDEX_HTML = '''
673
  const pages = [];
674
  document.querySelectorAll('.comic-page').forEach(p => {
675
  const grid = p.querySelector('.comic-grid');
676
- // Extract layout class
677
  let layout = 'layout-rows';
678
  if(grid.classList.contains('layout-cols')) layout = 'layout-cols';
679
  if(grid.classList.contains('layout-grid')) layout = 'layout-grid';
680
- if(grid.classList.contains('layout-slanted')) layout = 'layout-slanted';
681
 
 
 
 
 
682
  const panels = [];
683
  grid.querySelectorAll('.panel').forEach(pan => {
684
  const img = pan.querySelector('img');
@@ -702,7 +703,7 @@ INDEX_HTML = '''
702
  bubbles: bubbles
703
  });
704
  });
705
- pages.push({ layout: layout, panels: panels });
706
  });
707
  return pages;
708
  }
@@ -722,8 +723,18 @@ INDEX_HTML = '''
722
  const div = document.createElement('div'); div.className = 'comic-page';
723
  const grid = document.createElement('div');
724
 
725
- // Restore Layout
726
  grid.className = 'comic-grid ' + (page.layout || 'layout-rows');
 
 
 
 
 
 
 
 
 
 
 
727
 
728
  page.panels.forEach((pan) => {
729
  const pDiv = document.createElement('div'); pDiv.className = 'panel';
@@ -735,12 +746,7 @@ INDEX_HTML = '''
735
  if(pan.fit === 'cover') img.classList.add('fit-cover');
736
 
737
  updateImageTransform(img);
738
-
739
- // === EVENTS FOR PANNING AND ZOOMING ===
740
- img.onmousedown = (e) => {
741
- selectPanel(pDiv); // Auto select
742
- startPan(e, img);
743
- };
744
  img.onwheel = (e) => handleWheelZoom(e, img);
745
 
746
  pDiv.appendChild(img);
@@ -748,328 +754,130 @@ INDEX_HTML = '''
748
  grid.appendChild(pDiv);
749
  });
750
  div.appendChild(grid); pageWrapper.appendChild(div); con.appendChild(pageWrapper);
751
-
752
- // Sync layout dropdown if this is the first page
753
  if(pageIdx === 0) document.getElementById('layout-select').value = (page.layout || 'layout-rows');
754
  });
755
- selectedBubble = null;
756
- selectedPanel = null;
757
- document.getElementById('bubble-type-select').disabled = true;
758
- document.getElementById('font-select').disabled = true;
759
  }
760
 
761
- async function upload() {
762
- const f = document.getElementById('file-upload').files[0];
763
- const pCount = document.getElementById('page-count').value;
764
- const panelCount = document.getElementById('panels-per-page').value;
765
- if(!f) return alert("Select a video");
766
- sid = genUUID(); localStorage.setItem('comic_sid', sid); currentSaveCode = null;
767
- document.querySelector('.upload-box').style.display='none';
768
- document.getElementById('loading-view').style.display='flex';
769
- const fd = new FormData(); fd.append('file', f); fd.append('target_pages', pCount); fd.append('panels_per_page', panelCount);
770
- const r = await fetch(`/uploader?sid=${sid}`, {method:'POST', body:fd});
771
- if(r.ok) interval = setInterval(checkStatus, 2000);
772
- else { alert("Upload failed"); location.reload(); }
773
  }
774
 
775
- async function checkStatus() {
776
- try {
777
- const r = await fetch(`/status?sid=${sid}`); const d = await r.json();
778
- document.getElementById('status-text').innerText = d.message;
779
- if(d.progress >= 100) { clearInterval(interval); document.getElementById('upload-container').style.display='none'; document.getElementById('editor-container').style.display='block'; loadNewComic(); }
780
- else if (d.progress < 0) { clearInterval(interval); document.getElementById('status-text').textContent = "Error: " + d.message; document.querySelector('.loader').style.display = 'none'; }
781
- } catch(e) {}
782
- }
 
 
 
 
 
 
 
 
 
 
 
783
 
784
- function loadNewComic() {
785
- fetch(`/output/pages.json?sid=${sid}`).then(r=>r.json()).then(data => {
786
- const cleanData = data.map((p, pi) => ({
787
- layout: 'layout-rows', // Default layout
788
- panels: p.panels.map((pan, j) => ({
789
- src: `/frames/${pan.image}?sid=${sid}`,
790
- fit: 'contain', // Default no cut
791
- bubbles: (p.bubbles && p.bubbles[j] && p.bubbles[j].dialog) ? [{
792
- text: p.bubbles[j].dialog,
793
- // Backend now sends -1, -1 for center.
794
- // If coordinates are valid, append 'px', else use defaults.
795
- left: (p.bubbles[j].bubble_offset_x > 0 ? p.bubbles[j].bubble_offset_x + 'px' : '50%'),
796
- top: (p.bubbles[j].bubble_offset_y > 0 ? p.bubbles[j].bubble_offset_y + 'px' : '50%'),
797
- type: (p.bubbles[j].type || 'speech'),
798
- classes: `speech-bubble ${p.bubbles[j].type || 'speech'} tail-bottom`,
799
- transform: (p.bubbles[j].bubble_offset_x > 0 ? 'none' : 'translate(-50%, -50%)')
800
- }] : []
801
- }))
802
- }));
803
- renderFromState(cleanData); saveDraft(true);
804
- });
805
- }
806
 
807
  function createBubbleHTML(data) {
808
  const b = document.createElement('div');
809
  const type = data.type || 'speech';
810
  b.className = data.classes || `speech-bubble ${type} tail-bottom`;
811
  if (type === 'thought' && !b.className.includes('pos-')) b.className += ' pos-bl';
812
-
813
  b.dataset.type = type;
814
  b.style.left = data.left; b.style.top = data.top;
815
  if(data.transform) b.style.transform = data.transform;
816
-
817
  if(data.width) b.style.width = data.width; if(data.height) b.style.height = data.height;
818
  if(data.font) b.style.fontFamily = data.font;
819
  if(data.colors) { b.style.setProperty('--bubble-fill-color', data.colors.fill || '#4ECDC4'); b.style.setProperty('--bubble-text-color', data.colors.text || '#ffffff'); }
820
  if(data.tailPos) b.style.setProperty('--tail-pos', data.tailPos);
821
-
822
  const textSpan = document.createElement('span'); textSpan.className = 'bubble-text'; textSpan.textContent = data.text || ''; b.appendChild(textSpan);
823
-
824
  if(type === 'thought') { for(let i=1; i<=2; i++){ const d = document.createElement('div'); d.className = `thought-dot thought-dot-${i}`; b.appendChild(d); } }
825
-
826
  ['nw', 'ne', 'sw', 'se'].forEach(dir => { const handle = document.createElement('div'); handle.className = `resize-handle ${dir}`; handle.onmousedown = (e) => startResize(e, dir); b.appendChild(handle); });
827
-
828
  b.onmousedown = (e) => {
829
  if(e.target.classList.contains('resize-handle')) return;
830
  e.stopPropagation(); selectBubble(b);
831
- isDragging = true;
832
- startX = e.clientX; startY = e.clientY;
833
- initX = b.offsetLeft; initY = b.offsetTop;
834
-
835
- // If we drag, remove centering transform to prevent jump
836
- if(b.style.transform.includes('translate')) {
837
- b.style.transform = 'none';
838
- // Recalculate position to be exact pixels instead of %
839
- b.style.left = (b.offsetLeft - b.offsetWidth/2) + 'px';
840
- b.style.top = (b.offsetTop - b.offsetHeight/2) + 'px';
841
- initX = parseInt(b.style.left);
842
- initY = parseInt(b.style.top);
843
- }
844
  };
845
  b.onclick = (e) => { e.stopPropagation(); };
846
  b.ondblclick = (e) => { e.stopPropagation(); editBubbleText(b); };
847
  return b;
848
  }
849
-
850
  function editBubbleText(bubble) {
851
  if (currentlyEditing) return; currentlyEditing = bubble;
852
  const textSpan = bubble.querySelector('.bubble-text'); const textarea = document.createElement('textarea');
853
  textarea.value = textSpan.textContent; bubble.appendChild(textarea); textSpan.style.display = 'none'; textarea.focus();
854
- const finishEditing = () => {
855
- textSpan.textContent = textarea.value; textarea.remove(); textSpan.style.display = ''; currentlyEditing = null;
856
- saveDraft(true);
857
- };
858
  textarea.addEventListener('blur', finishEditing, { once: true });
859
  textarea.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); textarea.blur(); } });
860
  }
861
-
862
- document.addEventListener('mousemove', (e) => {
863
- if(isDragging && selectedBubble) { selectedBubble.style.left = (initX + e.clientX - startX) + 'px'; selectedBubble.style.top = (initY + e.clientY - startY) + 'px'; }
864
- if(isResizing && selectedBubble) { resizeBubble(e); }
865
- if(isPanning && selectedPanel) { panImage(e); }
866
- });
867
-
868
- document.addEventListener('mouseup', () => {
869
- if(isDragging || isResizing || isPanning) {
870
- saveDraft(true);
871
- }
872
- isDragging = false; isResizing = false; isPanning = false;
873
- });
874
-
875
  function startResize(e, dir) { e.preventDefault(); e.stopPropagation(); isResizing = true; resizeHandle = dir; const rect = selectedBubble.getBoundingClientRect(); originalWidth = rect.width; originalHeight = rect.height; originalMouseX = e.clientX; originalMouseY = e.clientY; }
876
  function resizeBubble(e) { if (!isResizing || !selectedBubble) return; const dx = e.clientX - originalMouseX; const dy = e.clientY - originalMouseY; if(resizeHandle.includes('e')) selectedBubble.style.width = (originalWidth + dx)+'px'; if(resizeHandle.includes('s')) selectedBubble.style.height = (originalHeight + dy)+'px'; }
877
-
878
  function selectBubble(el) {
879
- if(selectedBubble) selectedBubble.classList.remove('selected');
880
- if(selectedPanel) { selectedPanel.classList.remove('selected'); selectedPanel = null; }
881
  selectedBubble = el; el.classList.add('selected');
882
- document.getElementById('bubble-type-select').disabled = false;
883
- document.getElementById('font-select').disabled = false;
884
- document.getElementById('bubble-text-color').disabled = false;
885
- document.getElementById('bubble-fill-color').disabled = false;
886
- document.getElementById('tail-controls').style.display = 'block';
887
- document.getElementById('bubble-type-select').value = el.dataset.type || 'speech';
888
  }
889
-
890
  function selectPanel(el) {
891
- if(selectedPanel) selectedPanel.classList.remove('selected');
892
- if(selectedBubble) { selectedBubble.classList.remove('selected'); selectedBubble = null; }
893
  selectedPanel = el; el.classList.add('selected');
894
- document.getElementById('zoom-slider').disabled = false;
895
- const img = el.querySelector('img');
896
- document.getElementById('zoom-slider').value = img.dataset.zoom || 100;
897
- document.getElementById('bubble-type-select').disabled = true;
898
- document.getElementById('font-select').disabled = true;
899
- document.getElementById('tail-controls').style.display = 'none';
900
-
901
- // Update Fit Button Text
902
  document.getElementById('fit-btn').innerText = img.classList.contains('fit-cover') ? "Fit: Cover" : "Fit: Contain";
903
  }
904
-
905
- function addBubble() {
906
- if(!selectedPanel) return alert("Select a panel first");
907
- // Force center placement
908
- const b = createBubbleHTML({ text: "Text", left: "50%", top: "50%", type: 'speech', classes: "speech-bubble speech tail-bottom", transform: "translate(-50%, -50%)" });
909
- selectedPanel.appendChild(b); selectBubble(b); saveDraft(true);
910
- }
911
-
912
- function deleteBubble() {
913
- if(!selectedBubble) return alert("Select a bubble");
914
- selectedBubble.remove(); selectedBubble=null; saveDraft(true);
915
- }
916
-
917
- function changeBubbleType(type) {
918
- if(!selectedBubble) return;
919
- selectedBubble.dataset.type = type;
920
- selectedBubble.className = 'speech-bubble ' + type + ' selected';
921
-
922
- if(type === 'thought') selectedBubble.classList.add('pos-bl');
923
- else selectedBubble.classList.add('tail-bottom');
924
-
925
- selectedBubble.querySelectorAll('.thought-dot').forEach(d=>d.remove());
926
- if(type === 'thought') { for(let i=1; i<=2; i++){ const d = document.createElement('div'); d.className = `thought-dot thought-dot-${i}`; selectedBubble.appendChild(d); } }
927
- saveDraft(true);
928
- }
929
-
930
  function changeFont(font) { if(!selectedBubble) return; selectedBubble.style.fontFamily = font; saveDraft(true); }
931
-
932
- function rotateTail() {
933
- if(!selectedBubble) return;
934
- const type = selectedBubble.dataset.type;
935
-
936
- if(type === 'speech') {
937
- const positions = ['tail-bottom', 'tail-right', 'tail-top', 'tail-left'];
938
- let current = 0;
939
- positions.forEach((pos, i) => { if(selectedBubble.classList.contains(pos)) current = i; });
940
- selectedBubble.classList.remove(positions[current]);
941
- selectedBubble.classList.add(positions[(current + 1) % 4]);
942
- }
943
- else if (type === 'thought') {
944
- const positions = ['pos-bl', 'pos-br', 'pos-tr', 'pos-tl'];
945
- let current = 0;
946
- positions.forEach((pos, i) => { if(selectedBubble.classList.contains(pos)) current = i; });
947
- selectedBubble.classList.remove(positions[current]);
948
- selectedBubble.classList.add(positions[(current + 1) % 4]);
949
- }
950
- saveDraft(true);
951
- }
952
-
953
  function slideTail(v) { if(selectedBubble) { selectedBubble.style.setProperty('--tail-pos', v+'%'); saveDraft(true); } }
954
-
955
  document.getElementById('bubble-text-color').addEventListener('change', (e) => { if(selectedBubble) { selectedBubble.style.setProperty('--bubble-text-color', e.target.value); saveDraft(true); } });
956
  document.getElementById('bubble-fill-color').addEventListener('change', (e) => { if(selectedBubble) { selectedBubble.style.setProperty('--bubble-fill-color', e.target.value); saveDraft(true); } });
957
-
958
  function handleZoom(el) { if(!selectedPanel) return; const img = selectedPanel.querySelector('img'); img.dataset.zoom = el.value; updateImageTransform(img); }
959
  document.getElementById('zoom-slider').addEventListener('change', () => saveDraft(true));
960
-
961
- // === NEW LOGIC: WHEEL ZOOM ON IMAGE ===
962
- function handleWheelZoom(e, img) {
963
- e.preventDefault();
964
- e.stopPropagation();
965
-
966
- if(!selectedPanel) selectPanel(img.closest('.panel'));
967
-
968
- let scale = parseFloat(img.dataset.zoom) || 100;
969
- const delta = -Math.sign(e.deltaY) * 10; // Zoom sensitivity
970
-
971
- scale += delta;
972
- if(scale < 10) scale = 10;
973
- if(scale > 500) scale = 500;
974
-
975
- img.dataset.zoom = scale;
976
- document.getElementById('zoom-slider').value = scale;
977
- updateImageTransform(img);
978
- }
979
-
980
- // === NEW LOGIC: UNRESTRICTED PANNING ===
981
- function startPan(e, img) {
982
- e.preventDefault();
983
- isPanning = true;
984
- selectedPanel = img.closest('.panel');
985
- panStartX = e.clientX;
986
- panStartY = e.clientY;
987
- panStartTx = parseFloat(img.dataset.translateX) || 0;
988
- panStartTy = parseFloat(img.dataset.translateY) || 0;
989
- img.classList.add('panning');
990
- }
991
-
992
- function panImage(e) {
993
- if(!isPanning || !selectedPanel) return;
994
- const img = selectedPanel.querySelector('img');
995
- // 1:1 Movement
996
- const dx = e.clientX - panStartX;
997
- const dy = e.clientY - panStartY;
998
-
999
- img.dataset.translateX = panStartTx + dx;
1000
- img.dataset.translateY = panStartTy + dy;
1001
- updateImageTransform(img);
1002
- }
1003
-
1004
  function updateImageTransform(img) { const z = (img.dataset.zoom || 100) / 100; const x = img.dataset.translateX || 0; const y = img.dataset.translateY || 0; img.style.transform = `translateX(${x}px) translateY(${y}px) scale(${z})`; img.classList.toggle('pannable', true); }
1005
  function resetPanelTransform() { if(!selectedPanel) return alert("Select a panel"); const img = selectedPanel.querySelector('img'); img.dataset.zoom = 100; img.dataset.translateX = 0; img.dataset.translateY = 0; document.getElementById('zoom-slider').value = 100; updateImageTransform(img); saveDraft(true); }
1006
-
1007
- // === LAYOUT & FIT FUNCTIONS ===
1008
- function changeLayout(newLayout) {
1009
- // Apply layout to all pages for consistency
1010
- document.querySelectorAll('.comic-grid').forEach(g => {
1011
- g.className = 'comic-grid ' + newLayout;
1012
- });
1013
- saveDraft(true);
1014
- }
1015
-
1016
- function toggleFitMode() {
1017
- if(!selectedPanel) return alert("Select a panel");
1018
- const img = selectedPanel.querySelector('img');
1019
- if(img.classList.contains('fit-cover')) {
1020
- img.classList.remove('fit-cover'); // Switch to Contain
1021
- document.getElementById('fit-btn').innerText = "Fit: Contain";
1022
- } else {
1023
- img.classList.add('fit-cover'); // Switch to Cover
1024
- document.getElementById('fit-btn').innerText = "Fit: Cover";
1025
- }
1026
- saveDraft(true);
1027
- }
1028
-
1029
  function replacePanelImage() { if(!selectedPanel) return alert("Select a panel"); const inp = document.getElementById('image-uploader'); inp.onchange = async (e) => { const fd = new FormData(); fd.append('image', e.target.files[0]); const img = selectedPanel.querySelector('img'); const r = await fetch(`/replace_panel?sid=${sid}`, {method:'POST', body:fd}); const d = await r.json(); if(d.success) { img.src = `/frames/${d.new_filename}?sid=${sid}`; saveDraft(true); } inp.value = ''; }; inp.click(); }
1030
  async function adjustFrame(dir) { if(isProcessing) return; if(!selectedPanel) return alert("Select a panel"); 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(`/regenerate_frame?sid=${sid}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({filename:fname, direction:dir}) }); const d = await r.json(); if(d.success) { img.src = `/frames/${fname}?sid=${sid}&t=${Date.now()}`; } else { alert('Error: ' + d.message); } } catch(e) { console.error(e); } img.style.opacity = '1'; setProcessing(false); saveDraft(true); }
1031
  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); }
1032
 
1033
- async function exportComic() {
1034
- const pgs = document.querySelectorAll('.comic-page');
1035
- if(pgs.length === 0) return alert("No pages found");
1036
-
1037
- // Remove selection highlights
1038
- if(selectedBubble) selectedBubble.classList.remove('selected');
1039
- if(selectedPanel) selectedPanel.classList.remove('selected');
1040
- alert(`Exporting ${pgs.length} page(s)...`);
1041
-
1042
- // --- 0% ERROR FIX ---
1043
- // 1. Lock specific pixel dimensions + 1px buffer to prevent word wrapping
1044
- const bubbles = document.querySelectorAll('.speech-bubble');
1045
- bubbles.forEach(b => {
1046
- const rect = b.getBoundingClientRect();
1047
- // Add slight buffer (1px) to width to handle sub-pixel rendering differences
1048
- // This prevents "just fitting" words from wrapping in the export
1049
- b.style.width = (rect.width + 1) + 'px';
1050
- b.style.height = rect.height + 'px';
1051
- b.style.display = 'flex';
1052
- b.style.alignItems = 'center';
1053
- b.style.justifyContent = 'center';
1054
- });
1055
-
1056
- for(let i = 0; i < pgs.length; i++) {
1057
- try {
1058
- const u = await htmlToImage.toPng(pgs[i], {
1059
- pixelRatio: 2, // High quality
1060
- style: { transform: 'none' }
1061
- });
1062
- const a = document.createElement('a');
1063
- a.href = u;
1064
- a.download = `Comic-Page-${i+1}.png`;
1065
- a.click();
1066
- } catch(err) {
1067
- console.error(err);
1068
- alert(`Failed to export page ${i+1}`);
1069
- }
1070
- }
1071
  }
1072
-
 
 
1073
  function goBackToUpload() { if(confirm('Go home? Unsaved changes will be lost.')) { document.getElementById('editor-container').style.display = 'none'; document.getElementById('upload-container').style.display = 'flex'; document.getElementById('loading-view').style.display = 'none'; } }
1074
  </script>
1075
  </body> </html> '''
 
332
 
333
  .comic-page { background: white; width: 600px; height: 400px; box-shadow: 0 4px 10px rgba(0,0,0,0.1); position: relative; overflow: hidden; border: 2px solid #000; padding: 10px; }
334
 
335
+ /* === LAYOUTS === */
336
  .comic-grid { width: 100%; height: 100%; position: relative; background: #000; display:grid; gap: 10px; }
337
 
 
338
  .comic-grid.layout-rows { grid-template-columns: 1fr; grid-auto-rows: 1fr; }
 
 
339
  .comic-grid.layout-cols { grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); grid-template-rows: 1fr; }
 
 
340
  .comic-grid.layout-grid { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; }
341
 
342
+ /* === CUSTOM SLANT LAYOUT === */
343
+ .comic-grid.layout-custom-slant { display: block; position: relative; }
344
+ .comic-grid.layout-custom-slant .panel { position: absolute; width: 100%; border: none; background: transparent; }
345
+ /* Panel 1 (Top) */
346
+ .comic-grid.layout-custom-slant .panel:nth-child(1) { top: 0; height: 100%; z-index:2; clip-path: polygon(0 0, 100% 0, 100% var(--split-r, 55%), 0 var(--split-l, 45%)); }
347
+ /* Panel 2 (Bottom) */
348
+ .comic-grid.layout-custom-slant .panel:nth-child(2) { top: 0; height: 100%; z-index:1; clip-path: polygon(0 var(--split-l, 45%), 100% var(--split-r, 55%), 100% 100%, 0 100%); }
349
+ .comic-grid.layout-custom-slant .panel:nth-child(n+3) { display: none; }
350
+
351
+ /* Split Handles */
352
+ .split-handle {
353
+ position: absolute; width: 20px; height: 20px; background: #2196F3; border: 2px solid white; border-radius: 50%;
354
+ cursor: ns-resize; z-index: 1000; box-shadow: 0 2px 5px rgba(0,0,0,0.3); display: none;
355
+ }
356
+ .layout-custom-slant .split-handle { display: block; }
357
+ .split-handle.left { left: -10px; top: var(--split-l, 45%); }
358
+ .split-handle.right { right: -10px; top: var(--split-r, 55%); }
359
 
360
 
361
  .panel { overflow: hidden; background: #eee; cursor: pointer; border: 2px solid #000; position: relative; }
362
+ .layout-custom-slant .panel { border: none; }
363
  .panel.selected { z-index: 20; border-color: #2196F3; }
364
 
 
365
  .panel img {
366
  width: 100%; height: 100%;
367
+ object-fit: contain;
368
  transition: transform 0.1s ease-out;
369
  transform-origin: center center;
370
  pointer-events: auto;
 
395
  .speech-bubble.selected { outline: 2px dashed #4CAF50; z-index: 100; }
396
  .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; }
397
 
 
398
  .speech-bubble.speech {
399
  --b: 3em; --h: 1.8em; --t: 0.6; --p: var(--tail-pos, 50%); --r: 1.2em;
400
+ background: var(--bubble-fill-color, #4ECDC4); color: var(--bubble-text-color, #fff); padding: 0;
 
 
401
  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);
402
  }
403
  .speech-bubble.speech:before {
404
  content: ""; position: absolute; width: var(--b); height: var(--h);
405
  background: inherit; border-bottom-left-radius: 100%; pointer-events: none; z-index: 1;
406
+ -webkit-mask: radial-gradient(calc(var(--t)*100%) 105% at 100% 0,#0000 99%,#000 101%); mask: radial-gradient(calc(var(--t)*100%) 105% at 100% 0,#0000 99%,#000 101%);
 
407
  }
408
 
409
  .speech-bubble.speech.tail-bottom:before { top: 100%; left: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); }
 
413
  .speech-bubble.speech.tail-right { border-radius: var(--r); }
414
  .speech-bubble.speech.tail-right:before { left: 100%; top: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); transform: rotate(-90deg); transform-origin: top left; }
415
 
 
416
  .speech-bubble.thought { background: white; border: 2px dashed #555; color: #333; border-radius: 50%; }
417
  .speech-bubble.thought::before { display:none; }
418
  .thought-dot { position: absolute; background-color: white; border: 2px solid #555; border-radius: 50%; z-index: -1; }
 
495
  <option value="layout-rows">Rows (Horizontal)</option>
496
  <option value="layout-cols">Cols (Vertical)</option>
497
  <option value="layout-grid">Grid (2x2)</option>
498
+ <option value="layout-custom-slant">Custom Slant (Drag Points)</option>
499
  </select>
500
  </div>
501
 
 
586
  let currentSaveCode = null;
587
  let isProcessing = false;
588
  let interval, selectedBubble = null, selectedPanel = null;
589
+ let isDragging = false, isResizing = false, isPanning = false, isDraggingSplit = false;
590
  let startX, startY, initX, initY, panStartX, panStartY, panStartTx, panStartTy;
591
  let resizeHandle, originalWidth, originalHeight, originalMouseX, originalMouseY;
592
  let currentlyEditing = null;
593
+ let selectedSplitHandle = null;
594
 
595
  // UNDO SYSTEM
596
  let historyStack = [];
 
671
  const pages = [];
672
  document.querySelectorAll('.comic-page').forEach(p => {
673
  const grid = p.querySelector('.comic-grid');
 
674
  let layout = 'layout-rows';
675
  if(grid.classList.contains('layout-cols')) layout = 'layout-cols';
676
  if(grid.classList.contains('layout-grid')) layout = 'layout-grid';
677
+ if(grid.classList.contains('layout-custom-slant')) layout = 'layout-custom-slant';
678
 
679
+ // Layout Split Pos
680
+ const splitL = grid.style.getPropertyValue('--split-l') || '45%';
681
+ const splitR = grid.style.getPropertyValue('--split-r') || '55%';
682
+
683
  const panels = [];
684
  grid.querySelectorAll('.panel').forEach(pan => {
685
  const img = pan.querySelector('img');
 
703
  bubbles: bubbles
704
  });
705
  });
706
+ pages.push({ layout: layout, splitL: splitL, splitR: splitR, panels: panels });
707
  });
708
  return pages;
709
  }
 
723
  const div = document.createElement('div'); div.className = 'comic-page';
724
  const grid = document.createElement('div');
725
 
 
726
  grid.className = 'comic-grid ' + (page.layout || 'layout-rows');
727
+ if(page.layout === 'layout-custom-slant') {
728
+ grid.style.setProperty('--split-l', page.splitL || '45%');
729
+ grid.style.setProperty('--split-r', page.splitR || '55%');
730
+
731
+ // Add handles
732
+ const hL = document.createElement('div'); hL.className = 'split-handle left';
733
+ hL.onmousedown = (e) => startSplitDrag(e, hL, grid, 'left');
734
+ const hR = document.createElement('div'); hR.className = 'split-handle right';
735
+ hR.onmousedown = (e) => startSplitDrag(e, hR, grid, 'right');
736
+ grid.appendChild(hL); grid.appendChild(hR);
737
+ }
738
 
739
  page.panels.forEach((pan) => {
740
  const pDiv = document.createElement('div'); pDiv.className = 'panel';
 
746
  if(pan.fit === 'cover') img.classList.add('fit-cover');
747
 
748
  updateImageTransform(img);
749
+ img.onmousedown = (e) => { selectPanel(pDiv); startPan(e, img); };
 
 
 
 
 
750
  img.onwheel = (e) => handleWheelZoom(e, img);
751
 
752
  pDiv.appendChild(img);
 
754
  grid.appendChild(pDiv);
755
  });
756
  div.appendChild(grid); pageWrapper.appendChild(div); con.appendChild(pageWrapper);
 
 
757
  if(pageIdx === 0) document.getElementById('layout-select').value = (page.layout || 'layout-rows');
758
  });
759
+ selectedBubble = null; selectedPanel = null;
 
 
 
760
  }
761
 
762
+ // === SPLIT DRAGGING LOGIC ===
763
+ function startSplitDrag(e, handle, grid, side) {
764
+ e.preventDefault(); e.stopPropagation();
765
+ isDraggingSplit = true;
766
+ selectedSplitHandle = { handle, grid, side };
 
 
 
 
 
 
 
767
  }
768
 
769
+ document.addEventListener('mousemove', (e) => {
770
+ if(isDragging && selectedBubble) { selectedBubble.style.left = (initX + e.clientX - startX) + 'px'; selectedBubble.style.top = (initY + e.clientY - startY) + 'px'; }
771
+ if(isResizing && selectedBubble) { resizeBubble(e); }
772
+ if(isPanning && selectedPanel) { panImage(e); }
773
+
774
+ if(isDraggingSplit && selectedSplitHandle) {
775
+ const rect = selectedSplitHandle.grid.getBoundingClientRect();
776
+ let y = e.clientY - rect.top;
777
+ let percent = (y / rect.height) * 100;
778
+ if(percent < 10) percent = 10;
779
+ if(percent > 90) percent = 90;
780
+
781
+ if(selectedSplitHandle.side === 'left') {
782
+ selectedSplitHandle.grid.style.setProperty('--split-l', percent + '%');
783
+ } else {
784
+ selectedSplitHandle.grid.style.setProperty('--split-r', percent + '%');
785
+ }
786
+ }
787
+ });
788
 
789
+ document.addEventListener('mouseup', () => {
790
+ if(isDragging || isResizing || isPanning || isDraggingSplit) {
791
+ saveDraft(true);
792
+ }
793
+ isDragging = false; isResizing = false; isPanning = false; isDraggingSplit = false; selectedSplitHandle = null;
794
+ });
795
+
796
+ // ... (Existing Functions for Bubbles, Upload, etc. same as before) ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
797
 
798
  function createBubbleHTML(data) {
799
  const b = document.createElement('div');
800
  const type = data.type || 'speech';
801
  b.className = data.classes || `speech-bubble ${type} tail-bottom`;
802
  if (type === 'thought' && !b.className.includes('pos-')) b.className += ' pos-bl';
 
803
  b.dataset.type = type;
804
  b.style.left = data.left; b.style.top = data.top;
805
  if(data.transform) b.style.transform = data.transform;
 
806
  if(data.width) b.style.width = data.width; if(data.height) b.style.height = data.height;
807
  if(data.font) b.style.fontFamily = data.font;
808
  if(data.colors) { b.style.setProperty('--bubble-fill-color', data.colors.fill || '#4ECDC4'); b.style.setProperty('--bubble-text-color', data.colors.text || '#ffffff'); }
809
  if(data.tailPos) b.style.setProperty('--tail-pos', data.tailPos);
 
810
  const textSpan = document.createElement('span'); textSpan.className = 'bubble-text'; textSpan.textContent = data.text || ''; b.appendChild(textSpan);
 
811
  if(type === 'thought') { for(let i=1; i<=2; i++){ const d = document.createElement('div'); d.className = `thought-dot thought-dot-${i}`; b.appendChild(d); } }
 
812
  ['nw', 'ne', 'sw', 'se'].forEach(dir => { const handle = document.createElement('div'); handle.className = `resize-handle ${dir}`; handle.onmousedown = (e) => startResize(e, dir); b.appendChild(handle); });
 
813
  b.onmousedown = (e) => {
814
  if(e.target.classList.contains('resize-handle')) return;
815
  e.stopPropagation(); selectBubble(b);
816
+ isDragging = true; startX = e.clientX; startY = e.clientY; initX = b.offsetLeft; initY = b.offsetTop;
817
+ if(b.style.transform.includes('translate')) { b.style.transform = 'none'; b.style.left = (b.offsetLeft - b.offsetWidth/2) + 'px'; b.style.top = (b.offsetTop - b.offsetHeight/2) + 'px'; initX = parseInt(b.style.left); initY = parseInt(b.style.top); }
 
 
 
 
 
 
 
 
 
 
 
818
  };
819
  b.onclick = (e) => { e.stopPropagation(); };
820
  b.ondblclick = (e) => { e.stopPropagation(); editBubbleText(b); };
821
  return b;
822
  }
 
823
  function editBubbleText(bubble) {
824
  if (currentlyEditing) return; currentlyEditing = bubble;
825
  const textSpan = bubble.querySelector('.bubble-text'); const textarea = document.createElement('textarea');
826
  textarea.value = textSpan.textContent; bubble.appendChild(textarea); textSpan.style.display = 'none'; textarea.focus();
827
+ const finishEditing = () => { textSpan.textContent = textarea.value; textarea.remove(); textSpan.style.display = ''; currentlyEditing = null; saveDraft(true); };
 
 
 
828
  textarea.addEventListener('blur', finishEditing, { once: true });
829
  textarea.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); textarea.blur(); } });
830
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
831
  function startResize(e, dir) { e.preventDefault(); e.stopPropagation(); isResizing = true; resizeHandle = dir; const rect = selectedBubble.getBoundingClientRect(); originalWidth = rect.width; originalHeight = rect.height; originalMouseX = e.clientX; originalMouseY = e.clientY; }
832
  function resizeBubble(e) { if (!isResizing || !selectedBubble) return; const dx = e.clientX - originalMouseX; const dy = e.clientY - originalMouseY; if(resizeHandle.includes('e')) selectedBubble.style.width = (originalWidth + dx)+'px'; if(resizeHandle.includes('s')) selectedBubble.style.height = (originalHeight + dy)+'px'; }
 
833
  function selectBubble(el) {
834
+ if(selectedBubble) selectedBubble.classList.remove('selected'); if(selectedPanel) { selectedPanel.classList.remove('selected'); selectedPanel = null; }
 
835
  selectedBubble = el; el.classList.add('selected');
836
+ document.getElementById('bubble-type-select').disabled = false; document.getElementById('font-select').disabled = false; document.getElementById('bubble-text-color').disabled = false; document.getElementById('bubble-fill-color').disabled = false; document.getElementById('tail-controls').style.display = 'block'; document.getElementById('bubble-type-select').value = el.dataset.type || 'speech';
 
 
 
 
 
837
  }
 
838
  function selectPanel(el) {
839
+ if(selectedPanel) selectedPanel.classList.remove('selected'); if(selectedBubble) { selectedBubble.classList.remove('selected'); selectedBubble = null; }
 
840
  selectedPanel = el; el.classList.add('selected');
841
+ document.getElementById('zoom-slider').disabled = false; const img = el.querySelector('img'); document.getElementById('zoom-slider').value = img.dataset.zoom || 100; document.getElementById('bubble-type-select').disabled = true; document.getElementById('font-select').disabled = true; document.getElementById('tail-controls').style.display = 'none';
 
 
 
 
 
 
 
842
  document.getElementById('fit-btn').innerText = img.classList.contains('fit-cover') ? "Fit: Cover" : "Fit: Contain";
843
  }
844
+ function addBubble() { if(!selectedPanel) return alert("Select a panel first"); const b = createBubbleHTML({ text: "Text", left: "50%", top: "50%", type: 'speech', classes: "speech-bubble speech tail-bottom", transform: "translate(-50%, -50%)" }); selectedPanel.appendChild(b); selectBubble(b); saveDraft(true); }
845
+ function deleteBubble() { if(!selectedBubble) return alert("Select a bubble"); selectedBubble.remove(); selectedBubble=null; saveDraft(true); }
846
+ function changeBubbleType(type) { if(!selectedBubble) return; selectedBubble.dataset.type = type; selectedBubble.className = 'speech-bubble ' + type + ' selected'; if(type === 'thought') selectedBubble.classList.add('pos-bl'); else selectedBubble.classList.add('tail-bottom'); selectedBubble.querySelectorAll('.thought-dot').forEach(d=>d.remove()); if(type === 'thought') { for(let i=1; i<=2; i++){ const d = document.createElement('div'); d.className = `thought-dot thought-dot-${i}`; selectedBubble.appendChild(d); } } saveDraft(true); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
847
  function changeFont(font) { if(!selectedBubble) return; selectedBubble.style.fontFamily = font; saveDraft(true); }
848
+ function rotateTail() { if(!selectedBubble) return; const type = selectedBubble.dataset.type; if(type === 'speech') { const positions = ['tail-bottom', 'tail-right', 'tail-top', 'tail-left']; let current = 0; positions.forEach((pos, i) => { if(selectedBubble.classList.contains(pos)) current = i; }); selectedBubble.classList.remove(positions[current]); selectedBubble.classList.add(positions[(current + 1) % 4]); } else if (type === 'thought') { const positions = ['pos-bl', 'pos-br', 'pos-tr', 'pos-tl']; let current = 0; positions.forEach((pos, i) => { if(selectedBubble.classList.contains(pos)) current = i; }); selectedBubble.classList.remove(positions[current]); selectedBubble.classList.add(positions[(current + 1) % 4]); } saveDraft(true); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
849
  function slideTail(v) { if(selectedBubble) { selectedBubble.style.setProperty('--tail-pos', v+'%'); saveDraft(true); } }
 
850
  document.getElementById('bubble-text-color').addEventListener('change', (e) => { if(selectedBubble) { selectedBubble.style.setProperty('--bubble-text-color', e.target.value); saveDraft(true); } });
851
  document.getElementById('bubble-fill-color').addEventListener('change', (e) => { if(selectedBubble) { selectedBubble.style.setProperty('--bubble-fill-color', e.target.value); saveDraft(true); } });
 
852
  function handleZoom(el) { if(!selectedPanel) return; const img = selectedPanel.querySelector('img'); img.dataset.zoom = el.value; updateImageTransform(img); }
853
  document.getElementById('zoom-slider').addEventListener('change', () => saveDraft(true));
854
+ function handleWheelZoom(e, img) { e.preventDefault(); e.stopPropagation(); if(!selectedPanel) selectPanel(img.closest('.panel')); let scale = parseFloat(img.dataset.zoom) || 100; const delta = -Math.sign(e.deltaY) * 10; scale += delta; if(scale < 10) scale = 10; if(scale > 500) scale = 500; img.dataset.zoom = scale; document.getElementById('zoom-slider').value = scale; updateImageTransform(img); }
855
+ function startPan(e, img) { e.preventDefault(); isPanning = true; selectedPanel = img.closest('.panel'); panStartX = e.clientX; panStartY = e.clientY; panStartTx = parseFloat(img.dataset.translateX) || 0; panStartTy = parseFloat(img.dataset.translateY) || 0; img.classList.add('panning'); }
856
+ function panImage(e) { if(!isPanning || !selectedPanel) return; const img = selectedPanel.querySelector('img'); const dx = e.clientX - panStartX; const dy = e.clientY - panStartY; img.dataset.translateX = panStartTx + dx; img.dataset.translateY = panStartTy + dy; updateImageTransform(img); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
857
  function updateImageTransform(img) { const z = (img.dataset.zoom || 100) / 100; const x = img.dataset.translateX || 0; const y = img.dataset.translateY || 0; img.style.transform = `translateX(${x}px) translateY(${y}px) scale(${z})`; img.classList.toggle('pannable', true); }
858
  function resetPanelTransform() { if(!selectedPanel) return alert("Select a panel"); const img = selectedPanel.querySelector('img'); img.dataset.zoom = 100; img.dataset.translateX = 0; img.dataset.translateY = 0; document.getElementById('zoom-slider').value = 100; updateImageTransform(img); saveDraft(true); }
859
+ function changeLayout(newLayout) { document.querySelectorAll('.comic-grid').forEach(g => { g.className = 'comic-grid ' + newLayout; if(newLayout === 'layout-custom-slant' && !g.querySelector('.split-handle')) { const hL = document.createElement('div'); hL.className = 'split-handle left'; hL.onmousedown = (e) => startSplitDrag(e, hL, g, 'left'); const hR = document.createElement('div'); hR.className = 'split-handle right'; hR.onmousedown = (e) => startSplitDrag(e, hR, g, 'right'); g.appendChild(hL); g.appendChild(hR); } }); saveDraft(true); }
860
+ function toggleFitMode() { if(!selectedPanel) return alert("Select a panel"); const img = selectedPanel.querySelector('img'); if(img.classList.contains('fit-cover')) { img.classList.remove('fit-cover'); document.getElementById('fit-btn').innerText = "Fit: Contain"; } else { img.classList.add('fit-cover'); document.getElementById('fit-btn').innerText = "Fit: Cover"; } saveDraft(true); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
861
  function replacePanelImage() { if(!selectedPanel) return alert("Select a panel"); const inp = document.getElementById('image-uploader'); inp.onchange = async (e) => { const fd = new FormData(); fd.append('image', e.target.files[0]); const img = selectedPanel.querySelector('img'); const r = await fetch(`/replace_panel?sid=${sid}`, {method:'POST', body:fd}); const d = await r.json(); if(d.success) { img.src = `/frames/${d.new_filename}?sid=${sid}`; saveDraft(true); } inp.value = ''; }; inp.click(); }
862
  async function adjustFrame(dir) { if(isProcessing) return; if(!selectedPanel) return alert("Select a panel"); 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(`/regenerate_frame?sid=${sid}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({filename:fname, direction:dir}) }); const d = await r.json(); if(d.success) { img.src = `/frames/${fname}?sid=${sid}&t=${Date.now()}`; } else { alert('Error: ' + d.message); } } catch(e) { console.error(e); } img.style.opacity = '1'; setProcessing(false); saveDraft(true); }
863
  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); }
864
 
865
+ async function upload() {
866
+ const f = document.getElementById('file-upload').files[0];
867
+ const pCount = document.getElementById('page-count').value;
868
+ const panelCount = document.getElementById('panels-per-page').value;
869
+ if(!f) return alert("Select a video");
870
+ sid = genUUID(); localStorage.setItem('comic_sid', sid); currentSaveCode = null;
871
+ document.querySelector('.upload-box').style.display='none';
872
+ document.getElementById('loading-view').style.display='flex';
873
+ const fd = new FormData(); fd.append('file', f); fd.append('target_pages', pCount); fd.append('panels_per_page', panelCount);
874
+ const r = await fetch(`/uploader?sid=${sid}`, {method:'POST', body:fd});
875
+ if(r.ok) interval = setInterval(checkStatus, 2000);
876
+ else { alert("Upload failed"); location.reload(); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
877
  }
878
+ 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) {} }
879
+ function loadNewComic() { fetch(`/output/pages.json?sid=${sid}`).then(r=>r.json()).then(data => { const cleanData = data.map((p, pi) => ({ layout: 'layout-rows', panels: p.panels.map((pan, j) => ({ src: `/frames/${pan.image}?sid=${sid}`, fit: 'contain', bubbles: (p.bubbles && p.bubbles[j] && p.bubbles[j].dialog) ? [{ text: p.bubbles[j].dialog, left: (p.bubbles[j].bubble_offset_x > 0 ? p.bubbles[j].bubble_offset_x + 'px' : '50%'), top: (p.bubbles[j].bubble_offset_y > 0 ? p.bubbles[j].bubble_offset_y + 'px' : '50%'), type: (p.bubbles[j].type || 'speech'), classes: `speech-bubble ${p.bubbles[j].type || 'speech'} tail-bottom`, transform: (p.bubbles[j].bubble_offset_x > 0 ? 'none' : 'translate(-50%, -50%)') }] : [] })) })); renderFromState(cleanData); saveDraft(true); }); }
880
+ 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'); document.querySelectorAll('.split-handle').forEach(h => h.style.display = 'none'); alert(`Exporting ${pgs.length} page(s)...`); const bubbles = document.querySelectorAll('.speech-bubble'); bubbles.forEach(b => { const rect = b.getBoundingClientRect(); b.style.width = (rect.width + 1) + 'px'; b.style.height = rect.height + 'px'; b.style.display = 'flex'; b.style.alignItems = 'center'; b.style.justifyContent = 'center'; }); 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}`); } } document.querySelectorAll('.split-handle').forEach(h => h.style.display = ''); }
881
  function goBackToUpload() { if(confirm('Go home? Unsaved changes will be lost.')) { document.getElementById('editor-container').style.display = 'none'; document.getElementById('upload-container').style.display = 'flex'; document.getElementById('loading-view').style.display = 'none'; } }
882
  </script>
883
  </body> </html> '''