Update app_enhanced.py
Browse files- app_enhanced.py +100 -49
app_enhanced.py
CHANGED
|
@@ -336,27 +336,30 @@ INDEX_HTML = '''
|
|
| 336 |
z-index: 1;
|
| 337 |
overflow: hidden;
|
| 338 |
/* UPDATED: THICK WHITE BORDER */
|
| 339 |
-
border:
|
| 340 |
-
box-shadow: 0 0
|
|
|
|
| 341 |
}
|
| 342 |
|
| 343 |
/* HANDLES FOR DRAGGING */
|
| 344 |
.handle {
|
| 345 |
position: absolute;
|
| 346 |
-
/* UPDATED: LARGER HANDLES */
|
| 347 |
-
width:
|
| 348 |
background: #ff4757;
|
| 349 |
border: 3px solid white;
|
| 350 |
border-radius: 50%;
|
| 351 |
cursor: ew-resize;
|
| 352 |
z-index: 999;
|
| 353 |
transform: translate(-50%, -50%);
|
| 354 |
-
box-shadow: 0
|
|
|
|
| 355 |
}
|
|
|
|
| 356 |
.handle.horiz {
|
| 357 |
cursor: ns-resize;
|
| 358 |
-
width:
|
| 359 |
-
border-radius:
|
| 360 |
}
|
| 361 |
|
| 362 |
/* PANELS */
|
|
@@ -383,10 +386,12 @@ INDEX_HTML = '''
|
|
| 383 |
position: absolute; display: flex; justify-content: center; align-items: center;
|
| 384 |
width: auto; height: auto; min-width: 80px; min-height: 50px; max-width: 250px;
|
| 385 |
box-sizing: border-box;
|
|
|
|
| 386 |
z-index: 500;
|
| 387 |
cursor: move; font-family: 'Comic Neue', cursive; font-weight: bold;
|
| 388 |
font-size: 13px; text-align: center;
|
| 389 |
line-height: 1.2; --tail-pos: 50%; padding: 5px;
|
|
|
|
| 390 |
}
|
| 391 |
.speech-bubble.selected { outline: 2px dashed #4CAF50; z-index: 501; }
|
| 392 |
|
|
@@ -516,8 +521,11 @@ INDEX_HTML = '''
|
|
| 516 |
let currentSaveCode = null;
|
| 517 |
let isProcessing = false;
|
| 518 |
let interval, selectedBubble = null, selectedPanel = null;
|
| 519 |
-
|
| 520 |
-
|
|
|
|
|
|
|
|
|
|
| 521 |
let activeLayoutHandle = null, activePageId = null;
|
| 522 |
|
| 523 |
const DEFAULT_LAYOUT = {
|
|
@@ -632,6 +640,7 @@ INDEX_HTML = '''
|
|
| 632 |
div.id = pageId;
|
| 633 |
div.dataset.layout = JSON.stringify(layout);
|
| 634 |
|
|
|
|
| 635 |
for(let i=0; i<4; i++) {
|
| 636 |
const pDiv = document.createElement('div');
|
| 637 |
pDiv.className = 'panel'; pDiv.id = `${pageId}-p${i}`;
|
|
@@ -647,6 +656,7 @@ INDEX_HTML = '''
|
|
| 647 |
div.appendChild(pDiv);
|
| 648 |
}
|
| 649 |
|
|
|
|
| 650 |
const handles = [
|
| 651 |
{id: 'h1-top', t:0, l:0}, {id: 'h1-bot', t:0, l:0},
|
| 652 |
{id: 'h2-top', t:0, l:0}, {id: 'h2-bot', t:0, l:0},
|
|
@@ -715,42 +725,97 @@ INDEX_HTML = '''
|
|
| 715 |
setH('h-tier', w/2, ty);
|
| 716 |
}
|
| 717 |
|
|
|
|
|
|
|
| 718 |
function startDragHandle(e, pageId, handleRole) {
|
| 719 |
e.stopPropagation(); e.preventDefault();
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 723 |
}
|
| 724 |
|
|
|
|
| 725 |
document.addEventListener('mousemove', (e) => {
|
| 726 |
-
if(
|
| 727 |
-
|
|
|
|
|
|
|
| 728 |
const rect = pageEl.getBoundingClientRect();
|
| 729 |
const state = JSON.parse(pageEl.dataset.layout);
|
| 730 |
const x = Math.max(0, Math.min(1000, (e.clientX - rect.left) * (1000 / rect.width)));
|
| 731 |
const y = Math.max(50, Math.min(650, (e.clientY - rect.top) * (700 / rect.height)));
|
|
|
|
| 732 |
|
| 733 |
-
if(
|
| 734 |
-
else if(
|
| 735 |
-
else if(
|
| 736 |
-
else if(
|
| 737 |
-
else if(
|
| 738 |
|
| 739 |
pageEl.dataset.layout = JSON.stringify(state);
|
| 740 |
-
drawLayout(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 741 |
}
|
| 742 |
-
else if(isDragging && selectedBubble) {
|
| 743 |
-
selectedBubble.style.left = (initX + e.clientX - startX) + 'px';
|
| 744 |
-
selectedBubble.style.top = (initY + e.clientY - startY) + 'px';
|
| 745 |
-
}
|
| 746 |
-
else if(isResizing && selectedBubble) { resizeBubble(e); }
|
| 747 |
-
else if(isPanning && selectedPanel) { panImage(e); }
|
| 748 |
});
|
| 749 |
|
| 750 |
document.addEventListener('mouseup', () => {
|
| 751 |
-
if(
|
| 752 |
-
|
| 753 |
-
|
|
|
|
|
|
|
| 754 |
});
|
| 755 |
|
| 756 |
function updateGutter(val) {
|
|
@@ -773,13 +838,12 @@ INDEX_HTML = '''
|
|
| 773 |
return;
|
| 774 |
}
|
| 775 |
|
| 776 |
-
// CONTAIN LOGIC: Ensure WHOLE image fits
|
| 777 |
const pW = 1000;
|
| 778 |
const pH = panel.offsetHeight;
|
| 779 |
const iW = img.naturalWidth || img.width;
|
| 780 |
const iH = img.naturalHeight || img.height;
|
| 781 |
|
| 782 |
-
//
|
| 783 |
const scale = Math.min(pW / iW, pH / iH);
|
| 784 |
|
| 785 |
const tx = (pW - iW * scale) / 2;
|
|
@@ -802,21 +866,9 @@ INDEX_HTML = '''
|
|
| 802 |
img.classList.toggle('pannable', true);
|
| 803 |
}
|
| 804 |
|
| 805 |
-
function startPan(e, img) { e.preventDefault(); isPanning = true; selectedPanel = img.closest('.panel'); panStartX = e.clientX; panStartY = e.clientY; panStartTx = parseFloat(img.dataset.translateX || 0); panStartTy = parseFloat(img.dataset.translateY || 0); img.classList.add('panning'); }
|
| 806 |
-
function panImage(e) { if(!isPanning || !selectedPanel) return; const img = selectedPanel.querySelector('img'); img.dataset.translateX = panStartTx + (e.clientX - panStartX); img.dataset.translateY = panStartTy + (e.clientY - panStartY); updateImageTransform(img); }
|
| 807 |
function handleZoom(el) { if(!selectedPanel) return; const img = selectedPanel.querySelector('img'); img.dataset.zoom = el.value; updateImageTransform(img); }
|
| 808 |
function resetPanelTransform() { if(!selectedPanel) return alert("Select a panel"); const img = selectedPanel.querySelector('img'); fitImageToPanel(img, selectedPanel, null); saveDraft(true); }
|
| 809 |
|
| 810 |
-
function selectPanel(el) {
|
| 811 |
-
if(selectedPanel) selectedPanel.classList.remove('selected');
|
| 812 |
-
if(selectedBubble) { selectedBubble.classList.remove('selected'); selectedBubble = null; }
|
| 813 |
-
selectedPanel = el; el.classList.add('selected');
|
| 814 |
-
document.getElementById('zoom-slider').disabled = false;
|
| 815 |
-
const img = el.querySelector('img');
|
| 816 |
-
document.getElementById('zoom-slider').value = img.dataset.zoom || 100;
|
| 817 |
-
document.getElementById('bubble-type-select').disabled = true; document.getElementById('font-select').disabled = true;
|
| 818 |
-
}
|
| 819 |
-
|
| 820 |
function createBubbleHTML(data) {
|
| 821 |
const b = document.createElement('div');
|
| 822 |
const type = data.type || 'speech';
|
|
@@ -830,14 +882,15 @@ INDEX_HTML = '''
|
|
| 830 |
const textSpan = document.createElement('span'); textSpan.className = 'bubble-text'; textSpan.textContent = data.text || ''; b.appendChild(textSpan);
|
| 831 |
if(type === 'thought') { for(let i=1; i<=2; i++){ const d = document.createElement('div'); d.className = `thought-dot thought-dot-${i}`; b.appendChild(d); } }
|
| 832 |
['nw', 'ne', 'sw', 'se'].forEach(dir => { const handle = document.createElement('div'); handle.className = `resize-handle ${dir}`; handle.onmousedown = (e) => startResize(e, dir); b.appendChild(handle); });
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
|
| 836 |
-
|
| 837 |
b.onclick = (e) => { e.stopPropagation(); };
|
| 838 |
b.ondblclick = (e) => { e.stopPropagation(); editBubbleText(b); };
|
| 839 |
return b;
|
| 840 |
}
|
|
|
|
| 841 |
function selectBubble(el) { if(selectedBubble) selectedBubble.classList.remove('selected'); if(selectedPanel) { selectedPanel.classList.remove('selected'); selectedPanel = null; } selectedBubble = el; el.classList.add('selected'); document.getElementById('bubble-type-select').disabled = false; document.getElementById('font-select').disabled = false; document.getElementById('bubble-text-color').disabled = false; document.getElementById('bubble-fill-color').disabled = false; document.getElementById('bubble-type-select').value = el.dataset.type || 'speech'; }
|
| 842 |
function addBubble() { if(!selectedPanel) return alert("Select a panel (click an image) to add bubble to that page."); const pageDiv = selectedPanel.closest('.comic-page'); const b = createBubbleHTML({ text: "Text", left: "100px", top: "100px", type: 'speech', classes: "speech-bubble speech tail-bottom" }); pageDiv.appendChild(b); selectBubble(b); saveDraft(true); }
|
| 843 |
function deleteBubble() { if(!selectedBubble) return alert("Select a bubble"); selectedBubble.remove(); selectedBubble=null; saveDraft(true); }
|
|
@@ -847,8 +900,6 @@ INDEX_HTML = '''
|
|
| 847 |
function slideTail(v) { if(selectedBubble) { selectedBubble.style.setProperty('--tail-pos', v+'%'); saveDraft(true); } }
|
| 848 |
document.getElementById('bubble-text-color').addEventListener('change', (e) => { if(selectedBubble) { selectedBubble.style.setProperty('--bubble-text-color', e.target.value); saveDraft(true); } });
|
| 849 |
document.getElementById('bubble-fill-color').addEventListener('change', (e) => { if(selectedBubble) { selectedBubble.style.setProperty('--bubble-fill-color', e.target.value); saveDraft(true); } });
|
| 850 |
-
function startResize(e, dir) { e.preventDefault(); e.stopPropagation(); isResizing = true; resizeHandle = dir; const rect = selectedBubble.getBoundingClientRect(); originalWidth = rect.width; originalHeight = rect.height; originalMouseX = e.clientX; originalMouseY = e.clientY; }
|
| 851 |
-
function resizeBubble(e) { if (!isResizing || !selectedBubble) return; const dx = e.clientX - originalMouseX; const dy = e.clientY - originalMouseY; if(resizeHandle.includes('e')) selectedBubble.style.width = (originalWidth + dx)+'px'; if(resizeHandle.includes('s')) selectedBubble.style.height = (originalHeight + dy)+'px'; }
|
| 852 |
function replacePanelImage() { if(!selectedPanel) return alert("Select a panel"); const inp = document.getElementById('image-uploader'); inp.onchange = async (e) => { const fd = new FormData(); fd.append('image', e.target.files[0]); const img = selectedPanel.querySelector('img'); const r = await fetch(`/replace_panel?sid=${sid}`, {method:'POST', body:fd}); const d = await r.json(); if(d.success) { img.src = `/frames/${d.new_filename}?sid=${sid}`; saveDraft(true); } inp.value = ''; }; inp.click(); }
|
| 853 |
async function adjustFrame(dir) { if(isProcessing) return; if(!selectedPanel) return alert("Select a panel"); const img = selectedPanel.querySelector('img'); let fname = img.src.split('/').pop().split('?')[0]; setProcessing(true); img.style.opacity = '0.5'; try { const r = await fetch(`/regenerate_frame?sid=${sid}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({filename:fname, direction:dir}) }); const d = await r.json(); if(d.success) { img.src = `/frames/${fname}?sid=${sid}&t=${Date.now()}`; } else { alert('Error: ' + d.message); } } catch(e) { console.error(e); } img.style.opacity = '1'; setProcessing(false); saveDraft(true); }
|
| 854 |
async function gotoTimestamp() { if(isProcessing) return; if(!selectedPanel) return alert("Select a panel"); let v = document.getElementById('timestamp-input').value.trim(); if(!v) return; if(v.includes(':')) { let p = v.split(':'); v = parseInt(p[0]) * 60 + parseFloat(p[1]); } else { v = parseFloat(v); } if(isNaN(v)) return alert("Invalid time"); const img = selectedPanel.querySelector('img'); let fname = img.src.split('/').pop().split('?')[0]; setProcessing(true); img.style.opacity = '0.5'; try { const r = await fetch(`/goto_timestamp?sid=${sid}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({filename:fname, timestamp:v}) }); const d = await r.json(); if(d.success) { img.src = `/frames/${fname}?sid=${sid}&t=${Date.now()}`; document.getElementById('timestamp-input').value = ''; } else { alert('Error: ' + d.message); } } catch(e) { console.error(e); } img.style.opacity = '1'; setProcessing(false); saveDraft(true); }
|
|
|
|
| 336 |
z-index: 1;
|
| 337 |
overflow: hidden;
|
| 338 |
/* UPDATED: THICK WHITE BORDER */
|
| 339 |
+
border: 12px solid white;
|
| 340 |
+
box-shadow: 0 0 40px rgba(0,0,0,0.5);
|
| 341 |
+
box-sizing: border-box;
|
| 342 |
}
|
| 343 |
|
| 344 |
/* HANDLES FOR DRAGGING */
|
| 345 |
.handle {
|
| 346 |
position: absolute;
|
| 347 |
+
/* UPDATED: MUCH LARGER HANDLES (30px) */
|
| 348 |
+
width: 30px; height: 30px;
|
| 349 |
background: #ff4757;
|
| 350 |
border: 3px solid white;
|
| 351 |
border-radius: 50%;
|
| 352 |
cursor: ew-resize;
|
| 353 |
z-index: 999;
|
| 354 |
transform: translate(-50%, -50%);
|
| 355 |
+
box-shadow: 0 4px 8px rgba(0,0,0,0.4);
|
| 356 |
+
transition: transform 0.1s;
|
| 357 |
}
|
| 358 |
+
.handle:hover { transform: translate(-50%, -50%) scale(1.1); }
|
| 359 |
.handle.horiz {
|
| 360 |
cursor: ns-resize;
|
| 361 |
+
width: 80px; height: 20px;
|
| 362 |
+
border-radius: 10px;
|
| 363 |
}
|
| 364 |
|
| 365 |
/* PANELS */
|
|
|
|
| 386 |
position: absolute; display: flex; justify-content: center; align-items: center;
|
| 387 |
width: auto; height: auto; min-width: 80px; min-height: 50px; max-width: 250px;
|
| 388 |
box-sizing: border-box;
|
| 389 |
+
/* UPDATED Z-INDEX TO ENSURE DRAGGABILITY */
|
| 390 |
z-index: 500;
|
| 391 |
cursor: move; font-family: 'Comic Neue', cursive; font-weight: bold;
|
| 392 |
font-size: 13px; text-align: center;
|
| 393 |
line-height: 1.2; --tail-pos: 50%; padding: 5px;
|
| 394 |
+
user-select: none;
|
| 395 |
}
|
| 396 |
.speech-bubble.selected { outline: 2px dashed #4CAF50; z-index: 501; }
|
| 397 |
|
|
|
|
| 521 |
let currentSaveCode = null;
|
| 522 |
let isProcessing = false;
|
| 523 |
let interval, selectedBubble = null, selectedPanel = null;
|
| 524 |
+
|
| 525 |
+
// DRAG MODES
|
| 526 |
+
let dragMode = null; // 'layout', 'bubble', 'pan', 'resize-bubble'
|
| 527 |
+
let dragData = {}; // Stores offsets and active IDs
|
| 528 |
+
|
| 529 |
let activeLayoutHandle = null, activePageId = null;
|
| 530 |
|
| 531 |
const DEFAULT_LAYOUT = {
|
|
|
|
| 640 |
div.id = pageId;
|
| 641 |
div.dataset.layout = JSON.stringify(layout);
|
| 642 |
|
| 643 |
+
// Generate 4 Panels
|
| 644 |
for(let i=0; i<4; i++) {
|
| 645 |
const pDiv = document.createElement('div');
|
| 646 |
pDiv.className = 'panel'; pDiv.id = `${pageId}-p${i}`;
|
|
|
|
| 656 |
div.appendChild(pDiv);
|
| 657 |
}
|
| 658 |
|
| 659 |
+
// Generate Handles
|
| 660 |
const handles = [
|
| 661 |
{id: 'h1-top', t:0, l:0}, {id: 'h1-bot', t:0, l:0},
|
| 662 |
{id: 'h2-top', t:0, l:0}, {id: 'h2-bot', t:0, l:0},
|
|
|
|
| 725 |
setH('h-tier', w/2, ty);
|
| 726 |
}
|
| 727 |
|
| 728 |
+
// --- INTERACTION LOGIC (FIXED) ---
|
| 729 |
+
|
| 730 |
function startDragHandle(e, pageId, handleRole) {
|
| 731 |
e.stopPropagation(); e.preventDefault();
|
| 732 |
+
dragMode = 'layout';
|
| 733 |
+
dragData = { handle: handleRole, pageId: pageId };
|
| 734 |
+
}
|
| 735 |
+
|
| 736 |
+
function startBubbleDrag(e, bubble) {
|
| 737 |
+
if(e.target.classList.contains('resize-handle')) return;
|
| 738 |
+
e.stopPropagation(); e.preventDefault();
|
| 739 |
+
selectBubble(bubble);
|
| 740 |
+
dragMode = 'bubble';
|
| 741 |
+
dragData = {
|
| 742 |
+
el: bubble,
|
| 743 |
+
startX: e.clientX, startY: e.clientY,
|
| 744 |
+
initX: bubble.offsetLeft, initY: bubble.offsetTop
|
| 745 |
+
};
|
| 746 |
+
}
|
| 747 |
+
|
| 748 |
+
function startResize(e, dir) {
|
| 749 |
+
e.stopPropagation(); e.preventDefault();
|
| 750 |
+
dragMode = 'resize-bubble';
|
| 751 |
+
const rect = selectedBubble.getBoundingClientRect();
|
| 752 |
+
dragData = {
|
| 753 |
+
el: selectedBubble, dir: dir,
|
| 754 |
+
startX: e.clientX, startY: e.clientY,
|
| 755 |
+
startW: rect.width, startH: rect.height
|
| 756 |
+
};
|
| 757 |
+
}
|
| 758 |
+
|
| 759 |
+
function startPan(e, img) {
|
| 760 |
+
e.preventDefault();
|
| 761 |
+
selectPanel(img.closest('.panel'));
|
| 762 |
+
dragMode = 'pan';
|
| 763 |
+
dragData = {
|
| 764 |
+
el: img,
|
| 765 |
+
startX: e.clientX, startY: e.clientY,
|
| 766 |
+
startTx: parseFloat(img.dataset.translateX || 0),
|
| 767 |
+
startTy: parseFloat(img.dataset.translateY || 0)
|
| 768 |
+
};
|
| 769 |
+
img.classList.add('panning');
|
| 770 |
}
|
| 771 |
|
| 772 |
+
// GLOBAL MOUSEMOVE
|
| 773 |
document.addEventListener('mousemove', (e) => {
|
| 774 |
+
if(!dragMode) return;
|
| 775 |
+
|
| 776 |
+
if(dragMode === 'layout') {
|
| 777 |
+
const pageEl = document.getElementById(dragData.pageId);
|
| 778 |
const rect = pageEl.getBoundingClientRect();
|
| 779 |
const state = JSON.parse(pageEl.dataset.layout);
|
| 780 |
const x = Math.max(0, Math.min(1000, (e.clientX - rect.left) * (1000 / rect.width)));
|
| 781 |
const y = Math.max(50, Math.min(650, (e.clientY - rect.top) * (700 / rect.height)));
|
| 782 |
+
const role = dragData.handle;
|
| 783 |
|
| 784 |
+
if(role === 'h1-top') state.r1.topX = x;
|
| 785 |
+
else if(role === 'h1-bot') state.r1.botX = x;
|
| 786 |
+
else if(role === 'h2-top') state.r2.topX = x;
|
| 787 |
+
else if(role === 'h2-bot') state.r2.botX = x;
|
| 788 |
+
else if(role === 'h-tier') state.tierY = y;
|
| 789 |
|
| 790 |
pageEl.dataset.layout = JSON.stringify(state);
|
| 791 |
+
drawLayout(dragData.pageId);
|
| 792 |
+
}
|
| 793 |
+
else if(dragMode === 'bubble') {
|
| 794 |
+
const d = dragData;
|
| 795 |
+
d.el.style.left = (d.initX + e.clientX - d.startX) + 'px';
|
| 796 |
+
d.el.style.top = (d.initY + e.clientY - d.startY) + 'px';
|
| 797 |
+
}
|
| 798 |
+
else if(dragMode === 'resize-bubble') {
|
| 799 |
+
const d = dragData;
|
| 800 |
+
const dx = e.clientX - d.startX;
|
| 801 |
+
const dy = e.clientY - d.startY;
|
| 802 |
+
if(d.dir.includes('e')) d.el.style.width = (d.startW + dx)+'px';
|
| 803 |
+
if(d.dir.includes('s')) d.el.style.height = (d.startH + dy)+'px';
|
| 804 |
+
}
|
| 805 |
+
else if(dragMode === 'pan') {
|
| 806 |
+
const d = dragData;
|
| 807 |
+
d.el.dataset.translateX = d.startTx + (e.clientX - d.startX);
|
| 808 |
+
d.el.dataset.translateY = d.startTy + (e.clientY - d.startY);
|
| 809 |
+
updateImageTransform(d.el);
|
| 810 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 811 |
});
|
| 812 |
|
| 813 |
document.addEventListener('mouseup', () => {
|
| 814 |
+
if(dragMode) {
|
| 815 |
+
if(dragMode === 'pan' && dragData.el) dragData.el.classList.remove('panning');
|
| 816 |
+
dragMode = null;
|
| 817 |
+
saveDraft(true);
|
| 818 |
+
}
|
| 819 |
});
|
| 820 |
|
| 821 |
function updateGutter(val) {
|
|
|
|
| 838 |
return;
|
| 839 |
}
|
| 840 |
|
|
|
|
| 841 |
const pW = 1000;
|
| 842 |
const pH = panel.offsetHeight;
|
| 843 |
const iW = img.naturalWidth || img.width;
|
| 844 |
const iH = img.naturalHeight || img.height;
|
| 845 |
|
| 846 |
+
// CONTAIN LOGIC: Ensure WHOLE image fits
|
| 847 |
const scale = Math.min(pW / iW, pH / iH);
|
| 848 |
|
| 849 |
const tx = (pW - iW * scale) / 2;
|
|
|
|
| 866 |
img.classList.toggle('pannable', true);
|
| 867 |
}
|
| 868 |
|
|
|
|
|
|
|
| 869 |
function handleZoom(el) { if(!selectedPanel) return; const img = selectedPanel.querySelector('img'); img.dataset.zoom = el.value; updateImageTransform(img); }
|
| 870 |
function resetPanelTransform() { if(!selectedPanel) return alert("Select a panel"); const img = selectedPanel.querySelector('img'); fitImageToPanel(img, selectedPanel, null); saveDraft(true); }
|
| 871 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 872 |
function createBubbleHTML(data) {
|
| 873 |
const b = document.createElement('div');
|
| 874 |
const type = data.type || 'speech';
|
|
|
|
| 882 |
const textSpan = document.createElement('span'); textSpan.className = 'bubble-text'; textSpan.textContent = data.text || ''; b.appendChild(textSpan);
|
| 883 |
if(type === 'thought') { for(let i=1; i<=2; i++){ const d = document.createElement('div'); d.className = `thought-dot thought-dot-${i}`; b.appendChild(d); } }
|
| 884 |
['nw', 'ne', 'sw', 'se'].forEach(dir => { const handle = document.createElement('div'); handle.className = `resize-handle ${dir}`; handle.onmousedown = (e) => startResize(e, dir); b.appendChild(handle); });
|
| 885 |
+
|
| 886 |
+
// BUBBLE DRAG HANDLER
|
| 887 |
+
b.onmousedown = (e) => startBubbleDrag(e, b);
|
| 888 |
+
|
| 889 |
b.onclick = (e) => { e.stopPropagation(); };
|
| 890 |
b.ondblclick = (e) => { e.stopPropagation(); editBubbleText(b); };
|
| 891 |
return b;
|
| 892 |
}
|
| 893 |
+
|
| 894 |
function selectBubble(el) { if(selectedBubble) selectedBubble.classList.remove('selected'); if(selectedPanel) { selectedPanel.classList.remove('selected'); selectedPanel = null; } selectedBubble = el; el.classList.add('selected'); document.getElementById('bubble-type-select').disabled = false; document.getElementById('font-select').disabled = false; document.getElementById('bubble-text-color').disabled = false; document.getElementById('bubble-fill-color').disabled = false; document.getElementById('bubble-type-select').value = el.dataset.type || 'speech'; }
|
| 895 |
function addBubble() { if(!selectedPanel) return alert("Select a panel (click an image) to add bubble to that page."); const pageDiv = selectedPanel.closest('.comic-page'); const b = createBubbleHTML({ text: "Text", left: "100px", top: "100px", type: 'speech', classes: "speech-bubble speech tail-bottom" }); pageDiv.appendChild(b); selectBubble(b); saveDraft(true); }
|
| 896 |
function deleteBubble() { if(!selectedBubble) return alert("Select a bubble"); selectedBubble.remove(); selectedBubble=null; saveDraft(true); }
|
|
|
|
| 900 |
function slideTail(v) { if(selectedBubble) { selectedBubble.style.setProperty('--tail-pos', v+'%'); saveDraft(true); } }
|
| 901 |
document.getElementById('bubble-text-color').addEventListener('change', (e) => { if(selectedBubble) { selectedBubble.style.setProperty('--bubble-text-color', e.target.value); saveDraft(true); } });
|
| 902 |
document.getElementById('bubble-fill-color').addEventListener('change', (e) => { if(selectedBubble) { selectedBubble.style.setProperty('--bubble-fill-color', e.target.value); saveDraft(true); } });
|
|
|
|
|
|
|
| 903 |
function replacePanelImage() { if(!selectedPanel) return alert("Select a panel"); const inp = document.getElementById('image-uploader'); inp.onchange = async (e) => { const fd = new FormData(); fd.append('image', e.target.files[0]); const img = selectedPanel.querySelector('img'); const r = await fetch(`/replace_panel?sid=${sid}`, {method:'POST', body:fd}); const d = await r.json(); if(d.success) { img.src = `/frames/${d.new_filename}?sid=${sid}`; saveDraft(true); } inp.value = ''; }; inp.click(); }
|
| 904 |
async function adjustFrame(dir) { if(isProcessing) return; if(!selectedPanel) return alert("Select a panel"); const img = selectedPanel.querySelector('img'); let fname = img.src.split('/').pop().split('?')[0]; setProcessing(true); img.style.opacity = '0.5'; try { const r = await fetch(`/regenerate_frame?sid=${sid}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({filename:fname, direction:dir}) }); const d = await r.json(); if(d.success) { img.src = `/frames/${fname}?sid=${sid}&t=${Date.now()}`; } else { alert('Error: ' + d.message); } } catch(e) { console.error(e); } img.style.opacity = '1'; setProcessing(false); saveDraft(true); }
|
| 905 |
async function gotoTimestamp() { if(isProcessing) return; if(!selectedPanel) return alert("Select a panel"); let v = document.getElementById('timestamp-input').value.trim(); if(!v) return; if(v.includes(':')) { let p = v.split(':'); v = parseInt(p[0]) * 60 + parseFloat(p[1]); } else { v = parseFloat(v); } if(isNaN(v)) return alert("Invalid time"); const img = selectedPanel.querySelector('img'); let fname = img.src.split('/').pop().split('?')[0]; setProcessing(true); img.style.opacity = '0.5'; try { const r = await fetch(`/goto_timestamp?sid=${sid}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({filename:fname, timestamp:v}) }); const d = await r.json(); if(d.success) { img.src = `/frames/${fname}?sid=${sid}&t=${Date.now()}`; document.getElementById('timestamp-input').value = ''; } else { alert('Error: ' + d.message); } } catch(e) { console.error(e); } img.style.opacity = '1'; setProcessing(false); saveDraft(true); }
|