From 804c6cfd864634381c336c173da2acefbe3158ed Mon Sep 17 00:00:00 2001 From: jessikitty Date: Wed, 20 May 2026 09:48:15 +1000 Subject: [PATCH] feat: canvas-based cumulative photo pile, unified polaroid for photos+videos, larger main frame --- public/js/app.js | 140 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 101 insertions(+), 39 deletions(-) diff --git a/public/js/app.js b/public/js/app.js index 41bc6fe..7466d8c 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -1,10 +1,11 @@ -// === Frambe v1.3.0 - Vintage Polaroid Pile === +// === Frambe v1.3.0 - Vintage Polaroid Pile (Canvas) === (function () { 'use strict'; var config = {}, assets = [], currentIndex = -1, slideshowTimer = null; var overlayVisible = true, overlayTimeout = null, selectedSource = null, selectedAlbumId = null; var selectedPersonId = null, isRunning = false, refreshTimer = null, urlDriven = false; - var pileHistory = [], PILE_SIZE = 5, currentVideoPlaying = false; + var currentVideoPlaying = false; + var pileCanvas, pileCtx; // canvas for accumulated polaroid pile var $setupScreen = document.getElementById('setup-screen'), $slideshowScreen = document.getElementById('slideshow-screen'); var $connectionStatus = document.getElementById('connection-status'), $setupContent = document.getElementById('setup-content'); @@ -17,15 +18,8 @@ var $overlay = document.getElementById('overlay'), $btnSettings = document.getElementById('btn-settings'); var $progressBar = document.getElementById('progress-bar'); - var pilePositions = [ - { x: -18, y: -12, rot: -11, scale: 0.85 }, { x: 22, y: -8, rot: 7, scale: 0.80 }, - { x: -14, y: 15, rot: 14, scale: 0.78 }, { x: 20, y: 12, rot: -9, scale: 0.82 }, - { x: -5, y: -20, rot: 4, scale: 0.75 } - ]; - function getUrlParams() { var p={},s=window.location.search.substring(1);if(!s)return p;var pairs=s.split('&');for(var i=0;i';html+=thu?'':'
📁
';html+='
'+escapeHtml(a.albumName)+'
'+a.assetCount+' items
';}$albumsList.innerHTML=html;}catch(e){$albumsList.innerHTML='

Failed to load albums

';} } window.selectSource = function(src){selectedSource=src;selectedAlbumId=null;selectedPersonId=null;document.getElementById('btn-all-photos').classList.toggle('selected',src==='random');document.getElementById('btn-favorites').classList.toggle('selected',src==='favorites');var items=document.querySelectorAll('.album-item');for(var i=0;i0)console.log('[Frambe] Refresh added '+added+' new asset(s)');}catch(e){console.warn('[Frambe] Refresh failed: '+e.message);}}, (config.refreshInterval||300)*1000); } + // ========================= + // CANVAS PILE + // ========================= + function initPileCanvas() { + pileCanvas = document.getElementById('pile-canvas'); + pileCanvas.width = window.innerWidth * (window.devicePixelRatio || 1); + pileCanvas.height = window.innerHeight * (window.devicePixelRatio || 1); + pileCanvas.style.width = window.innerWidth + 'px'; + pileCanvas.style.height = window.innerHeight + 'px'; + pileCtx = pileCanvas.getContext('2d'); + pileCtx.scale(window.devicePixelRatio || 1, window.devicePixelRatio || 1); + } + + 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); + } + } + + function dropPhotoPile(imgSrc) { + var img = new Image(); + img.crossOrigin = 'anonymous'; + img.onload = function () { + var vw = window.innerWidth, vh = window.innerHeight; + // 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; + var innerH = innerW * (img.height / img.width); + var totalH = innerH + padding + bottomPad; + + // Random position across the screen + var cx = Math.random() * vw; + var cy = Math.random() * vh; + var rot = (Math.random() - 0.5) * 30; // -15 to +15 degrees + + pileCtx.save(); + pileCtx.translate(cx, cy); + pileCtx.rotate(rot * Math.PI / 180); + + // Shadow + pileCtx.shadowColor = 'rgba(0,0,0,0.5)'; + pileCtx.shadowBlur = 15; + pileCtx.shadowOffsetX = 3; + pileCtx.shadowOffsetY = 5; + + // White polaroid border + pileCtx.fillStyle = '#f0ebe3'; + pileCtx.fillRect(-polaroidW/2, -totalH/2, polaroidW, totalH); + + // Clear shadow for the image + pileCtx.shadowColor = 'transparent'; + pileCtx.shadowBlur = 0; + pileCtx.shadowOffsetX = 0; + pileCtx.shadowOffsetY = 0; + + // Photo inside + pileCtx.drawImage(img, -polaroidW/2 + padding, -totalH/2 + padding, innerW, innerH); + + pileCtx.restore(); + console.log('[Frambe] Dropped polaroid onto pile'); + }; + img.onerror = function () { console.warn('[Frambe] Pile image failed to load'); }; + img.src = imgSrc; + } + + // ========================= + // SLIDESHOW ENGINE + // ========================= async function doStartSlideshow() { if (!selectedSource) return; $btnStart.disabled = true; $btnStart.innerHTML = ' Loading…'; @@ -67,25 +131,26 @@ if (!assets.length) { $btnStart.textContent = 'No photos found'; setTimeout(function(){$btnStart.textContent='▶ Start Slideshow';$btnStart.disabled=false;},2000); return; } $setupScreen.style.display = 'none'; $slideshowScreen.style.display = 'block'; document.body.classList.remove('setup-mode'); isRunning = true; + initPileCanvas(); if (!config.showClock) $clock.style.display = 'none'; if (!config.showDate) $dateDisplay.style.display = 'none'; if (!config.showExif) $exifInfo.style.display = 'none'; if (!config.showProgress) $progressBar.style.display = 'none'; if (!config.backgroundBlur) $bgBlur.style.display = 'none'; updateClock(); setInterval(updateClock, 1000); - currentIndex = -1; pileHistory = []; + currentIndex = -1; showNextAsset(); scheduleOverlayHide(); startRefreshTimer(); } catch (err) { console.error('[Frambe] Start failed: '+err.message); $btnStart.textContent='Error: '+err.message; setTimeout(function(){$btnStart.textContent='▶ Start Slideshow';$btnStart.disabled=false;},3000); } } window.startSlideshow = function () { doStartSlideshow(); }; - window.exitSlideshow = function () { if (urlDriven) { window.location.href = window.location.pathname; return; } 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');clearPile(); + $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); } @@ -97,53 +162,50 @@ 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)); - if (pileHistory.length > 0 || currentIndex > 0) { var pi = currentIndex - 1; if (pi < 0) pi = assets.length - 1; if (assets[pi]) addToPile(assets[pi]); } + + // Drop previous photo onto the pile canvas + if (currentIndex > 0 || pileCanvas) { + 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 $mainFrame.classList.remove('visible'); + var img = new Image(); img.onload = function () { setTimeout(function () { displayAsset(asset, thumbUrl, isVideo); }, 400); }; 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'; } + + // Preload next + 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'); } + + // Always polaroid style + $mainVideo.style.display = 'none'; $mainPhoto.style.display = 'none'; + if (isVideo) { - $mainFrame.classList.remove('polaroid'); $mainFrame.classList.add('filmstrip'); - $mainPhoto.style.display = 'none'; $mainVideo.style.display = 'block'; + $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(); }; - var maxDur = Math.max((config.slideshowInterval||30)*3, 120)*1000; - slideshowTimer = setTimeout(function(){ if(currentVideoPlaying){console.log('[Frambe] Video timeout');showNextAsset();} }, maxDur); + slideshowTimer = setTimeout(function(){ if(currentVideoPlaying){console.log('[Frambe] Video timeout');showNextAsset();} }, Math.max((config.slideshowInterval||30)*3, 120)*1000); } else { - $mainFrame.classList.remove('filmstrip'); $mainFrame.classList.add('polaroid'); - $mainVideo.style.display = 'none'; $mainPhoto.style.display = 'block'; $mainPhoto.src = thumbUrl; + $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); } function stopVideo() { if(currentVideoPlaying||$mainVideo.src){try{$mainVideo.pause();}catch(e){}$mainVideo.removeAttribute('src');$mainVideo.load();$mainVideo.onended=null;currentVideoPlaying=false;} } - function addToPile(asset) { - pileHistory.push(asset.id); if (pileHistory.length > PILE_SIZE) pileHistory.shift(); - for (var i = 0; i < PILE_SIZE; i++) { - var $pile = document.getElementById('pile-' + i), $inner = $pile.querySelector('.pile-inner'); - var histIdx = pileHistory.length - 1 - i; - if (histIdx >= 0 && pileHistory[histIdx]) { - var aid = pileHistory[histIdx], pos = pilePositions[i]; - $inner.style.cssText = 'width:100%;height:100%;background-color:#f5f0e8;padding:4% 4% 14% 4%;box-shadow:0 2px 15px rgba(0,0,0,0.4),0 0 1px rgba(0,0,0,0.2);border-radius:2px;'; - $inner.innerHTML = '
'; - $pile.style.transform = 'translate(calc(-50% + '+pos.x+'vmin), calc(-50% + '+pos.y+'vmin)) rotate('+pos.rot+'deg) scale('+pos.scale+')'; - $pile.style.opacity = 0.35 - (i * 0.04); $pile.classList.add('visible'); - } else { $pile.classList.remove('visible'); } - } - } - function clearPile() { pileHistory=[];for(var i=0;i