快應(yīng)用 canvas教程

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

通過(guò)本節(jié),你將學(xué)會(huì):

創(chuàng)建畫布

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

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

canvas 組件

和其他組件一樣,在快應(yīng)用 template 中添加即可。同時(shí)可為其添加需要的樣式。

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

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

渲染腳本

單獨(dú)的 canvas 組件僅僅是一個(gè)透明矩形,我們需要通過(guò)渲染腳本來(lái)進(jìn)一步操作。

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

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

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

完整示例代碼如下:

<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簡(jiǎn)單繪制'
      })
    },
    onShow() {
      if (!this.drawComplete) {
        this.drawCanvas()
      }
    },
    drawCanvas() {
      const canvas = this.$element('new-canvas') //獲取 canvas 組件
      const ctx = canvas.getContext('2d') //獲取 canvas 繪圖上下文

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

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

      this.drawComplete = true
    }
  }
</script>

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

輸出效果如圖

基礎(chǔ)示例

繪制

坐標(biāo)系

開(kāi)始畫圖之前,需要了解一下畫布的坐標(biāo)系。

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

坐標(biāo)系

填充繪制(fill)

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

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

描邊繪制(stroke)

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

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

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

繪制圖形

繪制矩形

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

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

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

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

繪制路徑

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

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

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

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

beginPath()

開(kāi)始一條新路徑,這是生成路徑的第一步操作。

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

closePath()

閉合當(dāng)前路徑。

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

stroke()

描邊繪制當(dāng)前路徑。

fill()

填充繪制當(dāng)前路徑。

當(dāng)調(diào)用 ?fill()?時(shí),當(dāng)前沒(méi)有閉合的路徑會(huì)自動(dòng)閉合,不需要手動(dòng)調(diào)用 closePath() 函數(shù)。調(diào)用 ?stroke()? 時(shí)不會(huì)自動(dòng)閉合。

moveTo(x, y)

移動(dòng)筆觸。將當(dāng)前路徑繪制的筆觸移動(dòng)到某個(gè)坐標(biāo)點(diǎn)。

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

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

路徑繪制命令

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

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

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

組合使用

這里,我們展示一個(gè)組合使用的效果,繪制一個(gè)快應(yīng)用的 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()
}

實(shí)現(xiàn)效果如下

組合繪制路徑

顏色和樣式

通過(guò)剛才的例子,我們學(xué)會(huì)了繪制圖形。

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

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

顏色

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

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

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

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

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

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

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

漸變色

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

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

這里要注意的是,漸變色對(duì)象的坐標(biāo)尺寸都是相對(duì)畫布的。應(yīng)用了漸變色的圖形實(shí)際起到的是類似“蒙版”的效果。

//填充繪制一個(gè)矩形,填充顏色為深紅到深藍(lán)的線性漸變色
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)

//描邊繪制另一個(gè)矩形,邊框顏色為深藍(lán)到深紅的線性漸變色
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í)候,為描邊的線條增加線型。

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

線寬(lineWidth)

線寬

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

這里要注意兩點(diǎn):

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

端點(diǎn)樣式(lineCap)

端點(diǎn)樣式

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

這里要注意的是,?round? 和 ?square? 會(huì)使得線段描繪出來(lái)的視覺(jué)長(zhǎng)度,兩端各多出半個(gè)線寬,可參考藍(lán)色輔助線。

交點(diǎn)樣式(lineJoin)

交點(diǎn)樣式

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

交點(diǎn)最大斜接長(zhǎng)度(miterLimit)

交點(diǎn)最大斜接長(zhǎng)度

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

?miterLimit? 屬性就是用來(lái)設(shè)定外延交點(diǎn)與連接點(diǎn)的最大距離,如果交點(diǎn)距離大于此值,交點(diǎn)樣式會(huì)自動(dòng)變成了 ?bevel?。

示例

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

使用虛線

用 ?setLineDash? 方法和 ?lineDashOffset? 屬性來(lái)制定虛線樣式。 ?setLineDash? 方法接受一個(gè)數(shù)組,來(lái)指定線段與間隙的交替;?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è)置虛線線段和間隙長(zhǎng)度 分別為 4px 2px
        ctx.setLineDash([4, 2])
        // 設(shè)置虛線的起始偏移量
        ctx.lineDashOffset = -offset
        ctx.strokeRect(10, 10, 200, 200)
    }, 20)
}

運(yùn)行效果如下

繪制虛線

組合使用

通過(guò)學(xué)習(xí),我們?yōu)閯偛爬L制的快應(yīng)用 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()
}

實(shí)現(xiàn)效果如下

顏色和樣式

繪制文字

和繪制圖形類似,快應(yīng)用 canvas 也提供 ?fillText? 和 ?strokeText? 兩種方法來(lái)繪制文字。

基本用法

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

文字樣式

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

字體(font)

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

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

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

這兩個(gè)屬性控制了文體相對(duì)與繪制定位點(diǎn)的對(duì)齊方式。

示例

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

使用圖片

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

圖像對(duì)象

為了能夠在 canvas 中使用圖片,需要使用圖像對(duì)象來(lái)加載圖片。

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

圖片加載

修改圖像對(duì)象的 src 屬性,即可啟動(dòng)圖片加載。

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

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

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? 在畫布中進(jìn)行圖片繪制了。

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

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

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

基礎(chǔ)

drawImage(image, x, y)

其中 image 是加載的圖像對(duì)象,x 和 y 是其在目標(biāo) canvas 里的起始坐標(biāo)。

這種方法會(huì)將圖片原封不動(dòng)的繪制在畫布上,是最基本的繪制方法。

縮放

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

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

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

切片

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

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

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

切片繪圖

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

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

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

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

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

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

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

合成與裁切

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

合成

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

?globalCompositeOperation = type?

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

source-over

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

canvas合成方式 source-over

source-atop

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

canvas合成方式 source-atop

source-in

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

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

兩個(gè)重疊圖形的顏色是通過(guò)顏色值相加來(lái)確定的。

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)

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

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

      // 繪制第二個(gè)矩形
      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? 繪制一個(gè)不可見(jiàn)的圖形。一旦設(shè)置好裁切路徑,那么你在畫布上新繪制的所有內(nèi)容都將局限在該區(qū)域內(nèi),區(qū)域以外進(jìn)行繪制是沒(méi)有任何效果的。

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

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

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

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

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

運(yùn)行效果如下

疊加效果

變形

到目前位置,我們所有的繪制,都是基于標(biāo)準(zhǔn)坐標(biāo)系來(lái)繪制的。

標(biāo)準(zhǔn)坐標(biāo)系的特點(diǎn)是:

  • 原點(diǎn)在左上角
  • 尺寸與畫布像素點(diǎn) 1:1

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

變形的基本方法

  • 平移: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()

變形的基本原則

  • 不會(huì)改變已經(jīng)繪制的圖形
  • 改變的是坐標(biāo)系
  • 變形之后的所有繪制將依照新的坐標(biāo)系來(lái)繪制

舉例

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

運(yùn)行效果如圖。

變形

可以看到,雖然每次 ?fillRect? 繪制的參數(shù)沒(méi)有變化,但是因?yàn)樽鴺?biāo)系變了,最終繪制出來(lái)的就是位置不同的圖形。

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

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

在繪制復(fù)雜圖形的時(shí)候,就會(huì)帶來(lái)重復(fù)獲取樣式的問(wèn)題。

如何優(yōu)化呢?

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

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

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

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

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

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

  • strokeStyle , fillStyle , globalAlpha , lineWidth , lineCap , lineJoin , miterLimit 的值
  • 當(dāng)前的裁切路徑
  • 當(dāng)前應(yīng)用的變形

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

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

舉例

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

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

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

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

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

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

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

運(yùn)行效果如下:

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

繪制動(dòng)畫

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

基本原理

canvas 動(dòng)畫的基本原理并不復(fù)雜,就是利用 ?setInterval? 和 ?setTimeout? 來(lái)逐幀的在畫布上繪制圖形。

基本步驟

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

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

像素操作 

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

ImageData 對(duì)象

在快應(yīng)用中 ImageData 對(duì)象是一個(gè)普通對(duì)象,其中存儲(chǔ)著 canvas 對(duì)象真實(shí)的像素?cái)?shù)據(jù),它包含以下幾個(gè)屬性

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

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

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

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

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

你可能用會(huì)使用 Uint8ClampedArray.length 屬性來(lái)讀取像素?cái)?shù)組的大?。ㄒ?bytes 為單位):

const numBytes = imageData.data.length

創(chuàng)建一個(gè) ImageData 對(duì)象

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

const myImageData = ctx.createImageData(width, height)

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

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

const myImageData = ctx.createImageData(anotherImageData)

得到場(chǎng)景像素?cái)?shù)據(jù)

為了獲得一個(gè)包含畫布場(chǎng)景像素?cái)?shù)據(jù)的 ImageData 對(duì)像,你可以用 getImageData() 方法:

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

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

在場(chǎng)景中寫入像素?cái)?shù)據(jù)

你可以用 putImageData() 方法去對(duì)場(chǎng)景進(jìn)行像素?cái)?shù)據(jù)的寫入。

ctx.putImageData(myImageData, dx, dy)

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

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

ctx.putImageData(myImageData, 0, 0)

舉例

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

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

    // 得到場(chǎng)景像素?cái)?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
    }

    // 在場(chǎng)景中寫入像素?cái)?shù)據(jù)
    ctx.putImageData(imageData, 0, 0)
}

運(yùn)行效果如下

操作像素

總結(jié)

了解 canvas 的特點(diǎn),現(xiàn)在就可以實(shí)現(xiàn)基本組件無(wú)法實(shí)現(xiàn)的視覺(jué)效果。


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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)