Update app_enhanced.py
Browse files- app_enhanced.py +171 -94
app_enhanced.py
CHANGED
|
@@ -118,8 +118,7 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
|
|
| 118 |
raw_moments = [{'text': s.content, 'start': s.start.total_seconds(), 'end': s.end.total_seconds()} for s in valid_subs]
|
| 119 |
|
| 120 |
if target_pages <= 0: target_pages = 1
|
| 121 |
-
|
| 122 |
-
panels_per_page = 5
|
| 123 |
total_panels_needed = target_pages * panels_per_page
|
| 124 |
|
| 125 |
selected_moments = []
|
|
@@ -177,16 +176,12 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
|
|
| 177 |
elif '!' in dialogue and dialogue.isupper(): b_type = 'reaction'
|
| 178 |
elif '?' in dialogue: b_type = 'speech'
|
| 179 |
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
b = bubble(dialog=dialogue, bubble_offset_x=bx, bubble_offset_y=by, lip_x=lip[0], lip_y=lip[1], type=b_type)
|
| 187 |
-
bubbles_list.append(b)
|
| 188 |
-
except:
|
| 189 |
-
bubbles_list.append(bubble(dialog=dialogue, type=b_type))
|
| 190 |
|
| 191 |
pages = []
|
| 192 |
for i in range(target_pages):
|
|
@@ -336,41 +331,41 @@ INDEX_HTML = '''
|
|
| 336 |
/* ========================================= */
|
| 337 |
/* 🎨 NEW POLYGON TEMPLATE LAYOUT CSS */
|
| 338 |
/* ========================================= */
|
| 339 |
-
|
| 340 |
-
/*
|
| 341 |
-
Total Width: 1000px
|
| 342 |
-
Tier Height: 350px
|
| 343 |
-
Gutter: 10px
|
| 344 |
-
|
| 345 |
-
Row 1 Y: 0 - 350
|
| 346 |
-
Row 2 Y: 360 - 710
|
| 347 |
-
Total Height: 710px
|
| 348 |
-
*/
|
| 349 |
|
| 350 |
.comic-wrapper { max-width: 1050px; margin: 0 auto; }
|
| 351 |
.page-wrapper { margin: 30px auto; display: flex; flex-direction: column; align-items: center; }
|
| 352 |
.page-title { text-align: center; color: #333; margin-bottom: 10px; font-size: 18px; font-weight: bold; }
|
| 353 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 354 |
.comic-page {
|
| 355 |
background: white;
|
| 356 |
width: 1000px;
|
| 357 |
height: 710px;
|
| 358 |
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
|
| 359 |
position: relative;
|
| 360 |
-
|
| 361 |
-
|
| 362 |
}
|
| 363 |
|
| 364 |
-
/*
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
.panel {
|
| 366 |
position: absolute;
|
| 367 |
background: #eee;
|
| 368 |
cursor: pointer;
|
| 369 |
-
overflow: hidden;
|
| 370 |
-
|
| 371 |
}
|
| 372 |
-
|
| 373 |
-
.panel.selected { filter: brightness(0.9) sepia(0.2); z-index: 5; }
|
| 374 |
|
| 375 |
.panel img {
|
| 376 |
width: 100%;
|
|
@@ -382,54 +377,71 @@ INDEX_HTML = '''
|
|
| 382 |
.panel img.pannable { cursor: grab; }
|
| 383 |
.panel img.panning { cursor: grabbing; }
|
| 384 |
|
| 385 |
-
/*
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
Row 1 Top: 0px. Row 1 Height: 350px.
|
| 393 |
-
Row 2 Top: 360px. Row 2 Height: 350px.
|
| 394 |
*/
|
| 395 |
-
|
| 396 |
-
/* --- TIER 1 --- */
|
| 397 |
-
/* Panel 0 (Top Left) */
|
| 398 |
.panel-0 {
|
| 399 |
-
top: 0; left: 0; width:
|
| 400 |
-
clip-path: polygon(0% 0%,
|
| 401 |
}
|
| 402 |
|
| 403 |
-
/* Panel 1 (Top Right)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 404 |
.panel-1 {
|
| 405 |
-
top: 0; left:
|
| 406 |
-
clip-path: polygon(
|
| 407 |
}
|
| 408 |
|
| 409 |
-
/* --- TIER 2 --- */
|
| 410 |
-
/*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 411 |
.panel-2 {
|
| 412 |
-
top: 360px; left: 0; width:
|
| 413 |
-
clip-path: polygon(0% 0%,
|
| 414 |
}
|
| 415 |
|
| 416 |
-
/* Panel 3 (Bottom Middle)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
.panel-3 {
|
| 418 |
-
top: 360px; left:
|
| 419 |
-
clip-path: polygon(
|
| 420 |
}
|
| 421 |
|
| 422 |
-
/* Panel 4 (Bottom Right)
|
|
|
|
|
|
|
|
|
|
| 423 |
.panel-4 {
|
| 424 |
-
top: 360px; left:
|
| 425 |
-
clip-path: polygon(
|
| 426 |
}
|
| 427 |
|
| 428 |
/* SPEECH BUBBLES */
|
|
|
|
| 429 |
.speech-bubble {
|
| 430 |
position: absolute; display: flex; justify-content: center; align-items: center;
|
| 431 |
width: 150px; height: 80px; min-width: 50px; min-height: 30px; box-sizing: border-box;
|
| 432 |
-
z-index:
|
|
|
|
| 433 |
font-size: 13px; text-align: center;
|
| 434 |
overflow: visible;
|
| 435 |
line-height: 1.2;
|
|
@@ -453,7 +465,7 @@ INDEX_HTML = '''
|
|
| 453 |
border-radius: inherit;
|
| 454 |
}
|
| 455 |
|
| 456 |
-
.speech-bubble.selected { outline: 2px dashed #4CAF50; z-index:
|
| 457 |
.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); text-align:center; padding:8px; z-index:102; resize:none; font: inherit; white-space: pre-wrap; }
|
| 458 |
|
| 459 |
/* SPEECH BUBBLE CSS (Tails) */
|
|
@@ -722,31 +734,44 @@ INDEX_HTML = '''
|
|
| 722 |
} catch(e) { console.error(e); alert("Failed to restore."); }
|
| 723 |
}
|
| 724 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 725 |
function getCurrentState() {
|
| 726 |
const pages = [];
|
| 727 |
document.querySelectorAll('.comic-page').forEach(p => {
|
| 728 |
const panels = [];
|
|
|
|
|
|
|
| 729 |
p.querySelectorAll('.panel').forEach(pan => {
|
| 730 |
const img = pan.querySelector('img');
|
| 731 |
-
const bubbles = [];
|
| 732 |
-
pan.querySelectorAll('.speech-bubble').forEach(b => {
|
| 733 |
-
const textEl = b.querySelector('.bubble-text');
|
| 734 |
-
bubbles.push({
|
| 735 |
-
text: textEl ? textEl.textContent : '',
|
| 736 |
-
left: b.style.left, top: b.style.top, width: b.style.width, height: b.style.height,
|
| 737 |
-
classes: b.className.replace(' selected', ''),
|
| 738 |
-
type: b.dataset.type, font: b.style.fontFamily,
|
| 739 |
-
tailPos: b.style.getPropertyValue('--tail-pos'),
|
| 740 |
-
colors: { fill: b.style.getPropertyValue('--bubble-fill-color'), text: b.style.getPropertyValue('--bubble-text-color') }
|
| 741 |
-
});
|
| 742 |
-
});
|
| 743 |
panels.push({
|
| 744 |
src: img.src,
|
| 745 |
zoom: img.dataset.zoom, tx: img.dataset.translateX, ty: img.dataset.translateY,
|
| 746 |
-
bubbles:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 747 |
});
|
| 748 |
});
|
| 749 |
-
|
|
|
|
| 750 |
});
|
| 751 |
return pages;
|
| 752 |
}
|
|
@@ -764,12 +789,11 @@ INDEX_HTML = '''
|
|
| 764 |
pageWrapper.appendChild(pageTitle);
|
| 765 |
|
| 766 |
const div = document.createElement('div');
|
| 767 |
-
div.className = 'comic-page';
|
| 768 |
|
| 769 |
-
// Render
|
| 770 |
page.panels.forEach((pan, idx) => {
|
| 771 |
const pDiv = document.createElement('div');
|
| 772 |
-
// Cycle through 0-4 for layout positions if there are more panels, or just use idx
|
| 773 |
const posClass = `panel-${idx % 5}`;
|
| 774 |
pDiv.className = `panel ${posClass}`;
|
| 775 |
|
|
@@ -780,10 +804,33 @@ INDEX_HTML = '''
|
|
| 780 |
updateImageTransform(img);
|
| 781 |
img.onmousedown = (e) => startPan(e, img);
|
| 782 |
pDiv.appendChild(img);
|
| 783 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 784 |
div.appendChild(pDiv);
|
| 785 |
});
|
| 786 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 787 |
pageWrapper.appendChild(div);
|
| 788 |
con.appendChild(pageWrapper);
|
| 789 |
});
|
|
@@ -817,18 +864,45 @@ INDEX_HTML = '''
|
|
| 817 |
|
| 818 |
function loadNewComic() {
|
| 819 |
fetch(`/output/pages.json?sid=${sid}`).then(r=>r.json()).then(data => {
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 832 |
renderFromState(cleanData); saveDraft(true);
|
| 833 |
});
|
| 834 |
}
|
|
@@ -914,9 +988,15 @@ INDEX_HTML = '''
|
|
| 914 |
}
|
| 915 |
|
| 916 |
function addBubble() {
|
| 917 |
-
|
| 918 |
-
|
| 919 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 920 |
}
|
| 921 |
|
| 922 |
function deleteBubble() {
|
|
@@ -986,12 +1066,9 @@ INDEX_HTML = '''
|
|
| 986 |
alert(`Exporting ${pgs.length} page(s)...`);
|
| 987 |
|
| 988 |
// --- 0% ERROR FIX ---
|
| 989 |
-
// 1. Lock specific pixel dimensions + 1px buffer to prevent word wrapping
|
| 990 |
const bubbles = document.querySelectorAll('.speech-bubble');
|
| 991 |
bubbles.forEach(b => {
|
| 992 |
const rect = b.getBoundingClientRect();
|
| 993 |
-
// Add slight buffer (1px) to width to handle sub-pixel rendering differences
|
| 994 |
-
// This prevents "just fitting" words from wrapping in the export
|
| 995 |
b.style.width = (rect.width + 1) + 'px';
|
| 996 |
b.style.height = rect.height + 'px';
|
| 997 |
b.style.display = 'flex';
|
|
|
|
| 118 |
raw_moments = [{'text': s.content, 'start': s.start.total_seconds(), 'end': s.end.total_seconds()} for s in valid_subs]
|
| 119 |
|
| 120 |
if target_pages <= 0: target_pages = 1
|
| 121 |
+
panels_per_page = 5
|
|
|
|
| 122 |
total_panels_needed = target_pages * panels_per_page
|
| 123 |
|
| 124 |
selected_moments = []
|
|
|
|
| 176 |
elif '!' in dialogue and dialogue.isupper(): b_type = 'reaction'
|
| 177 |
elif '?' in dialogue: b_type = 'speech'
|
| 178 |
|
| 179 |
+
# Use simpler positioning logic for the backend; user adjusts in frontend
|
| 180 |
+
# Just putting default offsets
|
| 181 |
+
bx, by = 50, 50
|
| 182 |
+
|
| 183 |
+
b = bubble(dialog=dialogue, bubble_offset_x=bx, bubble_offset_y=by, type=b_type)
|
| 184 |
+
bubbles_list.append(b)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
|
| 186 |
pages = []
|
| 187 |
for i in range(target_pages):
|
|
|
|
| 331 |
/* ========================================= */
|
| 332 |
/* 🎨 NEW POLYGON TEMPLATE LAYOUT CSS */
|
| 333 |
/* ========================================= */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 334 |
|
| 335 |
.comic-wrapper { max-width: 1050px; margin: 0 auto; }
|
| 336 |
.page-wrapper { margin: 30px auto; display: flex; flex-direction: column; align-items: center; }
|
| 337 |
.page-title { text-align: center; color: #333; margin-bottom: 10px; font-size: 18px; font-weight: bold; }
|
| 338 |
|
| 339 |
+
/*
|
| 340 |
+
The Comic Page Container
|
| 341 |
+
Width: 1000px, Height: 710px
|
| 342 |
+
Overflow must be VISIBLE so bubbles can pop out if needed,
|
| 343 |
+
but we usually want them contained.
|
| 344 |
+
Actually, to fix "missing bubbles", we must allow them to sit on top of everything.
|
| 345 |
+
*/
|
| 346 |
.comic-page {
|
| 347 |
background: white;
|
| 348 |
width: 1000px;
|
| 349 |
height: 710px;
|
| 350 |
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
|
| 351 |
position: relative;
|
| 352 |
+
/* Ensure bubbles (children of this) are not hidden by panel clipping */
|
| 353 |
+
z-index: 1;
|
| 354 |
}
|
| 355 |
|
| 356 |
+
/*
|
| 357 |
+
PANELS:
|
| 358 |
+
Now defined with specific bounding boxes so images are not stretched/zoomed incorrectly.
|
| 359 |
+
Each panel has overflow:hidden to clip the image, but the bubbles will be siblings, not children.
|
| 360 |
+
*/
|
| 361 |
.panel {
|
| 362 |
position: absolute;
|
| 363 |
background: #eee;
|
| 364 |
cursor: pointer;
|
| 365 |
+
overflow: hidden;
|
| 366 |
+
z-index: 0;
|
| 367 |
}
|
| 368 |
+
.panel.selected { filter: brightness(0.9) sepia(0.2); z-index: 0; outline: 3px solid #2196F3; }
|
|
|
|
| 369 |
|
| 370 |
.panel img {
|
| 371 |
width: 100%;
|
|
|
|
| 377 |
.panel img.pannable { cursor: grab; }
|
| 378 |
.panel img.panning { cursor: grabbing; }
|
| 379 |
|
| 380 |
+
/* --- TIER 1 (Y: 0 - 350) --- */
|
| 381 |
+
|
| 382 |
+
/* Panel 0 (Top Left)
|
| 383 |
+
Starts at 0, ends at ~635.
|
| 384 |
+
Polygon: (0,0) -> (635.2, 0) -> (588.2, 350) -> (0, 350)
|
| 385 |
+
Box Width: ~636px. Box Height: 350px.
|
|
|
|
|
|
|
|
|
|
| 386 |
*/
|
|
|
|
|
|
|
|
|
|
| 387 |
.panel-0 {
|
| 388 |
+
top: 0; left: 0; width: 636px; height: 350px;
|
| 389 |
+
clip-path: polygon(0% 0%, 100% 0%, 92.5% 100%, 0% 100%);
|
| 390 |
}
|
| 391 |
|
| 392 |
+
/* Panel 1 (Top Right)
|
| 393 |
+
Starts at ~588 (to overlap/fit), ends at 1000.
|
| 394 |
+
Polygon: (635.2 + gap) ... actually lets follow the user coords:
|
| 395 |
+
Divider Top: 635.2, Bottom: 588.2.
|
| 396 |
+
So Right Panel starts after the divider.
|
| 397 |
+
Let's define Right Panel Box:
|
| 398 |
+
Left: 588px (min X), Width: 412px (1000-588).
|
| 399 |
+
Clip: (Top-Left of box is 588). Top-Left coord of polygon is 635.2. Relative X = 635.2 - 588 = 47.2.
|
| 400 |
+
So polygon starts at X=47.2px relative to box.
|
| 401 |
+
*/
|
| 402 |
.panel-1 {
|
| 403 |
+
top: 0; left: 588px; width: 412px; height: 350px;
|
| 404 |
+
clip-path: polygon(11.5% 0%, 100% 0%, 100% 100%, 0% 100%);
|
| 405 |
}
|
| 406 |
|
| 407 |
+
/* --- TIER 2 (Y: 360 - 710) Height 350 --- */
|
| 408 |
+
/* Gutter is 10px, so T2 starts at 360px */
|
| 409 |
+
|
| 410 |
+
/* Panel 2 (Bottom Left)
|
| 411 |
+
Coords: (0,0) -> (293.2, 0) -> (326.2, 350) -> (0, 350)
|
| 412 |
+
Box: Left 0, Width 327px.
|
| 413 |
+
*/
|
| 414 |
.panel-2 {
|
| 415 |
+
top: 360px; left: 0; width: 327px; height: 350px;
|
| 416 |
+
clip-path: polygon(0% 0%, 89.6% 0%, 100% 100%, 0% 100%);
|
| 417 |
}
|
| 418 |
|
| 419 |
+
/* Panel 3 (Bottom Middle)
|
| 420 |
+
Left Divider: Top 293.2, Bot 326.2.
|
| 421 |
+
Right Divider: Top 617.2, Bot 666.2.
|
| 422 |
+
Box Left: 293px. Width: 374px (667-293).
|
| 423 |
+
*/
|
| 424 |
.panel-3 {
|
| 425 |
+
top: 360px; left: 293px; width: 374px; height: 350px;
|
| 426 |
+
clip-path: polygon(0% 0%, 86.6% 0%, 100% 100%, 8.8% 100%);
|
| 427 |
}
|
| 428 |
|
| 429 |
+
/* Panel 4 (Bottom Right)
|
| 430 |
+
Right Divider: Top 617.2, Bot 666.2.
|
| 431 |
+
Box Left: 617px. Width: 383px (1000-617).
|
| 432 |
+
*/
|
| 433 |
.panel-4 {
|
| 434 |
+
top: 360px; left: 617px; width: 383px; height: 350px;
|
| 435 |
+
clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 12.8% 100%);
|
| 436 |
}
|
| 437 |
|
| 438 |
/* SPEECH BUBBLES */
|
| 439 |
+
/* Positioned absolute relative to .comic-page now */
|
| 440 |
.speech-bubble {
|
| 441 |
position: absolute; display: flex; justify-content: center; align-items: center;
|
| 442 |
width: 150px; height: 80px; min-width: 50px; min-height: 30px; box-sizing: border-box;
|
| 443 |
+
z-index: 100; /* Above panels */
|
| 444 |
+
cursor: move; font-family: 'Comic Neue', cursive; font-weight: bold;
|
| 445 |
font-size: 13px; text-align: center;
|
| 446 |
overflow: visible;
|
| 447 |
line-height: 1.2;
|
|
|
|
| 465 |
border-radius: inherit;
|
| 466 |
}
|
| 467 |
|
| 468 |
+
.speech-bubble.selected { outline: 2px dashed #4CAF50; z-index: 101; }
|
| 469 |
.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); text-align:center; padding:8px; z-index:102; resize:none; font: inherit; white-space: pre-wrap; }
|
| 470 |
|
| 471 |
/* SPEECH BUBBLE CSS (Tails) */
|
|
|
|
| 734 |
} catch(e) { console.error(e); alert("Failed to restore."); }
|
| 735 |
}
|
| 736 |
|
| 737 |
+
// UPDATED STATE RETRIEVAL
|
| 738 |
+
// Bubbles are now children of comic-page, not panels.
|
| 739 |
+
// Panels need to be associated with bubbles still for data consistency.
|
| 740 |
+
// For simplicity, we will save bubbles as a list belonging to the page,
|
| 741 |
+
// but the backend format expects bubbles inside panels.
|
| 742 |
+
// We will attempt to reconstruct that structure roughly or just store them.
|
| 743 |
+
// Actually, let's just store "page bubbles" separately in our new save format if needed,
|
| 744 |
+
// OR we can map bubbles back to panels spatially.
|
| 745 |
+
// SIMPLER APPROACH: Save the structure as it is visually (Page -> Panels, Page -> Bubbles).
|
| 746 |
+
|
| 747 |
function getCurrentState() {
|
| 748 |
const pages = [];
|
| 749 |
document.querySelectorAll('.comic-page').forEach(p => {
|
| 750 |
const panels = [];
|
| 751 |
+
const bubbles = []; // Page-level bubbles
|
| 752 |
+
|
| 753 |
p.querySelectorAll('.panel').forEach(pan => {
|
| 754 |
const img = pan.querySelector('img');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 755 |
panels.push({
|
| 756 |
src: img.src,
|
| 757 |
zoom: img.dataset.zoom, tx: img.dataset.translateX, ty: img.dataset.translateY,
|
| 758 |
+
bubbles: [] // Legacy structure kept empty
|
| 759 |
+
});
|
| 760 |
+
});
|
| 761 |
+
|
| 762 |
+
p.querySelectorAll('.speech-bubble').forEach(b => {
|
| 763 |
+
const textEl = b.querySelector('.bubble-text');
|
| 764 |
+
bubbles.push({
|
| 765 |
+
text: textEl ? textEl.textContent : '',
|
| 766 |
+
left: b.style.left, top: b.style.top, width: b.style.width, height: b.style.height,
|
| 767 |
+
classes: b.className.replace(' selected', ''),
|
| 768 |
+
type: b.dataset.type, font: b.style.fontFamily,
|
| 769 |
+
tailPos: b.style.getPropertyValue('--tail-pos'),
|
| 770 |
+
colors: { fill: b.style.getPropertyValue('--bubble-fill-color'), text: b.style.getPropertyValue('--bubble-text-color') }
|
| 771 |
});
|
| 772 |
});
|
| 773 |
+
|
| 774 |
+
pages.push({ panels: panels, pageBubbles: bubbles });
|
| 775 |
});
|
| 776 |
return pages;
|
| 777 |
}
|
|
|
|
| 789 |
pageWrapper.appendChild(pageTitle);
|
| 790 |
|
| 791 |
const div = document.createElement('div');
|
| 792 |
+
div.className = 'comic-page';
|
| 793 |
|
| 794 |
+
// 1. Render Panels
|
| 795 |
page.panels.forEach((pan, idx) => {
|
| 796 |
const pDiv = document.createElement('div');
|
|
|
|
| 797 |
const posClass = `panel-${idx % 5}`;
|
| 798 |
pDiv.className = `panel ${posClass}`;
|
| 799 |
|
|
|
|
| 804 |
updateImageTransform(img);
|
| 805 |
img.onmousedown = (e) => startPan(e, img);
|
| 806 |
pDiv.appendChild(img);
|
| 807 |
+
|
| 808 |
+
// Legacy support: if bubbles were inside panels
|
| 809 |
+
(pan.bubbles || []).forEach(bData => {
|
| 810 |
+
// Convert old relative coords (if any) to page relative?
|
| 811 |
+
// Ideally data is standardized. We append to Page div, not pDiv.
|
| 812 |
+
const b = createBubbleHTML(bData);
|
| 813 |
+
div.appendChild(b);
|
| 814 |
+
});
|
| 815 |
+
|
| 816 |
div.appendChild(pDiv);
|
| 817 |
});
|
| 818 |
|
| 819 |
+
// 2. Render Page-Level Bubbles (New Structure)
|
| 820 |
+
if(page.pageBubbles) {
|
| 821 |
+
page.pageBubbles.forEach(bData => {
|
| 822 |
+
div.appendChild(createBubbleHTML(bData));
|
| 823 |
+
});
|
| 824 |
+
}
|
| 825 |
+
|
| 826 |
+
// Bind click to deselect
|
| 827 |
+
div.onclick = (e) => {
|
| 828 |
+
if(e.target === div) {
|
| 829 |
+
if(selectedBubble) selectedBubble.classList.remove('selected'); selectedBubble = null;
|
| 830 |
+
if(selectedPanel) selectedPanel.classList.remove('selected'); selectedPanel = null;
|
| 831 |
+
}
|
| 832 |
+
}
|
| 833 |
+
|
| 834 |
pageWrapper.appendChild(div);
|
| 835 |
con.appendChild(pageWrapper);
|
| 836 |
});
|
|
|
|
| 864 |
|
| 865 |
function loadNewComic() {
|
| 866 |
fetch(`/output/pages.json?sid=${sid}`).then(r=>r.json()).then(data => {
|
| 867 |
+
// Convert Backend Data (Bubbles in Panels) to Frontend Data (Page Bubbles)
|
| 868 |
+
const cleanData = data.map((p, pi) => {
|
| 869 |
+
const panels = [];
|
| 870 |
+
const pageBubbles = [];
|
| 871 |
+
|
| 872 |
+
// Panel Geometry for Default Bubble Placement mapping
|
| 873 |
+
// We have 5 panels. We know their rough centers.
|
| 874 |
+
const panelCenters = [
|
| 875 |
+
{x: 315, y: 175}, // 0
|
| 876 |
+
{x: 790, y: 175}, // 1
|
| 877 |
+
{x: 160, y: 535}, // 2
|
| 878 |
+
{x: 480, y: 535}, // 3
|
| 879 |
+
{x: 800, y: 535} // 4
|
| 880 |
+
];
|
| 881 |
+
|
| 882 |
+
p.panels.forEach((pan, j) => {
|
| 883 |
+
panels.push({
|
| 884 |
+
src: `/frames/${pan.image}?sid=${sid}`,
|
| 885 |
+
bubbles: []
|
| 886 |
+
});
|
| 887 |
+
|
| 888 |
+
if(p.bubbles && p.bubbles[j] && p.bubbles[j].dialog) {
|
| 889 |
+
// Backend gives an offset relative to panel 0,0 usually.
|
| 890 |
+
// But now panels have offsets.
|
| 891 |
+
// Let's just place bubble near the center of the panel.
|
| 892 |
+
const center = panelCenters[j % 5] || {x:500, y:350};
|
| 893 |
+
|
| 894 |
+
pageBubbles.push({
|
| 895 |
+
text: p.bubbles[j].dialog,
|
| 896 |
+
left: (center.x - 75) + 'px', // Center - half bubble width
|
| 897 |
+
top: (center.y - 40) + 'px',
|
| 898 |
+
type: (p.bubbles[j].type || 'speech'),
|
| 899 |
+
classes: `speech-bubble ${p.bubbles[j].type || 'speech'} tail-bottom`
|
| 900 |
+
});
|
| 901 |
+
}
|
| 902 |
+
});
|
| 903 |
+
|
| 904 |
+
return { panels: panels, pageBubbles: pageBubbles };
|
| 905 |
+
});
|
| 906 |
renderFromState(cleanData); saveDraft(true);
|
| 907 |
});
|
| 908 |
}
|
|
|
|
| 988 |
}
|
| 989 |
|
| 990 |
function addBubble() {
|
| 991 |
+
// Add to the active page container
|
| 992 |
+
// If no panel is selected, finding the active page is tricky.
|
| 993 |
+
// We will default to the first page or the last clicked page context if we stored it.
|
| 994 |
+
// For now, if a panel is selected, add to that panel's parent page.
|
| 995 |
+
if(!selectedPanel) return alert("Select a panel to define which page to add to.");
|
| 996 |
+
|
| 997 |
+
const pageDiv = selectedPanel.parentElement;
|
| 998 |
+
const b = createBubbleHTML({ text: "Text", left: "50px", top: "50px", type: 'speech', classes: "speech-bubble speech tail-bottom" });
|
| 999 |
+
pageDiv.appendChild(b); selectBubble(b); saveDraft(true);
|
| 1000 |
}
|
| 1001 |
|
| 1002 |
function deleteBubble() {
|
|
|
|
| 1066 |
alert(`Exporting ${pgs.length} page(s)...`);
|
| 1067 |
|
| 1068 |
// --- 0% ERROR FIX ---
|
|
|
|
| 1069 |
const bubbles = document.querySelectorAll('.speech-bubble');
|
| 1070 |
bubbles.forEach(b => {
|
| 1071 |
const rect = b.getBoundingClientRect();
|
|
|
|
|
|
|
| 1072 |
b.style.width = (rect.width + 1) + 'px';
|
| 1073 |
b.style.height = rect.height + 'px';
|
| 1074 |
b.style.display = 'flex';
|