|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Interactive Comic Editor</title> |
|
|
<link id="pageStyle" rel="stylesheet" href="page.css"> |
|
|
<link rel="stylesheet" href="bubble.css"> |
|
|
|
|
|
<style> |
|
|
|
|
|
.bubble { |
|
|
cursor: move; |
|
|
transition: box-shadow 0.2s; |
|
|
user-select: none; |
|
|
} |
|
|
|
|
|
.bubble:hover { |
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.3); |
|
|
} |
|
|
|
|
|
.bubble.editing { |
|
|
cursor: text; |
|
|
} |
|
|
|
|
|
.bubble textarea { |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
border: none; |
|
|
background: transparent; |
|
|
font-family: inherit; |
|
|
font-size: inherit; |
|
|
font-weight: inherit; |
|
|
text-align: center; |
|
|
resize: none; |
|
|
outline: 2px solid #4CAF50; |
|
|
padding: 5px; |
|
|
box-sizing: border-box; |
|
|
} |
|
|
|
|
|
.edit-controls { |
|
|
position: fixed; |
|
|
bottom: 20px; |
|
|
right: 20px; |
|
|
background: rgba(0,0,0,0.85); |
|
|
color: white; |
|
|
padding: 15px 20px; |
|
|
border-radius: 10px; |
|
|
font-size: 14px; |
|
|
z-index: 1000; |
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.3); |
|
|
} |
|
|
|
|
|
.edit-controls h4 { |
|
|
margin: 0 0 10px 0; |
|
|
color: #4CAF50; |
|
|
} |
|
|
|
|
|
.edit-controls p { |
|
|
margin: 5px 0; |
|
|
opacity: 0.9; |
|
|
} |
|
|
|
|
|
.edit-controls button { |
|
|
margin-top: 10px; |
|
|
padding: 8px 16px; |
|
|
border: none; |
|
|
border-radius: 5px; |
|
|
cursor: pointer; |
|
|
font-weight: bold; |
|
|
transition: all 0.2s; |
|
|
} |
|
|
|
|
|
.save-btn { |
|
|
background: #4CAF50; |
|
|
color: white; |
|
|
margin-right: 10px; |
|
|
} |
|
|
|
|
|
.save-btn:hover { |
|
|
background: #45a049; |
|
|
} |
|
|
|
|
|
.reset-btn { |
|
|
background: #f44336; |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.reset-btn:hover { |
|
|
background: #da190b; |
|
|
} |
|
|
</style> |
|
|
|
|
|
<script src="page.js"></script> |
|
|
<script src="page_place.js"></script> |
|
|
</head> |
|
|
|
|
|
<body> |
|
|
<div class="button"> |
|
|
<button onclick="prevPage()"><img src="assets/backward.png" alt=""></button> |
|
|
</div> |
|
|
|
|
|
<div class="wrapper"> |
|
|
<div class="grid-container"> |
|
|
<div class="grid-item" id="_1"></div> |
|
|
<div class="grid-item" id="_2"></div> |
|
|
<div class="grid-item" id="_3"></div> |
|
|
<div class="grid-item" id="_4"></div> |
|
|
<div class="grid-item" id="_5"></div> |
|
|
<div class="grid-item" id="_6"></div> |
|
|
<div class="grid-item" id="_7"></div> |
|
|
<div class="grid-item" id="_8"></div> |
|
|
<div class="grid-item" id="_9"></div> |
|
|
<div class="grid-item" id="_10"></div> |
|
|
<div class="grid-item" id="_11"></div> |
|
|
<div class="grid-item" id="_12"></div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="button"> |
|
|
<button onclick="nextPage()"><img src="assets/forward.png" alt=""></button> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="edit-controls"> |
|
|
<h4>✏️ Comic Editor</h4> |
|
|
<p>• <strong>Drag</strong> speech bubbles to reposition</p> |
|
|
<p>• <strong>Double-click</strong> to edit text</p> |
|
|
<p>• <strong>Enter</strong> to save text</p> |
|
|
<button class="save-btn" onclick="saveComic()">💾 Save Changes</button> |
|
|
<button class="reset-btn" onclick="resetComic()">↩️ Reset</button> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
|
|
|
let currentEditBubble = null; |
|
|
let draggedBubble = null; |
|
|
let offset = {x: 0, y: 0}; |
|
|
|
|
|
|
|
|
window.addEventListener('load', () => { |
|
|
setTimeout(initializeEditor, 500); |
|
|
}); |
|
|
|
|
|
function initializeEditor() { |
|
|
|
|
|
document.querySelectorAll('.bubble').forEach(bubble => { |
|
|
|
|
|
bubble.addEventListener('dblclick', (e) => { |
|
|
e.stopPropagation(); |
|
|
editBubbleText(bubble); |
|
|
}); |
|
|
|
|
|
bubble.addEventListener('mousedown', startDrag); |
|
|
}); |
|
|
|
|
|
|
|
|
document.addEventListener('mousemove', drag); |
|
|
document.addEventListener('mouseup', stopDrag); |
|
|
|
|
|
|
|
|
loadSavedState(); |
|
|
} |
|
|
|
|
|
function editBubbleText(bubble) { |
|
|
if (currentEditBubble) return; |
|
|
|
|
|
currentEditBubble = bubble; |
|
|
bubble.classList.add('editing'); |
|
|
|
|
|
const text = bubble.innerText; |
|
|
const textarea = document.createElement('textarea'); |
|
|
textarea.value = text; |
|
|
|
|
|
bubble.innerHTML = ''; |
|
|
bubble.appendChild(textarea); |
|
|
textarea.focus(); |
|
|
textarea.select(); |
|
|
|
|
|
textarea.addEventListener('keydown', (e) => { |
|
|
if (e.key === 'Enter' && !e.shiftKey) { |
|
|
e.preventDefault(); |
|
|
saveBubbleText(bubble, textarea.value); |
|
|
} |
|
|
if (e.key === 'Escape') { |
|
|
saveBubbleText(bubble, text); |
|
|
} |
|
|
}); |
|
|
|
|
|
textarea.addEventListener('blur', () => { |
|
|
setTimeout(() => { |
|
|
if (currentEditBubble === bubble) { |
|
|
saveBubbleText(bubble, textarea.value); |
|
|
} |
|
|
}, 100); |
|
|
}); |
|
|
} |
|
|
|
|
|
function saveBubbleText(bubble, text) { |
|
|
bubble.innerText = text; |
|
|
bubble.classList.remove('editing'); |
|
|
currentEditBubble = null; |
|
|
saveState(); |
|
|
} |
|
|
|
|
|
function startDrag(e) { |
|
|
if (e.target.tagName === 'TEXTAREA') return; |
|
|
|
|
|
const bubble = e.target.closest('.bubble'); |
|
|
if (!bubble || currentEditBubble) return; |
|
|
|
|
|
draggedBubble = bubble; |
|
|
const rect = bubble.getBoundingClientRect(); |
|
|
offset.x = e.clientX - rect.left; |
|
|
offset.y = e.clientY - rect.top; |
|
|
|
|
|
bubble.style.opacity = '0.9'; |
|
|
bubble.style.zIndex = '100'; |
|
|
e.preventDefault(); |
|
|
} |
|
|
|
|
|
function drag(e) { |
|
|
if (!draggedBubble) return; |
|
|
|
|
|
const parent = draggedBubble.parentElement; |
|
|
const parentRect = parent.getBoundingClientRect(); |
|
|
|
|
|
let x = e.clientX - parentRect.left - offset.x; |
|
|
let y = e.clientY - parentRect.top - offset.y; |
|
|
|
|
|
|
|
|
x = Math.max(0, Math.min(x, parentRect.width - draggedBubble.offsetWidth)); |
|
|
y = Math.max(0, Math.min(y, parentRect.height - draggedBubble.offsetHeight)); |
|
|
|
|
|
draggedBubble.style.left = x + 'px'; |
|
|
draggedBubble.style.top = y + 'px'; |
|
|
} |
|
|
|
|
|
function stopDrag() { |
|
|
if (draggedBubble) { |
|
|
draggedBubble.style.opacity = ''; |
|
|
draggedBubble.style.zIndex = ''; |
|
|
saveState(); |
|
|
draggedBubble = null; |
|
|
} |
|
|
} |
|
|
|
|
|
function saveState() { |
|
|
const bubbles = []; |
|
|
document.querySelectorAll('.bubble').forEach((bubble, index) => { |
|
|
bubbles.push({ |
|
|
index: index, |
|
|
text: bubble.innerText, |
|
|
left: bubble.style.left, |
|
|
top: bubble.style.top |
|
|
}); |
|
|
}); |
|
|
localStorage.setItem('comicBubbles', JSON.stringify(bubbles)); |
|
|
} |
|
|
|
|
|
function loadSavedState() { |
|
|
const saved = localStorage.getItem('comicBubbles'); |
|
|
if (!saved) return; |
|
|
|
|
|
try { |
|
|
const bubbles = JSON.parse(saved); |
|
|
const elements = document.querySelectorAll('.bubble'); |
|
|
|
|
|
bubbles.forEach((data, index) => { |
|
|
if (elements[index]) { |
|
|
elements[index].innerText = data.text; |
|
|
if (data.left) elements[index].style.left = data.left; |
|
|
if (data.top) elements[index].style.top = data.top; |
|
|
} |
|
|
}); |
|
|
} catch (e) { |
|
|
console.error('Failed to load saved state:', e); |
|
|
} |
|
|
} |
|
|
|
|
|
function saveComic() { |
|
|
saveState(); |
|
|
alert('✅ Comic changes saved!\n\nYour edits are saved locally. To export the comic, use your browser\'s print function or take a screenshot.'); |
|
|
} |
|
|
|
|
|
function resetComic() { |
|
|
if (confirm('Reset all changes to original?')) { |
|
|
localStorage.removeItem('comicBubbles'); |
|
|
location.reload(); |
|
|
} |
|
|
} |
|
|
</script> |
|
|
</body> |
|
|
</html> |