start
My sister likes meteors. If she says she doesn't like meteors, she must be a fake sister.
Now let's make a meteor shower and romance with the programmer's wild way.
To draw a meteor shower, first of all, naturally we have to draw a meteor.
Students who have played with canvas, you draw circles and square lines like this 6. If you were asked to draw the following thing, would you think you used fake canvas? Canvas didn't draw an api with a tail.
Draw a meteor
Yes, there is no api, but that doesn't mean we can't draw it. Meteor is a small stone, and then because the speed is too fast to generate a large amount of heat to drive the surrounding air to glow and heat, so the flying place looks like the tail of a meteor. Let's first study the image of the meteor, the whole meteor is in its own trajectory, the current position is the brightest, the outline is the clearest, and the previous place is off the current position orbit. The farther the tracks are, the darker and blurred they become.
The above analysis results are critical. On canvas, every frame is redrawn once, and the time interval between each frame is very short. Where meteors pass will become more and more blurred and disappear. Is there a way to blur the canvas image every frame instead of clearing it all? If so, we can draw a small segment of the meteor's trajectory with a line segment in each frame, and finally draw the effect of the meteor.
Cheat paper! You might say, it's like a meteor???
Don't worry. Let me draw you some more paragraphs.
What? Or not? Let's draw it a little bit, so it's always like this?
The pictures above are simulated on ps. Essentially, ps is also painted on canvas. Let's try it on canvas right away.
Well, just code it.
// coordinate 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); } } // meteor class ShootingStar { constructor(init=new Crood, final=new Crood, size=3, speed=200, onDistory=null) { this.init = init; // initial position this.final = final; // Final position this.size = size; // Size this.speed = speed; // Speed: Pixels/s // Total Flight Time 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; // Past time this.prev = this.init.copy(); // Last frame position this.now = this.init.copy(); // current location 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)'; // Clear the canvas with a "translucent" background for each frame ctx.fillRect(0, 0, cvs.width, cvs.height); ctx.restore(); shootingStar.draw(ctx, delta); } })(); tick();
Effect: A meteor
sogoyi look, a lively meteor!!!! Does it feel more realistic?
meteor shower
We add a meteor shower Meteor Shower class to generate more random meteors and make meteor showers.
// coordinate 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); } } // meteor class ShootingStar { constructor(init=new Crood, final=new Crood, size=3, speed=200, onDistory=null) { this.init = init; // initial position this.final = final; // Final position this.size = size; // Size this.speed = speed; // Speed: Pixels/s // Total Flight Time 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; // Past time this.prev = this.init.copy(); // Last frame position this.now = this.init.copy(); // current location 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)'; // Clear the canvas with a "translucent" background for each frame 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();
Effect: Meteor shower
Transparent background
Don't worry, this meteor shower is a bit monotonous. You can see that in the code above, we brushed the canvas once with a transparency of 0.2 black. The background is dark. What if we need a transparent background?
For example, we need to use this night scene picture as the background, and then add our meteors on it, so that we don't need to brush the background every frame. Because we want to make sure that all but meteors are transparent.
Here we are going to use a cold door property, global Composite Operation, global composite operation? Forgive my wild translation.
This property is actually used to define the combined display effect between the drawing after drawing and the drawing before drawing.
He can set these values.
These attributes indicate that there is no need to read them carefully, let alone write them down, and read them directly. api example The operation effect is very clear. In the example, the filling square is drawn first, and then the filling circle is drawn.
Is it clear at a glance?
For us, the original image is all meteors drawn in each frame, and the target image is a translucent black rectangle covering the canvas after the meteors are drawn. And what we want to keep in each frame is that the last frame has a 0.8 transparency meteor, covering the black rectangle of the canvas and we can't display it.
Note that destination-out and destination-in here. In the example, only part of the source image is preserved, which meets the requirement that only meteors are preserved. I don't think the description on w3cschool is correct. I'll summarize it with my own understanding.
destination-in: Only the source image in the intersection area of the source image (rectangle) and the target image (circle) is preserved.
destination-out: The source image of the area after the source image (rectangle) is subtracted from the target image (circle)
The transparency of the target image in the appeal example is 1, and the subtracted part of the source image is completely missing. What we want is that he can partially erase it according to the target transparency. Change the code in the example to see if it supports translucent computing.
It seems that this property supports translucent computing. The overlap between the source image and the target image is preserved in a translucent form. That is to say, if we want to keep a meteor of 0.8 transparency, we can set up the global Composite Operation in this way.
ctx.fillStyle = 'rgba(0,0,0,0.8)' globalCompositeOperation = 'destination-in'; ctx.fillRect(0, 0, cvs.width, cvs.height); // perhaps ctx.fillStyle = 'rgba(0,0,0,0.2)' globalCompositeOperation = 'destination-out'; ctx.fillRect(0, 0, cvs.width, cvs.height);
Final effect
With the global Composite Operations, the final effect is as follows:
github: https://github.com/gnauhca/dailyeffecttest/tree/master/b-meteorshower
Make an appointment with your sister to watch the meteor shower.
...
What? You don't have a sister?