jhh6576 commited on
Commit
e0f2d51
·
verified ·
1 Parent(s): 3803d92

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. app_enhanced.py +125 -88
app_enhanced.py CHANGED
@@ -509,13 +509,13 @@ class EnhancedComicGenerator:
509
  .panel img { width: 100%; height: 100%; object-fit: cover; object-position: center; transition: transform 0.1s ease-out; }
510
  .panel img.pannable { cursor: grab; }
511
  .panel img.panning { cursor: grabbing; }
512
- .speech-bubble { font-family: 'Comic Neue', cursive; position: absolute; display: flex; justify-content: center; align-items: center; width: 150px; height: 80px; min-width: 50px; min-height: 30px; box-sizing: border-box; padding: 8px; box-shadow: 2px 2px 5px rgba(0,0,0,0.3); z-index: 10; cursor: move; overflow: visible; font-size: 14pt; font-weight: bold; text-align: center; }
513
  .bubble-text { padding: 2px; word-wrap: break-word; }
514
  .speech-bubble.selected { outline: 2px dashed #4CAF50; }
515
  .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); font: inherit; text-align: center; resize: none; padding: 8px; z-index: 102; }
516
  .speech-bubble.speech { background: white; border: 2px solid #333; color: #333; border-radius: 15px; }
517
  .speech-bubble.thought { background: white; border: 2px dashed #555; color: #333; border-radius: 50%; }
518
- .speech-bubble.reaction { background: #FFD700; border: 3px solid #E53935; color: #D32F2F; font-weight: 900; text-transform: uppercase; width: 180px; height: 180px; clip-path: polygon(0% 25%, 17% 21%, 17% 0%, 31% 16%, 50% 4%, 69% 16%, 83% 0%, 83% 21%, 100% 25%, 85% 45%, 95% 62%, 82% 79%, 100% 97%, 79% 89%, 60% 98%, 46% 82%, 27% 95%, 15% 78%, 5% 62%, 15% 45%); }
519
  .speech-bubble.narration { background: #FAFAFA; border: 2px solid #BDBDBD; color: #424242; border-radius: 3px; }
520
  .speech-bubble.idea { background: linear-gradient(180deg,#FFFDD0 0%, #FFF8B5 100%); border: 2px solid #FFA500; color: #6a4b00; border-radius: 40% 60% 40% 60% / 60% 40% 60% 40%; }
521
  .speech-bubble.speech::after, .speech-bubble.idea::after { content: ''; position: absolute; width: 0; height: 0; border-left: 10px solid transparent; border-right: 10px solid transparent; }
@@ -552,8 +552,6 @@ class EnhancedComicGenerator:
552
  .color-picker-grid div { text-align: center; }
553
  .color-picker-grid label { font-size: 11px; }
554
  .color-picker-grid input[type="color"] { height: 25px; padding: 2px; }
555
- .font-size-control { display: grid; grid-template-columns: 1fr auto; gap: 5px; align-items: center; }
556
- #font-size-label { font-size: 11px; color: white; text-align: right; }
557
  </style>
558
  </head>
559
  <body>
@@ -575,10 +573,6 @@ class EnhancedComicGenerator:
575
  <option value="'Gloria Hallelujah', cursive">Gloria</option>
576
  <option value="'Lato', sans-serif">Lato</option>
577
  </select>
578
- <div class="font-size-control">
579
- <input type="range" id="font-size-slider" min="8" max="24" value="14" step="1" disabled>
580
- <span id="font-size-label">14pt</span>
581
- </div>
582
  <div class="color-picker-grid">
583
  <div>
584
  <label for="bubble-text-color">Text</label>
@@ -629,7 +623,8 @@ class EnhancedComicGenerator:
629
  let currentlySelectedBubble = null;
630
  let currentlySelectedPanel = null;
631
  let isPanning = false, panStartX, panStartY, panStartTranslateX, panStartTranslateY;
632
- let isResizing = false, resizeHandle, originalWidth, originalHeight, originalX, originalY, originalMouseX, originalMouseY;
 
633
 
634
  function renderComic(data) {
635
  const container = document.getElementById('comic-pages');
@@ -651,12 +646,14 @@ class EnhancedComicGenerator:
651
  const img = document.createElement('img');
652
  img.src = '/frames/final/' + panelData.image;
653
  panelDiv.appendChild(img);
654
- if (pageData.bubbles && pageData.bubbles[panelIndex] && pageData.bubbles[panelIndex].dialog) {
 
 
655
  const bubbleDiv = createBubbleElement({
656
  id: `initial-${pageIndex}-${panelIndex}`,
657
- text: pageData.bubbles[panelIndex].dialog || '',
658
- left: `${pageData.bubbles[panelIndex].bubble_offset_x ?? 50}px`,
659
- top: `${pageData.bubbles[panelIndex].bubble_offset_y ?? 20}px`,
660
  });
661
  panelDiv.appendChild(bubbleDiv);
662
  }
@@ -682,12 +679,6 @@ class EnhancedComicGenerator:
682
  document.getElementById('bubble-fill-color').addEventListener('input', (e) => {
683
  if(currentlySelectedBubble) currentlySelectedBubble.style.backgroundColor = e.target.value;
684
  });
685
- document.getElementById('font-size-slider').addEventListener('input', (e) => {
686
- if(currentlySelectedBubble) {
687
- currentlySelectedBubble.style.fontSize = e.target.value + 'pt';
688
- document.getElementById('font-size-label').textContent = e.target.value + 'pt';
689
- }
690
- });
691
 
692
  document.addEventListener('mousemove', e => { if (isPanning) panImage(e); if (draggedBubble) drag(e); if(isResizing) resizeBubble(e); });
693
  document.addEventListener('mouseup', e => { if (isPanning) stopPan(e); if (draggedBubble) stopDrag(e); if(isResizing) stopResize(e);});
@@ -732,7 +723,21 @@ class EnhancedComicGenerator:
732
  }
733
 
734
  function applyBubbleType(bubble, type) {
735
- // function content...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
736
  }
737
 
738
  function changeBubbleType(type) {
@@ -746,7 +751,13 @@ class EnhancedComicGenerator:
746
  }
747
 
748
  function rotateBubbleTail() {
749
- // function content...
 
 
 
 
 
 
750
  }
751
 
752
  function selectPanel(panel) {
@@ -754,7 +765,6 @@ class EnhancedComicGenerator:
754
  panel.classList.add('selected');
755
  currentlySelectedPanel = panel;
756
  selectBubble(null);
757
-
758
  const img = currentlySelectedPanel.querySelector('img');
759
  document.getElementById('zoom-slider').value = img.dataset.zoom || 100;
760
  document.getElementById('zoom-slider').disabled = false;
@@ -763,23 +773,16 @@ class EnhancedComicGenerator:
763
  function selectBubble(bubble) {
764
  if (currentlySelectedBubble) currentlySelectedBubble.classList.remove('selected');
765
  currentlySelectedBubble = bubble;
766
- const bubbleControls = ['bubble-text-color', 'bubble-fill-color', 'bubble-type-select', 'font-select', 'font-size-slider'];
767
-
768
  if (currentlySelectedBubble) {
769
  currentlySelectedBubble.classList.add('selected');
770
  if (currentlySelectedPanel) currentlySelectedPanel.classList.remove('selected');
771
  currentlySelectedPanel = null;
772
-
773
  const styles = window.getComputedStyle(currentlySelectedBubble);
774
  document.getElementById('bubble-text-color').value = rgbToHex(styles.color);
775
  document.getElementById('bubble-fill-color').value = rgbToHex(styles.backgroundColor);
776
  document.getElementById('bubble-type-select').value = currentlySelectedBubble.dataset.type || 'speech';
777
  document.getElementById('font-select').value = styles.fontFamily.split(',')[0].replace(/"/g, "").replace(/'/g, "").trim();
778
- const fontSize = parseInt(styles.fontSize, 10);
779
- document.getElementById('font-size-slider').value = fontSize;
780
- document.getElementById('font-size-label').textContent = fontSize + 'pt';
781
-
782
-
783
  document.getElementById('zoom-slider').disabled = true;
784
  bubbleControls.forEach(id => document.getElementById(id).disabled = false);
785
  } else {
@@ -792,9 +795,8 @@ class EnhancedComicGenerator:
792
  currentlyEditing = bubble;
793
  const textSpan = bubble.querySelector('.bubble-text');
794
  const textarea = document.createElement('textarea');
795
- const originalHeight = bubble.style.height || `${bubble.offsetHeight}px`;
796
- bubble.style.height = originalHeight;
797
-
798
  textarea.value = textSpan.textContent;
799
  bubble.appendChild(textarea);
800
  textSpan.style.display = 'none';
@@ -803,20 +805,27 @@ class EnhancedComicGenerator:
803
  textSpan.textContent = textarea.value;
804
  bubble.removeChild(textarea);
805
  textSpan.style.display = '';
806
- bubble.style.height = originalHeight;
807
- setTimeout(() => { bubble.style.height = 'auto'; }, 0);
808
  currentlyEditing = null;
 
809
  };
810
  textarea.addEventListener('blur', finishEditing, { once: true });
811
  textarea.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); textarea.blur(); }});
812
  }
813
 
814
  function startDrag(e) {
815
- // function content...
 
 
 
 
 
816
  }
817
 
818
  function drag(e) {
819
- // function content...
 
 
 
820
  }
821
 
822
  function stopDrag() { draggedBubble = null; }
@@ -854,28 +863,13 @@ class EnhancedComicGenerator:
854
  const dy = e.clientY - originalMouseY;
855
  const bubble = currentlySelectedBubble;
856
 
857
- if (bubble.classList.contains('reaction')) {
858
- const scaleX = (originalWidth + dx) / originalWidth;
859
- const scaleY = (originalHeight + dy) / originalHeight;
860
- bubble.style.transform = `scale(${Math.max(scaleX, scaleY)})`;
861
- return;
862
- }
863
-
864
- if (resizeHandle.includes('e')) bubble.style.width = `${originalWidth + dx}px`;
865
- if (resizeHandle.includes('w')) {
866
- bubble.style.width = `${originalWidth - dx}px`;
867
- bubble.style.left = `${originalX + dx}px`;
868
- }
869
- if (resizeHandle.includes('s')) bubble.style.height = `${originalHeight + dy}px`;
870
- if (resizeHandle.includes('n')) {
871
- bubble.style.height = `${originalHeight - dy}px`;
872
- bubble.style.top = `${originalY + dy}px`;
873
- }
874
  }
875
 
876
- function stopResize() {
877
- isResizing = false;
878
- }
879
 
880
  function clearSavedState() {
881
  if (confirm("Reset all edits?")) {
@@ -918,31 +912,33 @@ class EnhancedComicGenerator:
918
  }
919
 
920
  function adjustFrame(direction) {
921
- // function content...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
922
  }
923
 
924
- function updateImageTransform(img, translateX, translateY) {
925
  const zoom = (img.dataset.zoom || 100) / 100;
926
-
927
- const panel = img.parentElement;
928
- const panelWidth = panel.clientWidth;
929
- const panelHeight = panel.clientHeight;
930
- const imgWidth = img.naturalWidth * (img.clientHeight / img.naturalHeight);
931
- const imgHeight = img.clientHeight;
932
-
933
- const zoomedWidth = imgWidth * zoom;
934
- const zoomedHeight = imgHeight * zoom;
935
-
936
- const maxTranslateX = Math.max(0, (zoomedWidth - panelWidth) / 2);
937
- const maxTranslateY = Math.max(0, (zoomedHeight - panelHeight) / 2);
938
-
939
- const clampedX = Math.max(-maxTranslateX, Math.min(maxTranslateX, translateX));
940
- const clampedY = Math.max(-maxTranslateY, Math.min(maxTranslateY, translateY));
941
-
942
- img.dataset.translateX = clampedX;
943
- img.dataset.translateY = clampedY;
944
-
945
- img.style.transform = `translateX(${clampedX}px) translateY(${clampedY}px) scale(${zoom})`;
946
  img.classList.toggle('pannable', zoom > 1);
947
  }
948
 
@@ -950,7 +946,7 @@ class EnhancedComicGenerator:
950
  if (!currentlySelectedPanel) return;
951
  const img = currentlySelectedPanel.querySelector('img');
952
  img.dataset.zoom = event.target.value;
953
- updateImageTransform(img, parseFloat(img.dataset.translateX || 0), parseFloat(img.dataset.translateY || 0));
954
  }
955
 
956
  function resetPanelTransform() {
@@ -960,14 +956,13 @@ class EnhancedComicGenerator:
960
  img.dataset.translateX = 0;
961
  img.dataset.translateY = 0;
962
  document.getElementById('zoom-slider').value = 100;
963
- updateImageTransform(img, 0, 0);
964
  }
965
 
966
  function startPan(event) {
967
  if (event.button !== 0) return;
968
  const img = event.target;
969
- const zoom = parseFloat(img.dataset.zoom || 100);
970
- if (zoom <= 100) return;
971
  event.preventDefault();
972
  isPanning = true;
973
  img.classList.add('panning');
@@ -980,9 +975,9 @@ class EnhancedComicGenerator:
980
  function panImage(event) {
981
  if (!isPanning || !currentlySelectedPanel) return;
982
  const img = currentlySelectedPanel.querySelector('img');
983
- const newTranslateX = panStartTranslateX + (event.clientX - panStartX);
984
- const newTranslateY = panStartTranslateY + (event.clientY - panStartY);
985
- updateImageTransform(img, newTranslateX, newTranslateY);
986
  }
987
 
988
  function stopPan() {
@@ -992,11 +987,53 @@ class EnhancedComicGenerator:
992
  }
993
 
994
  function addBubbleToPanel() {
995
- // function content...
 
 
 
 
 
 
 
 
 
 
996
  }
997
 
998
  function gotoTimestamp() {
999
- // function content...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1000
  }
1001
  </script>
1002
  </body>
 
509
  .panel img { width: 100%; height: 100%; object-fit: cover; object-position: center; transition: transform 0.1s ease-out; }
510
  .panel img.pannable { cursor: grab; }
511
  .panel img.panning { cursor: grabbing; }
512
+ .speech-bubble { font-family: 'Comic Neue', cursive; position: absolute; display: flex; justify-content: center; align-items: center; width: 150px; height: 80px; min-width: 50px; max-width: 220px; min-height: 30px; box-sizing: border-box; padding: 8px; box-shadow: 2px 2px 5px rgba(0,0,0,0.3); z-index: 10; cursor: move; overflow: visible; font-size: 13px; font-weight: bold; text-align: center; }
513
  .bubble-text { padding: 2px; word-wrap: break-word; }
514
  .speech-bubble.selected { outline: 2px dashed #4CAF50; }
515
  .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); font: inherit; text-align: center; resize: none; padding: 8px; z-index: 102; }
516
  .speech-bubble.speech { background: white; border: 2px solid #333; color: #333; border-radius: 15px; }
517
  .speech-bubble.thought { background: white; border: 2px dashed #555; color: #333; border-radius: 50%; }
518
+ .speech-bubble.reaction { background: #FFD700; border: 3px solid #E53935; color: #D32F2F; font-weight: 900; text-transform: uppercase; width: 180px; clip-path: polygon(0% 25%, 17% 21%, 17% 0%, 31% 16%, 50% 4%, 69% 16%, 83% 0%, 83% 21%, 100% 25%, 85% 45%, 95% 62%, 82% 79%, 100% 97%, 79% 89%, 60% 98%, 46% 82%, 27% 95%, 15% 78%, 5% 62%, 15% 45%); }
519
  .speech-bubble.narration { background: #FAFAFA; border: 2px solid #BDBDBD; color: #424242; border-radius: 3px; }
520
  .speech-bubble.idea { background: linear-gradient(180deg,#FFFDD0 0%, #FFF8B5 100%); border: 2px solid #FFA500; color: #6a4b00; border-radius: 40% 60% 40% 60% / 60% 40% 60% 40%; }
521
  .speech-bubble.speech::after, .speech-bubble.idea::after { content: ''; position: absolute; width: 0; height: 0; border-left: 10px solid transparent; border-right: 10px solid transparent; }
 
552
  .color-picker-grid div { text-align: center; }
553
  .color-picker-grid label { font-size: 11px; }
554
  .color-picker-grid input[type="color"] { height: 25px; padding: 2px; }
 
 
555
  </style>
556
  </head>
557
  <body>
 
573
  <option value="'Gloria Hallelujah', cursive">Gloria</option>
574
  <option value="'Lato', sans-serif">Lato</option>
575
  </select>
 
 
 
 
576
  <div class="color-picker-grid">
577
  <div>
578
  <label for="bubble-text-color">Text</label>
 
623
  let currentlySelectedBubble = null;
624
  let currentlySelectedPanel = null;
625
  let isPanning = false, panStartX, panStartY, panStartTranslateX, panStartTranslateY;
626
+ let isResizing = false;
627
+ let resizeHandle, originalWidth, originalHeight, originalX, originalY, originalMouseX, originalMouseY;
628
 
629
  function renderComic(data) {
630
  const container = document.getElementById('comic-pages');
 
646
  const img = document.createElement('img');
647
  img.src = '/frames/final/' + panelData.image;
648
  panelDiv.appendChild(img);
649
+ // *** FIX: Corrected logic to show all bubbles ***
650
+ if (pageData.bubbles && pageData.bubbles[panelIndex]) {
651
+ const bubbleData = pageData.bubbles[panelIndex];
652
  const bubbleDiv = createBubbleElement({
653
  id: `initial-${pageIndex}-${panelIndex}`,
654
+ text: bubbleData.dialog || '',
655
+ left: `${bubbleData.bubble_offset_x ?? 50}px`,
656
+ top: `${bubbleData.bubble_offset_y ?? 20}px`,
657
  });
658
  panelDiv.appendChild(bubbleDiv);
659
  }
 
679
  document.getElementById('bubble-fill-color').addEventListener('input', (e) => {
680
  if(currentlySelectedBubble) currentlySelectedBubble.style.backgroundColor = e.target.value;
681
  });
 
 
 
 
 
 
682
 
683
  document.addEventListener('mousemove', e => { if (isPanning) panImage(e); if (draggedBubble) drag(e); if(isResizing) resizeBubble(e); });
684
  document.addEventListener('mouseup', e => { if (isPanning) stopPan(e); if (draggedBubble) stopDrag(e); if(isResizing) stopResize(e);});
 
723
  }
724
 
725
  function applyBubbleType(bubble, type) {
726
+ bubble.querySelectorAll('.thought-dot').forEach(el => el.remove());
727
+ let classesToKeep = 'speech-bubble';
728
+ if (bubble.classList.contains('selected')) classesToKeep += ' selected';
729
+ if (bubble.classList.contains('flipped')) classesToKeep += ' flipped';
730
+ if (bubble.classList.contains('flipped-vertical')) classesToKeep += ' flipped-vertical';
731
+ bubble.className = classesToKeep;
732
+ bubble.classList.add(type);
733
+ bubble.dataset.type = type;
734
+ if (type === 'thought') {
735
+ for (let i = 1; i <= 2; i++) {
736
+ const dot = document.createElement('div');
737
+ dot.className = `thought-dot thought-dot-${i}`;
738
+ bubble.appendChild(dot);
739
+ }
740
+ }
741
  }
742
 
743
  function changeBubbleType(type) {
 
751
  }
752
 
753
  function rotateBubbleTail() {
754
+ if (!currentlySelectedBubble) { alert("Please select a bubble first."); return; }
755
+ const isFlippedH = currentlySelectedBubble.classList.contains('flipped');
756
+ const isFlippedV = currentlySelectedBubble.classList.contains('flipped-vertical');
757
+ if (!isFlippedH && !isFlippedV) { currentlySelectedBubble.classList.add('flipped'); }
758
+ else if (isFlippedH && !isFlippedV) { currentlySelectedBubble.classList.add('flipped-vertical'); }
759
+ else if (isFlippedH && isFlippedV) { currentlySelectedBubble.classList.remove('flipped'); }
760
+ else { currentlySelectedBubble.classList.remove('flipped-vertical'); }
761
  }
762
 
763
  function selectPanel(panel) {
 
765
  panel.classList.add('selected');
766
  currentlySelectedPanel = panel;
767
  selectBubble(null);
 
768
  const img = currentlySelectedPanel.querySelector('img');
769
  document.getElementById('zoom-slider').value = img.dataset.zoom || 100;
770
  document.getElementById('zoom-slider').disabled = false;
 
773
  function selectBubble(bubble) {
774
  if (currentlySelectedBubble) currentlySelectedBubble.classList.remove('selected');
775
  currentlySelectedBubble = bubble;
776
+ const bubbleControls = ['bubble-text-color', 'bubble-fill-color', 'bubble-type-select', 'font-select'];
 
777
  if (currentlySelectedBubble) {
778
  currentlySelectedBubble.classList.add('selected');
779
  if (currentlySelectedPanel) currentlySelectedPanel.classList.remove('selected');
780
  currentlySelectedPanel = null;
 
781
  const styles = window.getComputedStyle(currentlySelectedBubble);
782
  document.getElementById('bubble-text-color').value = rgbToHex(styles.color);
783
  document.getElementById('bubble-fill-color').value = rgbToHex(styles.backgroundColor);
784
  document.getElementById('bubble-type-select').value = currentlySelectedBubble.dataset.type || 'speech';
785
  document.getElementById('font-select').value = styles.fontFamily.split(',')[0].replace(/"/g, "").replace(/'/g, "").trim();
 
 
 
 
 
786
  document.getElementById('zoom-slider').disabled = true;
787
  bubbleControls.forEach(id => document.getElementById(id).disabled = false);
788
  } else {
 
795
  currentlyEditing = bubble;
796
  const textSpan = bubble.querySelector('.bubble-text');
797
  const textarea = document.createElement('textarea');
798
+ const originalHeight = bubble.offsetHeight;
799
+ bubble.style.height = `${originalHeight}px`;
 
800
  textarea.value = textSpan.textContent;
801
  bubble.appendChild(textarea);
802
  textSpan.style.display = 'none';
 
805
  textSpan.textContent = textarea.value;
806
  bubble.removeChild(textarea);
807
  textSpan.style.display = '';
 
 
808
  currentlyEditing = null;
809
+ bubble.style.height = 'auto'; // Let it reflow naturally
810
  };
811
  textarea.addEventListener('blur', finishEditing, { once: true });
812
  textarea.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); textarea.blur(); }});
813
  }
814
 
815
  function startDrag(e) {
816
+ const bubble = e.target.closest('.speech-bubble');
817
+ if (!bubble || currentlyEditing) return;
818
+ draggedBubble = bubble;
819
+ selectBubble(bubble);
820
+ const rect = bubble.getBoundingClientRect();
821
+ offset = { x: e.clientX - rect.left, y: e.clientY - rect.top };
822
  }
823
 
824
  function drag(e) {
825
+ if (!draggedBubble) return;
826
+ const parentRect = draggedBubble.parentElement.getBoundingClientRect();
827
+ draggedBubble.style.left = `${e.clientX - parentRect.left - offset.x}px`;
828
+ draggedBubble.style.top = `${e.clientY - parentRect.top - offset.y}px`;
829
  }
830
 
831
  function stopDrag() { draggedBubble = null; }
 
863
  const dy = e.clientY - originalMouseY;
864
  const bubble = currentlySelectedBubble;
865
 
866
+ if (resizeHandle.includes('e')) { bubble.style.width = `${originalWidth + dx}px`; }
867
+ if (resizeHandle.includes('w')) { bubble.style.width = `${originalWidth - dx}px`; bubble.style.left = `${originalX + dx}px`; }
868
+ if (resizeHandle.includes('s')) { bubble.style.height = `${originalHeight + dy}px`; }
869
+ if (resizeHandle.includes('n')) { bubble.style.height = `${originalHeight - dy}px`; bubble.style.top = `${originalY + dy}px`; }
 
 
 
 
 
 
 
 
 
 
 
 
 
870
  }
871
 
872
+ function stopResize() { isResizing = false; }
 
 
873
 
874
  function clearSavedState() {
875
  if (confirm("Reset all edits?")) {
 
912
  }
913
 
914
  function adjustFrame(direction) {
915
+ if (!currentlySelectedPanel) { alert("Please select a panel first."); return; }
916
+ const img = currentlySelectedPanel.querySelector('img');
917
+ let filename = img.src.substring(img.src.lastIndexOf('/') + 1).split('?')[0];
918
+ img.style.opacity = '0.5';
919
+ fetch('/regenerate_frame', {
920
+ method: 'POST',
921
+ headers: { 'Content-Type': 'application/json' },
922
+ body: JSON.stringify({ filename, direction })
923
+ })
924
+ .then(res => res.json())
925
+ .then(data => {
926
+ if (data.success) {
927
+ img.src = `/frames/final/${filename}?t=${new Date().getTime()}`;
928
+ } else { alert('Error: ' + data.message); }
929
+ img.style.opacity = '1';
930
+ })
931
+ .catch(() => {
932
+ alert('An error occurred.');
933
+ img.style.opacity = '1';
934
+ });
935
  }
936
 
937
+ function updateImageTransform(img) {
938
  const zoom = (img.dataset.zoom || 100) / 100;
939
+ const x = img.dataset.translateX || 0;
940
+ const y = img.dataset.translateY || 0;
941
+ img.style.transform = `translateX(${x}px) translateY(${y}px) scale(${zoom})`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
942
  img.classList.toggle('pannable', zoom > 1);
943
  }
944
 
 
946
  if (!currentlySelectedPanel) return;
947
  const img = currentlySelectedPanel.querySelector('img');
948
  img.dataset.zoom = event.target.value;
949
+ updateImageTransform(img);
950
  }
951
 
952
  function resetPanelTransform() {
 
956
  img.dataset.translateX = 0;
957
  img.dataset.translateY = 0;
958
  document.getElementById('zoom-slider').value = 100;
959
+ updateImageTransform(img);
960
  }
961
 
962
  function startPan(event) {
963
  if (event.button !== 0) return;
964
  const img = event.target;
965
+ if (parseFloat(img.dataset.zoom || 100) <= 100) return;
 
966
  event.preventDefault();
967
  isPanning = true;
968
  img.classList.add('panning');
 
975
  function panImage(event) {
976
  if (!isPanning || !currentlySelectedPanel) return;
977
  const img = currentlySelectedPanel.querySelector('img');
978
+ img.dataset.translateX = panStartTranslateX + (event.clientX - panStartX);
979
+ img.dataset.translateY = panStartTranslateY + (event.clientY - panStartY);
980
+ updateImageTransform(img);
981
  }
982
 
983
  function stopPan() {
 
987
  }
988
 
989
  function addBubbleToPanel() {
990
+ if (!currentlySelectedPanel) { alert("Please select a panel first."); return; }
991
+ const newBubble = createBubbleElement({
992
+ id: `new-bubble-${Date.now()}`,
993
+ text: 'New Text...',
994
+ left: '10%',
995
+ top: '10%'
996
+ });
997
+ currentlySelectedPanel.appendChild(newBubble);
998
+ initializeBubbleEvents(newBubble);
999
+ selectBubble(newBubble);
1000
+ editBubbleText(newBubble);
1001
  }
1002
 
1003
  function gotoTimestamp() {
1004
+ if (!currentlySelectedPanel) { alert("Please select a panel first."); return; }
1005
+ const input = document.getElementById('timestamp-input');
1006
+ const timeStr = input.value.trim();
1007
+ if (!timeStr) return;
1008
+ let parsedSeconds = 0;
1009
+ if (timeStr.includes(':')) {
1010
+ const parts = timeStr.split(':');
1011
+ parsedSeconds = parseInt(parts[0], 10) * 60 + parseFloat(parts[1]);
1012
+ } else {
1013
+ parsedSeconds = parseFloat(timeStr);
1014
+ }
1015
+ if (isNaN(parsedSeconds)) { alert("Invalid time format."); return; }
1016
+ const img = currentlySelectedPanel.querySelector('img');
1017
+ let filename = img.src.substring(img.src.lastIndexOf('/') + 1).split('?')[0];
1018
+ img.style.opacity = '0.5';
1019
+ fetch('/goto_timestamp', {
1020
+ method: 'POST',
1021
+ headers: { 'Content-Type': 'application/json' },
1022
+ body: JSON.stringify({ filename, timestamp: parsedSeconds })
1023
+ })
1024
+ .then(res => res.json())
1025
+ .then(data => {
1026
+ if (data.success) {
1027
+ img.src = `/frames/final/${filename}?t=${new Date().getTime()}`;
1028
+ input.value = '';
1029
+ resetPanelTransform();
1030
+ } else { alert('Error: ' + data.message); }
1031
+ img.style.opacity = '1';
1032
+ })
1033
+ .catch(() => {
1034
+ alert('An error occurred.');
1035
+ img.style.opacity = '1';
1036
+ });
1037
  }
1038
  </script>
1039
  </body>