mirror of
https://github.com/mickael-kerjean/filestash
synced 2025-12-06 08:22:24 +01:00
chore (image): ux improvements for image viewer
This commit is contained in:
parent
922285e2c6
commit
e84c12293d
3 changed files with 109 additions and 10 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}),
|
||||
)),
|
||||
)),
|
||||
));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue