最先
妹子都喜好流星,假如她说不喜好,那她一定是一个假妹子。
如今就一起来做一场流星雨,用程序员的野门路浪漫一下。
要画一场流星雨,起首,天然我们要会画一颗流星。
玩过 canvas 的同砚,你画圆画方画线条这么 6,假如说叫你画下面这个玩艺儿,你会不会以为你用的是假 canvas?canvas 没有画一个带尾巴玩艺儿的 api 啊。
画一颗流星
是的,的倒是没这个 api,然则不代表我们画不出来。流星就是一个小石头,然后由于速率过快发生大批的热量动员四周的氛围发光发烧,所以经飞过的处所看起来就像是流星的尾巴,我们先研究一下流星这个图象,悉数流星处于他本身的活动轨迹当中,当前的位置最亮,表面最清晰,而之前划过的处所离当前位置轨迹距离越远就越昏暗越隐约。
上面的剖析结果很症结, canvas 上是每一帧就重绘一次,每一帧之间的时候距离很短。流星经由的处所会愈来愈隐约末了消逝不见,那有没有能够让画布画的图象每过一帧就变隐约一点而不是悉数消灭的方法?假如能够如许,就能够把每一帧用线段画一小段流星的活动轨迹,末了画出流星的结果。
骗纸!你或许会说,这那边像流星了???
别急,让我多画几段给你看看。
什么? 照样不像? 我们把它画小点,这下总该像了把?
上面几幅图我是在 ps 上模仿的,本质上 ps 也是在画布上绘画,我们立时在 canvas 上尝尝。
那,直接代码完成一下。
// 坐标
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
快约上你的妹子看流星雨吧。
…
什么? 你没有妹子?