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 (
{
- 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 (