react-konva 如何实现 根据以鼠标为中心缩放图片?

原生canvas 可以实现 ,但是用的 konva 库, 不知道怎么做了就

需求是 需要想百度地图 高德地图哪种类似的 以所选区域为中心缩放
https://codesandbox.io/s/affectionate-resonance-2f7lhn?file=/...

import { useEffect, useRef, useState } from "react"
import { useSelector } from "react-redux"
import { Stage, Layer, Group, Image } from "react-konva"
import { vec2 } from "gl-matrix"
import styled from "styled-components"
import Konva from "konva"
import useImage from "use-image" 

import type { KonvaEventObject } from "konva/lib/Node"
import type { RootState } from "@/stores"

const ScStage = styled(Stage)`
    width: 100%;
    height: calc(100% - 100px);
    overflow: hidden;
`

const StageContainer2 = () => {
    const { imgUrl } = useSelector((state: RootState) => state.measure)
    const { cursor } = useSelector((state: RootState) => state.cursor)
    const { lateral } = useSelector((state: RootState) => state.showPoint)

    const { rotate, scaleX, contrast, brightness } = useSelector((state: RootState) => state.transform)

    const [scale, setScale] = useState(1)
    const [width, setWidth] = useState(1000)
    const [height, setHeight] = useState(1000)
    const [image] = useImage(imgUrl)

    const stageRef = useRef<Konva.Stage | null>(null)
    const layerRef = useRef<Konva.Layer | null>(null)
    const imageRef = useRef(null)

    const [offsetX, setOffsetX] = useState(0)
    const [offsetY, setOffsetY] = useState(0)

    const max = 4   // 放大最大的比例
    const min = 0.5 // 缩小最小的比例
    const step = 0.03 // 每次缩放的比例

    function onWheel(e: KonvaEventObject<WheelEvent>) {

        const x = e.evt.offsetX
        const y = e.evt.offsetY

        const offsetX = (x - layerRef.current.offsetX()) * layerRef.current.scaleX() / (layerRef.current.scaleX() - step) - (x - layerRef.current.offsetX())
        const offsetY = (y - layerRef.current.offsetY()) * layerRef.current.scaleY() / (layerRef.current.scaleY() - step) - (y - layerRef.current.offsetY())

// 这里写的不是很对
        if (e.evt.wheelDelta && e.evt.wheelDelta > 0) {
            // 放大
            if (layerRef.current.scaleX() < max && layerRef.current.scaleY() < max) {
                layerRef.current.scaleX(layerRef.current.scaleX() + step)
                layerRef.current.scaleY(layerRef.current.scaleY() + step)
                layerRef.current.move({ x: -offsetX, y: -offsetY }) // 跟随鼠标偏移位置
            }
        } else {
            // 缩小
            if (layerRef.current.scaleX() > min && layerRef.current.scaleY() > min) {
                layerRef.current.scaleX(layerRef.current.scaleX() - step)
                layerRef.current.scaleY(layerRef.current.scaleY() - step)
                layerRef.current.move({ x: offsetX, y: offsetY }) // 跟随鼠标偏移位置
            }
        }

        // const stage = e.target.getStage()
        // const mousePos = stage?.getPointerPosition()!
        //
        // const imagePos = imageRef.current?.position()!

    }

    useEffect(() => {
        const stage = stageRef.current?.getStage()
        const stageWrapper = stageRef.current!.attrs.container
        setStageSize()

        function setStageSize() {
            stage?.width(stageWrapper.clientWidth)
            stage?.height(stageWrapper.clientHeight)
            setWidth(stageWrapper.clientWidth)
            setHeight(stageWrapper.clientHeight)
        }
    }, [stageRef])

    return (
        <>
            <ScStage ref={stageRef} style={{ cursor }} onWheel={onWheel}>
                <Layer ref={layerRef} scaleX={scaleX} rotation={rotate} x={width / 2} y={height / 2}>
                    <Group draggable
                           scale={{ x: scale, y: scale }}
                           offset={{ x: image?.width! / 2, y: image?.height! / 2 }}
                    >
                        {lateral && (
                            <Image
                                ref={imageRef}
                                image={image}
                                filters={[Konva.Filters.Brighten, Konva.Filters.Contrast]}
                                contrast={contrast}
                                brightness={brightness}
                            />
                        )} 
                    </Group>
                </Layer>
            </ScStage>
        </>
    )
}

export default StageContainer2
阅读 1.6k
1 个回答

解决这个问题,首先要逆变换求出鼠标点击的位置在变换前的位置,然后将这个位置作为新的变换坐标系原点(原点在线性变换中是不动点,利用这个性质,就可以做出跟随鼠标的效果)。
如果图形已经发生过变换,修改原点会导致“跃变”效果,也就是在鼠标开始动的瞬间图形会跑掉(把setPosition去掉就可以重现这种现象)。因此需要对“跃变”的副作用进行修正,也就是在修改原点的同时,把图像“拽”回原位置。

  const [scale, setScale] = useState(2);
  const [offset, setOffset] = useState({
    x: 80,
    y: 80,
  });
  const [position, setPosition] = useState({ x: 0, y: 0 });

  const wheelCallback = useCallback(
    ({
      //@ts-ignore
      evt: { layerX, layerY, deltaY },
    }: Konva.KonvaEventObject<WheelEvent>) => {
      if (!shapeRef.current) return;

      const currentTransform = shapeRef.current.getTransform();
      const currentTransformMatrix = [...currentTransform.getMatrix()];
      const { x, y, offsetX, offsetY } = shapeRef.current.attrs;

      // 当前的变换坐标原点为 [offsetX, offsetY], 为不变点

      // 鼠标点击的位置
      const currentViewCursorInLayer = {
        x: layerX,
        y: layerY,
      };

      // 当前变换矩阵求逆,以便还原鼠标位置在变换前的真实位置
      const currentTransformInvert = currentTransform.copy().invert();

      // 逆变换求鼠标位置变换前的位置,所以是 Actual ,该位置将作为新的变换原点
      // 该位置实际上是在以 [x, y] 为原点的坐标系中表达的,所以是 InShape
      const currentActualCursorInShape = currentTransformInvert.point(
        currentViewCursorInLayer
      );

      // 其实赋值前后是完全等效的,这里为了避免困惑,故重命名
      const nextOffset = {
        x: currentActualCursorInShape.x,
        y: currentActualCursorInShape.y,
      };

      // 修改原点
      setOffset(nextOffset);

      // 跟随滚轮缩放
      if (deltaY < 0) {
        setScale(scale + 0.1);
      } else if (scale > 0.1) {
        setScale(scale - 0.1);
      } else {
        setScale(0.1);
      }

      // 原点位置变更,变更的矢量由旧原点指向新原点,直接移动一下好了
      const nextMatrix = [...currentTransformMatrix];
      const nextTransform = new Konva.Transform(nextMatrix).translate(
        -(nextOffset.x - offsetX),
        -(nextOffset.y - offsetY)
      );

      // 修改原点,会导致位置突变,需要计算突变发生后,鼠标相对于图像的位置会被移动到何处
      // 变换只是视觉效果,所以是 View
      const nextViewCursorInLayer = nextTransform.point(nextOffset);

      // 鼠标位置在两次变化间的移动矢量
      const cursorMoveVector = {
        x: layerX - nextViewCursorInLayer.x,
        y: layerY - nextViewCursorInLayer.y,
      };

      // 使用前述移动矢量,修正位置
      setPosition({
        x: x + cursorMoveVector.x,
        y: y + cursorMoveVector.y
      });
    },
    [scale, shapeRef]
  );

      return <Stage width={800} height={800}>
        <Layer>
          <Group
            scale={{ x: scale, y: scale }}
            onDragEnd={dragCallback}
            onWheel={wheelCallback}
            position={position}
            offset={offset}
            ref={shapeRef}
            draggable
          >
            <Rect
              width={160}
              height={160}
              strokeWidth={1}
              stroke="blue"
              fill={"red"}
            />
          </Group>
        </Layer>
      </Stage>
👆这里用的 Rect ,方便测试,Image 差别应该不大。

以上代码在单层未变换 Layer 中测试可用,但没有测试过复杂的复合变换,注意shapeRef要作为Group的属性,而非Image

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题