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