Alexander Zinchuk 3afcde3217 Initial commit
2021-04-09 14:11:51 +03:00

212 lines
5.6 KiB
TypeScript

/*
@source https://github.com/ajainarayanan/react-pan-zoom
Heavily inspired/lifted from this idea: https://stackoverflow.com/a/39311435/661768
without jqueryUI or jquery dependency.
*/
import React, {
FC, memo, useEffect, useRef, useState,
} from '../../lib/teact/teact';
import { areSortedArraysEqual } from '../../util/iteratees';
import './PanZoom.scss';
export interface IDragData {
x: number;
y: number;
dx: number;
dy: number;
}
export interface OwnProps {
children: any;
className?: string;
noWrap: boolean;
canPan: boolean;
zoomLevel: number;
panDeltaX: number;
panDeltaY: number;
onPan?: (x: number, y: number) => void;
}
const INITIAL_MATRIX = [
1, 0, 0, 1, 0, 0,
];
const SCALE_VALUES = {
1: 1,
1.5: 1.5,
2: 2.2,
2.5: 3.3,
3: 5.5,
};
const ZOOM_SAFE_AREA = 150;
function calculateSafeZoneOnZoom(oldScale: number, matrixData: number[], wrapper: HTMLDivElement | null) {
const image = wrapper && wrapper.querySelector('.active img');
if (!wrapper || !image) {
return matrixData;
}
const wrapperRect = wrapper.getBoundingClientRect();
const imageRect = image.getBoundingClientRect();
const newImgWidth = (imageRect.width / oldScale) * matrixData[0];
const newImgHeight = (imageRect.height / oldScale) * matrixData[3];
const newImgX = (wrapperRect.width - newImgWidth) / 2 + matrixData[4];
const newImgY = (wrapperRect.height - newImgHeight) / 2 + matrixData[5];
if (wrapperRect.width && wrapperRect.width - ZOOM_SAFE_AREA < newImgX) {
matrixData[4] -= newImgX + wrapperRect.width - ZOOM_SAFE_AREA;
} else if (newImgWidth && newImgWidth + newImgX < ZOOM_SAFE_AREA) {
matrixData[4] -= newImgWidth + newImgX - ZOOM_SAFE_AREA;
}
if (wrapperRect.height && wrapperRect.height - ZOOM_SAFE_AREA < newImgY) {
matrixData[5] -= newImgY + wrapperRect.height - ZOOM_SAFE_AREA;
} else if (newImgHeight && newImgHeight + newImgY < ZOOM_SAFE_AREA) {
matrixData[5] -= newImgHeight + newImgY - ZOOM_SAFE_AREA;
}
return matrixData;
}
const PanZoom: FC<OwnProps> = ({
children,
className,
noWrap,
canPan,
zoomLevel,
panDeltaX,
panDeltaY,
onPan,
}) => {
const tunedZoomLevel = SCALE_VALUES[zoomLevel as keyof typeof SCALE_VALUES] || zoomLevel;
const [isDragging, setIsDragging] = useState<boolean>(false);
const [dragData, setDragData] = useState<IDragData>({
dx: panDeltaX, dy: panDeltaY, x: 0, y: 0,
});
// [zoom, skew, skew, zoom, dx, dy] - see https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/matrix()
const [matrixData, setMatrixData] = useState<number[]>(INITIAL_MATRIX);
// Used to set cursor while moving.
// eslint-disable-next-line no-null/no-null
const panWrapperRef = useRef<HTMLDivElement>(null);
// Used to set transform for pan.
// eslint-disable-next-line no-null/no-null
const panContainerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const newZoomLevel = tunedZoomLevel || matrixData[0];
const newPandx = panDeltaX || matrixData[4];
const newPandy = panDeltaY || matrixData[5];
const newMatrixData = [...matrixData];
if (matrixData[0] !== newZoomLevel) {
newMatrixData[0] = newZoomLevel || newMatrixData[0];
newMatrixData[3] = newZoomLevel || newMatrixData[3];
}
if (matrixData[4] !== newPandx) {
newMatrixData[4] = newPandx;
}
if (matrixData[5] !== newPandy) {
newMatrixData[5] = newPandy;
}
if (!areSortedArraysEqual(matrixData, newMatrixData)) {
setMatrixData(calculateSafeZoneOnZoom(matrixData[0], newMatrixData, panWrapperRef.current));
}
// eslint-disable-next-line
}, [panDeltaX, panDeltaY, tunedZoomLevel]);
useEffect(() => {
if (!canPan) {
setMatrixData(INITIAL_MATRIX);
}
}, [canPan]);
useEffect(() => {
if (panContainerRef.current) {
panContainerRef.current.style.transform = `matrix(${matrixData.toString()})`;
}
}, [noWrap, matrixData]);
const handleMouseDown = (e: React.MouseEvent<EventTarget>) => {
if (!canPan) {
return;
}
e.stopPropagation();
e.preventDefault();
const offsetX = matrixData[4];
const offsetY = matrixData[5];
const newDragData: IDragData = {
dx: offsetX,
dy: offsetY,
x: e.pageX,
y: e.pageY,
};
setDragData(newDragData);
setIsDragging(true);
if (panWrapperRef.current) {
panWrapperRef.current.classList.add('move');
}
};
const handleMouseUp = () => {
setIsDragging(false);
if (panWrapperRef.current) {
panWrapperRef.current.classList.remove('move');
}
if (onPan) {
onPan(matrixData[4], matrixData[5]);
}
};
function getNewMatrixData(x: number, y: number): number[] {
const newMatrixData = [...matrixData];
const deltaX = dragData.x - x;
const deltaY = dragData.y - y;
newMatrixData[4] = dragData.dx - deltaX;
newMatrixData[5] = dragData.dy - deltaY;
return newMatrixData;
}
const handleMouseMove = (e: React.MouseEvent<EventTarget>) => {
if (isDragging) {
const newMatrixData = getNewMatrixData(e.pageX, e.pageY);
setMatrixData(newMatrixData);
if (panContainerRef.current) {
panContainerRef.current.style.transform = `matrix(${matrixData.toString()})`;
}
}
};
if (noWrap) {
return children;
}
return (
<div
ref={panWrapperRef}
className={`pan-wrapper ${className || ''}`}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseMove={handleMouseMove}
>
<div
ref={panContainerRef}
className="pan-container"
>
{children}
</div>
</div>
);
};
export default memo(PanZoom);