From c1467ee32afccdecd5bfa9df0826425568676ad3 Mon Sep 17 00:00:00 2001 From: thislight Date: Fri, 6 Dec 2024 20:46:44 +0800 Subject: [PATCH] MediaQuickview: supports grabbing --- src/platform/MediaQuickview.css | 15 +++- src/platform/MediaQuickview.tsx | 151 ++++++++++++++++++++++++++------ 2 files changed, 135 insertions(+), 31 deletions(-) diff --git a/src/platform/MediaQuickview.css b/src/platform/MediaQuickview.css index b1ceb07..0161fcf 100644 --- a/src/platform/MediaQuickview.css +++ b/src/platform/MediaQuickview.css @@ -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; + } + } } \ No newline at end of file diff --git a/src/platform/MediaQuickview.tsx b/src/platform/MediaQuickview.tsx index 2a9d7e1..74fabd2 100644 --- a/src/platform/MediaQuickview.tsx +++ b/src/platform/MediaQuickview.tsx @@ -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 ( {props.alt} { - const { top, left, width, height } = - currentTarget.getBoundingClientRect(); + transform: `scale(${props.scale}) translateX(${-props.offsetX}px) translateY(${-props.offsetY}px)`, }} > ); @@ -101,8 +88,13 @@ export type MediaQuickviewProps = { onClose?(): void; }; +function clamp(value: T, min: T, max: T) { + return Math.max(Math.min(value, max), min); +} + const MediaQuickview: Component = (props) => { let root: HTMLDialogElement; + const [lightOut, setLightOut] = createSignal(false); function onDialogClick( @@ -123,6 +115,93 @@ const MediaQuickview: Component = (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 ( { @@ -132,7 +211,7 @@ const MediaQuickview: Component = (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 = (props) => { - root.close()}> + root.close()} + disableFocusRipple + > @@ -160,10 +243,22 @@ const MediaQuickview: Component = (props) => { {(item, index) => { return ( -
+
- +