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