图片以鼠标为中心缩放

1,391 阅读1分钟

项目里使用到这个功能, 所以就写了个.

刚接触到需求时的思路

第一个想法就是每次将缩放的基点设置为当前鼠标所在的位置, 可是后来发现在一个地方缩放的时候好像没有问题,可是移动鼠标后再次缩放, 图片会有一个明显的平移现象, 显然这个不是想要的结果, 先上这个方法的代码

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <img
      width="560"
      src="https://t7.baidu.com/it/u=1653814446,2847580380&fm=193&f=GIF"
      alt=""
    />

    <script>
      // 每次缩放的时候, 修改缩放的基点
      const img = document.querySelector("img");

      const useZoom = (callback = () => {}) => {
        let scale = 1;
        const zoom = (e) => {
          const { offsetX, offsetY } = e;
          let _scale = scale;
          _scale += e.deltaY / 1000;
          if (_scale < 0.3) return;
          scale = _scale;
          callback({ x: offsetX, y: offsetY }, scale);
        };

        return {
          zoom,
        };
      };

      const update = (position, scale) => {
        img.style.transformOrigin = `${position.x}px ${position.y}px`;
        img.style.transform = `scale(${scale})`;
      };

      const { zoom } = useZoom(update);
      img.onwheel = zoom;
    </script>
  </body>
</html>

调整过的思路

然后通过调整思路, 其实以鼠标为中心缩放, 要做的就是先缩小, 在把鼠标对应的点移动到鼠标所在的位置, 也就是每次触发事件的时候,之前偏移的位置 + 这次偏移,鼠标对应点移动的距离就可以

有一下两种实现方式:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div class="log"></div>
    <img
      width="560"
      src="https://t7.baidu.com/it/u=1653814446,2847580380&fm=193&f=GIF"
      alt=""
    />

    <script>
      const useZoom = (el, callback = () => {}) => {
        const { width, height } = el.getBoundingClientRect();

        let scale = 1;
        const transform = {
          x: 0,
          y: 0,
        };

        const wheelZoom = (e) => {
          e.preventDefault();
          let ratio = e.deltaY / 1000;

          // 需要缩放的比例
          const _scale = scale * ratio + scale;
          scale = _scale;
          // 图片缩放以中心为基点, 上下各缩放多少
          const origin = {
            x: ratio * width * 0.5,
            y: ratio * height * 0.5,
          };

          // 鼠标在图片上的坐标
          const positionX = e.clientX - transform.x;
          const positionY = e.clientY - transform.y;

          // 鼠标所在点在图片上缩放后移动的坐标
          const ratioX = ratio * positionX;
          const ratioY = ratio * positionY;

          // 鼠标坐在点移动的位置 - 图片移动的位置 = 需要移动的位置
          transform.x -= ratioX - origin.x;
          transform.y -= ratioY - origin.y;

          callback({ ...transform }, scale);
        };

        return {
          wheelZoom,
        };
      };
    </script>
    <script>
      const img = document.querySelector("img");
      const log = document.querySelector(".log");

      img.onload = () => {
        const { wheelZoom } = useZoom(img, (transform, scale) => {
          // translate 和 scale 的顺序影响最终效果
          img.style.transform = `translate(${transform.x}px, ${transform.y}px) scale(${scale})`;
          log.innerHTML = `x = ${transform.x.toFixed(0)}<br>
            y = ${transform.y.toFixed(0)}<br>
            scale = ${scale.toFixed(5)}`;
        });

        img.addEventListener("wheel", wheelZoom);
      };
    </script>
    <style>
      * {
        margin: 0;
        padding: 0;
      }
      body {
        height: 100vh;
        background: #000;
        overflow: hidden;
      }
      .wrap {
        margin: 200px;
      }
      img {
        touch-action: none;
      }
      .log {
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 99;
        padding: 5px;
        color: #fff;
        font-size: 12px;
        line-height: 18px;
        pointer-events: none;
      }
    </style>
  </body>
</html>

第二种

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div class="log"></div>
    <div class="wrap">
      <img
        width="560"
        src="https://t7.baidu.com/it/u=1653814446,2847580380&fm=193&f=GIF"
        alt=""
      />
    </div>

    <script>
      const useZoom = (el, callback = () => {}) => {
        el.style.transformOrigin = "0 0";
        /** 缩放的比例 */
        let scale = 1;
        /** 平移的距离 */
        const translateData = { x: 0, y: 0 };
        const { width, height } = el.getBoundingClientRect();

        /** 重置数据, 并触发回调更新元素 */
        const reset = () => {
          scale = 1;
          translateData = { x: 0, y: 0 };
          callback(translateData, scale);
        };

        const wheelZoom = (event) => {
          let _scale = scale;
          _scale += event.deltaY > 0 ? -0.09 : 0.1;
          if (_scale < 0.3) return;
          let _translateData = distanceMovedZoom(event, scale, _scale);
          scale = _scale;

          // 需要移动的距离 = 已经移动的距离 + 需要再次移动的距离
          translateData.x += _translateData.x;
          translateData.y += _translateData.y;

          callback(translateData, scale);
        };

        /** 计算每次需要移动的距离 */
        const distanceMovedZoom = (
          { offsetX, offsetY },
          oldScale,
          newScale
        ) => {
          const newWidth = width * newScale;
          const newHeight = height * newScale;
          const diffWidth = width * oldScale - newWidth;
          const diffHeight = height * oldScale - newHeight;
          // 鼠标在图片上坐标比例, offsetX 是取原始大小的值, 所用要除 widht
          const xRatio = offsetX / width;
          const yRatio = offsetY / height;

          // 需要再次移动的距离 x = (新的宽度 - 旧的宽度) * 鼠标在旧的宽度的比例
          return { x: diffWidth * xRatio, y: diffHeight * yRatio };
        };

        return {
          wheelZoom,
          reset,
        };
      };
    </script>
    <script>
      const img = document.querySelector("img");
      const log = document.querySelector(".log");

      img.onload = () => {
        const { wheelZoom } = useZoom(img, (transform, scale) => {
          // translate 和 scale 的顺序影响最终效果
          img.style.transform = `translate(${transform.x}px, ${transform.y}px) scale(${scale})`;
          log.innerHTML = `x = ${transform.x.toFixed(0)}<br>
            y = ${transform.y.toFixed(0)}<br>
            scale = ${scale.toFixed(5)}`;
        });

        img.addEventListener("wheel", wheelZoom);
      };
    </script>
    <style>
      * {
        margin: 0;
        padding: 0;
      }
      body {
        height: 100vh;
        background: #000;
        overflow: hidden;
      }
      .wrap {
        margin: 200px;
      }
      img {
        touch-action: none;
      }
      .log {
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 99;
        padding: 5px;
        color: #fff;
        font-size: 12px;
        line-height: 18px;
        pointer-events: none;
      }
    </style>
  </body>
</html>

踩的坑

  • 项目里是调整宽度, 所以每次获取鼠标在图片中的坐标都是当前大小对应的值, 现在使用的是缩放, 获取的坐标都是相对于原本的宽度
  • 缩放后的图片宽度, 获取的是原本的宽度, 缩放对应的宽度需要通过 getBoundingClientRect 获取或者计算出来
  • transform 里面属性的顺序影响最终效果