不知不覺就已經好久沒寫過博客了,自從七月正式畢業后,離開了實習了將近九個月的老東家,進了鼠廠后,做的事都是比較傳統的前端活,之前在tpy的時候只管做移動h5的特效以及小游戲,再加上實習所以時間比較充裕,canvas玩的比較多,而現在因為工作都是些傳統前端工作,而且也忙,就基本上沒再寫過canvas相關的效果了。這個博客自己只是想分享一些自己做過的一些好玩的demo,所以正經的那些項目心得、插件什么的就基本上都不會放上來了。
剛好昨天的時候閑了下來,就看了一下以前寫的一些玩意,所以又想折騰下以前很喜歡折騰的粒子了。其實原理差不多,就是變着法子折騰,順便自己也復習一下。這次的demo除了粒子運動之外,還加了鼠標的干涉。所以自己覺得還是有點搞頭,所以就分享一下。
先上個demo 效果:http://whxaxes.github.io/canvas-test/src/Particle-demo/orangutan/index.html ,表示不要再說什么在低版本IE上沒效果之類的,這個是H5啊,同時最好在chrome上看,其他瀏覽器我都沒測,純碎為了好玩而做出來。有興趣的可以把代碼拷回去自己深究。
圖片或文字都可以分解成粒子。原理此前的博客都有說過,不過也再簡單啰嗦一下,就是先將圖片或者文字畫在canvas上,然后通過畫布對象的getImageData獲取到畫布上的所有像素點,也就是imageData對象的data數組,存放着畫布的所有像素的rgba值。
然后再遍歷像素點,獲取到當前像素點的rgba的a值也就是alpha透明度不為0,我直接舍棄了地透明度的,所以我寫的判斷是直接大於125就行了,255為不透明。更具體的原理可查看此前我的這個博文:隨便談談用canvas來實現文字圖片粒子化
獲取到粒子的位置后,就實例化出粒子對象,代碼如下:
ctx.drawImage(img, this.imgx, this.imgy, img.width, img.height); var imgData = ctx.getImageData(this.imgx, this.imgy, img.width, img.height); for (var x = 0; x < img.width; x += particleSize_x) { for (var y = 0; y < img.height; y += particleSize_y) { var i = (y * imgData.width + x) * 4; if (imgData.data[i + 3] >= 125) { var color = "rgba(" + imgData.data[i] + "," + imgData.data[i + 1] + "," + imgData.data[i + 2] + "," + imgData.data[i + 3] + ")"; var x_random = x + Math.random() * 20, vx = -Math.random() * 200 + 400, y_random = img.height/2 - Math.random() * 40 + 20, vy; if (y_random < this.imgy + img.height / 2) { vy = Math.random() * 300; } else { vy = -Math.random() * 300; } particleArray.push(new Particle(x_random + this.imgx,y_random + this.imgy,x + this.imgx,y + this.imgy,vx,vy,color)); particleArray[particleArray.length - 1].drawSelf(); } } }
將實例化的粒子對象扔進一個數組里保存起來。然后執行動畫循環。
particleArray.sort(function (a, b) { return a.ex - b.ex; }); if (!this.isInit) { this.isInit = true; animate(function (tickTime) { if (animateArray.length < particleArray.length) { if (that.end > (particleArray.length - 1)) { that.end = (particleArray.length - 1) } animateArray = animateArray.concat(particleArray.slice(that.start, that.end)) that.start += that.ite; that.end += that.ite; } animateArray.forEach(function (i) { this.update(tickTime); }) }) }
animate方法的回調即為每次畫布逐幀循環時調用的方法,其中animateArray就是真正用於放置於循環舞台的粒子對象,也就是上面demo中看到的從左到右一個一個粒子出現的效果,其實就是從particleArray中取粒子對象,在每一幀中扔幾十個進animateArray中,所以就有了粒子一個一個出來的效果。
animate方法代碼如下:
var tickTime = 16; function animate(tick) { if (typeof tick == "function") { var tickTime = 16; ctx.clearRect(0, 0, canvas.width, canvas.height); tick(tickTime); RAF(function () { animate(tick) }) } }
這個代碼就比較簡單了,設置每一幀之間的時間差,我一般是設成16毫秒,這個就自己看哈,給tick方法傳參循環。
在逐幀循環回調中,觸發每個粒子對象的update,其中粒子的運動函數,繪畫函數全部會由update函數觸發。
下面這個是粒子對象的封裝,其中x,y為粒子的位置,ex,ey為粒子的目標位置,vx,vy為粒子的速度,color為粒子的顏色,particleSize為粒子的大小,stop是粒子是否靜止,maxCheckTimes和checkLength和checkTimes是檢測粒子是否靜止的屬性,因為粒子在運動的時候,位置是無時無刻都在變化,所以是沒有絕對靜止的,所以需要手動檢測是否約等於靜止,然后再給予粒子靜止狀態,當粒子與目標位置的距離小於checkLength,並且在連續10幀的檢測都粒子與距離目標都是小於checkLength,則說明粒子約等於靜止了,將粒子的stop屬性置為true,再接下來的動畫逐幀循環中,對於stop為true的粒子則不進行運動計算:
function Particle(x, y, ex, ey, vx, vy, color) { this.x = x; this.y = y; this.ex = ex; this.ey = ey; this.vx = vx; this.vy = vy; this.a = 1500; this.color = color; this.width = particleSize_x; this.height = particleSize_y; this.stop = false;this.maxCheckTimes = 10; this.checkLength = 5; this.checkTimes = 0; } var oldColor = ""; Particle.prototype = { constructor: Particle, drawSelf: function () { if (oldColor != this.color) { ctx.fillStyle = this.color; oldColor = this.color } ctx.fillRect(this.x - this.width / 2, this.y - this.height / 2, this.width, this.height); }, update: function (tickTime) { if (this.stop) { this.x = this.ex; this.y = this.ey; } else { tickTime = tickTime / 1000; var cx = this.ex - this.x; var cy = this.ey - this.y; var angle = Math.atan(cy / cx); var ax = Math.abs(this.a * Math.cos(angle)); ax = this.x > this.ex ? -ax : ax var ay = Math.abs(this.a * Math.sin(angle)); ay = this.y > this.ey ? -ay : ay; this.vx += ax * tickTime; this.vy += ay * tickTime; this.vx = ~~this.vx * 0.95; this.vy = ~~this.vy * 0.95; this.x += this.vx * tickTime; this.y += this.vy * tickTime; if (Math.abs(this.x - this.ex) <= this.checkLength && Math.abs(this.y - this.ey) <= this.checkLength) { this.checkTimes++; if (this.checkTimes >= this.maxCheckTimes) { this.stop = true; } } else { this.checkTimes = 0 } } this.drawSelf(); this._checkMouse(); }, _checkMouse: function () { if (!mouseX) { if (this.recordX) { this.stop = false; this.checkTimes = 0; this.a = 1500; this.ex = this.recordX; this.ey = this.recordY; this.recordX = null; this.recordY = null; } return; } var distance = Math.sqrt(Math.pow(mouseX - this.x, 2) + Math.pow(mouseY - this.y, 2)); var angle = Math.atan((mouseY - this.y) / (mouseX - this.x)); if (distance < mouseRadius) { this.stop = false; this.checkTimes = 0; if (!this.recordX) { this.recordX = this.ex; this.recordY = this.ey; } this.a = 2000; var xc = Math.abs((mouseRadius - distance) * Math.cos(angle)); var yc = Math.abs((mouseRadius - distance) * Math.sin(angle)); xc = mouseX > this.x ? -xc : xc; yc = mouseY > this.y ? -yc : yc; this.ex = this.x + xc; this.ey = this.y + yc; } else { if (this.recordX) { this.stop = false; this.checkTimes = 0; this.a = 1500; this.ex = this.recordX; this.ey = this.recordY; this.recordX = null; this.recordY = null; } } } };
粒子的方法中,drawself為粒子的繪制自身的方法,畫布的繪制對象的方法的調用次數越少,對整個動畫的性能提升越大。因此,把粒子畫成正方形,因為畫正方形只需調用一個fillRect方法,而如果畫圓形則需要先調用beginPath開始路徑的繪制,再調用arc繪制路徑,最后再通過fill填充顏色。性能方面肯定是畫正方形性能更好,於是直接用fillRect。而也對粒子的color進行緩存,如果連續繪制的多個粒子顏色相同,就不用重復調用fillStyle方法更新畫筆顏色。
然后是update方法,這個方法是粒子運動的核心,但是原理很簡單,就是一些簡單的運動學知識,獲取到粒子與目標點夾角的角度,通過角度將粒子的加速度分解為水平和垂直加速度,再計算出粒子在新的一幀的水平速度和垂直速度,然后再通過新的速度計算出粒子新的位置,最后再繪制出來。update方法底部的if else則是判斷粒子是否靜止的代碼。
粒子的最后一個方法,checkmouse其實就是檢測鼠標位置,如果粒子跟鼠標的距離小於15,則將粒子的目標位置置於與鼠標距離為15的地方,為了保證鼠標移開后粒子還能回到原來的地方,所以用了個recordX和recordY來記錄粒子初始的位置,當鼠標離開粒子時,重置粒子的目標位置。從而讓粒子回到原來的位置。
基本上整個的原理就這樣,代碼有時會有所更新,若要最新源碼,請直接訪問github地址:
https://github.com/whxaxes/canvas-test/tree/gh-pages/src/Particle-demo/orangutan