|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ComicEditor { |
|
|
constructor(containerId) { |
|
|
this.container = document.getElementById(containerId); |
|
|
this.bubbles = []; |
|
|
this.selectedBubble = null; |
|
|
this.isDragging = false; |
|
|
this.dragOffset = { x: 0, y: 0 }; |
|
|
this.isEditing = false; |
|
|
|
|
|
this.init(); |
|
|
} |
|
|
|
|
|
init() { |
|
|
|
|
|
this.addStyles(); |
|
|
|
|
|
|
|
|
this.loadComicData(); |
|
|
|
|
|
|
|
|
this.setupEventListeners(); |
|
|
|
|
|
|
|
|
this.createToolbar(); |
|
|
} |
|
|
|
|
|
addStyles() { |
|
|
const style = document.createElement('style'); |
|
|
style.textContent = ` |
|
|
.comic-editor-container { |
|
|
position: relative; |
|
|
user-select: none; |
|
|
background: #f0f0f0; |
|
|
padding: 20px; |
|
|
border-radius: 10px; |
|
|
} |
|
|
|
|
|
.comic-page { |
|
|
position: relative; |
|
|
background: white; |
|
|
margin: 20px auto; |
|
|
box-shadow: 0 4px 20px rgba(0,0,0,0.1); |
|
|
width: 800px; /* exact width */ |
|
|
height: 1080px; /* exact height */ |
|
|
} |
|
|
|
|
|
.comic-panel { |
|
|
position: absolute; |
|
|
border: 2px solid #333; |
|
|
overflow: hidden; |
|
|
background: white; |
|
|
} |
|
|
|
|
|
.comic-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: 15px; |
|
|
cursor: move; |
|
|
min-width: 100px; |
|
|
min-height: 50px; |
|
|
box-shadow: 2px 2px 5px rgba(0,0,0,0.1); |
|
|
transition: transform 0.1s; |
|
|
z-index: 10; |
|
|
} |
|
|
|
|
|
.speech-bubble:hover { |
|
|
transform: scale(1.02); |
|
|
box-shadow: 4px 4px 10px rgba(0,0,0,0.2); |
|
|
} |
|
|
|
|
|
.speech-bubble.selected { |
|
|
border-color: #007bff; |
|
|
box-shadow: 0 0 0 3px rgba(0,123,255,0.3); |
|
|
z-index: 100; |
|
|
} |
|
|
|
|
|
.speech-bubble.dragging { |
|
|
opacity: 0.8; |
|
|
z-index: 1000; |
|
|
} |
|
|
|
|
|
.bubble-text { |
|
|
font-family: 'Comic Sans MS', cursive; |
|
|
font-size: 14px; |
|
|
font-weight: bold; |
|
|
text-align: center; |
|
|
line-height: 1.4; |
|
|
color: #000; |
|
|
word-wrap: break-word; |
|
|
cursor: text; |
|
|
} |
|
|
|
|
|
.bubble-text.editing { |
|
|
background: rgba(255,255,255,0.9); |
|
|
border: 1px dashed #007bff; |
|
|
padding: 5px; |
|
|
outline: none; |
|
|
} |
|
|
|
|
|
.bubble-tail { |
|
|
position: absolute; |
|
|
bottom: -15px; |
|
|
left: 20px; |
|
|
width: 0; |
|
|
height: 0; |
|
|
border-left: 15px solid transparent; |
|
|
border-right: 5px solid transparent; |
|
|
border-top: 20px solid #333; |
|
|
transform: rotate(-20deg); |
|
|
} |
|
|
|
|
|
.bubble-tail::after { |
|
|
content: ''; |
|
|
position: absolute; |
|
|
bottom: 3px; |
|
|
left: -12px; |
|
|
width: 0; |
|
|
height: 0; |
|
|
border-left: 12px solid transparent; |
|
|
border-right: 4px solid transparent; |
|
|
border-top: 16px solid white; |
|
|
} |
|
|
|
|
|
.editor-toolbar { |
|
|
position: fixed; |
|
|
top: 20px; |
|
|
right: 20px; |
|
|
background: white; |
|
|
border: 2px solid #333; |
|
|
border-radius: 10px; |
|
|
padding: 15px; |
|
|
box-shadow: 0 4px 20px rgba(0,0,0,0.1); |
|
|
z-index: 1000; |
|
|
} |
|
|
|
|
|
.toolbar-btn { |
|
|
display: block; |
|
|
width: 100%; |
|
|
padding: 10px 15px; |
|
|
margin: 5px 0; |
|
|
background: #007bff; |
|
|
color: white; |
|
|
border: none; |
|
|
border-radius: 5px; |
|
|
cursor: pointer; |
|
|
font-weight: bold; |
|
|
transition: background 0.2s; |
|
|
} |
|
|
|
|
|
.toolbar-btn:hover { |
|
|
background: #0056b3; |
|
|
} |
|
|
|
|
|
.toolbar-btn.danger { |
|
|
background: #dc3545; |
|
|
} |
|
|
|
|
|
.toolbar-btn.danger:hover { |
|
|
background: #c82333; |
|
|
} |
|
|
|
|
|
.toolbar-btn.success { |
|
|
background: #28a745; |
|
|
} |
|
|
|
|
|
.toolbar-btn.success:hover { |
|
|
background: #218838; |
|
|
} |
|
|
|
|
|
.toolbar-btn.download { |
|
|
background: #ff66b3; /* pink */ |
|
|
color: white; |
|
|
} |
|
|
.toolbar-btn.download:hover { |
|
|
background: #ff4da6; |
|
|
} |
|
|
|
|
|
.resize-handle { |
|
|
position: absolute; |
|
|
width: 10px; |
|
|
height: 10px; |
|
|
background: #007bff; |
|
|
border: 1px solid white; |
|
|
border-radius: 50%; |
|
|
cursor: nwse-resize; |
|
|
} |
|
|
|
|
|
.resize-handle.se { |
|
|
bottom: -5px; |
|
|
right: -5px; |
|
|
} |
|
|
|
|
|
.coordinates { |
|
|
position: absolute; |
|
|
bottom: -25px; |
|
|
left: 0; |
|
|
font-size: 10px; |
|
|
color: #666; |
|
|
background: white; |
|
|
padding: 2px 5px; |
|
|
border-radius: 3px; |
|
|
display: none; |
|
|
} |
|
|
|
|
|
.selected .coordinates { |
|
|
display: block; |
|
|
} |
|
|
|
|
|
.edit-hint { |
|
|
position: fixed; |
|
|
bottom: 20px; |
|
|
left: 50%; |
|
|
transform: translateX(-50%); |
|
|
background: #333; |
|
|
color: white; |
|
|
padding: 10px 20px; |
|
|
border-radius: 20px; |
|
|
font-size: 14px; |
|
|
z-index: 1000; |
|
|
opacity: 0; |
|
|
transition: opacity 0.3s; |
|
|
} |
|
|
|
|
|
.edit-hint.show { |
|
|
opacity: 1; |
|
|
} |
|
|
`; |
|
|
document.head.appendChild(style); |
|
|
} |
|
|
|
|
|
loadComicData() { |
|
|
|
|
|
const savedData = localStorage.getItem('comicEditorData'); |
|
|
if (savedData) { |
|
|
const data = JSON.parse(savedData); |
|
|
this.renderComic(data); |
|
|
} else { |
|
|
|
|
|
this.loadFromServer(); |
|
|
} |
|
|
} |
|
|
|
|
|
loadFromServer() { |
|
|
|
|
|
fetch('/load_comic') |
|
|
.then(response => response.json()) |
|
|
.then(data => { |
|
|
if (data.error) { |
|
|
console.error('Error loading comic:', data.error); |
|
|
this.createDefaultComic(); |
|
|
} else { |
|
|
this.renderComic(data); |
|
|
} |
|
|
}) |
|
|
.catch(error => { |
|
|
console.error('Failed to load comic:', error); |
|
|
this.createDefaultComic(); |
|
|
}); |
|
|
} |
|
|
|
|
|
createDefaultComic() { |
|
|
|
|
|
const sampleData = { |
|
|
pages: [{ |
|
|
width: 800, |
|
|
height: 600, |
|
|
panels: [ |
|
|
{ |
|
|
x: 10, y: 10, width: 380, height: 280, |
|
|
image: '/frames/frame000.png' |
|
|
}, |
|
|
{ |
|
|
x: 410, y: 10, width: 380, height: 280, |
|
|
image: '/frames/frame001.png' |
|
|
} |
|
|
], |
|
|
bubbles: [ |
|
|
{ |
|
|
id: 'bubble1', |
|
|
x: 50, y: 50, width: 150, height: 60, |
|
|
text: 'Add your text here!', |
|
|
panelIndex: 0 |
|
|
} |
|
|
] |
|
|
}] |
|
|
}; |
|
|
|
|
|
this.renderComic(sampleData); |
|
|
} |
|
|
|
|
|
renderComic(data) { |
|
|
this.container.innerHTML = ''; |
|
|
this.container.className = 'comic-editor-container'; |
|
|
|
|
|
data.pages.forEach((page, pageIndex) => { |
|
|
const pageDiv = document.createElement('div'); |
|
|
pageDiv.className = 'comic-page'; |
|
|
pageDiv.style.width = page.width + 'px'; |
|
|
pageDiv.style.height = page.height + 'px'; |
|
|
pageDiv.dataset.pageIndex = pageIndex; |
|
|
|
|
|
|
|
|
page.panels.forEach((panel, panelIndex) => { |
|
|
const panelDiv = document.createElement('div'); |
|
|
panelDiv.className = 'comic-panel'; |
|
|
panelDiv.style.left = panel.x + 'px'; |
|
|
panelDiv.style.top = panel.y + 'px'; |
|
|
panelDiv.style.width = panel.width + 'px'; |
|
|
panelDiv.style.height = panel.height + 'px'; |
|
|
panelDiv.dataset.panelIndex = panelIndex; |
|
|
|
|
|
const img = document.createElement('img'); |
|
|
img.src = panel.image; |
|
|
panelDiv.appendChild(img); |
|
|
|
|
|
pageDiv.appendChild(panelDiv); |
|
|
}); |
|
|
|
|
|
|
|
|
page.bubbles.forEach(bubble => { |
|
|
this.createBubble(bubble, pageDiv); |
|
|
}); |
|
|
|
|
|
this.container.appendChild(pageDiv); |
|
|
}); |
|
|
} |
|
|
|
|
|
createBubble(bubbleData, pageDiv) { |
|
|
const bubble = document.createElement('div'); |
|
|
bubble.className = 'speech-bubble'; |
|
|
bubble.id = bubbleData.id || 'bubble_' + Date.now(); |
|
|
bubble.style.left = bubbleData.x + 'px'; |
|
|
bubble.style.top = bubbleData.y + 'px'; |
|
|
bubble.style.width = bubbleData.width + 'px'; |
|
|
bubble.style.height = bubbleData.height + 'px'; |
|
|
|
|
|
|
|
|
const text = document.createElement('div'); |
|
|
text.className = 'bubble-text'; |
|
|
text.textContent = bubbleData.text || 'Click to edit'; |
|
|
text.contentEditable = false; |
|
|
bubble.appendChild(text); |
|
|
|
|
|
|
|
|
const tail = document.createElement('div'); |
|
|
tail.className = 'bubble-tail'; |
|
|
bubble.appendChild(tail); |
|
|
|
|
|
|
|
|
const resizeHandle = document.createElement('div'); |
|
|
resizeHandle.className = 'resize-handle se'; |
|
|
bubble.appendChild(resizeHandle); |
|
|
|
|
|
|
|
|
const coords = document.createElement('div'); |
|
|
coords.className = 'coordinates'; |
|
|
bubble.appendChild(coords); |
|
|
|
|
|
|
|
|
bubble.dataset.bubbleData = JSON.stringify(bubbleData); |
|
|
|
|
|
pageDiv.appendChild(bubble); |
|
|
this.bubbles.push(bubble); |
|
|
|
|
|
|
|
|
this.setupBubbleEvents(bubble); |
|
|
} |
|
|
|
|
|
setupEventListeners() { |
|
|
|
|
|
document.addEventListener('mousemove', (e) => this.handleMouseMove(e)); |
|
|
document.addEventListener('mouseup', (e) => this.handleMouseUp(e)); |
|
|
|
|
|
|
|
|
document.addEventListener('keydown', (e) => { |
|
|
if (e.key === 'Delete' && this.selectedBubble && !this.isEditing) { |
|
|
this.deleteBubble(this.selectedBubble); |
|
|
} |
|
|
if (e.key === 'Escape') { |
|
|
this.deselectBubble(); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
this.container.addEventListener('click', (e) => { |
|
|
if (e.target === this.container || e.target.classList.contains('comic-page')) { |
|
|
this.deselectBubble(); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
setupBubbleEvents(bubble) { |
|
|
const text = bubble.querySelector('.bubble-text'); |
|
|
const resizeHandle = bubble.querySelector('.resize-handle'); |
|
|
|
|
|
|
|
|
bubble.addEventListener('mousedown', (e) => { |
|
|
if (e.target === text && this.isEditing) return; |
|
|
if (e.target === resizeHandle) return; |
|
|
|
|
|
this.startDragging(bubble, e); |
|
|
}); |
|
|
|
|
|
|
|
|
bubble.addEventListener('click', (e) => { |
|
|
e.stopPropagation(); |
|
|
this.selectBubble(bubble); |
|
|
}); |
|
|
|
|
|
|
|
|
text.addEventListener('dblclick', (e) => { |
|
|
e.stopPropagation(); |
|
|
this.startEditingText(bubble, text); |
|
|
}); |
|
|
|
|
|
|
|
|
text.addEventListener('blur', () => { |
|
|
if (this.isEditing) { |
|
|
this.stopEditingText(bubble, text); |
|
|
} |
|
|
}); |
|
|
|
|
|
text.addEventListener('keydown', (e) => { |
|
|
if (e.key === 'Enter' && !e.shiftKey) { |
|
|
e.preventDefault(); |
|
|
text.blur(); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
resizeHandle.addEventListener('mousedown', (e) => { |
|
|
e.stopPropagation(); |
|
|
this.startResizing(bubble, e); |
|
|
}); |
|
|
} |
|
|
|
|
|
startDragging(bubble, e) { |
|
|
this.isDragging = true; |
|
|
this.selectedBubble = bubble; |
|
|
bubble.classList.add('dragging'); |
|
|
|
|
|
const rect = bubble.getBoundingClientRect(); |
|
|
const containerRect = this.container.getBoundingClientRect(); |
|
|
|
|
|
this.dragOffset = { |
|
|
x: e.clientX - rect.left, |
|
|
y: e.clientY - rect.top |
|
|
}; |
|
|
|
|
|
this.selectBubble(bubble); |
|
|
} |
|
|
|
|
|
handleMouseMove(e) { |
|
|
if (!this.isDragging || !this.selectedBubble) return; |
|
|
|
|
|
const containerRect = this.container.getBoundingClientRect(); |
|
|
const pageRect = this.selectedBubble.parentElement.getBoundingClientRect(); |
|
|
|
|
|
let newX = e.clientX - pageRect.left - this.dragOffset.x; |
|
|
let newY = e.clientY - pageRect.top - this.dragOffset.y; |
|
|
|
|
|
|
|
|
const maxX = pageRect.width - this.selectedBubble.offsetWidth; |
|
|
const maxY = pageRect.height - this.selectedBubble.offsetHeight; |
|
|
|
|
|
newX = Math.max(0, Math.min(newX, maxX)); |
|
|
newY = Math.max(0, Math.min(newY, maxY)); |
|
|
|
|
|
this.selectedBubble.style.left = newX + 'px'; |
|
|
this.selectedBubble.style.top = newY + 'px'; |
|
|
|
|
|
this.updateCoordinates(this.selectedBubble); |
|
|
} |
|
|
|
|
|
handleMouseUp(e) { |
|
|
if (this.isDragging && this.selectedBubble) { |
|
|
this.selectedBubble.classList.remove('dragging'); |
|
|
this.isDragging = false; |
|
|
this.saveBubblePosition(this.selectedBubble); |
|
|
} |
|
|
} |
|
|
|
|
|
selectBubble(bubble) { |
|
|
|
|
|
this.deselectBubble(); |
|
|
|
|
|
|
|
|
this.selectedBubble = bubble; |
|
|
bubble.classList.add('selected'); |
|
|
|
|
|
this.updateCoordinates(bubble); |
|
|
this.showHint('Double-click to edit text • Drag to move • Delete key to remove'); |
|
|
} |
|
|
|
|
|
deselectBubble() { |
|
|
if (this.selectedBubble) { |
|
|
this.selectedBubble.classList.remove('selected'); |
|
|
this.selectedBubble = null; |
|
|
} |
|
|
this.hideHint(); |
|
|
} |
|
|
|
|
|
startEditingText(bubble, textElement) { |
|
|
this.isEditing = true; |
|
|
textElement.contentEditable = true; |
|
|
textElement.classList.add('editing'); |
|
|
textElement.focus(); |
|
|
|
|
|
|
|
|
const range = document.createRange(); |
|
|
range.selectNodeContents(textElement); |
|
|
const selection = window.getSelection(); |
|
|
selection.removeAllRanges(); |
|
|
selection.addRange(range); |
|
|
|
|
|
this.showHint('Press Enter to save • Shift+Enter for new line'); |
|
|
} |
|
|
|
|
|
stopEditingText(bubble, textElement) { |
|
|
this.isEditing = false; |
|
|
textElement.contentEditable = false; |
|
|
textElement.classList.remove('editing'); |
|
|
|
|
|
|
|
|
this.saveBubbleText(bubble, textElement.textContent); |
|
|
this.hideHint(); |
|
|
} |
|
|
|
|
|
deleteBubble(bubble) { |
|
|
if (confirm('Delete this speech bubble?')) { |
|
|
bubble.remove(); |
|
|
const index = this.bubbles.indexOf(bubble); |
|
|
if (index > -1) { |
|
|
this.bubbles.splice(index, 1); |
|
|
} |
|
|
this.selectedBubble = null; |
|
|
this.saveComicData(); |
|
|
} |
|
|
} |
|
|
|
|
|
updateCoordinates(bubble) { |
|
|
const coords = bubble.querySelector('.coordinates'); |
|
|
coords.textContent = `x: ${parseInt(bubble.style.left)}, y: ${parseInt(bubble.style.top)}`; |
|
|
} |
|
|
|
|
|
createToolbar() { |
|
|
const toolbar = document.createElement('div'); |
|
|
toolbar.className = 'editor-toolbar'; |
|
|
|
|
|
|
|
|
const addBtn = document.createElement('button'); |
|
|
addBtn.className = 'toolbar-btn'; |
|
|
addBtn.textContent = '➕ Add Bubble'; |
|
|
addBtn.onclick = () => this.addNewBubble(); |
|
|
toolbar.appendChild(addBtn); |
|
|
|
|
|
|
|
|
const saveBtn = document.createElement('button'); |
|
|
saveBtn.className = 'toolbar-btn success'; |
|
|
saveBtn.textContent = '💾 Save Comic'; |
|
|
saveBtn.onclick = () => this.saveComic(); |
|
|
toolbar.appendChild(saveBtn); |
|
|
|
|
|
|
|
|
const exportBtn = document.createElement('button'); |
|
|
exportBtn.className = 'toolbar-btn download'; |
|
|
exportBtn.textContent = '⬇️ Download'; |
|
|
exportBtn.onclick = () => this.downloadPages(); |
|
|
toolbar.appendChild(exportBtn); |
|
|
|
|
|
|
|
|
const resetBtn = document.createElement('button'); |
|
|
resetBtn.className = 'toolbar-btn danger'; |
|
|
resetBtn.textContent = '🔄 Reset'; |
|
|
resetBtn.onclick = () => this.resetComic(); |
|
|
toolbar.appendChild(resetBtn); |
|
|
|
|
|
document.body.appendChild(toolbar); |
|
|
} |
|
|
|
|
|
addNewBubble() { |
|
|
const page = this.container.querySelector('.comic-page'); |
|
|
if (!page) return; |
|
|
|
|
|
const newBubble = { |
|
|
id: 'bubble_' + Date.now(), |
|
|
x: 100, |
|
|
y: 100, |
|
|
width: 150, |
|
|
height: 60, |
|
|
text: 'New bubble!' |
|
|
}; |
|
|
|
|
|
this.createBubble(newBubble, page); |
|
|
this.saveComicData(); |
|
|
} |
|
|
|
|
|
saveBubblePosition(bubble) { |
|
|
this.saveComicData(); |
|
|
} |
|
|
|
|
|
saveBubbleText(bubble, text) { |
|
|
const data = JSON.parse(bubble.dataset.bubbleData || '{}'); |
|
|
data.text = text; |
|
|
bubble.dataset.bubbleData = JSON.stringify(data); |
|
|
this.saveComicData(); |
|
|
} |
|
|
|
|
|
saveComicData() { |
|
|
const data = { |
|
|
pages: [] |
|
|
}; |
|
|
|
|
|
this.container.querySelectorAll('.comic-page').forEach(page => { |
|
|
const pageData = { |
|
|
width: parseInt(page.style.width), |
|
|
height: parseInt(page.style.height), |
|
|
panels: [], |
|
|
bubbles: [] |
|
|
}; |
|
|
|
|
|
|
|
|
page.querySelectorAll('.comic-panel').forEach(panel => { |
|
|
pageData.panels.push({ |
|
|
x: parseInt(panel.style.left), |
|
|
y: parseInt(panel.style.top), |
|
|
width: parseInt(panel.style.width), |
|
|
height: parseInt(panel.style.height), |
|
|
image: panel.querySelector('img').src |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
page.querySelectorAll('.speech-bubble').forEach(bubble => { |
|
|
pageData.bubbles.push({ |
|
|
id: bubble.id, |
|
|
x: parseInt(bubble.style.left), |
|
|
y: parseInt(bubble.style.top), |
|
|
width: parseInt(bubble.style.width), |
|
|
height: parseInt(bubble.style.height), |
|
|
text: bubble.querySelector('.bubble-text').textContent |
|
|
}); |
|
|
}); |
|
|
|
|
|
data.pages.push(pageData); |
|
|
}); |
|
|
|
|
|
localStorage.setItem('comicEditorData', JSON.stringify(data)); |
|
|
this.showHint('Comic saved!'); |
|
|
} |
|
|
|
|
|
saveComic() { |
|
|
this.saveComicData(); |
|
|
|
|
|
|
|
|
fetch('/save_comic', { |
|
|
method: 'POST', |
|
|
headers: { |
|
|
'Content-Type': 'application/json', |
|
|
}, |
|
|
body: JSON.stringify(this.getComicData()) |
|
|
}) |
|
|
.then(response => response.json()) |
|
|
.then(data => { |
|
|
this.showHint('Comic saved to server!'); |
|
|
}) |
|
|
.catch(error => { |
|
|
console.error('Error:', error); |
|
|
this.showHint('Error saving to server!'); |
|
|
}); |
|
|
} |
|
|
|
|
|
exportComic() { |
|
|
const data = this.getComicData(); |
|
|
const json = JSON.stringify(data, null, 2); |
|
|
|
|
|
|
|
|
const blob = new Blob([json], { type: 'application/json' }); |
|
|
const url = URL.createObjectURL(blob); |
|
|
const a = document.createElement('a'); |
|
|
a.href = url; |
|
|
a.download = 'comic_data.json'; |
|
|
a.click(); |
|
|
URL.revokeObjectURL(url); |
|
|
|
|
|
this.showHint('Comic exported!'); |
|
|
} |
|
|
|
|
|
resetComic() { |
|
|
if (confirm('Reset all changes? This cannot be undone!')) { |
|
|
localStorage.removeItem('comicEditorData'); |
|
|
this.loadFromServer(); |
|
|
this.showHint('Comic reset!'); |
|
|
} |
|
|
} |
|
|
|
|
|
getComicData() { |
|
|
return JSON.parse(localStorage.getItem('comicEditorData') || '{}'); |
|
|
} |
|
|
|
|
|
showHint(message) { |
|
|
let hint = document.querySelector('.edit-hint'); |
|
|
if (!hint) { |
|
|
hint = document.createElement('div'); |
|
|
hint.className = 'edit-hint'; |
|
|
document.body.appendChild(hint); |
|
|
} |
|
|
|
|
|
hint.textContent = message; |
|
|
hint.classList.add('show'); |
|
|
|
|
|
clearTimeout(this.hintTimeout); |
|
|
this.hintTimeout = setTimeout(() => { |
|
|
hint.classList.remove('show'); |
|
|
}, 3000); |
|
|
} |
|
|
|
|
|
hideHint() { |
|
|
const hint = document.querySelector('.edit-hint'); |
|
|
if (hint) { |
|
|
hint.classList.remove('show'); |
|
|
} |
|
|
} |
|
|
|
|
|
startResizing(bubble, e) { |
|
|
e.preventDefault(); |
|
|
|
|
|
const startX = e.clientX; |
|
|
const startY = e.clientY; |
|
|
const startWidth = parseInt(bubble.style.width); |
|
|
const startHeight = parseInt(bubble.style.height); |
|
|
|
|
|
const handleResize = (e) => { |
|
|
const newWidth = startWidth + (e.clientX - startX); |
|
|
const newHeight = startHeight + (e.clientY - startY); |
|
|
|
|
|
bubble.style.width = Math.max(100, newWidth) + 'px'; |
|
|
bubble.style.height = Math.max(50, newHeight) + 'px'; |
|
|
|
|
|
this.updateCoordinates(bubble); |
|
|
}; |
|
|
|
|
|
const stopResize = () => { |
|
|
document.removeEventListener('mousemove', handleResize); |
|
|
document.removeEventListener('mouseup', stopResize); |
|
|
this.saveComicData(); |
|
|
}; |
|
|
|
|
|
document.addEventListener('mousemove', handleResize); |
|
|
document.addEventListener('mouseup', stopResize); |
|
|
} |
|
|
|
|
|
|
|
|
downloadPages() { |
|
|
const pages = this.container.querySelectorAll('.comic-page'); |
|
|
if (!pages.length) return; |
|
|
pages.forEach((page, idx) => { |
|
|
html2canvas(page, {width: 800, height: 1080, scale: 2, useCORS: true, allowTaint: true}).then(canvas => { |
|
|
canvas.toBlob(blob => { |
|
|
const a = document.createElement('a'); |
|
|
a.download = `comic_page_${idx+1}.png`; |
|
|
a.href = URL.createObjectURL(blob); |
|
|
a.click(); |
|
|
URL.revokeObjectURL(a.href); |
|
|
}, 'image/png'); |
|
|
}); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
if (document.getElementById('comic-editor')) { |
|
|
window.comicEditor = new ComicEditor('comic-editor'); |
|
|
} |
|
|
}); |