import { createSignal, untrack } from "solid-js"; import { clamp } from "../../utils"; const MIN_SCALE = 1; const MAX_SCALE = 5; const CLOSE_SCALE = 1.02; export type ZoomOverlayImage = { pageNum: number; imageUrl: string; width: number | null; height: number | null; }; type ZoomPinchStart = { centerX: number; centerY: number; }; type ZoomPinchMove = ZoomPinchStart & { scale: number; }; type ZoomReset = ZoomPinchStart & { scale: number; }; type ZoomDragMove = { dx: number; dy: number; }; type ZoomWheelMove = ZoomPinchStart & { delta: number; }; export type ZoomOverlayActions = { endPinch: () => void; moveDrag: (move: ZoomDragMove) => void; movePinch: (pinch: ZoomPinchMove) => void; moveWheel: (wheel: ZoomWheelMove) => void; reset: (reset: ZoomReset) => void; startDrag: () => void; startPinch: (pinch: ZoomPinchStart) => void; }; export function ZoomOverlay(props: { actionsRef: (actions: ZoomOverlayActions) => void; image: ZoomOverlayImage | null; onClose: () => void; }) { const [transform, setTransform] = createSignal("translate3d(0px, 0px, 0) scale(1)"); let element!: HTMLDivElement; let scale = 1; let requestedScale = 1; let closeScale = CLOSE_SCALE; let minScale = MIN_SCALE; let maxScale = MAX_SCALE; let offsetX = 0; let offsetY = 0; let pinchStartScale = 1; let pinchStartOffsetX = 0; let pinchStartOffsetY = 0; let pinchStartCenterX = 0; let pinchStartCenterY = 0; let dragStartOffsetX = 0; let dragStartOffsetY = 0; const renderTransform = () => { setTransform(`translate3d(${offsetX}px, ${offsetY}px, 0) scale(${scale})`); }; const startPinch = (pinch: ZoomPinchStart) => { pinchStartScale = scale; pinchStartOffsetX = offsetX; pinchStartOffsetY = offsetY; pinchStartCenterX = pinch.centerX; pinchStartCenterY = pinch.centerY; }; const actions: ZoomOverlayActions = { reset(reset): void { scale = Math.max(0.01, reset.scale); requestedScale = scale; closeScale = scale * CLOSE_SCALE; minScale = Math.min(MIN_SCALE, scale); maxScale = Math.max(MAX_SCALE, scale * MAX_SCALE); offsetX = 0; offsetY = 0; startPinch(reset); renderTransform(); }, startPinch, movePinch(pinch): void { if (!props.image) { return; } requestedScale = pinchStartScale * pinch.scale; scale = clamp(requestedScale, minScale, maxScale); const rect = element.getBoundingClientRect(); const viewportCenterX = rect.left + rect.width / 2; const viewportCenterY = rect.top + rect.height / 2; const ratio = scale / pinchStartScale; offsetX = pinch.centerX - viewportCenterX - (pinchStartCenterX - viewportCenterX - pinchStartOffsetX) * ratio; offsetY = pinch.centerY - viewportCenterY - (pinchStartCenterY - viewportCenterY - pinchStartOffsetY) * ratio; renderTransform(); }, moveWheel(wheel): void { if (!props.image) { return; } const nextScale = clamp( scale * Math.exp(-clamp(wheel.delta, -100, 100) * 0.0025), minScale, maxScale, ); if (nextScale === scale) { return; } const rect = element.getBoundingClientRect(); const viewportCenterX = rect.left + rect.width / 2; const viewportCenterY = rect.top + rect.height / 2; const ratio = nextScale / scale; offsetX = wheel.centerX - viewportCenterX - (wheel.centerX - viewportCenterX - offsetX) * ratio; offsetY = wheel.centerY - viewportCenterY - (wheel.centerY - viewportCenterY - offsetY) * ratio; scale = nextScale; requestedScale = nextScale; renderTransform(); }, endPinch(): void { if (requestedScale <= closeScale) { props.onClose(); return; } renderTransform(); }, startDrag(): void { dragStartOffsetX = offsetX; dragStartOffsetY = offsetY; }, moveDrag(move): void { if (!props.image) { return; } offsetX = dragStartOffsetX + move.dx; offsetY = dragStartOffsetY + move.dy; renderTransform(); }, }; untrack(() => props.actionsRef(actions)); return ( ); }