UNPKG

@zoom-image/core

Version:
472 lines (466 loc) 17.7 kB
var ZoomImage = (function (exports) { 'use strict'; // ../../node_modules/.pnpm/@namnode+store@0.1.0/node_modules/@namnode/store/dist/chunk-TZNK2OF3.mjs function f(o) { let r = /* @__PURE__ */ new Set(), s = false, a = o, e, c = (t = {}) => { e = { ...e, ...t }, i(); }, i = () => { if (s) return; let t = false; if (e) { for (let n in e) if (a[n] !== e[n]) { t = true; break; } } t && (a = { ...a, ...e }, r.forEach((n) => n({ state: a, updatedProperties: e })), e = void 0); }; return { subscribe: (t) => (r.add(t), () => { r.delete(t); }), cleanup: () => r.clear(), getState: () => a, setState: c, batch: (t) => { s = true, t(), s = false, i(); } }; } // src/utils.ts function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); } function preventDefault(event) { event.preventDefault(); } var keySet = /* @__PURE__ */ new Set(["ArrowUp", "ArrowRight", "ArrowDown", "ArrowLeft"]); function preventDefaultForScrollKeys(event) { if (keySet.has(event.key)) { preventDefault(event); return false; } } var controller = new AbortController(); var signal = controller.signal; function disableScroll() { window.addEventListener("DOMMouseScroll", preventDefault, { signal }); window.addEventListener("wheel", preventDefault, { passive: false, signal }); window.addEventListener("touchmove", preventDefault, { passive: false, signal }); window.addEventListener("keydown", preventDefaultForScrollKeys, { signal }); } function enableScroll() { controller?.abort(); } function getSourceImage(container) { if (!container) { throw new Error("Please specify a container for the zoom image"); } const sourceImgElement = container.querySelector("img"); if (!sourceImgElement) { throw new Error("Please place an image inside the container"); } return sourceImgElement; } function getPointersCenter(first, second) { return { x: (first.x + second.x) / 2, y: (first.y + second.y) / 2 }; } function computeZoomGesture(prev, curr) { const prevCenter = getPointersCenter(prev[0], prev[1]); const currCenter = getPointersCenter(curr[0], curr[1]); const centerDist = { x: currCenter.x - prevCenter.x, y: currCenter.y - prevCenter.y }; const prevDistance = Math.hypot(prev[0].x - prev[1].x, prev[0].y - prev[1].y); const currDistance = Math.hypot(curr[0].x - curr[1].x, curr[0].y - curr[1].y); let scale = currDistance / prevDistance; const eps = 1e-5; if (Math.abs(scale - 1) < eps) { scale = 1 + eps; } return { scale, center: { // We shift the zoom center away such that the translation part of the gesture // is also captured by the zoom operation. x: prevCenter.x + centerDist.x / (1 - scale), y: prevCenter.y + centerDist.y / (1 - scale) } }; } function makeMaybeCallFunction(predicateFn, fn) { return (arg) => { if (predicateFn()) { fn(arg); } }; } // src/createZoomImageWheel.ts var ZOOM_DELTA = 0.5; var defaultInitialState = { currentZoom: 1, enable: true, currentPositionX: 0, currentPositionY: 0, currentRotation: 0 }; var defaultShouldZoomOnSingleTouch = () => true; function createZoomImageWheel(container, options = {}) { const sourceImgElement = getSourceImage(container); const finalOptions = { maxZoom: options.maxZoom || 4, wheelZoomRatio: options.wheelZoomRatio || 0.1, dblTapAnimationDuration: options.dblTapAnimationDuration || 300, initialState: { ...defaultInitialState, ...options.initialState }, shouldZoomOnSingleTouch: options.shouldZoomOnSingleTouch || defaultShouldZoomOnSingleTouch }; const store = f(finalOptions.initialState); const checkDimensionSwitched = () => { return [90, 270].includes(store.getState().currentRotation % 360); }; const calculatePositionX = (newPositionX, currentZoom) => { if (newPositionX > 0) return 0; const width = container.clientWidth; if (newPositionX + width * currentZoom < width) return -width * (currentZoom - 1); return newPositionX; }; const calculatePositionY = (newPositionY, currentZoom) => { if (newPositionY > 0) return 0; const height = container.clientHeight; if (newPositionY + height * currentZoom < height) return -height * (currentZoom - 1); return newPositionY; }; const updateStateOnNewZoom = (currentZoom) => { const zoomPointX = container.clientWidth / 2; const zoomPointY = container.clientHeight / 2; const isDimensionSwitched = checkDimensionSwitched(); const currentState = store.getState(); const zoomX = isDimensionSwitched ? currentState.currentPositionY : currentState.currentPositionX; const zoomY = isDimensionSwitched ? currentState.currentPositionX : currentState.currentPositionY; const zoomTargetX = (zoomPointX - zoomX) / currentState.currentZoom; const zoomTargetY = (zoomPointY - zoomY) / currentState.currentZoom; store.setState({ currentZoom, currentPositionX: calculatePositionX(-zoomTargetX * currentZoom + zoomPointX, currentZoom), currentPositionY: calculatePositionY(-zoomTargetY * currentZoom + zoomPointY, currentZoom) }); }; let prevTwoPositions = null; let enabledScroll = true; const pointerMap = /* @__PURE__ */ new Map(); let lastPositionX = 0; let lastPositionY = 0; let startX = 0; let startY = 0; container.style.overflow = "hidden"; sourceImgElement.style.transformOrigin = "0 0"; function updateZoom() { const currentState = store.getState(); sourceImgElement.style.transform = `translate(${currentState.currentPositionX}px, ${currentState.currentPositionY}px) scale(${currentState.currentZoom})`; container.style.rotate = `${currentState.currentRotation}deg`; } function setState(newState) { store.batch(() => { const currentState = store.getState(); if (typeof newState.enable === "boolean" && newState.enable !== currentState.enable) { store.setState({ enable: newState.enable }); if (!newState.enable) { return; } } if (typeof newState.currentRotation === "number") { const newCurrentRotation = newState.currentRotation; store.setState({ currentRotation: newCurrentRotation }); } if (typeof newState.currentZoom === "number" && newState.currentZoom !== currentState.currentZoom) { const newCurrentZoom = clamp(newState.currentZoom, 1, finalOptions.maxZoom); if (newCurrentZoom === currentState.currentZoom) { return; } updateStateOnNewZoom(newCurrentZoom); } }); updateZoom(); } function processZoomWheel({ delta, x, y }) { const containerRect = container.getBoundingClientRect(); const currentState = store.getState(); const isDimensionSwitched = checkDimensionSwitched(); let zoomPointX = -1; let zoomPointY = -1; switch (currentState.currentRotation % 360) { case 0: zoomPointX = x - containerRect.left; zoomPointY = y - containerRect.top; break; case 90: zoomPointX = Math.abs(x - containerRect.right); zoomPointY = Math.abs(y - containerRect.top); break; case 180: zoomPointX = Math.abs(x - containerRect.right); zoomPointY = Math.abs(y - containerRect.bottom); break; case 270: zoomPointX = Math.abs(x - containerRect.left); zoomPointY = Math.abs(y - containerRect.bottom); break; } const zoomX = isDimensionSwitched ? currentState.currentPositionY : currentState.currentPositionX; const zoomY = isDimensionSwitched ? currentState.currentPositionX : currentState.currentPositionY; const zoomTargetX = (zoomPointX - zoomX) / currentState.currentZoom; const zoomTargetY = (zoomPointY - zoomY) / currentState.currentZoom; const newCurrentZoom = clamp( currentState.currentZoom + delta * finalOptions.wheelZoomRatio * currentState.currentZoom, 1, finalOptions.maxZoom ); const newX = calculatePositionX(-zoomTargetX * newCurrentZoom + zoomPointX, newCurrentZoom); const newY = calculatePositionY(-zoomTargetY * newCurrentZoom + zoomPointY, newCurrentZoom); store.setState({ currentZoom: newCurrentZoom, currentPositionX: isDimensionSwitched ? newY : newX, currentPositionY: isDimensionSwitched ? newX : newY }); } function updatePositionsForSinglePointerFlow() { if (pointerMap.size === 1) { const { x, y } = pointerMap.values().next().value; const isDimensionSwitched = checkDimensionSwitched(); startX = isDimensionSwitched ? y : x; startY = isDimensionSwitched ? x : y; } const currentState = store.getState(); lastPositionX = currentState.currentPositionX; lastPositionY = currentState.currentPositionY; } function _handleWheel(event) { event.preventDefault(); if (store.getState().currentZoom === finalOptions.maxZoom && event.deltaY < 0) { return; } const delta = -clamp(event.deltaY, -ZOOM_DELTA, ZOOM_DELTA); processZoomWheel({ delta, x: event.clientX, y: event.clientY }); updateZoom(); updatePositionsForSinglePointerFlow(); } function _handlePointerMove(event) { event.preventDefault(); const { clientX, clientY, pointerId } = event; for (const [cachedPointerId] of pointerMap.entries()) { if (cachedPointerId === pointerId) { pointerMap.set(cachedPointerId, { x: clientX, y: clientY }); } } const { currentZoom, currentRotation } = store.getState(); if (pointerMap.size === 1 && currentZoom !== 1) { const isDimensionSwitched = checkDimensionSwitched(); const normalizedClientX = isDimensionSwitched ? clientY : clientX; const normalizedClientY = isDimensionSwitched ? clientX : clientY; let offsetX = -1; let offsetY = -1; switch (currentRotation % 360) { case 0: offsetX = normalizedClientX - startX; offsetY = normalizedClientY - startY; break; case 90: offsetX = normalizedClientX - startX; offsetY = startY - normalizedClientY; break; case 180: offsetX = startX - normalizedClientX; offsetY = startY - normalizedClientY; break; case 270: offsetX = startX - normalizedClientX; offsetY = normalizedClientY - startY; break; } store.setState({ currentPositionX: calculatePositionX(lastPositionX + offsetX, currentZoom), currentPositionY: calculatePositionY(lastPositionY + offsetY, currentZoom) }); updateZoom(); } } const animationState = { startTimestamp: null, // the state at the start of the zoom animation start: { x: 0, y: 0, zoom: 0 }, // the target state at the end of the zoom animation target: { x: 0, y: 0, zoom: 0 } }; function animateZoom(touchCoordinate) { const currentState = store.getState(); animationState.startTimestamp = null; animationState.start = { x: currentState.currentPositionX, y: currentState.currentPositionY, zoom: currentState.currentZoom }; if (currentState.currentZoom > 1) { animationState.target = { x: 0, y: 0, zoom: 1 }; } else { animationState.target = { zoom: finalOptions.maxZoom, x: touchCoordinate.x * (1 - finalOptions.maxZoom), y: touchCoordinate.y * (1 - finalOptions.maxZoom) }; } function lerp(a, b, t) { return a * (1 - t) + b * t; } function frame(timestamp) { if (animationState.startTimestamp === null) { animationState.startTimestamp = timestamp; } let t = (timestamp - animationState.startTimestamp) / finalOptions.dblTapAnimationDuration; if (t > 1) { t = 1; } store.setState({ currentPositionX: lerp(animationState.start.x, animationState.target.x, t), currentPositionY: lerp(animationState.start.y, animationState.target.y, t), currentZoom: lerp(animationState.start.zoom, animationState.target.zoom, t) }); updateZoom(); if (t < 1) { requestAnimationFrame(frame); } } requestAnimationFrame(frame); } let touchTimer = null; const durationBetweenTap = 300; function _handleTouchStart(event) { if (event.touches.length > 1) { return; } if (touchTimer === null) { touchTimer = setTimeout(() => { touchTimer = null; }, durationBetweenTap); } else { clearTimeout(touchTimer); touchTimer = null; const rect = container.getBoundingClientRect(); const touch = event.touches[0]; animateZoom({ x: touch.clientX - rect.left, y: touch.clientY - rect.top }); return; } } function _handleTouchMove(event) { if (finalOptions.shouldZoomOnSingleTouch()) event.preventDefault(); if (event.touches.length > 1) { event.preventDefault(); const currentTwoPositions = [...event.touches].map((t) => ({ x: t.clientX, y: t.clientY })); if (prevTwoPositions !== null) { const { scale, center } = computeZoomGesture(prevTwoPositions, currentTwoPositions); processZoomWheel({ delta: Math.log(scale) / finalOptions.wheelZoomRatio, ...center }); } prevTwoPositions = currentTwoPositions; updateZoom(); return; } } function _handlePointerDown(event) { if (event.pointerType === "touch" && !finalOptions.shouldZoomOnSingleTouch()) return; event.preventDefault(); if (pointerMap.size === 2) { return; } if (enabledScroll) { disableScroll(); enabledScroll = false; } const { clientX, clientY, pointerId } = event; const currentState = store.getState(); lastPositionX = currentState.currentPositionX; lastPositionY = currentState.currentPositionY; const isDimensionSwitched = checkDimensionSwitched(); startX = isDimensionSwitched ? clientY : clientX; startY = isDimensionSwitched ? clientX : clientY; pointerMap.set(pointerId, { x: clientX, y: clientY }); } function _handlePointerUp(event) { event.preventDefault(); pointerMap.delete(event.pointerId); if (pointerMap.size < 2) { prevTwoPositions = null; } if (pointerMap.size === 0 && !enabledScroll) { enableScroll(); enabledScroll = true; } updatePositionsForSinglePointerFlow(); } function _handlePointerLeave(event) { event.preventDefault(); pointerMap.delete(event.pointerId); prevTwoPositions = null; if (!enabledScroll) { enableScroll(); enabledScroll = true; } } function checkZoomEnabled() { return store.getState().enable; } const handleWheel = makeMaybeCallFunction(checkZoomEnabled, _handleWheel); const handlePointerDown = makeMaybeCallFunction(checkZoomEnabled, _handlePointerDown); const handlePointerLeave = makeMaybeCallFunction(checkZoomEnabled, _handlePointerLeave); const handlePointerMove = makeMaybeCallFunction(checkZoomEnabled, _handlePointerMove); const handlePointerUp = makeMaybeCallFunction(checkZoomEnabled, _handlePointerUp); const handleTouchStart = makeMaybeCallFunction(checkZoomEnabled, _handleTouchStart); const handleTouchMove = makeMaybeCallFunction(checkZoomEnabled, _handleTouchMove); const controller2 = new AbortController(); const { signal: signal2 } = controller2; container.addEventListener("wheel", handleWheel, { signal: signal2 }); container.addEventListener("touchstart", handleTouchStart, { signal: signal2 }); container.addEventListener("touchmove", handleTouchMove, { signal: signal2 }); container.addEventListener("pointerdown", handlePointerDown, { signal: signal2 }); container.addEventListener("pointerleave", handlePointerLeave, { signal: signal2 }); container.addEventListener("pointermove", handlePointerMove, { signal: signal2 }); container.addEventListener("pointerup", handlePointerUp, { signal: signal2 }); container.addEventListener( "touchend", () => { enabledScroll = true; enableScroll(); }, { signal: signal2 } ); if (store.getState().currentZoom !== defaultInitialState.currentZoom) { updateStateOnNewZoom(store.getState().currentZoom); updateZoom(); } return { cleanup() { controller2.abort(); store.cleanup(); }, subscribe: store.subscribe, setState, getState: store.getState }; } exports.createZoomImageWheel = createZoomImageWheel; return exports; })({}); //# sourceMappingURL=out.js.map //# sourceMappingURL=createZoomImageWheel.global.js.map