Update app_enhanced.py
Browse files- app_enhanced.py +118 -45
app_enhanced.py
CHANGED
|
@@ -14,6 +14,7 @@ from typing import List
|
|
| 14 |
import traceback
|
| 15 |
|
| 16 |
# --- ROBUST IMPORTS WITH FALLBACKS ---
|
|
|
|
| 17 |
try:
|
| 18 |
from backend.keyframes.keyframes import black_bar_crop
|
| 19 |
print("✅ Black bar cropping module loaded.")
|
|
@@ -28,9 +29,8 @@ try:
|
|
| 28 |
except Exception as e:
|
| 29 |
print(f"⚠️ Could not load SimpleColorEnhancer: {e}. This feature will be SKIPPED.")
|
| 30 |
class SimpleColorEnhancer:
|
| 31 |
-
def enhance_batch(self, *args, **kwargs):
|
| 32 |
-
|
| 33 |
-
pass
|
| 34 |
|
| 35 |
try:
|
| 36 |
from backend.quality_color_enhancer import QualityColorEnhancer
|
|
@@ -38,9 +38,9 @@ try:
|
|
| 38 |
except Exception as e:
|
| 39 |
print(f"⚠️ Could not load QualityColorEnhancer: {e}. This feature will be SKIPPED.")
|
| 40 |
class QualityColorEnhancer:
|
| 41 |
-
def batch_enhance(self, *args, **kwargs):
|
| 42 |
-
|
| 43 |
-
|
| 44 |
|
| 45 |
try:
|
| 46 |
from backend.class_def import bubble, panel, Page
|
|
@@ -306,14 +306,14 @@ class EnhancedComicGenerator:
|
|
| 306 |
cap.release()
|
| 307 |
if not ret or frame is None:
|
| 308 |
return {"success": False, "message": f"No frame at {target_time:.2f}s."}
|
|
|
|
| 309 |
new_path = os.path.join(self.frames_dir, frame_filename)
|
| 310 |
cv2.imwrite(new_path, frame)
|
| 311 |
|
| 312 |
-
#
|
| 313 |
print(f"🎨 Applying enhancements to the new frame: {frame_filename}")
|
| 314 |
self._enhance_all_images(single_image_path=new_path)
|
| 315 |
self._enhance_quality_colors(single_image_path=new_path)
|
| 316 |
-
# <<< MODIFICATION END >>>
|
| 317 |
|
| 318 |
if isinstance(frame_to_time[frame_filename], dict):
|
| 319 |
frame_to_time[frame_filename]['time'] = target_time
|
|
@@ -343,14 +343,14 @@ class EnhancedComicGenerator:
|
|
| 343 |
ret, frame = cap.read()
|
| 344 |
cap.release()
|
| 345 |
if not ret or frame is None: return {"success": False, "message": f"Could not retrieve frame at {timestamp_seconds:.2f}s."}
|
|
|
|
| 346 |
new_path = os.path.join(self.frames_dir, frame_filename)
|
| 347 |
cv2.imwrite(new_path, frame)
|
| 348 |
-
|
| 349 |
-
#
|
| 350 |
print(f"🎨 Applying enhancements to the new frame from timestamp: {frame_filename}")
|
| 351 |
self._enhance_all_images(single_image_path=new_path)
|
| 352 |
self._enhance_quality_colors(single_image_path=new_path)
|
| 353 |
-
# <<< MODIFICATION END >>>
|
| 354 |
|
| 355 |
with open(metadata_path, 'r') as f: frame_to_time = json.load(f)
|
| 356 |
if frame_filename in frame_to_time:
|
|
@@ -437,20 +437,42 @@ class EnhancedComicGenerator:
|
|
| 437 |
update_status(f"Error: {e}", -1)
|
| 438 |
return False
|
| 439 |
|
|
|
|
| 440 |
def _enhance_all_images(self, single_image_path=None):
|
| 441 |
-
|
| 442 |
-
if single_image_path: target_dir = os.path.dirname(single_image_path)
|
| 443 |
-
if not os.path.exists(target_dir): return
|
| 444 |
try:
|
| 445 |
-
SimpleColorEnhancer()
|
| 446 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 447 |
|
| 448 |
def _enhance_quality_colors(self, single_image_path=None):
|
| 449 |
-
|
| 450 |
-
if single_image_path: target_dir = os.path.dirname(single_image_path)
|
| 451 |
try:
|
| 452 |
-
QualityColorEnhancer()
|
| 453 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 454 |
|
| 455 |
def _create_ai_bubbles_from_moments(self, black_x, black_y):
|
| 456 |
bubbles, frame_files = [], sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')])
|
|
@@ -502,7 +524,6 @@ class EnhancedComicGenerator:
|
|
| 502 |
|
| 503 |
def _copy_template_files(self):
|
| 504 |
try:
|
| 505 |
-
# <<< MODIFICATION START: Updated CSS rules for a longer bubble tail >>>
|
| 506 |
template_html = '''<!DOCTYPE html>
|
| 507 |
<html lang="en">
|
| 508 |
<head>
|
|
@@ -524,30 +545,50 @@ class EnhancedComicGenerator:
|
|
| 524 |
.panel img { width: 100%; height: 100%; object-fit: cover; object-position: center; transition: transform 0.1s ease-out; }
|
| 525 |
.panel img.pannable { cursor: grab; }
|
| 526 |
.panel img.panning { cursor: grabbing; }
|
| 527 |
-
.speech-bubble {
|
| 528 |
.bubble-text { padding: 2px; word-wrap: break-word; }
|
| 529 |
.speech-bubble.selected { outline: 2px dashed #4CAF50; }
|
| 530 |
.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; }
|
| 531 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 532 |
.speech-bubble.thought { background: white; border: 2px dashed #555; color: #333; border-radius: 50%; }
|
| 533 |
.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%); }
|
| 534 |
.speech-bubble.narration { background: #FAFAFA; border: 2px solid #BDBDBD; color: #424242; border-radius: 3px; }
|
| 535 |
.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%; }
|
| 536 |
-
.speech-bubble.
|
| 537 |
-
|
| 538 |
-
.speech-bubble.speech::after { border-top: 20px solid #333; bottom: -19px; left: 20px; }
|
| 539 |
-
.speech-bubble.idea::after { border-top: 20px solid #FFA500; bottom: -19px; left: 20px; }
|
| 540 |
.speech-bubble.thought::after { display: none; }
|
| 541 |
.thought-dot { position: absolute; background-color: white; border: 2px solid #555; border-radius: 50%; z-index: -1; }
|
| 542 |
.thought-dot-1 { width: 20px; height: 20px; bottom: -20px; left: 15px; }
|
| 543 |
.thought-dot-2 { width: 12px; height: 12px; bottom: -32px; left: 5px; }
|
| 544 |
-
.speech-bubble.flipped.speech::after, .speech-bubble.flipped.idea::after { left: auto; right: 20px; }
|
| 545 |
-
.speech-bubble.flipped.thought .thought-dot-1 { left: auto; right: 15px; }
|
| 546 |
-
.speech-bubble.flipped.thought .thought-dot-2 { left: auto; right: 5px; }
|
| 547 |
-
/* <<< CSS MODIFICATION: Adjusted vertically flipped tail position >>> */
|
| 548 |
-
.speech-bubble.flipped-vertical.speech::after, .speech-bubble.flipped-vertical.idea::after { bottom: auto; top: -19px; transform: rotate(180deg); }
|
| 549 |
-
.speech-bubble.flipped-vertical.thought .thought-dot-1 { bottom: auto; top: -20px; }
|
| 550 |
-
.speech-bubble.flipped-vertical.thought .thought-dot-2 { bottom: auto; top: -32px; }
|
| 551 |
.resize-handle { position: absolute; width: 10px; height: 10px; background: #2196F3; border: 1px solid white; border-radius: 50%; display: none; z-index: 11; }
|
| 552 |
.speech-bubble.selected .resize-handle { display: block; }
|
| 553 |
.resize-handle.se { bottom: -5px; right: -5px; cursor: se-resize; }
|
|
@@ -691,7 +732,12 @@ class EnhancedComicGenerator:
|
|
| 691 |
if(currentlySelectedBubble) currentlySelectedBubble.style.color = e.target.value;
|
| 692 |
});
|
| 693 |
document.getElementById('bubble-fill-color').addEventListener('input', (e) => {
|
| 694 |
-
if(currentlySelectedBubble)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 695 |
});
|
| 696 |
|
| 697 |
document.addEventListener('mousemove', e => { if (isPanning) panImage(e); if (draggedBubble) drag(e); if(isResizing) resizeBubble(e); });
|
|
@@ -737,13 +783,19 @@ class EnhancedComicGenerator:
|
|
| 737 |
|
| 738 |
function applyBubbleType(bubble, type) {
|
| 739 |
bubble.querySelectorAll('.thought-dot').forEach(el => el.remove());
|
|
|
|
| 740 |
let classesToKeep = 'speech-bubble';
|
| 741 |
if (bubble.classList.contains('selected')) classesToKeep += ' selected';
|
| 742 |
-
|
| 743 |
-
if (bubble.classList.contains('flipped-vertical')) classesToKeep += ' flipped-vertical';
|
| 744 |
bubble.className = classesToKeep;
|
| 745 |
bubble.classList.add(type);
|
| 746 |
bubble.dataset.type = type;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 747 |
if (type === 'thought') {
|
| 748 |
for (let i = 1; i <= 2; i++) {
|
| 749 |
const dot = document.createElement('div');
|
|
@@ -763,15 +815,29 @@ class EnhancedComicGenerator:
|
|
| 763 |
currentlySelectedBubble.style.fontFamily = font;
|
| 764 |
}
|
| 765 |
|
|
|
|
| 766 |
function rotateBubbleTail() {
|
| 767 |
if (!currentlySelectedBubble) { alert("Please select a bubble first."); return; }
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 774 |
}
|
|
|
|
| 775 |
|
| 776 |
function selectPanel(panel) {
|
| 777 |
document.querySelectorAll('.panel.selected').forEach(p => p.classList.remove('selected'));
|
|
@@ -792,7 +858,15 @@ class EnhancedComicGenerator:
|
|
| 792 |
|
| 793 |
const styles = window.getComputedStyle(currentlySelectedBubble);
|
| 794 |
document.getElementById('bubble-text-color').value = rgbToHex(styles.color);
|
| 795 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 796 |
document.getElementById('bubble-type-select').value = currentlySelectedBubble.dataset.type || 'speech';
|
| 797 |
document.getElementById('font-select').value = styles.fontFamily.split(',')[0].replace(/"/g, "").replace(/'/g, "").trim();
|
| 798 |
|
|
@@ -1069,7 +1143,6 @@ class EnhancedComicGenerator:
|
|
| 1069 |
</script>
|
| 1070 |
</body>
|
| 1071 |
</html>'''
|
| 1072 |
-
# <<< MODIFICATION END >>>
|
| 1073 |
with open(os.path.join(self.output_dir, 'page.html'), 'w', encoding='utf-8') as f:
|
| 1074 |
f.write(template_html)
|
| 1075 |
print("📄 Template files copied successfully!")
|
|
|
|
| 14 |
import traceback
|
| 15 |
|
| 16 |
# --- ROBUST IMPORTS WITH FALLBACKS ---
|
| 17 |
+
# (Assuming these modules have a method to process a single image, e.g., enhance_single)
|
| 18 |
try:
|
| 19 |
from backend.keyframes.keyframes import black_bar_crop
|
| 20 |
print("✅ Black bar cropping module loaded.")
|
|
|
|
| 29 |
except Exception as e:
|
| 30 |
print(f"⚠️ Could not load SimpleColorEnhancer: {e}. This feature will be SKIPPED.")
|
| 31 |
class SimpleColorEnhancer:
|
| 32 |
+
def enhance_batch(self, *args, **kwargs): print("-> Skipping simple color enhancement (module not loaded).")
|
| 33 |
+
def enhance_single(self, *args, **kwargs): print("-> Skipping simple color enhancement (module not loaded).")
|
|
|
|
| 34 |
|
| 35 |
try:
|
| 36 |
from backend.quality_color_enhancer import QualityColorEnhancer
|
|
|
|
| 38 |
except Exception as e:
|
| 39 |
print(f"⚠️ Could not load QualityColorEnhancer: {e}. This feature will be SKIPPED.")
|
| 40 |
class QualityColorEnhancer:
|
| 41 |
+
def batch_enhance(self, *args, **kwargs): print("-> Skipping quality color enhancement (module not loaded).")
|
| 42 |
+
def enhance_single(self, *args, **kwargs): print("-> Skipping quality color enhancement (module not loaded).")
|
| 43 |
+
|
| 44 |
|
| 45 |
try:
|
| 46 |
from backend.class_def import bubble, panel, Page
|
|
|
|
| 306 |
cap.release()
|
| 307 |
if not ret or frame is None:
|
| 308 |
return {"success": False, "message": f"No frame at {target_time:.2f}s."}
|
| 309 |
+
|
| 310 |
new_path = os.path.join(self.frames_dir, frame_filename)
|
| 311 |
cv2.imwrite(new_path, frame)
|
| 312 |
|
| 313 |
+
# Apply the same enhancements to the new frame to maintain style consistency
|
| 314 |
print(f"🎨 Applying enhancements to the new frame: {frame_filename}")
|
| 315 |
self._enhance_all_images(single_image_path=new_path)
|
| 316 |
self._enhance_quality_colors(single_image_path=new_path)
|
|
|
|
| 317 |
|
| 318 |
if isinstance(frame_to_time[frame_filename], dict):
|
| 319 |
frame_to_time[frame_filename]['time'] = target_time
|
|
|
|
| 343 |
ret, frame = cap.read()
|
| 344 |
cap.release()
|
| 345 |
if not ret or frame is None: return {"success": False, "message": f"Could not retrieve frame at {timestamp_seconds:.2f}s."}
|
| 346 |
+
|
| 347 |
new_path = os.path.join(self.frames_dir, frame_filename)
|
| 348 |
cv2.imwrite(new_path, frame)
|
| 349 |
+
|
| 350 |
+
# Apply enhancements to the new frame from the specific timestamp
|
| 351 |
print(f"🎨 Applying enhancements to the new frame from timestamp: {frame_filename}")
|
| 352 |
self._enhance_all_images(single_image_path=new_path)
|
| 353 |
self._enhance_quality_colors(single_image_path=new_path)
|
|
|
|
| 354 |
|
| 355 |
with open(metadata_path, 'r') as f: frame_to_time = json.load(f)
|
| 356 |
if frame_filename in frame_to_time:
|
|
|
|
| 437 |
update_status(f"Error: {e}", -1)
|
| 438 |
return False
|
| 439 |
|
| 440 |
+
# <<< MODIFICATION START: Made enhancement functions process single files efficiently >>>
|
| 441 |
def _enhance_all_images(self, single_image_path=None):
|
| 442 |
+
"""Enhances images. If single_image_path is provided, only enhances that file."""
|
|
|
|
|
|
|
| 443 |
try:
|
| 444 |
+
enhancer = SimpleColorEnhancer()
|
| 445 |
+
if single_image_path and os.path.exists(single_image_path):
|
| 446 |
+
# Assumes the enhancer class has a method for single images.
|
| 447 |
+
# If it doesn't, this will gracefully fail or you can implement a fallback.
|
| 448 |
+
enhancer.enhance_single(single_image_path)
|
| 449 |
+
elif not single_image_path:
|
| 450 |
+
enhancer.enhance_batch(self.frames_dir)
|
| 451 |
+
except Exception as e:
|
| 452 |
+
print(f"❌ Simple enhancement failed: {e}. Falling back to batch processing.")
|
| 453 |
+
# Fallback for safety if enhance_single doesn't exist
|
| 454 |
+
try:
|
| 455 |
+
SimpleColorEnhancer().enhance_batch(self.frames_dir)
|
| 456 |
+
except Exception as e2:
|
| 457 |
+
print(f"❌ Fallback simple enhancement also failed: {e2}")
|
| 458 |
|
| 459 |
def _enhance_quality_colors(self, single_image_path=None):
|
| 460 |
+
"""Enhances images with a quality model. If single_image_path is provided, only enhances that file."""
|
|
|
|
| 461 |
try:
|
| 462 |
+
enhancer = QualityColorEnhancer()
|
| 463 |
+
if single_image_path and os.path.exists(single_image_path):
|
| 464 |
+
# Assumes the enhancer class has a method for single images.
|
| 465 |
+
enhancer.enhance_single(single_image_path)
|
| 466 |
+
elif not single_image_path:
|
| 467 |
+
enhancer.batch_enhance(self.frames_dir)
|
| 468 |
+
except Exception as e:
|
| 469 |
+
print(f"⚠️ Quality enhancement failed: {e}. Falling back to batch processing.")
|
| 470 |
+
# Fallback for safety if enhance_single doesn't exist
|
| 471 |
+
try:
|
| 472 |
+
QualityColorEnhancer().batch_enhance(self.frames_dir)
|
| 473 |
+
except Exception as e2:
|
| 474 |
+
print(f"⚠️ Fallback quality enhancement also failed: {e2}")
|
| 475 |
+
# <<< MODIFICATION END >>>
|
| 476 |
|
| 477 |
def _create_ai_bubbles_from_moments(self, black_x, black_y):
|
| 478 |
bubbles, frame_files = [], sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')])
|
|
|
|
| 524 |
|
| 525 |
def _copy_template_files(self):
|
| 526 |
try:
|
|
|
|
| 527 |
template_html = '''<!DOCTYPE html>
|
| 528 |
<html lang="en">
|
| 529 |
<head>
|
|
|
|
| 545 |
.panel img { width: 100%; height: 100%; object-fit: cover; object-position: center; transition: transform 0.1s ease-out; }
|
| 546 |
.panel img.pannable { cursor: grab; }
|
| 547 |
.panel img.panning { cursor: grabbing; }
|
| 548 |
+
.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; 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; font-family: 'Comic Neue', cursive; }
|
| 549 |
.bubble-text { padding: 2px; word-wrap: break-word; }
|
| 550 |
.speech-bubble.selected { outline: 2px dashed #4CAF50; }
|
| 551 |
.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; }
|
| 552 |
+
|
| 553 |
+
/* <<< MODIFICATION START: New CSS for 'speech' bubble type >>> */
|
| 554 |
+
.speech-bubble.speech {
|
| 555 |
+
color: #fff;
|
| 556 |
+
font-size: 16px;
|
| 557 |
+
text-align: center;
|
| 558 |
+
padding: 1em;
|
| 559 |
+
/* Remove standard border/background to allow gradient to show */
|
| 560 |
+
border: none;
|
| 561 |
+
background: none;
|
| 562 |
+
border-radius: 0;
|
| 563 |
+
/* Gradient background applied via border-image */
|
| 564 |
+
border-image: fill 0 linear-gradient(30deg, #4ECDC4, #6A4A3C);
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
/* Generic tail for all bubbles */
|
| 568 |
+
.speech-bubble::after, .speech-bubble.idea::after { content: ''; position: absolute; width: 0; height: 0; border-left: 10px solid transparent; border-right: 10px solid transparent; }
|
| 569 |
+
|
| 570 |
+
/* Specific tail for the new 'speech' bubble style */
|
| 571 |
+
.speech-bubble.speech::after {
|
| 572 |
+
border-top: 20px solid #6A4A3C; /* Long tail, color matches one end of the gradient */
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
/* 4-WAY TAIL POSITIONING CLASSES for .speech bubbles */
|
| 576 |
+
.speech-bubble.speech.tail-bl::after { bottom: -19px; left: 20px; transform: rotate(0deg); }
|
| 577 |
+
.speech-bubble.speech.tail-br::after { bottom: -19px; right: 20px; transform: rotate(0deg); }
|
| 578 |
+
.speech-bubble.speech.tail-tr::after { top: -19px; right: 20px; transform: rotate(180deg); }
|
| 579 |
+
.speech-bubble.speech.tail-tl::after { top: -19px; left: 20px; transform: rotate(180deg); }
|
| 580 |
+
/* <<< MODIFICATION END >>> */
|
| 581 |
+
|
| 582 |
.speech-bubble.thought { background: white; border: 2px dashed #555; color: #333; border-radius: 50%; }
|
| 583 |
.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%); }
|
| 584 |
.speech-bubble.narration { background: #FAFAFA; border: 2px solid #BDBDBD; color: #424242; border-radius: 3px; }
|
| 585 |
.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%; }
|
| 586 |
+
.speech-bubble.idea::after { border-top: 10px solid #FFA500; bottom: -9px; left: 20px; }
|
| 587 |
+
|
|
|
|
|
|
|
| 588 |
.speech-bubble.thought::after { display: none; }
|
| 589 |
.thought-dot { position: absolute; background-color: white; border: 2px solid #555; border-radius: 50%; z-index: -1; }
|
| 590 |
.thought-dot-1 { width: 20px; height: 20px; bottom: -20px; left: 15px; }
|
| 591 |
.thought-dot-2 { width: 12px; height: 12px; bottom: -32px; left: 5px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 592 |
.resize-handle { position: absolute; width: 10px; height: 10px; background: #2196F3; border: 1px solid white; border-radius: 50%; display: none; z-index: 11; }
|
| 593 |
.speech-bubble.selected .resize-handle { display: block; }
|
| 594 |
.resize-handle.se { bottom: -5px; right: -5px; cursor: se-resize; }
|
|
|
|
| 732 |
if(currentlySelectedBubble) currentlySelectedBubble.style.color = e.target.value;
|
| 733 |
});
|
| 734 |
document.getElementById('bubble-fill-color').addEventListener('input', (e) => {
|
| 735 |
+
if(currentlySelectedBubble) {
|
| 736 |
+
// The new speech bubble uses a gradient, so standard fill color is disabled for it.
|
| 737 |
+
if (currentlySelectedBubble.dataset.type !== 'speech') {
|
| 738 |
+
currentlySelectedBubble.style.backgroundColor = e.target.value;
|
| 739 |
+
}
|
| 740 |
+
}
|
| 741 |
});
|
| 742 |
|
| 743 |
document.addEventListener('mousemove', e => { if (isPanning) panImage(e); if (draggedBubble) drag(e); if(isResizing) resizeBubble(e); });
|
|
|
|
| 783 |
|
| 784 |
function applyBubbleType(bubble, type) {
|
| 785 |
bubble.querySelectorAll('.thought-dot').forEach(el => el.remove());
|
| 786 |
+
// Preserve essential classes
|
| 787 |
let classesToKeep = 'speech-bubble';
|
| 788 |
if (bubble.classList.contains('selected')) classesToKeep += ' selected';
|
| 789 |
+
|
|
|
|
| 790 |
bubble.className = classesToKeep;
|
| 791 |
bubble.classList.add(type);
|
| 792 |
bubble.dataset.type = type;
|
| 793 |
+
|
| 794 |
+
if (type === 'speech') {
|
| 795 |
+
// Set initial tail position for speech bubbles
|
| 796 |
+
bubble.classList.add('tail-bl');
|
| 797 |
+
bubble.dataset.tailPos = '0';
|
| 798 |
+
}
|
| 799 |
if (type === 'thought') {
|
| 800 |
for (let i = 1; i <= 2; i++) {
|
| 801 |
const dot = document.createElement('div');
|
|
|
|
| 815 |
currentlySelectedBubble.style.fontFamily = font;
|
| 816 |
}
|
| 817 |
|
| 818 |
+
// <<< MODIFICATION START: Rewritten 4-way tail rotation function >>>
|
| 819 |
function rotateBubbleTail() {
|
| 820 |
if (!currentlySelectedBubble) { alert("Please select a bubble first."); return; }
|
| 821 |
+
// This function now only works for the 'speech' type which has the new tail logic
|
| 822 |
+
if (currentlySelectedBubble.dataset.type !== 'speech') {
|
| 823 |
+
alert("Tail rotation is only available for the 'Speech' bubble type.");
|
| 824 |
+
return;
|
| 825 |
+
}
|
| 826 |
+
|
| 827 |
+
const positions = ['tail-bl', 'tail-br', 'tail-tr', 'tail-tl'];
|
| 828 |
+
let currentPos = parseInt(currentlySelectedBubble.dataset.tailPos || 0);
|
| 829 |
+
|
| 830 |
+
// Remove current position class
|
| 831 |
+
currentlySelectedBubble.classList.remove(positions[currentPos]);
|
| 832 |
+
|
| 833 |
+
// Get next position, cycling back to 0
|
| 834 |
+
let nextPos = (currentPos + 1) % positions.length;
|
| 835 |
+
|
| 836 |
+
// Add new position class and update data attribute
|
| 837 |
+
currentlySelectedBubble.classList.add(positions[nextPos]);
|
| 838 |
+
currentlySelectedBubble.dataset.tailPos = nextPos;
|
| 839 |
}
|
| 840 |
+
// <<< MODIFICATION END >>>
|
| 841 |
|
| 842 |
function selectPanel(panel) {
|
| 843 |
document.querySelectorAll('.panel.selected').forEach(p => p.classList.remove('selected'));
|
|
|
|
| 858 |
|
| 859 |
const styles = window.getComputedStyle(currentlySelectedBubble);
|
| 860 |
document.getElementById('bubble-text-color').value = rgbToHex(styles.color);
|
| 861 |
+
|
| 862 |
+
const fillColorPicker = document.getElementById('bubble-fill-color');
|
| 863 |
+
if (currentlySelectedBubble.dataset.type === 'speech') {
|
| 864 |
+
fillColorPicker.disabled = true; // Disable fill for gradient bubble
|
| 865 |
+
} else {
|
| 866 |
+
fillColorPicker.disabled = false;
|
| 867 |
+
fillColorPicker.value = rgbToHex(styles.backgroundColor);
|
| 868 |
+
}
|
| 869 |
+
|
| 870 |
document.getElementById('bubble-type-select').value = currentlySelectedBubble.dataset.type || 'speech';
|
| 871 |
document.getElementById('font-select').value = styles.fontFamily.split(',')[0].replace(/"/g, "").replace(/'/g, "").trim();
|
| 872 |
|
|
|
|
| 1143 |
</script>
|
| 1144 |
</body>
|
| 1145 |
</html>'''
|
|
|
|
| 1146 |
with open(os.path.join(self.output_dir, 'page.html'), 'w', encoding='utf-8') as f:
|
| 1147 |
f.write(template_html)
|
| 1148 |
print("📄 Template files copied successfully!")
|