Update app_enhanced.py
Browse files- app_enhanced.py +116 -88
app_enhanced.py
CHANGED
|
@@ -543,30 +543,101 @@ class EnhancedComicGenerator:
|
|
| 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
|
| 547 |
.speech-bubble.speech {
|
| 548 |
background: var(--bubble-fill-color, white);
|
| 549 |
color: var(--bubble-text-color, #333);
|
| 550 |
-
border
|
|
|
|
| 551 |
padding: 0;
|
| 552 |
-
border: none;
|
| 553 |
-
box-shadow: 0 1px 4px rgba(0,0,0,0.25);
|
| 554 |
}
|
| 555 |
|
| 556 |
-
.speech-bubble.speech
|
| 557 |
content: '';
|
| 558 |
position: absolute;
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 570 |
}
|
| 571 |
/* <<< MODIFICATION END >>> */
|
| 572 |
|
|
@@ -605,8 +676,6 @@ class EnhancedComicGenerator:
|
|
| 605 |
.color-picker-grid div { text-align: center; }
|
| 606 |
.color-picker-grid label { font-size: 11px; }
|
| 607 |
.color-picker-grid input[type="color"] { height: 25px; padding: 2px; }
|
| 608 |
-
.slider-group { margin-top: 5px; }
|
| 609 |
-
.slider-group label { font-size: 11px; margin-right: 5px; }
|
| 610 |
</style>
|
| 611 |
</head>
|
| 612 |
<body>
|
|
@@ -641,21 +710,9 @@ class EnhancedComicGenerator:
|
|
| 641 |
<button onclick="addBubbleToPanel()" class="action-button">💬 Add Bubble</button>
|
| 642 |
<button onclick="deleteBubble()" class="reset-button">🗑️ Delete Bubble</button>
|
| 643 |
</div>
|
| 644 |
-
<!-- <<< MODIFICATION: New Tail Controls Section >>> -->
|
| 645 |
<div class="control-group" id="tail-controls" style="display: none;">
|
| 646 |
<label>Tail Controls</label>
|
| 647 |
-
|
| 648 |
-
<label for="tail-pos-slider">Position</label>
|
| 649 |
-
<input type="range" id="tail-pos-slider" min="0" max="100" value="12" step="1">
|
| 650 |
-
</div>
|
| 651 |
-
<div class="slider-group">
|
| 652 |
-
<label for="tail-size-slider">Size</label>
|
| 653 |
-
<input type="range" id="tail-size-slider" min="15" max="50" value="25" step="1">
|
| 654 |
-
</div>
|
| 655 |
-
<div class="slider-group">
|
| 656 |
-
<label for="tail-shape-slider">Shape</label>
|
| 657 |
-
<input type="range" id="tail-shape-slider" min="0" max="100" value="50" step="1">
|
| 658 |
-
</div>
|
| 659 |
</div>
|
| 660 |
<div class="control-group">
|
| 661 |
<label>Panel Tools (Select Panel):</label>
|
|
@@ -748,21 +805,6 @@ class EnhancedComicGenerator:
|
|
| 748 |
currentlySelectedBubble.style.setProperty('--bubble-fill-color', e.target.value);
|
| 749 |
}
|
| 750 |
});
|
| 751 |
-
|
| 752 |
-
// <<< MODIFICATION: Event listeners for new tail sliders >>>
|
| 753 |
-
document.getElementById('tail-pos-slider').addEventListener('input', (e) => {
|
| 754 |
-
if (currentlySelectedBubble) updateTailPosition(e.target.value);
|
| 755 |
-
});
|
| 756 |
-
document.getElementById('tail-size-slider').addEventListener('input', (e) => {
|
| 757 |
-
if (currentlySelectedBubble) {
|
| 758 |
-
currentlySelectedBubble.style.setProperty('--tail-size', e.target.value + 'px');
|
| 759 |
-
}
|
| 760 |
-
});
|
| 761 |
-
document.getElementById('tail-shape-slider').addEventListener('input', (e) => {
|
| 762 |
-
if (currentlySelectedBubble) {
|
| 763 |
-
currentlySelectedBubble.style.setProperty('--tail-shape', e.target.value + '%');
|
| 764 |
-
}
|
| 765 |
-
});
|
| 766 |
|
| 767 |
document.addEventListener('mousemove', e => { if (isPanning) panImage(e); if (draggedBubble) drag(e); if(isResizing) resizeBubble(e); });
|
| 768 |
document.addEventListener('mouseup', e => { if (isPanning) stopPan(e); if (draggedBubble) stopDrag(e); if(isResizing) stopResize(e);});
|
|
@@ -791,8 +833,6 @@ class EnhancedComicGenerator:
|
|
| 791 |
bubbleDiv.style.left = data.left;
|
| 792 |
bubbleDiv.style.top = data.top;
|
| 793 |
applyBubbleType(bubbleDiv, 'speech');
|
| 794 |
-
// Set initial tail position on creation
|
| 795 |
-
updateTailPosition(12, bubbleDiv); // 12 is bottom-left quadrant
|
| 796 |
return bubbleDiv;
|
| 797 |
}
|
| 798 |
|
|
@@ -816,6 +856,10 @@ class EnhancedComicGenerator:
|
|
| 816 |
bubble.classList.add(type);
|
| 817 |
bubble.dataset.type = type;
|
| 818 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 819 |
if (type === 'thought') {
|
| 820 |
for (let i = 1; i <= 2; i++) {
|
| 821 |
const dot = document.createElement('div');
|
|
@@ -835,39 +879,28 @@ class EnhancedComicGenerator:
|
|
| 835 |
if (!currentlySelectedBubble) return;
|
| 836 |
currentlySelectedBubble.style.fontFamily = font;
|
| 837 |
}
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
const
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
left = `${mappedVal}%`;
|
| 860 |
-
rotation = '225deg';
|
| 861 |
-
} else { // Left Edge
|
| 862 |
-
const mappedVal = ((val - 75) / 25) * 100;
|
| 863 |
-
top = `${mappedVal}%`;
|
| 864 |
-
left = '0%';
|
| 865 |
-
rotation = '315deg';
|
| 866 |
}
|
| 867 |
-
|
| 868 |
-
bubble.style.setProperty('--tail-top', top);
|
| 869 |
-
bubble.style.setProperty('--tail-left', left);
|
| 870 |
-
bubble.style.setProperty('--tail-rotation', `rotate(${rotation})`);
|
| 871 |
}
|
| 872 |
|
| 873 |
function selectPanel(panel) {
|
|
@@ -903,15 +936,10 @@ class EnhancedComicGenerator:
|
|
| 903 |
|
| 904 |
document.getElementById('zoom-slider').disabled = true;
|
| 905 |
bubbleControls.forEach(id => document.getElementById(id).disabled = false);
|
| 906 |
-
|
| 907 |
-
|
|
|
|
| 908 |
tailControls.style.display = 'block';
|
| 909 |
-
const currentSize = parseInt(styles.getPropertyValue('--tail-size')) || 25;
|
| 910 |
-
document.getElementById('tail-size-slider').value = currentSize;
|
| 911 |
-
const currentShape = parseInt(styles.getPropertyValue('--tail-shape')) || 50;
|
| 912 |
-
document.getElementById('tail-shape-slider').value = currentShape;
|
| 913 |
-
const currentPos = currentlySelectedBubble.dataset.tailCircularPos || 12;
|
| 914 |
-
document.getElementById('tail-pos-slider').value = currentPos;
|
| 915 |
} else {
|
| 916 |
tailControls.style.display = 'none';
|
| 917 |
}
|
|
|
|
| 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 Robust Bordered Bubble CSS >>> */
|
| 547 |
.speech-bubble.speech {
|
| 548 |
background: var(--bubble-fill-color, white);
|
| 549 |
color: var(--bubble-text-color, #333);
|
| 550 |
+
border: 2px solid var(--bubble-border-color, #333);
|
| 551 |
+
border-radius: 10px;
|
| 552 |
padding: 0;
|
|
|
|
|
|
|
| 553 |
}
|
| 554 |
|
| 555 |
+
.speech-bubble.speech:after, .speech-bubble.speech:before {
|
| 556 |
content: '';
|
| 557 |
position: absolute;
|
| 558 |
+
display: block;
|
| 559 |
+
width: 0;
|
| 560 |
+
}
|
| 561 |
+
|
| 562 |
+
/* 4-WAY TAIL ROTATION CLASSES */
|
| 563 |
+
/* BOTTOM */
|
| 564 |
+
.speech-bubble.speech.tail-bottom:after {
|
| 565 |
+
border-style: solid;
|
| 566 |
+
border-width: 15px 15px 0 0;
|
| 567 |
+
border-color: var(--bubble-fill-color, white) transparent transparent transparent;
|
| 568 |
+
bottom: -15px;
|
| 569 |
+
left: 50%;
|
| 570 |
+
transform: translateX(-50%);
|
| 571 |
+
z-index: 1;
|
| 572 |
+
}
|
| 573 |
+
.speech-bubble.speech.tail-bottom:before {
|
| 574 |
+
border-style: solid;
|
| 575 |
+
border-width: 17px 17px 0 0;
|
| 576 |
+
border-color: var(--bubble-border-color, #333) transparent transparent transparent;
|
| 577 |
+
bottom: -19px;
|
| 578 |
+
left: 50%;
|
| 579 |
+
transform: translateX(-50%);
|
| 580 |
+
z-index: 0;
|
| 581 |
+
}
|
| 582 |
+
|
| 583 |
+
/* TOP */
|
| 584 |
+
.speech-bubble.speech.tail-top:after {
|
| 585 |
+
border-style: solid;
|
| 586 |
+
border-width: 0 15px 15px 0;
|
| 587 |
+
border-color: transparent var(--bubble-fill-color, white) transparent transparent;
|
| 588 |
+
top: -15px;
|
| 589 |
+
left: 50%;
|
| 590 |
+
transform: translateX(-50%) rotate(180deg);
|
| 591 |
+
z-index: 1;
|
| 592 |
+
}
|
| 593 |
+
.speech-bubble.speech.tail-top:before {
|
| 594 |
+
border-style: solid;
|
| 595 |
+
border-width: 0 17px 17px 0;
|
| 596 |
+
border-color: transparent var(--bubble-border-color, #333) transparent transparent;
|
| 597 |
+
top: -19px;
|
| 598 |
+
left: 50%;
|
| 599 |
+
transform: translateX(-50%) rotate(180deg);
|
| 600 |
+
z-index: 0;
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
/* RIGHT */
|
| 604 |
+
.speech-bubble.speech.tail-right:after {
|
| 605 |
+
border-style: solid;
|
| 606 |
+
border-width: 15px 15px 0 0;
|
| 607 |
+
border-color: var(--bubble-fill-color, white) transparent transparent transparent;
|
| 608 |
+
top: 50%;
|
| 609 |
+
right: -15px;
|
| 610 |
+
transform: translateY(-50%) rotate(90deg);
|
| 611 |
+
z-index: 1;
|
| 612 |
+
}
|
| 613 |
+
.speech-bubble.speech.tail-right:before {
|
| 614 |
+
border-style: solid;
|
| 615 |
+
border-width: 17px 17px 0 0;
|
| 616 |
+
border-color: var(--bubble-border-color, #333) transparent transparent transparent;
|
| 617 |
+
top: 50%;
|
| 618 |
+
right: -19px;
|
| 619 |
+
transform: translateY(-50%) rotate(90deg);
|
| 620 |
+
z-index: 0;
|
| 621 |
+
}
|
| 622 |
+
|
| 623 |
+
/* LEFT */
|
| 624 |
+
.speech-bubble.speech.tail-left:after {
|
| 625 |
+
border-style: solid;
|
| 626 |
+
border-width: 15px 15px 0 0;
|
| 627 |
+
border-color: var(--bubble-fill-color, white) transparent transparent transparent;
|
| 628 |
+
top: 50%;
|
| 629 |
+
left: -15px;
|
| 630 |
+
transform: translateY(-50%) rotate(-90deg);
|
| 631 |
+
z-index: 1;
|
| 632 |
+
}
|
| 633 |
+
.speech-bubble.speech.tail-left:before {
|
| 634 |
+
border-style: solid;
|
| 635 |
+
border-width: 17px 17px 0 0;
|
| 636 |
+
border-color: var(--bubble-border-color, #333) transparent transparent transparent;
|
| 637 |
+
top: 50%;
|
| 638 |
+
left: -19px;
|
| 639 |
+
transform: translateY(-50%) rotate(-90deg);
|
| 640 |
+
z-index: 0;
|
| 641 |
}
|
| 642 |
/* <<< MODIFICATION END >>> */
|
| 643 |
|
|
|
|
| 676 |
.color-picker-grid div { text-align: center; }
|
| 677 |
.color-picker-grid label { font-size: 11px; }
|
| 678 |
.color-picker-grid input[type="color"] { height: 25px; padding: 2px; }
|
|
|
|
|
|
|
| 679 |
</style>
|
| 680 |
</head>
|
| 681 |
<body>
|
|
|
|
| 710 |
<button onclick="addBubbleToPanel()" class="action-button">💬 Add Bubble</button>
|
| 711 |
<button onclick="deleteBubble()" class="reset-button">🗑️ Delete Bubble</button>
|
| 712 |
</div>
|
|
|
|
| 713 |
<div class="control-group" id="tail-controls" style="display: none;">
|
| 714 |
<label>Tail Controls</label>
|
| 715 |
+
<button onclick="rotateBubbleTail()" class="secondary-button">🔄 Rotate Tail</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 716 |
</div>
|
| 717 |
<div class="control-group">
|
| 718 |
<label>Panel Tools (Select Panel):</label>
|
|
|
|
| 805 |
currentlySelectedBubble.style.setProperty('--bubble-fill-color', e.target.value);
|
| 806 |
}
|
| 807 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 808 |
|
| 809 |
document.addEventListener('mousemove', e => { if (isPanning) panImage(e); if (draggedBubble) drag(e); if(isResizing) resizeBubble(e); });
|
| 810 |
document.addEventListener('mouseup', e => { if (isPanning) stopPan(e); if (draggedBubble) stopDrag(e); if(isResizing) stopResize(e);});
|
|
|
|
| 833 |
bubbleDiv.style.left = data.left;
|
| 834 |
bubbleDiv.style.top = data.top;
|
| 835 |
applyBubbleType(bubbleDiv, 'speech');
|
|
|
|
|
|
|
| 836 |
return bubbleDiv;
|
| 837 |
}
|
| 838 |
|
|
|
|
| 856 |
bubble.classList.add(type);
|
| 857 |
bubble.dataset.type = type;
|
| 858 |
|
| 859 |
+
if (type === 'speech') {
|
| 860 |
+
bubble.classList.add('tail-bottom');
|
| 861 |
+
bubble.dataset.tailPos = '0';
|
| 862 |
+
}
|
| 863 |
if (type === 'thought') {
|
| 864 |
for (let i = 1; i <= 2; i++) {
|
| 865 |
const dot = document.createElement('div');
|
|
|
|
| 879 |
if (!currentlySelectedBubble) return;
|
| 880 |
currentlySelectedBubble.style.fontFamily = font;
|
| 881 |
}
|
| 882 |
+
|
| 883 |
+
function rotateBubbleTail() {
|
| 884 |
+
if (!currentlySelectedBubble) return;
|
| 885 |
+
const bubbleType = currentlySelectedBubble.dataset.type;
|
| 886 |
+
|
| 887 |
+
if (bubbleType === 'speech') {
|
| 888 |
+
const positions = ['tail-bottom', 'tail-right', 'tail-top', 'tail-left'];
|
| 889 |
+
let currentPos = parseInt(currentlySelectedBubble.dataset.tailPos || 0);
|
| 890 |
+
currentlySelectedBubble.classList.remove(positions[currentPos]);
|
| 891 |
+
let nextPos = (currentPos + 1) % positions.length;
|
| 892 |
+
currentlySelectedBubble.classList.add(positions[nextPos]);
|
| 893 |
+
currentlySelectedBubble.dataset.tailPos = nextPos;
|
| 894 |
+
} else if (bubbleType === 'thought') {
|
| 895 |
+
const isFlippedH = currentlySelectedBubble.classList.contains('flipped');
|
| 896 |
+
const isFlippedV = currentlySelectedBubble.classList.contains('flipped-vertical');
|
| 897 |
+
if (!isFlippedH && !isFlippedV) { currentlySelectedBubble.classList.add('flipped'); }
|
| 898 |
+
else if (isFlippedH && !isFlippedV) { currentlySelectedBubble.classList.add('flipped-vertical'); }
|
| 899 |
+
else if (isFlippedH && isFlippedV) { currentlySelectedBubble.classList.remove('flipped'); }
|
| 900 |
+
else { currentlySelectedBubble.classList.remove('flipped-vertical'); }
|
| 901 |
+
} else {
|
| 902 |
+
alert("Tail rotation is only available for Speech and Thought bubbles.");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 903 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 904 |
}
|
| 905 |
|
| 906 |
function selectPanel(panel) {
|
|
|
|
| 936 |
|
| 937 |
document.getElementById('zoom-slider').disabled = true;
|
| 938 |
bubbleControls.forEach(id => document.getElementById(id).disabled = false);
|
| 939 |
+
|
| 940 |
+
const bubbleType = currentlySelectedBubble.dataset.type;
|
| 941 |
+
if (bubbleType === 'speech' || bubbleType === 'thought') {
|
| 942 |
tailControls.style.display = 'block';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 943 |
} else {
|
| 944 |
tailControls.style.display = 'none';
|
| 945 |
}
|