概述
詳細
一、准備工作
1、如果不需要任何修改的話,直接使用dist文件夾內的文件即可
2、如果需要修改,需要安裝node
3、打包js運行webpack 打包css運行gulp css 使用dist/index.html預覽
4、學習es6與canvas基礎知識,api
二、程序實現
文件結構:
retina屏兼容
retina屏會使用多個物理像素渲染一個獨立像素,導致一倍圖在retina屏幕上模糊,canvas也是這樣,所以我們應該把canvas畫布的大小設為canvas元素大小的2或3倍。元素大小在css中設置
const canvas = selector('#canvas') const ctx = canvas.getContext('2d') const RATIO = 3 const canvasOffset = canvas.getBoundingClientRect() canvas.width = canvasOffset.width * RATIO canvas.height = canvasOffset.height * RATIO
坐標系轉化
把相對於瀏覽器窗口的坐標轉化為canvas坐標,需要注意的是,如果兼容了retina,需要乘上devicePixelRatio。后面所有出現的坐標,都要通過這個函數轉化
function windowToCanvas (x, y) { return { x: (x - canvasOffset.left) * RATIO, y: (y - canvasOffset.top) * RATIO } }
不得不提的是,《HTML5 Canvas核心技術》有一個相同的函數,但是書上那個是錯的(也有可能我看的那本是假書)
獲取touch點的坐標
function getTouchPosition (e) { let touch = e.changedTouches[0] return windowToCanvas(touch.clientX, touch.clientY) }
畫布狀態的儲存和恢復
進行繪圖操作時,我們會頻繁設置canvas繪圖環境的屬性(線寬,顏色等),大多數情況下我們只是臨時設置,比如畫藍色的線段,又要畫一個紅色的正方形,為了不影響兩個繪圖操作,我們需要在每次繪制時,先保存環境屬性(save),繪圖完畢后恢復(restore)
ctx.save() ctx.fillStyle = "#333" ctx.strokeStyle = "#666" ctx.restore()
繪制表面的儲存與恢復
主要用於臨時性的繪圖操作,比如用手指拖出一個方形時,首先要在touchstart事件里儲存拖動開始時的繪制表面(getImageData),touchmove的事件函數中,首先要先恢復touch開始時的繪圖表面(putImageData),再根據當前的坐標值畫出一個方形,繼續拖動時,剛才畫出的方形會被事件函數的恢復繪圖表面覆蓋掉,在重新繪制一個方形,所以無論怎么拖動,我們看到的只是畫了一個方形,下面是畫板demo中方形工具的類
// 工具基礎 寬度,顏色,是否在繪畫中,是否被選中 class Basic { constructor (width = RATIO, color = '#000') { this.width = width this.color = color this.drawing = false this.isSelect = false } } class Rect extends Basic { constructor (width = RATIO, color = '#000') { super(width, color) this.startPosition = { x: 0, y: 0 } this.firstDot = ctx.getImageData(0, 0, canvasWidth, canvasHeight) } begin (loc) { this.firstDot = ctx.getImageData(0, 0, canvasWidth, canvasHeight) //在這里儲存繪圖表面 saveImageData(this.firstDot) Object.assign(this.startPosition, loc) ctx.save() // 儲存畫布狀態 ctx.lineWidth = this.width ctx.strokeStyle = this.color } draw (loc) { ctx.putImageData(this.firstDot, 0, 0) //恢復繪圖表面,並開始繪制方形 const rect = { x: this.startPosition.x <= loc.x ? this.startPosition.x : loc.x, y: this.startPosition.y <= loc.y ? this.startPosition.y : loc.y, width: Math.abs(this.startPosition.x - loc.x), height: Math.abs(this.startPosition.y - loc.y) } ctx.beginPath() ctx.rect(rect.x, rect.y, rect.width, rect.height) ctx.stroke() } end (loc) { ctx.putImageData(this.firstDot, 0, 0) const rect = { x: this.startPosition.x <= loc.x ? this.startPosition.x : loc.x, y: this.startPosition.y <= loc.y ? this.startPosition.y : loc.y, width: Math.abs(this.startPosition.x - loc.x), height: Math.abs(this.startPosition.y - loc.y) } ctx.beginPath() ctx.rect(rect.x, rect.y, rect.width, rect.height) ctx.stroke() ctx.restore() //恢復畫布狀態 } bindEvent () { canvas.addEventListener('touchstart', (e) => { e.preventDefault() if (!this.isSelect) { return false } this.drawing = true let loc = getTouchPosition(e) this.begin(loc) }) canvas.addEventListener('touchmove', (e) => { e.preventDefault() if (!this.isSelect) { return false } if (this.drawing) { let loc = getTouchPosition(e) this.draw(loc) } }) canvas.addEventListener('touchend', (e) => { e.preventDefault() if (!this.isSelect) { return false } let loc = getTouchPosition(e) this.end(loc) this.drawing = false }) } }
橢圓的繪制方法(均勻壓縮法)
原理是在壓縮過的坐標系中繪制一個圓形,那看起來就是一個橢圓了。因為是通過拖動繪制橢圓,所以在我們拖動時,必然拖出了一個方形,那其實就是以方形的中心為圓心,較長邊的一半為半徑畫圓,這個圓要畫在壓縮過的坐標系中,壓縮比例就是較窄邊與較長邊的比,圓心的坐標也要根據壓縮比例做坐標變換,圓形工具類代碼如下
class Round extends Basic{ constructor (width = RATIO, color = '#000') { super(width, color) this.startPosition = { x: 0, y: 0 } this.firstDot = ctx.getImageData(0, 0, canvasWidth, canvasHeight) } drawCalculate (loc) { ctx.save() ctx.lineWidth = this.width ctx.strokeStyle = this.color ctx.putImageData(this.firstDot, 0, 0) //恢復繪圖表面 const rect = { width: loc.x - this.startPosition.x, height: loc.y - this.startPosition.y } // 計算方形的寬高(帶有正負值) const rMax = Math.max(Math.abs(rect.width), Math.abs(rect.height)) // 選出較長邊 rect.x = this.startPosition.x + rect.width / 2 // 計算壓縮前的圓心坐標 rect.y = this.startPosition.y + rect.height / 2 rect.scale = { x: Math.abs(rect.width) / rMax, y: Math.abs(rect.height) / rMax } // 計算壓縮比例 ctx.scale(rect.scale.x, rect.scale.y) ctx.beginPath() ctx.arc(rect.x / rect.scale.x, rect.y / rect.scale.y, rMax / 2, 0, Math.PI * 2) ctx.stroke() ctx.restore() } begin (loc) { this.firstDot = ctx.getImageData(0, 0, canvasWidth, canvasHeight) //儲存繪圖表面 saveImageData(this.firstDot) Object.assign(this.startPosition, loc) } draw (loc) { this.drawCalculate(loc) } end (loc) { this.drawCalculate(loc) } bindEvent () { canvas.addEventListener('touchstart', (e) => { e.preventDefault() if (!this.isSelect) { return false } this.drawing = true let loc = getTouchPosition(e) this.begin(loc) }) canvas.addEventListener('touchmove', (e) => { e.preventDefault() if (!this.isSelect) { return false } if (this.drawing) { let loc = getTouchPosition(e) this.draw(loc) } }) canvas.addEventListener('touchend', (e) => { e.preventDefault() if (!this.isSelect) { return false } let loc = getTouchPosition(e) this.end(loc) this.drawing = false }) } }
撤銷操作
上述例子中都有個 saveImageData() 函數,這個函數是把當前繪圖表面儲存在一個數組中,點擊撤銷的時候用於恢復上一步的繪圖表面
const lastImageData = [] function saveImageData (data) { (lastImageData.length == 5) && (lastImageData.shift()) // 上限為儲存5步,太多了怕掛掉 lastImageData.push(data) } document.getElementById("cancel").addEventListener('click', () => { if(lastImageData.length < 1) return false ctx.putImageData(lastImageData[lastImageData.length - 1], 0, 0) lastImageData.pop() })
三、運行效果
點擊目錄里index.html
四、其他補充
還有一些簡單地工具如線寬選擇,調色板就不敘述了,有問題歡迎評論
注:本文著作權歸作者,由demo大師發表,拒絕轉載,轉載需要作者授權