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;iNo albums found
';return;}var html='';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