快應用 canvas教程

2020-08-08 15:26 更新
了解如何正確使用 canvas 畫布,以及通過 canvas 繪制圖形及動畫。

通過本節(jié),你將學會:

創(chuàng)建畫布

快應用的 canvas 功能由兩部分組成,canvas 組件和渲染腳本。

canvas 組件中,用于繪制圖形的部分,稱之為 畫布

canvas 組件

和其他組件一樣,在快應用 template 中添加即可。同時可為其添加需要的樣式。

這里需要注意,與 HTML 中 canvas 不同的是:

  • 暫不支持 width、height 屬性,尺寸由 style 控制。
  • 默認尺寸為 0 x 0。
  • 底色默認為白色,background-color 無效。
  • 支持 margin 樣式,但 padding、border 無效。
  • 不能有子節(jié)點。
  • 獲取節(jié)點的方式需要采用快應用標準的 $element 方法。

渲染腳本

單獨的 canvas 組件僅僅是一個透明矩形,我們需要通過渲染腳本來進一步操作。

首先通過? $element ?和 id 來獲取 canvas 組件節(jié)點,再通過 ?getContext ?方法創(chuàng)建 canvas 繪圖上下文。

?getContext ?方法的參數(shù)目前僅支持 ?'2d'?,創(chuàng)建的 canvas 繪圖上下文是一個 CanvasRenderingContext2D 對象。

在后續(xù)腳本中操作該對象即可繪制圖形。

完整示例代碼如下:

<template>
  <div class="doc-page">
    <div class="content">
      <canvas class="new-canvas" id="new-canvas"></canvas>
    </div>
  </div>
</template>

<style>
  .content {
    flex-direction: column;
    align-items: center;
    width: 100%;
  }
  .new-canvas {
    height: 380px;
    width: 380px;
  }
</style>

<script>
  export default {
    private: {
      drawComplete: false
    },
    onInit() {
      this.$page.setTitleBar({
        text: 'canvas簡單繪制'
      })
    },
    onShow() {
      if (!this.drawComplete) {
        this.drawCanvas()
      }
    },
    drawCanvas() {
      const canvas = this.$element('new-canvas') //獲取 canvas 組件
      const ctx = canvas.getContext('2d') //獲取 canvas 繪圖上下文

      //繪制一個矩形
      ctx.fillStyle = 'rgb(200,0,0)'
      ctx.fillRect(20, 20, 200, 200)

      //繪制另一個矩形
      ctx.fillStyle = 'rgba(0, 0, 200, 0.5)'
      ctx.fillRect(80, 80, 200, 200)

      this.drawComplete = true
    }
  }
</script>

如果你想進入頁面即渲染?canvas?,只能在?onShow?中獲取?canvas ?組件節(jié)點,繪制圖形。

輸出效果如圖

基礎(chǔ)示例

繪制

坐標系

開始畫圖之前,需要了解一下畫布的坐標系。

如下圖所示,坐標系原點為左上角(坐標為(0,0))。所有元素的位置都相對于原點定位。x 軸向右遞增,y 軸向下遞增。

坐標系

填充繪制(fill)

canvas 繪圖的基本繪制方式之一是填充繪制。

填充是指用指定的內(nèi)容填滿所要繪制的圖形,最終生成一個實心的圖案。

描邊繪制(stroke)

canvas 繪圖的另一種基本繪制方式是描邊繪制。

描邊繪制是指,沿著所要繪制的圖形邊緣,使用指定的內(nèi)容進行描繪,最終生成的是空心的圖案。

如果既要填充又要描邊,則需要分別繪制兩次完成最終圖案。

繪制圖形

繪制矩形

矩形,是最基礎(chǔ)的形狀。canvas 提供了三種方法繪制矩形:

//填充繪制矩形
ctx.fillRect(x, y, width, height)

//描邊繪制矩形
ctx.strokeRect(x, y, width, height)

//擦除矩形區(qū)域,相當于用白色底色填充繪制
ctx.clearRect(x, y, width, height)

繪制路徑

路徑,是另一種基礎(chǔ)形狀。通過控制筆觸的坐標點,在畫布上繪制圖形。

與繪制矩形的直接繪制不同,繪制路徑需要一些額外的步驟。

  • 首先,需要創(chuàng)建路徑起始點。
  • 然后,你使用各種路徑繪制命令去畫出路徑。此時路徑是不可見的。
  • 根據(jù)需要,選擇是否把路徑封閉。
  • 通過描邊或填充方法來實際繪制圖形。

為此,我們需要了解以下一些基本方法。

beginPath()

開始一條新路徑,這是生成路徑的第一步操作。

一條路徑本質(zhì)上是由多段子路徑(直線、弧形、等等)組成。而每次調(diào)用 beginPath 之后,子路徑清空重置,然后就可以重新繪制新的圖形。

closePath()

閉合當前路徑。

?closePath()? 不是必須的操作,相當于繪制一條當前位置到路徑起始位置的直線子路徑。

stroke()

描邊繪制當前路徑。

fill()

填充繪制當前路徑。

當調(diào)用 ?fill()?時,當前沒有閉合的路徑會自動閉合,不需要手動調(diào)用 closePath() 函數(shù)。調(diào)用 ?stroke()? 時不會自動閉合。

moveTo(x, y)

移動筆觸。將當前路徑繪制的筆觸移動到某個坐標點。

相當于繪制一條真正不可見的子路徑。通常用于繪制不連續(xù)的路徑。

調(diào)用 ?beginPath()? 之后,或者 canvas 剛創(chuàng)建的時候,當前路徑為空,第一條路徑繪制命令無論實際上是什么,通常都會被視為 ?moveTo?。因此,在開始新路徑之后建議通過 ?moveTo? 指定起始位置。

路徑繪制命令

路徑繪制命令是實際繪制路徑線條的一些命令。包括有:

  • 繪制直線:?lineTo?
  • 繪制圓?。?arc?、?arcTo?
  • 貝塞爾曲線:?quadraticCurveTo?、?bezierCurveTo?
  • 矩形:?rect?

這些命令都是用來繪制不同子路徑的命令。具體的用途和參數(shù),可以查閱 參考文檔

組合使用

這里,我們展示一個組合使用的效果,繪制一個快應用的 logo。

drawCanvas () {
    const canvas = this.$element('newCanvas')
    const ctx = canvas.getContext('2d')

    const r = 20
    const h = 380
    const p = Math.PI

    ctx.beginPath()
    ctx.moveTo(r * 2, r)
    ctx.arc(r * 2, r * 2, r, -p / 2, -p, true)
    ctx.lineTo(r, h - r * 2)
    ctx.arc(r * 2, h - r * 2, r, p, p / 2, true)
    ctx.lineTo(h - r * 2, h - r)
    ctx.arc(h - r * 2, h - r * 2, r, p / 2, 0, true)
    ctx.lineTo(h - r, r * 2)
    ctx.arc(h - r * 2, r * 2, r, 0, -p / 2, true)
    ctx.closePath()
    ctx.stroke()

    const s = 60

    ctx.beginPath()
    ctx.moveTo(h / 2 + s, h / 2)
    ctx.arc(h / 2, h / 2, s, 0, -p / 2 * 3, true)
    ctx.arc(h / 2, h / 2 + s + s / 2, s / 2, -p / 2, p / 2, false)
    ctx.arc(h / 2, h / 2, s * 2, -p / 2 * 3, 0, false)
    ctx.arc(h / 2 + s + s / 2, h / 2, s / 2, 0, p, false)
    ctx.moveTo(h / 2 + s * 2, h / 2 + s + s / 2)
    ctx.arc(h / 2 + s + s / 2, h / 2 + s + s / 2, s / 2, 0, p * 2, false)
    ctx.moveTo(h / 2 + s / 4 * 3, h / 2 + s / 2)
    ctx.arc(h / 2 + s / 2, h / 2 + s / 2, s / 4, 0, p * 2, false)
    ctx.fill()
}

實現(xiàn)效果如下

組合繪制路徑

顏色和樣式

通過剛才的例子,我們學會了繪制圖形。

但是我們看到,不管是填充還是描邊,畫出來的都是簡單的黑白圖形。如果想要指定描繪的內(nèi)容,畫出更豐富的效果應該如何操作呢?

有兩個重要的屬性可以做到,?fillStyle? 和 ?strokeStyle?。顧名思義,分別是為填充和描邊指定樣式。

顏色

在本章節(jié)最初的例子里,其實已經(jīng)看到上色的基本方法,就是直接用顏色作為指定樣式。

ctx.fillStyle = 'rgb(200,0,0)'
ctx.fillRect(20, 20, 200, 200)

一旦設(shè)置了 ?fillStyle? 或者 ?strokeStyle? 的值,新值就會成為新繪制的圖形的默認值。如果你要給每個圖形上不同的顏色,需要畫完一種樣式的圖形后,重新設(shè)置 ?fillStyle? 或 ?strokeStyle? 的值。

//填充繪制一個矩形,顏色為暗紅色
ctx.fillStyle = 'rgb(200,0,0)'
ctx.fillRect(20, 20, 200, 200)

//描邊繪制另一個矩形,邊框顏色為半透明藍色
ctx.strokeStyle = 'rgba(0, 0, 200, 0.5)'
ctx.strokeRect(80, 80, 200, 200)

canvas 的顏色支持各種 CSS 色彩值。

// 以下值均為 '紅色'
ctx.fillStyle = 'red' //色彩名稱
ctx.fillStyle = '#ff0000' //十六進制色值
ctx.fillStyle = 'rgb(255,0,0)' //rgb色值
ctx.fillStyle = 'rgba(255,0,0,1)' //rgba色值

漸變色

除了使用純色,還支持使用漸變色。先創(chuàng)建漸變色對象,并將漸變色對象作為樣式進行繪圖,就能繪制出漸變色的圖形。

漸變色對象可以使用 ?createLinearGradient? 創(chuàng)建線性漸變,然后使用 ?addColorStop? 上色。

這里要注意的是,漸變色對象的坐標尺寸都是相對畫布的。應用了漸變色的圖形實際起到的是類似“蒙版”的效果。

//填充繪制一個矩形,填充顏色為深紅到深藍的線性漸變色
const linGrad1 = ctx.createLinearGradient(0, 0, 300, 300)
linGrad1.addColorStop(0, 'rgb(200, 0, 0)')
linGrad1.addColorStop(1, 'rgb(0, 0, 200)')
ctx.fillStyle = linGrad1
ctx.fillRect(20, 20, 200, 200)

//描邊繪制另一個矩形,邊框顏色為深藍到深紅的線性漸變色
const linGrad2 = ctx.createLinearGradient(0, 0, 300, 300)
linGrad2.addColorStop(0, 'rgb(0, 0, 200)')
linGrad2.addColorStop(1, 'rgb(200, 0, 0)')
ctx.strokeStyle = linGrad2
ctx.strokeRect(80, 80, 200, 200)

線型

除了顏色,還可以在描邊繪制圖形的時候,為描邊的線條增加線型。

線型可設(shè)置的項目包括:

線寬(lineWidth)

線寬

顧名思義,線寬就是描邊線條的寬度,單位是像素。

這里要注意兩點:

線條的寬度會向圖形的內(nèi)部及外部同時延伸,會侵占圖形的內(nèi)部空間。在使用較寬線條時特別需要注意圖形內(nèi)部填充部分是否被過度擠壓。常用解決方法可以嘗試先描邊后填充??赡軙霈F(xiàn)的半渲染像素點。例如,繪制一條 (1, 1) 到 (1, 3),線寬為 1px 的線段,是在 x = 1 的位置,向左右各延伸 0.5px 進行繪制。但是由于實際最小繪制單位是一個像素點,那么最終繪制出來的效果將是線寬 2px,但是顏色減半的線段,視覺上看就會模糊。常用解決方法,一種是改用偶數(shù)的線寬繪制;另一種可以將線段繪制的起始點做適當偏移,例如偏移至 (1.5, 1) 到 (1.5, 3),左右各延伸 0.5px 后,正好布滿一個像素點,不會出現(xiàn)半像素渲染了。

端點樣式(lineCap)

端點樣式

端點樣式?jīng)Q定了線段端點顯示的樣子。從上至下依次為 ?butt?,?round ?和 ?square?,其中 ?butt?為默認值。

這里要注意的是,?round? 和 ?square? 會使得線段描繪出來的視覺長度,兩端各多出半個線寬,可參考藍色輔助線。

交點樣式(lineJoin)

交點樣式

交點樣式?jīng)Q定了圖形中兩線段連接處所顯示的樣子。從上至下依次為 ?miter?, ?bevel? 和 ?round?,?miter? 為默認值。

交點最大斜接長度(miterLimit)

交點最大斜接長度

在上圖交點樣式為 ?miter? 的展示中,線段的外側(cè)邊緣會延伸交匯于一點上。線段直接夾角比較大的,交點不會太遠,但當夾角減少時,交點距離會呈指數(shù)級增大。

?miterLimit? 屬性就是用來設(shè)定外延交點與連接點的最大距離,如果交點距離大于此值,交點樣式會自動變成了 ?bevel?。

示例

ctx.lineWidth = 20
ctx.lineCap = 'round'
ctx.lineJoin = 'bevel'
ctx.strokeRect(80, 80, 200, 200)

使用虛線

用 ?setLineDash? 方法和 ?lineDashOffset? 屬性來制定虛線樣式。 ?setLineDash? 方法接受一個數(shù)組,來指定線段與間隙的交替;?lineDashOffset? 屬性設(shè)置起始偏移量。

示例

drawLineDashCanvas () {
    const canvas = this.$element('linedash-canvas')
    const ctx = canvas.getContext('2d')

    let offset = 0

    // 繪制螞蟻線
    setInterval(() => {
        offset++

        if (offset > 16) {
            offset = 0
        }

        ctx.clearRect(0, 0, 300, 300)
        // 設(shè)置虛線線段和間隙長度 分別為 4px 2px
        ctx.setLineDash([4, 2])
        // 設(shè)置虛線的起始偏移量
        ctx.lineDashOffset = -offset
        ctx.strokeRect(10, 10, 200, 200)
    }, 20)
}

運行效果如下

繪制虛線

組合使用

通過學習,我們?yōu)閯偛爬L制的快應用 logo 添加顏色和樣式。

drawCanvas () {
    const r = 20
    const h = 380
    const p = Math.PI

    const linGrad1 = ctx.createLinearGradient(h, h, 0, 0)
    linGrad1.addColorStop(0, '#FFFAFA')
    linGrad1.addColorStop(0.8, '#E4C700')
    linGrad1.addColorStop(1, 'rgba(228,199,0,0)')

    ctx.fillStyle = linGrad1
    ctx.fillRect(0, 0, h, h)

    const linGrad2 = ctx.createLinearGradient(0, 0, h, h)
    linGrad2.addColorStop(0, '#C1FFC1')
    linGrad2.addColorStop(0.5, '#ffffff')
    linGrad2.addColorStop(1, '#00BFFF')

    ctx.beginPath()
    ctx.moveTo(r * 2, r)
    ctx.arc(r * 2, r * 2, r, -p / 2, -p, true)
    ctx.lineTo(r, h - r * 2)
    ctx.arc(r * 2, h - r * 2, r, p, p / 2, true)
    ctx.lineTo(h - r * 2, h - r)
    ctx.arc(h - r * 2, h - r * 2, r, p / 2, 0, true)
    ctx.lineTo(h - r, r * 2)
    ctx.arc(h - r * 2, r * 2, r, 0, -p / 2, true)
    ctx.closePath()
    ctx.lineWidth = 10
    ctx.strokeStyle = linGrad2
    ctx.stroke()

    const s = 60

    ctx.beginPath()
    ctx.moveTo(h / 2 + s, h / 2)
    ctx.arc(h / 2, h / 2, s, 0, -p / 2 * 3, true)
    ctx.arc(h / 2, h / 2 + s + s / 2, s / 2, -p / 2, p / 2, false)
    ctx.arc(h / 2, h / 2, s * 2, -p / 2 * 3, 0, false)
    ctx.arc(h / 2 + s + s / 2, h / 2, s / 2, 0, p, false)
    ctx.fillStyle = '#4286f5'
    ctx.fill()

    ctx.beginPath()
    ctx.moveTo(h / 2 + s * 2, h / 2 + s + s / 2)
    ctx.arc(h / 2 + s + s / 2, h / 2 + s + s / 2, s / 2, 0, p * 2, false)
    ctx.fillStyle = 'rgb(234, 67, 53)'
    ctx.fill()

    ctx.beginPath()
    ctx.moveTo(h / 2 + s / 4 * 3, h / 2 + s / 2)
    ctx.arc(h / 2 + s / 2, h / 2 + s / 2, s / 4, 0, p * 2, false)
    ctx.fillStyle = 'rgba(250, 188, 5, 1)'
    ctx.fill()
}

實現(xiàn)效果如下

顏色和樣式

繪制文字

和繪制圖形類似,快應用 canvas 也提供 ?fillText? 和 ?strokeText? 兩種方法來繪制文字。

基本用法

//填充繪制
ctx.fillText('Hello world', 10, 50)

文字樣式

除了基本的樣式,文字還提供了獨有的樣式。

字體(font)

可以直接使用符合 CSS font 語法的字符串作為文字樣式的字體屬性。默認值為 ?'10px sans-serif'?。

要注意的是,不同于 web,目前快應用還無法引入外部字體文件,對于字體的選擇,僅限 serif、sans-serif 和 monosapce。

對齊方式(textAlign)和 水平對齊方式(textBaseline)

這兩個屬性控制了文體相對與繪制定位點的對齊方式。

示例

ctx.font = '48px sans-serif'
ctx.textAlign = 'left'
ctx.textBaseline = 'top'
ctx.fillText('Hello world', 10, 50)

使用圖片

除了直接在 canvas 中繪制各種圖形,快應用還支持使用圖片。

圖像對象

為了能夠在 canvas 中使用圖片,需要使用圖像對象來加載圖片。

const img = new Image() //新建圖像對象

圖片加載

修改圖像對象的 src 屬性,即可啟動圖片加載。

src 既可以使用 URI 來加載本地圖片,也使用 URL 加載網(wǎng)絡(luò)圖片。

const img = new Image() //新建圖像對象

img.src = '/common/logo.png' //加載本地圖片
img.src = 'https://www.quickapp.cn/assets/images/home/logo.png' //加載網(wǎng)絡(luò)圖片

//加載成功的回調(diào)
img.onload = () => {
  console.log('圖片加載完成')
}

//加載失敗的回調(diào)
img.onerror = () => {
  console.log('圖片加載失敗')
}

繪制圖片

圖片加載成功之后,就可以使用 ?drawImage? 在畫布中進行圖片繪制了。

為避免圖片未加載完成或加載失敗導致填充錯誤,建議在加載成功的回調(diào)中進行圖片填充操作。

img.onload = () => {
  ctx.drawImage(img, 0, 0)
}

使用 ?drawImage? 繪制圖片也有 3 種不同的基本形式,通過不同的參數(shù)來控制。

基礎(chǔ)

drawImage(image, x, y)

其中 image 是加載的圖像對象,x 和 y 是其在目標 canvas 里的起始坐標。

這種方法會將圖片原封不動的繪制在畫布上,是最基本的繪制方法。

縮放

drawImage(image, x, y, width, height)

相對基礎(chǔ)方法,多了兩個 ?width?、?height? 參數(shù),指定了繪制的尺寸。

這種方法會將圖片縮放成指定的尺寸后,繪制在畫布上。

切片

drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)

其中 image 與基礎(chǔ)方法一樣,是加載的圖像對象。

其它 8 個參數(shù)可以參照下方的圖解,前 4 個是定義圖源的切片位置和尺寸,后 4 個則是定義切片的目標繪制位置和尺寸。

切片繪圖

在填充和描邊繪制中使用圖片

圖片不僅僅可以直接繪制在畫布中,還可以將圖片像漸變色一樣,作為繪制圖形的樣式,在填充和描邊繪制中使用。

首先,需要通過 ?createPattern? 創(chuàng)建圖元對象,然后就可以將圖元對象作為樣式用在圖形的繪制中了。

同樣,為避免圖片未加載完成或加載失敗導致填充錯誤,建議在加載成功的回調(diào)中進行操作。

img.onload = () => {
  const imgPat = ctx.createPattern(img, 'repeat') //創(chuàng)建圖元對象
  const p = Math.PI

  //填充繪制一個圓,使用圖片作為填充元素
  ctx.beginPath()
  ctx.moveTo(50, 30)
  ctx.arc(100, 100, 60, 0, p * 2, false)
  ctx.fillStyle = imgPat
  ctx.fill()

  //描邊繪制一個圓,使用圖片作為描邊元素
  ctx.moveTo(100, 30)
  ctx.beginPath()
  ctx.arc(250, 250, 50, 0, p * 2, false)
  ctx.strokeStyle = imgPat
  ctx.lineWidth = 30
  ctx.stroke()
}

合成與裁切

在之前的例子里面,我們總是將一個圖形畫在另一個之上,對于其他更多的情況,僅僅這樣是遠遠不夠的。比如,對合成的圖形來說,繪制順序會有限制。不過,我們可以利用 globalCompositeOperation 屬性來改變這種狀況。此外, clip 屬性允許我們隱藏不想看到的部分圖形。

合成

我們不僅可以在已有圖形后面再畫新圖形,還可以用來遮蓋指定區(qū)域,清除畫布中的某些部分(清除區(qū)域不僅限于矩形,像 clearRect() 方法做的那樣)以及更多其他操作。

?globalCompositeOperation = type?

這個屬性設(shè)定了在畫新圖形時采用的遮蓋策略,其值是一個用于標識不同遮蓋方式的字符串。

source-over

這是默認設(shè)置,并在現(xiàn)有畫布上下文之上繪制新圖形。

canvas合成方式 source-over

source-atop

新圖形只在與現(xiàn)有畫布內(nèi)容重疊的地方繪制。

canvas合成方式 source-atop

source-in

新圖形只在新圖形和目標畫布重疊的地方繪制。其他的都是透明的。

canvas合成方式 source-in

source-out

在不與現(xiàn)有畫布內(nèi)容重疊的地方繪制新圖形。

canvas合成方式 source-out

destination-over

在現(xiàn)有的畫布內(nèi)容后面繪制新的圖形。

canvas合成方式 destination-over

destination-atop

現(xiàn)有的畫布只保留與新圖形重疊的部分,新的圖形是在畫布內(nèi)容后面繪制的。

canvas合成方式 destination-atop

destination-in

現(xiàn)有的畫布內(nèi)容保持在新圖形和現(xiàn)有畫布內(nèi)容重疊的位置。其他的都是透明的。

canvas合成方式 destination-in

destination-out

現(xiàn)有內(nèi)容保持在新圖形不重疊的地方。

canvas合成方式 destination-out

lighter

兩個重疊圖形的顏色是通過顏色值相加來確定的。

canvas合成方式 lighter

copy

只顯示新圖形。

canvas合成方式 copy

xor

圖像中,那些重疊和正常繪制之外的其他地方是透明的。

canvas合成方式 xor

舉例

<template>
  <div class="page">
    <text class=glo-type>{{globalCompositeOperation}}</text>
    <canvas id="cavs" class="canvas"></canvas>
    <input class="btn" value="切換合成方式" type="button" onclick="changeGlobalCompositeOperation"></input>
  </div>
</template>

<style>
  .page {
    flex-direction: column;
    align-items: center;
  }

  .glo-type {
    margin: 20px;
  }

  .canvas {
    width: 320px;
    height: 320px;
    border: 1px solid red;
  }

  .btn {
    width: 500px;
    height: 80px;
    text-align: center;
    border-radius: 5px;
    margin: 20px;
    color: #ffffff;
    font-size: 30px;
    background-color: #0faeff;
  }
</style>

<script>
  export default {
    private: {
      globalCompositeOperation: 'source-over'
    },
    onShow () {
      this.draw()
    },
    draw () {
      const ctx = this.$element('cavs').getContext('2d')

      // 清除畫布
      ctx.clearRect(0, 0, 320, 320)

      // 正常繪制第一個矩形
      ctx.globalCompositeOperation = 'source-over'
      ctx.fillStyle = 'skyblue'
      ctx.fillRect(10, 10, 200, 200)

      // 設(shè)置canvas的合成方式
      ctx.globalCompositeOperation = this.globalCompositeOperation

      // 繪制第二個矩形
      ctx.fillStyle = 'rgba(255, 0, 0, 0.5)'
      ctx.fillRect(110, 110, 200, 200)
    },
    // 切換canvas合成方式
    changeGlobalCompositeOperation () {
      const globalCompositeOperationArr = ['source-over', 'source-atop',
        'source-in', 'source-out',
        'destination-over', 'destination-atop',
        'destination-in', 'destination-out',
        'lighter', 'copy', 'xor']

      const index = globalCompositeOperationArr.indexOf(this.globalCompositeOperation)
      if (index < globalCompositeOperationArr.length - 1) {
        this.globalCompositeOperation = globalCompositeOperationArr[index + 1]
      }
      else {
        this.globalCompositeOperation = globalCompositeOperationArr[0]
      }

      this.draw()
    }
  }
</script>

裁切

裁切路徑,就是用 ?clip? 繪制一個不可見的圖形。一旦設(shè)置好裁切路徑,那么你在畫布上新繪制的所有內(nèi)容都將局限在該區(qū)域內(nèi),區(qū)域以外進行繪制是沒有任何效果的。

已有的內(nèi)容不受影響。

要取消裁切路徑的效果,可以繪制一個和畫布等大的矩形裁切路徑。

//繪制一個紅色矩形
ctx.fillStyle = 'rgb(200,0,0)'
ctx.fillRect(20, 20, 200, 200)

//使用裁切路徑繪制一個圓
ctx.beginPath()
ctx.arc(120, 120, 120, 0, Math.PI * 2, true)
ctx.clip()

//繪制一個藍色矩形,超出圓形裁切路徑之外的部分無法繪制
ctx.fillStyle = 'rgba(0, 0, 200)'
ctx.fillRect(80, 80, 200, 200)

運行效果如下

疊加效果

變形

到目前位置,我們所有的繪制,都是基于標準坐標系來繪制的。

標準坐標系的特點是:

  • 原點在左上角
  • 尺寸與畫布像素點 1:1

現(xiàn)在介紹的變形,就是改變標準坐標系的方法。

變形的基本方法

  • 平移:translate(x, y)
  • 旋轉(zhuǎn):rotate(angle)
  • 縮放:scale(x, y)
  • 變形:transform(m11, m12, m21, m22, dx, dy)、setTransform(m11, m12, m21, m22, dx, dy)、resetTransform()

變形的基本原則

  • 不會改變已經(jīng)繪制的圖形
  • 改變的是坐標系
  • 變形之后的所有繪制將依照新的坐標系來繪制

舉例

for (let i = 0; i < 6; i++) {
  ctx.fillRect(0, 0, 40, 40)
  ctx.translate(50, 0)
}

運行效果如圖。

變形

可以看到,雖然每次 ?fillRect? 繪制的參數(shù)沒有變化,但是因為坐標系變了,最終繪制出來的就是位置不同的圖形。

狀態(tài)保存與恢復

通過前面的學習,我可以看到,每次圖形繪制其實都帶著非常豐富的狀態(tài)。

在繪制復雜圖形的時候,就會帶來重復獲取樣式的問題。

如何優(yōu)化呢?

canvas 狀態(tài)的保存與恢復

ctx.save() //保存
ctx.restore() //恢復

canvas 狀態(tài)就是當前所有樣式的一個快照。

save 和 restore 方法是用來保存和恢復 canvas 狀態(tài)的。

canvas 狀態(tài)存儲在棧中,每次 save 的時候,當前的狀態(tài)就被推送到棧中保存。

一個 canvas 狀態(tài)包括:

  • strokeStyle , fillStyle , globalAlpha , lineWidth , lineCap , lineJoin , miterLimit 的值
  • 當前的裁切路徑
  • 當前應用的變形

你可以調(diào)用任意多次 save 方法。

每一次調(diào)用 restore 方法,上一個保存的狀態(tài)就從棧中彈出,所有設(shè)定都恢復。

舉例

ctx.fillRect(20, 20, 200, 200) // 使用默認設(shè)置,即黑色樣式,繪制一個矩形

ctx.save() // 保存當前黑色樣式的狀態(tài)

ctx.fillStyle = '#ff0000' // 設(shè)置一個填充樣式,紅色
ctx.fillRect(30, 30, 200, 200) // 使用紅色樣式繪制一個矩形

ctx.save() // 保存當前紅色樣式的狀態(tài)

ctx.fillStyle = '#00ff00' // 設(shè)置一個新的填充樣式,綠色
ctx.fillRect(40, 40, 200, 200) // 使用綠色樣式繪制一個矩形

ctx.restore() // 取出棧頂?shù)募t色樣式狀態(tài),恢復
ctx.fillRect(50, 50, 200, 200) // 此時狀態(tài)為紅色樣式,繪制一個矩形

ctx.restore() // 取出棧頂?shù)暮谏珮邮綘顟B(tài),恢復
ctx.fillRect(60, 60, 200, 200) // 此時狀態(tài)為黑色樣式,繪制一個矩形

運行效果如下:

狀態(tài)保存與恢復

繪制動畫

之前我們介紹都是靜態(tài)圖像的繪制,接下來介紹動畫的繪制方法。

基本原理

canvas 動畫的基本原理并不復雜,就是利用 ?setInterval? 和 ?setTimeout? 來逐幀的在畫布上繪制圖形。

基本步驟

在每一幀繪制的過程中,基本遵循以下步驟。

  • 清空 canvas除非接下來要畫的內(nèi)容會完全充滿畫布(例如背景圖),否則你需要清空所有內(nèi)容。最簡單的做法就是用 clearRect。
  • 保存 canvas 狀態(tài)如果你要改變一些會改變 canvas 狀態(tài)的設(shè)置(樣式,變形之類的),又要在每畫一幀之時都是原始狀態(tài)的話,你需要先保存一下。
  • 繪制動畫圖形(animated shapes)這一步才是重繪動畫幀。
  • 恢復 canvas 狀態(tài)如果已經(jīng)保存了 canvas 的狀態(tài),可以先恢復它,然后重繪下一幀。

像素操作 

到目前為止,我們尚未深入了解 canvas 畫布真實像素的原理,事實上,你可以直接通過 ImageData 對象操縱像素數(shù)據(jù),直接讀取或?qū)?shù)據(jù)數(shù)組寫入該對象中。

ImageData 對象

在快應用中 ImageData 對象是一個普通對象,其中存儲著 canvas 對象真實的像素數(shù)據(jù),它包含以下幾個屬性

  • width 使用像素描述 ImageData 的實際寬度
  • height 使用像素描述 ImageData 的實際高度
  • data Uint8ClampedArray 類型,描述了一個一維數(shù)組,包含以 RGBA 順序的數(shù)據(jù),數(shù)據(jù)使用 0 至 255(包含)的整數(shù)表示

data 屬性返回一個 Uint8ClampedArray,它可以被使用作為查看初始像素數(shù)據(jù)。每個像素用 4 個 1 bytes 值(按照紅,綠,藍和透明值的順序; 這就是 "RGBA" 格式) 來代表。每個顏色值部份用 0 至 255 來代表。每個部份被分配到一個在數(shù)組內(nèi)連續(xù)的索引,左上角像素的紅色部份在數(shù)組的索引 0 位置。像素從左到右被處理,然后往下,遍歷整個數(shù)組。

Uint8ClampedArray 包含 高度 × 寬度 × 4 bytes 數(shù)據(jù),索引值從 0 到(高度 × 寬度 × 4) - 1

例如,要讀取圖片中位于第 50 行,第 200 列的像素的藍色部份,你會寫以下代碼:

const blueComponent = imageData.data[50 * (imageData.width * 4) + 200 * 4 + 2]

你可能用會使用 Uint8ClampedArray.length 屬性來讀取像素數(shù)組的大小(以 bytes 為單位):

const numBytes = imageData.data.length

創(chuàng)建一個 ImageData 對象

去創(chuàng)建一個新的,空白的 ImageData 對象,你應該會使用 createImageData() 方法。有 2 個版本的 createImageData() 方法

const myImageData = ctx.createImageData(width, height)

上面代碼創(chuàng)建了一個新的具體特定尺寸的 ImageData 對象。所有像素被預設(shè)為透明黑。

你也可以創(chuàng)建一個被 anotherImageData 對象指定的相同像素的 ImageData 對象。這個新的對象像素全部被預設(shè)為透明黑。這個并非復制了圖片數(shù)據(jù)。

const myImageData = ctx.createImageData(anotherImageData)

得到場景像素數(shù)據(jù)

為了獲得一個包含畫布場景像素數(shù)據(jù)的 ImageData 對像,你可以用 getImageData() 方法:

const myImageData = ctx.getImageData(left, top, width, height)

這個方法會返回一個 ImageData 對象,它代表了畫布區(qū)域的對象數(shù)據(jù),此畫布的四個角落分別表示為(left, top),(left + width, top),(left, top + height),以及(left + width, top + height)四個點。這些坐標點被設(shè)定為畫布坐標空間元素。

在場景中寫入像素數(shù)據(jù)

你可以用 putImageData() 方法去對場景進行像素數(shù)據(jù)的寫入。

ctx.putImageData(myImageData, dx, dy)

dx 和 dy 參數(shù)表示你希望在場景內(nèi)左上角繪制的像素數(shù)據(jù)所得到的設(shè)備坐標。

例如,為了在場景內(nèi)左上角繪制 myImageData 代表的圖片,你可以寫如下的代碼:

ctx.putImageData(myImageData, 0, 0)

舉例

在這個例子里,我們接著對剛才的快應用 logo 進行置灰色,我們使用 getImageData 獲取 ImageData 對象,遍歷所有像素以改變他們的數(shù)值。然后我們將被修改的像素數(shù)組通過 putImageData() 放回到畫布中去。 grayscale 函數(shù)僅僅是用以計算紅綠和藍的平均值。你也可以用加權(quán)平均,例如 x = 0.299r + 0.587g + 0.114b 這個公式

setGray() {
    const canvas = this.$element('new-canvas')
    const ctx = canvas.getContext('2d')
    const canvasW = 380
    const canvasH = 380

    // 得到場景像素數(shù)據(jù)
    const imageData = ctx.getImageData(0, 0, 380, 380)
    const data = imageData.data

    for (let i = 0; i < data.length; i += 4) {
        const avg = (data[i] + data[i + 1] + data[i + 2]) / 3
        data[i] = avg; // red
        data[i + 1] = avg; // green
        data[i + 2] = avg; // blue
    }

    // 在場景中寫入像素數(shù)據(jù)
    ctx.putImageData(imageData, 0, 0)
}

運行效果如下

操作像素

總結(jié)

了解 canvas 的特點,現(xiàn)在就可以實現(xiàn)基本組件無法實現(xiàn)的視覺效果。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號