MediaQuickview: supports grabbing

This commit is contained in:
thislight 2024-12-06 20:46:44 +08:00
parent 543e920e3a
commit ebf9283dc6
No known key found for this signature in database
GPG key ID: FCFE5192241CCD4E
2 changed files with 135 additions and 31 deletions

View file

@ -46,15 +46,18 @@
height: 100%;
max-width: 100vw;
max-height: 100vh;
contain: layout style size paint;
contain: strict;
display: grid;
place-items: center;
cursor: grab;
>* {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
object-position: center;
transform-origin: 0 0;
}
}
}
@ -66,4 +69,10 @@
}
}
}
&.moving {
>.Scaffold>.pages>.page {
cursor: grabbing;
}
}
}

View file

@ -1,5 +1,7 @@
import { createCallback, createSubRoot } from "@solid-primitives/rootless";
import {
batch,
createMemo,
createRenderEffect,
createSignal,
Index,
@ -16,6 +18,7 @@ import "./MediaQuickview.css";
import AppTopBar from "~material/AppTopBar";
import { IconButton } from "@suid/material";
import { Close } from "@suid/icons-material";
import { createStore, unwrap } from "solid-js/store";
function renderIsolateMediaQuickview(
each: QuickviewMedia[],
@ -53,35 +56,19 @@ export function createMediaQuickview() {
return createCallback(renderIsolateMediaQuickview);
}
function ImagePage(props: { src: string; alt?: string }) {
const [scale, setScale] = createSignal(1);
const [offsetX, setOffsetX] = createSignal(0);
const [offsetY, setOffsetY] = createSignal(0);
const onWheel = (event: WheelEvent & { currentTarget: HTMLElement }) => {
// This is a de-facto standard for scaling:
// Browsers will simulate ctrl + wheel for two point scaling gesture.
if (event.ctrlKey) {
event.preventDefault();
event.stopPropagation();
const offset = event.deltaY;
setScale((x) => x + offset / event.currentTarget.clientHeight);
}
};
function ImagePage(props: {
src: string;
alt?: string;
scale: number;
offsetX: number;
offsetY: number;
}) {
return (
<img
src={props.src}
alt={props.alt}
onWheel={onWheel}
style={{
transform: `scale(${scale()}) translateX(${offsetX()}) translateY(${offsetY()})`,
}}
onLoad={({ currentTarget }) => {
const { top, left, width, height } =
currentTarget.getBoundingClientRect();
transform: `scale(${props.scale}) translateX(${-props.offsetX}px) translateY(${-props.offsetY}px)`,
}}
></img>
);
@ -101,8 +88,13 @@ export type MediaQuickviewProps = {
onClose?(): void;
};
function clamp<T extends number>(value: T, min: T, max: T) {
return Math.max(Math.min(value, max), min);
}
const MediaQuickview: Component<MediaQuickviewProps> = (props) => {
let root: HTMLDialogElement;
const [lightOut, setLightOut] = createSignal(false);
function onDialogClick(
@ -123,6 +115,93 @@ const MediaQuickview: Component<MediaQuickviewProps> = (props) => {
}
}
const [transformations, setTransformations] = createStore(
[] as {
scale: number;
/**
* positive = the left edge move towards left
*/
offsetX: number;
/**
* positive = the top edge move towards top
*/
offsetY: number;
}[],
);
const transformationGetOrSetDefault = (index: number) => {
if (transformations.length <= index) {
setTransformations(index, {
scale: 1,
offsetX: 0,
offsetY: 0,
});
}
return transformations[index];
};
const onWheel = (
index: number,
event: WheelEvent & { currentTarget: HTMLElement },
) => {
// This is a de-facto standard for scaling:
// Browsers will simulate ctrl + wheel for two point scaling gesture on trackpad.
if (event.ctrlKey) {
event.preventDefault();
event.stopPropagation();
const { clientX, clientY, currentTarget, deltaY } = event;
const offset = -deltaY; // not reversed wheel: wheel up = scale +, wheel down = scale -
const scaleOffset = offset / currentTarget.clientHeight;
// Map the screen scale to the image viewport scale
const userOriginX = clientX - currentTarget.clientLeft;
const userOriginY = clientY - currentTarget.clientTop;
setTransformations(index, ({ scale, offsetX, offsetY }) => {
const nscale = scale + scaleOffset;
return {
offsetX: offsetX + (userOriginX * nscale - userOriginX),
offsetY: offsetY + (userOriginY * nscale - userOriginX),
scale: nscale,
};
});
}
};
const [isMoving, setIsMoving] = createSignal(false);
let movOriginX: number = 0,
movOriginY: number = 0;
const onMouseDown = (event: MouseEvent) => {
event.preventDefault();
setIsMoving(true);
movOriginX = event.clientX;
movOriginY = event.clientY;
};
const onMouseMove = (index: number, event: MouseEvent) => {
if (!isMoving()) {
return;
}
const dx = movOriginX - event.clientX;
const dy = movOriginY - event.clientY ;
setTransformations(index, ({ offsetX, offsetY }) => {
return {
offsetX: offsetX + dx,
offsetY: offsetY + dy,
};
});
movOriginX = event.clientX;
movOriginY = event.clientY;
};
const onMouseUp = (event: MouseEvent) => {
setIsMoving(false);
};
return (
<dialog
ref={(e) => {
@ -132,7 +211,7 @@ const MediaQuickview: Component<MediaQuickviewProps> = (props) => {
});
}}
class="MediaQuickview"
classList={{ lightout: lightOut() }}
classList={{ lightout: lightOut(), moving: isMoving() }}
onClose={props.onClose}
onCancel={props.onClose}
onClick={onDialogClick}
@ -140,7 +219,11 @@ const MediaQuickview: Component<MediaQuickviewProps> = (props) => {
<Scaffold
topbar={
<AppTopBar>
<IconButton color="inherit" onClick={(e) => root.close()}>
<IconButton
color="inherit"
onClick={(e) => root.close()}
disableFocusRipple
>
<Close />
</IconButton>
</AppTopBar>
@ -160,10 +243,22 @@ const MediaQuickview: Component<MediaQuickviewProps> = (props) => {
<Index each={props.each}>
{(item, index) => {
return (
<section class="page">
<section
class="page"
onWheel={[onWheel, index]}
onMouseDown={onMouseDown}
onMouseMove={[onMouseMove, index]}
onMouseUp={onMouseUp}
>
<Switch>
<Match when={item().cat === "image"}>
<ImagePage src={item().src} alt={item().alt} />
<ImagePage
src={item().src}
alt={item().alt}
scale={transformationGetOrSetDefault(index).scale}
offsetX={transformationGetOrSetDefault(index).offsetX}
offsetY={transformationGetOrSetDefault(index).offsetY}
/>
</Match>
</Switch>
</section>