From de9ed94dbffeb28d43918417f786d192086841fc Mon Sep 17 00:00:00 2001 From: Ross Date: Sun, 4 Jan 2026 21:50:37 +0000 Subject: [PATCH] feat: enhance card customization options and UI layout in the editor --- README.md | 2 +- app.js | 342 ++++++++++++++++++++++++++++++++++++++++++++------- index.html | 349 ++++++++++++++++++++++++++++++++++++----------------- 3 files changed, 543 insertions(+), 150 deletions(-) diff --git a/README.md b/README.md index f727805..fb0781c 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ A standalone web application for creating and customizing Yoto card covers. This - **Multiple Templates**: Classic, Frozen, Minimal, and Vintage cover styles - **Title Styles**: Classic, Large, Small, Folded, and Condensed title effects - **Color Customization**: Customize accent colors and title colors -- **Full Bleed Option**: Choose whether images cover the full card background +- **Full Cover Image (Full Bleed) Option**: Choose whether images cover the full card background - **Title Shadows**: Add depth with optional title shadows ### Card Variations diff --git a/app.js b/app.js index 2ca9e48..c2ed988 100644 --- a/app.js +++ b/app.js @@ -36,6 +36,15 @@ class YotoCoversApp { fontFamily: 'Comic Sans MS', fontWeight: 'bold', footerColor: '#ff6b6b' + }, + polaroid: { + backgroundStyle: 'polaroid', + fontFamily: 'Arial', + fontWeight: 'normal', + footerColor: '#666666', + fullBleed: false, + showFooter: false, + imagePosition: 'center' } }; @@ -58,7 +67,6 @@ class YotoCoversApp { this.cardVariations = []; this.activeCardIndex = -1; // Currently selected card for editing - this.globalTitleFontSize = 20; // Global title font size in pixels this.init(); } @@ -208,9 +216,26 @@ class YotoCoversApp { 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()); - document.getElementById('card-show-footer').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-image-fit').addEventListener('change', () => this.updateActiveCard()); + document.getElementById('card-image-position').addEventListener('change', () => this.updateActiveCard()); document.getElementById('card-image-select').addEventListener('change', () => this.updateActiveCard()); // Title style selection auto-update @@ -223,10 +248,10 @@ class YotoCoversApp { }); // Global title font size - document.getElementById('global-title-font-size').addEventListener('input', (e) => { - this.globalTitleFontSize = parseInt(e.target.value); - this.generatePreview(); - }); + // 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', () => { @@ -238,6 +263,14 @@ class YotoCoversApp { 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) { @@ -329,6 +362,17 @@ class YotoCoversApp { 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; + } + this.updateActiveCard(); } @@ -341,13 +385,16 @@ class YotoCoversApp { fontFamily: 'Arial', fontWeight: 'normal', footerColor: '#111111', - titleStyle: 'folded', + titleStyle: 'classic', accentColor: '#f1c40f', + showAccent: false, titleColor: '#111111', fullBleed: true, titleShadow: false, showTitle: true, - showFooter: true + showFooter: true, + imageFit: 'cover', + imagePosition: 'center' }; this.cardVariations.push(variation); this.updateCardList(); @@ -451,10 +498,17 @@ class YotoCoversApp { // Set template preset (detect which preset matches current features) let matchingPreset = ''; for (const [presetName, preset] of Object.entries(this.templates)) { - if (variation.backgroundStyle === preset.backgroundStyle && + let matches = variation.backgroundStyle === preset.backgroundStyle && variation.fontFamily === preset.fontFamily && variation.fontWeight === preset.fontWeight && - variation.footerColor === preset.footerColor) { + 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 (matches) { matchingPreset = presetName; break; } @@ -476,12 +530,15 @@ class YotoCoversApp { // Set title style document.querySelectorAll('.title-style-option').forEach(option => { - option.classList.toggle('selected', option.dataset.style === (variation.titleStyle || 'folded')); + 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'; @@ -491,6 +548,16 @@ class YotoCoversApp { 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 image options + document.getElementById('card-image-fit').value = variation.imageFit || 'cover'; + document.getElementById('card-image-position').value = variation.imagePosition || 'center'; + // Populate image options const imageSelect = document.getElementById('card-image-select'); imageSelect.innerHTML = ''; @@ -503,6 +570,158 @@ class YotoCoversApp { }); 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 titleSection = document.getElementById('title-section'); + const footerSection = document.getElementById('footer-section'); + + if (showTitle) { + titleSection.classList.remove('collapsed'); + } else { + titleSection.classList.add('collapsed'); + } + + if (showFooter) { + footerSection.classList.remove('collapsed'); + } else { + footerSection.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'); + 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' + if (title.style.cursor !== 'move') { + 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); + }); + } 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() { @@ -515,13 +734,20 @@ class YotoCoversApp { 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 || 'folded'; + 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.imageFit = document.getElementById('card-image-fit').value; + variation.imagePosition = document.getElementById('card-image-position').value; variation.imageIndex = parseInt(document.getElementById('card-image-select').value); this.updateCardList(); @@ -566,7 +792,7 @@ class YotoCoversApp { // Apply zoom after iframe loads iframe.onload = () => { - this.applyZoomToPreview(); + this.initDragFunctionality(); }; // Clean up the previous blob URL @@ -604,6 +830,8 @@ class YotoCoversApp { 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 cardShowAccent; if (this.cardVariations.length > 0) { // Use variation settings @@ -611,8 +839,9 @@ class YotoCoversApp { imageIndex = variation && variation.imageIndex >= 0 ? variation.imageIndex : 0; cardTitle = (variation && variation.title) || `Card ${i + 1}`; cardFooter = (variation && variation.footer) || ''; - cardTitleStyle = (variation && variation.titleStyle) || 'folded'; + 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; @@ -623,13 +852,20 @@ class YotoCoversApp { 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; } else { // No variations - auto-assign images imageIndex = i < this.images.length ? i : 0; cardTitle = `Card ${i + 1}`; cardFooter = ''; - cardTitleStyle = 'folded'; + cardTitleStyle = 'classic'; cardAccentColor = '#f1c40f'; + cardShowAccent = false; cardTitleColor = '#111111'; cardFullBleed = true; cardTitleShadow = false; @@ -640,6 +876,12 @@ class YotoCoversApp { cardFooterColor = '#111111'; cardShowTitle = true; cardShowFooter = true; + cardImageFit = 'cover'; + cardImagePosition = 'center'; + cardTitleFontSize = 20; + cardTitleX = 50; + cardTitleY = 20; + cardTitleDraggable = false; } const imageUrl = this.images.length > 0 ? this.images[imageIndex].dataUrl : 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; @@ -654,11 +896,18 @@ class YotoCoversApp { 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, isVisible: true }); } else { @@ -673,11 +922,18 @@ class YotoCoversApp { 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: 20, + titleDraggable: false, isVisible: false }); } @@ -719,6 +975,7 @@ class YotoCoversApp { ${pageCards.map((card, index) => `
${this.escapeHtml(card.title)}
` : ''} - ${!card.fullBleed ? `
cover
` : ''} -
+ ${!card.fullBleed ? `
cover
` : ''} + ${card.showAccent ? `
` : `
`} ${card.showFooter ? `