乐趣区

关于图片:react实现图片缩放平移positiontransform实现

很多网页都会给文案附上一些图片补充形容,例如在说到地址时,会在旁边附上一张地图,并且在地图上标注该地址。如果附上的图片很小,很难看分明地址的具体信息,有些产品经理会给图片设计一个能够平移、放大放大的性能。本文将一一实现上述性能。

废话不多说,先给上效果图:

次要三个性能点:

  • 图片平移
  • 图片缩放
  • 车站标注

图片平移

图片平移能够监听这三个事件实现: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 个参数改正transformOrigintransform: 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`;
}

整体代码示例:

const imgInfo = {
  lableLeft: "1900",
  lableTop: "2000",
}

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})
  /** 图片原始大小,默认设置为 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
  }, [])

  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);
  };

  // 初始图片缩放比例(图片有原始的图片大小)const imgScaleRateX = initial.width / natural.width;
  const imgScaleRateY = initial.height / natural.height;

  const labelTransformOrigin = () => {return `${initial.width / 2 - Number(imgInfo.lableLeft) * imgScaleRateX}px ${initial.height / 2 - Number(imgInfo.lableTop) * imgScaleRateY
    }px`;
  }

  /** 图标地位计算 */
  const labelStyle = (): React.CSSProperties => {
    const transformX = -initial.width / 2;
    const transformY = -initial.height / 2;
    // 图标绝对父元素坐标 = 图标初始地位坐标 + 平移量
    const labelLeft = parseInt(`${imgInfo.lableLeft}`, 10) * imgScaleRateX + Number(imgStyle.left || WIDTH / 2);
    const labelTop = parseInt(`${imgInfo.lableTop}`, 10) * imgScaleRateY + Number(imgStyle.top || HEIGHT / 2);
    return {
      left: labelLeft,
      top: labelTop,
      transformOrigin: labelTransformOrigin(),
      transform: `matrix(${rate}, 0, 0, ${rate}, ${transformX}, ${transformY})`,
    }
  }


  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>
      <span className={styles.label} style={labelStyle()}></span>
    </div>
  )
}
退出移动版