JavaScript+Canvas模擬實(shí)現(xiàn)支付寶畫(huà)年兔游戲
接近過(guò)年了,支付寶的集福的活動(dòng)又開(kāi)始了,集美們的五福集齊了沒(méi)有。每年的集?;顒?dòng)都有一些小游戲,今年也不例外,畫(huà)年畫(huà)就是其中之一,本篇用canvas來(lái)寫(xiě)一個(gè)畫(huà)年兔的游戲。
動(dòng)手前的思路
畫(huà)年畫(huà)游戲規(guī)則是:跟著特定輪廓畫(huà)出線(xiàn)條來(lái)。
思考1、如何讓鼠標(biāo)只能在特定區(qū)域內(nèi)畫(huà)畫(huà)?
首先要獲取到這個(gè)輪廓區(qū)域所在畫(huà)布上的位置,判斷鼠標(biāo)繪畫(huà)的位置是否在指定范圍內(nèi)。用canvas的getImageData函數(shù)能夠獲取到畫(huà)布上有顏色的像素點(diǎn),然后根據(jù)像素點(diǎn)分布計(jì)算出像素點(diǎn)所在位置。
getImageData函數(shù)的用法在我之前的文章canvas文字粒子特效中有詳細(xì)介紹,不懂的可以去看看。
getImageData(canvas, ctx) { const data = ctx.getImageData( 0, 0, canvas.width, canvas.height, ).data; const gap = 4; const points = []; const length = data.length; for (let i = 0, wl = canvas.width * gap; i < length; i += gap) { if (data[i + gap - 1] == 255) { // 根據(jù)透明度判斷 const x = (i % wl) / gap; const y = Math.ceil(i / wl); points.push([x, y]); } } return points; }
思考2、如何讓繪制的圖畫(huà)動(dòng)起來(lái)
通過(guò)定時(shí)旋轉(zhuǎn)畫(huà)布實(shí)現(xiàn)。我選擇用幀動(dòng)畫(huà)requestAnimationFrame函數(shù),比setInterval函數(shù)性能更好一點(diǎn)。
思考3、如何撤銷(xiāo)上一步操作
將每一步繪制的點(diǎn)都記錄到創(chuàng)建的棧中,每一次撤銷(xiāo)都把上一步的繪制點(diǎn)刪除。
思考4、如何判斷線(xiàn)條繪制完畢
想了很久沒(méi)有什么太好的辦法,如果你有想法可以分享給我。當(dāng)mouseup事件執(zhí)行,會(huì)判斷當(dāng)前步驟下繪制的點(diǎn)數(shù)是否>=30,如果滿(mǎn)足條件會(huì)延遲半秒執(zhí)行下一步繪制,當(dāng)mousedown在半秒內(nèi)觸發(fā),延遲函數(shù)會(huì)取消,等待下一個(gè)mouseup事件。
// mouseup事件 const length = _this.execStack.reduce((prev, next) => { if (next.step == _this.curStep) { prev += next.points.length; } return prev; }, 0) if (length >= 30) { _this.timer = setTimeout(() => { if (_this.curStep == 2) { _this.curStep = 2.5 _this.canEdit = false; _this.animate('ears_1', 3); } else if (_this.curStep == 4) { _this.curStep = 4.5 _this.canEdit = false; _this.animate('shake_head', 5); } else if (_this.curStep == 6) { _this.curStep = 6.5 _this.canEdit = false; _this.animate('shake_body', 7); } else if (_this.curStep <= 6) { _this.canEdit = true; _this.curStep += 1; } _this.execCanvas(); }, 500); }
關(guān)鍵步驟
1、創(chuàng)建一個(gè)RabbitPainting類(lèi),初始化時(shí)監(jiān)聽(tīng)canvas的鼠標(biāo)點(diǎn)擊事件。要注意的是移動(dòng)端和pc端監(jiān)聽(tīng)的事件不同:
this.pcEvents = ['mousedown', 'mousemove', 'mouseup']; this.mobileEvents = ['touchstart', 'touchmove', 'touchend'];
我的代碼里對(duì)兼容的處理比較粗糙,只是將大致功能做出來(lái)了,所以大家多看看思路。
鼠標(biāo)移開(kāi)之后,需要解除事件監(jiān)聽(tīng),當(dāng)鼠標(biāo)重新按壓時(shí)再綁定事件。mousedown事件監(jiān)聽(tīng)流程如下:
鼠標(biāo)移動(dòng)時(shí),會(huì)得到兩個(gè)點(diǎn),鼠標(biāo)按壓位置和鼠標(biāo)移動(dòng)位置,如果繪制的線(xiàn)都是從按壓點(diǎn)到移動(dòng)點(diǎn)的話(huà),就會(huì)畫(huà)出:
上圖所示,紅色的線(xiàn)是鼠標(biāo)移動(dòng)路徑,黑色的線(xiàn)是canvas畫(huà)出的線(xiàn)條,所以mousemove函數(shù)執(zhí)行后要更新初始按壓點(diǎn),使前后兩個(gè)點(diǎn)銜接在一起。
線(xiàn)條繪制函數(shù)如下:
drawLine(point) { const { ctx } = this ctx.beginPath() ctx.moveTo(point.startX, point.startY); ctx.lineTo(point.endX, point.endY); if (point.style) { for (let key in point.style) { ctx[key] = point.style[key] } } ctx.stroke(); ctx.closePath() }
2、兔子輪廓繪制,采用貝塞爾2階函數(shù)繪制圖形
// 外輪廓樣式 const wrapperStyle = { lineWidth: "30", strokeStyle: this.tipPathColor[0] } // 內(nèi)虛線(xiàn)樣式 const innerStyle = { lineWidth: "3", strokeStyle: this.tipPathColor[1], lineDash: [15, 12] }
drawCurve({ list, wrapperStyle, innerStyle }) { const { tempCtx: ctx } = this list.forEach(point => { const { x, y, list } = point; ctx.beginPath(); ctx.moveTo(x, y); ctx.bezierCurveTo(...list); for (let key in wrapperStyle) { ctx[key] = wrapperStyle[key] } ctx.stroke(); ctx.save() ctx.beginPath(); ctx.moveTo(x, y); ctx.bezierCurveTo(...list); for (let key in innerStyle) { if (key == 'lineDash') { ctx.setLineDash(innerStyle[key]); } ctx[key] = innerStyle[key] } ctx.stroke(); ctx.restore(); }) }
貝塞爾曲線(xiàn)的關(guān)鍵在于設(shè)置p1和p2兩個(gè)控制點(diǎn),大家自行把握。
我的兔子輪廓總體是這樣的:
3、旋轉(zhuǎn)畫(huà)布功能
使用canvas的rotate函數(shù),畫(huà)布的默認(rèn)中心點(diǎn)是(0,0),所以旋轉(zhuǎn)時(shí)需要用translate(x,y)函數(shù)將中心點(diǎn)移動(dòng)到特定位置。要注意旋轉(zhuǎn)后將畫(huà)布的中心點(diǎn)還原到(0,0)。
const rotateCanvas = (centerPoints, item) => { ctx.save() ctx.translate(...centerPoints) ctx.rotate(Math.PI / 180 * item.curDeg) ctx.translate(-centerPoints[0], -centerPoints[1]) }
注意ctx.save(),用來(lái)記錄畫(huà)布旋轉(zhuǎn)之前的狀態(tài),繪制結(jié)束后需要用ctx.restore()將畫(huà)布狀態(tài)還原,否則定時(shí)函數(shù)執(zhí)行角度旋轉(zhuǎn)時(shí)角度會(huì)累加。
4、眨眼睛動(dòng)畫(huà)
眨眼睛是用一張精靈圖,因?yàn)閳D片是我自己畫(huà)的,只有六幀,所以動(dòng)畫(huà)看起來(lái)不是很好,將就看著吧。
以上就是JavaScript+Canvas模擬實(shí)現(xiàn)支付寶畫(huà)年兔游戲的詳細(xì)內(nèi)容,更多關(guān)于JavaScript Canvas畫(huà)年兔游戲的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
微信小程序?qū)崿F(xiàn)菜單左右聯(lián)動(dòng)
這篇文章主要為大家詳細(xì)介紹了微信小程序?qū)崿F(xiàn)菜單左右聯(lián)動(dòng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-05-05javascript函數(shù)特點(diǎn)實(shí)例分析
這篇文章主要介紹了javascript函數(shù)特點(diǎn),實(shí)例分析了javascript函數(shù)傳遞參數(shù)及調(diào)用方法,需要的朋友可以參考下2015-05-05JavaScript移除數(shù)組元素減少長(zhǎng)度的方法
數(shù)組想必大家對(duì)它并不陌生吧,有些新手朋友們都不知道如何移除數(shù)組元素,下面為大家介紹個(gè)示例,喜歡的朋友可以了解下2013-09-09javascript+css實(shí)現(xiàn)單擊顏色褪去效果
javascript+css實(shí)現(xiàn)單擊顏色褪去效果...2007-08-08Bootstrap實(shí)現(xiàn)input控件失去焦點(diǎn)時(shí)驗(yàn)證
這篇文章主要介紹了Bootstrap實(shí)現(xiàn)input控件失去焦點(diǎn)時(shí)驗(yàn)證的相關(guān)資料,非常不錯(cuò),需要的朋友可以參考下,需要的朋友可以參考下2016-08-08JS使用正則表達(dá)式實(shí)現(xiàn)常用的表單驗(yàn)證功能分析
這篇文章主要介紹了JS使用正則表達(dá)式實(shí)現(xiàn)常用的表單驗(yàn)證功能,結(jié)合實(shí)例形式分析了JS基于正則表達(dá)式的表單驗(yàn)證功能原理、實(shí)現(xiàn)技巧與操作注意事項(xiàng),需要的朋友可以參考下2020-04-04