Files
yoto-covers/app.js
T

1664 lines
78 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Yoto Up Covers Web App
class YotoCoversApp {
constructor() {
this.images = [];
this.currentTemplate = 'classic';
this.currentTitleStyle = 'folded';
// Template presets - each defines a set of feature defaults
this.templates = {
classic: {
backgroundStyle: 'solid',
fontFamily: 'Arial',
fontWeight: 'normal',
footerColor: '#666666'
},
frozen: {
backgroundStyle: 'gradient',
fontFamily: 'Arial',
fontWeight: 'normal',
footerColor: '#4a90e2'
},
minimal: {
backgroundStyle: 'solid',
fontFamily: 'Arial',
fontWeight: 'normal',
footerColor: '#999999'
},
vintage: {
backgroundStyle: 'gradient',
fontFamily: 'Georgia',
fontWeight: 'normal',
footerColor: '#654321'
},
comic: {
backgroundStyle: 'comic',
fontFamily: 'Comic Sans MS',
fontWeight: 'bold',
footerColor: '#ff6b6b',
fullBleed: false,
panelMode: true,
titlePanel: { borderWidth: 2, rotation: -2, expand: true },
imagePanel: { borderWidth: 3, rotation: 1 },
footerPanel: { borderWidth: 2, rotation: 3 }
},
polaroid: {
backgroundStyle: 'polaroid',
fontFamily: 'Arial',
fontWeight: 'normal',
footerColor: '#666666',
fullBleed: false,
showFooter: false,
imagePosition: 'center'
}
};
this.printLayout = {
cardsPerRow: 2,
cardsPerColumn: 5,
pageMargin: 10,
cardSpacing: 5
};
this.templateSettings = {
title: '',
footer: '',
accentColor: '#f1c40f',
titleColor: '#111111',
fullBleed: true,
titleShadow: false,
titleFontSize: null,
footerFontSize: null
};
this.cardVariations = [];
this.activeCardIndex = -1; // Currently selected card for editing
this.init();
}
init() {
this.bindEvents();
this.updateUI();
this.updateCardList();
}
bindEvents() {
// Print layout settings
document.getElementById('cards-per-row').addEventListener('change', (e) => {
this.printLayout.cardsPerRow = parseInt(e.target.value);
this.generatePreview();
});
document.getElementById('cards-per-column').addEventListener('change', (e) => {
this.printLayout.cardsPerColumn = parseInt(e.target.value);
this.generatePreview();
});
document.getElementById('page-margin').addEventListener('input', (e) => {
this.printLayout.pageMargin = parseInt(e.target.value);
this.generatePreview();
});
document.getElementById('card-spacing').addEventListener('input', (e) => {
this.printLayout.cardSpacing = parseInt(e.target.value);
this.generatePreview();
});
// Template settings - REMOVED: Now per-card only
// document.getElementById('template-select').addEventListener('change', (e) => {
// this.currentTemplate = e.target.value;
// this.generatePreview();
// });
// document.getElementById('title-input').addEventListener('input', (e) => {
// this.templateSettings.title = e.target.value;
// this.generatePreview();
// });
// document.getElementById('footer-input').addEventListener('input', (e) => {
// this.templateSettings.footer = e.target.value;
// this.generatePreview();
// });
// Title style selection - REMOVED: Now per-card only
// document.querySelectorAll('.title-style-option').forEach(option => {
// option.addEventListener('click', () => {
// document.querySelectorAll('.title-style-option').forEach(opt => opt.classList.remove('selected'));
// option.classList.add('selected');
// this.currentTitleStyle = option.dataset.style;
// this.generatePreview();
// });
// });
// Color pickers - REMOVED: Now per-card only
// document.getElementById('accent-color').addEventListener('input', (e) => {
// this.templateSettings.accentColor = e.target.value;
// document.getElementById('accent-color-text').value = e.target.value;
// this.generatePreview();
// });
// document.getElementById('title-color').addEventListener('input', (e) => {
// this.templateSettings.titleColor = e.target.value;
// document.getElementById('title-color-text').value = e.target.value;
// this.generatePreview();
// });
// Checkboxes - REMOVED: Now per-card only
// document.getElementById('full-bleed').addEventListener('change', (e) => {
// this.templateSettings.fullBleed = e.target.checked;
// this.generatePreview();
// });
// document.getElementById('title-shadow').addEventListener('change', (e) => {
// this.templateSettings.titleShadow = e.target.checked;
// this.generatePreview();
// });
// Image upload
const imageUpload = document.getElementById('image-upload');
const fileInputLabel = document.querySelector('.file-input-label');
imageUpload.addEventListener('change', (e) => this.handleImageUpload(e.target.files));
imageUpload.addEventListener('dragover', (e) => {
e.preventDefault();
fileInputLabel.classList.add('dragover');
});
imageUpload.addEventListener('dragleave', (e) => {
e.preventDefault();
fileInputLabel.classList.remove('dragover');
});
imageUpload.addEventListener('drop', (e) => {
e.preventDefault();
fileInputLabel.classList.remove('dragover');
this.handleImageUpload(e.dataTransfer.files);
});
// Image management
document.getElementById('clear-images').addEventListener('click', () => this.clearAllImages());
// Actions
document.getElementById('generate-preview').addEventListener('click', () => this.generatePreview());
document.getElementById('download-html').addEventListener('click', () => this.downloadHTML());
document.getElementById('open-preview').addEventListener('click', () => this.openPreviewInNewTab());
document.getElementById('print-preview').addEventListener('click', () => this.printPreview());
// Card management
document.getElementById('add-card').addEventListener('click', () => this.addCardVariation());
document.getElementById('active-card-select').addEventListener('change', (e) => {
const index = parseInt(e.target.value);
if (index >= 0) {
this.selectCard(index);
} else {
this.activeCardIndex = -1;
this.updateCardList();
this.showCardEditor();
}
});
document.getElementById('update-card').addEventListener('click', () => this.updateActiveCard());
document.getElementById('delete-card').addEventListener('click', () => this.deleteActiveCard());
// Card editor auto-update
document.getElementById('card-title-input').addEventListener('input', () => this.updateActiveCard());
document.getElementById('card-footer-input').addEventListener('input', () => this.updateActiveCard());
document.getElementById('card-template-preset').addEventListener('change', (e) => {
const templateName = e.target.value;
if (templateName) {
this.applyTemplatePreset(templateName);
}
});
document.getElementById('card-background-style').addEventListener('change', () => this.updateActiveCard());
document.getElementById('card-font-family').addEventListener('change', () => this.updateActiveCard());
document.getElementById('card-font-weight').addEventListener('change', () => this.updateActiveCard());
document.getElementById('card-accent-color').addEventListener('input', (e) => {
document.getElementById('card-accent-color-text').value = e.target.value;
this.updateActiveCard();
});
document.getElementById('card-title-color').addEventListener('input', (e) => {
document.getElementById('card-title-color-text').value = e.target.value;
this.updateActiveCard();
});
document.getElementById('card-footer-color').addEventListener('input', (e) => {
document.getElementById('card-footer-color-text').value = e.target.value;
this.updateActiveCard();
});
document.getElementById('card-full-bleed').addEventListener('change', () => this.updateActiveCard());
document.getElementById('card-show-accent').addEventListener('change', (e) => {
const accentGroup = document.getElementById('accent-color-group');
accentGroup.style.display = e.target.checked ? 'block' : 'none';
this.updateActiveCard();
});
document.getElementById('card-title-shadow').addEventListener('change', () => this.updateActiveCard());
document.getElementById('card-show-title').addEventListener('change', () => {
this.updateActiveCard();
this.updateSectionVisibility();
});
document.getElementById('card-show-footer').addEventListener('change', () => {
this.updateActiveCard();
this.updateSectionVisibility();
});
document.getElementById('card-title-font-size').addEventListener('input', () => this.updateActiveCard());
document.getElementById('card-title-x').addEventListener('input', () => this.updateActiveCard());
document.getElementById('card-title-y').addEventListener('input', () => this.updateActiveCard());
document.getElementById('card-title-draggable').addEventListener('change', () => this.updateActiveCard());
document.getElementById('card-footer-font-size').addEventListener('input', () => this.updateActiveCard());
document.getElementById('card-footer-x').addEventListener('input', () => this.updateActiveCard());
document.getElementById('card-footer-y').addEventListener('input', () => this.updateActiveCard());
document.getElementById('card-footer-draggable').addEventListener('change', () => this.updateActiveCard());
document.getElementById('card-image-fit').addEventListener('change', () => this.updateActiveCard());
document.getElementById('card-image-position').addEventListener('change', () => this.updateActiveCard());
document.getElementById('card-image-select').addEventListener('change', () => this.updateActiveCard());
// Panel settings
document.getElementById('card-panel-mode').addEventListener('change', () => {
this.updateActiveCard();
this.updateSectionVisibility();
});
document.getElementById('card-title-panel-border').addEventListener('input', () => this.updateActiveCard());
document.getElementById('card-title-panel-rotation').addEventListener('input', () => this.updateActiveCard());
document.getElementById('card-title-panel-expand').addEventListener('change', () => this.updateActiveCard());
document.getElementById('card-image-panel-border').addEventListener('input', () => this.updateActiveCard());
document.getElementById('card-image-panel-rotation').addEventListener('input', () => this.updateActiveCard());
document.getElementById('card-footer-panel-border').addEventListener('input', () => this.updateActiveCard());
document.getElementById('card-footer-panel-rotation').addEventListener('input', () => this.updateActiveCard());
// Title style selection auto-update
document.querySelectorAll('.title-style-option').forEach(option => {
option.addEventListener('click', () => {
document.querySelectorAll('.title-style-option').forEach(opt => opt.classList.remove('selected'));
option.classList.add('selected');
this.updateActiveCard();
});
});
// Global title font size
// document.getElementById('global-title-font-size').addEventListener('input', (e) => {
// this.globalTitleFontSize = parseInt(e.target.value);
// this.generatePreview();
// });
// Mobile toggles
document.getElementById('toggle-settings').addEventListener('click', () => {
const panel = document.querySelector('.settings-panel');
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
});
document.getElementById('toggle-editor').addEventListener('click', () => {
const panel = document.querySelector('.edit-panel');
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
});
// Section header click handlers for collapsing/expanding
document.querySelectorAll('.config-section h4').forEach(header => {
header.addEventListener('click', () => {
const section = header.parentElement;
section.classList.toggle('collapsed');
});
});
}
async handleImageUpload(files) {
const initialImageCount = this.images.length;
for (const file of files) {
if (file.type.startsWith('image/')) {
try {
const imageData = await this.readFileAsDataURL(file);
const coverImage = new CoverImage(file.name, imageData);
this.images.push(coverImage);
// Automatically create a card variation for this image
const variation = {
title: '',
footer: '',
imageIndex: this.images.length - 1, // Use the index of the newly added image
template: this.currentTemplate
};
this.cardVariations.push(variation);
} catch (error) {
this.showStatusMessage(`Error loading ${file.name}: ${error.message}`, 'error');
}
}
}
// If no images were uploaded before and we now have images, remove the default single card
if (initialImageCount === 0 && this.images.length > 0 && this.cardVariations.length === 1 &&
!this.cardVariations[0].title && !this.cardVariations[0].footer && this.cardVariations[0].imageIndex === -1) {
this.cardVariations = []; // Clear the default empty variation
}
this.updateImageList();
this.updateCardList();
this.generatePreview();
}
readFileAsDataURL(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(new Error('Failed to read file'));
reader.readAsDataURL(file);
});
}
updateImageList() {
const imageList = document.getElementById('image-list');
imageList.innerHTML = '';
this.images.forEach((image, index) => {
const item = document.createElement('div');
item.className = 'image-item';
item.innerHTML = `
<div class="image-info">
<div class="image-name">${image.name}</div>
<div class="image-meta">${image.width || 0} × ${image.height || 0}px</div>
</div>
<div class="image-actions">
<button class="btn btn-danger" onclick="app.removeImage(${index})">×</button>
</div>
`;
imageList.appendChild(item);
});
}
removeImage(index) {
this.images.splice(index, 1);
this.updateImageList();
this.updateCardList();
this.generatePreview();
}
clearAllImages() {
this.images = [];
this.updateImageList();
this.updateCardList();
this.generatePreview();
}
applyTemplatePreset(templateName) {
if (!this.templates[templateName]) return;
const preset = this.templates[templateName];
document.getElementById('card-background-style').value = preset.backgroundStyle;
document.getElementById('card-font-family').value = preset.fontFamily;
document.getElementById('card-font-weight').value = preset.fontWeight;
document.getElementById('card-footer-color').value = preset.footerColor;
document.getElementById('card-footer-color-text').value = preset.footerColor;
// Set additional properties if defined in preset
if (preset.fullBleed !== undefined) {
document.getElementById('card-full-bleed').checked = preset.fullBleed;
}
if (preset.showFooter !== undefined) {
document.getElementById('card-show-footer').checked = preset.showFooter;
}
if (preset.imagePosition !== undefined) {
document.getElementById('card-image-position').value = preset.imagePosition;
}
// Set panel properties if defined in preset
document.getElementById('card-panel-mode').checked = preset.panelMode === true;
if (preset.titlePanel !== undefined) {
document.getElementById('card-title-panel-border').value = preset.titlePanel.borderWidth;
document.getElementById('card-title-panel-rotation').value = preset.titlePanel.rotation;
document.getElementById('card-title-panel-expand').checked = preset.titlePanel.expand;
}
if (preset.imagePanel !== undefined) {
document.getElementById('card-image-panel-border').value = preset.imagePanel.borderWidth;
document.getElementById('card-image-panel-rotation').value = preset.imagePanel.rotation;
}
if (preset.footerPanel !== undefined) {
document.getElementById('card-footer-panel-border').value = preset.footerPanel.borderWidth;
document.getElementById('card-footer-panel-rotation').value = preset.footerPanel.rotation;
}
this.updateActiveCard();
}
addCardVariation() {
const variation = {
title: '',
footer: '',
imageIndex: -1,
backgroundStyle: 'solid',
fontFamily: 'Arial',
fontWeight: 'normal',
footerColor: '#111111',
titleStyle: 'classic',
accentColor: '#f1c40f',
showAccent: false,
titleColor: '#111111',
fullBleed: true,
titleShadow: false,
showTitle: true,
showFooter: true,
imageFit: 'cover',
imagePosition: 'center',
titleFontSize: 20,
footerFontSize: 12,
titleX: 50,
titleY: 20,
footerX: 50,
footerY: 90,
titleDraggable: false,
footerDraggable: false,
panelMode: false,
titlePanel: { borderWidth: 0, rotation: 0, expand: false },
imagePanel: { borderWidth: 0, rotation: 0 },
footerPanel: { borderWidth: 0, rotation: 0 }
};
this.cardVariations.push(variation);
this.updateCardList();
this.generatePreview();
}
updateCardVariation(index, field, value) {
this.cardVariations[index][field] = value;
this.generatePreview();
}
removeCardVariation(index) {
this.cardVariations.splice(index, 1);
this.updateCardVariations();
this.generatePreview();
}
updateCardVariations() {
// This method is now replaced by updateCardList
this.updateCardList();
}
updateCardList() {
const container = document.getElementById('card-list');
container.innerHTML = '';
if (this.cardVariations.length === 0) {
container.innerHTML = '<p style="color: #999999; text-align: center; padding: 20px;">No cards yet. Upload images or click "Add Card".</p>';
return;
}
this.cardVariations.forEach((variation, index) => {
const cardItem = document.createElement('div');
cardItem.className = `card-list-item ${index === this.activeCardIndex ? 'active' : ''}`;
cardItem.onclick = () => this.selectCard(index);
cardItem.innerHTML = `
<div style="display: flex; align-items: center; justify-content: space-between;">
<div>
<strong>Card ${index + 1}</strong>
<span style="color: #666666; margin-left: 10px;">
${variation.title || 'No title'}
${(() => {
for (const [presetName, preset] of Object.entries(this.templates)) {
if (variation.backgroundStyle === preset.backgroundStyle &&
variation.fontFamily === preset.fontFamily &&
variation.fontWeight === preset.fontWeight &&
variation.footerColor === preset.footerColor) {
return presetName;
}
}
return 'Custom';
})()} •
${variation.imageIndex >= 0 ? `Image ${variation.imageIndex + 1}` : 'Default image'}
</span>
</div>
</div>
`;
container.appendChild(cardItem);
});
this.updateActiveCardSelector();
}
updateActiveCardSelector() {
const selector = document.getElementById('active-card-select');
selector.innerHTML = '<option value="-1">No card selected</option>';
this.cardVariations.forEach((variation, index) => {
const option = document.createElement('option');
option.value = index;
option.textContent = `Card ${index + 1}: ${variation.title || 'Untitled'}`;
if (index === this.activeCardIndex) option.selected = true;
selector.appendChild(option);
});
}
selectCard(index) {
this.activeCardIndex = index;
this.updateCardList();
this.showCardEditor();
}
selectCardFromPreview(index) {
// Called from iframe when clicking on a card in the preview
this.selectCard(index);
}
showCardEditor() {
const editor = document.getElementById('card-editor');
if (this.activeCardIndex === -1) {
editor.style.display = 'none';
return;
}
const variation = this.cardVariations[this.activeCardIndex];
if (!variation) return;
// Populate card settings
document.getElementById('card-editor-title').textContent = `Edit Card ${this.activeCardIndex + 1}`;
// Set template preset (detect which preset matches current features)
let matchingPreset = '';
for (const [presetName, preset] of Object.entries(this.templates)) {
let matches = variation.backgroundStyle === preset.backgroundStyle &&
variation.fontFamily === preset.fontFamily &&
variation.fontWeight === preset.fontWeight &&
variation.footerColor === preset.footerColor;
// Check additional properties if defined in preset
if (preset.fullBleed !== undefined && variation.fullBleed !== preset.fullBleed) matches = false;
if (preset.showFooter !== undefined && variation.showFooter !== preset.showFooter) matches = false;
if (preset.imagePosition !== undefined && variation.imagePosition !== preset.imagePosition) matches = false;
if (preset.panelMode !== undefined && variation.panelMode !== preset.panelMode) matches = false;
if (preset.titlePanel !== undefined) {
if (variation.titlePanel?.borderWidth !== preset.titlePanel.borderWidth ||
variation.titlePanel?.rotation !== preset.titlePanel.rotation ||
variation.titlePanel?.expand !== preset.titlePanel.expand) matches = false;
}
if (preset.imagePanel !== undefined) {
if (variation.imagePanel?.borderWidth !== preset.imagePanel.borderWidth ||
variation.imagePanel?.rotation !== preset.imagePanel.rotation) matches = false;
}
if (preset.footerPanel !== undefined) {
if (variation.footerPanel?.borderWidth !== preset.footerPanel.borderWidth ||
variation.footerPanel?.rotation !== preset.footerPanel.rotation) matches = false;
}
if (matches) {
matchingPreset = presetName;
break;
}
}
document.getElementById('card-template-preset').value = matchingPreset;
// Set individual features
document.getElementById('card-background-style').value = variation.backgroundStyle || 'solid';
document.getElementById('card-font-family').value = variation.fontFamily || 'Arial';
document.getElementById('card-font-weight').value = variation.fontWeight || 'normal';
document.getElementById('card-footer-color').value = variation.footerColor || '#111111';
document.getElementById('card-footer-color-text').value = variation.footerColor || '#111111';
// Set title
document.getElementById('card-title-input').value = variation.title || '';
// Set footer
document.getElementById('card-footer-input').value = variation.footer || '';
// Set title style
document.querySelectorAll('.title-style-option').forEach(option => {
option.classList.toggle('selected', option.dataset.style === (variation.titleStyle || 'classic'));
});
// Set colors
document.getElementById('card-accent-color').value = variation.accentColor || '#f1c40f';
document.getElementById('card-accent-color-text').value = variation.accentColor || '#f1c40f';
document.getElementById('card-show-accent').checked = variation.showAccent === true;
const accentGroup = document.getElementById('accent-color-group');
accentGroup.style.display = variation.showAccent ? 'block' : 'none';
document.getElementById('card-title-color').value = variation.titleColor || '#111111';
document.getElementById('card-title-color-text').value = variation.titleColor || '#111111';
// Set checkboxes
document.getElementById('card-full-bleed').checked = variation.fullBleed !== false;
document.getElementById('card-title-shadow').checked = variation.titleShadow === true;
document.getElementById('card-show-title').checked = variation.showTitle !== false;
document.getElementById('card-show-footer').checked = variation.showFooter !== false;
// Set title options
document.getElementById('card-title-font-size').value = variation.titleFontSize || 20;
document.getElementById('card-title-x').value = variation.titleX !== undefined ? variation.titleX : 50;
document.getElementById('card-title-y').value = variation.titleY !== undefined ? variation.titleY : 20;
document.getElementById('card-title-draggable').checked = variation.titleDraggable === true;
// Set footer options
document.getElementById('card-footer-font-size').value = variation.footerFontSize || 12;
document.getElementById('card-footer-x').value = variation.footerX !== undefined ? variation.footerX : 50;
document.getElementById('card-footer-y').value = variation.footerY !== undefined ? variation.footerY : 90;
document.getElementById('card-footer-draggable').checked = variation.footerDraggable === true;
// Set image options
document.getElementById('card-image-fit').value = variation.imageFit || 'cover';
document.getElementById('card-image-position').value = variation.imagePosition || 'center';
// Set panel options
document.getElementById('card-panel-mode').checked = variation.panelMode === true;
document.getElementById('card-title-panel-border').value = variation.titlePanel?.borderWidth || 0;
document.getElementById('card-title-panel-rotation').value = variation.titlePanel?.rotation || 0;
document.getElementById('card-title-panel-expand').checked = variation.titlePanel?.expand === true;
document.getElementById('card-image-panel-border').value = variation.imagePanel?.borderWidth || 0;
document.getElementById('card-image-panel-rotation').value = variation.imagePanel?.rotation || 0;
document.getElementById('card-footer-panel-border').value = variation.footerPanel?.borderWidth || 0;
document.getElementById('card-footer-panel-rotation').value = variation.footerPanel?.rotation || 0;
// Populate image options
const imageSelect = document.getElementById('card-image-select');
imageSelect.innerHTML = '<option value="-1">Use default image</option>';
this.images.forEach((img, imgIndex) => {
const option = document.createElement('option');
option.value = imgIndex;
option.textContent = img.name;
if (variation.imageIndex === imgIndex) option.selected = true;
imageSelect.appendChild(option);
});
editor.style.display = 'block';
// Update section visibility based on show/hide toggles
this.updateSectionVisibility();
}
updateSectionVisibility() {
const showTitle = document.getElementById('card-show-title').checked;
const showFooter = document.getElementById('card-show-footer').checked;
const panelMode = document.getElementById('card-panel-mode').checked;
const titleSection = document.getElementById('title-section');
const footerSection = document.getElementById('footer-section');
const panelSection = document.getElementById('panel-section');
if (showTitle) {
titleSection.classList.remove('collapsed');
} else {
titleSection.classList.add('collapsed');
}
if (showFooter) {
footerSection.classList.remove('collapsed');
} else {
footerSection.classList.add('collapsed');
}
if (panelMode) {
panelSection.classList.remove('collapsed');
} else {
panelSection.classList.add('collapsed');
}
}
initDragFunctionality() {
console.log('initDragFunctionality called');
// This will be called when the preview iframe loads
const iframe = document.getElementById('preview-iframe');
if (!iframe) {
console.log('No iframe found');
return;
}
const initializeDrag = () => {
console.log('initializeDrag called');
try {
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
if (!iframeDoc) {
console.log('No iframe document');
return;
}
const titles = iframeDoc.querySelectorAll('.title, .comic-title-panel');
console.log('Found titles:', titles.length);
titles.forEach((title, index) => {
console.log(`Title ${index} cursor:`, title.style.cursor);
// Only make draggable if cursor is set to 'move' and not a comic panel
if (title.style.cursor !== 'move' || title.classList.contains('comic-title-panel')) {
console.log(`Title ${index} not draggable, skipping`);
return;
}
let isDragging = false;
let startX, startY, initialLeft, initialTop;
title.addEventListener('mousedown', (e) => {
console.log('Title mousedown fired');
if (title.style.cursor !== 'move') return;
isDragging = true;
// Get iframe position and convert mouse coordinates to iframe-relative
const iframeRect = iframe.getBoundingClientRect();
startX = e.clientX - iframeRect.left;
startY = e.clientY - iframeRect.top;
// Store initial position
initialLeft = parseFloat(title.style.left) || 50;
initialTop = parseFloat(title.style.top) || 20;
title.style.zIndex = '1000';
title.style.userSelect = 'none';
e.preventDefault();
});
const handleMouseMove = (e) => {
if (!isDragging) return;
// Get iframe position in main document
const iframeRect = iframe.getBoundingClientRect();
// Convert mouse coordinates to be relative to iframe viewport
const mouseX = e.clientX - iframeRect.left;
const mouseY = e.clientY - iframeRect.top;
const card = title.closest('.card');
const cardRect = card.getBoundingClientRect();
// Convert card position to be relative to iframe
const cardLeftRelative = cardRect.left - iframeRect.left;
const cardTopRelative = cardRect.top - iframeRect.top;
// Calculate mouse position relative to the card (within iframe)
const relativeMouseX = mouseX - cardLeftRelative;
const relativeMouseY = mouseY - cardTopRelative;
// Convert to percentage
const newLeft = Math.max(0, Math.min(100, (relativeMouseX / cardRect.width) * 100));
const newTop = Math.max(0, Math.min(100, (relativeMouseY / cardRect.height) * 100));
console.log('Drag debug:', { mouseX, mouseY, iframeRect, cardRect, cardLeftRelative, cardTopRelative, relativeMouseX, relativeMouseY, newLeft, newTop });
title.style.left = `${newLeft}%`;
title.style.top = `${newTop}%`;
console.log('Title style set to:', title.style.left, title.style.top);
// Update the form values if this card is currently selected
const cardIndex = parseInt(card.getAttribute('data-card-index'));
if (cardIndex === this.activeCardIndex && this.cardVariations[cardIndex]) {
document.getElementById('card-title-x').value = Math.round(newLeft);
document.getElementById('card-title-y').value = Math.round(newTop);
// Update the variation data
this.cardVariations[cardIndex].titleX = Math.round(newLeft);
this.cardVariations[cardIndex].titleY = Math.round(newTop);
}
};
const handleMouseUp = () => {
if (isDragging) {
isDragging = false;
title.style.zIndex = '';
title.style.userSelect = '';
}
};
// Add event listeners to both iframe document and main document
// to handle mouse events that might occur outside the iframe
iframeDoc.addEventListener('mousemove', handleMouseMove);
iframeDoc.addEventListener('mouseup', handleMouseUp);
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
});
// Initialize footer drag functionality
const footers = iframeDoc.querySelectorAll('.footer, .comic-footer-panel');
console.log('Found footers:', footers.length);
footers.forEach((footer, index) => {
console.log(`Footer ${index} cursor:`, footer.style.cursor);
// Only make draggable if cursor is set to 'move' and not a comic panel
if (footer.style.cursor !== 'move' || footer.classList.contains('comic-footer-panel')) {
console.log(`Footer ${index} not draggable, skipping`);
return;
}
let isDragging = false;
let startX, startY, initialLeft, initialBottom;
footer.addEventListener('mousedown', (e) => {
console.log('Footer mousedown fired');
if (footer.style.cursor !== 'move') return;
isDragging = true;
// Get iframe position and convert mouse coordinates to iframe-relative
const iframeRect = iframe.getBoundingClientRect();
startX = e.clientX - iframeRect.left;
startY = e.clientY - iframeRect.top;
// Store initial position
initialLeft = parseFloat(footer.style.left) || 50;
initialBottom = parseFloat(footer.style.bottom) || 10;
footer.style.zIndex = '1000';
footer.style.userSelect = 'none';
e.preventDefault();
});
const handleMouseMove = (e) => {
if (!isDragging) return;
// Get iframe position in main document
const iframeRect = iframe.getBoundingClientRect();
// Convert mouse coordinates to be relative to iframe viewport
const mouseX = e.clientX - iframeRect.left;
const mouseY = e.clientY - iframeRect.top;
const card = footer.closest('.card');
const cardRect = card.getBoundingClientRect();
// Calculate new position as percentage
const deltaX = mouseX - startX;
const deltaY = mouseY - startY;
const newLeftPercent = Math.max(0, Math.min(100, initialLeft + (deltaX / cardRect.width) * 100));
const newBottomPercent = Math.max(0, Math.min(100, initialBottom - (deltaY / cardRect.height) * 100));
footer.style.left = `${newLeftPercent}%`;
footer.style.bottom = `${newBottomPercent}%`;
// Update the card data
const cardIndex = Array.from(card.parentElement.children).indexOf(card);
if (this.cardVariations[cardIndex]) {
this.cardVariations[cardIndex].footerX = Math.round(newLeftPercent);
this.cardVariations[cardIndex].footerY = Math.round(100 - newBottomPercent);
this.updateUI();
}
};
const handleMouseUp = () => {
if (isDragging) {
isDragging = false;
footer.style.zIndex = '';
footer.style.userSelect = '';
}
};
// Add event listeners to both iframe document and main document
// to handle mouse events that might occur outside the iframe
iframeDoc.addEventListener('mousemove', handleMouseMove);
iframeDoc.addEventListener('mouseup', handleMouseUp);
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
});
} catch (e) {
// Ignore cross-origin errors
console.log('Drag functionality initialization failed:', e);
}
};
// Try to initialize immediately if the iframe is already loaded
if (iframe.contentDocument && (iframe.contentDocument.readyState === 'complete' || iframe.contentDocument.readyState === 'interactive')) {
console.log('Iframe already loaded, initializing immediately');
initializeDrag();
} else {
console.log('Setting iframe onload');
iframe.onload = () => {
console.log('iframe onload fired');
initializeDrag();
};
}
}
updateActiveCard() {
if (this.activeCardIndex === -1) return;
const variation = this.cardVariations[this.activeCardIndex];
variation.backgroundStyle = document.getElementById('card-background-style').value;
variation.fontFamily = document.getElementById('card-font-family').value;
variation.fontWeight = document.getElementById('card-font-weight').value;
variation.footerColor = document.getElementById('card-footer-color').value;
variation.title = document.getElementById('card-title-input').value;
variation.footer = document.getElementById('card-footer-input').value;
variation.titleStyle = document.querySelector('.title-style-option.selected')?.dataset.style || 'classic';
variation.accentColor = document.getElementById('card-accent-color').value;
variation.showAccent = document.getElementById('card-show-accent').checked;
variation.titleColor = document.getElementById('card-title-color').value;
variation.fullBleed = document.getElementById('card-full-bleed').checked;
variation.titleShadow = document.getElementById('card-title-shadow').checked;
variation.showTitle = document.getElementById('card-show-title').checked;
variation.showFooter = document.getElementById('card-show-footer').checked;
variation.titleFontSize = parseInt(document.getElementById('card-title-font-size').value);
variation.titleX = parseInt(document.getElementById('card-title-x').value);
variation.titleY = parseInt(document.getElementById('card-title-y').value);
variation.titleDraggable = document.getElementById('card-title-draggable').checked;
variation.footerFontSize = parseInt(document.getElementById('card-footer-font-size').value);
variation.footerX = parseInt(document.getElementById('card-footer-x').value);
variation.footerY = parseInt(document.getElementById('card-footer-y').value);
variation.footerDraggable = document.getElementById('card-footer-draggable').checked;
variation.imageFit = document.getElementById('card-image-fit').value;
variation.imagePosition = document.getElementById('card-image-position').value;
variation.imageIndex = parseInt(document.getElementById('card-image-select').value);
// Panel settings
variation.panelMode = document.getElementById('card-panel-mode').checked;
variation.titlePanel = {
borderWidth: parseInt(document.getElementById('card-title-panel-border').value),
rotation: parseInt(document.getElementById('card-title-panel-rotation').value),
expand: document.getElementById('card-title-panel-expand').checked
};
variation.imagePanel = {
borderWidth: parseInt(document.getElementById('card-image-panel-border').value),
rotation: parseInt(document.getElementById('card-image-panel-rotation').value)
};
variation.footerPanel = {
borderWidth: parseInt(document.getElementById('card-footer-panel-border').value),
rotation: parseInt(document.getElementById('card-footer-panel-rotation').value)
};
this.updateCardList();
this.generatePreview();
}
deleteActiveCard() {
if (this.activeCardIndex === -1) return;
if (confirm(`Delete Card ${this.activeCardIndex + 1}?`)) {
this.cardVariations.splice(this.activeCardIndex, 1);
this.activeCardIndex = -1;
this.updateCardList();
this.showCardEditor();
this.generatePreview();
}
}
async generatePreview() {
const placeholder = document.getElementById('preview-placeholder');
const iframe = document.getElementById('preview-iframe');
if (this.images.length === 0 && !this.templateSettings.title) {
placeholder.textContent = 'Add an image or enter a title to generate preview';
placeholder.style.display = 'flex';
iframe.style.display = 'none';
return;
}
placeholder.innerHTML = '<div class="loading"></div> Generating preview...';
placeholder.style.display = 'flex';
iframe.style.display = 'none';
try {
const html = await this.generateHTML();
const blob = new Blob([html], { type: 'text/html' });
const url = URL.createObjectURL(blob);
iframe.src = url;
iframe.style.display = 'block';
placeholder.style.display = 'none';
// Apply zoom after iframe loads
iframe.onload = () => {
this.initDragFunctionality();
};
// Clean up the previous blob URL
if (iframe.dataset.previousUrl) {
URL.revokeObjectURL(iframe.dataset.previousUrl);
}
iframe.dataset.previousUrl = url;
} catch (error) {
placeholder.textContent = `Error generating preview: ${error.message}`;
this.showStatusMessage(`Preview generation failed: ${error.message}`, 'error');
}
}
async generateHTML() {
// Determine number of cards to generate
let totalCards;
if (this.cardVariations.length > 0) {
totalCards = this.cardVariations.length;
} else if (this.images.length > 0) {
totalCards = this.images.length; // Create one card per image if no variations
} else {
totalCards = 1; // Single card if no images or variations
}
// For print sheet, arrange cards in grid, but don't exceed available space
const cardsPerPage = this.printLayout.cardsPerRow * this.printLayout.cardsPerColumn;
const pagesNeeded = Math.ceil(totalCards / cardsPerPage);
const totalCardsToGenerate = pagesNeeded * cardsPerPage; // Fill pages completely, but we'll hide empty slots
const cards = [];
// Generate cards
for (let i = 0; i < totalCardsToGenerate; i++) {
if (i < totalCards) {
let imageIndex, cardTitle, cardFooter, cardTitleStyle, cardAccentColor, cardTitleColor, cardFullBleed, cardTitleShadow;
let cardBackgroundStyle, cardFontFamily, cardFontWeight, cardFooterColor, cardShowTitle, cardShowFooter;
let cardImageFit, cardImagePosition, cardTitleFontSize, cardTitleX, cardTitleY, cardTitleDraggable;
let cardFooterFontSize, cardFooterX, cardFooterY, cardFooterDraggable;
let cardShowAccent;
let cardPanelMode, cardTitlePanel, cardImagePanel, cardFooterPanel;
if (this.cardVariations.length > 0) {
// Use variation settings
const variation = this.cardVariations[i];
imageIndex = variation && variation.imageIndex >= 0 ? variation.imageIndex : 0;
cardTitle = (variation && variation.title) || `Card ${i + 1}`;
cardFooter = (variation && variation.footer) || '';
cardTitleStyle = (variation && variation.titleStyle) || 'classic';
cardAccentColor = (variation && variation.accentColor) || '#f1c40f';
cardShowAccent = variation && variation.showAccent !== undefined ? variation.showAccent : false;
cardTitleColor = (variation && variation.titleColor) || '#111111';
cardFullBleed = variation && variation.fullBleed !== undefined ? variation.fullBleed : true;
cardTitleShadow = variation && variation.titleShadow !== undefined ? variation.titleShadow : false;
// Individual features
cardBackgroundStyle = (variation && variation.backgroundStyle) || 'solid';
cardFontFamily = (variation && variation.fontFamily) || 'Arial';
cardFontWeight = (variation && variation.fontWeight) || 'normal';
cardFooterColor = (variation && variation.footerColor) || '#111111';
cardShowTitle = variation && variation.showTitle !== undefined ? variation.showTitle : true;
cardShowFooter = variation && variation.showFooter !== undefined ? variation.showFooter : true;
cardImageFit = (variation && variation.imageFit) || 'cover';
cardImagePosition = (variation && variation.imagePosition) || 'center';
cardTitleFontSize = (variation && variation.titleFontSize) || 20;
cardTitleX = variation && variation.titleX !== undefined ? variation.titleX : 50;
cardTitleY = variation && variation.titleY !== undefined ? variation.titleY : 20;
cardTitleDraggable = variation && variation.titleDraggable !== undefined ? variation.titleDraggable : false;
cardFooterFontSize = (variation && variation.footerFontSize) || 12;
cardFooterX = variation && variation.footerX !== undefined ? variation.footerX : 50;
cardFooterY = variation && variation.footerY !== undefined ? variation.footerY : 90;
cardFooterDraggable = variation && variation.footerDraggable !== undefined ? variation.footerDraggable : false;
cardPanelMode = variation && variation.panelMode !== undefined ? variation.panelMode : false;
cardTitlePanel = variation && variation.titlePanel ? variation.titlePanel : { borderWidth: 0, rotation: 0, expand: false };
cardImagePanel = variation && variation.imagePanel ? variation.imagePanel : { borderWidth: 0, rotation: 0 };
cardFooterPanel = variation && variation.footerPanel ? variation.footerPanel : { borderWidth: 0, rotation: 0 };
} else {
// No variations - auto-assign images
imageIndex = i < this.images.length ? i : 0;
cardTitle = `Card ${i + 1}`;
cardFooter = '';
cardTitleStyle = 'classic';
cardAccentColor = '#f1c40f';
cardShowAccent = false;
cardTitleColor = '#111111';
cardFullBleed = true;
cardTitleShadow = false;
// Default features
cardBackgroundStyle = 'solid';
cardFontFamily = 'Arial';
cardFontWeight = 'normal';
cardFooterColor = '#111111';
cardShowTitle = true;
cardShowFooter = true;
cardImageFit = 'cover';
cardImagePosition = 'center';
cardTitleFontSize = 20;
cardTitleX = 50;
cardTitleY = 20;
cardTitleDraggable = false;
cardFooterFontSize = 12;
cardFooterX = 50;
cardFooterY = 90;
cardFooterDraggable = false;
cardPanelMode = false;
cardTitlePanel = { borderWidth: 0, rotation: 0, expand: false };
cardImagePanel = { borderWidth: 0, rotation: 0 };
cardFooterPanel = { borderWidth: 0, rotation: 0 };
}
const imageUrl = this.images.length > 0 ? this.images[imageIndex].dataUrl : 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
cards.push({
imageUrl,
title: cardTitle,
footer: cardFooter,
backgroundStyle: cardBackgroundStyle,
fontFamily: cardFontFamily,
fontWeight: cardFontWeight,
footerColor: cardFooterColor,
titleStyle: cardTitleStyle,
accentColor: cardAccentColor,
showAccent: cardShowAccent,
titleColor: cardTitleColor,
fullBleed: cardFullBleed,
titleShadow: cardTitleShadow,
showTitle: cardShowTitle,
showFooter: cardShowFooter,
imageFit: cardImageFit,
imagePosition: cardImagePosition,
titleFontSize: cardTitleFontSize,
titleX: cardTitleX,
titleY: cardTitleY,
titleDraggable: cardTitleDraggable,
footerFontSize: cardFooterFontSize,
footerX: cardFooterX,
footerY: cardFooterY,
footerDraggable: cardFooterDraggable,
panelMode: cardPanelMode,
titlePanel: cardTitlePanel,
imagePanel: cardImagePanel,
footerPanel: cardFooterPanel,
isVisible: true
});
} else {
// Empty card for filling the grid
cards.push({
imageUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
title: '',
footer: '',
backgroundStyle: 'solid',
fontFamily: 'Arial',
fontWeight: 'normal',
footerColor: '#111111',
titleStyle: 'folded',
accentColor: '#f1c40f',
showAccent: false,
titleColor: '#111111',
fullBleed: true,
titleShadow: false,
showTitle: false,
showFooter: false,
imageFit: 'cover',
imagePosition: 'center',
titleFontSize: 20,
titleX: 50,
titleY: 10,
titleDraggable: false,
panelMode: false,
titlePanel: { borderWidth: 0, rotation: 0, expand: false },
imagePanel: { borderWidth: 0, rotation: 0 },
footerPanel: { borderWidth: 0, rotation: 0 },
isVisible: false
});
}
}
// Fixed card dimensions: 54mm x 85.6mm
const cardWidthMm = 54;
const cardHeightMm = 85.6;
// Calculate grid layout
const pageWidthMm = 210; // A4 width
const pageHeightMm = 297; // A4 height
// For multiple pages, we'll create multiple print-sheet divs
let htmlContent = '';
for (let page = 0; page < pagesNeeded; page++) {
const startIndex = page * cardsPerPage;
const endIndex = Math.min(startIndex + cardsPerPage, totalCardsToGenerate);
const pageCards = cards.slice(startIndex, endIndex);
// Calculate positions for this page's grid
const gridCols = this.printLayout.cardsPerRow;
const gridRows = this.printLayout.cardsPerColumn;
htmlContent += `
<div class="print-page" style="page-break-after: ${page < pagesNeeded - 1 ? 'always' : 'auto'}; width: ${pageWidthMm}mm; padding: ${this.printLayout.pageMargin}mm; box-sizing: border-box;">
<div class="print-sheet" style="
display: grid;
grid-template-columns: repeat(${gridCols}, ${cardWidthMm}mm);
grid-template-rows: repeat(${gridRows}, ${cardHeightMm}mm);
gap: ${this.printLayout.cardSpacing}mm;
width: ${gridCols * cardWidthMm + (gridCols - 1) * this.printLayout.cardSpacing}mm;
height: ${gridRows * cardHeightMm + (gridRows - 1) * this.printLayout.cardSpacing}mm;
margin: 0;
padding: 0;
box-sizing: border-box;
">
${pageCards.map((card, index) => `
<div class="card card-${startIndex + index} ${card.isVisible ? '' : 'empty-card'}"
data-card-index="${startIndex + index}"
data-background-style="${card.backgroundStyle}"
onclick="if (window.parent && window.parent.selectCardFromPreview) { window.parent.selectCardFromPreview(${startIndex + index}); }"
style="cursor: pointer;
width: ${cardWidthMm}mm;
height: ${cardHeightMm}mm;
box-sizing: border-box;
font-family: 'DejaVuSans', serif;
${card.isVisible && card.backgroundStyle === 'gradient' ? 'background: linear-gradient(135deg, #e8f4fd 0%, #d1e8f0 100%);' : ''}
${card.isVisible && card.backgroundStyle === 'pattern' ? 'background: linear-gradient(45deg, #f5f5dc 0%, #deb887 100%);' : ''}
${card.isVisible && card.backgroundStyle === 'comic' ? 'background: #f9f9f9; border: 1px solid #ccc;' : ''}
${card.isVisible && card.backgroundStyle === 'polaroid' ? 'border: 8px solid #fff; border-bottom: 24px solid #fff; box-shadow: 0 4px 12px rgba(0,0,0,0.3); position: relative;' : ''}
${card.isVisible && card.backgroundStyle === 'polaroid' && !card.fullBleed ? 'background: #f8f9fa;' : ''}
${card.isVisible && card.backgroundStyle === 'solid' ? 'background: #f8f9fa;' : ''}
${card.isVisible && card.fullBleed ? `background: url('${card.imageUrl}') no-repeat ${card.imagePosition}/${card.imageFit};` : ''}
position: relative;
border: ${card.isVisible ? 'none' : '1px dashed #ccc'};
overflow: hidden;
">
${card.isVisible ? `
${card.panelMode ? `
<!-- Comic Style Panels -->
<div class="comic-panels">
${card.showTitle ? (card.titleStyle === 'split' ? `
<style>
@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap');
.comic-panel.comic-title-panel.split {
--s: 4px;
--o: calc(0.5 * var(--s));
--sl: calc(50% - 0.5px), #0000 calc(50% + 0.5px);
position: absolute;
left: 8%;
top: 6%;
${card.titlePanel.expand ? 'width: auto; min-width: 30%; max-width: 60%;' : 'width: 40%;'}
height: ${card.titlePanel.expand ? 'auto' : '15%'};
border: ${card.titlePanel.borderWidth}px solid #000;
place-self: center;
place-content: center;
padding: calc(0.1em) 0.05em 0 0;
background:
linear-gradient(22.5deg, #fff var(--sl)) text,
linear-gradient(22.5deg, #000 var(--sl)),
linear-gradient(${card.titleColor} 50%, #0000 0)
0 0/ 1% var(--s);
color: #0000;
-webkit-text-stroke: #fff calc(var(--o));
text-transform: uppercase;
mix-blend-mode: screen;
font: clamp(1em, min(65vh, 35vw), 2em)/ 0.75 'Bebas Neue', sans-serif;
transform: rotate(${card.titlePanel.rotation}deg);
box-shadow: 2px 2px 4px rgba(0,0,0,0.2);
@supports (line-height: 1cap) { line-height: 1cap; }
}
@media (min-width: 480px) and (min-height: 320px) {
.comic-panel.comic-title-panel.split { --s: 6px; }
}
</style>
<div class="comic-panel comic-title-panel split">${this.escapeHtml(card.title)}</div>
` : `<div class="comic-panel comic-title-panel" style="
position: absolute;
left: 8%;
top: 6%;
${card.titlePanel.expand ? 'width: auto; min-width: 30%; max-width: 60%;' : 'width: 40%;'}
height: ${card.titlePanel.expand ? 'auto' : '15%'};
border: ${card.titlePanel.borderWidth}px solid #000;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
white-space: pre-line;
text-align: center;
font-size: ${card.titleFontSize * 0.8}px;
font-weight: ${card.fontWeight === 'bold' ? '700' : '400'};
font-family: '${card.fontFamily}', serif;
color: ${card.titleColor};
${card.titleShadow ? 'text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);' : ''}
transform: rotate(${card.titlePanel.rotation}deg);
box-shadow: 2px 2px 4px rgba(0,0,0,0.2);
${card.titleDraggable && !card.panelMode ? 'cursor: move; user-select: none;' : ''}
" data-panel-type="title">${this.escapeHtml(card.title)}</div>`) : ''}
<div class="comic-panel comic-image-panel" style="
position: absolute;
left: 15%;
top: 25%;
right: 15%;
bottom: ${card.showFooter ? '35%' : '15%'};
border: ${card.imagePanel.borderWidth}px solid #000;
background: ${card.fullBleed ? `url('${card.imageUrl}') no-repeat ${card.imagePosition}/${card.imageFit}` : '#f8f9fa'};
transform: rotate(${card.imagePanel.rotation}deg);
box-shadow: 3px 3px 6px rgba(0,0,0,0.3);
overflow: hidden;
">
${!card.fullBleed ? `<img src="${card.imageUrl}" alt="cover" style="
width: 100%;
height: 100%;
object-fit: ${card.imageFit};
object-position: ${card.imagePosition};
"/>` : ''}
${card.showAccent ? `<div class="overlay" style="
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: ${card.accentColor}40;
pointer-events: none;
"></div>` : ''}
</div>
${card.showFooter ? `<div class="comic-panel comic-footer-panel" style="
position: absolute;
right: 8%;
bottom: 6%;
width: 35%;
height: 12%;
border: ${card.footerPanel.borderWidth}px solid #000;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: ${card.fontWeight};
font-family: '${card.fontFamily}', serif;
color: ${card.footerColor};
transform: rotate(${card.footerPanel.rotation}deg);
box-shadow: 2px 2px 4px rgba(0,0,0,0.2);
text-align: center;
padding: 2px;
white-space: pre-line;
">${this.escapeHtml(card.footer)}</div>` : ''}
</div>
` : `
<!-- Standard Layout -->
${card.showTitle ? (card.titleStyle === 'split' ? `
<style>
@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap');
.title.split {
--s: 4px;
--o: calc(0.5 * var(--s));
--sl: calc(50% - 0.5px), #0000 calc(50% + 0.5px);
position: absolute;
left: ${card.titleX}%;
top: ${card.titleY}%;
transform: translate(-50%, -50%);
place-self: center;
place-content: center;
padding: calc(0.1em) 0.05em 0 0;
height: calc(1lh - var(--o));
background:
linear-gradient(22.5deg, #fff var(--sl)) text,
linear-gradient(22.5deg, #000 var(--sl)),
linear-gradient(${card.titleColor} 50%, #0000 0)
0 0/ 1% var(--s);
color: #0000;
-webkit-text-stroke: #fff calc(var(--o));
text-transform: uppercase;
mix-blend-mode: screen;
font: clamp(2em, min(65vh, 35vw), 3.9em)/ 0.75 'Bebas Neue', sans-serif;
${card.titleDraggable ? 'cursor: move; user-select: none;' : ''}
@supports (line-height: 1cap) { line-height: 1cap; }
}
@media (min-width: 480px) and (min-height: 320px) {
.title.split { --s: 6px; }
}
</style>
<div class="title split">${this.escapeHtml(card.title)}</div>
` : `<div class="title ${card.titleStyle}" data-heading="${this.escapeHtml(card.title)}" style="
color: ${card.titleColor};
font-weight: ${card.fontWeight === 'bold' ? '700' : '400'};
font-family: '${card.fontFamily}', serif;
font-size: ${card.titleFontSize}px;
${card.titleShadow ? 'text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);' : ''}
position: absolute;
left: ${card.titleX}%;
top: ${card.titleY}%;
transform: translate(-50%, -50%);
white-space: pre-line;
text-align: center;
${card.titleDraggable ? 'cursor: move; user-select: none;' : ''}
">${this.escapeHtml(card.title)}</div>`) : ''}
${!card.fullBleed ? `<div class="hero"><img src="${card.imageUrl}" alt="cover" style="object-fit: ${card.imageFit}; object-position: ${card.imagePosition};"/></div>` : ''}
${card.showAccent ? `<div class="overlay" style="background: ${card.accentColor}40;"></div>` : `<div class="overlay" style="background: rgba(0,0,0,0);"></div>`}
${card.showFooter ? `<div class="footer" style="
color: ${card.footerColor};
font-family: '${card.fontFamily}', serif;
font-weight: ${card.fontWeight};
font-size: ${card.footerFontSize}px;
position: absolute;
left: ${card.footerX}%;
bottom: ${100 - card.footerY}%;
transform: translate(-50%, 50%);
white-space: pre-line;
text-align: center;
${card.footerDraggable ? 'cursor: move; user-select: none;' : ''}
">${this.escapeHtml(card.footer)}</div>` : ''}
`}
` : ''}
</div>
`).join('')}
</div>
</div>`;
}
// Generate HTML for print sheet
const html = `
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Yoto Covers Print Sheet</title>
<style>
@font-face { font-family: 'DejaVuSans'; src: local('DejaVu Sans'); }
@media screen {
html, body { margin: 0; padding: 20px; background: #f5f5f5; }
.print-page { border: 2px dashed #ccc; margin: 20px auto; background: white; page-break-after: always; width: 210mm; max-width: 100%; }
.print-page:last-child { page-break-after: auto; }
.print-sheet { max-width: 100%; height: auto !important; }
}
@media print {
html, body { margin: 0; padding: 0; background: white; }
.print-page { border: none; margin: 0; page-break-after: always; }
.print-page:last-child { page-break-after: auto; }
@page { margin: 0; size: A4; }
}
.card {
box-sizing: border-box;
position: relative;
}
.empty-card {
background: #f9f9f9;
}
${cards.map((card, index) => `
.card-${index} .title {
color: ${card.titleColor};
font-family: '${card.fontFamily}', serif;
font-weight: ${card.fontWeight === 'bold' ? '700' : '400'};
}
.card-${index} .footer {
color: ${card.footerColor};
}
`).join('')}
.title {
position: absolute;
top: 6%;
left: 6%;
right: 6%;
text-align: center;
font-size: calc(108px + 0.5rem);
color: ${this.templateSettings.titleColor};
font-weight: 900;
text-transform: uppercase;
z-index: 10;
${this.templateSettings.titleShadow ? 'text-shadow: 1px 4px 6px rgba(0,0,0,0.3);' : ''}
}
.title.classic {
/* Default classic style - already defined above */
}
.title.condensed {
font-size: calc(96px + 0.5rem);
font-weight: 900;
letter-spacing: -2px;
font-stretch: condensed;
}
.title.outlined {
-webkit-text-stroke: 2px white;
text-stroke: 2px white;
color: transparent;
}
.title.folded {
position: absolute;
left: 6%;
right: 6%;
top: 6%;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Source Code Pro', monospace;
font-weight: 900;
white-space: nowrap;
color: lch(76 39.21 9.23 / 0.5);
text-transform: uppercase;
transform: skew(10deg) rotate(-10deg);
text-shadow: 1px 4px 6px lch(90 2.22 62.5), 0 0 0 lch(28 26.21 12.27), 1px 4px 6px lch(90 2.22 62.5);
z-index: 10;
}
.title.folded::before {
content: attr(data-heading);
position: absolute;
left: 0;
top: -4.8%;
overflow: hidden;
height: 50%;
color: lch(97 2.19 62.49);
transform: translate(0.15em, 0) skew(-13deg) scale(1, 1.2);
text-shadow: 2px -1px 6px rgba(0,0,0,0.2);
}
.title.folded::after {
content: attr(data-heading);
position: absolute;
left: 0;
color: lch(83 2.26 62.51);
transform: translate(0, 0) skew(13deg) scale(1, 0.8);
clip-path: polygon(0 50%, 100% 50%, 100% 100%, 0 100%);
text-shadow: 2px -1px 6px lch(0 0 0 / 0.3);
}
.hero {
position: absolute;
top: 18%;
left: 6%;
right: 6%;
bottom: 18%;
display: flex;
align-items: center;
justify-content: center;
}
${cards.some(card => card.backgroundStyle === 'polaroid') ? `
.card[data-background-style="polaroid"] .hero {
top: 12%;
left: 8%;
right: 8%;
bottom: 25%;
}
` : ''}
.hero img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 8px;
}
.overlay {
position: absolute;
top: 18%;
left: 6%;
right: 6%;
bottom: 18%;
border-radius: 8px;
pointer-events: none;
background: rgba(0,0,0,0);
}
${cards.some(card => card.backgroundStyle === 'polaroid') ? `
.card[data-background-style="polaroid"] .overlay {
top: 12%;
left: 8%;
right: 8%;
bottom: 25%;
}
` : ''}
.footer {
position: absolute;
bottom: 4%;
left: 6%;
right: 6%;
height: 10%;
background: ${this.templateSettings.accentColor};
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
color: #000;
border-radius: 6px;
}
</style>
</head>
<body>
${htmlContent}
</body>
</html>`;
return html;
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML.replace(/\n/g, '<br>');
}
openPreviewInNewTab() {
const iframe = document.getElementById('preview-iframe');
if (iframe.src) {
window.open(iframe.src, '_blank');
} else {
this.showStatusMessage('No preview available. Generate a preview first.', 'error');
}
}
printPreview() {
const iframe = document.getElementById('preview-iframe');
if (iframe.contentWindow) {
iframe.contentWindow.print();
} else {
this.showStatusMessage('No preview available to print.', 'error');
}
}
downloadHTML() {
if (!this.templateSettings.title && this.images.length === 0) {
this.showStatusMessage('Add content before downloading.', 'error');
return;
}
this.generateHTML().then(html => {
const blob = new Blob([html], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `yoto-cover-${Date.now()}.html`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
this.showStatusMessage('HTML file downloaded successfully!', 'success');
}).catch(error => {
this.showStatusMessage(`Download failed: ${error.message}`, 'error');
});
}
showStatusMessage(message, type = 'info') {
const statusDiv = document.getElementById('status-message');
statusDiv.textContent = message;
statusDiv.className = `status-message status-${type}`;
statusDiv.style.display = 'block';
setTimeout(() => {
statusDiv.style.display = 'none';
}, 5000);
}
updateUI() {
// Initialize print layout controls
document.getElementById('cards-per-row').value = this.printLayout.cardsPerRow;
document.getElementById('cards-per-column').value = this.printLayout.cardsPerColumn;
document.getElementById('page-margin').value = this.printLayout.pageMargin;
document.getElementById('card-spacing').value = this.printLayout.cardSpacing;
}
}
class CoverImage {
constructor(name, dataUrl) {
this.name = name;
this.dataUrl = dataUrl;
this.width = 0;
this.height = 0;
// Get image dimensions
this.getImageDimensions();
}
async getImageDimensions() {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
this.width = img.naturalWidth;
this.height = img.naturalHeight;
resolve();
};
img.src = this.dataUrl;
});
}
}
// Initialize the app when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
window.app = new YotoCoversApp();
window.selectCardFromPreview = (index) => window.app.selectCardFromPreview(index);
});