Update app_enhanced.py
Browse files- app_enhanced.py +44 -3
app_enhanced.py
CHANGED
|
@@ -308,6 +308,13 @@ class EnhancedComicGenerator:
|
|
| 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 |
if isinstance(frame_to_time[frame_filename], dict):
|
| 312 |
frame_to_time[frame_filename]['time'] = target_time
|
| 313 |
else:
|
|
@@ -338,6 +345,13 @@ class EnhancedComicGenerator:
|
|
| 338 |
if not ret or frame is None: return {"success": False, "message": f"Could not retrieve frame at {timestamp_seconds:.2f}s."}
|
| 339 |
new_path = os.path.join(self.frames_dir, frame_filename)
|
| 340 |
cv2.imwrite(new_path, frame)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
with open(metadata_path, 'r') as f: frame_to_time = json.load(f)
|
| 342 |
if frame_filename in frame_to_time:
|
| 343 |
if isinstance(frame_to_time[frame_filename], dict):
|
|
@@ -488,6 +502,7 @@ class EnhancedComicGenerator:
|
|
| 488 |
|
| 489 |
def _copy_template_files(self):
|
| 490 |
try:
|
|
|
|
| 491 |
template_html = '''<!DOCTYPE html>
|
| 492 |
<html lang="en">
|
| 493 |
<head>
|
|
@@ -519,8 +534,9 @@ class EnhancedComicGenerator:
|
|
| 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 |
-
|
| 523 |
-
.speech-bubble.
|
|
|
|
| 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; }
|
|
@@ -528,7 +544,8 @@ class EnhancedComicGenerator:
|
|
| 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 |
-
|
|
|
|
| 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; }
|
|
@@ -676,6 +693,7 @@ class EnhancedComicGenerator:
|
|
| 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,6 +710,7 @@ class EnhancedComicGenerator:
|
|
| 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,6 +734,7 @@ class EnhancedComicGenerator:
|
|
| 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';
|
|
@@ -759,10 +779,12 @@ class EnhancedComicGenerator:
|
|
| 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');
|
|
@@ -773,6 +795,7 @@ class EnhancedComicGenerator:
|
|
| 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,6 +811,7 @@ class EnhancedComicGenerator:
|
|
| 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,6 +825,7 @@ class EnhancedComicGenerator:
|
|
| 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,13 +834,16 @@ class EnhancedComicGenerator:
|
|
| 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,6 +855,7 @@ class EnhancedComicGenerator:
|
|
| 827 |
selectBubble(null);
|
| 828 |
}
|
| 829 |
}
|
|
|
|
| 830 |
function startResize(e, dir) {
|
| 831 |
e.preventDefault();
|
| 832 |
e.stopPropagation();
|
|
@@ -841,11 +870,13 @@ class EnhancedComicGenerator:
|
|
| 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,13 +888,16 @@ class EnhancedComicGenerator:
|
|
| 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.");
|
|
@@ -930,6 +964,7 @@ class EnhancedComicGenerator:
|
|
| 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;
|
|
@@ -937,6 +972,7 @@ class EnhancedComicGenerator:
|
|
| 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');
|
|
@@ -953,6 +989,7 @@ class EnhancedComicGenerator:
|
|
| 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;
|
|
@@ -965,6 +1002,7 @@ class EnhancedComicGenerator:
|
|
| 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');
|
|
@@ -972,11 +1010,13 @@ class EnhancedComicGenerator:
|
|
| 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({
|
|
@@ -1029,6 +1069,7 @@ class EnhancedComicGenerator:
|
|
| 1029 |
</script>
|
| 1030 |
</body>
|
| 1031 |
</html>'''
|
|
|
|
| 1032 |
with open(os.path.join(self.output_dir, 'page.html'), 'w', encoding='utf-8') as f:
|
| 1033 |
f.write(template_html)
|
| 1034 |
print("📄 Template files copied successfully!")
|
|
|
|
| 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 |
+
# <<< MODIFICATION START: Apply enhancements to the new frame for consistency >>>
|
| 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
|
| 320 |
else:
|
|
|
|
| 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 |
+
# <<< MODIFICATION START: Apply enhancements to the new frame for consistency >>>
|
| 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:
|
| 357 |
if isinstance(frame_to_time[frame_filename], dict):
|
|
|
|
| 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>
|
|
|
|
| 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.speech::after, .speech-bubble.idea::after { content: ''; position: absolute; width: 0; height: 0; border-left: 10px solid transparent; border-right: 10px solid transparent; }
|
| 537 |
+
/* <<< CSS MODIFICATION: Made bubble tail longer >>> */
|
| 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; }
|
|
|
|
| 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; }
|
|
|
|
| 693 |
document.getElementById('bubble-fill-color').addEventListener('input', (e) => {
|
| 694 |
if(currentlySelectedBubble) currentlySelectedBubble.style.backgroundColor = e.target.value;
|
| 695 |
});
|
| 696 |
+
|
| 697 |
document.addEventListener('mousemove', e => { if (isPanning) panImage(e); if (draggedBubble) drag(e); if(isResizing) resizeBubble(e); });
|
| 698 |
document.addEventListener('mouseup', e => { if (isPanning) stopPan(e); if (draggedBubble) stopDrag(e); if(isResizing) stopResize(e);});
|
| 699 |
document.addEventListener('mouseleave', e => { if (isPanning) stopPan(e); if (draggedBubble) stopDrag(e); if(isResizing) stopResize(e);});
|
|
|
|
| 710 |
handle.addEventListener('mousedown', (e) => startResize(e, dir));
|
| 711 |
});
|
| 712 |
}
|
| 713 |
+
|
| 714 |
function createBubbleElement(data) {
|
| 715 |
const bubbleDiv = document.createElement('div');
|
| 716 |
bubbleDiv.dataset.id = data.id;
|
|
|
|
| 734 |
if (b.length == 1) b = "0" + b;
|
| 735 |
return "#" + r + g + b;
|
| 736 |
}
|
| 737 |
+
|
| 738 |
function applyBubbleType(bubble, type) {
|
| 739 |
bubble.querySelectorAll('.thought-dot').forEach(el => el.remove());
|
| 740 |
let classesToKeep = 'speech-bubble';
|
|
|
|
| 779 |
currentlySelectedPanel = panel;
|
| 780 |
selectBubble(null);
|
| 781 |
}
|
| 782 |
+
|
| 783 |
function selectBubble(bubble) {
|
| 784 |
if (currentlySelectedBubble) currentlySelectedBubble.classList.remove('selected');
|
| 785 |
currentlySelectedBubble = bubble;
|
| 786 |
const bubbleControls = ['bubble-text-color', 'bubble-fill-color', 'bubble-type-select', 'font-select'];
|
| 787 |
+
|
| 788 |
if (currentlySelectedBubble) {
|
| 789 |
currentlySelectedBubble.classList.add('selected');
|
| 790 |
if (currentlySelectedPanel) currentlySelectedPanel.classList.remove('selected');
|
|
|
|
| 795 |
document.getElementById('bubble-fill-color').value = rgbToHex(styles.backgroundColor);
|
| 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 |
+
|
| 799 |
document.getElementById('zoom-slider').disabled = true;
|
| 800 |
bubbleControls.forEach(id => document.getElementById(id).disabled = false);
|
| 801 |
} else {
|
|
|
|
| 811 |
const textarea = document.createElement('textarea');
|
| 812 |
const originalHeight = bubble.offsetHeight;
|
| 813 |
bubble.style.height = `${originalHeight}px`;
|
| 814 |
+
|
| 815 |
textarea.value = textSpan.textContent;
|
| 816 |
bubble.appendChild(textarea);
|
| 817 |
textSpan.style.display = 'none';
|
|
|
|
| 825 |
textarea.addEventListener('blur', finishEditing, { once: true });
|
| 826 |
textarea.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); textarea.blur(); }});
|
| 827 |
}
|
| 828 |
+
|
| 829 |
function startDrag(e) {
|
| 830 |
const bubble = e.target.closest('.speech-bubble');
|
| 831 |
if (!bubble || currentlyEditing) return;
|
|
|
|
| 834 |
const rect = bubble.getBoundingClientRect();
|
| 835 |
offset = { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
| 836 |
}
|
| 837 |
+
|
| 838 |
function drag(e) {
|
| 839 |
if (!draggedBubble) return;
|
| 840 |
const parentRect = draggedBubble.parentElement.getBoundingClientRect();
|
| 841 |
draggedBubble.style.left = `${e.clientX - parentRect.left - offset.x}px`;
|
| 842 |
draggedBubble.style.top = `${e.clientY - parentRect.top - offset.y}px`;
|
| 843 |
}
|
| 844 |
+
|
| 845 |
function stopDrag() { draggedBubble = null; }
|
| 846 |
+
|
| 847 |
function deleteBubble() {
|
| 848 |
if (!currentlySelectedBubble) {
|
| 849 |
alert("Please select a bubble to delete.");
|
|
|
|
| 855 |
selectBubble(null);
|
| 856 |
}
|
| 857 |
}
|
| 858 |
+
|
| 859 |
function startResize(e, dir) {
|
| 860 |
e.preventDefault();
|
| 861 |
e.stopPropagation();
|
|
|
|
| 870 |
originalMouseX = e.clientX;
|
| 871 |
originalMouseY = e.clientY;
|
| 872 |
}
|
| 873 |
+
|
| 874 |
function resizeBubble(e) {
|
| 875 |
if (!isResizing || !currentlySelectedBubble) return;
|
| 876 |
const dx = e.clientX - originalMouseX;
|
| 877 |
const dy = e.clientY - originalMouseY;
|
| 878 |
const bubble = currentlySelectedBubble;
|
| 879 |
+
|
| 880 |
if (resizeHandle.includes('e')) bubble.style.width = `${originalWidth + dx}px`;
|
| 881 |
if (resizeHandle.includes('w')) {
|
| 882 |
bubble.style.width = `${originalWidth - dx}px`;
|
|
|
|
| 888 |
bubble.style.top = `${originalY + dy}px`;
|
| 889 |
}
|
| 890 |
}
|
| 891 |
+
|
| 892 |
function stopResize() { isResizing = false; }
|
| 893 |
+
|
| 894 |
function clearSavedState() {
|
| 895 |
if (confirm("Reset all edits?")) {
|
| 896 |
localStorage.removeItem('comicEditorState');
|
| 897 |
window.location.reload();
|
| 898 |
}
|
| 899 |
}
|
| 900 |
+
|
| 901 |
async function exportPagesToPNG() {
|
| 902 |
const pages = document.querySelectorAll('.comic-page');
|
| 903 |
if (pages.length === 0) return alert("No pages found.");
|
|
|
|
| 964 |
img.style.opacity = '1';
|
| 965 |
});
|
| 966 |
}
|
| 967 |
+
|
| 968 |
function updateImageTransform(img) {
|
| 969 |
const zoom = (img.dataset.zoom || 100) / 100;
|
| 970 |
const x = img.dataset.translateX || 0;
|
|
|
|
| 972 |
img.style.transform = `translateX(${x}px) translateY(${y}px) scale(${zoom})`;
|
| 973 |
img.classList.toggle('pannable', zoom > 1);
|
| 974 |
}
|
| 975 |
+
|
| 976 |
function handleZoom(event) {
|
| 977 |
if (!currentlySelectedPanel) return;
|
| 978 |
const img = currentlySelectedPanel.querySelector('img');
|
|
|
|
| 989 |
document.getElementById('zoom-slider').value = 100;
|
| 990 |
updateImageTransform(img);
|
| 991 |
}
|
| 992 |
+
|
| 993 |
function startPan(event) {
|
| 994 |
if (event.button !== 0) return;
|
| 995 |
const img = event.target;
|
|
|
|
| 1002 |
panStartTranslateX = parseFloat(img.dataset.translateX || 0);
|
| 1003 |
panStartTranslateY = parseFloat(img.dataset.translateY || 0);
|
| 1004 |
}
|
| 1005 |
+
|
| 1006 |
function panImage(event) {
|
| 1007 |
if (!isPanning || !currentlySelectedPanel) return;
|
| 1008 |
const img = currentlySelectedPanel.querySelector('img');
|
|
|
|
| 1010 |
img.dataset.translateY = panStartTranslateY + (event.clientY - panStartY);
|
| 1011 |
updateImageTransform(img);
|
| 1012 |
}
|
| 1013 |
+
|
| 1014 |
function stopPan() {
|
| 1015 |
if (!isPanning) return;
|
| 1016 |
isPanning = false;
|
| 1017 |
currentlySelectedPanel?.querySelector('img')?.classList.remove('panning');
|
| 1018 |
}
|
| 1019 |
+
|
| 1020 |
function addBubbleToPanel() {
|
| 1021 |
if (!currentlySelectedPanel) { alert("Please select a panel first."); return; }
|
| 1022 |
const newBubble = createBubbleElement({
|
|
|
|
| 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!")
|