123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209 |
- import React from "react";
- import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil";
- import { BoardConfigAtom, BoardStateAtom, PanZoomRotateAtom } from "./";
- import { insideClass } from "../../utils/";
- import usePrevious from "../../hooks/usePrevious";
- import styled from "styled-components";
- import debounce from "lodash.debounce";
- import Gesture from "./Gesture";
- const TOLERANCE = 100;
- const Pane = styled.div.attrs(({ translateX, translateY, scale, rotate }) => ({
- style: {
- transform: `translate(${translateX}px, ${translateY}px) scale(${scale}) rotate(${rotate}deg)`,
- },
- className: "board-pane",
- }))`
- transform-origin: top left;
- display: inline-block;
- `;
- const PanZoomRotate = ({ children, moveFirst }) => {
- const [dim, setDim] = useRecoilState(PanZoomRotateAtom);
- const config = useRecoilValue(BoardConfigAtom);
- const setBoardState = useSetRecoilState(BoardStateAtom);
- const prevDim = usePrevious(dim);
- const [scale, setScale] = React.useState({
- scale: config.scale,
- x: 0,
- y: 0,
- });
- const wrappedRef = React.useRef(null);
- // React on scale change
- React.useLayoutEffect(() => {
- setDim((prevDim) => {
- const { top, left } = wrappedRef.current.getBoundingClientRect();
- const displayX = scale.x - left;
- const deltaX = displayX - (displayX / prevDim.scale) * scale.scale;
- const displayY = scale.y - top;
- const deltaY = displayY - (displayY / prevDim.scale) * scale.scale;
- return {
- ...prevDim,
- scale: scale.scale,
- translateX: prevDim.translateX + deltaX,
- translateY: prevDim.translateY + deltaY,
- };
- });
- }, [scale, setDim]);
- // Center board on game loading
- React.useEffect(() => {
- const { innerHeight, innerWidth } = window;
- setDim((prev) => ({
- ...prev,
- scale: config.scale,
- translateX: innerWidth / 2 - (config.size / 2) * config.scale,
- translateY: innerHeight / 2 - (config.size / 2) * config.scale,
- }));
- }, [config.size, config.scale, setDim]);
- // Keep board inside viewport
- React.useEffect(() => {
- const { width, height } = wrappedRef.current.getBoundingClientRect();
- const { innerHeight, innerWidth } = window;
- const newDim = {};
- if (dim.translateX > innerWidth - TOLERANCE) {
- newDim.translateX = innerWidth - TOLERANCE;
- }
- if (dim.translateX + width < TOLERANCE) {
- newDim.translateX = TOLERANCE - width;
- }
- if (dim.translateY > innerHeight - TOLERANCE) {
- newDim.translateY = innerHeight - TOLERANCE;
- }
- if (dim.translateY + height < TOLERANCE) {
- newDim.translateY = TOLERANCE - height;
- }
- if (Object.keys(newDim).length > 0) {
- setDim((prevDim) => ({
- ...prevDim,
- ...newDim,
- }));
- }
- }, [dim.translateX, dim.translateY, setDim]);
- // Debounce set center to avoid too many render
- // eslint-disable-next-line react-hooks/exhaustive-deps
- const debouncedUpdateCenter = React.useCallback(
- debounce(() => {
- const { innerHeight, innerWidth } = window;
- setDim((prevDim) => {
- return {
- ...prevDim,
- centerX: (innerWidth / 2 - prevDim.translateX) / prevDim.scale,
- centerY: (innerHeight / 2 - prevDim.translateY) / prevDim.scale,
- };
- });
- }, 300),
- [setDim]
- );
- React.useEffect(() => {
- debouncedUpdateCenter();
- }, [debouncedUpdateCenter, dim.translateX, dim.translateY]);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- const updateBoardStateZoomingDelay = React.useCallback(
- debounce((newState) => {
- setBoardState(newState);
- }, 300),
- [setBoardState]
- );
- // eslint-disable-next-line react-hooks/exhaustive-deps
- const updateBoardStatePanningDelay = React.useCallback(
- debounce((newState) => {
- setBoardState(newState);
- }, 200),
- [setBoardState]
- );
- // Update boardState on zoom or pan
- React.useEffect(() => {
- if (!prevDim) {
- return;
- }
- if (prevDim.scale !== dim.scale) {
- setBoardState((prev) =>
- !prev.zooming ? { ...prev, zooming: true } : prev
- );
- updateBoardStateZoomingDelay((prev) =>
- prev.zooming ? { ...prev, zooming: false } : prev
- );
- }
- if (
- prevDim.translateY !== dim.translateY ||
- prevDim.translateX !== dim.translateX
- ) {
- setBoardState((prev) =>
- !prev.panning ? { ...prev, panning: true } : prev
- );
- updateBoardStatePanningDelay((prev) =>
- prev.panning ? { ...prev, panning: false } : prev
- );
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [dim, updateBoardStatePanningDelay, updateBoardStateZoomingDelay]);
- const onZoom = ({ clientX, clientY, scale }) => {
- setScale((prevScale) => {
- let newScale = prevScale.scale * (1 - scale / 200);
- if (newScale > 8) {
- newScale = 8;
- }
- if (newScale < 0.1) {
- newScale = 0.1;
- }
- return {
- scale: newScale,
- x: clientX,
- y: clientY,
- };
- });
- };
- const onPan = ({ deltaX, deltaY }) => {
- setDim((prevDim) => {
- return {
- ...prevDim,
- translateX: prevDim.translateX + deltaX,
- translateY: prevDim.translateY + deltaY,
- };
- });
- };
- const onDrag = (state) => {
- const { target } = state;
- const outsideItem =
- !insideClass(target, "item") || insideClass(target, "locked");
- if (moveFirst && outsideItem) {
- onPan(state);
- }
- };
- return (
- <Gesture onPan={onPan} onZoom={onZoom} onDrag={onDrag}>
- <Pane {...dim} ref={wrappedRef}>
- {children}
- </Pane>
- </Gesture>
- );
- };
- export default PanZoomRotate;
|