1. 程式人生 > 其它 >canvas基礎簡單易懂教程(完結,多圖)

canvas基礎簡單易懂教程(完結,多圖)

目錄

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 繪製路徑

繪製路徑的作用是為了設定一個不規則的多邊形狀態

路徑都是閉合的,使用路徑進行繪製的時候需要既定的步驟:

  1. 需要設定路徑的起點

  2. 使用繪製命令畫出路徑

  3. 封閉路徑

  4. 填充或者繪製已經封閉路徑的形狀

// 建立一個路徑
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()

總結我們要繪製一個圖形,要按照順序

  1. 開始路徑ctx.beginPath()
  2. 移動繪製點ctx.moveTo(x, y)
  3. 描述繪製路徑ctx.lineTo(x, y)
  4. 多次描述繪製路徑ctx.lineTo(x, y)
  5. 閉合路徑ctx.closePath()
  6. 描邊ctx.stroke()
  7. 填充ctx.fill()

此時我們發現之前我們在學習繪製矩形的時候使用的是fillRectstrokeRect,但是實際上fillstroke也是具有繪製填充功能的

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的型別,從左到右依次為buttroundsquare

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: shadowOffsetXshadowOffsetY 用來設定陰影在 X 和 Y 軸的延伸距離,它們是不受變換矩陣所影響的。負值表示陰影會往上或左延伸,正值則表示會往下或右延伸,它們預設都為 0
  • shadowOffsetY: shadowOffsetXshadowOffsetY 用來設定陰影在 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的矩形(裁剪)選擇框的寬度。如果不說明,整個矩形(裁剪)從座標的sxsy開始,到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()方法被呼叫後,當前的狀態就被推送到棧中儲存。一個繪畫狀態包括:

你可以呼叫任意多次 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-insource-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並且好好的利用它!