了解如何正確使用 canvas 畫布,以及通過(guò) canvas 繪制圖形及動(dòng)畫。
通過(guò)本節(jié),你將學(xué)會(huì):
快應(yīng)用的 canvas 功能由兩部分組成,canvas 組件和渲染腳本。
canvas 組件中,用于繪制圖形的部分,稱之為 畫布。
和其他組件一樣,在快應(yīng)用 template 中添加即可。同時(shí)可為其添加需要的樣式。
這里需要注意,與 HTML 中 canvas 不同的是:
單獨(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),繪制圖形。
輸出效果如圖
開(kāi)始畫圖之前,需要了解一下畫布的坐標(biāo)系。
如下圖所示,坐標(biāo)系原點(diǎn)為左上角(坐標(biāo)為(0,0))。所有元素的位置都相對(duì)于原點(diǎn)定位。x 軸向右遞增,y 軸向下遞增。
canvas 繪圖的基本繪制方式之一是填充繪制。
填充是指用指定的內(nèi)容填滿所要繪制的圖形,最終生成一個(gè)實(shí)心的圖案。
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),在畫布上繪制圖形。
與繪制矩形的直接繪制不同,繪制路徑需要一些額外的步驟。
為此,我們需要了解以下一些基本方法。
開(kāi)始一條新路徑,這是生成路徑的第一步操作。
一條路徑本質(zhì)上是由多段子路徑(直線、弧形、等等)組成。而每次調(diào)用 beginPath 之后,子路徑清空重置,然后就可以重新繪制新的圖形。
閉合當(dāng)前路徑。
?closePath()
? 不是必須的操作,相當(dāng)于繪制一條當(dāng)前位置到路徑起始位置的直線子路徑。
描邊繪制當(dāng)前路徑。
填充繪制當(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)閉合。
移動(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)目包括:
顧名思義,線寬就是描邊線條的寬度,單位是像素。
這里要注意兩點(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)樣式?jīng)Q定了線段端點(diǎn)顯示的樣子。從上至下依次為 ?butt
?,?round
?和 ?square
?,其中 ?butt
?為默認(rèn)值。
這里要注意的是,?round
? 和 ?square
? 會(huì)使得線段描繪出來(lái)的視覺(jué)長(zhǎng)度,兩端各多出半個(gè)線寬,可參考藍(lán)色輔助線。
交點(diǎn)樣式?jīng)Q定了圖形中兩線段連接處所顯示的樣子。從上至下依次為 ?miter
?, ?bevel
? 和 ?round
?,?miter
? 為默認(rèn)值。
在上圖交點(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ú)有的樣式。
可以直接使用符合 CSS font 語(yǔ)法的字符串作為文字樣式的字體屬性。默認(rèn)值為 ?'10px sans-serif'
?。
要注意的是,不同于 web,目前快應(yīng)用還無(wú)法引入外部字體文件,對(duì)于字體的選擇,僅限 serif、sans-serif 和 monosapce。
這兩個(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)用還支持使用圖片。
為了能夠在 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)控制。
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í)不同遮蓋方式的字符串。
這是默認(rèn)設(shè)置,并在現(xiàn)有畫布上下文之上繪制新圖形。
新圖形只在與現(xiàn)有畫布內(nèi)容重疊的地方繪制。
新圖形只在新圖形和目標(biāo)畫布重疊的地方繪制。其他的都是透明的。
在不與現(xiàn)有畫布內(nèi)容重疊的地方繪制新圖形。
在現(xiàn)有的畫布內(nèi)容后面繪制新的圖形。
現(xiàn)有的畫布只保留與新圖形重疊的部分,新的圖形是在畫布內(nèi)容后面繪制的。
現(xiàn)有的畫布內(nèi)容保持在新圖形和現(xiàn)有畫布內(nèi)容重疊的位置。其他的都是透明的。
現(xiàn)有內(nèi)容保持在新圖形不重疊的地方。
兩個(gè)重疊圖形的顏色是通過(guò)顏色值相加來(lái)確定的。
只顯示新圖形。
圖像中,那些重疊和正常繪制之外的其他地方是透明的。
<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)是:
現(xiàn)在介紹的變形,就是改變標(biāo)準(zhǔn)坐標(biāo)系的方法。
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)的就是位置不同的圖形。
通過(guò)前面的學(xué)習(xí),我可以看到,每次圖形繪制其實(shí)都帶著非常豐富的狀態(tài)。
在繪制復(fù)雜圖形的時(shí)候,就會(huì)帶來(lái)重復(fù)獲取樣式的問(wèn)題。
如何優(yōu)化呢?
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)包括:
你可以調(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)圖像的繪制,接下來(lái)介紹動(dòng)畫的繪制方法。
canvas 動(dòng)畫的基本原理并不復(fù)雜,就是利用 ?setInterval
? 和 ?setTimeout
? 來(lái)逐幀的在畫布上繪制圖形。
在每一幀繪制的過(guò)程中,基本遵循以下步驟。
到目前為止,我們尚未深入了解 canvas 畫布真實(shí)像素的原理,事實(shí)上,你可以直接通過(guò) ImageData 對(duì)象操縱像素?cái)?shù)據(jù),直接讀取或?qū)?shù)據(jù)數(shù)組寫入該對(duì)象中。
在快應(yīng)用中 ImageData 對(duì)象是一個(gè)普通對(duì)象,其中存儲(chǔ)著 canvas 對(duì)象真實(shí)的像素?cái)?shù)據(jù),它包含以下幾個(gè)屬性
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ì)象,你應(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)
為了獲得一個(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)空間元素。
你可以用 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)行效果如下
了解 canvas 的特點(diǎn),現(xiàn)在就可以實(shí)現(xiàn)基本組件無(wú)法實(shí)現(xiàn)的視覺(jué)效果。
更多建議: