Canvas學習
canvas 讀音 /ˈkænvəs/, 即kæn və s(看我死).
學習的目的主要是為了網狀關系拓撲圖形的繪制.
推薦文檔:https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API/Tutorial
一、 Canvas概述
canvas是用來繪制圖形的.它可以用於動畫、游戲畫面、數據可視化、圖片編輯以及實時視頻處理等方面。
長久以來, web上的動畫都是Flash. 比如動畫廣告\ 游戲等等, 基本都是Flash 實現的. Flash目前都被禁用了, 而且漏洞很多, 重量很大, 需要安裝Adobe Flash Player, 而且也會卡頓和不流暢等等.
canvas是HTML5提出的新標簽,徹底顛覆了Flash的主導地位。無論是廣告、游戲都可以使用canvas實現。
Canvas 是一個輕量級的畫布, 我們使用Canvas進行JS的編程,不需要增加額外的組件,性能也很好,不卡頓,在手機中也很流暢。
1.1 Hello world
我們可以在頁面中設置一個canvas標簽
<canvas width="500" height="500">
當前的瀏覽器版本不支持,請升級瀏覽器
</canvas>
canvas的標簽屬性只有兩個,width和height,表示的是canvas畫布的寬度和高度,不要用css來設置,而是用屬性來設置,畫布會失真變形。
標簽的innerContent是用來提示低版本瀏覽器(IE6、7、8)並不能正常使用canvas,高版本的瀏覽器是看不到canvas標簽內部的文字的。
// 得到canvas的畫布
const myCanvas:HTMLCanvasElement = document.getElementById("main_canvas") as HTMLCanvasElement// 返回某種類型的HTMLElement
// 得到畫布的上下文,上下文有兩個,2d的上下文和3d的上下文
// 所有的圖像繪制都是通過ctx屬性或者是方法進行設置的,和canvas標簽沒有關系了
const ctx = myCanvas.getContext("2d")
if(ctx !== null) {
// 設置顏色
ctx.fillStyle = 'green'
// 繪制矩形
ctx.fillRect(100, 100, 200, 50)
}
通過上面的代碼我們發下canvas本質上就是利用代碼在瀏覽器的頁面上進行畫畫,比如上面的代碼fillRect就代表在頁面中繪制矩形,一共四個屬性,前兩個100,100代表(x, y), 即填充起始位置,200代表寬,50代表高,單位都是px。
1.2 Canvas的像素化
我們用canvas繪制了一個圖形,一旦繪制成功了,canvas就像素化了他們。canvas沒有能力,從畫布上再次得到這個圖形,也就是我們沒有能力去修改已經在畫布上的內容,這個就是canvas比較輕量的原因,Flash重的原因之一就有它可以通過對應的api得到已經上“畫布”的內容然后再次繪制
如果我們想要這個canvas圖形移動,必須按照:清屏——更新——渲染的邏輯進行編程。總之,就是重新再畫一次
1.3 Canvas的動畫思想
要使用面向對象的思想來創建動畫。
canvas上畫布的元素,就被像素化了,所以不能通過style.left方法進行修改,而且必須要重新繪制。
// 得到畫布
const myCanvas:HTMLCanvasElement = document.getElementById("main_canvas") as HTMLCanvasElement
// 獲取上下文
const ctx = myCanvas.getContext("2d")
if(ctx !== null) {
// 設置顏色
ctx.fillStyle = "blue"
// 初始信號量
let left:number = -200
// 動畫過程
setInterval(() => {
// 清除畫布,0,0代表從什么位置開始,600,600代表清除的寬度和高度
ctx.clearRect(0,0,600,600)
// 更新信號量
left++
// 如果已經走出畫布,則更新信號量為初始位置
if(left > 600) {
left = -200
}
ctx.fillRect(left, 100, 200, 200)
},10)
}
實際上,動畫的生成就是相關靜態畫面連續播放,這個就是動畫的過程。我們把每一次繪制的靜態話面叫做一幀,時間的間隔(定時器的間隔)就是表示的是幀的間隔。
1.4 面向對象思維實現canvas動畫
因為canvas不能得到已經上屏的對象,所以我們要維持對象的狀態。在canvas動畫重,我們都是用面向對象來進行編程,因為我們可以使用面向對象的方式來維持canvas需要的屬性和狀態;
// 得到畫布
const myCanvas:HTMLCanvasElement = document.getElementById("main_canvas") as HTMLCanvasElement
// 獲取上下文
const ctx = myCanvas.getContext("2d")
class Rect {
// 維護狀態
constructor(
public x: number,
public y: number,
public w: number,
public h: number,
public color: string
) {
}
// 更新的方法
update() {
this.x++
if(this.x > 600) {
this.x = -200
}
}
// 渲染
render(ctx: CanvasRenderingContext2D) {
// 設置顏色
ctx.fillStyle = this.color
// 渲染
ctx.fillRect(this.x, this.y, this.w, this.h)
}
}
// 實例化
let myRect1: Rect = new Rect(-100, 200, 100, 100, 'purple')
let myRect2: Rect = new Rect(-100, 400, 100, 100, 'pink')
// 動畫過程
// 更新的辦法
setInterval(() => {
// 清除畫布,0,0代表從什么位置開始,600,600代表清除的寬度和高度
if(ctx !== null) {
// 清屏
ctx.clearRect(0,0,600,600)
// 更新方法
myRect1.update()
myRect2.update()
// 渲染方法
myRect1.render(ctx)
myRect2.render(ctx)
}
},10)
動畫過程在主定時器重,每一幀都會調用實例的更新和渲染方法。
二、Canvas的繪制功能
2.1 繪制矩形
填充一個矩形:
if(ctx !== null) {
// 設置顏色
ctx.fillStyle = 'green'
// 填充矩形
ctx.fillRect(100, 100, 300, 50)
}
參數含義:分別代表填充坐標x、填充坐標y、矩形的高度和寬度。
繪制矩形邊框,和填充不同的是繪制使用的是strokeRect, 和strokeStyle實現的
if (ctx !== null) {
// 設置顏色
ctx.strokeStyle = 'red'
// 繪制矩形
ctx.strokeRect(300, 100, 100, 100)
}
參數含義:分別代表繪制坐標x、繪制坐標y、矩形的高度和寬度。
清除畫布,使用clearRect
// 擦除畫布內容
btn3.onclick = () => {
if (ctx !== null) {
ctx.clearRect(0, 0, 600, 600)
}
}
參數含義:分別代表擦除坐標x、擦除坐標y、擦除的高度和擦除的寬度。
2.2 繪制路徑
繪制路徑的作用是為了設置一個不規則的多邊形狀態
路徑都是閉合的,使用路徑進行繪制的時候需要既定的步驟:
-
需要設置路徑的起點
-
使用繪制命令畫出路徑
-
封閉路徑
-
填充或者繪制已經封閉路徑的形狀
// 創建一個路徑
ctx.beginPath()
// 1. 移動繪制點
ctx.moveTo(100, 100)
// 2. 描述行進路徑
ctx.lineTo(200, 200)
ctx.lineTo(400, 180)
ctx.lineTo(380, 50)
// 3. 封閉路徑
ctx.closePath();
// 4. 繪制這個不規則的圖形
ctx.strokeStyle = 'red'
ctx.stroke()
ctx.fillStyle = 'orange'
ctx.fill()
總結我們要繪制一個圖形,要按照順序
- 開始路徑
ctx.beginPath()
- 移動繪制點
ctx.moveTo(x, y)
- 描述繪制路徑
ctx.lineTo(x, y)
- 多次描述繪制路徑
ctx.lineTo(x, y)
- 閉合路徑
ctx.closePath()
- 描邊
ctx.stroke()
- 填充
ctx.fill()
此時我們發現之前我們在學習繪制矩形的時候使用的是fillRect
和strokeRect
,但是實際上fill
和stroke
也是具有繪制填充功能的
stroke()
: 通過線條來繪制圖形輪廓。
fill()
: 通過填充路徑的內容區域生成實心的圖形。
我們在繪制路徑的時候選擇不關閉路徑(closePath),這個時候會實現自封閉現象(只針對fill,stroke不生效)
2.3 繪制圓弧
arc(x, y, radius, startAngle, endAngle, anticlockwise)
畫一個以(x, y)為圓心的以radius為半徑的圓弧(圓), 從startAngle開始到endAngle結束,按照anticlockwise給定的方向(默認為順時針false, true為逆時針)來生成。
// 創建一個路徑
ctx.beginPath()
// 移動繪制點
// ctx.arc(200, 200, 100, 0, 2 * Math.PI, false)
ctx.arc(200, 200, 100, 0, 2 * 3.14, false)
ctx.stroke()
圓弧也是繪制路徑的一種,也需要beginPath和stroke.
參數的含義:200, 200代表的是起始x,y坐標,100代表的是圓心半徑,0和1代表的是開始和結束位置,單位如果是數字,代表的是一個圓弧的弧度(一個圓的弧度是Math.PI * 2, 約等於7個弧度),所以在順時針的情況下,如果如果兩個參數的差為7,則代表繪制一個圓。
2.4 炫彩小球
// 得到畫布
const myCanvas: HTMLCanvasElement = document.getElementById("main_canvas") as HTMLCanvasElement
// 獲取上下文
const ctx = myCanvas.getContext("2d")
class Ball {
color: string // 小球的顏色
r: number // 小球的半徑
dx: number // 小球在x軸的運動速度/幀
dy: number // 小球在y軸的運動速度/幀
constructor(public x: number, public y: number) {
// 設置隨機顏色
this.color = this.getRandomColor()
// 設置隨機半徑[1, 101)
this.r = Math.floor(Math.random() * 100 + 1)
// 設置x軸, y軸的運動速度(-5, 5)
this.dx = Math.floor(Math.random() * 10) - 5
this.dy = Math.floor(Math.random() * 10) - 5
}
// 隨機顏色,最后返回的是類似'#3fe432'
getRandomColor(): string {
let allType = "0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f"
let allTypeArr = allType.split(',')
let color = '#'
for (let i = 0; i < 6; i++) {
let random = Math.floor(Math.random() * allTypeArr.length)
color += allTypeArr[random]
}
return color
}
// 渲染小球
render(): void {
if(ctx !== null) {
ctx.beginPath()
ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2, false)
ctx.fillStyle=this.color
ctx.fill()
}
}
// 更新小球
update(): void {
// 小球的運動
this.x += this.dx
this.y += this.dy
this.r -= 0.5
// 如果小球的半徑小於0了,從數組中刪除
if(this.r <= 0) {
this.remove()
}
}
// 移除小球
remove(): void {
for(let i = 0; i < ballArr.length; i++) {
if(ballArr[i] === this) {
ballArr.splice(i, 1)
}
}
}
}
// 維護小球的數組
let ballArr: Ball[] = []
// canvas設置鼠標監聽
myCanvas.addEventListener("mousemove", (event)=> {
ballArr.push(new Ball(event.offsetX, event.offsetY))
})
// 定時器進行動畫渲染和更新
setInterval(function() {
// 動畫的邏輯,清屏-更新-渲染
if(ctx !== null) {
ctx.clearRect(0, 0, myCanvas.width, myCanvas.height)
}
for(let i = 0; i < ballArr.length; i++) {
// 小球的更新和渲染
ballArr[i].update()
if(ballArr[i]) {
ballArr[i].render()
}
}
// 60 幀
}, 1000 / 60)
2.5 透明度
透明度的值是0到1之間: (1是完全不透明,0是完全透明)
ctx.globalAlpha = 1
2.6 小球連線
// 得到畫布
const myCanvas: HTMLCanvasElement = document.getElementById("mycanvas") as HTMLCanvasElement
// 獲取上下文
const ctx = myCanvas.getContext("2d")
// 設置畫布的尺寸
myCanvas.width = document.documentElement.clientWidth - 30
myCanvas.height = document.documentElement.clientHeight - 30
class Ball {
x: number = Math.floor(Math.random() * myCanvas.width)
y: number = Math.floor(Math.random() * myCanvas.height)
r: number = 10
color: string = 'gray'
dx: number = Math.floor(Math.random() * 10) - 5
dy: number = Math.floor(Math.random() * 10) - 5
constructor() {
ballArr.push(this)
}
// 小球的渲染
render() {
if(ctx !== null) {
ctx.beginPath()
// 透明度
ctx.globalAlpha = 1
// 畫小球
ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2, false)
ctx.fillStyle = this.color
ctx.fill()
}
}
// 小球的更新
update() {
// 更新x
this.x += this.dx
// 糾正x
if(this.x <= this.r) {
this.x = this.r
} else if ( this.x >= myCanvas.width - this.r) {
this.x = myCanvas.width - this.r
}
// 更新y
this.y += this.dy
// 糾正y
if(this.y <= this.r) {
this.y = this.r
} else if ( this.y >= myCanvas.height - this.r) {
this.y = myCanvas.height - this.r
}
// 碰壁返回
if(this.x + this.r >= myCanvas.width || this.x - this.r <= 0) {
this.dx *= -1
}
if(this.y + this.r >= myCanvas.height || this.y - this.r <= 0) {
this.dy *= -1
}
}
}
// 小球數組
let ballArr: Ball[] = []
// 創建20個小球
for(let i = 0; i < 20; i++) {
new Ball()
}
// 定時器動畫
setInterval(() => {
// 清除畫布
if(ctx !== null) {
ctx.clearRect(0, 0, myCanvas.width, myCanvas.height)
}
// 小球渲染和更新
for(let i = 0; i < ballArr.length; i++) {
ballArr[i].render()
ballArr[i].update()
}
// 畫線的邏輯
if(ctx !== null) {
for(let i = 0; i < ballArr.length; i++) {
for(let j = i + 1; j < ballArr.length; j++) {
let distance = Math.sqrt(Math.pow((ballArr[i].x - ballArr[j].x), 2) + Math.pow((ballArr[i].y -ballArr[j].y), 2))
if( distance <= 150) {
ctx.strokeStyle = '#000'
ctx.beginPath()
// 線的透明度,根據當前已經連線的小球的距離進行線的透明度設置
// 距離越近透明度越大,距離越遠透明度越小
ctx.globalAlpha = 1 - distance / 150
ctx.moveTo(ballArr[i].x, ballArr[i].y)
ctx.lineTo(ballArr[j].x, ballArr[j].y)
ctx.closePath()
ctx.stroke()
}
}
}
}
}, 1000/60)
2.7 線型
lineWidth
我們可以利用lineWidth設置線的粗細,屬性值為number型,默認為1,沒有單位
ctx.lineWidth = 1.0
lineCap
我們可以使用lineCap指定如何繪制每一條線段末端的屬性:"butt" | "round" | "square"
, 其中butt
代表線段末端以方形結束,round
表示線段末端以圓形結束,square
線段末端以方形結束,但是增加了一個寬度和線段相同,高度是線段厚度一半的矩形區域,默認是butt
。
ctx.lineCap = "round";
該圖是三種lineCapd的類型,從左到右依次為butt
、 round
和square
。
lineJoin
我們可以使用lineJoin來設置設置2個長度不為0的相連部分(線段,圓弧,曲線)如何連接在一起的屬性(長度為0的變形部分,其指定的末端和控制點在同一位置,會被忽略):"bevel" | "round" | "miter"
。
ctx.lineJoin = "bevel";
round
表示通過填充一個額外的,圓心在相連部分末端的扇形,繪制拐角的形狀。 圓角的半徑是線段的寬度。bevel
表示在相連部分的末端填充一個額外的以三角形為底的區域, 每個部分都有各自獨立的矩形拐角。mitter
表示通過延伸相連部分的外邊緣,使其相交於一點,形成一個額外的菱形區域。
setLineDash
我們可以使用setLineDash方法在填充線時使用虛線模式。
ctx.setLineDash(segments);
segments
是一個Array
數組。一組描述交替繪制線段和間距(坐標空間單位)長度的數字。 如果數組元素的數量是奇數, 數組的元素會被復制並重復。例如,[5, 15, 25]
會變成[5, 15, 25, 5, 15, 25]
。數組內部是虛線的交替狀態
// 得到畫布
const myCanvas: HTMLCanvasElement = document.getElementById("mycanvas") as HTMLCanvasElement
// 獲取上下文
const ctx = myCanvas.getContext("2d")
// 畫布的尺寸
myCanvas.width = document.documentElement.clientWidth - 30
myCanvas.height = document.documentElement.clientHeight - 30
if(ctx !== null) {
ctx.setLineDash([15, 15]);
ctx.strokeRect(50,50, 90, 90)
ctx.setLineDash([15,10,2,10])
ctx.strokeRect(200,50, 90, 90)
}
lineDashOffset
我們可以使用lineDashOffset設置虛線偏移量的屬性。設置的是起始偏移量,使線向左移動value。
ctx.lineDashOffset = value;
value
偏移量是float精度的數字。 初始值為0.0
。
2.8 文本
我們可以在畫布上繪制文字:
ctx.font = "30px 微軟雅黑" // 空格前為文字大小,空格后為字體類型
// 第一個參數為文字內容,第二和第三個參數為文字繪制坐標,
// 第四個參數是可選參數,代表文字的最大寬度,如果字體寬度超過該值則壓縮字體寬度
ctx.fillText("你好,世界!", 100, 100)
我們可以使用textAlign
來設置文本的對齊選項。可選的值包括:start
, end
, left
, right
or center
。默認值是 start
。該對齊是基於fillText方法的x的值。
ctx.textAlign = "left" || "right" || "center" || "start" || "end";
left
: 文本左對齊。right
: 文本右對齊。center
: 文本居中對齊。start
: 文本對齊界線開始的地方 (左對齊指本地從左向右,右對齊指本地從右向左)。end
: 文本對齊界線結束的地方 (左對齊指本地從左向右,右對齊指本地從右向左)。
2.9 漸變 Gradients
提供兩種漸變方式,一種是線性漸變,一種是徑向漸變。
- 線性漸變:createLinearGradient 方法接受 4 個參數,表示圖形漸變線的起點 (x1,y1) 與終點 (x2,y2),漸變將沿着這條線向兩邊漸變。
ctx.createLinearGradient(x1, y1, x2, y2)
addColorStop內部接收兩個參數,第一個參數是當前漸變的位置(0~1之間的小數),第二個參數是顏色。
let liner: CanvasGradient = ctx.createLinearGradient(0, 0, 100, 100)
liner.addColorStop(0, 'red')
liner.addColorStop(.5, 'blue')
liner.addColorStop(.8, 'yellow')
liner.addColorStop(1, 'green')
ctx.fillStyle = liner
ctx.fillRect(10, 10, 100,100)
徑向漸變:createRadialGradient方法接受 6 個參數,前三個定義一個以 (x1,y1) 為原點,半徑為 r1 的開始圓形,后三個參數則定義另一個以 (x2,y2) 為原點,半徑為 r2 的結束圓形。
let radial: CanvasGradient = ctx.createRadialGradient(100, 100, 0, 100, 100, 100)
radial.addColorStop(0, 'red')
radial.addColorStop(1, 'purple')
ctx.fillStyle = radial
ctx.arc(100, 100, 100, 0, Math.PI *2, false)
ctx.fill(
2.10 陰影
我們可以在畫布中設置畫布的陰影的狀態:
shadowOffsetX
:shadowOffsetX
和shadowOffsetY
用來設定陰影在 X 和 Y 軸的延伸距離,它們是不受變換矩陣所影響的。負值表示陰影會往上或左延伸,正值則表示會往下或右延伸,它們默認都為0
。shadowOffsetY
:shadowOffsetX
和shadowOffsetY
用來設定陰影在 X 和 Y 軸的延伸距離,它們是不受變換矩陣所影響的。負值表示陰影會往上或左延伸,正值則表示會往下或右延伸,它們默認都為0
。shadowBlur
:shadowBlur
用於設定陰影的模糊程度,其數值並不跟像素數量掛鈎,也不受變換矩陣的影響,默認為0
。shadowColor
:shadowColor
是標准的 CSS 顏色值,用於設定陰影顏色效果,默認是全透明的黑色。
ctx.shadowOffsetX = 1 // 陰影左右偏離的距離
ctx.shadowOffsetY = 1 // 陰影上下偏離的距離
ctx.shadowBlur = 1 // 模糊狀態
ctx.shadowColor = 'green' // 陰影顏色
ctx.font ='30px 宋體'
ctx.fillText('你好,世界!', 100, 100)
三、使用圖片
canvs中使用drawImage來繪制圖片,主要是把外部的圖片導入進來,繪制到畫布上。
圖片的渲染過程:
// 導入圖片
if(ctx !== null) {
// 第一步是創建一個image元素
let image:HTMLImageElement = new Image()
// 用src設置圖片的地址
image.src = "image/test1.png"
// 必須要在onload函數內繪制圖片,否則不會渲染
image.onload = function() {
ctx.drawImage(image, 100, 100)
}
}
如果我們在drawImage里設置的參數一共是兩個(不包含第一個image參數),表示的是圖片的加載位置。
ctx.drawImage(image, 100, 100)
如果drawImage有四個參數,分別表示圖片的繪制位置和圖片的寬高。(注意,此時圖像會被拉伸)
ctx.drawImage(image, 100, 100, 50, 50)
還可以使用八個參數的drawImage, 前四個參數指的是你在圖片中設置切片的寬度和高度,以及切片的位置,后四個參數指的是切片在畫布上的位置和切片寬度高度。
// ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
ctx.drawImage(image, 100, 300, 200, 200)
ctx.drawImage(image, 100, 100, 200, 200, 100, 100, 200, 200)
- sx: 需要繪制到目標上下文中的,
image
的矩形(裁剪)選擇框的左上角 X 軸坐標。 - sy: 需要繪制到目標上下文中的,
image
的矩形(裁剪)選擇框的左上角 Y 軸坐標。 - sWidth: 需要繪制到目標上下文中的,
image
的矩形(裁剪)選擇框的寬度。如果不說明,整個矩形(裁剪)從坐標的sx
和sy
開始,到image
的右下角結束。 - sHeight: 需要繪制到目標上下文中的,
image
的矩形(裁剪)選擇框的高度。 - dx:
image
的左上角在目標canvas上 X 軸坐標。 - dy:
image
的左上角在目標canvas上 Y 軸坐標。 - dWidth:
image
在目標canvas上繪制的寬度。 允許對繪制的image
進行縮放。 如果不說明, 在繪制時image
寬度不會縮放。 - dHeight:
image
在目標canvas上繪制的高度。 允許對繪制的image
進行縮放。 如果不說明, 在繪制時image
高度不會縮放。
四、資源管理器
我們在開發游戲的時候,有一些靜態資源是需要請求回來的,否則如果直接開始,某些靜態資源沒有,會報錯,或者空白,比如我們的游戲被禁錮,如果沒有請求回來就直接開始,頁面會有空白現象。
資源管理器就是當游戲需要資源全部加載完畢的時候,再開始游戲
我們現在主要是圖片的資源,所以我們要在canvas渲染過程中進行圖片的資源加載。
4.1 獲取對象中屬性的長度
有下面一個JSON(對象),此時我們想獲取當前這個JSON屬性數量
this.imgURL = {
'fengjing1':'./image/下載1.jpg',
'fengjing2':'./image/下載2.jpg',
'fengjing3':'./image/下載3.jpg',
'fengjing4':'./image/下載4.jpg',
'fengjing5':'./image/下載5.jpg',
}
此時我們使用this.imgURL.length
是得不到的,因為當前的this.imgURL.length
指的是獲取imgURL對象的length
屬性,而不是獲取當前對象的屬性個數,會返回undefined
正確答案是使用Object.keys()
來獲取當前的屬性key列表,然后通過這個列表獲取長度。
Object.keys(this.imgURL).length
4.2 管理器的實現
interface StringOrImage {
// 定義了一個接口,該接口要求對象的屬性是string或者是HTMLImageElement類型
[index: string]: string | HTMLImageElement
}
class Game {
dom: HTMLCanvasElement
ctx: CanvasRenderingContext2D | null
imgURL: StringOrImage
constructor() {
// 得到畫布
this.dom = document.getElementById("mycanvas") as HTMLCanvasElement
// 獲取上下文
this.ctx = this.dom.getContext("2d")
// 在屬性中保存需要的圖片地址
this.imgURL = {
// 'fengjing1':'./image/下載1.jpg',
// 'fengjing2':'./image/下載2.jpg',
// 'fengjing3':'./image/下載3.jpg',
// 'fengjing4':'./image/下載4.jpg',
// 'fengjing5':'./image/下載5.jpg',
'fengjing1':'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.jj20.com%2Fup%2Fallimg%2F4k%2Fs%2F02%2F2109242332225H9-0-lp.jpg&refer=http%3A%2F%2Fimg.jj20.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1651933471&t=34b40d339ce3bc4177afb393e7785575',
'fengjing2':'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Ffile02.16sucai.com%2Fd%2Ffile%2F2014%2F0827%2Fc0c92bd51bb72e6d12d5b877dce338e8.jpg&refer=http%3A%2F%2Ffile02.16sucai.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1651933483&t=453f28e751e0d54d70a2e3393e57b423',
'fengjing3':'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.jj20.com%2Fup%2Fallimg%2F1113%2F032120114622%2F200321114622-4-1200.jpg&refer=http%3A%2F%2Fimg.jj20.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1651933493&t=e9017fa69deb525312e214d2583a76d4',
'fengjing4':'https://pic.rmb.bdstatic.com/1530971282b420d77bdfb6444d854f952fe31f0d1e.jpeg',
'fengjing5':'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.jj20.com%2Fup%2Fallimg%2Ftp01%2F1ZZQ214233446-0-lp.jpg&refer=http%3A%2F%2Fimg.jj20.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1651933521&t=2cc050574824ec2761b539ab3a697522',
}
// 獲取資源圖片的總數
let imgCount = Object.keys(this.imgURL).length
// 計數器,記錄的是加載完畢的數量
let count = 0
// 遍歷imgURL對象獲取每一個路徑地址
for(let key in this.imgURL) {
// 備份每一張圖片的地址
let src: string = this.imgURL[key] as string
// 創建一個圖片
this.imgURL[key] = new Image();
// 判斷圖片是否加載完成,如果完成了,記數,如果加載完畢的數量和總數量相同了,則說明資源加載完畢
// 第一種方法,將值提取出去做類型縮小
let value = this.imgURL[key]
// 類型縮小成HTMLImageElement類型
if(typeof value !== 'string') {
value.src = src
value.onload = () => {
// 增加計數器
count++
if(this.ctx !== null) {
// 清屏
this.ctx.clearRect(0, 0, 600, 600)
this.ctx.font = '16px Arial'
this.ctx.fillText("圖片已經加載:" + count +" / " + imgCount, 50, 50)
// 判斷圖片是否加載完畢,如果加載完畢了再開始顯示
if(count === imgCount) {
this.start()
}
}
}
}
// 第二種方法,使用as直接斷言成HTMLImageElement
//(this.imgURL[key] as HTMLImageElement).src = src
}
}
start() {
if(this.ctx !== null) {
// 清屏
this.ctx.clearRect(0, 0, 600, 600)
let startX = 0
let startY = 0
for(let key in this.imgURL) {
this.ctx.drawImage(this.imgURL[key] as HTMLImageElement, startX, startY, 100, 100)
startX += 100
startY += 100
}
}
}
}
new Game()
五、變形
canvas是可以進行變形的,但是變形的不是元素,而是ctx,ctx就是整個畫布的渲染區域,整個畫布在變形,我們需要在畫布變形前進行保存和恢復:
save()
:保存畫布(canvas)的所有狀態。restore()
:save 和 restore 方法是用來保存和恢復 canvas 狀態的,都沒有參數。Canvas 的狀態就是當前畫面應用的所有樣式和變形的一個快照。
Canvas狀態存儲在棧中,每當save()
方法被調用后,當前的狀態就被推送到棧中保存。一個繪畫狀態包括:
- 當前應用的變形(即移動,旋轉和縮放,見下)
- 以及下面這些屬性:
strokeStyle
,fillStyle
,globalAlpha
,lineWidth
,lineCap
,lineJoin
,miterLimit
,lineDashOffset
,shadowOffsetX
,shadowOffsetY
,shadowBlur
,shadowColor
,globalCompositeOperation
,font
,textAlign
,textBaseline
,direction
,imageSmoothingEnabled
- 當前的裁切路徑(clipping path),會在下一節介紹
你可以調用任意多次 save
方法。每一次調用 restore
方法,上一個保存的狀態就從棧中彈出,所有設定都恢復。
以下的例子可以很好的印證這兩個的用法:
// 得到畫布
const myCanvas: HTMLCanvasElement = document.getElementById('mycanvas') as HTMLCanvasElement
// 獲得上下文
const ctx = myCanvas.getContext('2d')
if (ctx !== null) {
ctx.fillRect(0, 0, 150, 150); // 使用默認設置繪制一個矩形
ctx.save(); // 保存默認狀態
ctx.fillStyle = '#09F' // 在原有配置基礎上對顏色做改變
ctx.fillRect(15, 15, 120, 120); // 使用新的設置繪制一個矩形
ctx.save(); // 保存當前狀態
ctx.fillStyle = '#FFF' // 再次改變顏色配置
ctx.globalAlpha = 0.5;
ctx.fillRect(30, 30, 90, 90); // 使用新的配置繪制一個矩形
ctx.restore(); // 重新加載之前的顏色狀態
ctx.fillRect(45, 45, 60, 60); // 使用上一次的配置繪制一個矩形
ctx.restore(); // 加載默認顏色配置
ctx.fillRect(60, 60, 30, 30); // 使用加載的配置繪制一個矩形
}
5.1 移動translate
translate(x, y)
: translate
方法接受兩個參數。x 是左右偏移量,y 是上下偏移量。
在做變形之前先保存狀態是一個良好的習慣。大多數情況下,調用 restore 方法比手動恢復原先的狀態要簡單得多。又,如果你是在一個循環中做位移但沒有保存和恢復 canvas 的狀態,很可能到最后會發現怎么有些東西不見了,那是因為它很可能已經超出 canvas 范圍以外了。
我們知道了變形實際上就是將整個畫布進行的變形,所以如果一旦我們的變形操作變多了,畫布將變得不可控。
所以如果我們使用到變形,一定記住下面的規律:變形之前要先備份,將世界和平的狀態進行備份,然后再變形,變形完畢后再恢復到世界和平的樣子,不要影響下一次的操作。
// 得到畫布
const myCanvas: HTMLCanvasElement = document.getElementById('mycanvas') as HTMLCanvasElement
// 獲得上下文
const ctx = myCanvas.getContext('2d')
if (ctx !== null) {
// 保存
ctx.save()
ctx.translate(50, 50)
ctx.fillRect(0, 0, 120, 120)
// 恢復
ctx.restore()
// 渲染位置是沒有存檔之前的位置
ctx.fillRect(120, 300, 120, 120)
}
5.2 旋轉 rotate
rotate(angle)
這個方法只接受一個參數:旋轉的角度(angle),它是順時針方向的,以弧度為單位的值。
旋轉的中心點始終是 canvas 的原點,如果要改變它,我們需要用到 translate
方法。
5.3 縮放 scale
scale(x, y)
: scale
方法可以縮放畫布的水平和垂直的單位。兩個參數都是實數,可以為負數,x 為水平縮放因子,y 為垂直縮放因子,如果比1小,會縮小圖形, 如果比1大會放大圖形。默認值為1, 為實際大小。
畫布初始情況下, 是以左上角坐標為原點的第一象限。如果參數為負實數, 相當於以x 或 y軸作為對稱軸鏡像反轉(例如, 使用translate(0,canvas.height); scale(1,-1);
以y軸作為對稱軸鏡像反轉, 就可得到著名的笛卡爾坐標系,左下角為原點)。
默認情況下,canvas 的 1 個單位為 1 個像素。舉例說,如果我們設置縮放因子是 0.5,1 個單位就變成對應 0.5 個像素,這樣繪制出來的形狀就會是原先的一半。同理,設置為 2.0 時,1 個單位就對應變成了 2 像素,繪制的結果就是圖形放大了 2 倍。
5.4 變形 transform
transform(a, b, c, d, e, f)
a (m11)
: 水平方向的縮放;
b(m12)
: 豎直方向的傾斜偏移;
c(m21)
: 水平方向的傾斜偏移;
d(m22)
: 豎直方向的縮放;
e(dx)
: 水平方向的移動;
f(dy)
: 豎直方向的移動.
// 得到畫布
const myCanvas: HTMLCanvasElement = document.getElementById('mycanvas') as HTMLCanvasElement
// 獲得上下文
const ctx = myCanvas.getContext('2d')
if (ctx !== null) {
// 保存
ctx.save()
ctx.transform(0.5, 0, 1, 0.5, 100, 100)
ctx.fillRect(0, 0, 100,100)
// 恢復
ctx.restore()
// 渲染位置是沒有存檔之前的位置
ctx.fillRect(0, 200, 100, 100)
}
5.5 滾動的車輪案例
- index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>canvas的變形-滾動的車輪</title>
<link rel="stylesheet" href="./css/reset.css" type="text/css">
<link rel="stylesheet" href="./css/index.css" type="text/css">
</head>
<body>
<canvas id="mycanvas"width="1200" height="600" >
當前的瀏覽器版本不支持,請升級瀏覽器
</canvas>
<script src='./dist/canvas.js'></script>
</body>
</html>
- 所需圖片
- canvas.ts
// 得到畫布
const myCanvas: HTMLCanvasElement = document.getElementById('mycanvas') as HTMLCanvasElement
// 獲得上下文
const ctx = myCanvas.getContext('2d')
if (ctx !== null) {
// 第一步是創建一個image元素
const image:HTMLImageElement = new Image()
// 用src設置圖片的地址
image.src = "image/汽車車輪.png"
// 必須要在onload函數內繪制圖片,否則不會渲染
image.onload = () => {
// 定時器
// 旋轉的度數
let deg = 0
// 位置
let x= -100
setInterval(() => {
// 清除畫布
ctx.clearRect(0, 0, myCanvas.width, myCanvas.height)
deg += 0.1
x += 5
if(x >= myCanvas.width - 100) {
x = -100
}
// 備份
ctx.save()
// 平移, 目前我們的原點為(100,300)
ctx.translate(x, 300)
// 旋轉,因為旋轉始終在canvas的原點,所以我們得用translate改變原點。
ctx.rotate(deg)
// 我們得讓車輪的中心處於原點,所以我們需要在第一個和第二個參數各為第三和第四個參數的一半然后再加負號
ctx.drawImage(image, -100, -100, 200, 200)
// 恢復
ctx.restore()
}, 1000/60)
}
}
- 整體架構
- 實現的效果
六、合成與裁剪
合成其實就是我們常見的蒙版狀態,本質就是如何進行壓蓋,如何進行顯示。
在之前我們總是將一個圖形畫在另一個之上,對於其他更多的情況,僅僅這樣是遠遠不夠的。比如,對合成的圖形來說,繪制順序會有限制。不過,我們可以利用 globalCompositeOperation
屬性來改變這種狀況。此外, clip
屬性允許我們隱藏不想看到的部分圖形。
比如我們此時花了一個方和一個圓,第一次畫的是方,第二次畫的是圓,所以會出現圓壓蓋方的現象
// 得到畫布
const myCanvas: HTMLCanvasElement = document.getElementById('mycanvas') as HTMLCanvasElement
// 獲得上下文
const ctx = myCanvas.getContext('2d')
if (ctx !== null) {
ctx.fillStyle = 'skyblue'
ctx.fillRect(100, 100, 100, 100)
ctx.fillStyle = 'deeppink'
ctx.beginPath()
ctx.arc(200, 200, 60, 0, 7,false)
ctx.fill()
}
6.1 globalCompositeOperation
globalCompositeOperation = type
這個屬性設定了在畫新圖形時采用的遮蓋策略,其值是一個標識12種遮蓋方式的字符串。具體情況看MDN。
我們可以通過這個屬性來對上方設置壓蓋順序:
比如說此時我們想讓粉色在下面, 可以使用destination-over:
// 得到畫布
const myCanvas: HTMLCanvasElement = document.getElementById('mycanvas') as HTMLCanvasElement
// 獲得上下文
const ctx = myCanvas.getContext('2d')
if (ctx !== null) {
ctx.fillStyle = 'skyblue'
ctx.fillRect(100, 100, 100, 100)
ctx.globalCompositeOperation= 'destination-over'
ctx.fillStyle = 'deeppink'
ctx.beginPath()
ctx.arc(200, 200, 60, 0, 7,false)
ctx.fill()
}
6.2 裁剪路徑
裁切路徑和普通的 canvas 圖形差不多,不同的是它的作用是遮罩,用來隱藏不需要的部分。如下圖所示。紅邊五角星就是裁切路徑,所有在路徑以外的部分都不會在 canvas 上繪制出來。
如果和上面介紹的 globalCompositeOperation
屬性作一比較,它可以實現與 source-in
和 source-atop
差不多的效果。最重要的區別是裁切路徑不會在 canvas 上繪制東西,而且它永遠不受新圖形的影響。這些特性使得它在特定區域里繪制圖形時相當好用。
clip()
: 將當前正在構建的路徑轉換為當前的裁剪路徑。默認情況下,canvas 有一個與它自身一樣大的裁切路徑(也就是沒有裁切效果)。
6.3 刮刮樂案例
- index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>canvas實現刮刮樂</title>
<link rel="stylesheet" href="./css/reset.css" type="text/css">
<link rel="stylesheet" href="./css/index.css" type="text/css">
</head>
<body>
<div>
特等獎
<canvas width="250" height="60" id="mycanvas">
當前的瀏覽器版本不支持,請升級瀏覽器
</canvas>
</div>
<script src='./dist/canvas.js'></script>
</body>
</html>
- index.css
div {
border: 1px solid #000;
width: 250px;
height: 60px;
font-size: 40px;
line-height: 60px;
text-align: center;
position: relative;
user-select: none;
}
canvas {
position: absolute;
left: 0;
top: 0;
}
- canvas.ts
// 得到畫布
const myCanvas: HTMLCanvasElement = document.getElementById('mycanvas') as HTMLCanvasElement
// 獲得上下文
const ctx = myCanvas.getContext('2d')
if (ctx !== null) {
ctx.fillStyle = '#333'
ctx.fillRect(0, 0, 250, 60)
// 設置新畫上的元素,實際上就是擦除之前的元素
ctx.globalCompositeOperation = 'destination-out'
const func = (event:any) => {
// 畫圖
ctx.beginPath()
ctx.arc(event.offsetX, event.offsetY,10, 0, Math.PI * 2,false)
ctx.fill()
}
// 按下
myCanvas.onmousedown = () => {
// 添加鼠標移動事件
myCanvas.addEventListener('mousemove', func)
}
// 松開
myCanvas.onmouseup = () => {
// 刪除鼠標移動事件
myCanvas.removeEventListener('mousemove', func)
}
}
- 實現效果
七、總結
至此,一個簡單的學習canvas教程已經完結,大家還是多看看文檔吧,希望這個教程能讓大家喜歡上canvas並且好好的利用它!