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

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. app_enhanced.py +89 -77
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; 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,6 +552,8 @@ 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
  </style>
556
  </head>
557
  <body>
@@ -573,6 +575,10 @@ class EnhancedComicGenerator:
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,6 +629,7 @@ class EnhancedComicGenerator:
623
  let currentlySelectedBubble = null;
624
  let currentlySelectedPanel = null;
625
  let isPanning = false, panStartX, panStartY, panStartTranslateX, panStartTranslateY;
 
626
 
627
  function renderComic(data) {
628
  const container = document.getElementById('comic-pages');
@@ -675,6 +682,12 @@ class EnhancedComicGenerator:
675
  document.getElementById('bubble-fill-color').addEventListener('input', (e) => {
676
  if(currentlySelectedBubble) currentlySelectedBubble.style.backgroundColor = e.target.value;
677
  });
 
 
 
 
 
 
678
 
679
  document.addEventListener('mousemove', e => { if (isPanning) panImage(e); if (draggedBubble) drag(e); if(isResizing) resizeBubble(e); });
680
  document.addEventListener('mouseup', e => { if (isPanning) stopPan(e); if (draggedBubble) stopDrag(e); if(isResizing) stopResize(e);});
@@ -686,7 +699,6 @@ class EnhancedComicGenerator:
686
  bubble.addEventListener('mousedown', e => { e.stopPropagation(); startDrag(e); });
687
  bubble.addEventListener('click', e => { e.stopPropagation(); selectBubble(bubble); });
688
 
689
- // Add resize handles
690
  ['nw', 'ne', 'sw', 'se'].forEach(dir => {
691
  const handle = document.createElement('div');
692
  handle.className = `resize-handle ${dir}`;
@@ -720,21 +732,7 @@ class EnhancedComicGenerator:
720
  }
721
 
722
  function applyBubbleType(bubble, type) {
723
- bubble.querySelectorAll('.thought-dot').forEach(el => el.remove());
724
- let classesToKeep = 'speech-bubble';
725
- if (bubble.classList.contains('selected')) classesToKeep += ' selected';
726
- if (bubble.classList.contains('flipped')) classesToKeep += ' flipped';
727
- if (bubble.classList.contains('flipped-vertical')) classesToKeep += ' flipped-vertical';
728
- bubble.className = classesToKeep;
729
- bubble.classList.add(type);
730
- bubble.dataset.type = type;
731
- if (type === 'thought') {
732
- for (let i = 1; i <= 2; i++) {
733
- const dot = document.createElement('div');
734
- dot.className = `thought-dot thought-dot-${i}`;
735
- bubble.appendChild(dot);
736
- }
737
- }
738
  }
739
 
740
  function changeBubbleType(type) {
@@ -748,13 +746,7 @@ class EnhancedComicGenerator:
748
  }
749
 
750
  function rotateBubbleTail() {
751
- if (!currentlySelectedBubble) { alert("Please select a bubble first."); return; }
752
- const isFlippedH = currentlySelectedBubble.classList.contains('flipped');
753
- const isFlippedV = currentlySelectedBubble.classList.contains('flipped-vertical');
754
- if (!isFlippedH && !isFlippedV) { currentlySelectedBubble.classList.add('flipped'); }
755
- else if (isFlippedH && !isFlippedV) { currentlySelectedBubble.classList.add('flipped-vertical'); }
756
- else if (isFlippedH && isFlippedV) { currentlySelectedBubble.classList.remove('flipped'); }
757
- else { currentlySelectedBubble.classList.remove('flipped-vertical'); }
758
  }
759
 
760
  function selectPanel(panel) {
@@ -771,11 +763,7 @@ class EnhancedComicGenerator:
771
  function selectBubble(bubble) {
772
  if (currentlySelectedBubble) currentlySelectedBubble.classList.remove('selected');
773
  currentlySelectedBubble = bubble;
774
- const textColorPicker = document.getElementById('bubble-text-color');
775
- const fillColorPicker = document.getElementById('bubble-fill-color');
776
- const typeSelect = document.getElementById('bubble-type-select');
777
- const fontSelect = document.getElementById('font-select');
778
- const bubbleControls = [textColorPicker, fillColorPicker, typeSelect, fontSelect];
779
 
780
  if (currentlySelectedBubble) {
781
  currentlySelectedBubble.classList.add('selected');
@@ -783,15 +771,19 @@ class EnhancedComicGenerator:
783
  currentlySelectedPanel = null;
784
 
785
  const styles = window.getComputedStyle(currentlySelectedBubble);
786
- textColorPicker.value = rgbToHex(styles.color);
787
- fillColorPicker.value = rgbToHex(styles.backgroundColor);
788
- typeSelect.value = currentlySelectedBubble.dataset.type || 'speech';
789
- fontSelect.value = styles.fontFamily.split(',')[0].replace(/"/g, "").replace(/'/g, "").trim();
 
 
 
 
790
 
791
  document.getElementById('zoom-slider').disabled = true;
792
- bubbleControls.forEach(c => c.disabled = false);
793
  } else {
794
- bubbleControls.forEach(c => c.disabled = true);
795
  }
796
  }
797
 
@@ -800,7 +792,6 @@ class EnhancedComicGenerator:
800
  currentlyEditing = bubble;
801
  const textSpan = bubble.querySelector('.bubble-text');
802
  const textarea = document.createElement('textarea');
803
- // FIX: Preserve height before editing
804
  const originalHeight = bubble.style.height || `${bubble.offsetHeight}px`;
805
  bubble.style.height = originalHeight;
806
 
@@ -812,7 +803,6 @@ class EnhancedComicGenerator:
812
  textSpan.textContent = textarea.value;
813
  bubble.removeChild(textarea);
814
  textSpan.style.display = '';
815
- // FIX: Re-apply height, then set to auto to allow natural flow
816
  bubble.style.height = originalHeight;
817
  setTimeout(() => { bubble.style.height = 'auto'; }, 0);
818
  currentlyEditing = null;
@@ -822,19 +812,11 @@ class EnhancedComicGenerator:
822
  }
823
 
824
  function startDrag(e) {
825
- const bubble = e.target.closest('.speech-bubble');
826
- if (!bubble || currentlyEditing) return;
827
- draggedBubble = bubble;
828
- selectBubble(bubble);
829
- const rect = bubble.getBoundingClientRect();
830
- offset = { x: e.clientX - rect.left, y: e.clientY - rect.top };
831
  }
832
 
833
  function drag(e) {
834
- if (!draggedBubble) return;
835
- const parentRect = draggedBubble.parentElement.getBoundingClientRect();
836
- draggedBubble.style.left = `${e.clientX - parentRect.left - offset.x}px`;
837
- draggedBubble.style.top = `${e.clientY - parentRect.top - offset.y}px`;
838
  }
839
 
840
  function stopDrag() { draggedBubble = null; }
@@ -850,10 +832,7 @@ class EnhancedComicGenerator:
850
  selectBubble(null);
851
  }
852
  }
853
-
854
- let isResizing = false;
855
- let resizeHandle, originalWidth, originalHeight, originalX, originalY, originalMouseX, originalMouseY;
856
-
857
  function startResize(e, dir) {
858
  e.preventDefault();
859
  e.stopPropagation();
@@ -875,16 +854,19 @@ class EnhancedComicGenerator:
875
  const dy = e.clientY - originalMouseY;
876
  const bubble = currentlySelectedBubble;
877
 
878
- if (resizeHandle.includes('e')) {
879
- bubble.style.width = `${originalWidth + dx}px`;
 
 
 
880
  }
 
 
881
  if (resizeHandle.includes('w')) {
882
  bubble.style.width = `${originalWidth - dx}px`;
883
  bubble.style.left = `${originalX + dx}px`;
884
  }
885
- if (resizeHandle.includes('s')) {
886
- bubble.style.height = `${originalHeight + dy}px`;
887
- }
888
  if (resizeHandle.includes('n')) {
889
  bubble.style.height = `${originalHeight - dy}px`;
890
  bubble.style.top = `${originalY + dy}px`;
@@ -921,8 +903,8 @@ class EnhancedComicGenerator:
921
  .then(data => {
922
  if (data.success) {
923
  img.src = `/frames/final/${data.new_filename}?t=${new Date().getTime()}`;
924
- img.style.objectFit = 'contain'; // FIX: Show full image
925
- resetPanelTransform(); // FIX: Reset any old zoom/pan
926
  } else { alert('Error replacing image: ' + data.error); }
927
  img.style.opacity = '1';
928
  })
@@ -939,12 +921,36 @@ class EnhancedComicGenerator:
939
  // function content...
940
  }
941
 
942
- function updateImageTransform(img) {
943
- // function content...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
944
  }
945
 
946
  function handleZoom(event) {
947
- // function content...
 
 
 
948
  }
949
 
950
  function resetPanelTransform() {
@@ -954,33 +960,39 @@ class EnhancedComicGenerator:
954
  img.dataset.translateX = 0;
955
  img.dataset.translateY = 0;
956
  document.getElementById('zoom-slider').value = 100;
957
- updateImageTransform(img);
958
  }
959
 
960
  function startPan(event) {
961
- // function content...
 
 
 
 
 
 
 
 
 
 
962
  }
963
 
964
  function panImage(event) {
965
- // function content...
 
 
 
 
966
  }
967
 
968
  function stopPan() {
969
- // function content...
 
 
970
  }
971
 
972
  function addBubbleToPanel() {
973
- if (!currentlySelectedPanel) { alert("Please select a panel first."); return; }
974
- const newBubble = createBubbleElement({
975
- id: `new-bubble-${Date.now()}`,
976
- text: 'New Text...',
977
- left: '10%',
978
- top: '10%'
979
- });
980
- currentlySelectedPanel.appendChild(newBubble);
981
- initializeBubbleEvents(newBubble);
982
- selectBubble(newBubble);
983
- editBubbleText(newBubble);
984
  }
985
 
986
  function gotoTimestamp() {
 
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
  .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
  <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
  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');
 
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);});
 
699
  bubble.addEventListener('mousedown', e => { e.stopPropagation(); startDrag(e); });
700
  bubble.addEventListener('click', e => { e.stopPropagation(); selectBubble(bubble); });
701
 
 
702
  ['nw', 'ne', 'sw', 'se'].forEach(dir => {
703
  const handle = document.createElement('div');
704
  handle.className = `resize-handle ${dir}`;
 
732
  }
733
 
734
  function applyBubbleType(bubble, type) {
735
+ // function content...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
736
  }
737
 
738
  function changeBubbleType(type) {
 
746
  }
747
 
748
  function rotateBubbleTail() {
749
+ // function content...
 
 
 
 
 
 
750
  }
751
 
752
  function selectPanel(panel) {
 
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');
 
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 {
786
+ bubbleControls.forEach(id => document.getElementById(id).disabled = true);
787
  }
788
  }
789
 
 
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
 
 
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;
 
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; }
 
832
  selectBubble(null);
833
  }
834
  }
835
+
 
 
 
836
  function startResize(e, dir) {
837
  e.preventDefault();
838
  e.stopPropagation();
 
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`;
 
903
  .then(data => {
904
  if (data.success) {
905
  img.src = `/frames/final/${data.new_filename}?t=${new Date().getTime()}`;
906
+ img.style.objectFit = 'contain';
907
+ resetPanelTransform();
908
  } else { alert('Error replacing image: ' + data.error); }
909
  img.style.opacity = '1';
910
  })
 
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
 
949
  function handleZoom(event) {
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
  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');
974
+ panStartX = event.clientX;
975
+ panStartY = event.clientY;
976
+ panStartTranslateX = parseFloat(img.dataset.translateX || 0);
977
+ panStartTranslateY = parseFloat(img.dataset.translateY || 0);
978
  }
979
 
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() {
989
+ if (!isPanning) return;
990
+ isPanning = false;
991
+ currentlySelectedPanel?.querySelector('img')?.classList.remove('panning');
992
  }
993
 
994
  function addBubbleToPanel() {
995
+ // function content...
 
 
 
 
 
 
 
 
 
 
996
  }
997
 
998
  function gotoTimestamp() {