| <!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> |