|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Comic Viewer - Editable</title> |
|
|
<style> |
|
|
body { |
|
|
margin: 0; |
|
|
padding: 0; |
|
|
background: #2c3e50; |
|
|
font-family: Arial, sans-serif; |
|
|
} |
|
|
|
|
|
.comic-container { |
|
|
max-width: 900px; |
|
|
margin: 0 auto; |
|
|
background: white; |
|
|
padding: 10px; |
|
|
border-radius: 10px; |
|
|
box-shadow: 0 4px 20px rgba(0,0,0,0.3); |
|
|
} |
|
|
|
|
|
.comic-page { |
|
|
position: relative; |
|
|
margin: 20px auto 0 auto; |
|
|
background: #f9f9f9; |
|
|
border: 2px solid #333; |
|
|
width: 800px; |
|
|
height: 1080px; |
|
|
overflow: hidden; |
|
|
} |
|
|
|
|
|
.panel-grid { |
|
|
display: grid; |
|
|
grid-template-columns: 1fr 1fr; |
|
|
grid-template-rows: 1fr 1fr; |
|
|
gap: 0; |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
} |
|
|
|
|
|
.panel { |
|
|
position: relative; |
|
|
border: 3px solid #333; |
|
|
background: white; |
|
|
overflow: hidden; |
|
|
} |
|
|
|
|
|
.panel img { |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
object-fit: contain; |
|
|
background: #000; |
|
|
} |
|
|
|
|
|
.speech-bubble { |
|
|
position: absolute; |
|
|
background: white; |
|
|
border: 3px solid #333; |
|
|
border-radius: 20px; |
|
|
padding: 10px 15px; |
|
|
font-family: 'Comic Sans MS', cursive; |
|
|
font-size: 14px; |
|
|
font-weight: bold; |
|
|
text-align: center; |
|
|
cursor: move; |
|
|
min-width: 100px; |
|
|
max-width: 200px; |
|
|
word-wrap: break-word; |
|
|
user-select: none; |
|
|
z-index: 10; |
|
|
} |
|
|
|
|
|
.speech-bubble:hover { |
|
|
box-shadow: 0 4px 10px rgba(0,0,0,0.2); |
|
|
transform: scale(1.02); |
|
|
} |
|
|
|
|
|
.speech-bubble.editing { |
|
|
cursor: text; |
|
|
background: #fffacd; |
|
|
} |
|
|
|
|
|
.bubble-tail { |
|
|
position: absolute; |
|
|
bottom: -12px; |
|
|
left: 20px; |
|
|
width: 0; |
|
|
height: 0; |
|
|
border-left: 12px solid transparent; |
|
|
border-right: 8px solid transparent; |
|
|
border-top: 15px solid #333; |
|
|
} |
|
|
|
|
|
.bubble-tail::after { |
|
|
content: ''; |
|
|
position: absolute; |
|
|
bottom: 3px; |
|
|
left: -9px; |
|
|
width: 0; |
|
|
height: 0; |
|
|
border-left: 9px solid transparent; |
|
|
border-right: 6px solid transparent; |
|
|
border-top: 11px solid white; |
|
|
} |
|
|
|
|
|
.controls { |
|
|
text-align: center; |
|
|
margin: 20px 0; |
|
|
padding: 20px; |
|
|
background: #ecf0f1; |
|
|
border-radius: 10px; |
|
|
} |
|
|
|
|
|
.btn { |
|
|
padding: 10px 20px; |
|
|
margin: 0 5px; |
|
|
background: #3498db; |
|
|
color: white; |
|
|
border: none; |
|
|
border-radius: 5px; |
|
|
cursor: pointer; |
|
|
font-weight: bold; |
|
|
} |
|
|
|
|
|
.btn:hover { |
|
|
background: #2980b9; |
|
|
} |
|
|
|
|
|
.btn.success { |
|
|
background: #27ae60; |
|
|
} |
|
|
|
|
|
.btn.success:hover { |
|
|
background: #229954; |
|
|
} |
|
|
|
|
|
.instructions { |
|
|
background: #f39c12; |
|
|
color: white; |
|
|
padding: 15px; |
|
|
border-radius: 5px; |
|
|
text-align: center; |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="comic-container"> |
|
|
<h1 style="text-align: center; color: #2c3e50;">📚 Interactive Comic Editor</h1> |
|
|
|
|
|
<div class="instructions"> |
|
|
💡 <strong>How to use:</strong> Drag bubbles to move them | Double-click to edit text | Click "Add Bubble" to create new ones |
|
|
</div> |
|
|
|
|
|
<div class="controls"> |
|
|
<button class="btn" onclick="addNewBubble()">➕ Add Bubble</button> |
|
|
<button class="btn success" onclick="saveComic()">💾 Save Changes</button> |
|
|
<button class="btn" onclick="resetPositions()">🔄 Reset</button> |
|
|
<button class="btn" onclick="downloadPages()">⬇️ Download Pages</button> |
|
|
</div> |
|
|
|
|
|
<div id="comic-pages"> |
|
|
|
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script> |
|
|
|
|
|
<script> |
|
|
let bubbles = []; |
|
|
let currentBubble = null; |
|
|
let isDragging = false; |
|
|
let dragOffset = {x: 0, y: 0}; |
|
|
let isEditing = false; |
|
|
|
|
|
|
|
|
const comicData = { |
|
|
pages: [ |
|
|
{ |
|
|
panels: [ |
|
|
{image: '/frames/frame000.png', bubbles: [{id: 1, x: 20, y: 20, text: 'Hello!'}]}, |
|
|
{image: '/frames/frame001.png', bubbles: [{id: 2, x: 20, y: 20, text: 'How are you?'}]}, |
|
|
{image: '/frames/frame002.png', bubbles: [{id: 3, x: 20, y: 20, text: 'Great!'}]}, |
|
|
{image: '/frames/frame003.png', bubbles: [{id: 4, x: 20, y: 20, text: 'See you!'}]} |
|
|
] |
|
|
} |
|
|
] |
|
|
}; |
|
|
|
|
|
function initComic() { |
|
|
const container = document.getElementById('comic-pages'); |
|
|
container.innerHTML = ''; |
|
|
|
|
|
comicData.pages.forEach((page, pageIdx) => { |
|
|
const pageDiv = document.createElement('div'); |
|
|
pageDiv.className = 'comic-page'; |
|
|
pageDiv.innerHTML = '<div class="panel-grid"></div>'; |
|
|
|
|
|
const grid = pageDiv.querySelector('.panel-grid'); |
|
|
|
|
|
page.panels.forEach((panel, panelIdx) => { |
|
|
const panelDiv = document.createElement('div'); |
|
|
panelDiv.className = 'panel'; |
|
|
panelDiv.dataset.panelIdx = panelIdx; |
|
|
panelDiv.innerHTML = `<img src="${panel.image}" alt="Panel ${panelIdx + 1}" crossorigin="anonymous">`; |
|
|
|
|
|
|
|
|
panel.bubbles.forEach(bubble => { |
|
|
createBubble(panelDiv, bubble); |
|
|
}); |
|
|
|
|
|
grid.appendChild(panelDiv); |
|
|
}); |
|
|
|
|
|
container.appendChild(pageDiv); |
|
|
}); |
|
|
|
|
|
|
|
|
document.addEventListener('mousemove', handleMouseMove); |
|
|
document.addEventListener('mouseup', handleMouseUp); |
|
|
} |
|
|
|
|
|
function createBubble(panel, bubbleData) { |
|
|
const bubble = document.createElement('div'); |
|
|
bubble.className = 'speech-bubble'; |
|
|
bubble.dataset.id = bubbleData.id; |
|
|
bubble.style.left = bubbleData.x + 'px'; |
|
|
bubble.style.top = bubbleData.y + 'px'; |
|
|
bubble.innerHTML = ` |
|
|
<span class="bubble-text">${bubbleData.text}</span> |
|
|
<div class="bubble-tail"></div> |
|
|
`; |
|
|
|
|
|
|
|
|
bubble.addEventListener('mousedown', startDrag); |
|
|
|
|
|
|
|
|
bubble.addEventListener('dblclick', startEdit); |
|
|
|
|
|
panel.appendChild(bubble); |
|
|
bubbles.push(bubble); |
|
|
} |
|
|
|
|
|
function startDrag(e) { |
|
|
if (isEditing) return; |
|
|
|
|
|
currentBubble = e.currentTarget; |
|
|
isDragging = true; |
|
|
|
|
|
const rect = currentBubble.getBoundingClientRect(); |
|
|
const parentRect = currentBubble.parentElement.getBoundingClientRect(); |
|
|
|
|
|
dragOffset.x = e.clientX - rect.left; |
|
|
dragOffset.y = e.clientY - rect.top; |
|
|
|
|
|
currentBubble.style.zIndex = 1000; |
|
|
e.preventDefault(); |
|
|
} |
|
|
|
|
|
function handleMouseMove(e) { |
|
|
if (!isDragging || !currentBubble) return; |
|
|
|
|
|
const parent = currentBubble.parentElement; |
|
|
const parentRect = parent.getBoundingClientRect(); |
|
|
|
|
|
let newX = e.clientX - parentRect.left - dragOffset.x; |
|
|
let newY = e.clientY - parentRect.top - dragOffset.y; |
|
|
|
|
|
|
|
|
newX = Math.max(0, Math.min(newX, parentRect.width - currentBubble.offsetWidth)); |
|
|
newY = Math.max(0, Math.min(newY, parentRect.height - currentBubble.offsetHeight)); |
|
|
|
|
|
currentBubble.style.left = newX + 'px'; |
|
|
currentBubble.style.top = newY + 'px'; |
|
|
} |
|
|
|
|
|
function handleMouseUp() { |
|
|
if (currentBubble) { |
|
|
currentBubble.style.zIndex = 10; |
|
|
} |
|
|
isDragging = false; |
|
|
currentBubble = null; |
|
|
} |
|
|
|
|
|
function startEdit(e) { |
|
|
const bubble = e.currentTarget; |
|
|
const textSpan = bubble.querySelector('.bubble-text'); |
|
|
|
|
|
isEditing = true; |
|
|
bubble.classList.add('editing'); |
|
|
|
|
|
|
|
|
textSpan.contentEditable = true; |
|
|
textSpan.focus(); |
|
|
|
|
|
|
|
|
const range = document.createRange(); |
|
|
range.selectNodeContents(textSpan); |
|
|
const sel = window.getSelection(); |
|
|
sel.removeAllRanges(); |
|
|
sel.addRange(range); |
|
|
|
|
|
|
|
|
textSpan.addEventListener('blur', () => stopEdit(bubble, textSpan), {once: true}); |
|
|
textSpan.addEventListener('keydown', (e) => { |
|
|
if (e.key === 'Enter' && !e.shiftKey) { |
|
|
e.preventDefault(); |
|
|
textSpan.blur(); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
function stopEdit(bubble, textSpan) { |
|
|
isEditing = false; |
|
|
bubble.classList.remove('editing'); |
|
|
textSpan.contentEditable = false; |
|
|
} |
|
|
|
|
|
function addNewBubble() { |
|
|
const panels = document.querySelectorAll('.panel'); |
|
|
if (panels.length === 0) return; |
|
|
|
|
|
|
|
|
const panel = panels[0]; |
|
|
const newBubble = { |
|
|
id: Date.now(), |
|
|
x: 50, |
|
|
y: 50, |
|
|
text: 'New text!' |
|
|
}; |
|
|
|
|
|
createBubble(panel, newBubble); |
|
|
} |
|
|
|
|
|
function saveComic() { |
|
|
const data = { |
|
|
pages: [] |
|
|
}; |
|
|
|
|
|
|
|
|
document.querySelectorAll('.comic-page').forEach(page => { |
|
|
const pageData = {panels: []}; |
|
|
|
|
|
page.querySelectorAll('.panel').forEach(panel => { |
|
|
const panelData = { |
|
|
image: panel.querySelector('img').src, |
|
|
bubbles: [] |
|
|
}; |
|
|
|
|
|
panel.querySelectorAll('.speech-bubble').forEach(bubble => { |
|
|
panelData.bubbles.push({ |
|
|
id: bubble.dataset.id, |
|
|
x: parseInt(bubble.style.left), |
|
|
y: parseInt(bubble.style.top), |
|
|
text: bubble.querySelector('.bubble-text').textContent |
|
|
}); |
|
|
}); |
|
|
|
|
|
pageData.panels.push(panelData); |
|
|
}); |
|
|
|
|
|
data.pages.push(pageData); |
|
|
}); |
|
|
|
|
|
|
|
|
localStorage.setItem('comicData', JSON.stringify(data)); |
|
|
|
|
|
|
|
|
alert('Comic saved! Your changes have been stored.'); |
|
|
|
|
|
console.log('Saved data:', data); |
|
|
} |
|
|
|
|
|
function resetPositions() { |
|
|
if (confirm('Reset all bubble positions to default?')) { |
|
|
initComic(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function downloadPages() { |
|
|
const pages = document.querySelectorAll('.comic-page'); |
|
|
pages.forEach((page, idx) => { |
|
|
html2canvas(page, {width: 800, height: 1080, scale: 2, allowTaint: true, useCORS: true}).then(canvas => { |
|
|
canvas.toBlob(blob => { |
|
|
const link = document.createElement('a'); |
|
|
link.download = `comic_page_${idx+1}.png`; |
|
|
link.href = URL.createObjectURL(blob); |
|
|
link.click(); |
|
|
URL.revokeObjectURL(link.href); |
|
|
}, 'image/png'); |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
const savedData = localStorage.getItem('comicData'); |
|
|
if (savedData) { |
|
|
Object.assign(comicData, JSON.parse(savedData)); |
|
|
} |
|
|
|
|
|
|
|
|
initComic(); |
|
|
</script> |
|
|
</body> |
|
|
</html> |