一棵樹-可視化之圖形化基礎之向量

做者:肖劍華


  • 可視化是前端可視化
  • 圖形是計算機圖形學
  • 向量就是那個向量,高中學過的,你懂的
  • 樹是那棵賊醜的樹

結果


首先先看看本文最終的結果。



是否是賊醜!是否是能在畫展上賣個好價格!
javascript

過程


好了,話很少說, 看看這棵賊醜的樹是怎麼誕生的吧。
html

座標系


座標系,或者說平面直角座標系,是幾何圖形學的基礎,其次是點、線、面這些元素。


座標系你們都很熟悉, 最初接觸座標系應該是初中, 那時候的座標系不知你們還有沒有印象。


原點在中間, 水平軸是 x 軸, 豎軸是 y 軸, 分爲四個象限。


可是呢, html canvas 這貨, 默認原點在左上角, x 軸是跟平面直角座標系是一致的, y 軸是向下的!!
相信這種座標軸在平常工做中使用 canvas 繪圖給前端人不知道形成過多少麻煩, 計算起來費事費力, 還容易出 bug。


那麼如何把 canvas 的座標系變成平面直角座標系呢
前端

Maaaaaaaaagic!
const canvas = document.querySelector('canvas')
const ctx = canvas.getContext('2d')
// 咱們這裏把原點定位在canvas左下角
ctx.translate(0, canvas.height)
// 關鍵步驟: 將canvasY軸方向翻轉
ctx.scale(1, -1)


兩行代碼, 就完成了對座標系的翻轉。


咱們用一個 🌰 來驗證一下


假設,咱們要在寬 512 * 高 256 的一個 Canvas 畫布上實現以下的視覺效果。其中,山的高度是 100,底邊 200,兩座山的中心位置到中線的距離都是 80,太陽的圓心高度是 150。


咱們這裏使用 rough.js 增長一下趣味性
java

<canvas
  width="512"
  height="256"
  style="display: block;margin: 0 auto;background-color: #ccc"
></canvas>
const canvas = document.querySelector('canvas')
const rc = rough.canvas(canvas)
rc.ctx.translate(0, canvas.height)
rc.ctx.scale(1, -1)

const cSun = [canvas.width / 2, 106]
const diameter = 100 // 直徑

const hill1Points = {
  start: [76, 0], // 起始點
  top: [176, 100], // 頂點
  end: [276, 0] // 終點
}

const hill2Points = {
  start: [236, 0], // 起始點
  top: [336, 100], // 頂點
  end: [436, 0] // 終點
}

const hill1Options = {
  roughness: 0.8,
  stokeWidth: 2,
  fill: 'pink'
}

const hill2Options = {
  roughness: 0.8,
  stokeWidth: 2,
  fill: 'chocolate'
}

function createHillPath(point) {
  const { start, top, end } = point

  return `M${start[0]} ${start[1]}L${top[0]} ${top[1]}L${end[0]} ${end[1]}`
}

function paint() {
  rc.path(createHillPath(hill1Points), hill1Options)
  rc.path(createHillPath(hill2Points), hill2Options)

  rc.circle(cSun[0], cSun[1], diameter, {
    stroke: 'red',
    strokeWidth: 4,
    fill: 'rgba(255, 255, 0, 0.4)',
    fillStyle: 'solid'
  })
}

paint()


這裏咱們翻轉了座標系, 定義了 mountain1,mountain2,太陽 的各個點的座標, 徹底是參照直角座標系的座標。


最終的實現效果以下





(是否是也能在畫展上賣個不錯的價格)
git

向量

定義


說完直角座標系的轉換, 咱們來討論今天的正主, 向量(Vector)


向量的廣泛定義是具備大小和方向的量, 咱們這裏討論的向量是 幾何向量, 是用一組平面直角座標系的座標表示的
例如 (1, 1), 意思是, 頂點座標爲 x 爲 1,y 爲 0 的一條有向線段, 向量的方向是由 原點(0, 0) 指向頂點(1,1)的方向。


換言之, 知道了向量的頂點, 就知道了向量的大小和方向
github

向量的模


向量的大小也叫向量的模,是向量座標的平方和的算術平方根, length = Math.pow((x2 + y2), 0.5)。
canvas

向量的方向


向量的方向一方面可使用向量的頂點表示。


另一方面使用向量和 x 軸的夾角,也可以表示一個向量。


使用 javascript Math 的內置方法能夠獲得,計算方式:
segmentfault

// 構造函數在本文稍後的地方介紹
const v = new Vector2D(1, 10)
const dir = Math.atan2(v.y, v.x)

四則運算

加減法


示意圖:



如圖所示: 向量 v1(x1, y1)和向量 v2(x2, y2)相加獲得的新的向量就是兩個向量對應座標之和, 用公式表達就是
v1(x1, y1) + v2(x2, y2) = v3(x1 + x2, y1 + y2)


反之就是減法 v3(x1 + x2, y1 + y2) - v2 (x2, y2)= v1(x1, y1)
數組

乘除


向量乘法有 叉乘和點乘
dom

點乘示意圖:





物理意義是, 方向爲 va 方向,大小爲 va.length 的力, 沿 vb 方向拉動 vb.length 距離所作的功


va vb = va.length vb.length * cos(rad)

叉乘示意圖:





va vb = va.length va.length * sin(rad)


也能夠理解爲長度爲 va.length 的線段沿着 vb 方向移動到 vb 頂點掃過的面積, 反之就是除法


乘除這裏僅作概念上的介紹

單位向量


長度爲 1 的向量叫作單位向量, 知足這個條件的向量有無數條, 一個非 0 的向量除以他的模,就是這個向量的單位向量, 咱們取與 x 軸夾角爲 0 的向量:[1, 0]做爲單位向量

向量的旋轉


將一個向量轉動必定的角度 rad 以後的向量該如何計算呢。
這裏有比較複雜的推導過程, 所以能夠直接記住結論。


具體代碼在下面構造函數裏面展現

構造器

// 用一個長度爲2的數組表示一個向量, 下標爲0的位置表示x 下標爲1的位置表示 y
class Vector2D extends Array {
  constructor(x = 1, y = 0) {
    super(x, y)
  }

  get x() {
    return this[0]
  }

  get y() {
    return this[1]
  }

  set x(v) {
    this[0] = v
  }

  set y(v) {
    this[1] = v
  }

  add(v) {
    this.x = this.x + v.x
    this.y = this.y + v.y
    return this
  }

  length() {
    return Math.hypot(this.x, this.y)
  }

  rotate(rad) {
    const c = Math.cos(rad)
    const s = Math.sin(rad)
    const [x, y] = this
    this.x = x * c + y * -s
    this.y = x * s + y * c
    return this
  }
}


至此,畫出文章開頭的那個圖形的基本要素都已經準備好了。
下面, 讓咱們來見證一下世界名畫的產生。

動手畫圖

  1. 準備一個 512 * 512 的畫布
<html>
  ...
  <canvas
    width="512"
    height="512"
    style="display:block;margin:0 auto;background-color: #ccc"
  ></canvas>
  ...
</html>
  1. 翻轉 canvas 座標系
const canvas = document.querySelector('canvas')
const ctx = canvas.getContext('2d')
ctx.translate(0, canvas.height)
ctx.scale(1, -1)
  1. 定義繪製樹枝的方法
/**
 * 1. ctx canvas ctx 上下文對象
 * 2. 起始向量
 * 3. length 向量長度(樹枝長度)
 * 4. thickness 線段寬度
 * 5. 單位向量 dir 旋轉角度
 * 6. bias 隨機因子
 */
const canvas = document.querySelector('canvas')
const ctx = canvas.getContext('2d')
ctx.translate(0, canvas.height)
ctx.scale(1, -1)
ctx.lineCap = 'round'
console.log(canvas.width)
const v0 = new Vector2D(canvas.width / 2, 0)

function drawBranch(ctx, v0, length, thickness, rad, bias) {
  const v = new Vector2D().rotate(rad).scale(length)
  console.log(v, rad, length)
  const v1 = v0.copy().add(v)
  ctx.beginPath()
  ctx.lineWidth = thickness
  ctx.moveTo(...v0)
  ctx.lineTo(...v1)
  ctx.stroke()
  ctx.closePath()
}
// 定義好了以後咱們先畫一個樹枝試試看
drawBranch(ctx, v0, 50, 10, Math.PI / 2, 1)
  1. 遞歸畫圖
// 先定義收縮係數
const LENGTH_SHRINK = 0.9
const THICKNESS_SHRINK = 0.8
const RAD_SHRINK = 0.5
const BIAS_SHRINK = 1

function drawBranch(ctx, v0, length, thickness, rad, bias) {
  // ....

  if (thickness > 2) {
    // 畫左樹枝
    const left =
      Math.PI / 4 +
      RAD_SHRINK * (rad + 0.2) +
      drawBranch(
        ctx,
        v1,
        length * LENGTH_SHRINK,
        thickness * THICKNESS_SHRINK,
        left,
        bias
      )

    // 畫右樹枝
    const right = Math.PI / 4 + RAD_SHRINK * (rad - 0.2)
    drawBranch(
      ctx,
      v1,
      length * LENGTH_SHRINK,
      thickness * THICKNESS_SHRINK,
      right,
      bias
    )
  }
}

drawBranch(ctx, v0, 50, 10, Math.PI / 2, 1)


這一步畫出來的是一個比較規則的形狀, 代碼寫到這一步,樹的基本形狀已經出來了,可是 爲了展現效果, 向量翻轉上加一些隨機性來畫一顆更加接近天然狀態的樹。代碼以下:

function drawBranch(ctx, v0, length, thickness, rad, bias) {
  // ....

  if (thickness > 2) {
    // 畫左樹枝
    const left =
      Math.PI / 4 + RAD_SHRINK * (rad + 0.2) + bias * (Math.random() - 0.5) // 加些隨機數
    drawBranch(
      ctx,
      v1,
      length * LENGTH_SHRINK,
      thickness * THICKNESS_SHRINK,
      left,
      bias
    )

    // 畫右樹枝
    const right =
      Math.PI / 4 + RAD_SHRINK * (rad - 0.2) + bias * (Math.random() - 0.5) // 加些隨機數
    drawBranch(
      ctx,
      v1,
      length * LENGTH_SHRINK,
      thickness * THICKNESS_SHRINK,
      right,
      bias
    )
  }
}

drawBranch(ctx, v0, 50, 10, Math.PI / 2, 1)




等等等等, 效果圖:一棵光禿禿的樹





(是否是有點藝術內味兒了)


剩下的就是添加一些點綴, 把果子掛上

function drawBranch(ctx, v0, length, thickness, rad, bias) {
  // .....

  if (thickness < 5 && Math.random() < 0.3) {
    const th = 6 + Math.random()

    ctx.save()
    ctx.strokeStyle = '#e4393c'
    ctx.lineWidth = th
    ctx.beginPath()
    ctx.moveTo(...v1)
    ctx.lineTo(v1.x, v1.y + 2)
    ctx.stroke()
    ctx.closePath()
    ctx.restore()
  }
}

drawBranch(ctx, v0, 50, 10, Math.PI / 2, 3) // 這裏增大了隨機因子, 讓樹枝更加分散


此時效果圖就出來了:



(我再問一遍, 是否是很好看, 是否是很想花個幾百萬小錢買下它)


對於drawBranch第一調用, 能夠嘗試調一調參數,看看結果如何。


完整代碼地址:github

總結


本文首先展現瞭如何將 canvas 的座標系轉化爲直角座標系


其次用一個例子演示了,向量在圖形學內的基本運算。


向量運算的意義並不單單只是用來算點的位置和構造線段,這只是最初級的用法。


可視化呈現依賴於計算機圖形學,而向量運算是整個計算機圖形學的數學基礎。並且,在向量運算中,除了加法表示移動點和繪製線段外,向量的點乘、叉乘運算也有特殊的意義。

咱們是曉黑板前端,歡迎關注咱們的 知乎SegmentfaultCSDN簡書開源中國帳號。
相關文章
相關標籤/搜索