From 7f7ea978b9e32029ddc86a1edef820d8739c78e3 Mon Sep 17 00:00:00 2001 From: jessikitty Date: Fri, 22 May 2026 09:17:58 +1000 Subject: [PATCH] fix: pile uses shared FRAME_PAD_RATIO/FRAME_BOTTOM_RATIO/FRAME_COLOR to match main, stronger sepia wash on pile --- public/js/app.js | 88 ++++++++++++++---------------------------------- 1 file changed, 26 insertions(+), 62 deletions(-) diff --git a/public/js/app.js b/public/js/app.js index 6f8b65a..b0d83da 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -6,6 +6,10 @@ var selectedPersonId = null, isRunning = false, refreshTimer = null, urlDriven = false; var currentVideoPlaying = false; var pileCanvas, pileCtx; + // Shared polaroid proportions (match main frame CSS: ~3% sides, ~10% bottom) + var FRAME_PAD_RATIO = 0.03; + var FRAME_BOTTOM_RATIO = 0.10; + var FRAME_COLOR = '#ede8df'; var $setupScreen = document.getElementById('setup-screen'), $slideshowScreen = document.getElementById('slideshow-screen'); var $connectionStatus = document.getElementById('connection-status'), $setupContent = document.getElementById('setup-content'); @@ -66,73 +70,46 @@ } function clearPileCanvas() { - if (pileCtx) { - pileCtx.setTransform(1,0,0,1,0,0); - pileCtx.clearRect(0, 0, pileCanvas.width, pileCanvas.height); - pileCtx.scale(window.devicePixelRatio || 1, window.devicePixelRatio || 1); - } + if (pileCtx) { pileCtx.setTransform(1,0,0,1,0,0); pileCtx.clearRect(0, 0, pileCanvas.width, pileCanvas.height); pileCtx.scale(window.devicePixelRatio || 1, window.devicePixelRatio || 1); } } - // Animate a polaroid fading onto the pile canvas function dropPhotoPile(imgSrc) { var img = new Image(); img.crossOrigin = 'anonymous'; img.onload = function () { var vw = window.innerWidth, vh = window.innerHeight; + // Pile polaroid size: 18-25% of screen width var polaroidW = vw * (0.18 + Math.random() * 0.07); - var padding = polaroidW * 0.04; - var bottomPad = polaroidW * 0.12; - var innerW = polaroidW - padding * 2; + // Use shared proportions to match main frame + var pad = polaroidW * FRAME_PAD_RATIO; + var bottomPad = polaroidW * FRAME_BOTTOM_RATIO; + var innerW = polaroidW - pad * 2; var innerH = innerW * (img.height / img.width); - var totalH = innerH + padding + bottomPad; - var cx = Math.random() * vw; - var cy = Math.random() * vh; + var totalH = innerH + pad + bottomPad; + var cx = Math.random() * vw, cy = Math.random() * vh; var rot = (Math.random() - 0.5) * 30; - // Fade in over ~1 second - var startTime = null; - var fadeDuration = 1000; - + var startTime = null, fadeDuration = 1200; function drawFrame(timestamp) { if (!startTime) startTime = timestamp; - var elapsed = timestamp - startTime; - var alpha = Math.min(elapsed / fadeDuration, 1); - - // We need to redraw this specific polaroid with increasing alpha - // To avoid clearing the whole canvas, we draw on a temp canvas and composite + var alpha = Math.min((timestamp - startTime) / fadeDuration, 1); pileCtx.save(); pileCtx.globalAlpha = alpha; pileCtx.translate(cx, cy); pileCtx.rotate(rot * Math.PI / 180); - - // Shadow pileCtx.shadowColor = 'rgba(0,0,0,0.45)'; pileCtx.shadowBlur = 18; pileCtx.shadowOffsetX = 3; pileCtx.shadowOffsetY = 6; - - // White polaroid border - pileCtx.fillStyle = '#ede8df'; + pileCtx.fillStyle = FRAME_COLOR; pileCtx.fillRect(-polaroidW/2, -totalH/2, polaroidW, totalH); - - // Clear shadow for image - pileCtx.shadowColor = 'transparent'; - pileCtx.shadowBlur = 0; - pileCtx.shadowOffsetX = 0; - pileCtx.shadowOffsetY = 0; - - // Photo - pileCtx.drawImage(img, -polaroidW/2 + padding, -totalH/2 + padding, innerW, innerH); - - // Sepia/warm wash over the photo - pileCtx.fillStyle = 'rgba(160, 130, 80, 0.15)'; - pileCtx.fillRect(-polaroidW/2 + padding, -totalH/2 + padding, innerW, innerH); - + pileCtx.shadowColor = 'transparent'; pileCtx.shadowBlur = 0; pileCtx.shadowOffsetX = 0; pileCtx.shadowOffsetY = 0; + pileCtx.drawImage(img, -polaroidW/2 + pad, -totalH/2 + pad, innerW, innerH); + // Sepia wash + pileCtx.fillStyle = 'rgba(150, 120, 70, 0.2)'; + pileCtx.fillRect(-polaroidW/2 + pad, -totalH/2 + pad, innerW, innerH); pileCtx.restore(); - - if (alpha < 1) { - requestAnimationFrame(drawFrame); - } + if (alpha < 1) requestAnimationFrame(drawFrame); } requestAnimationFrame(drawFrame); }; @@ -169,8 +146,7 @@ isRunning=false;clearTimeout(slideshowTimer);if(refreshTimer)clearInterval(refreshTimer);stopVideo(); $slideshowScreen.style.display='none';$setupScreen.style.display='flex';document.body.classList.add('setup-mode'); $btnStart.textContent='▶ Start Slideshow';$btnStart.disabled=false; - $bgBlur.style.backgroundImage='';$bgBlur.classList.remove('visible');$mainFrame.classList.remove('visible'); - clearPileCanvas(); + $bgBlur.style.backgroundImage='';$bgBlur.classList.remove('visible');$mainFrame.classList.remove('visible');clearPileCanvas(); }; function showNextAsset() { currentIndex++;if(currentIndex>=assets.length){if(config.shuffle)shuffleArray(assets);currentIndex=0;}showAsset(currentIndex); } @@ -182,42 +158,30 @@ var asset = assets[index], isVideo = asset.type === 'VIDEO'; var thumbUrl = '/api/assets/' + asset.id + '/thumbnail?size=preview'; console.log('[Frambe] Showing ' + (isVideo ? 'VIDEO' : 'PHOTO') + ': ' + (asset.originalFileName || asset.id)); - - // Drop previous photo onto pile - if (currentIndex > 0) { - var pi = currentIndex - 1; if (pi < 0) pi = assets.length - 1; - if (assets[pi]) dropPhotoPile('/api/assets/' + assets[pi].id + '/thumbnail?size=thumbnail'); - } - - // Fade out main + if (currentIndex > 0) { var pi = currentIndex - 1; if (pi < 0) pi = assets.length - 1; if (assets[pi]) dropPhotoPile('/api/assets/' + assets[pi].id + '/thumbnail?size=thumbnail'); } $mainFrame.classList.remove('visible'); - var img = new Image(); img.onload = function () { setTimeout(function () { displayAsset(asset, thumbUrl, isVideo); }, 500); }; img.onerror = function () { setTimeout(showNextAsset, 500); }; img.src = thumbUrl; - var ni = index + 1; if (ni >= assets.length) ni = 0; if (assets[ni]) { var pre = new Image(); pre.src = '/api/assets/' + assets[ni].id + '/thumbnail?size=preview'; } } function displayAsset(asset, thumbUrl, isVideo) { if (config.backgroundBlur) { $bgBlur.style.backgroundImage = 'url(' + thumbUrl + ')'; $bgBlur.classList.add('visible'); } - $mainVideo.style.display = 'none'; $mainPhoto.style.display = 'none'; - if (isVideo) { $mainVideo.style.display = 'block'; $mainVideo.src = '/api/assets/' + asset.id + '/video'; $mainVideo.poster = thumbUrl; $mainVideo.load(); - $mainVideo.play().then(function(){ currentVideoPlaying=true; console.log('[Frambe] Video playing'); }).catch(function(e){ console.warn('[Frambe] Video autoplay failed: '+e.message); }); - $mainVideo.onended = function () { console.log('[Frambe] Video ended'); currentVideoPlaying=false; showNextAsset(); }; - slideshowTimer = setTimeout(function(){ if(currentVideoPlaying){console.log('[Frambe] Video timeout');showNextAsset();} }, Math.max((config.slideshowInterval||30)*3, 120)*1000); + $mainVideo.play().then(function(){ currentVideoPlaying=true; }).catch(function(e){ console.warn('[Frambe] Video autoplay failed: '+e.message); }); + $mainVideo.onended = function () { currentVideoPlaying=false; showNextAsset(); }; + slideshowTimer = setTimeout(function(){ if(currentVideoPlaying){showNextAsset();} }, Math.max((config.slideshowInterval||30)*3, 120)*1000); } else { $mainPhoto.style.display = 'block'; $mainPhoto.src = thumbUrl; slideshowTimer = setTimeout(showNextAsset, (config.slideshowInterval||30)*1000); } - requestAnimationFrame(function () { $mainFrame.classList.add('visible'); }); updateExifInfo(asset); startProgress(isVideo ? null : (config.slideshowInterval||30)*1000); }