UNPKG

react-zoom-pan-pinch

Version:
227 lines (198 loc) 6.01 kB
/* eslint-disable no-param-reassign */ import { roundNumber } from "../../utils"; import { BoundsType, PositionType, ReactZoomPanPinchContext, } from "../../models"; import { ComponentsSizesType } from "./bounds.types"; export function getComponentsSizes( wrapperComponent: HTMLDivElement, contentComponent: HTMLDivElement, newScale: number, ): ComponentsSizesType { const wrapperWidth = wrapperComponent.offsetWidth; const wrapperHeight = wrapperComponent.offsetHeight; const contentWidth = contentComponent.offsetWidth; const contentHeight = contentComponent.offsetHeight; const newContentWidth = contentWidth * newScale; const newContentHeight = contentHeight * newScale; const newDiffWidth = wrapperWidth - newContentWidth; const newDiffHeight = wrapperHeight - newContentHeight; return { wrapperWidth, wrapperHeight, newContentWidth, newDiffWidth, newContentHeight, newDiffHeight, }; } export const getBounds = ( wrapperWidth: number, newContentWidth: number, diffWidth: number, wrapperHeight: number, newContentHeight: number, diffHeight: number, centerZoomedOut: boolean, ): BoundsType => { const scaleWidthFactor = wrapperWidth > newContentWidth ? diffWidth * (centerZoomedOut ? 0.5 : 1) : 0; const scaleHeightFactor = wrapperHeight > newContentHeight ? diffHeight * (centerZoomedOut ? 0.5 : 1) : 0; const minPositionX = wrapperWidth - newContentWidth - scaleWidthFactor; const maxPositionX = scaleWidthFactor; const minPositionY = wrapperHeight - newContentHeight - scaleHeightFactor; const maxPositionY = scaleHeightFactor; return { minPositionX, maxPositionX, minPositionY, maxPositionY, scaleWidthFactor, scaleHeightFactor, }; }; export const calculateBounds = ( contextInstance: ReactZoomPanPinchContext, newScale: number, ): BoundsType => { const { wrapperComponent, contentComponent } = contextInstance; const { centerZoomedOut, disablePadding } = contextInstance.setup; if (!wrapperComponent || !contentComponent) { throw new Error("Components are not mounted"); } const { wrapperWidth, wrapperHeight, newContentWidth, newContentHeight, newDiffWidth, newDiffHeight, } = getComponentsSizes(wrapperComponent, contentComponent, newScale); const bounds = getBounds( wrapperWidth, newContentWidth, newDiffWidth, wrapperHeight, newContentHeight, newDiffHeight, Boolean(centerZoomedOut), ); const contentFitsCompletely = wrapperWidth >= newContentWidth && wrapperHeight >= newContentHeight; if (disablePadding && contentFitsCompletely && !centerZoomedOut) { bounds.minPositionX = 0; bounds.maxPositionX = 0; bounds.minPositionY = 0; bounds.maxPositionY = 0; } const { minPositionX: propMinX, maxPositionX: propMaxX, minPositionY: propMinY, maxPositionY: propMaxY, } = contextInstance.setup; // Explicit position props define content-space boundaries at scale=1. // Scale them so the same content region stays reachable at every zoom level. if (propMinX != null) { bounds.minPositionX = wrapperWidth * (1 - newScale) + propMinX * newScale; } if (propMaxX != null) { bounds.maxPositionX = propMaxX * newScale; } if (propMinY != null) { bounds.minPositionY = wrapperHeight * (1 - newScale) + propMinY * newScale; } if (propMaxY != null) { bounds.maxPositionY = propMaxY * newScale; } return bounds; }; export function clamp(v: number, min: number, max: number) { return Math.max(min, Math.min(v, max)); } // Based on @aholachek ;) // https://twitter.com/chpwn/status/285540192096497664 // iOS constant = 0.55 // https://medium.com/@nathangitter/building-fluid-interfaces-ios-swift-9732bb934bf5 function rubberband(distance: number, dimension: number, constant: number) { if (dimension === 0 || Math.abs(dimension) === Infinity) return distance ** (constant * 5); return (distance * dimension * constant) / (dimension + constant * distance); } export function rubberbandIfOutOfBounds( position: number, min: number, max: number, constant = 0.15, ) { if (constant === 0) return clamp(position, min, max); if (position < min) return -rubberband(min - position, max - min, constant) + min; if (position > max) return +rubberband(position - max, max - min, constant) + max; return position; } /** * Keeps value between given bounds, used for limiting view to given boundaries * 1# eg. boundLimiter(2, 0, 3, true) => 2 * 2# eg. boundLimiter(4, 0, 3, true) => 3 * 3# eg. boundLimiter(-2, 0, 3, true) => 0 * 4# eg. boundLimiter(10, 0, 3, false) => 10 */ export const boundLimiter = ( value: number, minBound: number, maxBound: number, isActive: boolean, ): number => { if (!isActive) return roundNumber(value, 2); if (value < minBound) return roundNumber(minBound, 2); if (value > maxBound) return roundNumber(maxBound, 2); return roundNumber(value, 2); }; export const handleCalculateBounds = ( contextInstance: ReactZoomPanPinchContext, newScale: number, ): BoundsType => { const bounds = calculateBounds(contextInstance, newScale); // Save bounds contextInstance.bounds = bounds; return bounds; }; export function getMouseBoundedPosition( positionX: number, positionY: number, bounds: BoundsType, limitToBounds: boolean, paddingValueX: number, paddingValueY: number, wrapperComponent: HTMLDivElement | null, ): PositionType { const { minPositionX, minPositionY, maxPositionX, maxPositionY } = bounds; let paddingX = 0; let paddingY = 0; if (wrapperComponent) { paddingX = paddingValueX; paddingY = paddingValueY; } const x = boundLimiter( positionX, minPositionX - paddingX, maxPositionX + paddingX, limitToBounds, ); const y = boundLimiter( positionY, minPositionY - paddingY, maxPositionY + paddingY, limitToBounds, ); return { x, y }; }