Testcomic / static /comic_editor.js
3v324v23's picture
Update Comic123 with local comic folder files
83e35a7
/**
* Interactive Comic Editor
* Allows dragging speech bubbles and editing text
*/
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() {
// Add editor styles
this.addStyles();
// Load comic data
this.loadComicData();
// Setup event listeners
this.setupEventListeners();
// Add toolbar
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() {
// Load existing comic data or create new
const savedData = localStorage.getItem('comicEditorData');
if (savedData) {
const data = JSON.parse(savedData);
this.renderComic(data);
} else {
// Load from server or create default
this.loadFromServer();
}
}
loadFromServer() {
// Load from server
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() {
// Create a default comic if loading fails
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;
// Render panels
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);
});
// Render bubbles
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';
// Add text
const text = document.createElement('div');
text.className = 'bubble-text';
text.textContent = bubbleData.text || 'Click to edit';
text.contentEditable = false;
bubble.appendChild(text);
// Add tail
const tail = document.createElement('div');
tail.className = 'bubble-tail';
bubble.appendChild(tail);
// Add resize handle
const resizeHandle = document.createElement('div');
resizeHandle.className = 'resize-handle se';
bubble.appendChild(resizeHandle);
// Add coordinates display
const coords = document.createElement('div');
coords.className = 'coordinates';
bubble.appendChild(coords);
// Store data
bubble.dataset.bubbleData = JSON.stringify(bubbleData);
pageDiv.appendChild(bubble);
this.bubbles.push(bubble);
// Setup bubble events
this.setupBubbleEvents(bubble);
}
setupEventListeners() {
// Document-wide mouse events
document.addEventListener('mousemove', (e) => this.handleMouseMove(e));
document.addEventListener('mouseup', (e) => this.handleMouseUp(e));
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.key === 'Delete' && this.selectedBubble && !this.isEditing) {
this.deleteBubble(this.selectedBubble);
}
if (e.key === 'Escape') {
this.deselectBubble();
}
});
// Click outside to deselect
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');
// Drag start
bubble.addEventListener('mousedown', (e) => {
if (e.target === text && this.isEditing) return;
if (e.target === resizeHandle) return;
this.startDragging(bubble, e);
});
// Click to select
bubble.addEventListener('click', (e) => {
e.stopPropagation();
this.selectBubble(bubble);
});
// Double-click to edit text
text.addEventListener('dblclick', (e) => {
e.stopPropagation();
this.startEditingText(bubble, text);
});
// Handle text editing
text.addEventListener('blur', () => {
if (this.isEditing) {
this.stopEditingText(bubble, text);
}
});
text.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
text.blur();
}
});
// Resize handle
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;
// Constrain to page bounds
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) {
// Deselect previous
this.deselectBubble();
// Select new
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();
// Select all text
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');
// Save the text
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';
// Add bubble button
const addBtn = document.createElement('button');
addBtn.className = 'toolbar-btn';
addBtn.textContent = '➕ Add Bubble';
addBtn.onclick = () => this.addNewBubble();
toolbar.appendChild(addBtn);
// Save button
const saveBtn = document.createElement('button');
saveBtn.className = 'toolbar-btn success';
saveBtn.textContent = '💾 Save Comic';
saveBtn.onclick = () => this.saveComic();
toolbar.appendChild(saveBtn);
// Export button
const exportBtn = document.createElement('button');
exportBtn.className = 'toolbar-btn download';
exportBtn.textContent = '⬇️ Download';
exportBtn.onclick = () => this.downloadPages();
toolbar.appendChild(exportBtn);
// Reset button
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: []
};
// Save panel data
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
});
});
// Save bubble data
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();
// Send to server
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);
// Create download
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);
}
/** Download each page as PNG using html2canvas */
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');
});
});
}
}
// Initialize when page loads
document.addEventListener('DOMContentLoaded', () => {
if (document.getElementById('comic-editor')) {
window.comicEditor = new ComicEditor('comic-editor');
}
});