';
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
});