物件導向的canvas畫圖程式
專案簡介
整個專案分為兩大部分
- 場景
場景負責canvas控制,事件監聽,動畫處理 - 精靈
精靈則指的是每一種可以繪製的canvas元素
Demo演示地址
Demo為最新程式碼
專案特點
可擴充套件性強
sprite精靈實現
父類
class Element {
constructor(options = {
fillStyle: `rgba(0,0,0,0)`,
lineWidth: 1,
strokeStyle: `rgba(0,0,0,255)`
}) {
this.options = options
}
setStyle(options){
this.options = Object.assign(this.options. options)
}
}
- 屬性:
-
options中儲存了所有的繪圖屬性
- fillStyle:設定或返回用於填充繪畫的顏色、漸變或模式
- strokeStyle:設定或返回用於筆觸的顏色、漸變或模式
- lineWidth:設定或返回當前的線條寬度
- 使用的都是getContext(“2d”)物件的原生屬性,此處只列出了這三種屬性,需要的話還可以繼續擴充。
- 有需要可以繼續擴充
- 方法:
- setStyle方法用於重新設定當前精靈的屬性
- 有需要可以繼續擴充
所有的精靈都繼承Element類。
子類
子類就是每一種精靈元素的具體實現,這裡我們介紹一遍Circle元素的實現
class Circle extends Element {
// 定位點的座標(這塊就是圓心),半徑,配置物件
constructor(x, y, r = 0, options) {
// 呼叫父類的建構函式
super(options)
this.x = x
this.y = y
this.r = r
}
// 改變元素大小
resize(x, y) {
this.r = Math.sqrt((this.x - x) ** 2 + (this.y - y) ** 2)
}
// 移動元素到新位置,接收兩個引數,新的元素位置
moveTo(x, y) {
this.x = x
this.y = y
}
// 判斷點是否在元素中,接收兩個引數,點的座標
choose(x, y) {
return ((x - this.x) ** 2 + (y - this.y) ** 2) < (this.r ** 2)
}
// 偏移,計算點和元素定位點的相對偏移量(ofsetX, offsetY)
getOffset(x, y) {
return {
x: x - this.x,
y: y - this.y
}
}
// 繪製元素實現,接收一個ctx物件,將當前元素繪製到指定畫布上
draw(ctx) {
// 取到繪製所需屬性
let {
fillStyle,
strokeStyle,
lineWidth
} = this.options
// 開始繪製beginPath() 方法開始一條路徑,或重置當前的路徑
ctx.beginPath()
// 設定屬性
ctx.fillStyle = fillStyle
ctx.strokeStyle = strokeStyle
ctx.lineWidth = lineWidth
// 畫圓
ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI)
// 填充顏色
ctx.stroke()
ctx.fill()
// 繪製完成
}
// 驗證函式,判斷當前元素是否滿足指定條件,此處用來檢驗是否將元素新增到場景中。
validate() {
return this.r >= 3
}
}
arc() 方法建立弧/曲線(用於建立圓或部分圓)
- x 圓的中心的 x 座標。
- y 圓的中心的 y 座標。
- r 圓的半徑。
- sAngle 起始角,以弧度計。(弧的圓形的三點鐘位置是 0 度)。
- eAngle 結束角,以弧度計。
- counterclockwise 可選。規定應該逆時針還是順時針繪圖。False = 順時針,true = 逆時針。
注意事項:
- 建構函式的形參只有兩個是必須的,就是定位點的座標。
- 其它的形參都必須有預設值。
所有方法的呼叫時機
- 我們在畫布上繪製元素的時候回撥用resize方法。
- 移動元素的時候呼叫moveTo方法。
- choose會在滑鼠按下時呼叫,判斷當前元素是否被選中。
- getOffset選中元素時呼叫,判斷選中位置。
- draw繪製函式,繪製元素到場景上時呼叫。
scene場景的實現
- 屬性介紹
class Sence {
constructor(id, options = {
width: 600,
height: 400
}) {
// 畫布屬性
this.canvas = document.querySelector(`#` + id)
this.canvas.width = options.width
this.canvas.height = options.height
this.width = options.width
this.height = options.height
// 繪圖的物件
this.ctx = this.canvas.getContext(`2d`)
// 離屏canvas
this.outCanvas = document.createElement(`canvas`)
this.outCanvas.width = this.width
this.outCanvas.height = this.height
this.outCtx = this.outCanvas.getContext(`2d`)
// 畫布狀態
this.stateList = {
drawing: `drawing`,
moving: `moving`
}
this.state = this.stateList.drawing
// 滑鼠狀態
this.mouseState = {
// 記錄滑鼠按下時的偏移量
offsetX: 0,
offsetY: 0,
down: false, //記錄滑鼠當前狀態是否按下
target: null //當前操作的目標元素
}
// 當前選中的精靈構造器
this.currentSpriteConstructor = null
// 儲存精靈
let sprites = []
this.sprites = sprites
/* .... */
}
}
- 事件邏輯
class Sence {
constructor(id, options = {
width: 600,
height: 400
}) {
/* ... */
// 監聽事件
this.canvas.addEventListener(`contextmenu`, (e) => {
console.log(e)
})
// 滑鼠按下時的處理邏輯
this.canvas.addEventListener(`mousedown`, (e) => {
// 只有左鍵按下時才會處理滑鼠事件
if (e.button === 0) {
// 滑鼠的位置
let x = e.offsetX
let y = e.offsetY
// 記錄滑鼠是否按下
this.mouseState.down = true
// 建立一個臨時target
// 記錄目標元素
let target = null
if (this.state === this.stateList.drawing) {
// 判斷當前有沒有精靈構造器,有的話就構造一個對應的精靈元素
if (this.currentSpriteConstructor) {
target = new this.currentSpriteConstructor(x, y)
}
} else if (this.state === this.stateList.moving) {
let sprites = this.sprites
// 遍歷所有的精靈,呼叫他們的choose方法,判斷有沒有被選中
for (let i = sprites.length - 1; i >= 0; i--) {
if (sprites[i].choose(x, y)) {
target = sprites[i]
break;
}
}
// 如果選中的話就呼叫target的getOffset方法,獲取偏移量
if (target) {
let offset = target.getOffset(x, y)
this.mouseState.offsetX = offset.x
this.mouseState.offsetY = offset.y
}
}
// 儲存當前目標元素
this.mouseState.target = target
// 在離屏canvas儲存除目標元素外的所有元素
let ctx = this.outCtx
// 清空離屏canvas
ctx.clearRect(0, 0, this.width, this.height)
// 將目標元素外的所有的元素繪製到離屏canvas中
this.sprites.forEach(item => {
if (item !== target) {
item.draw(ctx)
}
})
if(target){
// 開始動畫
this.anmite()
}
}
})
this.canvas.addEventListener(`mousemove`, (e) => {
// 如果滑鼠按下且有目標元素,才執行下面的程式碼
if (this.mouseState.down && this.mouseState.target) {
let x = e.offsetX
let y = e.offsetY
if (this.state === this.stateList.drawing) {
// 呼叫當前target的resize方法,改變大小
this.mouseState.target.resize(x, y)
} else if (this.state === this.stateList.moving) {
// 取到儲存的偏移量
let {
offsetX, offsetY
} = this.mouseState
// 呼叫moveTo方法將target移動到新的位置
this.mouseState.target.moveTo(x - offsetX, y - offsetY)
}
}
})
document.body.addEventListener(`mouseup`, (e) => {
if (this.mouseState.down) {
// 將滑鼠按下狀態記錄為false
this.mouseState.down = false
if (this.state === this.stateList.drawing) {
// 呼叫target的validate方法。判斷他要不要被加到場景去呢
if (this.mouseState.target.validate()) {
this.sprites.push(this.mouseState.target)
}
} else if (this.state === this.stateList.moving) {
// 什麼都不做
}
}
})
}
}
- 方法介紹
class Sence {
// 動畫
anmite() {
requestAnimationFrame(() => {
// 清除畫布
this.clear()
// 將離屏canvas繪製到當前canvas上
this.paint(this.outCanvas)
// 繪製target
this.mouseState.target.draw(this.ctx)
// 滑鼠是按下狀態就繼續執行下一幀動畫
if (this.mouseState.down) {
this.anmite()
}
})
}
// 可以將手動的建立的精靈新增到畫布中
append(sprite) {
this.sprites.push(sprite)
sprite.draw(this.ctx)
}
// 根據ID值,從場景中刪除對應元素
remove(id) {
this.sprites.splice(id, 1)
}
// clearRect清除指定區域的畫布內容
clear() {
this.ctx.clearRect(0, 0, this.width, this.height)
}
// 重繪整個畫布的內容
reset() {
this.clear()
this.sprites.forEach(element => {
element.draw(this.ctx)
})
}
// 將離屏canvas繪製到頁面的canvas畫布上
paint(canvas, x = 0, y = 0) {
this.ctx.drawImage(canvas, x, y, this.width, this.height)
}
// 設定當前選中的精靈構造器
setCurrentSprite(Element) {
this.currentSpriteConstructor = Element
}
}