rosary: add mystery images for all four mystery types with PiP fullscreen
All checks were successful
CI / update (push) Successful in 1m29s
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user