【思维笔记】以鼠标为中心缩放图层

1,288 阅读3分钟

image.png

产品:hi bro!这儿有个只有你才能做得出来的需求!

开发:就你*事儿多,说吧......

产品:咋们那个缩放效果太拉闸了?小兄弟能够弄得好看点吗?

开发:要啥自行车?又不是不能用......

开发同学虽然当时表现得毫不在意,但是背后偷偷发现许多的产品,做缩放时效果都很nice,毫无违和感,于是有了接下来的故事......

鼠标为中心缩放图层

顾名思义就是以鼠标位置为缩放的中心点,鼠标到图层各个边的距离等倍的缩小(放大)。 效果展示如下:

方案1:取巧

大概思路: 举个例子:一个800 * 800的盒子,以其中心点(400, 400)处缩小一半

image.png

由上图不难看出当以盒子中心缩小一半时,由黑色的盒子转换为红色的虚线盒子时,黑色盒子在x和y方向的偏移量均为200。好巧不巧,有木有发现原点距离黑色box的上左边距均是400,转换后距离红色box的上左距离均为200,偏移量刚好就是200,这200是怎么计算得到的呢?

dx = (400 - 0) * 0.5 = 200 => (e.clientX - x方向偏移量) * scale

dy = (400 - 0) * 0.5 = 200 => (e.clientY - y方向偏移量) * scale

那么综上可以得出:

如果需要以鼠标为中心点缩放图层,则本质上是缩放图层的同时,图层偏离距离为原本的偏移值➕dx(dy)

实现🌰:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Title</title>
    <style>
      * {
        margin: 0;
        padding: 0;
      }
      .box {
        position: absolute;
        left: 0;
        top: 0;
        transform-origin: 0 0;
      }
      img {
        width: 300px;
        display: inline-block;
      }
    </style>
  </head>
  <body>
    <div class="box" style="transform: matrix(1, 0, 0, 1, 300, 300)">
      <img src="bg.png" />
    </div>
    <script>
      const boxEl = document.querySelector('.box');
      boxEl.style.left = boxEl.offsetLeft + 'px';
      boxEl.style.top = boxEl.offsetTop + 'px';
      document.addEventListener(
        'mousewheel',
        (e) => {
          e.preventDefault();
          const matrix = boxEl.style.transform
            .slice(7, -1)
            .split(', ')
            .map((num) => Number(num));
          if (e.ctrlKey) {
            const scale = 1 - e.deltaY / 100;
            const clientX = e.clientX;
            const clientY = e.clientY;
            const x = (e.deltaY / 100) * (clientX - matrix[4]) + matrix[4];
            const y = (e.deltaY / 100) * (clientY - matrix[5]) + matrix[5];
            const afterMatrix = `matrix(${matrix[0] * scale},0,0,${
              matrix[3] * scale
            },${x},${y})`;
            boxEl.style.transform = afterMatrix;
          } else {
            const x = matrix[4] - e.deltaX;
            const y = matrix[5] - e.deltaY;
            const afterMatrix = `matrix(${matrix[0]},0,0,${matrix[3]},${x},${y})`;
            boxEl.style.transform = afterMatrix;
          }
        },
        {
          passive: false,
        }
      );
    </script>
  </body>
</html>

方案2:代数转换

使用代数转换的话,需要一定的数学功底以及对矩阵中的每个参数有一定的了解才可以。 推荐传送门:transform中的matrix(必须知道这个)

矩阵的行列式 伴随矩阵 伴随矩阵求逆矩阵 例子

什么❓你说不想学习这么多,好勒!那咋们就套公式吧!

image.png

OK,前面逼逼叨了一大堆的理论知识,我都看烦了...

思路: 鼠标所处的点坐标(x, y)对应着dom上面的某一点的坐标,然而要求出这一点的坐标需要计算出当前视图矩阵的逆矩阵得出原坐标,得出原坐标之后将坐标值带入新的矩阵便得到新的坐标(x1, y1),有了两个点的坐标之后,就可以计算出从(x, y)到(x1, y1)的偏移量(dx=x-x1,dy=y-y1),将偏移量加到新矩阵的偏移量上面就可以得出咋们想要的答案啦。

image.png

具体代码实现:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Title</title>
    <style>
      * {
        margin: 0;
        padding: 0;
      }
      .box {
        position: absolute;
        left: 0;
        top: 0;
        transform-origin: 0 0;
      }
      img {
        width: 300px;
        display: inline-block;
      }
    </style>
  </head>
  <body>
    <div class="box" style="transform: matrix(1, 0, 0, 1, 300, 300)">
      <img src="bg.png" />
    </div>
    <script>
      const boxEl = document.querySelector('.box');
      document.addEventListener(
        'mousewheel',
        (e) => {
          const matrix = boxEl.style.transform
            .slice(7, -1)
            .split(', ')
            .map((num) => Number(num));
          if (e.ctrlKey) {
            const scale = (1 - e.deltaY / 100) * matrix[0];
            const vpt = calcVpt({ x: e.clientX, y: e.clientY }, scale, matrix);
            boxEl.style.transform = `matrix(${vpt}`;
          } else {
            const x = matrix[4] - e.deltaX;
            const y = matrix[5] - e.deltaY;
            const afterMatrix = `matrix(${matrix[0]},0,0,${matrix[3]},${x},${y})`;
            boxEl.style.transform = afterMatrix;
          }
          e.preventDefault();
        },
        { passive: false }
      );

      function calcVpt(point, value, curVpt) {
        let before = point;
        let vpt = curVpt.slice(0);
        point = transformPoint(point, invertTransform(curVpt));
        vpt[0] = value;
        vpt[3] = value;
        var after = transformPoint(point, vpt);
        vpt[4] += before.x - after.x;
        vpt[5] += before.y - after.y;
        return vpt;
      }

      function transformPoint(p, t, ignoreOffset) {
        if (ignoreOffset) {
          return { x: t[0] * p.x + t[2] * p.y, y: t[1] * p.x + t[3] * p.y };
        }
        return {
          x: t[0] * p.x + t[2] * p.y + t[4],
          y: t[1] * p.x + t[3] * p.y + t[5],
        };
      }

      function invertTransform(t) {
        // 矩阵行列式
        let a = 1 / (t[0] * t[3] - t[1] * t[2]);
        // 矩阵的逆
        let r = [a * t[3], -a * t[1], -a * t[2], a * t[0]];
        let o = transformPoint({ x: t[4], y: t[5] }, r, true);
        r[4] = -o.x;
        r[5] = -o.y;
        return r;
      }
    </script>
  </body>
</html>

总结

思路写得挺乱的基本上是想到哪写到哪儿,本来线代都已经还给老师了,搜一搜还是捡回来了部分,还好只是三阶运算,不然...... 思路二主要是看fabric.js的源码时候发现的,刚开始不太看得懂,后面看多了,发现越看越眼熟,后面就整理了一波笔记~