이 튜토리얼의 운영 환경: Windows 10 시스템, 반응 버전 18.0.0, Dell G3 컴퓨터.
반응으로 스케일링을 구현하는 방법은 무엇입니까?
반응형 이미지 스케일링 및 패닝(위치, 변형 구현)
많은 웹페이지에서는 사본 설명을 보충하기 위해 일부 사진을 첨부합니다. 예를 들어 주소에 대해 이야기하면 옆에 지도가 첨부됩니다. , 그리고 지도에 이 주소를 표시하세요. 첨부된 사진이 매우 작아서 주소의 구체적인 정보를 명확하게 확인하기 어려운 경우, 일부 제품 관리자는 사진의 패닝, 확대/축소 기능을 설계할 것입니다. 이번 글에서는 위의 기능을 하나씩 구현해보겠습니다.
더 이상 고민하지 말고 렌더링은 다음과 같습니다.
주요 세 가지 기능 포인트:
- Picture pan
- Picture Zoom
- Station label
Picture pan
Picture pan은 다음 세 가지 이벤트 구현을 모니터링할 수 있습니다. onMouseDown, onMouseMove, onMouseUp
. onMouseDown
이벤트는 각 마우스 누름의 좌표 위치를 기록합니다. onMouseMove
이벤트는 각 번역의 거리와 드래그를 더한 거리를 계산합니다. 이전 사진과 상위 요소는 드래그한 사진과 상위 요소 사이의 거리와 같습니다.onMouseUp
이벤트가 트리거되면 로그아웃하거나 onMouseDown 실행을 방지하세요. onMouseMove
이벤트는 마우스가 이미지 안으로 이동할 때마다 이미지가 패닝되는 것을 방지합니다. onMouseDown、onMouseMove、onMouseUp
。onMouseDown
事件记录每次鼠标按下的坐标位置;onMouseMove
事件计算出每次平移的距离,该距离加上拖动前图片距离父元素的距离就等于拖动后图片相对于父元素的距离;onMouseUp
事件触发时,注销或者不让执行onMouseDown、onMouseMove
事件,防止只要鼠标移入图片就会平移。
这三个事件需要阻止浏览器的默认行为,不然在移动时会自动打开图片。
const WIDTH = 1200;const HEIGHT = 900;const DynamicStyle= () => { const imgRef = React.createRef<HTMLImageElement>(); /** 图片样式 */ const [imgStyle, setImgStyle] = useState<React.CSSProperties>({}); /** 记录鼠标是否按下 */ const [mouseDowmFlag, setMouseDowmFlag] = useState(false); /** 记录鼠标按下的坐标 */ const [mouseDowmPos, setMouseDowmPos] = useState<{x: number, y: number}>({x: 0, y: 0}) /** 鼠标可视区域时,重置鼠标按下的布尔值为false */ useEffect(() => { document.onmouseover = () => { if (mouseDowmFlag) { setMouseDowmFlag(false); } }; return () => { document.onmouseover = null; }; }, [mouseDowmFlag]) /** 平移 */ const handleMouseDown = (event: React.MouseEvent<HTMLImageElement>) => { const { clientX, clientY } = event; event.stopPropagation(); event.preventDefault(); // 阻止浏览器默认行为,拖动会打开图片 setMouseDowmFlag(true); // 控制只有在鼠标按下后才会执行mousemove setMouseDowmPos({ x: clientX, y: clientY, }); }; const handleMouseMove = (event: React.MouseEvent<HTMLImageElement>) => { event.stopPropagation(); event.preventDefault(); const { clientX, clientY } = event; const diffX = clientX - mouseDowmPos.x; const diffY = clientY - mouseDowmPos.y; if (!mouseDowmFlag || (diffX === 0 && diffY === 0)) return; const { offsetLeft, offsetTop } = imgRef.current as HTMLImageElement; const offsetX = parseInt(`${diffX + offsetLeft}`, 10); const offsetY = parseInt(`${diffY + offsetTop}`, 10); setMouseDowmPos({ x: clientX, y: clientY, }); setImgStyle({ ...imgStyle, left: offsetX, top: offsetY, }); }; const handleMouseUp = (event: React.MouseEvent<HTMLImageElement>) => { event.stopPropagation(); event.preventDefault(); setMouseDowmFlag(false); }; return ( <div className={styles.imgArea}> <img src={mapImg} alt='part' ref={imgRef} height={HEIGHT} style={imgStyle} onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} > </img> </div> ) }
图片缩放
图片缩放可以监听onWheel
事件,事件对象event
有一个记录滚轮滚动的属性deltaY
,当向上滚动时deltaY<0
,向下滚动时deltaY>0
。每次滚动修改其缩放的比例,同时更改transform
样式按比例进行缩放。
const WIDTH = 1200;const HEIGHT = 900;const SCALE = 0.2;const DynamicStyle= () => { const imgRef = React.createRef<HTMLImageElement>(); /** 初始化缩放比例,默认为1 */ const [rate, setRate] = useState(1); /** 图片样式 */ const [imgStyle, setImgStyle] = useState<React.CSSProperties>({}); /** 记录鼠标是否按下 */ const [mouseDowmFlag, setMouseDowmFlag] = useState(false); /** 记录鼠标按下的坐标 */ const [mouseDowmPos, setMouseDowmPos] = useState<{x: number, y: number}>({x: 0, y: 0}) /** 图片现在大小 */ const [initial, setInitial] = useState<{width: number, height: number}>({width: WIDTH, height: HEIGHT}); useEffect(() => { const { naturalWidth, naturalHeight, width, height } = imgRef.current as HTMLImageElement; setInitial({ width, height }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // console.log(natural, initial) useEffect(() => { document.onmouseover = () => { if (mouseDowmFlag) { setMouseDowmFlag(false); } }; return () => { document.onmouseover = null; }; }, [mouseDowmFlag]) /** 缩放 */ const handleWheelImage = (event: React.WheelEvent<HTMLImageElement>) => { // 向上为负,向下为正 const bigger = event.deltaY > 0 ? -1 : 1; // transform偏移量 const transformX = -initial.width / 2; const transformY = -initial.height / 2; if (bigger > 0 && rate < 2) { const enlargeRate = rate + SCALE; setImgStyle({ ...imgStyle, transform: `matrix(${enlargeRate}, 0, 0, ${enlargeRate}, ${transformX}, ${transformY})`, // 默认以图片中心为原点进行缩放 }); setRate(enlargeRate); } else if (bigger < 0 && rate > 1) { const shrinkRate = rate - SCALE; setImgStyle({ ...imgStyle, transform: `matrix(${shrinkRate}, 0, 0, ${shrinkRate}, ${transformX}, ${transformY})`, }); setRate(shrinkRate); } } /** 平移 */ const handleMouseDown = (event: React.MouseEvent<HTMLImageElement>) => { const { clientX, clientY } = event; event.stopPropagation(); event.preventDefault(); // 阻止浏览器默认行为,拖动会打开图片 setMouseDowmFlag(true); // 控制只有在鼠标按下后才会执行mousemove setMouseDowmPos({ x: clientX, y: clientY, }); }; const handleMouseMove = (event: React.MouseEvent<HTMLImageElement>) => { event.stopPropagation(); event.preventDefault(); const { clientX, clientY } = event; const diffX = clientX - mouseDowmPos.x; const diffY = clientY - mouseDowmPos.y; if (!mouseDowmFlag || (diffX === 0 && diffY === 0)) return; const { offsetLeft, offsetTop } = imgRef.current as HTMLImageElement; const offsetX = parseInt(`${diffX + offsetLeft}`, 10); const offsetY = parseInt(`${diffY + offsetTop}`, 10); setMouseDowmPos({ x: clientX, y: clientY, }); setImgStyle({ ...imgStyle, left: offsetX, top: offsetY, }); }; const handleMouseUp = (event: React.MouseEvent<HTMLImageElement>) => { event.stopPropagation(); event.preventDefault(); setMouseDowmFlag(false); }; return ( <div className={styles.imgArea}> <img src={mapImg} alt='part' height={HEIGHT} style={imgStyle} ref={imgRef} onWheel={handleWheelImage} onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} > </img> </div> ) }
.imgArea { position: relative; width: 1200px; height: 900px; margin: auto; border: 1px solid #da2727; overflow: hidden; & > img { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); cursor: move; } }
如果没有设置transformOrigin
,默认是相对于图片中心进行缩放,但是在初始为了让图片在可视区域内水平垂直居中,使用了transform: translate(-50%, -50%);
,因此为了缩放时相对于图片中心点,需要设置matrix
的第5、6个参数矫正transformOrigin
,transform: matrix(${shrinkRate}, 0, 0, ${shrinkRate}, ${transformX}, ${transformY})
车站标注
首先,定义一个常量表示图标的坐标,这个坐标是相对于原始图片左上角的定位。
const imgInfo = { lableLeft: "1900", lableTop: "2000",}
这里,解释一下原始图的概念:
随便在网上查看一个图片元素,比如上面。1200 x 900是页面定的图片大小,但图片还有一个真实大小4535 x 3402。
要计算图标在没有平移缩放时的初始坐标之前,需要算出图片的缩放比例(不是上面的rate
):
/** 图片原始大小,默认设置为1是防止计算图片原始大小与初始大小比例出现无穷大 */const [natural, setNatural] = useState<{width: number, height: number}>({width: 1, height: 1});/** 图片现在大小 */const [initial, setInitial] = useState<{width: number, height: number}>({width: WIDTH, height: HEIGHT});useEffect(() => { const { naturalWidth, naturalHeight, width, height } = imgRef.current as HTMLImageElement; setNatural({ width: naturalWidth, height: naturalHeight }); setInitial({ width, height }); // eslint-disable-next-line react-hooks/exhaustive-deps}, []) // 初始图片缩放比例(图片有原始的图片大小)const imgScaleRateX = initial.width / natural.width;const imgScaleRateY = initial.height / natural.height;
图标初始的坐标就可以计算出:
const labelLeft = parseInt(`${imgInfo.lableLeft}`, 10) * imgScaleRateX;const labelTop = parseInt(`${imgInfo.lableTop}`, 10) * imgScaleRateY;
当图片平移时,图标也需要跟着平移,这是的坐标计算:
// 图标相对父元素坐标 = 图标位置坐标 + 图片坐标const labelLeft = parseInt(`${imgInfo.lableLeft}`, 10) * imgScaleRateX + Number(imgStyle.left || WIDTH / 2); const labelTop = parseInt(`${imgInfo.lableTop}`, 10) * imgScaleRateY + Number(imgStyle.top || HEIGHT / 2);
当图片缩放时,图标需要随着图片一起缩放。如果没有对图标设置transformOrigin
,默认时相对图标的中心缩放的。为了保证图标随着图片一起缩放,那就必须使得图片和图标的缩放参照原点相同,图标的transformOrigin
const labelTransformOrigin = () => { return `${initial.width / 2 - Number(imgInfo.lableLeft) * imgScaleRateX}px ${ initial.height / 2 - Number(imgInfo.lableTop) * imgScaleRateY }px`; }
Picture Zoom은 onWheel
이벤트를 수신할 수 있습니다. 이벤트 개체 event
에는 스크롤 휠을 기록하는 deltaY
속성이 있습니다. 위로 스크롤하면 deltaY<0
, 아래로 스크롤하면 deltaY>0
됩니다. 각 스크롤은 배율 비율을 변경하고 변환
스타일을 비례적으로 배율 조정하도록 변경합니다.