Comic Strip Creator

Comic Strip Creator

Comic Strip Creator

Upload images or add text assets here. Drag them to panels!

Drag assets from above into the comic panels below.

Click on text in a panel to edit. Drag-and-drop assets into panels.

'; return; } assets.forEach(asset => { const div = document.createElement('div'); div.classList.add('csc-asset'); div.dataset.assetId = asset.id; div.draggable = true; if (asset.type === 'image') { div.classList.add('image'); const img = document.createElement('img'); img.src = asset.content; div.appendChild(img); } else if (asset.type === 'text_bubble' || asset.type === 'caption') { div.classList.add('text'); div.textContent = asset.content.length > 50 ? asset.content.substring(0, 47) + '...' : asset.content; } if (asset.used) { div.classList.add('used'); } assetsArea.appendChild(div); }); } /** * Handles clicking on a text asset in the assets area to edit it. */ function handleAssetClick(e) { const assetElement = e.target.closest('.csc-asset.text'); if (assetElement && !assetElement.classList.contains('used')) { const assetId = assetElement.dataset.assetId; const asset = assets.find(a => a.id === assetId); if (asset) { activeTextElement = assetElement; // Set the asset as active for modal openTextModal(asset.type, `Edit ${asset.type.replace('_', ' ')}`, asset.content, asset.id); } } } // === Drag & Drop === function handleDragStart(e) { const assetElement = e.target.closest('.csc-asset'); if (assetElement && !assetElement.classList.contains('used')) { draggedAssetId = assetElement.dataset.assetId; e.dataTransfer.setData('text/plain', draggedAssetId); e.target.classList.add('dragging'); } else { e.preventDefault(); // Don't allow dragging used assets } } function handleDragOver(e) { e.preventDefault(); // Allow drop } function handleDrop(e) { e.preventDefault(); const panel = e.target.closest('.csc-panel'); if (panel && draggedAssetId) { const asset = assets.find(a => a.id === draggedAssetId); if (asset && !asset.used) { assignAssetToPanel(asset, panel.id); } } if (draggedAssetId) { document.querySelector(`[data-asset-id="${draggedAssetId}"]`).classList.remove('dragging'); draggedAssetId = null; } } /** * Assigns an asset to a comic panel. */ function assignAssetToPanel(asset, panelId) { const panel = document.getElementById(panelId); if (!panel) return; let existingContent = panelContents[panelId] || {}; // Remove previous content of the same type from this panel if (asset.type === 'image' && existingContent.imageId) { const oldImage = assets.find(a => a.id === existingContent.imageId); if (oldImage) oldImage.used = false; panel.querySelector('.csc-panel-image')?.remove(); delete existingContent.imageId; } else if (asset.type === 'text_bubble' && existingContent.textBubbleId) { const oldText = assets.find(a => a.id === existingContent.textBubbleId); if (oldText) oldText.used = false; panel.querySelector('.csc-panel-text')?.remove(); delete existingContent.textBubbleId; } else if (asset.type === 'caption' && existingContent.captionId) { const oldCaption = assets.find(a => a.id === existingContent.captionId); if (oldCaption) oldCaption.used = false; panel.querySelector('.csc-panel-caption')?.remove(); delete existingContent.captionId; } // Add new asset if (asset.type === 'image') { const img = document.createElement('img'); img.src = asset.content; img.classList.add('csc-panel-image'); panel.prepend(img); // Prepend to ensure it's behind text existingContent.imageId = asset.id; } else if (asset.type === 'text_bubble') { const textDiv = document.createElement('div'); textDiv.classList.add('csc-panel-text'); textDiv.textContent = asset.content; textDiv.dataset.assetId = asset.id; // Link back to asset textDiv.dataset.type = 'text_bubble'; panel.appendChild(textDiv); existingContent.textBubbleId = asset.id; } else if (asset.type === 'caption') { const captionDiv = document.createElement('div'); captionDiv.classList.add('csc-panel-caption'); captionDiv.textContent = asset.content; captionDiv.dataset.assetId = asset.id; // Link back to asset captionDiv.dataset.type = 'caption'; panel.appendChild(captionDiv); existingContent.captionId = asset.id; } panelContents[panelId] = existingContent; asset.used = true; renderAssets(); // Update asset area to show 'used' status showMessage(null); // Clear errors } /** * Handles clicks inside a panel (for editing text or clearing). */ function handlePanelClick(e) { const textDiv = e.target.closest('.csc-panel-text'); const captionDiv = e.target.closest('.csc-panel-caption'); if (textDiv || captionDiv) { const element = textDiv || captionDiv; const assetId = element.dataset.assetId; const asset = assets.find(a => a.id === assetId); if (asset) { activeTextElement = element; // Set the panel text element as active for modal openTextModal(asset.type, `Edit ${asset.type.replace('_', ' ')}`, asset.content, asset.id); } } else { const panel = e.target.closest('.csc-panel'); if (panel && confirm('Clear this panel (image & text)?')) { clearPanel(panel.id); } } } /** * Clears all content from a specific panel. */ function clearPanel(panelId) { const panel = document.getElementById(panelId); if (!panel) return; let existingContent = panelContents[panelId] || {}; if (existingContent.imageId) { const imgAsset = assets.find(a => a.id === existingContent.imageId); if (imgAsset) imgAsset.used = false; panel.querySelector('.csc-panel-image')?.remove(); } if (existingContent.textBubbleId) { const textAsset = assets.find(a => a.id === existingContent.textBubbleId); if (textAsset) textAsset.used = false; panel.querySelector('.csc-panel-text')?.remove(); } if (existingContent.captionId) { const captionAsset = assets.find(a => a.id === existingContent.captionId); if (captionAsset) captionAsset.used = false; panel.querySelector('.csc-panel-caption')?.remove(); } panelContents[panelId] = {}; // Clear content for this panel renderAssets(); // Update asset area to show 'unused' status showMessage(null); } // === Layout & Rendering === /** * Handles layout dropdown changes. */ function handleLayoutChange(e) { currentLayout = e.target.value; panelContents = {}; // Clear panel contents on layout change assets.forEach(asset => asset.used = false); // Mark all assets as unused renderComicStripLayout(); renderAssets(); // Update asset area } /** * Renders the comic strip grid based on the current layout. */ function renderComicStripLayout() { comicStripArea.className = ''; // Clear all existing classes comicStripArea.classList.add('flex', 'flex-wrap', 'csc-comic-strip-area'); // Base classes comicStripArea.innerHTML = ''; // Clear previous panels const layout = layouts[currentLayout]; if (!layout) { console.error("Layout not found:", currentLayout); return; } if (layout.gridClasses) { comicStripArea.classList.add(...layout.gridClasses.split(' ')); comicStripArea.style.gridTemplateColumns = ''; // Clear custom template comicStripArea.style.gridTemplateRows = ''; } else if (layout.gridTemplate) { // Parse custom template and apply const parts = layout.gridTemplate.split(';'); parts.forEach(part => { const [prop, value] = part.split(':').map(s => s.trim()); if (prop && value) { if (prop === 'grid-template-columns') { comicStripArea.style.gridTemplateColumns = value; } else if (prop === 'grid-template-rows') { comicStripArea.style.gridTemplateRows = value; } } }); // Clear default grid classes if custom template is used comicStripArea.classList.remove('grid-cols-1', 'grid-cols-2', 'grid-cols-3', 'grid-rows-1', 'grid-rows-2', 'grid-rows-3'); } else { // Fallback for non-grid layouts, maybe just use flex comicStripArea.style.display = 'flex'; comicStripArea.style.flexDirection = 'row'; // Or column based on intent comicStripArea.style.gap = '5px'; // Minimal gap } for (let i = 0; i < layout.panelCount; i++) { const panel = document.createElement('div'); panel.id = `panel-${i}`; panel.classList.add('csc-panel'); // Apply custom spans if defined for this layout if (layout.panelSpans && layout.panelSpans[`panel-${i}`]) { panel.classList.add(...layout.panelSpans[`panel-${i}`].split(' ')); } comicStripArea.appendChild(panel); } } /** * Clears all panels and assets. */ function clearAll() { if (!confirm('Are you sure you want to clear the entire comic strip and all uploaded assets?')) { return; } assets = []; panelContents = {}; renderComicStripLayout(); // Redraw empty layout renderAssets(); // Clear asset area showMessage(null); } // === PDF Download === /** * Handles the download button click using html2canvas and jsPDF. */ async function handleDownload() { if (!html2canvas || !jsPDF) { showMessage("Error: Download libraries not loaded.", true); return; } // Check for at least one image in the strip const panels = Array.from(comicStripArea.querySelectorAll('.csc-panel')); const hasImage = panels.some(panel => panel.querySelector('.csc-panel-image')); if (!hasImage) { showMessage("Please add at least one image to your comic strip before downloading.", true); return; } showLoader(true); showMessage(null); try { // Temporarily remove modal if open const wasModalOpen = textModal.style.display !== 'none'; if (wasModalOpen) textModal.style.display = 'none'; // Ensure images are loaded before capturing await ensureAllPanelImagesLoaded(); const canvas = await html2canvas(comicStripArea, { useCORS: true, // Needed if images are from external sources, even data URLs logging: false, // Reduce console noise scale: 2, // Increase resolution for better PDF quality // Other options for html2canvas to ensure all content is captured windowWidth: comicStripArea.offsetWidth, windowHeight: comicStripArea.offsetHeight + 100, // Add some buffer x: comicStripArea.offsetLeft, y: comicStripArea.offsetTop, width: comicStripArea.offsetWidth, height: comicStripArea.offsetHeight, ignoreElements: (element) => element.id === 'csc-loader-overlay', }); const imgData = canvas.toDataURL('image/jpeg', 0.9); // Use JPEG for smaller file size const pdf = new jsPDF({ orientation: canvas.width > canvas.height ? 'l' : 'p', // landscape or portrait unit: 'px', format: [canvas.width, canvas.height] // Use canvas dimensions }); pdf.addImage(imgData, 'JPEG', 0, 0, canvas.width, canvas.height); pdf.save('comic_strip.pdf'); } catch (error) { console.error("Error generating PDF:", error); showMessage("Error creating PDF. Please ensure all images are loaded.", true); } finally { showLoader(false); if (wasModalOpen) textModal.style.display = 'flex'; // Restore modal } } /** * Helper to ensure all images within the panels are loaded before html2canvas capture. */ async function ensureAllPanelImagesLoaded() { const images = Array.from(comicStripArea.querySelectorAll('.csc-panel-image')); const promises = images.map(img => new Promise((resolve, reject) => { if (img.complete && img.naturalHeight !== 0) { resolve(); } else { img.onload = resolve; img.onerror = reject; // Reject if an image fails to load } })); // Use Promise.race to prevent infinite waiting if an image never loads // Wait for all images or a maximum of 3 seconds. return Promise.race([ Promise.all(promises), new Promise(resolve => setTimeout(() => { console.warn("Timeout waiting for some images to load for PDF capture."); resolve(); // Resolve anyway after timeout to not block }, 3000)) ]); } // === Text Input Modal === /** * Opens the text input modal. * @param {'text_bubble'|'caption'} type - The type of text being edited. * @param {string} title - The title for the modal. * @param {string} initialText - The text to pre-fill the textarea with. * @param {string} [assetIdToEdit] - Optional ID of an existing asset being edited. */ function openTextModal(type, title, initialText, assetIdToEdit = null) { modalTitle.textContent = title; modalTextarea.value = initialText; modalTextarea.dataset.type = type; // Store type in dataset modalTextarea.dataset.assetId = assetIdToEdit || ''; // Store asset ID if editing existing textModal.style.display = 'flex'; } /** * Closes the text input modal. */ function closeModal() { textModal.style.display = 'none'; activeTextElement = null; // Clear active element } /** * Saves the text from the modal. */ function saveModalText() { const text = modalTextarea.value.trim(); const type = modalTextarea.dataset.type; const assetId = modalTextarea.dataset.assetId; if (!text) { showMessage("Text cannot be empty.", true); return; } if (assetId) { // Editing an existing asset or panel text const asset = assets.find(a => a.id === assetId); if (asset) { asset.content = text; renderAssets(); // Update thumbnail } if (activeTextElement) { // Update the text directly in the panel activeTextElement.textContent = text; } } else { // Creating a new asset addTextAsset(type, text); } showMessage(null); // Clear previous errors closeModal(); } // === UI State Functions === function showLoader(show) { loaderOverlay.style.display = show ? 'flex' : 'none'; } function showMessage(message, isError = false) { if (!message) { messageArea.style.display = 'none'; return; } messageArea.textContent = message; messageArea.className = `mt-4 text-center font-medium ${isError ? 'text-red-600' : 'text-blue-600'}`; messageArea.style.display = 'block'; // Auto-hide message after a few seconds setTimeout(() => { if(messageArea.textContent === message) { // Only hide if it's still the same message messageArea.style.display = 'none'; } }, 4000); } // === Initial Render === renderComicStripLayout(); // Render initial empty layout renderAssets(); // Render empty assets area placeholder });