feat: canvas-based cumulative photo pile, unified polaroid for photos+videos, larger main frame
This commit is contained in:
+101
-39
@@ -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<pairs.length;i++){var kv=pairs[i].split('=');p[decodeURIComponent(kv[0])]=decodeURIComponent(kv[1]||'');}return p; }
|
||||
|
||||
async function autoLaunch(source, albumId, personId) { urlDriven=true;selectedSource=source;selectedAlbumId=albumId||null;selectedPersonId=personId||null;console.log('[Frambe] Auto-launching: source='+source+(albumId?' album='+albumId:'')+(personId?' person='+personId:''));await doStartSlideshow(); }
|
||||
async function autoLaunch(src, aid, pid) { urlDriven=true;selectedSource=src;selectedAlbumId=aid||null;selectedPersonId=pid||null;console.log('[Frambe] Auto-launching: source='+src+(aid?' album='+aid:'')+(pid?' person='+pid:''));await doStartSlideshow(); }
|
||||
|
||||
async function init() {
|
||||
document.body.classList.add('setup-mode');
|
||||
@@ -49,16 +43,86 @@
|
||||
}
|
||||
|
||||
function showError(msg) { $setupContent.style.display='none';$setupError.style.display='block';$errorDetail.textContent=msg; }
|
||||
|
||||
async function loadAlbums() { try { var albums=await(await fetch('/api/albums')).json();if(!albums.length){$albumsList.innerHTML='<p class="loading-text">No albums found</p>';return;}var html='';for(var i=0;i<albums.length;i++){var a=albums[i],thu=a.albumThumbnailAssetId?'/api/assets/'+a.albumThumbnailAssetId+'/thumbnail?size=thumbnail':'';html+='<div class="album-item" data-id="'+a.id+'" onclick="selectAlbum(\''+a.id+'\', this)">';html+=thu?'<img class="album-thumb" src="'+thu+'" alt="" loading="lazy">':'<div class="album-thumb" style="display:flex;align-items:center;justify-content:center;font-size:1.2rem">📁</div>';html+='<div class="album-info"><div class="album-name">'+escapeHtml(a.albumName)+'</div><div class="album-count">'+a.assetCount+' items</div></div></div>';}$albumsList.innerHTML=html;}catch(e){$albumsList.innerHTML='<p class="loading-text">Failed to load albums</p>';} }
|
||||
|
||||
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;i<items.length;i++)items[i].classList.remove('selected');$btnStart.disabled=false;};
|
||||
window.selectAlbum = function(id,el){selectedSource='album';selectedAlbumId=id;selectedPersonId=null;document.getElementById('btn-all-photos').classList.remove('selected');document.getElementById('btn-favorites').classList.remove('selected');var items=document.querySelectorAll('.album-item');for(var i=0;i<items.length;i++)items[i].classList.remove('selected');el.classList.add('selected');$btnStart.disabled=false;};
|
||||
|
||||
async function loadAssets() { var res;if(selectedSource==='album'&&selectedAlbumId){res=await fetch('/api/albums/'+selectedAlbumId);if(!res.ok)throw new Error('Album fetch failed: '+res.status);var al=await res.json();assets=al.assets||[];}else if(selectedSource==='person'&&selectedPersonId){res=await fetch('/api/people/'+selectedPersonId);if(!res.ok)throw new Error('Person fetch failed: '+res.status);assets=await res.json();}else if(selectedSource==='favorites'){res=await fetch('/api/assets/favorites');if(!res.ok)throw new Error('Favorites fetch failed: '+res.status);assets=await res.json();}else{res=await fetch('/api/assets/random?count=100');if(!res.ok)throw new Error('Random fetch failed: '+res.status);assets=await res.json();}if(config.shuffle)shuffleArray(assets);console.log('[Frambe] Loaded '+assets.length+' assets'); }
|
||||
|
||||
function startRefreshTimer() { if(refreshTimer)clearInterval(refreshTimer);refreshTimer=setInterval(async function(){try{var oldIds={};for(var i=0;i<assets.length;i++)oldIds[assets[i].id]=true;var nw,r;if(selectedSource==='album'&&selectedAlbumId){r=await(await fetch('/api/albums/'+selectedAlbumId)).json();nw=r.assets||[];}else if(selectedSource==='person'&&selectedPersonId){nw=await(await fetch('/api/people/'+selectedPersonId)).json();}else if(selectedSource==='favorites'){nw=await(await fetch('/api/assets/favorites')).json();}else return;var added=0;for(var j=0;j<nw.length;j++){if(!oldIds[nw[j].id]){assets.push(nw[j]);added++;}}if(added>0)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 = '<span class="spinner"></span> 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 = '<div style="width:100%;height:100%;background:url(/api/assets/'+aid+'/thumbnail?size=thumbnail) center/cover;background-color:#d4cfc5;"></div>';
|
||||
$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<PILE_SIZE;i++)document.getElementById('pile-'+i).classList.remove('visible'); }
|
||||
|
||||
function updateExifInfo(a) { if(!config.showExif||!a.exifInfo){$exifInfo.textContent='';return;}var p=[],e=a.exifInfo,loc=[e.city,e.state,e.country].filter(Boolean).join(', ');if(loc)p.push(loc);if(e.dateTimeOriginal)p.push(formatDate(new Date(e.dateTimeOriginal)));else if(a.fileCreatedAt)p.push(formatDate(new Date(a.fileCreatedAt)));if(e.make||e.model)p.push([e.make,e.model].filter(Boolean).join(' '));if(a.type==='VIDEO')p.push('Video');$exifInfo.textContent=p.join(' · '); }
|
||||
function startProgress(ms) { if(!config.showProgress)return;$progressFill.style.transition='none';$progressFill.style.width='0%';$progressFill.offsetWidth;if(ms){$progressFill.style.transition='width '+ms+'ms linear';$progressFill.style.width='100%';} }
|
||||
function updateClock() { var n=new Date();if(config.showClock)$clock.textContent=padZero(n.getHours())+':'+padZero(n.getMinutes());if(config.showDate)$dateDisplay.textContent=n.toLocaleDateString(undefined,{weekday:'long',day:'numeric',month:'long',year:'numeric'}); }
|
||||
|
||||
Reference in New Issue
Block a user