chore (image): ux improvements for image viewer

This commit is contained in:
MickaelK 2025-05-15 17:26:34 +10:00
parent 922285e2c6
commit e84c12293d
3 changed files with 109 additions and 10 deletions

View file

@ -22,12 +22,13 @@
.component_pager a svg {
transition: opacity 0.2s ease;
width: 37px;
padding: 5px;
padding: 8px;
background: var(--dark);
border-radius: 50%;
margin: auto;
opacity: 0.75;
}
.component_pager a svg:hover {
.touch-no .component_pager a svg:hover {
opacity: 1;
box-shadow: 0px 0px 5px rgba(255, 255, 255, 0.1);
}

View file

@ -30,14 +30,14 @@ export default async function(render, { $img }) {
const $page = createFragment(`
<div class="component_pager left hidden">
<a data-link>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="15 18 9 12 15 6"/>
</svg>
</a>
</div>
<div class="component_pager right hidden">
<a>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</a>
@ -85,11 +85,15 @@ function updateDOM({ $el, name, $img }) {
if (e.target.hasAttribute("data-link")) return;
e.preventDefault(); e.stopPropagation();
const sgn = $el.classList.contains("left") ? +1 : -1;
await animate($img, { keyframes: slideXOut(sgn * 10), time: 200 });
await animate($img, {
keyframes: slideXOut(sgn * 25),
time: 100,
easing: "ease-in",
});
$link.setAttribute("data-link", "true");
$link.click();
};
$link.setAttribute("href", "/view" + join(location, getCurrentPath() + "/../" + name));
$link.setAttribute("href", "/view" + join(location, getCurrentPath() + "/../" + name)); // TODO: name with "#" issue
$el.classList.remove("hidden");
}

View file

@ -1,12 +1,14 @@
import { createElement } from "../../../lib/skeleton/index.js";
import rxjs, { effect } from "../../../lib/rx.js";
import { qs } from "../../../lib/dom.js";
import { animate, opacityOut } from "../../../lib/animate.js";
import { loadJS } from "../../../helpers/loader.js";
export default function ({ $img, $page, $menubar }) {
export default function ({ $img, $page }) {
const $navigation = qs($page, `[data-bind="component_navigation"]`);
initZoomDesktop({ $img, $navigation });
initZoomMobile({ $img, $navigation });
}
function initZoomMobile({ $img, $navigation }) {
const state = {
active: false,
x: null,
@ -48,3 +50,95 @@ export default function ({ $img, $page, $menubar }) {
$navigation.classList.remove("hidden");
})));
}
function initZoomDesktop({ $img, $navigation }) {
const params = {
scale: 1,
x: 0,
y: 0,
};
$img.style.transformOrigin = "0 0";
$img.style.transition = "";
const apply = (transitionTime = null) => {
if (transitionTime !== null) $img.style.transition = `${transitionTime}ms ease transform`;
$img.style.transform = `translate(${params.x}px,${params.y}px) scale(${params.scale})`;
};
// zoom in / out using either: wheel, double click, keyboard shortcut
effect(rxjs.merge(
rxjs.fromEvent($img.parentElement, "dblclick").pipe(
rxjs.filter((e) => e.target === $img),
rxjs.map((e) => ({ scale: 2, clientX: e.clientX, clientY: e.clientY })),
),
rxjs.fromEvent($img.parentElement, "wheel", { passive: true }).pipe(
rxjs.throttleTime(100),
rxjs.map((e) => ({ scale: Math.exp(-e.deltaY / 300), clientX: e.clientX, clientY: e.clientY })),
),
rxjs.fromEvent(window, "keydown").pipe(
rxjs.filter(({ key }) => ["Escape", "+", "-", "ArrowUp", "ArrowDown"].indexOf(key) !== -1),
rxjs.withLatestFrom(rxjs.fromEvent(window, "mousemove").pipe(
rxjs.startWith({ clientX: null, clientY: null }),
)),
rxjs.mergeMap(([{ key }, { clientX, clientY }]) => {
let scale = 0;
if (["+", "ArrowUp"].indexOf(key) !== -1) scale = 3/2;
else if (["-", "ArrowDown"].indexOf(key) !== -1) scale = 2/3;
return rxjs.of({ clientX, clientY, scale });
}),
),
).pipe(rxjs.tap(({ clientX, clientY, scale }) => {
$navigation.classList.add("hidden");
const rect = $img.getBoundingClientRect();
const ox = clientX !== null ? clientX - rect.left : rect.width / 2;
const oy = clientY !== null ? clientY - rect.top : rect.height / 2;
let ns = params.scale * scale;
if (ns > 20) return;
params.x = ns <= 1 ? 0: params.x+(1-scale)*ox;
params.y = ns <= 1 ? 0: params.y+(1-scale)*oy;
params.scale = ns < 1 ? 1: ns;
if (params.scale === 1) $navigation.classList.remove("hidden");
apply(ns < 1 ? 500: 200);
})));
// grab / panning
effect(rxjs.fromEvent($img.parentElement, "mousedown").pipe(
rxjs.filter((event) => event.target === $img && event.button === 0 && params.scale > 1),
rxjs.tap(() => {
$img.style.cursor = "move";
$navigation.classList.add("hidden");
}),
rxjs.switchMap((event) => rxjs.fromEvent(window, "mousemove").pipe(
rxjs.pairwise(),
rxjs.takeUntil(rxjs.fromEvent(window, "mouseup")),
rxjs.map(([prev, curr]) => {
const dt = curr.timeStamp - prev.timeStamp;
const dx = curr.clientX - prev.clientX;
const dy = curr.clientY - prev.clientY;
params.x += dx;
params.y += dy;
return [dx/dt, dy/dt];
}),
rxjs.tap(() => apply(0)),
rxjs.startWith([0, 0]),
rxjs.last(),
rxjs.switchMap(([velocityX, velocityY]) => rxjs.EMPTY.pipe(
rxjs.finalize(() => $img.style.cursor = "default"),
rxjs.finalize((a) => {
const decay = 0.8;
const frameMs = 16;
const stopV = 0.05;
const speed = Math.hypot(velocityX, velocityY);
if (speed < stopV) return;
const nFramesTillStop = Math.ceil(Math.log(stopV / speed) / Math.log(decay));
const geosum = (decay * (1 - Math.pow(decay, nFramesTillStop))) / (1 - decay);
params.x += geosum * velocityX * frameMs;
params.y += geosum * velocityY * frameMs;
$img.style.transition = `transform ${nFramesTillStop * frameMs}ms cubic-bezier(0,0,0,1)`;
apply();
}),
)),
)),
));
}