6

线性代数在前端中的应用(一):实现鼠标滚轮缩放元素、Canvas图片和拖拽

 2 years ago
source link: https://segmentfault.com/a/1190000041443525
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

线性代数在前端中的应用(一):实现鼠标滚轮缩放元素、Canvas图片和拖拽

发布于 今天 08:59
English

在前端开发中,有些时候会遇到根据鼠标当前位置为原点,滚动滚轮实现图片、canvas、DOM元素缩放的需求。有些同学可能觉得有点难,但其实借助线性代数中的矩阵运算,可以非常容易地实现这一功能,更重要的是,数学作为一门学科,具有通用性,与具体的编程语言和环境无关,掌握好原理便可以实现通用性。

鼠标滚轮缩放元素和图片

缩放的本质

缩放的本质是矩阵变换。

当我们想缩放一个Div元素的时候,一般来说我们可以将其看成是对一个矩形的缩放。为了便于理解,我们这里以一个最简单的矩形的缩放为例子。如下图我们假定有一个边长都为4的矩形,我们以它的中心为原点,建立二维XY坐标轴,可以得到如下图:

01.png

当我们将矩形放大2倍,会得到一个边长都为8的矩形,继续以中心为原点,建立二维XY坐标轴,可以得到下图:

02.png

如果我们对这两张图的图形坐标点进行数学抽象,便可以得到以下两个矩阵:

[−22222−2−2−2] \left[ \begin{matrix} -2 & 2 \\ 2 & 2 \\ 2 & -2 \\ -2 & -2 \\ \end{matrix} \right]

⎣⎡​−222−2​22−2−2​⎦⎤​

[−44444−4−4−4] \left[ \begin{matrix} -4 & 4 \\ 4 & 4 \\ 4 & -4 \\ -4 & -4 \\ \end{matrix} \right]

⎣⎡​−444−4​44−4−4​⎦⎤​

也就是说矩形放大2倍这件事情,其实不过是矩阵A变换成矩阵B,这样我们就巧妙地将矩形缩放的问题,转化为矩阵之间的转换问题,可以借助矩阵数学公式进行抽象计算,接下来我们来了解下矩阵变换的基础:矩阵乘法。

A为的矩阵,B为的矩阵,那么称的矩阵C为矩阵AB的乘积,记作,其中矩阵C中的第行第列元素可以表示为:

如下所示:

还有一个原则需要特别注意的是:仅当矩阵A列数(column)等于矩阵B行数(row)时,A与B才可以相乘,否则不能矩阵相乘,这一点要切记!因为后面因为这个原则和方便计算,我们会把4x2矩阵转为4x4矩阵。

为了便于理解,这里截取了《3D数学基础:图形与游戏开发》这本书中关于3x3矩阵乘法的介绍,辅助大家理解和回忆矩阵乘法的具体细节。

矩阵乘法.jpg

当讨论变换时,在数学上一般用到函数(也称映射),即接受输入,产生输出。我们可以把abF函数/映射记为F(a)=b。要利用数学工具来解决矩阵之间变换(缩放是变换的一种,其他还有平移、旋转、切变等),最简单的方式也就是找到矩阵表达的映射,以及其运算规则。

在小学时,我们都学过数学的四则运算,例如现在存在一个数a,如果我们想要把a变成原来2倍,我们会使用:

a′=a∗2 a' = a * 2 a′=a∗2

假如我们要缩放矩阵,那么我们也需要找到类似的乘法规则,即一个矩阵和什么样的矩阵相乘可以得到它的倍数。还记得我们从幼儿园开始学习的数学知识么?除了0这个特殊的数字外,我们认识这个数字的世界是从1开始,由1的相加、减得到其他数字,例如我们上面需要的2,可以由1+1 1 + 1 1+1来获得,那么矩阵里的那个1是什么,便成为一件重要的事情。

矩阵里的那个1——单位矩阵

矩阵的乘法中,有一种矩阵起着特殊的作用,如同数的乘法中的1,这种矩阵被称为单位矩阵。它是个方阵,从左上角到右下角的对角线(称为主对角线)上的元素均为1。除此以外全都为0。

2x2的单位矩阵[1001] \left[ \begin{matrix} 1 & 0 \\ 0 & 1 \end{matrix} \right][10​01​],3x3的单位矩阵[100010001] \left[ \begin{matrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{matrix} \right]⎣⎡​100​010​001​⎦⎤​,4x4的单位矩阵[1000010000100001] \left[ \begin{matrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1\\ \end{matrix} \right]⎣⎡​1000​0100​0010​0001​⎦⎤​

根据单位矩阵的特点,任何矩阵与单位矩阵相乘都等于本身。

那既然知道了什么是"1",那"2"是什么呢?其实不难猜出,例如2x2矩阵的"2"即为[2002] \left[ \begin{matrix} 2 & 0 \\ 0 & 2 \end{matrix} \right][20​02​],也就是如果存在2x2矩阵A=[1001] A = \left[ \begin{matrix} 1 & 0 \\ 0 & 1 \end{matrix} \right]A=[10​01​],那么如果B=A∗[2002] B = A * \left[ \begin{matrix} 2 & 0 \\ 0 & 2 \end{matrix} \right] B=A∗[20​02​],根据上文提到的矩阵乘法的计算规则,我们可以得到B=[2002] B = \left[ \begin{matrix} 2 & 0 \\ 0 & 2 \end{matrix} \right] B=[20​02​],那么我们可以认为B矩阵是A矩阵放大后的2倍。

沿坐标轴的缩放

上文提到将矩阵放大2倍的说法,是为了方便理解,实际上更准确地来讲,是沿坐标轴进行放大,因为除了沿坐标轴缩放外,还可以沿任意方向缩放,例如朝着坐标轴第一象限45度方向进行缩放。由于本文鼠标滚轮缩放暂且不涉及到沿任意方向缩放,所以这个以后有空再写文章来讲解。

沿坐标轴的2D缩放矩阵

如果存在一个矩阵为M=[p00q] M= \left[\begin{matrix} p & 0 \\ 0 & q \end{matrix}\right]M=[p0​0q​],我们把它看成是2D坐标轴上分别平行与X轴的向量p、平行与Y轴的向量q这两个基向量。假定有2个缩放因子:kx k_{x} kx​和ky k_{y} ky​,那么有:

p′=kxp=kx[10]=[kx0] p^{'}=k_{x}p=k_{x}\left[\begin{matrix} 1 & 0 \end{matrix}\right]=\left[\begin{matrix} k_{x} & 0 \end{matrix}\right] p′=kx​p=kx​[1​0​]=[kx​​0​]

q′=kyp=ky[01]=[ky0] q^{'}=k_{y}p=k_{y}\left[\begin{matrix} 0 & 1 \end{matrix}\right]=\left[\begin{matrix} k_{y} & 0 \end{matrix}\right] q′=ky​p=ky​[0​1​]=[ky​​0​]

利用基向量构造矩阵,沿坐标轴的2D缩放矩阵就如下:

S(kx,ky)=[p′q′]=[kx00ky] S(k_{x},k_{y})=\left[ \begin{matrix} p^{'} \\ q^{'} \\ \end{matrix} \right]=\left[ \begin{matrix} k_{x} & 0 \\ 0 & k_{y} \end{matrix} \right] S(kx​,ky​)=[p′q′​]=[kx​0​0ky​​]

例如一个代表2D平面的矩阵MMM要在XXX轴放大2倍,YYY轴缩小3倍,那么就可以这样做去获得转换后的矩阵M′M^{'}M′:

M′=M∗[20013] M^{'}=M*\left[ \begin{matrix} 2 & 0 \\ 0 & \frac{1}{3} \end{matrix} \right] M′=M∗[20​031​​]

沿坐标轴的3D缩放矩阵

对于3D,增加第三个缩放因子kzk_{z}kz​,沿坐标轴的3D缩放矩阵就如下:

S(kx,ky,kz)=[kx000ky000kz] S(k_{x},k_{y},k_{z})=\left[ \begin{matrix} k_{x} & 0 & 0 \\ 0 & k_{y} & 0 \\ 0 & 0 & k_{z} \end{matrix} \right] S(kx​,ky​,kz​)=⎣⎡​kx​00​0ky​0​00kz​​⎦⎤​

沿坐标轴的4D缩放矩阵

对于4D,增加第四个缩放因子kWk_{W}kW​,沿坐标轴的4D缩放矩阵就如下:

S(kx,ky,kz,kw)=[kx0000ky0000kz0000kw] S(k_{x},k_{y},k_{z},k_{w})=\left[ \begin{matrix} k_{x} & 0 & 0 & 0 \\ 0 & k_{y} & 0 & 0 \\ 0 & 0 & k_{z} & 0 \\ 0 & 0 & 0 & k_{w} \end{matrix} \right] S(kx​,ky​,kz​,kw​)=⎣⎡​kx​000​0ky​00​00kz​0​000kw​​⎦⎤​

如何用3D矩阵表示2D矩阵?

3D矩阵和2D矩阵相比,矩阵多了关于ZZZ轴的表达,由于二维平面可以看成是在三维坐标系中"被拍平的物体",我们需要给其一个ZZZ轴值,但不能为0,此时ZZZ轴的值为1

例如上文提及的2D矩阵A:[−22222−2−2−2]\left[ \begin{matrix} -2 & 2 \\ 2 & 2 \\ 2 & -2 \\ -2 & -2 \\ \end{matrix} \right]⎣⎡​−222−2​22−2−2​⎦⎤​,转化为3D矩阵即为:[−2212212−21−2−21]\left[ \begin{matrix} -2 & 2 & 1 \\ 2 & 2 & 1 \\ 2 & -2 & 1 \\ -2 & -2 & 1 \\ \end{matrix} \right]⎣⎡​−222−2​22−2−2​1111​⎦⎤​

如何用4D矩阵表示2D矩阵?

4D矩阵和2D矩阵相比,矩阵多了关于ZZZ轴和WWW轴的表达。

例如上文提及的2D矩阵A:[−22222−2−2−2]\left[ \begin{matrix} -2 & 2 \\ 2 & 2 \\ 2 & -2 \\ -2 & -2 \\ \end{matrix} \right]⎣⎡​−222−2​22−2−2​⎦⎤​,转化为4D矩阵即为:[−221122112−211−2−211]\left[ \begin{matrix} -2 & 2 & 1 & 1\\ 2 & 2 & 1 &1 \\ 2 & -2 & 1 & 1 \\ -2 & -2 & 1 & 1 \\ \end{matrix} \right]⎣⎡​−222−2​22−2−2​1111​1111​⎦⎤​

矩阵计算库gl-matrix

gl-matrix是一个用JavaScript语言编写的开源矩阵计算库。我们可以利用这个库提供的矩阵之间的运算功能,来简化、加速我们的开发。为了避免降低复杂度,后文采用原生ES6的语法,采用<script>标签直接引用JS库,不引入任何前端编译工具链。

以鼠标当前位置为原点缩放元素

前文我们已经将元素的缩放简化成矩形的缩放,接下来继续进行抽象,将矩形的缩放简化为坐标点在坐标轴中的缩放,以点窥面。

假设在XY坐标轴XY坐标轴XY坐标轴中有两个坐标点(−3,0)\left( -3,0 \right)(−3,0)和(3,0)\left( 3,0 \right)(3,0),它们之间的距离为6,如下图:

(3,0)和(-3,0).png

将两个坐标点(−3,0)\left( -3,0 \right)(−3,0)和(3,0)\left( 3,0 \right)(3,0)以原点为中心、沿着X轴X轴X轴放大2倍延伸,可以得到新坐标点(−6,0)\left( -6,0 \right)(−6,0)和(6,0)\left( 6,0 \right)(6,0),它们之间的距离为12,如下图:

(6,0)和(-6,0).png

如果要保持放大后,维持两个坐标点的距离为12个单位,而X轴X轴X轴正方向那个坐标点的位置不变,那么我们需要在放大后,将两个坐标点沿着X轴X轴X轴向左平移3个单位,即-3,如下图:

(3,0)和(-9,0).png

观察可得:

−3=3−3∗2=3∗(1−2)即:缩放后在X/Y轴上偏移量=X/Y坐标值∗(1−缩放倍数) -3=3-3*2 = 3*(1-2) \\ 即: 缩放后在X/Y轴上偏移量=X/Y坐标值*(1-缩放倍数) −3=3−3∗2=3∗(1−2)即:缩放后在X/Y轴上偏移量=X/Y坐标值∗(1−缩放倍数)

其实上述的过程就是以当前鼠标点为原点缩放图形的过程抽象,即:先缩放图形,然后把原来的缩放点平移回先前的位置。

4x4平移矩阵

由于3x3变换矩阵表示的是线性变换,不包含平移,但是在4D中,仍然可以用4x4矩阵的矩阵乘法来表达平移:

[xyz1][100001000010ΔxΔyΔz1]=[x+Δxy+Δyz+Δz1] \left[\begin{matrix}x &y &z &1 \end{matrix}\right]\left[\begin{matrix}1 &0 &0 &0\\ 0&1&0&0\\0&0&1&0\\\Delta x &\Delta y &\Delta z&1 \end{matrix}\right]=\left[\begin{matrix}x+\Delta x &y+\Delta y &z+\Delta z &1 \end{matrix}\right] [x​y​z​1​]⎣⎡​100Δx​010Δy​001Δz​0001​⎦⎤​=[x+Δx​y+Δy​z+Δz​1​]

矩阵计算表达先缩放后平移

假定现有矩阵vvv,它先缩放再平移,缩放矩阵为R=[kx0000ky0000kz0000kw]R=\left[ \begin{matrix} k_{x} & 0 & 0 & 0 \\ 0 & k_{y} & 0 & 0 \\ 0 & 0 & k_{z} & 0 \\ 0 & 0 & 0 & k_{w} \end{matrix} \right]R=⎣⎡​kx​000​0ky​00​00kz​0​000kw​​⎦⎤​,平移矩阵为T=[100001000010ΔxΔyΔz1]T=\left[\begin{matrix}1 &0 &0 &0\\ 0&1&0&0\\0&0&1&0\\\Delta x &\Delta y &\Delta z&1 \end{matrix}\right]T=⎣⎡​100Δx​010Δy​001Δz​0001​⎦⎤​,那么:

v′=v∗R∗Tv^{'}=v*R*Tv′=v∗R∗T

矩阵实现Div元素以鼠标为原点进行缩放

假定现在页面有一个IDappdiv元素,位于页面中间位置,代码如下:

<!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>矩阵缩放Div</title>
    <style>
        *,
        *::before,
        *::after {
            box-sizing: border-box;
        }

        body {
            position: relative;
            background-color: #eee;
            min-height: 1000px;
            margin: 0;
            padding: 0;
        }

        #app {
            position: absolute;
            left: 50%;
            top: 50%;
            transform: translate(-50%, -50%);
            width: 200px;
            height: 200px;
            border: 1px dashed black;
        }
    </style>
</head>

<body>
    <div id="app"></div>
    <script src="./gl-matrix-min.js"></script>
    <script src="./index.js"></script>
</body>

</html>

布局效果如下:

布局效果.png

首先我们需要获得关于Div元素位置信息和宽高信息,用它们来组成矩阵,这个可以借助# Element.getBoundingClientRect()这个api。

然后监听div#app鼠标滚动事件,滚动时,根据事件对象的deltaY的值来判断是放大还是缩小,这里为了和Windows系统原生缩放方向保持一致,选择滚轮向下滚动时缩小,滚轮向上滚动时放大,即deltaY的值小于0时放大,小于0时缩小。

矩阵变换乘法,这里由于我们是采用4x4矩阵,所以可以利用glMatrix.mat4.multiply这个api,故有代码如下:

document.addEventListener("DOMContentLoaded", () => {
    const $app = document.querySelector(`#app`);

    $app.addEventListener("wheel", (e) => {
        const {clientX, clientY, deltaY } = e;
        let scale = 1 + (deltaY < 0 ? 0.1 : -0.1);
        scale = Math.max(scale > 0 ? scale : 1, 0.1);
        const {top, right, bottom, left}   = $app.getBoundingClientRect();
        const o = new Float32Array([
            left, top, 1, 1,
            right, top, 1, 1,
            right, bottom, 1, 1,
            left, bottom, 1, 1
        ]);
        const x = clientX * (1 - scale);
        const y = clientY * (1 - scale);
        const t = new Float32Array([
            scale, 0, 0, 0,
            0, scale, 0, 0,
            0, 0, 1, 0,
            0, 0, 0, 1
        ]);
        const m = new Float32Array([
            1, 0, 0, 0,
            0, 1, 0, 0,
            0, 0, 1, 0,
            x, y, 0, 1
        ]);
        // 在XY轴上进行缩放
        let res1 = glMatrix.mat4.multiply(new Float32Array([
            0, 0, 0, 0,
            0, 0, 0, 0,
            0, 0, 0, 0,
            0, 0, 0, 0
        ]), t, o);
        // 在XY轴上进行平移
        const res2 = glMatrix.mat4.multiply(new Float32Array([
            0, 0, 0, 0,
            0, 0, 0, 0,
            0, 0, 0, 0,
            0, 0, 0, 0
        ]), m, res1);
        $app.setAttribute("style", `left: ${res2[0]}px; top: ${res2[1]}px;width: ${res2[4] - res2[0]}px;height: ${res2[9] - res2[1]}px;transform: none;`);
    });
});

效果如下图:

鼠标原点缩放.gif

矩阵实现Div元素拖拽

用矩阵实现Div元素拖拽和我们平时实现拖拽的代码差不多,只是将绝对定位信息数据组成平移矩阵,具体代码如下:

document.addEventListener("DOMContentLoaded", () => {
    const $app = document.querySelector(`#app`);
    const width = $app.offsetWidth;
    const height = $app.offsetHeight;
    let isDrag = false;
    let x; // 鼠标拖拽时鼠标的横坐标值
    let y; // 鼠标拖拽时鼠标的纵坐标值
    let left; // 元素距离页面左上角顶点的横坐标偏移值
    let top; // 元素距离页面左上角顶点的纵坐标偏移值
    
    $app.addEventListener("mousedown", (e) => {
        const bcr = $app.getBoundingClientRect();
        isDrag = true;
        x = e.clientX;
        y = e.clientY;
        left = bcr.left + window.scrollX;
        top = bcr.top + window.scrollY;
    });
    document.addEventListener("mousemove", (e) => {
        if (!isDrag) {
            return;
        }
        const {clientX, clientY} = e;
        const movementX = clientX - (x - left); // 计算出X轴的偏移量
        const movementY = clientY - (y - top); // 计算出Y轴的偏移量
        // 平移矩阵
        const t = new Float32Array([
            movementX, movementY
        ]);
        // 计算出相对于页面左上角的绝对定位的矩阵
        const res = glMatrix.mat2.add(new Float32Array([0, 0]),  t, new Float32Array([0, 0]));
        $app.setAttribute("style", `left: ${res[0]}px;top:${res[1]}px;width:${width}px;height:${height}px;transform: none;`);
    })
    document.addEventListener("mouseup", () => {
        isDrag = false;
    });
});

矩阵同时实现Div元素拖拽和缩放

由于矩阵乘法符合结合律,假定现有矩阵vvv,它先缩放再平移,缩放矩阵为R=[kx0000ky0000kz0000kw]R=\left[ \begin{matrix} k_{x} & 0 & 0 & 0 \\ 0 & k_{y} & 0 & 0 \\ 0 & 0 & k_{z} & 0 \\ 0 & 0 & 0 & k_{w} \end{matrix} \right]R=⎣⎡​kx​000​0ky​00​00kz​0​000kw​​⎦⎤​,平移矩阵为T=[100001000010ΔxΔyΔz1]T=\left[\begin{matrix}1 &0 &0 &0\\ 0&1&0&0\\0&0&1&0\\\Delta x &\Delta y &\Delta z&1 \end{matrix}\right]T=⎣⎡​100Δx​010Δy​001Δz​0001​⎦⎤​,故而有:

v′=v∗R∗T=v∗([kx0000ky0000kz0000kw][100001000010ΔxΔyΔz1])=v∗[kx0000ky0000kz0ΔxΔyΔzkw]v^{'}=v*R*T=v*(\left[ \begin{matrix} k_{x} & 0 & 0 & 0 \\ 0 & k_{y} & 0 & 0 \\ 0 & 0 & k_{z} & 0 \\ 0 & 0 & 0 & k_{w} \end{matrix} \right]\left[\begin{matrix}1 &0 &0 &0\\ 0&1&0&0\\0&0&1&0\\\Delta x &\Delta y &\Delta z&1 \end{matrix}\right])=v*\left[ \begin{matrix} k_{x} & 0 & 0 & 0 \\ 0 & k_{y} & 0 & 0 \\ 0 & 0 & k_{z} & 0 \\ \Delta x &\Delta y &\Delta z & k_{w} \end{matrix} \right]v′=v∗R∗T=v∗(⎣⎡​kx​000​0ky​00​00kz​0​000kw​​⎦⎤​⎣⎡​100Δx​010Δy​001Δz​0001​⎦⎤​)=v∗⎣⎡​kx​00Δx​0ky​0Δy​00kz​Δz​000kw​​⎦⎤​
下面是同时实现Div元素拖拽和缩放的代码:

document.addEventListener("DOMContentLoaded", () => {
    const $app = document.querySelector(`#app`);
    let isDrag = false;
    let x; // 鼠标拖拽时鼠标的横坐标值
    let y; // 鼠标拖拽时鼠标的纵坐标值
    let left; // 元素距离页面左上角顶点的横坐标偏移值
    let top; // 元素距离页面左上角顶点的纵坐标偏移值


    function reDraw(el, t, move=false) {
        const bcr = el.getBoundingClientRect();
        const {width, height} = bcr;
        const o = new Float32Array([
            bcr.left, bcr.top, 1, 1,
            bcr.right, bcr.top, 1, 1,
            bcr.right, bcr.bottom, 1, 1,
            bcr.left, bcr.bottom, 1, 1,
        ]);
        const out = new Float32Array([
            0, 0, 0, 0, 
            0, 0, 0, 0, 
            0, 0, 0, 0, 
            0, 0, 0, 0,
        ]);
        const res = glMatrix.mat4.multiply(out,  t, o);
        const left = parseInt(res[0]);
        const top = parseInt(res[1]);
        // 如果是移动,那么不需要调整宽高
        const w = move ?  width : res[4] - left;
        const h = move ? height : res[9] - top;
        el.setAttribute("style", `left: ${left}px;top:${top}px;width:${w}px;height:${h}px;transform: none;`);
    }

    $app.addEventListener("mousedown", (e) => {
        const bcr = $app.getBoundingClientRect();
        isDrag = true;
        x = e.clientX;
        y = e.clientY;
        left = bcr.left + window.scrollX;
        top = bcr.top + window.scrollY;
    });
    document.addEventListener("mousemove", (e) => {
        if (!isDrag) {
            return;
        }
        const {clientX, clientY} = e;
        const movementX = clientX - (x - left); // 计算出X轴的偏移量
        const movementY = clientY - (y - top); // 计算出Y轴的偏移量
        // 4x4平移矩阵
        const t = new Float32Array([
            0, 0, 0, 0,
            0, 0, 0, 0,
            0, 0, 0, 0,
            movementX, movementY, 0, 1
        ]);
        reDraw($app, t, true);
    })
    document.addEventListener("mouseup", () => {
        isDrag = false;
    });
    $app.addEventListener("wheel", (e) => {
        const {clientX, clientY, deltaY } = e;
        const currSacle = 1 + (deltaY < 0 ? 0.1 : -0.1);
        const zoom = Math.max(currSacle > 0 ? currSacle : 1, 0.1);
        const x = (clientX + window.scrollX) * (1 - zoom);
        const y = (clientY + window.scrollY) * (1 - zoom);
        const t = new Float32Array([
            zoom, 0, 0, 0,
            0, zoom, 0, 0,
            0, 0, 1, 0,
            x, y, 0, 1,
        ]);
        reDraw($app, t);
    });
});

矩阵同时实现Canvas图片拖拽和缩放

Canvas图片拖拽和缩放的逻辑,和普通Div的拖拽和缩放的逻辑基本一致,不一样的地方在于我们要修改的是Canvas渲染的当前变换的矩阵,初始时为单位矩阵,我们只需要进行对应的矩阵变换,设置新的变换矩阵,交给Canvas底层渲染即可。具体代码如下:

<!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>Canvas缩放和拖拽</title>
    <style>
        body {
            position: relative;
            background-color: black;
            min-height: 1000px;
            margin: 0;
            padding: 0;
        }

        #app {
            border:1px solid white;
        }
    </style>
</head>
<body>
    <canvas id="app" width="640" height="340"></canvas>
    <script src="./gl-matrix-min.js"></script>
    <script src="./index.js"></script>
</body>
</html>
// index.js
document.addEventListener("DOMContentLoaded", () => {
    const $app = document.querySelector(`#app`);
    const {width, height} = $app.getBoundingClientRect();
    const ctx = $app.getContext("2d");
    const $img = document.createElement("img");
    $img.onload = () => {
        ctx.drawImage($img, 0, 0);
    };
    $img.src = "./01.png";
    let isDrag = false;
    let ov = new Float32Array([
            1, 0, 0, 0,
            0, 1, 0, 0,
            0, 0, 1, 0,
            0, 0, 0, 1,
    ]);

    function reDraw(ctx, o, t) {
        const out = new Float32Array([
            0, 0, 0, 0, 
            0, 0, 0, 0, 
            0, 0, 0, 0, 
            0, 0, 0, 0,
        ]);
        const nv = glMatrix.mat4.multiply(out,  t, o);
        ctx.save();
        ctx.clearRect(0, 0, width, height);
        ctx.transform(nv[0], nv[4], nv[1], nv[5], nv[12], nv[13]);
        ctx.drawImage($img, 0, 0);
        ctx.restore();
        return nv;
    }

    $app.addEventListener("mousedown", (e) => {
        isDrag = true;
    });

    document.addEventListener("mousemove", (e) => {
        if (!isDrag) {
            return;
        }
        const {movementX, movementY} = e;
        const t = new Float32Array([
            1, 0, 0, 0,
            0, 1, 0, 0,
            0, 0, 1, 0,
            movementX, movementY, 0, 1,
        ]);
        ov = reDraw(ctx, ov, t);
    });

    document.addEventListener("mouseup", (e) => {
        isDrag = false;
    });

    $app.addEventListener("wheel", (e) => {
        const {clientX, clientY, deltaY } = e;
        const currSacle = 1 + (deltaY < 0 ? 0.1 : -0.1);
        const zoom = Math.max(currSacle > 0 ? currSacle : 1, 0.1);
        const x = clientX * (1 - zoom);
        const y = clientY * (1 - zoom);
        const t = new Float32Array([
            zoom, 0, 0, 0,
            0, zoom, 0, 0,
            0, 0, 1, 0,
            x, y, 0, 1,
        ]);
        ov = reDraw(ctx, ov, t);
    });
});

canvas鼠标原点缩放.gif

这是一个关于线性代数在前端中运用的系列文章,接下来会分享线性代数更多的实用文章。

由于本人的数学水平一般,行文中难免有错误的地方,写这片文章的意义更多的是进行知识整理,方便日后回顾,如果能够引起你对数学在前端中运用的兴趣,那就更加好了,特别是对于和我一样的后台管理系统表单前端工程师,在表单之外寻找到其他的乐趣。

如果大家想要获得样例中完整的源代码,可以微信搜索前端列车长,关注后回复20220222,即可获得源代码链接,我们下次再见!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK