摘要:前言初學(xué),做了一個(gè)畫板應(yīng)用,地址點(diǎn)這里。本篇為的一些基礎(chǔ)思想和注意事項(xiàng),不是基礎(chǔ)。主要是在于事件上的實(shí)踐經(jīng)驗(yàn)屏兼容屏?xí)褂枚鄠€(gè)物理像素渲染一個(gè)獨(dú)立像素,導(dǎo)致一倍圖在屏幕上模糊,也是這樣,所以我們應(yīng)該把畫布的大小設(shè)為元素大小的或倍。
前言
初學(xué)canvas,做了一個(gè)畫板應(yīng)用,地址點(diǎn)這里 。本篇為canvas的一些基礎(chǔ)思想和注意事項(xiàng),不是基礎(chǔ)api。主要是在于touch事件上的實(shí)踐經(jīng)驗(yàn)
retina屏兼容retina屏?xí)褂枚鄠€(gè)物理像素渲染一個(gè)獨(dú)立像素,導(dǎo)致一倍圖在retina屏幕上模糊,canvas也是這樣,所以我們應(yīng)該把canvas畫布的大小設(shè)為canvas元素大小的2或3倍。元素大小在css中設(shè)置
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坐標(biāo)系轉(zhuǎn)化
把相對(duì)于瀏覽器窗口的坐標(biāo)轉(zhuǎn)化為canvas坐標(biāo),需要注意的是,如果兼容了retina,需要乘上devicePixelRatio。后面所有出現(xiàn)的坐標(biāo),都要通過這個(gè)函數(shù)轉(zhuǎn)化
function windowToCanvas (x, y) { return { x: (x - canvasOffset.left) * RATIO, y: (y - canvasOffset.top) * RATIO } }
不得不提的是,《HTML5 Canvas核心技術(shù)》有一個(gè)相同的函數(shù),但是書上那個(gè)是錯(cuò)的(也有可能我看的那本是假書)
獲取touch點(diǎn)的坐標(biāo)
function getTouchPosition (e) { let touch = e.changedTouches[0] return windowToCanvas(touch.clientX, touch.clientY) }畫布狀態(tài)的儲(chǔ)存和恢復(fù)
進(jìn)行繪圖操作時(shí),我們會(huì)頻繁設(shè)置canvas繪圖環(huán)境的屬性(線寬,顏色等),大多數(shù)情況下我們只是臨時(shí)設(shè)置,比如畫藍(lán)色的線段,又要畫一個(gè)紅色的正方形,為了不影響兩個(gè)繪圖操作,我們需要在每次繪制時(shí),先保存環(huán)境屬性(save),繪圖完畢后恢復(fù)(restore)
ctx.save() ctx.fillStyle = "#333" ctx.strokeStyle = "#666" ctx.restore()繪制表面的儲(chǔ)存與恢復(fù)
主要用于臨時(shí)性的繪圖操作,比如用手指拖出一個(gè)方形時(shí),首先要在touchstart事件里儲(chǔ)存拖動(dòng)開始時(shí)的繪制表面(getImageData),touchmove的事件函數(shù)中,首先要先恢復(fù)touch開始時(shí)的繪圖表面(putImageData),再根據(jù)當(dāng)前的坐標(biāo)值畫出一個(gè)方形,繼續(xù)拖動(dòng)時(shí),剛才畫出的方形會(huì)被事件函數(shù)的恢復(fù)繪圖表面覆蓋掉,在重新繪制一個(gè)方形,所以無論怎么拖動(dòng),我們看到的只是畫了一個(gè)方形,下面是畫板demo中方形工具的類
// 工具基礎(chǔ) 寬度,顏色,是否在繪畫中,是否被選中 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) //在這里儲(chǔ)存繪圖表面 saveImageData(this.firstDot) Object.assign(this.startPosition, loc) ctx.save() // 儲(chǔ)存畫布狀態(tài) ctx.lineWidth = this.width ctx.strokeStyle = this.color } draw (loc) { ctx.putImageData(this.firstDot, 0, 0) //恢復(fù)繪圖表面,并開始繪制方形 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() //恢復(fù)畫布狀態(tài) } 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 }) } }橢圓的繪制方法(均勻壓縮法)
原理是在壓縮過的坐標(biāo)系中繪制一個(gè)圓形,那看起來就是一個(gè)橢圓了。因?yàn)槭峭ㄟ^拖動(dòng)繪制橢圓,所以在我們拖動(dòng)時(shí),必然拖出了一個(gè)方形,那其實(shí)就是以方形的中心為圓心,較長(zhǎng)邊的一半為半徑畫圓,這個(gè)圓要畫在壓縮過的坐標(biāo)系中,壓縮比例就是較窄邊與較長(zhǎng)邊的比,圓心的坐標(biāo)也要根據(jù)壓縮比例做坐標(biāo)變換,圓形工具類代碼如下
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) //恢復(fù)繪圖表面 const rect = { width: loc.x - this.startPosition.x, height: loc.y - this.startPosition.y } // 計(jì)算方形的寬高(帶有正負(fù)值) const rMax = Math.max(Math.abs(rect.width), Math.abs(rect.height)) // 選出較長(zhǎng)邊 rect.x = this.startPosition.x + rect.width / 2 // 計(jì)算壓縮前的圓心坐標(biāo) rect.y = this.startPosition.y + rect.height / 2 rect.scale = { x: Math.abs(rect.width) / rMax, y: Math.abs(rect.height) / rMax } // 計(jì)算壓縮比例 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) //儲(chǔ)存繪圖表面 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 }) } }撤銷操作
上述例子中都有個(gè) saveImageData() 函數(shù),這個(gè)函數(shù)是把當(dāng)前繪圖表面儲(chǔ)存在一個(gè)數(shù)組中,點(diǎn)擊撤銷的時(shí)候用于恢復(fù)上一步的繪圖表面
const lastImageData = [] function saveImageData (data) { (lastImageData.length == 5) && (lastImageData.shift()) // 上限為儲(chǔ)存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() })總結(jié)
有一些看上去高大上的東西,了解了以后就會(huì)發(fā)現(xiàn)很簡(jiǎn)單,有了基礎(chǔ)的模型以后,再去一點(diǎn)一點(diǎn)豐富功能,所以有些時(shí)候不能總是看看看,一定要?jiǎng)邮?,yeah
我的博客即將搬運(yùn)同步至騰訊云+社區(qū),邀請(qǐng)大家一同入駐:https://cloud.tencent.com/dev...
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/82736.html
摘要:,算法就是這樣,那我們基于該算法再對(duì)現(xiàn)有代碼進(jìn)行一次升級(jí)改造設(shè)置線條顏色在原有的基礎(chǔ)上,我們創(chuàng)建了一個(gè)變量用于保存之前事件中鼠標(biāo)經(jīng)過的點(diǎn),根據(jù)該算法可知要繪制二次貝塞爾曲線起碼需要個(gè)點(diǎn)以上,因此我們只有在中的點(diǎn)數(shù)大于時(shí)才開始繪制。 背景概要 相信大家平時(shí)在學(xué)習(xí)canvas 或 項(xiàng)目開發(fā)中使用canvas的時(shí)候應(yīng)該都遇到過這樣的需求:實(shí)現(xiàn)一個(gè)可以書寫的畫板小工具。 嗯,相信這對(duì)canva...
摘要:,算法就是這樣,那我們基于該算法再對(duì)現(xiàn)有代碼進(jìn)行一次升級(jí)改造設(shè)置線條顏色在原有的基礎(chǔ)上,我們創(chuàng)建了一個(gè)變量用于保存之前事件中鼠標(biāo)經(jīng)過的點(diǎn),根據(jù)該算法可知要繪制二次貝塞爾曲線起碼需要個(gè)點(diǎn)以上,因此我們只有在中的點(diǎn)數(shù)大于時(shí)才開始繪制。 背景概要 相信大家平時(shí)在學(xué)習(xí)canvas 或 項(xiàng)目開發(fā)中使用canvas的時(shí)候應(yīng)該都遇到過這樣的需求:實(shí)現(xiàn)一個(gè)可以書寫的畫板小工具。 嗯,相信這對(duì)canva...
摘要:方法可以獲取到上下文二制作畫板畫板功能可以繪制不同顏色和粗細(xì)的線條,畫板上有橡皮擦功能,一鍵清除功能,下載功能。我們可以用來監(jiān)聽三種狀態(tài)。 學(xué)習(xí)制作畫板之前,我們先來了解一下canvas標(biāo)簽 一.canvas標(biāo)簽 1.canvas標(biāo)簽與img標(biāo)簽相似,但是canvas標(biāo)簽是一個(gè)閉合標(biāo)簽,并且沒有src alt屬性2.canvas標(biāo)簽有兩個(gè)屬性,width,height。我們?cè)陧?yè)面上用c...
閱讀 1789·2023-04-25 14:33
閱讀 3389·2021-11-22 15:22
閱讀 2188·2021-09-30 09:48
閱讀 2700·2021-09-14 18:01
閱讀 1750·2019-08-30 15:55
閱讀 3012·2019-08-30 15:53
閱讀 2149·2019-08-30 15:44
閱讀 657·2019-08-30 10:58