Update app_enhanced.py
Browse files- app_enhanced.py +147 -37
app_enhanced.py
CHANGED
|
@@ -513,8 +513,8 @@ class EnhancedComicGenerator:
|
|
| 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 |
-
|
| 517 |
-
font-size:
|
| 518 |
}
|
| 519 |
.bubble-text { padding: 2px; word-wrap: break-word; }
|
| 520 |
.speech-bubble.selected { outline: 2px dashed #4CAF50; }
|
|
@@ -522,46 +522,65 @@ class EnhancedComicGenerator:
|
|
| 522 |
|
| 523 |
.speech-bubble.speech {
|
| 524 |
font-family: "Permanent Marker", cursive;
|
| 525 |
-
color: #000;
|
| 526 |
padding: 1em;
|
| 527 |
-
--
|
| 528 |
-
--
|
| 529 |
-
--
|
| 530 |
-
--p:
|
| 531 |
-
--
|
| 532 |
--c: #FFFFFF;
|
| 533 |
-
border-radius: var(--r) var(--r) min(var(--r),100% - var(--p) - var(--b)/2) min(var(--r),var(--p) - var(--b)/2)/var(--r);
|
| 534 |
-
clip-path: polygon(0 100%,0 0,100% 0,100% 100%,
|
| 535 |
-
clamp(var(--b),var(--p) + var(--b)/2,100%) 100%,
|
| 536 |
-
calc(var(--p) + var(--x)) calc(100% + var(--h)),
|
| 537 |
-
clamp(0%,var(--p) - var(--b)/2,100% - var(--b)) 100%);
|
| 538 |
background: var(--c);
|
| 539 |
-
border-
|
| 540 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 541 |
}
|
| 542 |
|
| 543 |
.speech-bubble.thought {
|
| 544 |
-
border-radius: 50%;
|
| 545 |
-
border: 3px solid black;
|
| 546 |
-
background: white; color: black;
|
| 547 |
}
|
| 548 |
.speech-bubble.thought:before {
|
| 549 |
content: ''; position: absolute;
|
| 550 |
-
|
| 551 |
-
border-radius:
|
| 552 |
-
left:
|
| 553 |
-
box-shadow: 0 0 0
|
| 554 |
}
|
| 555 |
.speech-bubble.thought.flipped:before {
|
| 556 |
-
|
| 557 |
-
box-shadow: 0 0 0
|
| 558 |
}
|
| 559 |
|
| 560 |
.speech-bubble.reaction {
|
| 561 |
-
border-
|
| 562 |
-
background: #FFD700;
|
| 563 |
-
border: 3px solid #E53935;
|
| 564 |
-
color: #D32F2F;
|
| 565 |
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%);
|
| 566 |
}
|
| 567 |
|
|
@@ -662,6 +681,7 @@ class EnhancedComicGenerator:
|
|
| 662 |
let currentlySelectedBubble = null;
|
| 663 |
let currentlySelectedPanel = null;
|
| 664 |
let isPanning = false, panStartX, panStartY, panStartTranslateX, panStartTranslateY;
|
|
|
|
| 665 |
|
| 666 |
function renderComic(data) {
|
| 667 |
const container = document.getElementById('comic-pages');
|
|
@@ -766,6 +786,7 @@ class EnhancedComicGenerator:
|
|
| 766 |
let classesToKeep = ['speech-bubble'];
|
| 767 |
if (bubble.classList.contains('selected')) classesToKeep.push('selected');
|
| 768 |
if (bubble.classList.contains('flipped')) classesToKeep.push('flipped');
|
|
|
|
| 769 |
bubble.className = classesToKeep.join(' ');
|
| 770 |
bubble.classList.add(type);
|
| 771 |
bubble.dataset.type = type;
|
|
@@ -783,7 +804,12 @@ class EnhancedComicGenerator:
|
|
| 783 |
|
| 784 |
function rotateBubbleTail() {
|
| 785 |
if (!currentlySelectedBubble) { alert("Please select a bubble first."); return; }
|
| 786 |
-
currentlySelectedBubble.classList.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 787 |
}
|
| 788 |
|
| 789 |
function selectPanel(panel) {
|
|
@@ -913,7 +939,18 @@ class EnhancedComicGenerator:
|
|
| 913 |
}
|
| 914 |
|
| 915 |
async function exportPagesToPNG() {
|
| 916 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 917 |
}
|
| 918 |
|
| 919 |
function replacePanelImage() {
|
|
@@ -946,15 +983,41 @@ class EnhancedComicGenerator:
|
|
| 946 |
}
|
| 947 |
|
| 948 |
function adjustFrame(direction) {
|
| 949 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 950 |
}
|
| 951 |
|
| 952 |
function updateImageTransform(img) {
|
| 953 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 954 |
}
|
| 955 |
|
| 956 |
function handleZoom(event) {
|
| 957 |
-
|
|
|
|
|
|
|
|
|
|
| 958 |
}
|
| 959 |
|
| 960 |
function resetPanelTransform() {
|
|
@@ -968,15 +1031,30 @@ class EnhancedComicGenerator:
|
|
| 968 |
}
|
| 969 |
|
| 970 |
function startPan(event) {
|
| 971 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 972 |
}
|
| 973 |
|
| 974 |
function panImage(event) {
|
| 975 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 976 |
}
|
| 977 |
|
| 978 |
function stopPan() {
|
| 979 |
-
|
|
|
|
|
|
|
| 980 |
}
|
| 981 |
|
| 982 |
function addBubbleToPanel() {
|
|
@@ -994,7 +1072,39 @@ class EnhancedComicGenerator:
|
|
| 994 |
}
|
| 995 |
|
| 996 |
function gotoTimestamp() {
|
| 997 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 998 |
}
|
| 999 |
</script>
|
| 1000 |
</body>
|
|
|
|
| 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; }
|
|
|
|
| 522 |
|
| 523 |
.speech-bubble.speech {
|
| 524 |
font-family: "Permanent Marker", cursive;
|
|
|
|
| 525 |
padding: 1em;
|
| 526 |
+
--b: 3em; /* base */
|
| 527 |
+
--h: 1.8em; /* height */
|
| 528 |
+
--t: .6; /* thickness (from 0 to 1) */
|
| 529 |
+
--p: 20%; /* main position (0%:left 100%:right) */
|
| 530 |
+
--r: 1.2em; /* the radius */
|
| 531 |
--c: #FFFFFF;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 532 |
background: var(--c);
|
| 533 |
+
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);
|
| 534 |
+
position: relative;
|
| 535 |
+
}
|
| 536 |
+
.speech-bubble.speech:before {
|
| 537 |
+
content: "";
|
| 538 |
+
position: absolute;
|
| 539 |
+
top: 100%;
|
| 540 |
+
left: clamp(-1*var(--t)*var(--b),var(--p) - (var(--t) + 1)*var(--b)/2,100% - var(--b));
|
| 541 |
+
width: var(--b);
|
| 542 |
+
height: var(--h);
|
| 543 |
+
background: inherit;
|
| 544 |
+
border-bottom-right-radius: 100%;
|
| 545 |
+
-webkit-mask: radial-gradient(calc(var(--t)*100%) 105% at 0 0,#0000 99%,#000 101%);
|
| 546 |
+
mask: radial-gradient(calc(var(--t)*100%) 105% at 0 0,#0000 99%,#000 101%);
|
| 547 |
+
transform-origin: center;
|
| 548 |
+
}
|
| 549 |
+
.speech-bubble.speech.flipped:before {
|
| 550 |
+
left: auto;
|
| 551 |
+
right: clamp(-1*var(--t)*var(--b),var(--p) - (var(--t) + 1)*var(--b)/2,100% - var(--b));
|
| 552 |
+
transform: scaleX(-1);
|
| 553 |
+
}
|
| 554 |
+
.speech-bubble.speech.flipped-vertical:before {
|
| 555 |
+
top: auto;
|
| 556 |
+
bottom: 100%;
|
| 557 |
+
transform: scaleY(-1);
|
| 558 |
+
}
|
| 559 |
+
.speech-bubble.speech.flipped.flipped-vertical:before {
|
| 560 |
+
top: auto;
|
| 561 |
+
bottom: 100%;
|
| 562 |
+
left: auto;
|
| 563 |
+
right: clamp(-1*var(--t)*var(--b),var(--p) - (var(--t) + 1)*var(--b)/2,100% - var(--b));
|
| 564 |
+
transform: scale(-1, -1);
|
| 565 |
}
|
| 566 |
|
| 567 |
.speech-bubble.thought {
|
| 568 |
+
border-radius: 50%; border: 3px solid black; background: white; color: black;
|
|
|
|
|
|
|
| 569 |
}
|
| 570 |
.speech-bubble.thought:before {
|
| 571 |
content: ''; position: absolute;
|
| 572 |
+
width: 8px; height: 8px; bottom: -12px;
|
| 573 |
+
border-radius: 50%; background: white;
|
| 574 |
+
left: 20%;
|
| 575 |
+
box-shadow: 0 0 0 3px black, -10px 8px 0 0px white, -10px 8px 0 3px black;
|
| 576 |
}
|
| 577 |
.speech-bubble.thought.flipped:before {
|
| 578 |
+
left: auto; right: 20%;
|
| 579 |
+
box-shadow: 0 0 0 3px black, 10px 8px 0 0px white, 10px 8px 0 3px black;
|
| 580 |
}
|
| 581 |
|
| 582 |
.speech-bubble.reaction {
|
| 583 |
+
background: #FFD700; border: 3px solid #E53935; color: #D32F2F; font-weight: 900;
|
|
|
|
|
|
|
|
|
|
| 584 |
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%);
|
| 585 |
}
|
| 586 |
|
|
|
|
| 681 |
let currentlySelectedBubble = null;
|
| 682 |
let currentlySelectedPanel = null;
|
| 683 |
let isPanning = false, panStartX, panStartY, panStartTranslateX, panStartTranslateY;
|
| 684 |
+
let isResizing = false, resizeHandle, originalWidth, originalHeight, originalX, originalY, originalMouseX, originalMouseY;
|
| 685 |
|
| 686 |
function renderComic(data) {
|
| 687 |
const container = document.getElementById('comic-pages');
|
|
|
|
| 786 |
let classesToKeep = ['speech-bubble'];
|
| 787 |
if (bubble.classList.contains('selected')) classesToKeep.push('selected');
|
| 788 |
if (bubble.classList.contains('flipped')) classesToKeep.push('flipped');
|
| 789 |
+
if (bubble.classList.contains('flipped-vertical')) classesToKeep.push('flipped-vertical');
|
| 790 |
bubble.className = classesToKeep.join(' ');
|
| 791 |
bubble.classList.add(type);
|
| 792 |
bubble.dataset.type = type;
|
|
|
|
| 804 |
|
| 805 |
function rotateBubbleTail() {
|
| 806 |
if (!currentlySelectedBubble) { alert("Please select a bubble first."); return; }
|
| 807 |
+
const isFlippedH = currentlySelectedBubble.classList.contains('flipped');
|
| 808 |
+
const isFlippedV = currentlySelectedBubble.classList.contains('flipped-vertical');
|
| 809 |
+
if (!isFlippedH && !isFlippedV) { currentlySelectedBubble.classList.add('flipped'); }
|
| 810 |
+
else if (isFlippedH && !isFlippedV) { currentlySelectedBubble.classList.add('flipped-vertical'); }
|
| 811 |
+
else if (isFlippedH && isFlippedV) { currentlySelectedBubble.classList.remove('flipped'); }
|
| 812 |
+
else { currentlySelectedBubble.classList.remove('flipped-vertical'); }
|
| 813 |
}
|
| 814 |
|
| 815 |
function selectPanel(panel) {
|
|
|
|
| 939 |
}
|
| 940 |
|
| 941 |
async function exportPagesToPNG() {
|
| 942 |
+
const pages = document.querySelectorAll('.comic-page');
|
| 943 |
+
if (pages.length === 0) return alert("No pages found.");
|
| 944 |
+
alert(`Starting export of ${pages.length} page(s).`);
|
| 945 |
+
for (let i = 0; i < pages.length; i++) {
|
| 946 |
+
try {
|
| 947 |
+
const canvas = await html2canvas(pages[i], { scale: 2 });
|
| 948 |
+
const link = document.createElement('a');
|
| 949 |
+
link.download = `comic-page-${i + 1}.png`;
|
| 950 |
+
link.href = canvas.toDataURL('image/png');
|
| 951 |
+
link.click();
|
| 952 |
+
} catch (err) { alert(`Failed to export page ${i + 1}.`); }
|
| 953 |
+
}
|
| 954 |
}
|
| 955 |
|
| 956 |
function replacePanelImage() {
|
|
|
|
| 983 |
}
|
| 984 |
|
| 985 |
function adjustFrame(direction) {
|
| 986 |
+
if (!currentlySelectedPanel) { alert("Please select a panel first."); return; }
|
| 987 |
+
const img = currentlySelectedPanel.querySelector('img');
|
| 988 |
+
let filename = img.src.substring(img.src.lastIndexOf('/') + 1).split('?')[0];
|
| 989 |
+
img.style.opacity = '0.5';
|
| 990 |
+
fetch('/regenerate_frame', {
|
| 991 |
+
method: 'POST',
|
| 992 |
+
headers: { 'Content-Type': 'application/json' },
|
| 993 |
+
body: JSON.stringify({ filename, direction })
|
| 994 |
+
})
|
| 995 |
+
.then(res => res.json())
|
| 996 |
+
.then(data => {
|
| 997 |
+
if (data.success) {
|
| 998 |
+
img.src = `/frames/final/${filename}?t=${new Date().getTime()}`;
|
| 999 |
+
} else { alert('Error: ' + data.message); }
|
| 1000 |
+
img.style.opacity = '1';
|
| 1001 |
+
})
|
| 1002 |
+
.catch(() => {
|
| 1003 |
+
alert('An error occurred.');
|
| 1004 |
+
img.style.opacity = '1';
|
| 1005 |
+
});
|
| 1006 |
}
|
| 1007 |
|
| 1008 |
function updateImageTransform(img) {
|
| 1009 |
+
const zoom = (img.dataset.zoom || 100) / 100;
|
| 1010 |
+
const x = img.dataset.translateX || 0;
|
| 1011 |
+
const y = img.dataset.translateY || 0;
|
| 1012 |
+
img.style.transform = `translateX(${x}px) translateY(${y}px) scale(${zoom})`;
|
| 1013 |
+
img.classList.toggle('pannable', zoom > 1);
|
| 1014 |
}
|
| 1015 |
|
| 1016 |
function handleZoom(event) {
|
| 1017 |
+
if (!currentlySelectedPanel) return;
|
| 1018 |
+
const img = currentlySelectedPanel.querySelector('img');
|
| 1019 |
+
img.dataset.zoom = event.target.value;
|
| 1020 |
+
updateImageTransform(img);
|
| 1021 |
}
|
| 1022 |
|
| 1023 |
function resetPanelTransform() {
|
|
|
|
| 1031 |
}
|
| 1032 |
|
| 1033 |
function startPan(event) {
|
| 1034 |
+
if (event.button !== 0) return;
|
| 1035 |
+
const img = event.target;
|
| 1036 |
+
if (parseFloat(img.dataset.zoom || 100) <= 100) return;
|
| 1037 |
+
event.preventDefault();
|
| 1038 |
+
isPanning = true;
|
| 1039 |
+
img.classList.add('panning');
|
| 1040 |
+
panStartX = event.clientX;
|
| 1041 |
+
panStartY = event.clientY;
|
| 1042 |
+
panStartTranslateX = parseFloat(img.dataset.translateX || 0);
|
| 1043 |
+
panStartTranslateY = parseFloat(img.dataset.translateY || 0);
|
| 1044 |
}
|
| 1045 |
|
| 1046 |
function panImage(event) {
|
| 1047 |
+
if (!isPanning || !currentlySelectedPanel) return;
|
| 1048 |
+
const img = currentlySelectedPanel.querySelector('img');
|
| 1049 |
+
img.dataset.translateX = panStartTranslateX + (event.clientX - panStartX);
|
| 1050 |
+
img.dataset.translateY = panStartTranslateY + (event.clientY - panStartY);
|
| 1051 |
+
updateImageTransform(img);
|
| 1052 |
}
|
| 1053 |
|
| 1054 |
function stopPan() {
|
| 1055 |
+
if (!isPanning) return;
|
| 1056 |
+
isPanning = false;
|
| 1057 |
+
currentlySelectedPanel?.querySelector('img')?.classList.remove('panning');
|
| 1058 |
}
|
| 1059 |
|
| 1060 |
function addBubbleToPanel() {
|
|
|
|
| 1072 |
}
|
| 1073 |
|
| 1074 |
function gotoTimestamp() {
|
| 1075 |
+
if (!currentlySelectedPanel) { alert("Please select a panel first."); return; }
|
| 1076 |
+
const input = document.getElementById('timestamp-input');
|
| 1077 |
+
const timeStr = input.value.trim();
|
| 1078 |
+
if (!timeStr) return;
|
| 1079 |
+
let parsedSeconds = 0;
|
| 1080 |
+
if (timeStr.includes(':')) {
|
| 1081 |
+
const parts = timeStr.split(':');
|
| 1082 |
+
parsedSeconds = parseInt(parts[0], 10) * 60 + parseFloat(parts[1]);
|
| 1083 |
+
} else {
|
| 1084 |
+
parsedSeconds = parseFloat(timeStr);
|
| 1085 |
+
}
|
| 1086 |
+
if (isNaN(parsedSeconds)) { alert("Invalid time format."); return; }
|
| 1087 |
+
const img = currentlySelectedPanel.querySelector('img');
|
| 1088 |
+
let filename = img.src.substring(img.src.lastIndexOf('/') + 1).split('?')[0];
|
| 1089 |
+
img.style.opacity = '0.5';
|
| 1090 |
+
fetch('/goto_timestamp', {
|
| 1091 |
+
method: 'POST',
|
| 1092 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1093 |
+
body: JSON.stringify({ filename, timestamp: parsedSeconds })
|
| 1094 |
+
})
|
| 1095 |
+
.then(res => res.json())
|
| 1096 |
+
.then(data => {
|
| 1097 |
+
if (data.success) {
|
| 1098 |
+
img.src = `/frames/final/${filename}?t=${new Date().getTime()}`;
|
| 1099 |
+
input.value = '';
|
| 1100 |
+
resetPanelTransform();
|
| 1101 |
+
} else { alert('Error: ' + data.message); }
|
| 1102 |
+
img.style.opacity = '1';
|
| 1103 |
+
})
|
| 1104 |
+
.catch(() => {
|
| 1105 |
+
alert('An error occurred.');
|
| 1106 |
+
img.style.opacity = '1';
|
| 1107 |
+
});
|
| 1108 |
}
|
| 1109 |
</script>
|
| 1110 |
</body>
|