線性代數在前端中的應用(一):實現滑鼠滾輪縮放元素、Canvas圖片和拖拽

而井發表於2022-02-23

簡介

在前端開發中,有些時候會遇到根據滑鼠當前位置為原點,滾動滾輪實現圖片、canvas、DOM元素縮放的需求。有些同學可能覺得有點難,但其實藉助線性代數中的矩陣運算,可以非常容易地實現這一功能,更重要的是,數學作為一門學科,具有通用性,與具體的程式語言和環境無關,掌握好原理便可以實現通用性。

滑鼠滾輪縮放元素和圖片

縮放的本質

縮放的本質是矩陣變換。

當我們想縮放一個Div元素的時候,一般來說我們可以將其看成是對一個矩形的縮放。為了便於理解,我們這裡以一個最簡單的矩形的縮放為例子。如下圖我們假定有一個邊長都為4的矩形,我們以它的中心為原點,建立二維XY座標軸,可以得到如下圖:

01.png

當我們將矩形放大2倍,會得到一個邊長都為8的矩形,繼續以中心為原點,建立二維XY座標軸,可以得到下圖:

02.png

如果我們對這兩張圖的圖形座標點進行數學抽象,便可以得到以下兩個矩陣:

矩陣A:

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

矩陣B:

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

也就是說矩形放大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 $$

假如我們要縮放矩陣,那麼我們也需要找到類似的乘法規則,即一個矩陣和什麼樣的矩陣相乘可以得到它的倍數。還記得我們從幼兒園開始學習的數學知識麼?除了0這個特殊的數字外,我們認識這個數字的世界是從1開始,由1的相加、減得到其他數字,例如我們上面需要的2,可以由$$ 1 + 1 $$來獲得,那麼矩陣裡的那個1是什麼,便成為一件重要的事情。

矩陣裡的那個1——單位矩陣

矩陣的乘法中,有一種矩陣起著特殊的作用,如同數的乘法中的1,這種矩陣被稱為單位矩陣。它是個方陣,從左上角到右下角的對角線(稱為主對角線)上的元素均為1。除此以外全都為0。

2x2的單位矩陣$$ \left[ \begin{matrix} 1 & 0 \\ 0 & 1 \end{matrix} \right]$$,3x3的單位矩陣$$ \left[ \begin{matrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{matrix} \right]$$,4x4的單位矩陣$$ \left[ \begin{matrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1\\ \end{matrix} \right]$$

根據單位矩陣的特點,任何矩陣與單位矩陣相乘都等於本身。

那既然知道了什麼是"1",那"2"是什麼呢?其實不難猜出,例如2x2矩陣的"2"即為$$ \left[ \begin{matrix} 2 & 0 \\ 0 & 2 \end{matrix} \right]$$,也就是如果存在2x2矩陣$$ A = \left[ \begin{matrix} 1 & 0 \\ 0 & 1 \end{matrix} \right]$$,那麼如果$$ B = A * \left[ \begin{matrix} 2 & 0 \\ 0 & 2 \end{matrix} \right] $$,根據上文提到的矩陣乘法的計算規則,我們可以得到$$ B = \left[ \begin{matrix} 2 & 0 \\ 0 & 2 \end{matrix} \right] $$,那麼我們可以認為B矩陣是A矩陣放大後的2倍。

沿座標軸的縮放

上文提到將矩陣放大2倍的說法,是為了方便理解,實際上更準確地來講,是沿座標軸進行放大,因為除了沿座標軸縮放外,還可以沿任意方向縮放,例如朝著座標軸第一象限45度方向進行縮放。由於本文滑鼠滾輪縮放暫且不涉及到沿任意方向縮放,所以這個以後有空再寫文章來講解。

沿座標軸的2D縮放矩陣

如果存在一個矩陣為$$ M= \left[\begin{matrix} p & 0 \\ 0 & q \end{matrix}\right]$$,我們把它看成是2D座標軸上分別平行與X軸的向量p、平行與Y軸的向量q這兩個基向量。假定有2個縮放因子:\( k_{x} \)和\( k_{y} \),那麼有:

$$ p^{'}=k_{x}p=k_{x}\left[\begin{matrix} 1 & 0 \end{matrix}\right]=\left[\begin{matrix} k_{x} & 0 \end{matrix}\right] $$

$$ q^{'}=k_{y}p=k_{y}\left[\begin{matrix} 0 & 1 \end{matrix}\right]=\left[\begin{matrix} k_{y} & 0 \end{matrix}\right] $$

利用基向量構造矩陣,沿座標軸的2D縮放矩陣就如下:

$$ 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] $$

例如一個代表2D平面的矩陣\(M\)要在\(X\)軸放大2倍,\(Y\)軸縮小3倍,那麼就可以這樣做去獲得轉換後的矩陣\(M^{'}\):

$$ M^{'}=M*\left[ \begin{matrix} 2 & 0 \\ 0 & \frac{1}{3} \end{matrix} \right] $$

沿座標軸的3D縮放矩陣

對於3D,增加第三個縮放因子\(k_{z}\),沿座標軸的3D縮放矩陣就如下:

$$ 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] $$

沿座標軸的4D縮放矩陣

對於4D,增加第四個縮放因子\(k_{W}\),沿座標軸的4D縮放矩陣就如下:

$$ 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] $$

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

3D矩陣和2D矩陣相比,矩陣多了關於\(Z\)軸的表達,由於二維平面可以看成是在三維座標系中"被拍平的物體",我們需要給其一個\(Z\)軸值,但不能為0,此時\(Z\)軸的值為1

例如上文提及的2D矩陣A:$$\left[ \begin{matrix} -2 & 2 \\ 2 & 2 \\ 2 & -2 \\ -2 & -2 \\ \end{matrix} \right]$$,轉化為3D矩陣即為:$$\left[ \begin{matrix} -2 & 2 & 1 \\ 2 & 2 & 1 \\ 2 & -2 & 1 \\ -2 & -2 & 1 \\ \end{matrix} \right]$$

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

4D矩陣和2D矩陣相比,矩陣多了關於\(Z\)軸和\(W\)軸的表達。

例如上文提及的2D矩陣A:$$\left[ \begin{matrix} -2 & 2 \\ 2 & 2 \\ 2 & -2 \\ -2 & -2 \\ \end{matrix} \right]$$,轉化為4D矩陣即為:$$\left[ \begin{matrix} -2 & 2 & 1 & 1\\ 2 & 2 & 1 &1 \\ 2 & -2 & 1 & 1 \\ -2 & -2 & 1 & 1 \\ \end{matrix} \right]$$

矩陣計算庫gl-matrix

gl-matrix是一個用JavaScript語言編寫的開源矩陣計算庫。我們可以利用這個庫提供的矩陣之間的運算功能,來簡化、加速我們的開發。為了避免降低複雜度,後文採用原生ES6的語法,採用<script>標籤直接引用JS庫,不引入任何前端編譯工具鏈。

以滑鼠當前位置為原點縮放元素

前文我們已經將元素的縮放簡化成矩形的縮放,接下來繼續進行抽象,將矩形的縮放簡化為座標點在座標軸中的縮放,以點窺面。

假設在\(XY座標軸\)中有兩個座標點\(\left( -3,0 \right)\)和\(\left( 3,0 \right)\),它們之間的距離為6,如下圖:

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

將兩個座標點\(\left( -3,0 \right)\)和\(\left( 3,0 \right)\)以原點為中心、沿著\(X軸\)放大2倍延伸,可以得到新座標點\(\left( -6,0 \right)\)和\(\left( 6,0 \right)\),它們之間的距離為12,如下圖:

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

如果要保持放大後,維持兩個座標點的距離為12個單位,而\(X軸\)正方向那個座標點的位置不變,那麼我們需要在放大後,將兩個座標點沿著\(X軸\)向左平移3個單位,即-3,如下圖:

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

觀察可得:

$$ -3=3-3*2 = 3*(1-2) \\ 即: 縮放後在X/Y軸上偏移量=X/Y座標值*(1-縮放倍數) $$

其實上述的過程就是以當前滑鼠點為原點縮放圖形的過程抽象,即:先縮放圖形,然後把原來的縮放點平移回先前的位置。

4x4平移矩陣

由於3x3變換矩陣表示的是線性變換,不包含平移,但是在4D中,仍然可以用4x4矩陣的矩陣乘法來表達平移:

$$ \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] $$

矩陣計算表達先縮放後平移

假定現有矩陣\(v\),它先縮放再平移,縮放矩陣為$$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]$$,平移矩陣為$$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]$$,那麼:

$$v^{'}=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元素拖拽和縮放

由於矩陣乘法符合結合律,假定現有矩陣\(v\),它先縮放再平移,縮放矩陣為$$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]$$,平移矩陣為$$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]$$,故而有:

$$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]$$
下面是同時實現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,即可獲得原始碼連結,我們下次再見!

相關文章