rosary: add mystery images for all four mystery types with PiP fullscreen
All checks were successful
CI / update (push) Successful in 1m29s

Generalize mystery images from sorrowful-only to all mystery types (joyful,
sorrowful, glorious, luminous). Add PiP fullscreen mode with tap-to-show
controls and double-tap to toggle enlarged/fullscreen.
This commit is contained in:
2026-02-09 23:02:32 +01:00
parent 07554f16df
commit 6eaf0bb4f4
17 changed files with 227 additions and 61 deletions

View File

@@ -10,6 +10,7 @@ interface PipOptions {
initialCorner?: Corner;
smallHeight?: number;
largeHeight?: number;
fullscreenEnabled?: boolean;
}
type Corner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
@@ -17,13 +18,16 @@ type Corner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
export function createPip(opts: PipOptions = {}) {
const margin = opts.margin ?? 16;
const tapThreshold = opts.tapThreshold ?? 10;
const doubleTapMs = opts.doubleTapMs ?? 400;
const doubleTapMs = opts.doubleTapMs ?? 250;
const smallVh = opts.smallHeight ?? 25;
const largeVh = opts.largeHeight ?? 37.5;
const fullscreenEnabled = opts.fullscreenEnabled ?? false;
let corner: Corner = $state(opts.initialCorner ?? 'bottom-right');
let dragging = $state(false);
let enlarged = $state(false);
let fullscreen = $state(false);
let showControls = $state(false);
let dragOffset = { x: 0, y: 0 };
let dragPos = { x: 0, y: 0 };
@@ -59,7 +63,7 @@ export function createPip(opts: PipOptions = {}) {
const pos = getCornerPos(c, target);
corner = c;
dragPos = pos;
target.style.transition = 'transform 0.25s ease';
target.style.transition = 'transform 0.25s cubic-bezier(0.4, 0, 0.2, 1)';
target.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
target.addEventListener('transitionend', () => { target.style.transition = ''; }, { once: true });
}
@@ -82,15 +86,77 @@ export function createPip(opts: PipOptions = {}) {
if (corner.includes('bottom')) newY = rect.bottom - newH;
dragPos = { x: newX, y: newY };
el.style.transition = 'transform 0.25s ease';
el.style.transition = 'transform 0.25s cubic-bezier(0.4, 0, 0.2, 1)';
el.style.transform = `translate(${newX}px, ${newY}px)`;
el.addEventListener('transitionend', () => {
el!.style.transition = '';
}, { once: true });
}
function toggleFullscreen() {
if (!el) return;
showControls = false;
lastTapTime = 0; // cancel any pending single-tap timeout
const img = el.querySelector('img') as HTMLImageElement | null;
if (!fullscreen) {
// Enter fullscreen: dark bg appears, image grows + moves to center
fullscreen = true;
el.style.transition = 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)';
el.style.transform = 'translate(0px, 0px)';
if (img) {
img.style.transition = 'height 0.3s cubic-bezier(0.4, 0, 0.2, 1)';
img.style.height = '90vh';
img.style.maxWidth = '95vw';
img.style.maxHeight = '90vh';
}
setTimeout(() => {
if (el) el.style.transition = '';
if (img) img.style.transition = '';
}, 350);
} else {
// Exit fullscreen: image shrinks + moves to corner, then bg removed
const vh = window.innerHeight;
const vw = window.innerWidth;
const pipH = vh * (smallVh / 100);
let pipW = pipH;
if (img && img.naturalHeight > 0) {
pipW = pipH * (img.naturalWidth / img.naturalHeight);
}
const pos = {
x: corner.includes('right') ? vw - pipW - margin : margin,
y: corner.includes('bottom') ? vh - pipH - margin : margin,
};
dragPos = pos;
el.style.transition = 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)';
el.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
if (img) {
img.style.transition = 'height 0.3s cubic-bezier(0.4, 0, 0.2, 1)';
img.style.height = `${smallVh}vh`;
img.style.maxWidth = '';
img.style.maxHeight = '';
}
setTimeout(() => {
fullscreen = false;
enlarged = false;
if (el) el.style.transition = '';
if (img) {
img.style.transition = '';
img.style.height = '';
}
}, 350);
}
}
function show(target: HTMLElement) {
el = target;
showControls = false;
if (fullscreen) {
target.style.opacity = '1';
return;
}
const pos = getCornerPos(corner, target);
dragPos = pos;
target.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
@@ -98,11 +164,25 @@ export function createPip(opts: PipOptions = {}) {
}
function hide() {
if (fullscreen) {
fullscreen = false;
showControls = false;
if (el) {
const img = el.querySelector('img') as HTMLElement | null;
if (img) {
img.style.transition = '';
img.style.height = '';
img.style.maxWidth = '';
img.style.maxHeight = '';
}
el.style.transition = '';
}
}
if (el) el.style.opacity = '0';
}
function reposition() {
if (!el) return;
if (!el || fullscreen) return;
const pos = getCornerPos(corner, el);
dragPos = pos;
el.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
@@ -112,15 +192,19 @@ export function createPip(opts: PipOptions = {}) {
if (!el) return;
dragging = true;
dragMoved = false;
const r = el.getBoundingClientRect();
dragOffset = { x: e.clientX - r.left, y: e.clientY - r.top };
if (!fullscreen) {
const r = el.getBoundingClientRect();
dragOffset = { x: e.clientX - r.left, y: e.clientY - r.top };
el.style.transition = '';
}
el.setPointerCapture(e.pointerId);
el.style.transition = '';
e.preventDefault();
}
function onpointermove(e: PointerEvent) {
if (!dragging || !el) return;
if (!dragging || !el || fullscreen) return;
const x = e.clientX - dragOffset.x;
const y = e.clientY - dragOffset.y;
if (!dragMoved) {
@@ -140,23 +224,40 @@ export function createPip(opts: PipOptions = {}) {
const now = Date.now();
if (now - lastTapTime < doubleTapMs) {
lastTapTime = 0;
toggleEnlarged();
if (fullscreen) {
toggleFullscreen(); // exit fullscreen
} else {
toggleEnlarged();
}
return;
}
lastTapTime = now;
if (fullscreenEnabled) {
// Delayed single tap: toggle controls (skipped if double-tap follows)
const tapTime = now;
setTimeout(() => {
if (lastTapTime === tapTime) showControls = !showControls;
}, doubleTapMs);
}
return;
}
const r = el.getBoundingClientRect();
snapToCorner(el, nearestCorner(r.left, r.top, el));
if (!fullscreen) {
const r = el.getBoundingClientRect();
snapToCorner(el, nearestCorner(r.left, r.top, el));
}
}
return {
get corner() { return corner; },
get dragging() { return dragging; },
get enlarged() { return enlarged; },
get fullscreen() { return fullscreen; },
get showControls() { return showControls; },
show,
hide,
reposition,
toggleFullscreen,
onpointerdown,
onpointermove,
onpointerup