feat: add panel settings and footer customization options in the editor

This commit is contained in:
Ross
2026-01-04 22:17:51 +00:00
parent 279ed2ff6f
commit 554075a3a9
2 changed files with 412 additions and 22 deletions
+321 -20
View File
@@ -36,7 +36,11 @@ class YotoCoversApp {
fontFamily: 'Comic Sans MS',
fontWeight: 'bold',
footerColor: '#ff6b6b',
fullBleed: false
fullBleed: false,
panelMode: true,
titlePanel: { borderWidth: 2, rotation: -2, expand: true },
imagePanel: { borderWidth: 3, rotation: 1 },
footerPanel: { borderWidth: 2, rotation: 3 }
},
polaroid: {
backgroundStyle: 'polaroid',
@@ -235,10 +239,27 @@ class YotoCoversApp {
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', () => {
@@ -374,6 +395,22 @@ class YotoCoversApp {
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();
}
@@ -395,7 +432,19 @@ class YotoCoversApp {
showTitle: true,
showFooter: true,
imageFit: 'cover',
imagePosition: 'center'
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();
@@ -508,6 +557,20 @@ class YotoCoversApp {
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;
@@ -552,13 +615,29 @@ class YotoCoversApp {
// 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 : 10;
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>';
@@ -579,9 +658,11 @@ class YotoCoversApp {
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');
@@ -594,6 +675,12 @@ class YotoCoversApp {
} else {
footerSection.classList.add('collapsed');
}
if (panelMode) {
panelSection.classList.remove('collapsed');
} else {
panelSection.classList.add('collapsed');
}
}
initDragFunctionality() {
@@ -706,6 +793,89 @@ class YotoCoversApp {
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);
@@ -747,10 +917,30 @@ class YotoCoversApp {
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();
}
@@ -832,7 +1022,9 @@ class YotoCoversApp {
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
@@ -857,8 +1049,16 @@ class YotoCoversApp {
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 : 10;
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;
@@ -881,8 +1081,16 @@ class YotoCoversApp {
cardImagePosition = 'center';
cardTitleFontSize = 20;
cardTitleX = 50;
cardTitleY = 10;
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==';
@@ -909,6 +1117,14 @@ class YotoCoversApp {
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 {
@@ -935,6 +1151,10 @@ class YotoCoversApp {
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
});
}
@@ -995,29 +1215,66 @@ class YotoCoversApp {
overflow: hidden;
">
${card.isVisible ? `
${card.backgroundStyle === 'comic' ? `
${card.panelMode ? `
<!-- Comic Style Panels -->
<div class="comic-panels">
${card.showTitle ? `<div class="comic-panel comic-title-panel" style="
${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%;
width: 40%;
height: 15%;
border: 2px solid #000;
${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(-2deg);
transform: rotate(${card.titlePanel.rotation}deg);
box-shadow: 2px 2px 4px rgba(0,0,0,0.2);
${card.titleDraggable && card.backgroundStyle !== 'comic' ? 'cursor: move; user-select: none;' : ''}
" data-panel-type="title">${this.escapeHtml(card.title)}</div>` : ''}
${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;
@@ -1025,9 +1282,9 @@ class YotoCoversApp {
top: 25%;
right: 15%;
bottom: ${card.showFooter ? '35%' : '15%'};
border: 3px solid #000;
border: ${card.imagePanel.borderWidth}px solid #000;
background: ${card.fullBleed ? `url('${card.imageUrl}') no-repeat ${card.imagePosition}/${card.imageFit}` : '#f8f9fa'};
transform: rotate(1deg);
transform: rotate(${card.imagePanel.rotation}deg);
box-shadow: 3px 3px 6px rgba(0,0,0,0.3);
overflow: hidden;
">
@@ -1054,7 +1311,7 @@ class YotoCoversApp {
bottom: 6%;
width: 35%;
height: 12%;
border: 2px solid #000;
border: ${card.footerPanel.borderWidth}px solid #000;
background: #fff;
display: flex;
align-items: center;
@@ -1063,15 +1320,49 @@ class YotoCoversApp {
font-weight: ${card.fontWeight};
font-family: '${card.fontFamily}', serif;
color: ${card.footerColor};
transform: rotate(3deg);
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 ? `<div class="title ${card.titleStyle}" data-heading="${this.escapeHtml(card.title)}" style="
${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;
@@ -1081,14 +1372,24 @@ class YotoCoversApp {
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>` : ''}
">${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>` : ''}
`}
` : ''}
@@ -1269,7 +1570,7 @@ class YotoCoversApp {
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
return div.innerHTML.replace(/\n/g, '<br>');
}
openPreviewInNewTab() {