UNPKG

@zoom-image/core

Version:
373 lines (371 loc) 13.8 kB
import { getSourceImage, makeMaybeCallFunction, enableScroll, clamp, disableScroll, computeZoomGesture } from './chunk-5KET5YPV.mjs'; import { createStore } from '@namnode/store'; 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 = createStore(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 controller = new AbortController(); const { signal } = controller; container.addEventListener("wheel", handleWheel, { signal }); container.addEventListener("touchstart", handleTouchStart, { signal }); container.addEventListener("touchmove", handleTouchMove, { signal }); container.addEventListener("pointerdown", handlePointerDown, { signal }); container.addEventListener("pointerleave", handlePointerLeave, { signal }); container.addEventListener("pointermove", handlePointerMove, { signal }); container.addEventListener("pointerup", handlePointerUp, { signal }); container.addEventListener( "touchend", () => { enabledScroll = true; enableScroll(); }, { signal } ); if (store.getState().currentZoom !== defaultInitialState.currentZoom) { updateStateOnNewZoom(store.getState().currentZoom); updateZoom(); } return { cleanup() { controller.abort(); store.cleanup(); }, subscribe: store.subscribe, setState, getState: store.getState }; } export { createZoomImageWheel }; //# sourceMappingURL=out.js.map //# sourceMappingURL=chunk-HDXSJMON.mjs.map