mirror of
https://github.com/mickael-kerjean/filestash
synced 2025-12-06 08:22:24 +01:00
fix (imageviewer): better handling of zoom in viewer
This commit is contained in:
parent
e84c12293d
commit
ea30adad2f
1 changed files with 101 additions and 113 deletions
|
|
@ -4,141 +4,129 @@ import { qs } from "../../../lib/dom.js";
|
||||||
export default function ({ $img, $page }) {
|
export default function ({ $img, $page }) {
|
||||||
const $navigation = qs($page, `[data-bind="component_navigation"]`);
|
const $navigation = qs($page, `[data-bind="component_navigation"]`);
|
||||||
|
|
||||||
initZoomDesktop({ $img, $navigation });
|
effect(rxjs.merge(...builder({ $img })).pipe(
|
||||||
initZoomMobile({ $img, $navigation });
|
rxjs.tap(() => $navigation.classList.add("hidden")),
|
||||||
|
rxjs.scan((state, { clientX, clientY, moveX, moveY, scale, duration }) => {
|
||||||
|
state.x += moveX ?? 0;
|
||||||
|
state.y += moveY ?? 0;
|
||||||
|
const next = Math.min(20, Math.max(1, state.scale * (scale ?? 1)));
|
||||||
|
if (next > 1) {
|
||||||
|
const rect = $img.getBoundingClientRect();
|
||||||
|
const ox = (clientX ?? rect.left + rect.width / 2) - rect.left;
|
||||||
|
const oy = (clientY ?? rect.top + rect.height / 2) - rect.top;
|
||||||
|
const f = next / state.scale;
|
||||||
|
state.x += (1 - f) * ox;
|
||||||
|
state.y += (1 - f) * oy;
|
||||||
|
} else {
|
||||||
|
state.x = 0;
|
||||||
|
state.y = 0;
|
||||||
|
}
|
||||||
|
state.scale = next;
|
||||||
|
state.duration = duration ?? (next === 1 ? 500 : 200);
|
||||||
|
return state;
|
||||||
|
}, { scale: 1, x: 0, y: 0, duration: 0 }),
|
||||||
|
rxjs.tap(({ scale, x, y, duration }) => {
|
||||||
|
$img.style.transition = `transform ${duration}ms ease`;
|
||||||
|
$img.style.transform = `translate(${x}px,${y}px) scale(${scale})`;
|
||||||
|
if (scale === 1) $navigation.classList.remove("hidden");
|
||||||
|
}),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
function initZoomMobile({ $img, $navigation }) {
|
function builder({ $img }) {
|
||||||
const state = {
|
|
||||||
active: false,
|
|
||||||
x: null,
|
|
||||||
y: null,
|
|
||||||
distance: null,
|
|
||||||
};
|
|
||||||
const distance = ({ touches }) => Math.hypot(touches[0].pageX - touches[1].pageX, touches[0].pageY - touches[1].pageY);
|
|
||||||
|
|
||||||
effect(rxjs.fromEvent($img.parentElement, "touchstart").pipe(rxjs.tap((event) => {
|
|
||||||
if (event.touches.length < 2) return;
|
|
||||||
state.active = true;
|
|
||||||
event.preventDefault();
|
|
||||||
$img.style.transition = "0.1s ease transform";
|
|
||||||
$img.style.transformOrigin = "50% 50%";
|
|
||||||
$navigation.classList.add("hidden");
|
|
||||||
|
|
||||||
state.x = (event.touches[0].pageX + event.touches[1].pageX) / 2;
|
|
||||||
state.y = (event.touches[0].pageY + event.touches[1].pageY) / 2;
|
|
||||||
state.distance = distance(event);
|
|
||||||
})));
|
|
||||||
|
|
||||||
effect(rxjs.fromEvent($img.parentElement, "touchmove").pipe(rxjs.tap((event) => {
|
|
||||||
if (event.touches.length < 2) return;
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
let scale = distance(event) / state.distance;
|
|
||||||
if (scale < 1) scale = 1;
|
|
||||||
else if (scale > 20) scale = 20;
|
|
||||||
const deltaX = (((event.touches[0].pageX + event.touches[1].pageX) / 2) - state.x) * 2;
|
|
||||||
const deltaY = (((event.touches[0].pageY + event.touches[1].pageY) / 2) - state.y) * 2;
|
|
||||||
$img.style.transform = `translate3d(${deltaX}px, ${deltaY}px, 0) scale(${scale})`;
|
|
||||||
})));
|
|
||||||
|
|
||||||
effect(rxjs.fromEvent($img.parentElement, "touchend").pipe(rxjs.tap((event) => {
|
|
||||||
if (state.active === false) return;
|
|
||||||
state.active = true;
|
|
||||||
$img.style.transition = "0.3s ease transform";
|
|
||||||
$img.style.transform = "";
|
|
||||||
$navigation.classList.remove("hidden");
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
|
|
||||||
function initZoomDesktop({ $img, $navigation }) {
|
|
||||||
const params = {
|
|
||||||
scale: 1,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
$img.style.transformOrigin = "0 0";
|
$img.style.transformOrigin = "0 0";
|
||||||
$img.style.transition = "";
|
$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
|
return [
|
||||||
effect(rxjs.merge(
|
// zoom via double click
|
||||||
rxjs.fromEvent($img.parentElement, "dblclick").pipe(
|
rxjs.fromEvent($img.parentElement, "dblclick").pipe(
|
||||||
rxjs.filter((e) => e.target === $img),
|
rxjs.filter((e) => e.target === $img),
|
||||||
rxjs.map((e) => ({ scale: 2, clientX: e.clientX, clientY: e.clientY })),
|
rxjs.map((e) => ({ scale: 2, clientX: e.clientX, clientY: e.clientY })),
|
||||||
),
|
),
|
||||||
|
// zoom via scroll wheel
|
||||||
rxjs.fromEvent($img.parentElement, "wheel", { passive: true }).pipe(
|
rxjs.fromEvent($img.parentElement, "wheel", { passive: true }).pipe(
|
||||||
rxjs.throttleTime(100),
|
rxjs.throttleTime(100),
|
||||||
rxjs.map((e) => ({ scale: Math.exp(-e.deltaY / 300), clientX: e.clientX, clientY: e.clientY })),
|
rxjs.map((e) => ({ scale: Math.exp(-e.deltaY / 300), clientX: e.clientX, clientY: e.clientY })),
|
||||||
),
|
),
|
||||||
|
// zoom via keyboard shortcut
|
||||||
rxjs.fromEvent(window, "keydown").pipe(
|
rxjs.fromEvent(window, "keydown").pipe(
|
||||||
rxjs.filter(({ key }) => ["Escape", "+", "-", "ArrowUp", "ArrowDown"].indexOf(key) !== -1),
|
rxjs.filter(({ key }) => ["Escape", "+", "-", "ArrowUp", "ArrowDown"].indexOf(key) !== -1),
|
||||||
rxjs.withLatestFrom(rxjs.fromEvent(window, "mousemove").pipe(
|
rxjs.withLatestFrom(rxjs.fromEvent(window, "mousemove").pipe(
|
||||||
rxjs.startWith({ clientX: null, clientY: null }),
|
rxjs.startWith({ clientX: null, clientY: null }),
|
||||||
)),
|
)),
|
||||||
rxjs.mergeMap(([{ key }, { clientX, clientY }]) => {
|
rxjs.map(([{ key }, { clientX, clientY }]) => {
|
||||||
let scale = 0;
|
let scale = 0;
|
||||||
if (["+", "ArrowUp"].indexOf(key) !== -1) scale = 3/2;
|
if (["+", "ArrowUp"].indexOf(key) !== -1) scale = 3/2;
|
||||||
else if (["-", "ArrowDown"].indexOf(key) !== -1) scale = 2/3;
|
else if (["-", "ArrowDown"].indexOf(key) !== -1) scale = 2/3;
|
||||||
return rxjs.of({ clientX, clientY, scale });
|
return { clientX, clientY, scale };
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
).pipe(rxjs.tap(({ clientX, clientY, scale }) => {
|
// pinch zoom
|
||||||
$navigation.classList.add("hidden");
|
rxjs.fromEvent($img.parentElement, "touchstart").pipe(
|
||||||
const rect = $img.getBoundingClientRect();
|
rxjs.filter((e) => e.touches.length === 2),
|
||||||
const ox = clientX !== null ? clientX - rect.left : rect.width / 2;
|
rxjs.switchMap((event) => rxjs.fromEvent($img.parentElement, "touchmove").pipe(
|
||||||
const oy = clientY !== null ? clientY - rect.top : rect.height / 2;
|
rxjs.filter((event) => event.touches.length >= 2),
|
||||||
let ns = params.scale * scale;
|
rxjs.tap((event) => event.preventDefault()),
|
||||||
if (ns > 20) return;
|
rxjs.takeUntil(rxjs.fromEvent(window, "touchend")),
|
||||||
params.x = ns <= 1 ? 0: params.x+(1-scale)*ox;
|
rxjs.map((event) => ({
|
||||||
params.y = ns <= 1 ? 0: params.y+(1-scale)*oy;
|
clientX: (event.touches[0].pageX + event.touches[1].pageX) / 2,
|
||||||
params.scale = ns < 1 ? 1: ns;
|
clientY: (event.touches[0].pageY + event.touches[1].pageY) / 2,
|
||||||
if (params.scale === 1) $navigation.classList.remove("hidden");
|
distance: Math.hypot(
|
||||||
apply(ns < 1 ? 500: 200);
|
event.touches[0].pageX - event.touches[1].pageX,
|
||||||
})));
|
event.touches[0].pageY - event.touches[1].pageY,
|
||||||
|
),
|
||||||
// grab / panning
|
})),
|
||||||
effect(rxjs.fromEvent($img.parentElement, "mousedown").pipe(
|
rxjs.pairwise(),
|
||||||
rxjs.filter((event) => event.target === $img && event.button === 0 && params.scale > 1),
|
rxjs.map(([curr, prev]) => ({
|
||||||
rxjs.tap(() => {
|
clientX: curr.clientX,
|
||||||
$img.style.cursor = "move";
|
clientY: curr.clientY,
|
||||||
$navigation.classList.add("hidden");
|
scale: Math.sign(curr.distance - prev.distance) === 1 ? 0.95 : 1.025,
|
||||||
}),
|
moveX: prev.clientX - curr.clientX,
|
||||||
rxjs.switchMap((event) => rxjs.fromEvent(window, "mousemove").pipe(
|
moveY: prev.clientY - curr.clientY,
|
||||||
rxjs.pairwise(),
|
duration: 0,
|
||||||
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();
|
|
||||||
}),
|
|
||||||
)),
|
)),
|
||||||
)),
|
),
|
||||||
));
|
// grab and drag
|
||||||
|
rxjs.fromEvent($img.parentElement, "mousedown").pipe(
|
||||||
|
rxjs.filter(e => e.target === $img && e.button === 0),
|
||||||
|
rxjs.switchMap((down) => {
|
||||||
|
let prev = { x: down.clientX, y: down.clientY, t: down.timeStamp };
|
||||||
|
const move$ = rxjs.fromEvent(window, "mousemove").pipe(
|
||||||
|
rxjs.takeUntil(rxjs.fromEvent(window, "mouseup")),
|
||||||
|
rxjs.map(m => {
|
||||||
|
const dx = m.clientX - prev.x;
|
||||||
|
const dy = m.clientY - prev.y;
|
||||||
|
const dt = m.timeStamp - prev.t || 1;
|
||||||
|
prev = { x: m.clientX, y: m.clientY, t: m.timeStamp };
|
||||||
|
return { moveX: dx, moveY: dy, dt, duration: 0 };
|
||||||
|
}),
|
||||||
|
rxjs.tap(() => ($img.style.cursor = "move")),
|
||||||
|
rxjs.share(),
|
||||||
|
);
|
||||||
|
const $inertia = move$.pipe(
|
||||||
|
rxjs.startWith({ moveX: 0, moveY: 0, dt: 1 }),
|
||||||
|
rxjs.last(),
|
||||||
|
rxjs.tap(() => ($img.style.cursor = "default")),
|
||||||
|
rxjs.switchMap(({ moveX, moveY, dt }) => {
|
||||||
|
const DECAY = 0.8;
|
||||||
|
const FRAME = 16;
|
||||||
|
const STOPV = 0.05;
|
||||||
|
const vx = moveX / dt;
|
||||||
|
const vy = moveY / dt;
|
||||||
|
const speed = Math.hypot(vx, vy);
|
||||||
|
if (speed < STOPV) return rxjs.EMPTY;
|
||||||
|
const nFrames = Math.ceil(Math.log(STOPV / speed) / Math.log(DECAY));
|
||||||
|
const gsum = (DECAY * (1 - Math.pow(DECAY, nFrames))) / (1 - DECAY);
|
||||||
|
return rxjs.of({
|
||||||
|
moveX: vx * gsum * FRAME,
|
||||||
|
moveY: vy * gsum * FRAME,
|
||||||
|
duration: nFrames * FRAME
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return rxjs.merge(move$, $inertia);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue