如何應用 matrix3d 對映變幻

池中物王二狗發表於2024-06-20

如何應用 matrix3d 對映變幻

先上 demo

記得是在 2015 看到過的一個 html5 演示效果, 很驚豔

當時沒明白如何實現,現在我會了,做一個類似的:

image

又弄了一個拖動的 demo

image

我數學真的很差

“你好老師!學這個矩陣具體有什麼用?”

老師喝著水貌似想了一會兒回答:“考試用”..

這個問題我真問過老師

在校期間數學課上學到矩陣行列式相關的課程時,曾經問過當時的數學老師,學這個做啥用的,老師猶豫了半天給我的答案是 “考試用”

果然.. 最後我的成績是不及格

當然不能怪老師,我本來的數學成績一直也是墊底的

對於我這樣的學渣來說,學習不能學以致用就像玩的遊戲沒有及時正反饋一樣的難受

我是真的很難提起興趣去學

為啥選了前端開發這個坑呢?就是因為程式碼寫了重新整理一下瀏覽器就能看到效果,真實時反饋

直到工作多年以後,學到圖形學相關的知識才一知半解

為啥是一知半解呢,因為知道有這個東西,也知道用在哪裡,可就是不會用,至少在大部分的前端開發工作上用不到

能在網上找到的相關應用就是 css 中利用 transform 對某個元素進行 旋轉,平移,縮放,傾斜等, 這些基礎應用都被講爛了

像這樣:

.box {
    transform-origin:0 0;
    transform: rotate(10deg) translateX(30px)  scale(1.1, 1.1) skew(3deg, 3deg);
}

image

transform 後:

image

但你應該明白它最終是以矩陣形式表達的,八股文中有沒有要背的我不知道,畢竟這些知識很多人已經講過了。當然如果你這都不知道,可能八股還得再八股一下

透過火狐的開發者工具檢視 computed 皮膚中可以看到其實轉換成了對應的 matrix

image

結果 matrix:

image

線性變幻 linear transform 包括以下幾種

  1. scale 縮放
  2. rotate 旋轉
  3. skew 斜切
  4. translate 平移

用矩陣形式表達如下圖示:

image

image

這些都可以組合在一起形成 單個 matrix 矩陣實現變幻,

這在之前的我的 "EaselJS 原始碼分析系列--第二篇" 中有提到過,寬高,旋轉,斜切,轉換成 matrix 後一次變幻到位

那麼它到底是怎麼應用 matrix 變幻的呢?

拿上面的例子舉例: 實質是對綠色 box 四個座標 (0,0)、(200,0)、(0,200)、(200,200) 分別點乘了 (Dot product) matrix 矩陣:

matrix(1.07328, 0.247786, -0.13424, 1.0933, 29.5442, 5.20945)

得到新座標:

(29,5), (244.2002, 54.76665), (2.6962, 223.86945), (217, 273)

image

怎麼點乘?

說到點乘簡單複習一下大學數學基礎知識(看看就行了,你不需要自己算)

2 · 2

image

3 · 3

image

雖然你不用自己算,但 matrix 函式引數得搞明白

注意函式 matrix 引數 a, b, c, d, tx, ty 對應的位置:

matrix(a, b, c, d, tx, ty) 

image

手動計算是不可能的我們可以使用 numeric.js 數學庫用於點乘計算:

// 在 numeric.dot 中的位置
numeric.dot([
    [a, c, tx],
    [b, d ty],
    [0, 0, 1]
]
, [0, 0, 1]
)

這是上面四個座標的計算:

// matrix(1.07328, 0.247786, -0.13424, 1.0933, 29.5442, 5.20945);
// 位置 (0, 0)
const pos1 = numeric.dot([
    [1.07328, -0.13424, 29.5442],
    [0.247786, 1.0933, 5.20945],
    [0, 0, 1]]
    , [0, 0, 1])

// 位置 (200 ,0 )
const pos2 = numeric.dot([
    [1.07328, -0.13424, 29.5442],
    [0.247786, 1.0933, 5.20945],
    [0, 0, 1]]
    , [200, 0, 1])

// 位置 (0, 200) 
const pos3 = numeric.dot([
    [1.07328, -0.13424, 29.5442],
    [0.247786, 1.0933, 5.20945],
    [0, 0, 1]]
    , [0, 200, 1])

// 位置 (200, 200) 
const pos4 = numeric.dot([
    [1.07328, -0.13424, 29.5442],
    [0.247786, 1.0933, 5.20945],
    [0, 0, 1]]
    , [200, 200, 1])

console.log(pos1, pos2, pos3, pos4)

// (29,5), (244.2002, 54.76665), (2.6962, 223.86945), (217, 273)

講了這麼多,雖然看起來很厲害,但這又有什麼卵用呢?

再仔細想想,想要仿射變換直接使用 matrix 是符合直覺的

舊座標 · matrix = 新座標

所以想要自由就得用到 matrix

“仿射變換”的理論基礎

先要進行億點點線性代數運算

我們現在給四個角(座標點)對應編號(“舊座標”):

(x1,y1), (x2,y2) , (x3,y3) , (x4,y4)

目的是將它們對映(變幻)到對應的(“新座標”):

(u1,v1), (u2,v2) , (u3,v3) , (u4,v4)

即將座標 (xi, yi) 對映到 (ui, vi)

座標 (x, y) 被表示為 (kx, ky, k), k 不為 0

在齊次座標中 (3,2,1) 和 (6,4,2) 都可以表示 (3,2)

image

於是我們要求得轉置矩陣 H 要滿足上面等式中所有已知的角 (xi, yi), (ui, vi)

image

滿足 H 的值不是唯一的,舉例,給 H 縮放乘以一個常數,結果矩陣依然會對映對應的點(右側的 ki 也是一樣的)

為了簡化問題設假定將兩邊都縮放直到 h8 為 1 (簡化問題計算)

然後將乘數乘進去

image

現在將第3行等式代入前兩行把 ki 去掉先

image

記住我們要解決的是 hi 所以我們應該嘗試先把它們分開來

image

兩個等式中 h0..h7 空出缺少的部分用 0 填充 (為何要填充:H 等於 ki 若要等式相等則 H 必須是 h0..h7 完整)

將 h 提出來來,用矩形形式表示就是:

image

由於我們要表示的是四個座標點的對映,所以我們可以寫成這樣:

image

至此已經可以了,就是一個 Ah=b 的問題(即矩陣中常見的 Ax=b),可以用線性代數庫(比如:numeric.js 的 numeric.solve)來求解 h ,解得的 h 對應的 hi 用於 transform 形變矩陣

image

最後一個小問題,就是 Matrix3d 需要的是 4x4 的矩陣,我們從開始就忽略掉了 z 軸值(由於四個點都在同一個平面,所以 z = 0), 所以把 z 重新對映回矩陣

matrix3d(a, b, 0, 0, c, d, 0, 0, 0, 0, 1, 0, tx, ty, 0, 1)

image

image

這就是最後用於 css 上的 matrix3d 的矩陣

將其應用到 HTML/Javascript 內

建立一個 id 為 box 的 div 作為變化源

建立一個 id 為 targetBox 的 div 作為目標

  1. 建立一個 getPoints 用於獲取四個點座標

    ... 省略獲取 targetBox,box 的程式碼
    
    function getPoints(element){
         const rect = element.getBoundingClientRect();
         return [
             [rect.left, rect.top],
             [rect.left, rect.bottom],
             [rect.right, rect.top],
             [rect.right, rect.bottom],
         ];
     }
    
     const target = getPoints(targetBox)
     const origin = getPoints(box)
    
  2. 分別轉換成相對座標點

    function getFromPoints(points){
     // 對映前四個點相對座標 
     const result = [];
     const len = points.length;
     for (let k = 0; k < len; k++) {
         let p = points[k];
         result.push({
         x: p[0] - points[0][0],
         y: p[1] - points[0][1]
         });
     }
     /**
         result 
         [
         {x1, y1},
         {x2, y2},
         {x3, y3},
         {x4, y4},
         ]
     */
     return result;
     }
     function getToPoints(origin, target){
         // 對映後四個點相對座標 
         const result = [];
         for (let k = 0, len = target.length; k < len; k++) {
             p = target[k];
             result.push({
             x: p[0] - origin[0][0],
             y: p[1] - origin[0][1]
             });
         }
         return result;
     }
    
     const from = getFromPoints(origin);
     const to = getToPoints(origin, target);
    
  3. 透過 from 與 to 獲取 H

    function getTransform(from, to) {
         var A, H, b, h;
         A = []; // 8x8
         // 四個點的座標
         for (let i =0 ; i < 4; i++) {
           A.push([from[i].x, from[i].y, 1, 0, 0, 0, -from[i].x * to[i].x, -from[i].y * to[i].x]);
           A.push([0, 0, 0, from[i].x, from[i].y, 1, -from[i].x * to[i].y, -from[i].y * to[i].y]);
         }
         b = []; // 8x1
         for (let i = 0; i < 4; i++) {
           b.push(to[i].x);
           b.push(to[i].y);
         }
         // Solve A * h = b for h
         // 即矩陣中常見的 Ax=b
         // numeric.solve eg:
         // IN> numeric.solve([[1,2],[3,4]],[17,39])
         // OUT> [5,6]
         // https://ccc-js.github.io/numeric2/documentation.html
         h = numeric.solve(A, b);
    
         /**
           解得: h matrix
           [
             h0, h1, 0 h2
             h3, h4, 0 h5
             0,  0, 0, 1
             h6, h7, 0 h8
           ]
         */
         H = [
               [h[0], h[1], 0, h[2]],
               [h[3], h[4], 0, h[5]],
               [0, 0, 1, 0],
               [h[6], h[7], 0, 1]
             ];
         return H;
     };
    
     const H = getTransform(from, to);
    
  4. 生成 css 值應用到 div 上

    function getMatrixCSSParameters(H){
         // 獲取 css matrix3d(a, b, 0, 0, c, d, 0, 0, 0, 0, 1, 0, tx, ty, 0, 1) 引數
         const result = [];
         for (let i =0; i < 4; i++) {
             const  result1 = [];
             for (let j = 0; j < 4; j++) {
             result1.push(H[j][i].toFixed(20));
             }
             result.push(result1);
         }
         return result.join(',');
     }
    
     div.style.transform = `matrix3d(${getMatrixCSSParameters(H)})`;
    

走了一大圈,現在終於可以實現“仿射變換”了

用上面的程式碼實現 demo

demo 1 matrix3d 動畫變化

https://github.com/willian12345/blogpost/blob/main/matrix/free-css-3d-transform/transform3d.html

demo 2 matrix3d 四角拖動變化

https://github.com/willian12345/blogpost/blob/main/matrix/free-css-3d-transform/drag.html

直接使用現成的庫

我們碰到的問題大機率程式設計的前輩們都碰到過了

透過搜尋後發現“仿射變換” 在 opencv 中有現成的函式

const H = cv.getPerspectiveTransform(from, to);

然後就 github 上找了一下,果然是有人實現了

然後用它提供的函式實現一遍 demo2

https://github.com/willian12345/blogpost/blob/main/matrix/free-css-3d-transform/drag-perspective-transform.html


參考資料

https://developer.mozilla.org/zh-CN/docs/Web/CSS/transform-function/matrix

https://angrytools.com/css-generator/transform/

https://franklinta.com/2014/09/08/computing-css-matrix3d-transforms/

https://docs.opencv.org/2.4/modules/imgproc/doc/geometric_transformations.html#getperspectivetransform"

https://www.zweigmedia.com/RealWorld/tutorialsf1/frames3_2.html

https://ccc-js.github.io/numeric2/

https://github.com/fccm/getPerspectiveTransform


注:轉載請註明出處部落格園:王二狗Sheldon池中物 (willian12345@126.com)

相關文章