jhh6576 commited on
Commit
02bf691
·
verified ·
1 Parent(s): 8df5a7b

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. app_enhanced.py +129 -122
app_enhanced.py CHANGED
@@ -496,7 +496,7 @@ class EnhancedComicGenerator:
496
  <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
497
  <link rel="preconnect" href="https://fonts.googleapis.com">
498
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
499
- <link href="https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@700&family=Gloria+Hallelujah&family=Lato&family=Permanent+Marker&display=swap" rel="stylesheet">
500
  <style>
501
  body { margin: 0; padding: 20px; background: #f0f0f0; font-family: 'Lato', sans-serif; }
502
  .comic-container { max-width: 1200px; margin: 0 auto; }
@@ -509,89 +509,34 @@ 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 {
513
- position: absolute; display: flex; justify-content: center; align-items: center;
514
- width: 150px; height: 80px; min-width: 50px; min-height: 30px;
515
- box-sizing: border-box;
516
- z-index: 10; cursor: move;
517
- font-size: 14px; font-weight: bold; text-align: center;
518
- }
519
  .bubble-text { padding: 2px; word-wrap: break-word; }
520
  .speech-bubble.selected { outline: 2px dashed #4CAF50; }
521
  .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; }
522
-
523
- .speech-bubble.speech {
524
- font-family: "Permanent Marker", cursive;
525
- color: #000;
526
- padding: 1em;
527
- --r: 1.2em; /* radius */
528
- --b: 3em; /* base */
529
- --h: 1.8em; /* height */
530
- --t: .6; /* thickness */
531
- --p: 20%;
532
- --c: #FFFFFF;
533
- background: var(--c);
534
- border-radius: var(--r) var(--r) min(var(--r),100% - var(--p) - (1 - var(--t))*var(--b)/2) min(var(--r),var(--p) - (1 - var(--t))*var(--b)/2)/var(--r);
535
- position: relative;
536
- }
537
- .speech-bubble.speech:before {
538
- content: "";
539
- position: absolute;
540
- top: 100%;
541
- left: clamp(0px, var(--p) - var(--b)/2, 100% - var(--b));
542
- width: var(--b);
543
- height: var(--h);
544
- background: inherit;
545
- border-bottom-right-radius: 100%;
546
- -webkit-mask: radial-gradient(calc(var(--t)*100%) 105% at 0 0,#0000 99%,#000 101%);
547
- mask: radial-gradient(calc(var(--t)*100%) 105% at 0 0,#0000 99%,#000 101%);
548
- transform-origin: center;
549
- }
550
- .speech-bubble.speech.flipped:before {
551
- left: auto;
552
- right: clamp(0px, var(--p) - var(--b)/2, 100% - var(--b));
553
- transform: scaleX(-1);
554
- }
555
- .speech-bubble.speech.flipped-vertical:before {
556
- top: auto; bottom: 100%;
557
- transform: scaleY(-1);
558
- }
559
- .speech-bubble.speech.flipped.flipped-vertical:before {
560
- top: auto; bottom: 100%;
561
- left: auto; right: clamp(0px, var(--p) - var(--b)/2, 100% - var(--b));
562
- transform: scale(-1, -1);
563
- }
564
-
565
- .speech-bubble.thought {
566
- border-radius: 50%; border: 3px solid black; background: white; color: black;
567
- }
568
- .speech-bubble.thought:before {
569
- content: ''; position: absolute;
570
- width: 8px; height: 8px; bottom: -12px;
571
- border-radius: 50%; background: white;
572
- left: 20%;
573
- box-shadow: 0 0 0 3px black, -10px 8px 0 0px white, -10px 8px 0 3px black;
574
- }
575
- .speech-bubble.thought.flipped:before {
576
- left: auto; right: 20%;
577
- box-shadow: 0 0 0 3px black, 10px 8px 0 0px white, 10px 8px 0 3px black;
578
- }
579
-
580
- .speech-bubble.reaction {
581
- background: #FFD700; border: 3px solid #E53935; color: #D32F2F; font-weight: 900;
582
- clip-path: polygon(100% 50%,78% 60%,88% 82%,65% 76%,59% 99%,45% 80%,25% 93%,27% 69%,3% 67%,20% 50%,3% 33%,27% 31%,25% 7%,45% 20%,59% 1%,65% 24%,88% 18%,78% 40%);
583
- }
584
-
585
  .speech-bubble.narration { background: #FAFAFA; border: 2px solid #BDBDBD; color: #424242; border-radius: 3px; }
586
  .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%; }
587
-
 
 
 
 
 
 
 
 
 
 
 
 
588
  .resize-handle { position: absolute; width: 10px; height: 10px; background: #2196F3; border: 1px solid white; border-radius: 50%; display: none; z-index: 11; }
589
  .speech-bubble.selected .resize-handle { display: block; }
590
  .resize-handle.se { bottom: -5px; right: -5px; cursor: se-resize; }
591
  .resize-handle.sw { bottom: -5px; left: -5px; cursor: sw-resize; }
592
  .resize-handle.ne { top: -5px; right: -5px; cursor: ne-resize; }
593
  .resize-handle.nw { top: -5px; left: -5px; cursor: nw-resize; }
594
-
595
  .edit-controls { position: fixed; bottom: 20px; right: 20px; background: rgba(44, 62, 80, 0.9); color: white; padding: 10px 15px; border-radius: 8px; font-size: 13px; z-index: 1000; box-shadow: 0 4px 12px rgba(0,0,0,0.3); width: 220px; }
596
  .edit-controls h4 { margin: 0 0 10px 0; color: #26a69a; text-align: center; }
597
  .edit-controls button, .edit-controls select, .edit-controls input { margin-top: 5px; padding: 6px 8px; font-size: 12px; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; width: 100%; box-sizing: border-box; }
@@ -623,7 +568,6 @@ class EnhancedComicGenerator:
623
  <option value="speech">Speech</option><option value="thought">Thought</option><option value="reaction">Reaction</option><option value="narration">Narration</option><option value="idea">Idea</option>
624
  </select>
625
  <select id="font-select" onchange="changeFont(this.value)" disabled>
626
- <option value="'Permanent Marker', cursive">Permanent Marker</option>
627
  <option value="'Comic Neue', cursive">Comic Neue</option>
628
  <option value="'Bangers', cursive">Bangers</option>
629
  <option value="'Gloria Hallelujah', cursive">Gloria</option>
@@ -639,7 +583,7 @@ class EnhancedComicGenerator:
639
  <input type="color" id="bubble-fill-color" value="#FFFFFF" disabled>
640
  </div>
641
  </div>
642
- <button onclick="rotateBubbleTail()" class="secondary-button">🔄 Flip Tail</button>
643
  <button onclick="addBubbleToPanel()" class="action-button">💬 Add Bubble</button>
644
  <button onclick="deleteBubble()" class="reset-button">🗑️ Delete Bubble</button>
645
  </div>
@@ -679,6 +623,7 @@ class EnhancedComicGenerator:
679
  let currentlySelectedBubble = null;
680
  let currentlySelectedPanel = null;
681
  let isPanning = false, panStartX, panStartY, panStartTranslateX, panStartTranslateY;
 
682
 
683
  function renderComic(data) {
684
  const container = document.getElementById('comic-pages');
@@ -729,15 +674,8 @@ class EnhancedComicGenerator:
729
  if(currentlySelectedBubble) currentlySelectedBubble.style.color = e.target.value;
730
  });
731
  document.getElementById('bubble-fill-color').addEventListener('input', (e) => {
732
- if(currentlySelectedBubble) {
733
- if (currentlySelectedBubble.classList.contains('speech')) {
734
- currentlySelectedBubble.style.setProperty('--c', e.target.value);
735
- } else {
736
- currentlySelectedBubble.style.backgroundColor = e.target.value;
737
- }
738
- }
739
  });
740
-
741
  document.addEventListener('mousemove', e => { if (isPanning) panImage(e); if (draggedBubble) drag(e); if(isResizing) resizeBubble(e); });
742
  document.addEventListener('mouseup', e => { if (isPanning) stopPan(e); if (draggedBubble) stopDrag(e); if(isResizing) stopResize(e);});
743
  document.addEventListener('mouseleave', e => { if (isPanning) stopPan(e); if (draggedBubble) stopDrag(e); if(isResizing) stopResize(e);});
@@ -754,7 +692,6 @@ class EnhancedComicGenerator:
754
  handle.addEventListener('mousedown', (e) => startResize(e, dir));
755
  });
756
  }
757
-
758
  function createBubbleElement(data) {
759
  const bubbleDiv = document.createElement('div');
760
  bubbleDiv.dataset.id = data.id;
@@ -778,15 +715,22 @@ class EnhancedComicGenerator:
778
  if (b.length == 1) b = "0" + b;
779
  return "#" + r + g + b;
780
  }
781
-
782
  function applyBubbleType(bubble, type) {
783
- let classesToKeep = ['speech-bubble'];
784
- if (bubble.classList.contains('selected')) classesToKeep.push('selected');
785
- if (bubble.classList.contains('flipped')) classesToKeep.push('flipped');
786
- if (bubble.classList.contains('flipped-vertical')) classesToKeep.push('flipped-vertical');
787
- bubble.className = classesToKeep.join(' ');
 
788
  bubble.classList.add(type);
789
  bubble.dataset.type = type;
 
 
 
 
 
 
 
790
  }
791
 
792
  function changeBubbleType(type) {
@@ -803,8 +747,8 @@ class EnhancedComicGenerator:
803
  if (!currentlySelectedBubble) { alert("Please select a bubble first."); return; }
804
  const isFlippedH = currentlySelectedBubble.classList.contains('flipped');
805
  const isFlippedV = currentlySelectedBubble.classList.contains('flipped-vertical');
806
- if (!isFlippedH && !isFlippedV) { currentlySelectedBubble.classList.add('flipped'); }
807
- else if (isFlippedH && !isFlippedV) { currentlySelectedBubble.classList.add('flipped-vertical'); }
808
  else if (isFlippedH && isFlippedV) { currentlySelectedBubble.classList.remove('flipped'); }
809
  else { currentlySelectedBubble.classList.remove('flipped-vertical'); }
810
  }
@@ -815,12 +759,10 @@ class EnhancedComicGenerator:
815
  currentlySelectedPanel = panel;
816
  selectBubble(null);
817
  }
818
-
819
  function selectBubble(bubble) {
820
  if (currentlySelectedBubble) currentlySelectedBubble.classList.remove('selected');
821
  currentlySelectedBubble = bubble;
822
  const bubbleControls = ['bubble-text-color', 'bubble-fill-color', 'bubble-type-select', 'font-select'];
823
-
824
  if (currentlySelectedBubble) {
825
  currentlySelectedBubble.classList.add('selected');
826
  if (currentlySelectedPanel) currentlySelectedPanel.classList.remove('selected');
@@ -828,11 +770,9 @@ class EnhancedComicGenerator:
828
 
829
  const styles = window.getComputedStyle(currentlySelectedBubble);
830
  document.getElementById('bubble-text-color').value = rgbToHex(styles.color);
831
- const fillColor = currentlySelectedBubble.classList.contains('speech') ? styles.getPropertyValue('--c').trim() : styles.backgroundColor;
832
- document.getElementById('bubble-fill-color').value = rgbToHex(fillColor);
833
  document.getElementById('bubble-type-select').value = currentlySelectedBubble.dataset.type || 'speech';
834
  document.getElementById('font-select').value = styles.fontFamily.split(',')[0].replace(/"/g, "").replace(/'/g, "").trim();
835
-
836
  document.getElementById('zoom-slider').disabled = true;
837
  bubbleControls.forEach(id => document.getElementById(id).disabled = false);
838
  } else {
@@ -848,7 +788,6 @@ class EnhancedComicGenerator:
848
  const textarea = document.createElement('textarea');
849
  const originalHeight = bubble.offsetHeight;
850
  bubble.style.height = `${originalHeight}px`;
851
-
852
  textarea.value = textSpan.textContent;
853
  bubble.appendChild(textarea);
854
  textSpan.style.display = 'none';
@@ -862,7 +801,6 @@ class EnhancedComicGenerator:
862
  textarea.addEventListener('blur', finishEditing, { once: true });
863
  textarea.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); textarea.blur(); }});
864
  }
865
-
866
  function startDrag(e) {
867
  const bubble = e.target.closest('.speech-bubble');
868
  if (!bubble || currentlyEditing) return;
@@ -871,16 +809,13 @@ class EnhancedComicGenerator:
871
  const rect = bubble.getBoundingClientRect();
872
  offset = { x: e.clientX - rect.left, y: e.clientY - rect.top };
873
  }
874
-
875
  function drag(e) {
876
  if (!draggedBubble) return;
877
  const parentRect = draggedBubble.parentElement.getBoundingClientRect();
878
  draggedBubble.style.left = `${e.clientX - parentRect.left - offset.x}px`;
879
  draggedBubble.style.top = `${e.clientY - parentRect.top - offset.y}px`;
880
  }
881
-
882
  function stopDrag() { draggedBubble = null; }
883
-
884
  function deleteBubble() {
885
  if (!currentlySelectedBubble) {
886
  alert("Please select a bubble to delete.");
@@ -892,7 +827,6 @@ class EnhancedComicGenerator:
892
  selectBubble(null);
893
  }
894
  }
895
-
896
  function startResize(e, dir) {
897
  e.preventDefault();
898
  e.stopPropagation();
@@ -907,13 +841,11 @@ class EnhancedComicGenerator:
907
  originalMouseX = e.clientX;
908
  originalMouseY = e.clientY;
909
  }
910
-
911
  function resizeBubble(e) {
912
  if (!isResizing || !currentlySelectedBubble) return;
913
  const dx = e.clientX - originalMouseX;
914
  const dy = e.clientY - originalMouseY;
915
  const bubble = currentlySelectedBubble;
916
-
917
  if (resizeHandle.includes('e')) bubble.style.width = `${originalWidth + dx}px`;
918
  if (resizeHandle.includes('w')) {
919
  bubble.style.width = `${originalWidth - dx}px`;
@@ -925,18 +857,26 @@ class EnhancedComicGenerator:
925
  bubble.style.top = `${originalY + dy}px`;
926
  }
927
  }
928
-
929
  function stopResize() { isResizing = false; }
930
-
931
  function clearSavedState() {
932
  if (confirm("Reset all edits?")) {
933
  localStorage.removeItem('comicEditorState');
934
  window.location.reload();
935
  }
936
  }
937
-
938
  async function exportPagesToPNG() {
939
- // function content...
 
 
 
 
 
 
 
 
 
 
 
940
  }
941
 
942
  function replacePanelImage() {
@@ -969,15 +909,39 @@ class EnhancedComicGenerator:
969
  }
970
 
971
  function adjustFrame(direction) {
972
- // function content...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
973
  }
974
-
975
  function updateImageTransform(img) {
976
- // function content...
 
 
 
 
977
  }
978
-
979
  function handleZoom(event) {
980
- // function content...
 
 
 
981
  }
982
 
983
  function resetPanelTransform() {
@@ -989,19 +953,30 @@ class EnhancedComicGenerator:
989
  document.getElementById('zoom-slider').value = 100;
990
  updateImageTransform(img);
991
  }
992
-
993
  function startPan(event) {
994
- // function content...
 
 
 
 
 
 
 
 
 
995
  }
996
-
997
  function panImage(event) {
998
- // function content...
 
 
 
 
999
  }
1000
-
1001
  function stopPan() {
1002
- // function content...
 
 
1003
  }
1004
-
1005
  function addBubbleToPanel() {
1006
  if (!currentlySelectedPanel) { alert("Please select a panel first."); return; }
1007
  const newBubble = createBubbleElement({
@@ -1017,7 +992,39 @@ class EnhancedComicGenerator:
1017
  }
1018
 
1019
  function gotoTimestamp() {
1020
- // function content...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1021
  }
1022
  </script>
1023
  </body>
 
496
  <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
497
  <link rel="preconnect" href="https://fonts.googleapis.com">
498
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
499
+ <link href="https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@700&family=Gloria+Hallelujah&family=Lato&display=swap" rel="stylesheet">
500
  <style>
501
  body { margin: 0; padding: 20px; background: #f0f0f0; font-family: 'Lato', sans-serif; }
502
  .comic-container { max-width: 1200px; margin: 0 auto; }
 
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: 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; }
522
+ .speech-bubble.speech::after { border-top: 10px solid #333; bottom: -9px; left: 20px; }
523
+ .speech-bubble.idea::after { border-top: 10px solid #FFA500; bottom: -9px; left: 20px; }
524
+ .speech-bubble.thought::after { display: none; }
525
+ .thought-dot { position: absolute; background-color: white; border: 2px solid #555; border-radius: 50%; z-index: -1; }
526
+ .thought-dot-1 { width: 20px; height: 20px; bottom: -20px; left: 15px; }
527
+ .thought-dot-2 { width: 12px; height: 12px; bottom: -32px; left: 5px; }
528
+ .speech-bubble.flipped.speech::after, .speech-bubble.flipped.idea::after { left: auto; right: 20px; }
529
+ .speech-bubble.flipped.thought .thought-dot-1 { left: auto; right: 15px; }
530
+ .speech-bubble.flipped.thought .thought-dot-2 { left: auto; right: 5px; }
531
+ .speech-bubble.flipped-vertical.speech::after, .speech-bubble.flipped-vertical.idea::after { bottom: auto; top: -9px; transform: rotate(180deg); }
532
+ .speech-bubble.flipped-vertical.thought .thought-dot-1 { bottom: auto; top: -20px; }
533
+ .speech-bubble.flipped-vertical.thought .thought-dot-2 { bottom: auto; top: -32px; }
534
  .resize-handle { position: absolute; width: 10px; height: 10px; background: #2196F3; border: 1px solid white; border-radius: 50%; display: none; z-index: 11; }
535
  .speech-bubble.selected .resize-handle { display: block; }
536
  .resize-handle.se { bottom: -5px; right: -5px; cursor: se-resize; }
537
  .resize-handle.sw { bottom: -5px; left: -5px; cursor: sw-resize; }
538
  .resize-handle.ne { top: -5px; right: -5px; cursor: ne-resize; }
539
  .resize-handle.nw { top: -5px; left: -5px; cursor: nw-resize; }
 
540
  .edit-controls { position: fixed; bottom: 20px; right: 20px; background: rgba(44, 62, 80, 0.9); color: white; padding: 10px 15px; border-radius: 8px; font-size: 13px; z-index: 1000; box-shadow: 0 4px 12px rgba(0,0,0,0.3); width: 220px; }
541
  .edit-controls h4 { margin: 0 0 10px 0; color: #26a69a; text-align: center; }
542
  .edit-controls button, .edit-controls select, .edit-controls input { margin-top: 5px; padding: 6px 8px; font-size: 12px; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; width: 100%; box-sizing: border-box; }
 
568
  <option value="speech">Speech</option><option value="thought">Thought</option><option value="reaction">Reaction</option><option value="narration">Narration</option><option value="idea">Idea</option>
569
  </select>
570
  <select id="font-select" onchange="changeFont(this.value)" disabled>
 
571
  <option value="'Comic Neue', cursive">Comic Neue</option>
572
  <option value="'Bangers', cursive">Bangers</option>
573
  <option value="'Gloria Hallelujah', cursive">Gloria</option>
 
583
  <input type="color" id="bubble-fill-color" value="#FFFFFF" disabled>
584
  </div>
585
  </div>
586
+ <button onclick="rotateBubbleTail()" class="secondary-button">🔄 Rotate Tail</button>
587
  <button onclick="addBubbleToPanel()" class="action-button">💬 Add Bubble</button>
588
  <button onclick="deleteBubble()" class="reset-button">🗑️ Delete Bubble</button>
589
  </div>
 
623
  let currentlySelectedBubble = null;
624
  let currentlySelectedPanel = null;
625
  let isPanning = false, panStartX, panStartY, panStartTranslateX, panStartTranslateY;
626
+ let isResizing = false, resizeHandle, originalWidth, originalHeight, originalX, originalY, originalMouseX, originalMouseY;
627
 
628
  function renderComic(data) {
629
  const container = document.getElementById('comic-pages');
 
674
  if(currentlySelectedBubble) currentlySelectedBubble.style.color = e.target.value;
675
  });
676
  document.getElementById('bubble-fill-color').addEventListener('input', (e) => {
677
+ if(currentlySelectedBubble) currentlySelectedBubble.style.backgroundColor = e.target.value;
 
 
 
 
 
 
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);});
681
  document.addEventListener('mouseleave', e => { if (isPanning) stopPan(e); if (draggedBubble) stopDrag(e); if(isResizing) stopResize(e);});
 
692
  handle.addEventListener('mousedown', (e) => startResize(e, dir));
693
  });
694
  }
 
695
  function createBubbleElement(data) {
696
  const bubbleDiv = document.createElement('div');
697
  bubbleDiv.dataset.id = data.id;
 
715
  if (b.length == 1) b = "0" + b;
716
  return "#" + r + g + b;
717
  }
 
718
  function applyBubbleType(bubble, type) {
719
+ bubble.querySelectorAll('.thought-dot').forEach(el => el.remove());
720
+ let classesToKeep = 'speech-bubble';
721
+ if (bubble.classList.contains('selected')) classesToKeep += ' selected';
722
+ if (bubble.classList.contains('flipped')) classesToKeep += ' flipped';
723
+ if (bubble.classList.contains('flipped-vertical')) classesToKeep += ' flipped-vertical';
724
+ bubble.className = classesToKeep;
725
  bubble.classList.add(type);
726
  bubble.dataset.type = type;
727
+ if (type === 'thought') {
728
+ for (let i = 1; i <= 2; i++) {
729
+ const dot = document.createElement('div');
730
+ dot.className = `thought-dot thought-dot-${i}`;
731
+ bubble.appendChild(dot);
732
+ }
733
+ }
734
  }
735
 
736
  function changeBubbleType(type) {
 
747
  if (!currentlySelectedBubble) { alert("Please select a bubble first."); return; }
748
  const isFlippedH = currentlySelectedBubble.classList.contains('flipped');
749
  const isFlippedV = currentlySelectedBubble.classList.contains('flipped-vertical');
750
+ if (!isFlippedH && !isFlippedV) { currentlySelectedBubble.classList.add('flipped'); }
751
+ else if (isFlippedH && !isFlippedV) { currentlySelectedBubble.classList.add('flipped-vertical'); }
752
  else if (isFlippedH && isFlippedV) { currentlySelectedBubble.classList.remove('flipped'); }
753
  else { currentlySelectedBubble.classList.remove('flipped-vertical'); }
754
  }
 
759
  currentlySelectedPanel = panel;
760
  selectBubble(null);
761
  }
 
762
  function selectBubble(bubble) {
763
  if (currentlySelectedBubble) currentlySelectedBubble.classList.remove('selected');
764
  currentlySelectedBubble = bubble;
765
  const bubbleControls = ['bubble-text-color', 'bubble-fill-color', 'bubble-type-select', 'font-select'];
 
766
  if (currentlySelectedBubble) {
767
  currentlySelectedBubble.classList.add('selected');
768
  if (currentlySelectedPanel) currentlySelectedPanel.classList.remove('selected');
 
770
 
771
  const styles = window.getComputedStyle(currentlySelectedBubble);
772
  document.getElementById('bubble-text-color').value = rgbToHex(styles.color);
773
+ document.getElementById('bubble-fill-color').value = rgbToHex(styles.backgroundColor);
 
774
  document.getElementById('bubble-type-select').value = currentlySelectedBubble.dataset.type || 'speech';
775
  document.getElementById('font-select').value = styles.fontFamily.split(',')[0].replace(/"/g, "").replace(/'/g, "").trim();
 
776
  document.getElementById('zoom-slider').disabled = true;
777
  bubbleControls.forEach(id => document.getElementById(id).disabled = false);
778
  } else {
 
788
  const textarea = document.createElement('textarea');
789
  const originalHeight = bubble.offsetHeight;
790
  bubble.style.height = `${originalHeight}px`;
 
791
  textarea.value = textSpan.textContent;
792
  bubble.appendChild(textarea);
793
  textSpan.style.display = 'none';
 
801
  textarea.addEventListener('blur', finishEditing, { once: true });
802
  textarea.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); textarea.blur(); }});
803
  }
 
804
  function startDrag(e) {
805
  const bubble = e.target.closest('.speech-bubble');
806
  if (!bubble || currentlyEditing) return;
 
809
  const rect = bubble.getBoundingClientRect();
810
  offset = { x: e.clientX - rect.left, y: e.clientY - rect.top };
811
  }
 
812
  function drag(e) {
813
  if (!draggedBubble) return;
814
  const parentRect = draggedBubble.parentElement.getBoundingClientRect();
815
  draggedBubble.style.left = `${e.clientX - parentRect.left - offset.x}px`;
816
  draggedBubble.style.top = `${e.clientY - parentRect.top - offset.y}px`;
817
  }
 
818
  function stopDrag() { draggedBubble = null; }
 
819
  function deleteBubble() {
820
  if (!currentlySelectedBubble) {
821
  alert("Please select a bubble to delete.");
 
827
  selectBubble(null);
828
  }
829
  }
 
830
  function startResize(e, dir) {
831
  e.preventDefault();
832
  e.stopPropagation();
 
841
  originalMouseX = e.clientX;
842
  originalMouseY = e.clientY;
843
  }
 
844
  function resizeBubble(e) {
845
  if (!isResizing || !currentlySelectedBubble) return;
846
  const dx = e.clientX - originalMouseX;
847
  const dy = e.clientY - originalMouseY;
848
  const bubble = currentlySelectedBubble;
 
849
  if (resizeHandle.includes('e')) bubble.style.width = `${originalWidth + dx}px`;
850
  if (resizeHandle.includes('w')) {
851
  bubble.style.width = `${originalWidth - dx}px`;
 
857
  bubble.style.top = `${originalY + dy}px`;
858
  }
859
  }
 
860
  function stopResize() { isResizing = false; }
 
861
  function clearSavedState() {
862
  if (confirm("Reset all edits?")) {
863
  localStorage.removeItem('comicEditorState');
864
  window.location.reload();
865
  }
866
  }
 
867
  async function exportPagesToPNG() {
868
+ const pages = document.querySelectorAll('.comic-page');
869
+ if (pages.length === 0) return alert("No pages found.");
870
+ alert(`Starting export of ${pages.length} page(s).`);
871
+ for (let i = 0; i < pages.length; i++) {
872
+ try {
873
+ const canvas = await html2canvas(pages[i], { scale: 2 });
874
+ const link = document.createElement('a');
875
+ link.download = `comic-page-${i + 1}.png`;
876
+ link.href = canvas.toDataURL('image/png');
877
+ link.click();
878
+ } catch (err) { alert(`Failed to export page ${i + 1}.`); }
879
+ }
880
  }
881
 
882
  function replacePanelImage() {
 
909
  }
910
 
911
  function adjustFrame(direction) {
912
+ if (!currentlySelectedPanel) { alert("Please select a panel first."); return; }
913
+ const img = currentlySelectedPanel.querySelector('img');
914
+ let filename = img.src.substring(img.src.lastIndexOf('/') + 1).split('?')[0];
915
+ img.style.opacity = '0.5';
916
+ fetch('/regenerate_frame', {
917
+ method: 'POST',
918
+ headers: { 'Content-Type': 'application/json' },
919
+ body: JSON.stringify({ filename, direction })
920
+ })
921
+ .then(res => res.json())
922
+ .then(data => {
923
+ if (data.success) {
924
+ img.src = `/frames/final/${filename}?t=${new Date().getTime()}`;
925
+ } else { alert('Error: ' + data.message); }
926
+ img.style.opacity = '1';
927
+ })
928
+ .catch(() => {
929
+ alert('An error occurred.');
930
+ img.style.opacity = '1';
931
+ });
932
  }
 
933
  function updateImageTransform(img) {
934
+ const zoom = (img.dataset.zoom || 100) / 100;
935
+ const x = img.dataset.translateX || 0;
936
+ const y = img.dataset.translateY || 0;
937
+ img.style.transform = `translateX(${x}px) translateY(${y}px) scale(${zoom})`;
938
+ img.classList.toggle('pannable', zoom > 1);
939
  }
 
940
  function handleZoom(event) {
941
+ if (!currentlySelectedPanel) return;
942
+ const img = currentlySelectedPanel.querySelector('img');
943
+ img.dataset.zoom = event.target.value;
944
+ updateImageTransform(img);
945
  }
946
 
947
  function resetPanelTransform() {
 
953
  document.getElementById('zoom-slider').value = 100;
954
  updateImageTransform(img);
955
  }
 
956
  function startPan(event) {
957
+ if (event.button !== 0) return;
958
+ const img = event.target;
959
+ if (parseFloat(img.dataset.zoom || 100) <= 100) return;
960
+ event.preventDefault();
961
+ isPanning = true;
962
+ img.classList.add('panning');
963
+ panStartX = event.clientX;
964
+ panStartY = event.clientY;
965
+ panStartTranslateX = parseFloat(img.dataset.translateX || 0);
966
+ panStartTranslateY = parseFloat(img.dataset.translateY || 0);
967
  }
 
968
  function panImage(event) {
969
+ if (!isPanning || !currentlySelectedPanel) return;
970
+ const img = currentlySelectedPanel.querySelector('img');
971
+ img.dataset.translateX = panStartTranslateX + (event.clientX - panStartX);
972
+ img.dataset.translateY = panStartTranslateY + (event.clientY - panStartY);
973
+ updateImageTransform(img);
974
  }
 
975
  function stopPan() {
976
+ if (!isPanning) return;
977
+ isPanning = false;
978
+ currentlySelectedPanel?.querySelector('img')?.classList.remove('panning');
979
  }
 
980
  function addBubbleToPanel() {
981
  if (!currentlySelectedPanel) { alert("Please select a panel first."); return; }
982
  const newBubble = createBubbleElement({
 
992
  }
993
 
994
  function gotoTimestamp() {
995
+ if (!currentlySelectedPanel) { alert("Please select a panel first."); return; }
996
+ const input = document.getElementById('timestamp-input');
997
+ const timeStr = input.value.trim();
998
+ if (!timeStr) return;
999
+ let parsedSeconds = 0;
1000
+ if (timeStr.includes(':')) {
1001
+ const parts = timeStr.split(':');
1002
+ parsedSeconds = parseInt(parts[0], 10) * 60 + parseFloat(parts[1]);
1003
+ } else {
1004
+ parsedSeconds = parseFloat(timeStr);
1005
+ }
1006
+ if (isNaN(parsedSeconds)) { alert("Invalid time format."); return; }
1007
+ const img = currentlySelectedPanel.querySelector('img');
1008
+ let filename = img.src.substring(img.src.lastIndexOf('/') + 1).split('?')[0];
1009
+ img.style.opacity = '0.5';
1010
+ fetch('/goto_timestamp', {
1011
+ method: 'POST',
1012
+ headers: { 'Content-Type': 'application/json' },
1013
+ body: JSON.stringify({ filename, timestamp: parsedSeconds })
1014
+ })
1015
+ .then(res => res.json())
1016
+ .then(data => {
1017
+ if (data.success) {
1018
+ img.src = `/frames/final/${filename}?t=${new Date().getTime()}`;
1019
+ input.value = '';
1020
+ resetPanelTransform();
1021
+ } else { alert('Error: ' + data.message); }
1022
+ img.style.opacity = '1';
1023
+ })
1024
+ .catch(() => {
1025
+ alert('An error occurred.');
1026
+ img.style.opacity = '1';
1027
+ });
1028
  }
1029
  </script>
1030
  </body>