feat: add zoom and fullscreen controls to preview, enhance card fit options

This commit is contained in:
Ross
2026-01-11 21:34:32 +00:00
parent 554075a3a9
commit 6e32ffe450
2 changed files with 306 additions and 20 deletions
+159 -20
View File
@@ -57,7 +57,8 @@ class YotoCoversApp {
cardsPerRow: 2, cardsPerRow: 2,
cardsPerColumn: 5, cardsPerColumn: 5,
pageMargin: 10, pageMargin: 10,
cardSpacing: 5 cardSpacing: 5,
cardFit: 0
}; };
this.templateSettings = { this.templateSettings = {
title: '', title: '',
@@ -70,6 +71,15 @@ class YotoCoversApp {
footerFontSize: null footerFontSize: null
}; };
this.previewZoom = {
level: 1.0,
min: 0.25,
max: 3.0,
step: 0.25
};
this.isFullscreen = false;
this.cardVariations = []; this.cardVariations = [];
this.activeCardIndex = -1; // Currently selected card for editing this.activeCardIndex = -1; // Currently selected card for editing
this.init(); this.init();
@@ -103,6 +113,11 @@ class YotoCoversApp {
this.generatePreview(); this.generatePreview();
}); });
document.getElementById('global-card-fit').addEventListener('change', (e) => {
this.printLayout.cardFit = parseFloat(e.target.value);
this.generatePreview();
});
// Template settings - REMOVED: Now per-card only // Template settings - REMOVED: Now per-card only
// document.getElementById('template-select').addEventListener('change', (e) => { // document.getElementById('template-select').addEventListener('change', (e) => {
// this.currentTemplate = e.target.value; // this.currentTemplate = e.target.value;
@@ -181,6 +196,36 @@ class YotoCoversApp {
document.getElementById('open-preview').addEventListener('click', () => this.openPreviewInNewTab()); document.getElementById('open-preview').addEventListener('click', () => this.openPreviewInNewTab());
document.getElementById('print-preview').addEventListener('click', () => this.printPreview()); document.getElementById('print-preview').addEventListener('click', () => this.printPreview());
// Preview zoom and fullscreen
document.getElementById('zoom-in').addEventListener('click', () => this.zoomIn());
document.getElementById('zoom-out').addEventListener('click', () => this.zoomOut());
document.getElementById('zoom-reset').addEventListener('click', () => this.resetZoom());
document.getElementById('fullscreen-btn').addEventListener('click', () => this.toggleFullscreen());
// ESC key to exit fullscreen
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.isFullscreen) {
this.toggleFullscreen();
}
});
// Click on preview container close button area to exit fullscreen
document.querySelector('.preview-container').addEventListener('click', (e) => {
if (this.isFullscreen) {
const rect = e.currentTarget.getBoundingClientRect();
const closeButtonArea = {
left: rect.right - 200,
right: rect.right - 20,
top: rect.top + 20,
bottom: rect.top + 60
};
if (e.clientX >= closeButtonArea.left && e.clientX <= closeButtonArea.right &&
e.clientY >= closeButtonArea.top && e.clientY <= closeButtonArea.bottom) {
this.toggleFullscreen();
}
}
});
// Card management // Card management
document.getElementById('add-card').addEventListener('click', () => this.addCardVariation()); document.getElementById('add-card').addEventListener('click', () => this.addCardVariation());
document.getElementById('active-card-select').addEventListener('change', (e) => { document.getElementById('active-card-select').addEventListener('change', (e) => {
@@ -637,6 +682,9 @@ class YotoCoversApp {
document.getElementById('card-image-panel-rotation').value = variation.imagePanel?.rotation || 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-border').value = variation.footerPanel?.borderWidth || 0;
document.getElementById('card-footer-panel-rotation').value = variation.footerPanel?.rotation || 0; document.getElementById('card-footer-panel-rotation').value = variation.footerPanel?.rotation || 0;
// Set card fit
document.getElementById('card-fit').value = variation.cardFit !== undefined ? variation.cardFit : '';
// Populate image options // Populate image options
const imageSelect = document.getElementById('card-image-select'); const imageSelect = document.getElementById('card-image-select');
@@ -940,6 +988,14 @@ class YotoCoversApp {
borderWidth: parseInt(document.getElementById('card-footer-panel-border').value), borderWidth: parseInt(document.getElementById('card-footer-panel-border').value),
rotation: parseInt(document.getElementById('card-footer-panel-rotation').value) rotation: parseInt(document.getElementById('card-footer-panel-rotation').value)
}; };
// Update card fit
const cardFitValue = document.getElementById('card-fit').value;
if (cardFitValue === '') {
delete variation.cardFit;
} else {
variation.cardFit = parseFloat(cardFitValue);
}
this.updateCardList(); this.updateCardList();
this.generatePreview(); this.generatePreview();
@@ -984,6 +1040,7 @@ class YotoCoversApp {
// Apply zoom after iframe loads // Apply zoom after iframe loads
iframe.onload = () => { iframe.onload = () => {
this.initDragFunctionality(); this.initDragFunctionality();
this.updateZoom();
}; };
// Clean up the previous blob URL // Clean up the previous blob URL
@@ -1025,6 +1082,7 @@ class YotoCoversApp {
let cardFooterFontSize, cardFooterX, cardFooterY, cardFooterDraggable; let cardFooterFontSize, cardFooterX, cardFooterY, cardFooterDraggable;
let cardShowAccent; let cardShowAccent;
let cardPanelMode, cardTitlePanel, cardImagePanel, cardFooterPanel; let cardPanelMode, cardTitlePanel, cardImagePanel, cardFooterPanel;
let cardFit;
if (this.cardVariations.length > 0) { if (this.cardVariations.length > 0) {
// Use variation settings // Use variation settings
@@ -1059,6 +1117,7 @@ class YotoCoversApp {
cardTitlePanel = variation && variation.titlePanel ? variation.titlePanel : { borderWidth: 0, rotation: 0, expand: false }; cardTitlePanel = variation && variation.titlePanel ? variation.titlePanel : { borderWidth: 0, rotation: 0, expand: false };
cardImagePanel = variation && variation.imagePanel ? variation.imagePanel : { borderWidth: 0, rotation: 0 }; cardImagePanel = variation && variation.imagePanel ? variation.imagePanel : { borderWidth: 0, rotation: 0 };
cardFooterPanel = variation && variation.footerPanel ? variation.footerPanel : { borderWidth: 0, rotation: 0 }; cardFooterPanel = variation && variation.footerPanel ? variation.footerPanel : { borderWidth: 0, rotation: 0 };
cardFit = variation && variation.cardFit !== undefined ? variation.cardFit : this.printLayout.cardFit;
} else { } else {
// No variations - auto-assign images // No variations - auto-assign images
imageIndex = i < this.images.length ? i : 0; imageIndex = i < this.images.length ? i : 0;
@@ -1091,6 +1150,7 @@ class YotoCoversApp {
cardTitlePanel = { borderWidth: 0, rotation: 0, expand: false }; cardTitlePanel = { borderWidth: 0, rotation: 0, expand: false };
cardImagePanel = { borderWidth: 0, rotation: 0 }; cardImagePanel = { borderWidth: 0, rotation: 0 };
cardFooterPanel = { borderWidth: 0, rotation: 0 }; cardFooterPanel = { borderWidth: 0, rotation: 0 };
cardFit = this.printLayout.cardFit;
} }
const imageUrl = this.images.length > 0 ? this.images[imageIndex].dataUrl : 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; const imageUrl = this.images.length > 0 ? this.images[imageIndex].dataUrl : 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
@@ -1125,6 +1185,7 @@ class YotoCoversApp {
titlePanel: cardTitlePanel, titlePanel: cardTitlePanel,
imagePanel: cardImagePanel, imagePanel: cardImagePanel,
footerPanel: cardFooterPanel, footerPanel: cardFooterPanel,
cardFit: cardFit,
isVisible: true isVisible: true
}); });
} else { } else {
@@ -1155,14 +1216,17 @@ class YotoCoversApp {
titlePanel: { borderWidth: 0, rotation: 0, expand: false }, titlePanel: { borderWidth: 0, rotation: 0, expand: false },
imagePanel: { borderWidth: 0, rotation: 0 }, imagePanel: { borderWidth: 0, rotation: 0 },
footerPanel: { borderWidth: 0, rotation: 0 }, footerPanel: { borderWidth: 0, rotation: 0 },
cardFit: this.printLayout.cardFit,
isVisible: false isVisible: false
}); });
} }
} }
// Fixed card dimensions: 54mm x 85.6mm // Fixed card dimensions: 54mm x 85.6mm
const cardWidthMm = 54; const baseCardWidthMm = 54;
const cardHeightMm = 85.6; const baseCardHeightMm = 85.6;
const cardWidthMm = baseCardWidthMm;
const cardHeightMm = baseCardHeightMm;
// Calculate grid layout // Calculate grid layout
const pageWidthMm = 210; // A4 width const pageWidthMm = 210; // A4 width
@@ -1180,23 +1244,33 @@ class YotoCoversApp {
const gridCols = this.printLayout.cardsPerRow; const gridCols = this.printLayout.cardsPerRow;
const gridRows = this.printLayout.cardsPerColumn; const gridRows = this.printLayout.cardsPerColumn;
htmlContent += ` const pageBreak = page < pagesNeeded - 1 ? "always" : "auto";
<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;"> const gridColsValue = gridCols;
<div class="print-sheet" style=" const cardWidthValue = cardWidthMm;
display: grid; const cardHeightValue = cardHeightMm;
grid-template-columns: repeat(${gridCols}, ${cardWidthMm}mm); const spacingValue = this.printLayout.cardSpacing;
grid-template-rows: repeat(${gridRows}, ${cardHeightMm}mm); const widthValue = gridCols * cardWidthMm + (gridCols - 1) * this.printLayout.cardSpacing;
gap: ${this.printLayout.cardSpacing}mm; const heightValue = gridRows * cardHeightMm + (gridRows - 1) * this.printLayout.cardSpacing;
width: ${gridCols * cardWidthMm + (gridCols - 1) * this.printLayout.cardSpacing}mm;
height: ${gridRows * cardHeightMm + (gridRows - 1) * this.printLayout.cardSpacing}mm; htmlContent += `<div class="print-page" style="page-break-after: ${pageBreak}; width: ${pageWidthMm}mm; padding: ${this.printLayout.pageMargin}mm; box-sizing: border-box;">
margin: 0; <div class="print-sheet" style="
padding: 0; display: grid;
box-sizing: border-box; grid-template-columns: repeat(${gridColsValue}, ${cardWidthValue}mm);
"> grid-template-rows: repeat(${gridRows}, ${cardHeightValue}mm);
${pageCards.map((card, index) => ` gap: ${spacingValue}mm;
width: ${widthValue}mm;
height: ${heightValue}mm;
margin: 0;
padding: 0;
box-sizing: border-box;
">
${pageCards.map((card, index) => {
const cardFitFactor = card.cardFit || 0;
return `
<div class="card card-${startIndex + index} ${card.isVisible ? '' : 'empty-card'}" <div class="card card-${startIndex + index} ${card.isVisible ? '' : 'empty-card'}"
data-card-index="${startIndex + index}" data-card-index="${startIndex + index}"
data-background-style="${card.backgroundStyle}" data-background-style="${card.backgroundStyle}"
data-card-fit="${cardFitFactor}"
onclick="if (window.parent && window.parent.selectCardFromPreview) { window.parent.selectCardFromPreview(${startIndex + index}); }" onclick="if (window.parent && window.parent.selectCardFromPreview) { window.parent.selectCardFromPreview(${startIndex + index}); }"
style="cursor: pointer; style="cursor: pointer;
width: ${cardWidthMm}mm; width: ${cardWidthMm}mm;
@@ -1243,7 +1317,7 @@ class YotoCoversApp {
-webkit-text-stroke: #fff calc(var(--o)); -webkit-text-stroke: #fff calc(var(--o));
text-transform: uppercase; text-transform: uppercase;
mix-blend-mode: screen; mix-blend-mode: screen;
font: clamp(1em, min(65vh, 35vw), 2em)/ 0.75 'Bebas Neue', sans-serif; font: ${card.titleFontSize * 0.8}px/ 0.75 'Bebas Neue', sans-serif;
transform: rotate(${card.titlePanel.rotation}deg); transform: rotate(${card.titlePanel.rotation}deg);
box-shadow: 2px 2px 4px rgba(0,0,0,0.2); box-shadow: 2px 2px 4px rgba(0,0,0,0.2);
@supports (line-height: 1cap) { line-height: 1cap; } @supports (line-height: 1cap) { line-height: 1cap; }
@@ -1353,7 +1427,7 @@ class YotoCoversApp {
-webkit-text-stroke: #fff calc(var(--o)); -webkit-text-stroke: #fff calc(var(--o));
text-transform: uppercase; text-transform: uppercase;
mix-blend-mode: screen; mix-blend-mode: screen;
font: clamp(2em, min(65vh, 35vw), 3.9em)/ 0.75 'Bebas Neue', sans-serif; font: ${card.titleFontSize}px/ 0.75 'Bebas Neue', sans-serif;
${card.titleDraggable ? 'cursor: move; user-select: none;' : ''} ${card.titleDraggable ? 'cursor: move; user-select: none;' : ''}
@supports (line-height: 1cap) { line-height: 1cap; } @supports (line-height: 1cap) { line-height: 1cap; }
} }
@@ -1394,7 +1468,7 @@ class YotoCoversApp {
`} `}
` : ''} ` : ''}
</div> </div>
`).join('')} `;}).join('')}
</div> </div>
</div>`; </div>`;
} }
@@ -1427,6 +1501,14 @@ class YotoCoversApp {
.empty-card { .empty-card {
background: #f9f9f9; background: #f9f9f9;
} }
@media print {
${cards.map((card, index) => {
const fitFactor = card.cardFit || 0;
const widthMm = 54 * (1 + fitFactor);
const heightMm = 85.6 * (1 + fitFactor);
return `.card-${index} { width: ${widthMm}mm !important; height: ${heightMm}mm !important; }`;
}).join('\n ')}
}
${cards.map((card, index) => ` ${cards.map((card, index) => `
.card-${index} .title { .card-${index} .title {
color: ${card.titleColor}; color: ${card.titleColor};
@@ -1624,12 +1706,69 @@ class YotoCoversApp {
}, 5000); }, 5000);
} }
zoomIn() {
if (this.previewZoom.level < this.previewZoom.max) {
this.previewZoom.level = Math.min(this.previewZoom.max, this.previewZoom.level + this.previewZoom.step);
this.updateZoom();
}
}
zoomOut() {
if (this.previewZoom.level > this.previewZoom.min) {
this.previewZoom.level = Math.max(this.previewZoom.min, this.previewZoom.level - this.previewZoom.step);
this.updateZoom();
}
}
resetZoom() {
this.previewZoom.level = 1.0;
this.updateZoom();
}
updateZoom() {
const iframe = document.getElementById('preview-iframe');
const zoomLevel = document.getElementById('zoom-level');
if (iframe) {
iframe.style.transform = `scale(${this.previewZoom.level})`;
iframe.style.transformOrigin = 'top left';
}
if (zoomLevel) {
zoomLevel.textContent = Math.round(this.previewZoom.level * 100) + '%';
}
}
toggleFullscreen() {
const container = document.querySelector('.preview-container');
const overlay = document.getElementById('fullscreen-overlay');
const btn = document.getElementById('fullscreen-btn');
if (this.isFullscreen) {
// Exit fullscreen
container.classList.remove('fullscreen');
overlay.classList.remove('active');
btn.textContent = '⛶ Full Screen';
this.isFullscreen = false;
} else {
// Enter fullscreen
container.classList.add('fullscreen');
overlay.classList.add('active');
btn.textContent = '⛶ Exit Full Screen';
this.isFullscreen = true;
// Add click handler to overlay to exit fullscreen
overlay.onclick = () => this.toggleFullscreen();
}
}
updateUI() { updateUI() {
// Initialize print layout controls // Initialize print layout controls
document.getElementById('cards-per-row').value = this.printLayout.cardsPerRow; document.getElementById('cards-per-row').value = this.printLayout.cardsPerRow;
document.getElementById('cards-per-column').value = this.printLayout.cardsPerColumn; document.getElementById('cards-per-column').value = this.printLayout.cardsPerColumn;
document.getElementById('page-margin').value = this.printLayout.pageMargin; document.getElementById('page-margin').value = this.printLayout.pageMargin;
document.getElementById('card-spacing').value = this.printLayout.cardSpacing; document.getElementById('card-spacing').value = this.printLayout.cardSpacing;
document.getElementById('global-card-fit').value = this.printLayout.cardFit;
} }
} }
+147
View File
@@ -287,6 +287,8 @@
height: 600px; height: 600px;
border: none; border: none;
display: block; display: block;
transform-origin: top left;
transition: transform 0.2s ease;
} }
.preview-placeholder { .preview-placeholder {
@@ -301,6 +303,118 @@
text-align: center; text-align: center;
} }
.preview-controls {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
padding: 10px;
background: #f8f9fa;
border-radius: 6px;
}
.zoom-controls {
display: flex;
align-items: center;
gap: 10px;
}
.zoom-btn {
width: 30px;
height: 30px;
border: 1px solid #ddd;
border-radius: 4px;
background: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: bold;
transition: background-color 0.2s;
}
.zoom-btn:hover {
background: #f0f0f0;
}
.zoom-btn:active {
background: #e0e0e0;
}
.zoom-level {
font-size: 14px;
font-weight: 500;
min-width: 60px;
text-align: center;
}
.fullscreen-btn {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
background: white;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}
.fullscreen-btn:hover {
background: #f0f0f0;
}
.preview-container.fullscreen {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 9999;
border: none;
border-radius: 0;
background: white;
}
.preview-container.fullscreen .preview-iframe {
height: 100vh;
}
.preview-container.fullscreen::before {
content: '✕ Close (ESC)';
position: fixed;
top: 20px;
right: 20px;
z-index: 10000;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
font-weight: 500;
transition: background 0.2s;
}
.preview-container.fullscreen:hover::before {
background: rgba(0, 0, 0, 0.9);
}
.fullscreen-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.8);
z-index: 9998;
display: none;
cursor: pointer;
}
.fullscreen-overlay.active {
display: block;
}
.color-picker { .color-picker {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -612,6 +726,16 @@
<label for="card-spacing">Card spacing (mm)</label> <label for="card-spacing">Card spacing (mm)</label>
<input type="number" id="card-spacing" value="5" min="0" max="20"> <input type="number" id="card-spacing" value="5" min="0" max="20">
</div> </div>
<div class="form-group">
<label for="global-card-fit">Global card fit</label>
<select id="global-card-fit">
<option value="-0.02">Overfit (+2%)</option>
<option value="0" selected>Exact fit</option>
<option value="0.02">Underfit (-2%)</option>
</select>
<small style="color: #666; display: block; margin-top: 5px;">Adjust the print size for all cards (can be overridden per card)</small>
</div>
</div> </div>
</div> </div>
@@ -644,6 +768,16 @@
<div class="panel preview-panel"> <div class="panel preview-panel">
<h3 style="margin-bottom: 20px;">Preview</h3> <h3 style="margin-bottom: 20px;">Preview</h3>
<div class="preview-controls">
<div class="zoom-controls">
<button class="zoom-btn" id="zoom-out" title="Zoom Out">-</button>
<span class="zoom-level" id="zoom-level">100%</span>
<button class="zoom-btn" id="zoom-in" title="Zoom In">+</button>
<button class="zoom-btn" id="zoom-reset" title="Reset Zoom"></button>
</div>
<button class="fullscreen-btn" id="fullscreen-btn" title="Full Screen (Press ESC to exit)">⛶ Full Screen</button>
</div>
<div class="preview-container"> <div class="preview-container">
<div class="preview-placeholder" id="preview-placeholder"> <div class="preview-placeholder" id="preview-placeholder">
Click "Generate Preview" to see your cover Click "Generate Preview" to see your cover
@@ -930,6 +1064,17 @@
<input type="number" id="card-footer-font-size" value="12" min="8" max="100" step="2"> <input type="number" id="card-footer-font-size" value="12" min="8" max="100" step="2">
</div> </div>
<div class="form-group">
<label for="card-fit">Card print fit override</label>
<select id="card-fit">
<option value="">Use global setting</option>
<option value="-0.02">Overfit (+2%)</option>
<option value="0">Exact fit</option>
<option value="0.02">Underfit (-2%)</option>
</select>
<small style="color: #666; display: block; margin-top: 5px;">Override the global card fit setting for this card</small>
</div>
<div class="form-group"> <div class="form-group">
<label>Footer Position</label> <label>Footer Position</label>
<div class="position-controls"> <div class="position-controls">
@@ -988,6 +1133,8 @@
</div> </div>
</div> </div>
<div class="fullscreen-overlay" id="fullscreen-overlay"></div>
<script src="app.js"></script> <script src="app.js"></script>
</body> </body>
</html> </html>