jhh6576 commited on
Commit
0fb0aa7
·
verified ·
1 Parent(s): 05654c6

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. app_enhanced.py +103 -68
app_enhanced.py CHANGED
@@ -539,74 +539,83 @@ class EnhancedComicGenerator:
539
  .panel img.pannable { cursor: grab; }
540
  .panel img.panning { cursor: grabbing; }
541
  .speech-bubble { position: absolute; display: flex; justify-content: center; align-items: center; width: 150px; height: 80px; min-width: 50px; min-height: 30px; box-sizing: border-box; z-index: 10; cursor: move; overflow: visible; font-size: 13px; font-weight: bold; text-align: center; font-family: 'Comic Neue', cursive; }
542
- .bubble-text { padding: 0.8em; word-wrap: break-word; }
543
  .speech-bubble.selected { outline: 2px dashed #4CAF50; }
544
  .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; }
545
 
546
- /* <<< MODIFICATION START: New CSS for Speech Bubble with Sliders >>> */
547
  .speech-bubble.speech {
548
- background: var(--bubble-fill-color, white);
549
- color: var(--bubble-text-color, #333);
550
- border-radius: .4em;
 
551
  padding: 0;
552
  border: none;
553
- box-shadow: 0 1px 4px rgba(0,0,0,0.25);
 
 
 
 
 
 
 
 
 
 
 
 
 
554
  }
555
-
556
- .speech-bubble.speech::after {
557
  content: '';
558
  position: absolute;
559
- width: 0;
560
- height: 0;
561
- border-color: transparent;
562
- border-style: solid;
563
- /* Use CSS variables for dynamic control from sliders */
564
- border-width: var(--tail-size, 20px);
565
  }
566
 
567
- .speech-bubble.speech.tail-bottom::after {
568
- bottom: 0;
569
- left: var(--tail-pos, 50%); /* Controlled by position slider */
570
- border-top-color: var(--bubble-fill-color, white);
571
- border-bottom: 0;
572
- transform: translateX(-50%); /* Center the tip on the slider's value */
573
- margin-bottom: calc(-1 * var(--tail-size, 20px));
 
 
574
  }
575
- .speech-bubble.speech.tail-top::after {
576
- top: 0;
577
- left: var(--tail-pos, 50%);
578
- border-bottom-color: var(--bubble-fill-color, white);
579
- border-top: 0;
580
- transform: translateX(-50%);
581
- margin-top: calc(-1 * var(--tail-size, 20px));
582
- }
583
- .speech-bubble.speech.tail-right::after {
584
- top: var(--tail-pos, 50%);
585
- right: 0;
586
- border-left-color: var(--bubble-fill-color, white);
587
- border-right: 0;
588
- transform: translateY(-50%);
589
- margin-right: calc(-1 * var(--tail-size, 20px));
590
  }
591
- .speech-bubble.speech.tail-left::after {
592
- top: var(--tail-pos, 50%);
593
- left: 0;
594
- border-right-color: var(--bubble-fill-color, white);
595
- border-left: 0;
596
- transform: translateY(-50%);
597
- margin-left: calc(-1 * var(--tail-size, 20px));
598
  }
 
 
 
 
 
 
 
 
 
599
  /* <<< MODIFICATION END >>> */
600
 
601
- .speech-bubble.thought { background: white; border: 2px dashed #555; color: #333; border-radius: 50%; }
602
  .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%); }
603
  .speech-bubble.narration { background: #FAFAFA; border: 2px solid #BDBDBD; color: #424242; border-radius: 3px; }
604
  .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%; }
605
 
606
- .speech-bubble.thought::after { display: none; }
607
- .thought-dot { position: absolute; background-color: white; border: 2px solid #555; border-radius: 50%; z-index: -1; }
608
- .thought-dot-1 { width: 20px; height: 20px; bottom: -20px; left: 15px; }
609
- .thought-dot-2 { width: 12px; height: 12px; bottom: -32px; left: 5px; }
610
  .resize-handle { position: absolute; width: 10px; height: 10px; background: #2196F3; border: 1px solid white; border-radius: 50%; display: none; z-index: 11; }
611
  .speech-bubble.selected .resize-handle { display: block; }
612
  .resize-handle.se { bottom: -5px; right: -5px; cursor: se-resize; }
@@ -664,16 +673,15 @@ class EnhancedComicGenerator:
664
  <button onclick="addBubbleToPanel()" class="action-button">💬 Add Bubble</button>
665
  <button onclick="deleteBubble()" class="reset-button">🗑️ Delete Bubble</button>
666
  </div>
667
- <!-- <<< MODIFICATION: New Tail Controls Section >>> -->
668
  <div class="control-group" id="tail-controls" style="display: none;">
669
- <label>Tail Controls (Speech Only)</label>
670
  <div class="slider-group">
671
  <label for="tail-pos-slider">Position</label>
672
  <input type="range" id="tail-pos-slider" min="10" max="90" value="50" step="1">
673
  </div>
674
  <div class="slider-group">
675
  <label for="tail-size-slider">Size</label>
676
- <input type="range" id="tail-size-slider" min="10" max="40" value="20" step="1">
677
  </div>
678
  <button onclick="flipBubbleTail()" class="secondary-button">🔄 Flip Edge</button>
679
  </div>
@@ -769,7 +777,6 @@ class EnhancedComicGenerator:
769
  }
770
  });
771
 
772
- // <<< MODIFICATION: Event listeners for new tail sliders >>>
773
  document.getElementById('tail-pos-slider').addEventListener('input', (e) => {
774
  if (currentlySelectedBubble) {
775
  currentlySelectedBubble.style.setProperty('--tail-pos', e.target.value + '%');
@@ -777,6 +784,7 @@ class EnhancedComicGenerator:
777
  });
778
  document.getElementById('tail-size-slider').addEventListener('input', (e) => {
779
  if (currentlySelectedBubble) {
 
780
  currentlySelectedBubble.style.setProperty('--tail-size', e.target.value + 'px');
781
  }
782
  });
@@ -823,7 +831,7 @@ class EnhancedComicGenerator:
823
  }
824
 
825
  function applyBubbleType(bubble, type) {
826
- bubble.querySelectorAll('.thought-dot').forEach(el => el.remove());
827
  let classesToKeep = 'speech-bubble';
828
  if (bubble.classList.contains('selected')) classesToKeep += ' selected';
829
 
@@ -834,20 +842,24 @@ class EnhancedComicGenerator:
834
  if (type === 'speech') {
835
  bubble.classList.add('tail-bottom');
836
  bubble.dataset.tailEdge = 'bottom';
 
837
  }
838
  if (type === 'thought') {
839
- for (let i = 1; i <= 2; i++) {
840
- const dot = document.createElement('div');
841
- dot.className = `thought-dot thought-dot-${i}`;
842
- bubble.appendChild(dot);
843
- }
 
 
 
 
844
  }
845
  }
846
 
847
  function changeBubbleType(type) {
848
  if (!currentlySelectedBubble) return;
849
  applyBubbleType(currentlySelectedBubble, type);
850
- // Reselect bubble to update UI controls visibility
851
  selectBubble(currentlySelectedBubble);
852
  }
853
 
@@ -856,9 +868,10 @@ class EnhancedComicGenerator:
856
  currentlySelectedBubble.style.fontFamily = font;
857
  }
858
 
859
- // <<< MODIFICATION: Replaced rotate with flip function >>>
860
  function flipBubbleTail() {
861
- if (!currentlySelectedBubble || currentlySelectedBubble.dataset.type !== 'speech') return;
 
 
862
 
863
  const edges = ['bottom', 'right', 'top', 'left'];
864
  const currentEdge = currentlySelectedBubble.dataset.tailEdge || 'bottom';
@@ -869,6 +882,15 @@ class EnhancedComicGenerator:
869
 
870
  currentlySelectedBubble.classList.add('tail-' + edges[nextEdgeIndex]);
871
  currentlySelectedBubble.dataset.tailEdge = edges[nextEdgeIndex];
 
 
 
 
 
 
 
 
 
872
  }
873
 
874
  function selectPanel(panel) {
@@ -905,12 +927,11 @@ class EnhancedComicGenerator:
905
  document.getElementById('zoom-slider').disabled = true;
906
  bubbleControls.forEach(id => document.getElementById(id).disabled = false);
907
 
908
- // <<< MODIFICATION: Show/hide tail controls and set slider values >>>
909
- if (currentlySelectedBubble.dataset.type === 'speech') {
910
  tailControls.style.display = 'block';
911
- const currentSize = parseInt(styles.getPropertyValue('--tail-size')) || 20;
912
  document.getElementById('tail-size-slider').value = currentSize;
913
-
914
  const currentPos = parseInt(styles.getPropertyValue('--tail-pos')) || 50;
915
  document.getElementById('tail-pos-slider').value = currentPos;
916
  } else {
@@ -955,11 +976,25 @@ class EnhancedComicGenerator:
955
  offset = { x: e.clientX - rect.left, y: e.clientY - rect.top };
956
  }
957
 
 
958
  function drag(e) {
959
  if (!draggedBubble) return;
960
- const parentRect = draggedBubble.parentElement.getBoundingClientRect();
961
- draggedBubble.style.left = `${e.clientX - parentRect.left - offset.x}px`;
962
- draggedBubble.style.top = `${e.clientY - parentRect.top - offset.y}px`;
 
 
 
 
 
 
 
 
 
 
 
 
 
963
  }
964
 
965
  function stopDrag() { draggedBubble = null; }
 
539
  .panel img.pannable { cursor: grab; }
540
  .panel img.panning { cursor: grabbing; }
541
  .speech-bubble { position: absolute; display: flex; justify-content: center; align-items: center; width: 150px; height: 80px; min-width: 50px; min-height: 30px; box-sizing: border-box; z-index: 10; cursor: move; overflow: visible; font-size: 13px; font-weight: bold; text-align: center; font-family: 'Comic Neue', cursive; }
542
+ .bubble-text { padding: 0.8em; word-wrap: break-word; width: 100%; height: 100%; box-sizing: border-box; }
543
  .speech-bubble.selected { outline: 2px dashed #4CAF50; }
544
  .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; }
545
 
546
+ /* <<< MODIFICATION START: New Advanced CSS for Speech Bubble >>> */
547
  .speech-bubble.speech {
548
+ --r: 1em; /* radius */
549
+ --b: var(--tail-size, 2.5em); /* tail base size */
550
+ --c1: var(--bubble-border-color, #4A4A4A);
551
+ --c2: var(--bubble-fill-color, white);
552
  padding: 0;
553
  border: none;
554
+ color: var(--bubble-text-color, #333);
555
+ background: none;
556
+
557
+ /* Dynamic clip-path for tail cutout */
558
+ clip-path: polygon(
559
+ var(--r) 0, calc(100% - var(--r)) 0, 100% var(--r), 100% calc(100% - var(--r)),
560
+ calc(100% - var(--r)) 100%,
561
+ calc(clamp(var(--r), var(--tail-pos, 50%), calc(100% - var(--r))) + var(--b)/2) 100%,
562
+ clamp(var(--r), var(--tail-pos, 50%), calc(100% - var(--r))) calc(100% + var(--b)/2),
563
+ calc(clamp(var(--r), var(--tail-pos, 50%), calc(100% - var(--r))) - var(--b)/2) 100%,
564
+ var(--r) 100%, 0 calc(100% - var(--r)), 0 var(--r)
565
+ );
566
+
567
+ border-image: conic-gradient(var(--c1) 0 0) fill 1/2px; /* Inner border */
568
  }
569
+ .speech-bubble.speech::before {
 
570
  content: '';
571
  position: absolute;
572
+ inset: 0;
573
+ border-radius: var(--r);
574
+ background: var(--c2); /* Main fill color */
575
+ z-index: -1;
 
 
576
  }
577
 
578
+ /* --- 4-WAY ROTATION --- */
579
+ .speech-bubble.speech.tail-top { transform: rotate(180deg); }
580
+ .speech-bubble.speech.tail-left { transform: rotate(90deg); }
581
+ .speech-bubble.speech.tail-right { transform: rotate(-90deg); }
582
+ /* The text needs to be counter-rotated to stay upright */
583
+ .speech-bubble.speech.tail-top .bubble-text,
584
+ .speech-bubble.speech.tail-left .bubble-text,
585
+ .speech-bubble.speech.tail-right .bubble-text {
586
+ transform: rotate(var(--un-rotate, 0deg));
587
  }
588
+
589
+ /* --- Other Bubble Types --- */
590
+ .speech-bubble.thought {
591
+ background: var(--bubble-fill-color, white);
592
+ color: var(--bubble-text-color, #333);
593
+ border: 2px dashed var(--bubble-border-color, #555);
594
+ border-radius: 50%;
595
+ padding: 0;
 
 
 
 
 
 
 
596
  }
597
+ .thought-dot {
598
+ position: absolute;
599
+ background-color: var(--bubble-fill-color, white);
600
+ border: 2px dashed var(--bubble-border-color, #555);
601
+ border-radius: 50%;
602
+ z-index: -1;
 
603
  }
604
+ /* Tail controls for Thought Bubble */
605
+ .speech-bubble.thought.tail-bottom .thought-dot-1 { bottom: -30%; left: var(--tail-pos, 50%); width: var(--tail-size, 20px); height: var(--tail-size, 20px); transform: translateX(-50%); }
606
+ .speech-bubble.thought.tail-bottom .thought-dot-2 { bottom: -50%; left: calc(var(--tail-pos, 50%) - 15%); width: calc(var(--tail-size, 20px) * 0.6); height: calc(var(--tail-size, 20px) * 0.6); transform: translateX(-50%); }
607
+ .speech-bubble.thought.tail-top .thought-dot-1 { top: -30%; left: var(--tail-pos, 50%); width: var(--tail-size, 20px); height: var(--tail-size, 20px); transform: translateX(-50%); }
608
+ .speech-bubble.thought.tail-top .thought-dot-2 { top: -50%; left: calc(var(--tail-pos, 50%) - 15%); width: calc(var(--tail-size, 20px) * 0.6); height: calc(var(--tail-size, 20px) * 0.6); transform: translateX(-50%); }
609
+ .speech-bubble.thought.tail-right .thought-dot-1 { right: -30%; top: var(--tail-pos, 50%); width: var(--tail-size, 20px); height: var(--tail-size, 20px); transform: translateY(-50%); }
610
+ .speech-bubble.thought.tail-right .thought-dot-2 { right: -50%; top: calc(var(--tail-pos, 50%) - 15%); width: calc(var(--tail-size, 20px) * 0.6); height: calc(var(--tail-size, 20px) * 0.6); transform: translateY(-50%); }
611
+ .speech-bubble.thought.tail-left .thought-dot-1 { left: -30%; top: var(--tail-pos, 50%); width: var(--tail-size, 20px); height: var(--tail-size, 20px); transform: translateY(-50%); }
612
+ .speech-bubble.thought.tail-left .thought-dot-2 { left: -50%; top: calc(var(--tail-pos, 50%) - 15%); width: calc(var(--tail-size, 20px) * 0.6); height: calc(var(--tail-size, 20px) * 0.6); transform: translateY(-50%); }
613
  /* <<< MODIFICATION END >>> */
614
 
 
615
  .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%); }
616
  .speech-bubble.narration { background: #FAFAFA; border: 2px solid #BDBDBD; color: #424242; border-radius: 3px; }
617
  .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%; }
618
 
 
 
 
 
619
  .resize-handle { position: absolute; width: 10px; height: 10px; background: #2196F3; border: 1px solid white; border-radius: 50%; display: none; z-index: 11; }
620
  .speech-bubble.selected .resize-handle { display: block; }
621
  .resize-handle.se { bottom: -5px; right: -5px; cursor: se-resize; }
 
673
  <button onclick="addBubbleToPanel()" class="action-button">💬 Add Bubble</button>
674
  <button onclick="deleteBubble()" class="reset-button">🗑️ Delete Bubble</button>
675
  </div>
 
676
  <div class="control-group" id="tail-controls" style="display: none;">
677
+ <label>Tail Controls</label>
678
  <div class="slider-group">
679
  <label for="tail-pos-slider">Position</label>
680
  <input type="range" id="tail-pos-slider" min="10" max="90" value="50" step="1">
681
  </div>
682
  <div class="slider-group">
683
  <label for="tail-size-slider">Size</label>
684
+ <input type="range" id="tail-size-slider" min="10" max="40" value="25" step="1">
685
  </div>
686
  <button onclick="flipBubbleTail()" class="secondary-button">🔄 Flip Edge</button>
687
  </div>
 
777
  }
778
  });
779
 
 
780
  document.getElementById('tail-pos-slider').addEventListener('input', (e) => {
781
  if (currentlySelectedBubble) {
782
  currentlySelectedBubble.style.setProperty('--tail-pos', e.target.value + '%');
 
784
  });
785
  document.getElementById('tail-size-slider').addEventListener('input', (e) => {
786
  if (currentlySelectedBubble) {
787
+ // For speech bubble, it's the base width 'b'. For thought, it's the dot size.
788
  currentlySelectedBubble.style.setProperty('--tail-size', e.target.value + 'px');
789
  }
790
  });
 
831
  }
832
 
833
  function applyBubbleType(bubble, type) {
834
+ bubble.innerHTML = bubble.querySelector('.bubble-text').outerHTML; // Clear old pseudo-elements
835
  let classesToKeep = 'speech-bubble';
836
  if (bubble.classList.contains('selected')) classesToKeep += ' selected';
837
 
 
842
  if (type === 'speech') {
843
  bubble.classList.add('tail-bottom');
844
  bubble.dataset.tailEdge = 'bottom';
845
+ bubble.style.setProperty('--un-rotate', '0deg'); // Reset rotation
846
  }
847
  if (type === 'thought') {
848
+ bubble.classList.add('tail-bottom');
849
+ bubble.dataset.tailEdge = 'bottom';
850
+ // Add the dots for the thought bubble tail
851
+ const dot1 = document.createElement('div');
852
+ dot1.className = 'thought-dot thought-dot-1';
853
+ const dot2 = document.createElement('div');
854
+ dot2.className = 'thought-dot thought-dot-2';
855
+ bubble.appendChild(dot1);
856
+ bubble.appendChild(dot2);
857
  }
858
  }
859
 
860
  function changeBubbleType(type) {
861
  if (!currentlySelectedBubble) return;
862
  applyBubbleType(currentlySelectedBubble, type);
 
863
  selectBubble(currentlySelectedBubble);
864
  }
865
 
 
868
  currentlySelectedBubble.style.fontFamily = font;
869
  }
870
 
 
871
  function flipBubbleTail() {
872
+ if (!currentlySelectedBubble) return;
873
+ const type = currentlySelectedBubble.dataset.type;
874
+ if (type !== 'speech' && type !== 'thought') return;
875
 
876
  const edges = ['bottom', 'right', 'top', 'left'];
877
  const currentEdge = currentlySelectedBubble.dataset.tailEdge || 'bottom';
 
882
 
883
  currentlySelectedBubble.classList.add('tail-' + edges[nextEdgeIndex]);
884
  currentlySelectedBubble.dataset.tailEdge = edges[nextEdgeIndex];
885
+
886
+ // Handle counter-rotation for speech bubble text
887
+ if (type === 'speech') {
888
+ let rotation = '0deg';
889
+ if (edges[nextEdgeIndex] === 'top') rotation = '180deg';
890
+ if (edges[nextEdgeIndex] === 'left') rotation = '-90deg';
891
+ if (edges[nextEdgeIndex] === 'right') rotation = '90deg';
892
+ currentlySelectedBubble.style.setProperty('--un-rotate', rotation);
893
+ }
894
  }
895
 
896
  function selectPanel(panel) {
 
927
  document.getElementById('zoom-slider').disabled = true;
928
  bubbleControls.forEach(id => document.getElementById(id).disabled = false);
929
 
930
+ const type = currentlySelectedBubble.dataset.type;
931
+ if (type === 'speech' || type === 'thought') {
932
  tailControls.style.display = 'block';
933
+ const currentSize = parseFloat(styles.getPropertyValue('--tail-size')) || (type === 'speech' ? 40 : 20) ; // 40px base, 20px dot
934
  document.getElementById('tail-size-slider').value = currentSize;
 
935
  const currentPos = parseInt(styles.getPropertyValue('--tail-pos')) || 50;
936
  document.getElementById('tail-pos-slider').value = currentPos;
937
  } else {
 
976
  offset = { x: e.clientX - rect.left, y: e.clientY - rect.top };
977
  }
978
 
979
+ // <<< MODIFICATION: Drag function now constrains movement to fix edge bug >>>
980
  function drag(e) {
981
  if (!draggedBubble) return;
982
+ const parentPanel = draggedBubble.parentElement;
983
+ const parentRect = parentPanel.getBoundingClientRect();
984
+
985
+ let x = e.clientX - parentRect.left - offset.x;
986
+ let y = e.clientY - parentRect.top - offset.y;
987
+
988
+ // Constrain X position
989
+ x = Math.max(0, x); // Min-x
990
+ x = Math.min(parentRect.width - draggedBubble.offsetWidth, x); // Max-x
991
+
992
+ // Constrain Y position
993
+ y = Math.max(0, y); // Min-y
994
+ y = Math.min(parentRect.height - draggedBubble.offsetHeight, y); // Max-y
995
+
996
+ draggedBubble.style.left = `${x}px`;
997
+ draggedBubble.style.top = `${y}px`;
998
  }
999
 
1000
  function stopDrag() { draggedBubble = null; }