前端 Canvas 用法之 – 使用矩阵 (matrix) + 向量 (vector) 操控画布绘图

前言

使用 canvas 画布来操控图形或者图片的二维变化的方式一般是平移、缩放、和旋转。canvas 已相应提供了相关的 api。如:translate(), scale() 和 rotate(),但这些功能无法统一处理并且无法描述当前画布的处于某种状态。为了实现这种统一连贯性的状态描述,canvas 给我们提供了另外一种思路及方法,即使用:transform() / setTransform();因为画布上的每个对象都拥有一个当前的 3 x 3 变换矩阵,都可以使用一个 3 x 3 矩阵来描述当前状态,其实是 2 x 3 矩阵,但为了便于计算,人为添加第三行 0 0 1 变成 3 x 3 矩阵。费话不多话,下面直接看矩阵变换的已封装好的功能,为了便于读者理解,每一步都有详细解释及介绍。【点击查看 Demo 演示

源码解释

下面是矩阵变换 js 工具库

      function createMatrix() {
        /**
         * 初始一个矩阵,开始时,a, d 为 1,其它为 0
         * a  c  e
         * b  d  f
         * 0  0  1
         *
         * a	水平缩放绘图
         * b	水平倾斜绘图
         * c	垂直倾斜绘图
         * d	垂直缩放绘图
         * e	水平移动绘图
         * f	垂直移动绘图
         *
         * 1  0  0
         * 0  1  0
         * 0  0  1
         *
         * */
        let m = []
        m[0] = 1
        m[1] = 0
        m[2] = 0
        m[3] = 1
        m[4] = 0
        m[5] = 0
        return m
      }
      function multiply(m1, m2) {
        // 左乘
        /**
         * 已知矩阵 A 和矩阵 B,求 A 和 B 的乘积 C= AB
         * 常规方法:矩阵 C 中每一个元素 Cij = A 的第 i 行 乘以(点乘) B 的第 j 列
         *
         * m1[0]  m1[2]  m1[4]     m2[0]  m2[2]  m2[4]     m[0]  m[2]  m[4]
         * m1[1]  m1[3]  m1[5]  X  m2[1]  m2[3]  m2[5]  =  m[1]  m[3]  m[5]
         *  0      0      1          0      0      1        0      0     1
         *
         *
         * */
        let m = []
        m[0] = m1[0] * m2[0] + m1[2] * m2[1] + m1[4] * 0
        m[2] = m1[0] * m2[2] + m1[2] * m2[3] + m1[4] * 0
        m[4] = m1[0] * m2[4] + m1[2] * m2[5] + m1[4] * 1
        m[1] = m1[1] * m2[0] + m1[3] * m2[1] + m1[5] * 0
        m[3] = m1[1] * m2[2] + m1[3] * m2[3] + m1[5] * 0
        m[5] = m1[1] * m2[4] + m1[3] * m2[5] + m1[5] * 1
        return m
      }
      function translate(m, v) { // v 是个二维向量,即一个数组 [x, y],向量 v 是用于表示 x y 轴方向移动的距离,及其可表示移动的方向
        let arr = [...m]
        arr[4] = arr[4] + v[0]
        arr[5] = arr[5] + v[1]
        return arr
      }
      function rotate(m, rad) {
        /**
         * 假如 A(X, Y) 点初始角度为 a,绕圆点旋转 θ 度,在坐标系上可得 B 点的坐标(X', Y'),算出半径为 r = √(X² + Y²)
         * X' = cos(a + θ) * r
         * Y' = sin(a + θ) * r
         * 根据三角函数公式:cos(α + β) = cosαcosβ - sinαsinβ
         * 可得 X' = r * cosa * cosθ – r * sina * sinθ =  X * cosθ – Y * sinθ
         * 根据三角函数公式:sin(α + β) = sinαcosβ + cosαsinβ
         * 可得 Y' = sinacosθ * r + cosasinθ * r =  X * sinθ + Y * cosθ
         * X’       cosθ    -sinθ   0       X
         * Y’   =   sinθ    cosθ    0   X   Y
         * 1         0       0      1       1
         * 根据结合律结果应为:
         *
         * cosθ    -sinθ   0       m[0]  m[2]  m[4]      arr[0]  arr[2]  arr[4]
         * sinθ    cosθ    0   X   m[1]  m[3]  m[5]  =   arr[1]  arr[3]  arr[5]
         *  0       0      1       0      0      1         0       0        1
         *
         */
        return multiply(
          [
            Math.cos(rad),
            Math.sin(rad),
            -1 * Math.sin(rad),
            Math.cos(rad),
            0,
            0
          ],
          m
        )
      }
      function scale(m, v) { // v 是个二维向量,即一个数组 [scaleX, scaleY],向量 v 是用于表示 x y 方向的伸缩程度
        /**
         * 假如 X,Y 分别缩放(a, b)倍
         * X' = X * a
         * Y' = Y * b
         *
         * a    0    0       m[0]  m[2]  m[4]      arr[0]  arr[2]  arr[4]
         * 0    b    0   X   m[1]  m[3]  m[5]  =   arr[1]  arr[3]  arr[5]
         * 0    0    1       0      0      1         0       0        1
         *
         */
        return multiply([v[0], 0, 0, v[1], 0, 0], m)
      }
      function invert(m) {
        // 矩阵求逆
        /**
         * 待定系数法求逆
         * X' = X * a
         * Y' = Y * b
         *
         * m[0]  m[2]  m[4]      arr[0]  arr[2]  arr[4]       1   0   0
         * m[1]  m[3]  m[5]  *   arr[1]  arr[3]  arr[5]   =   0   1   0
         * 0      0      1         0       0        1         0   0   1
         *
         * m[0] * arr[0] + m[2] * arr[1] + m[4] * 0 = 1    =>  arr[0] = (1 - m[2] * arr[1]) / m[0]
         * m[0] * arr[2] + m[2] * arr[3] + m[4] * 0 = 0        => arr[2] = (0 - m[2] * arr[3]) / m[0]
         * m[0] * arr[4] + m[2] * arr[5] + m[4] * 1 = 0            => arr[4] = (0 - m[2] * arr[5] - m[4]) / m[0]
         * m[1] * arr[0] + m[3] * arr[1] + m[5] * 0 = 0    =>  arr[0] = (0 - m[3] * arr[1]) / m[1]
         * m[1] * arr[2] + m[3] * arr[3] + m[5] * 0 = 1        => arr[2] = (1 - m[3] * arr[3]) / m[1]
         * m[1] * arr[4] + m[3] * arr[5] + m[5] * 1 = 0            => arr[4] = (0 - m[3] * arr[5] - m[5]) / m[1]
         */
        let arr = []
        arr[1] = 1 / m[0] / (m[2] / m[0] - m[3] / m[1])
        arr[0] = 0 - (m[3] * arr[1]) / m[1]
        arr[3] = 1 / m[1] / (m[3] / m[1] - m[2] / m[0])
        arr[2] = 0 - (m[2] * arr[3]) / m[0]
        arr[5] = (m[4] / m[0] - m[5] / m[1]) / (m[3] / m[1] - m[2] / m[0])
        arr[4] = 0 - m[3] * arr[5] - m[5] / m[1]
        return arr
      }

使用上面的矩阵函数功能来操控画布画图

        var screenWidth = $(window).width() > 750 ? 750 : $(window).width()
        var screenHeight = $(window).height()
        var c = document.getElementById('myCanvas_1')
        c.setAttribute('width', screenWidth)
        c.setAttribute('height', screenHeight)
        var ctx = c.getContext('2d')
        var m = createMatrix()
        ctx.fillStyle = 'red'
        ctx.fillRect(100, 100, 250, 100)
        // translate
        m = translate(m, [30, 10]) // 平移 30 10
        ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5])
        ctx.fillStyle = 'orange'
        ctx.fillRect(100, 100, 250, 100)
        // rotate
        m = rotate(m, (30 * Math.PI) / 180) // 旋转 30 度
        ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5])
        ctx.fillStyle = 'yellow'
        ctx.fillRect(100, 100, 250, 100)
        // scale
        m = scale(m, [0.7, 0.5]) // 缩放 0.7, 0.5
        ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5])
        ctx.fillStyle = 'green'
        ctx.fillRect(100, 100, 250, 100)
        // invert 求逆
        m = invert(m)
        ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5])
        ctx.fillStyle = 'blue'
        ctx.fillRect(100, 100, 250, 100)

总结

为什么要写这遍文章,是因为读 echarts 的 2d 渲染引擎 zrender 的矩阵工具函数(https://github.com/ecomfe/zrender/blob/master/src/core/matrix.js)源码时发现其写法不好理解,同时略显啰嗦。于是决定自己按照最标准的数学知识来对其重构一遍,提高代码的可读性的同时便于使用。另外:如果读书时大家就知道线性代数可以用于 2D 图形、3D 处理,估计很多人也不至于那么盲目地被动学习,对线性代数这门课无感。

作者: 博主

Talk is cheap, show me the code!

发表评论

邮箱地址不会被公开。

Captcha Code