妹子都喜欢流星,若是她说不喜欢,那她必定是一个假妹子。javascript
如今就一块儿来作一场流星雨,用程序员的野路子浪漫一下。html
要画一场流星雨,首先,天然咱们要会画一颗流星。html5
玩过 canvas 的同窗,你画圆画方画线条这么 6,若是说叫你画下面这个玩意儿,你会不会以为你用的是假 canvas?canvas 没有画一个带尾巴玩意儿的 api 啊。java
是的,的倒是没这个 api,可是不表明咱们画不出来。流星就是一个小石头,而后由于速度过快产生大量的热量带动周围的空气发光发热,因此经飞过的地方看起来就像是流星的尾巴,咱们先研究一下流星这个图像,整个流星处于他本身的运动轨迹之中,当前的位置最亮,轮廓最清晰,而以前划过的地方离当前位置轨迹距离越远就越暗淡越模糊。git
上面的分析结果很关键, canvas 上是每一帧就重绘一次,每一帧之间的时间间隔很短。流星通过的地方会愈来愈模糊最后消失不见,那有没有可让画布画的图像每过一帧就变模糊一点而不是所有清除的办法?若是能够这样,就能够把每一帧用线段画一小段流星的运动轨迹,最后画出流星的效果。程序员
骗纸!你也许会说,这那里像流星了???
别急,让我多画几段给你看看。github
什么? 仍是不像? 咱们把它画小点,这下总该像了把?canvas
上面几幅图我是在 ps 上模拟的,本质上 ps 也是在画布上绘画,咱们立刻在 canvas 上试试。api
那,直接代码实现一下。dom
// 坐标 class Crood { constructor(x=0, y=0) { this.x = x; this.y = y; } setCrood(x, y) { this.x = x; this.y = y; } copy() { return new Crood(this.x, this.y); } } // 流星 class ShootingStar { constructor(init=new Crood, final=new Crood, size=3, speed=200, onDistory=null) { this.init = init; // 初始位置 this.final = final; // 最终位置 this.size = size; // 大小 this.speed = speed; // 速度:像素/s // 飞行总时间 this.dur = Math.sqrt(Math.pow(this.final.x-this.init.x, 2) + Math.pow(this.final.y-this.init.y, 2)) * 1000 / this.speed; this.pass = 0; // 已过去的时间 this.prev = this.init.copy(); // 上一帧位置 this.now = this.init.copy(); // 当前位置 this.onDistory = onDistory; } draw(ctx, delta) { this.pass += delta; this.pass = Math.min(this.pass, this.dur); let percent = this.pass / this.dur; this.now.setCrood( this.init.x + (this.final.x - this.init.x) * percent, this.init.y + (this.final.y - this.init.y) * percent ); // canvas ctx.strokeStyle = '#fff'; ctx.lineCap = 'round'; ctx.lineWidth = this.size; ctx.beginPath(); ctx.moveTo(this.now.x, this.now.y); ctx.lineTo(this.prev.x, this.prev.y); ctx.stroke(); this.prev.setCrood(this.now.x, this.now.y); if (this.pass === this.dur) { this.distory(); } } distory() { this.onDistory && this.onDistory(); } } // effet let cvs = document.querySelector('canvas'); let ctx = cvs.getContext('2d'); let T; let shootingStar = new ShootingStar( new Crood(100, 100), new Crood(400, 400), 3, 200, ()=>{cancelAnimationFrame(T)} ); let tick = (function() { let now = (new Date()).getTime(); let last = now; let delta; return function() { delta = now - last; delta = delta > 500 ? 30 : (delta < 16? 16 : delta); last = now; // console.log(delta); T = requestAnimationFrame(tick); ctx.save(); ctx.fillStyle = 'rgba(0,0,0,0.2)'; // 每一帧用 “半透明” 的背景色清除画布 ctx.fillRect(0, 0, cvs.width, cvs.height); ctx.restore(); shootingStar.draw(ctx, delta); } })(); tick();
sogoyi 快看,一颗活泼不作做的流星!!! 是否是感受动起来更加逼真一点?
咱们再加一个流星雨 MeteorShower 类,生成多一些随机位置的流星,作出流星雨。
// 坐标 class Crood { constructor(x=0, y=0) { this.x = x; this.y = y; } setCrood(x, y) { this.x = x; this.y = y; } copy() { return new Crood(this.x, this.y); } } // 流星 class ShootingStar { constructor(init=new Crood, final=new Crood, size=3, speed=200, onDistory=null) { this.init = init; // 初始位置 this.final = final; // 最终位置 this.size = size; // 大小 this.speed = speed; // 速度:像素/s // 飞行总时间 this.dur = Math.sqrt(Math.pow(this.final.x-this.init.x, 2) + Math.pow(this.final.y-this.init.y, 2)) * 1000 / this.speed; this.pass = 0; // 已过去的时间 this.prev = this.init.copy(); // 上一帧位置 this.now = this.init.copy(); // 当前位置 this.onDistory = onDistory; } draw(ctx, delta) { this.pass += delta; this.pass = Math.min(this.pass, this.dur); let percent = this.pass / this.dur; this.now.setCrood( this.init.x + (this.final.x - this.init.x) * percent, this.init.y + (this.final.y - this.init.y) * percent ); // canvas ctx.strokeStyle = '#fff'; ctx.lineCap = 'round'; ctx.lineWidth = this.size; ctx.beginPath(); ctx.moveTo(this.now.x, this.now.y); ctx.lineTo(this.prev.x, this.prev.y); ctx.stroke(); this.prev.setCrood(this.now.x, this.now.y); if (this.pass === this.dur) { this.distory(); } } distory() { this.onDistory && this.onDistory(); } } class MeteorShower { constructor(cvs, ctx) { this.cvs = cvs; this.ctx = ctx; this.stars = []; this.T; this.stop = false; this.playing = false; } createStar() { let angle = Math.PI / 3; let distance = Math.random() * 400; let init = new Crood(Math.random() * this.cvs.width|0, Math.random() * 100|0); let final = new Crood(init.x + distance * Math.cos(angle), init.y + distance * Math.sin(angle)); let size = Math.random() * 2; let speed = Math.random() * 400 + 100; let star = new ShootingStar( init, final, size, speed, ()=>{this.remove(star)} ); return star; } remove(star) { this.stars = this.stars.filter((s)=>{ return s !== star}); } update(delta) { if (!this.stop && this.stars.length < 20) { this.stars.push(this.createStar()); } this.stars.forEach((star)=>{ star.draw(this.ctx, delta); }); } tick() { if (this.playing) return; this.playing = true; let now = (new Date()).getTime(); let last = now; let delta; let _tick = ()=>{ if (this.stop && this.stars.length === 0) { cancelAnimationFrame(this.T); this.playing = false; return; } delta = now - last; delta = delta > 500 ? 30 : (delta < 16? 16 : delta); last = now; // console.log(delta); this.T = requestAnimationFrame(_tick); ctx.save(); ctx.fillStyle = 'rgba(0,0,0,0.2)'; // 每一帧用 “半透明” 的背景色清除画布 ctx.fillRect(0, 0, cvs.width, cvs.height); ctx.restore(); this.update(delta); } _tick(); } start() { this.stop = false; this.tick(); } stop() { this.stop = true; } } // effet let cvs = document.querySelector('canvas'); let ctx = cvs.getContext('2d'); let meteorShower = new MeteorShower(cvs, ctx); meteorShower.start();
先不急着激动,这个流星雨有点单调,能够看到上面的代码中,每一帧,咱们用了透明度为 0.2 的黑色刷了一遍画布,背景漆黑一片,若是说咱们的需求是透明背景呢?
好比,咱们要用这个夜景图片作背景,而后在上面加上咱们的流星,咱们每一帧刷一层背景的小伎俩就用不了啦。由于咱们要保证除开流星以外的部分,应该是透明的。
这里就要用到一个冷门的属性了,globalCompositeOperation,全局组合操做? 原谅我放荡不羁的翻译。
这个属性其实就是用来定义后绘制的图形与先绘制的图形之间的组合显示效果的。
他能够设置这些值
这些属性说明不必仔细看,更不用记下来,直接看 api 示例 运行效果就很清楚了。示例里,先绘制的是填充正方形,后绘制的是填充圆形。
是否是豁然开朗,一目了然?
对于咱们来讲,原图像是每一帧画完的全部流星,目标图像是画完流星以后半透明覆盖画布的黑色矩形。而咱们每一帧要保留的就是,上一帧 0.8 透明度的流星,覆盖画布黑色矩形咱们不能显示。
注意这里的 destination-out 和 destination-in,示例中这两个属性最终都只有部分源图像保留了下来,符合咱们只要保留流星的需求。我以为 w3cschool 上描述的不是很正确,我用我本身的理解归纳一下。
destination-in :只保留了源图像(矩形)和目标图像(圆)交集区域的源图像
destination-out:只保留了源图像(矩形)减去目标图像(圆)以后区域的源图像
上述示例目标图像的透明度是 1,源图像被减去的部分是彻底不见了。而咱们想要的是他能够按照目标透明度进行部分擦除。改一下示例里的代码看看是否支持半透明的计算。
看来这个属性支持半透明的计算。源图像和目标图像交叠的部分以半透明的形式保留了下来。也就是说若是咱们要保留 0.8 透明度的流星,能够这样设置 globalCompositeOperation
ctx.fillStyle = 'rgba(0,0,0,0.8)' globalCompositeOperation = 'destination-in'; ctx.fillRect(0, 0, cvs.width, cvs.height); // 或者 ctx.fillStyle = 'rgba(0,0,0,0.2)' globalCompositeOperation = 'destination-out'; ctx.fillRect(0, 0, cvs.width, cvs.height);
加上 globalCompositeOperation 以后的效果既最终效果:
github: https://github.com/gnauhca/dailyeffecttest/tree/master/b-meteorshower
快约上你的妹子看流星雨吧。
...
什么? 你没有妹子?