Effect:
- Lifelike flames follow the mouse and sparks appear to illuminate the background text.
- Drawing with canvas
- javascript is used, but there is no complex logic. Beginning Degree: Simple
- Welcome to my blog to read this article: https://clatterrr.com/archive...
Source code:
- Demonstration address: https://clatterrr.github.io/f...
- The source code is given below, or access to the github code repository https://github.com/clatterrr/...
Learning Notes:
google font
As mentioned in the last article. Beautiful HTML 5 page special effects learning notes (3) guess what the next color is?
Step-by-step detailed explanation of javascript
Step 1:
Very simple initialization function.
var oCanvas; init = function() { oCanvas = new Fire(); oCanvas.run(); } window.onload = init;
Step 2:
Initialize canvas, define various basic things, and add event monitoring to the mouse. The third parameter of addEventListener means that if false, the order of event processing is first-in-advance, and true is second-in-advance. See for details https://www.runoob.com/jsref/...
var Fire = function(){ this.canvas = document.getElementById('fire'); this.ctx = this.canvas.getContext('2d'); this.canvas.height = window.innerHeight; this.canvas.width = window.innerWidth; this.aFires = []; this.aSpark = []; this.aSpark2 = []; this.mouse = { x : this.canvas.width * .5, y : this.canvas.height * .75, } //Once the mouse moves, update this.mouse.x and this.mouse.y, because false is set, so the first-come events are handled first. this.canvas.addEventListener('mousemove', this.updateMouse.bind( this ), false); } Fire.prototype.updateMouse = function( e ){ this.mouse.x = e.clientX; this.mouse.y = e.clientY; }
Step 3:
Use the request Animation Frame to run the run function again for the next frame. Why not use setInterval? Because the intrinsic operating mechanism will make its running speed change with the performance of the randomizer, resulting in inaccurate time control. The request Animation Frame uses system time, guaranteeing 60 executions per second. Reference resources: https://www.cnblogs.com/xiaoh...
A new function is created by using the bind() method. When the bind() is called, this new function is specified by the first parameter of the bind, and the rest of the parameters will be used as parameters of the new function. Reference resources: https://developer.mozilla.org...
Fire.prototype.run = function(){ //Renew Flames and Sparks this.update(); this.draw(); //Steady redrawing requestAnimationFrame( this.run.bind( this ) ); }
Step 4:
Renew a flame (actually a red circle) and two sparks (actually a small rectangle). If the life cycle of a flame or spark is over, delete it. Otherwise, keep updating it.
Note that flame sparks are stored in arrays, so delete slice.
Fire.prototype.update = function(){ //Draw new flames (red circles) and sparks this.aFires.push( new Flame( this.mouse ) ); this.aSpark.push( new Spark( this.mouse ) ); this.aSpark2.push( new Spark( this.mouse ) ); //If the previous element, the new flame (red circle) and the spark's life cycle are not complete, keep updating it or delete it. for (var i = this.aFires.length - 1; i >= 0; i--) { if( this.aFires[i].alive ) this.aFires[i].update(); else this.aFires.splice( i, 1 ); } for (var i = this.aSpark.length - 1; i >= 0; i--) { if( this.aSpark[i].alive ) this.aSpark[i].update(); else this.aSpark.splice( i, 1 ); } for (var i = this.aSpark2.length - 1; i >= 0; i--) { if( this.aSpark2[i].alive ) this.aSpark2[i].update(); else this.aSpark2.splice( i, 1 ); } }
Step 5:
Take the flame as the particle. Some comments are in the code. Firstly, the constructor is used to determine the parameters of the flame. If the flame is updated, its coordinates, life cycle, and color calculation using life cycle are updated. The color here is HSLA color. http://caibaojian.com/css3/va....
The same is true of spark structure parameters and update parameters.
var Flame = function( mouse ){ //Mouse coordinates this.cx = mouse.x; this.cy = mouse.y; //Random generation around the mouse this.x = rand( this.cx - 25, this.cx + 25); this.y = rand( this.cy - 5, this.cy + 5); //Random Variables, Transverse and Longitudinal Axis and Radius Changes this.vy = rand( 1, 3 ); this.vx = rand( -1, 1 ); this.r = rand( 20, 30 ); //life cycle this.life = rand( 3, 6 ); this.alive = true; //Used to draw flame color this.c = { h : Math.floor( rand( 2, 40) ), s : 100, l : rand( 80, 100 ), a : 0, ta : rand( 0.8, 0.9 ) } } Flame.prototype.update = function() { //y coordinate change this.y -= this.vy; this.vy += 0.05; //x coordinate change this.x += this.vx; if( this.x < this.cx ) this.vx += 0.1; else this.vx -= 0.1; //Radius change if( this.r > 0 ) this.r -= 0.1; if( this.r <= 0 ) this.r = 0; //Calculate the life cycle, calculate the flame color according to the life cycle this.life -= 0.15; if( this.life <= 0 ){ this.c.a -= 0.05; if( this.c.a <= 0 ) this.alive = false; }else if( this.life > 0 && this.c.a < this.c.ta ){ this.c.a += .08; } }
Step 6:
canvas's home, drawing background, including black background, text FIRE, and a dark red circle following the flame. This big circle, more like a projection on the wall, needs to be found carefully. canvas Reference https://www.w3school.com.cn/t... It's only by slowly searching for one function and one function that you can make a deep impression.
Note that the soft-light and color-dodge parameters of global Composite Operation seem to be unexplained on domestic websites, but only in English mdn. Reference resources:
https://developer.mozilla.org...
Fire.prototype.draw = function(){ //Drawing Background this.ctx.globalCompositeOperation = "source-over"; this.ctx.fillStyle = "rgba( 15, 5, 2, 1 )"; this.ctx.fillRect( 0, 0, window.innerWidth, window.innerHeight ); //Define Gradient Color Styles this.grd = this.ctx.createRadialGradient( this.mouse.x, this.mouse.y - 200,200,this.mouse.x, this.mouse.y - 100,0 ); this.grd.addColorStop(0,"rgb( 15, 5, 2 )"); this.grd.addColorStop(1,"rgb( 30, 10, 2 )"); //Draw a circle and use the color gradient style. This circle is a big dark red circle. //The circle following the flame is more like a projection on the wall. It needs to be carefully observed. this.ctx.beginPath(); this.ctx.arc( this.mouse.x, this.mouse.y - 100, 200, 0, 2*Math.PI ); this.ctx.fillStyle= this.grd; this.ctx.fill(); //Drawing text Fire this.ctx.font = "15em Amatic SC"; this.ctx.textAlign = "center"; this.ctx.strokeStyle = "rgb(50, 20, 0)"; this.ctx.fillStyle = "rgb(120, 10, 0)"; this.ctx.lineWidth = 2; this.ctx.strokeText("Fire",this.canvas.width/2, this.canvas.height * .72 ); this.ctx.fillText("Fire",this.canvas.width/2, this.canvas.height * .72 ); //Drawing Flames this.ctx.globalCompositeOperation = "overlay"; for (var i = this.aFires.length - 1; i >= 0; i--)this.aFires[i].draw( this.ctx ); //Drawing Particles this.ctx.globalCompositeOperation = "soft-light"; for (var i = this.aSpark.length - 1; i >= 0; i--) if( ( i % 2 ) === 0 ) this.aSpark[i].draw( this.ctx ); //Drawing Particle 2 this.ctx.globalCompositeOperation = "color-dodge"; for (var i = this.aSpark2.length - 1; i >= 0; i--) this.aSpark2[i].draw( this.ctx ); }
The last step is:
The most important but also very simple thing is to really draw flames and sparks. beginpath(),arc(),fillstyle(), and fill() are canvas methods. Look for information slowly. Just notice that the color here is hsla rather than rgba.
Flame.prototype.draw = function( ctx ){ ctx.beginPath(); ctx.arc( this.x, this.y, this.r * 3, 0, 2*Math.PI ); ctx.fillStyle = "hsla( " + this.c.h + ", " + this.c.s + "%, " + this.c.l + "%, " + (this.c.a/20) + ")"; ctx.fill(); ctx.beginPath(); ctx.arc( this.x, this.y, this.r, 0, 2*Math.PI ); ctx.fillStyle = "hsla( " + this.c.h + ", " + this.c.s + "%, " + this.c.l + "%, " + this.c.a + ")"; ctx.fill(); }
Full source code:
<!doctype html> <html> <head> <meta charset="utf-8"> <title>Canvas Realize Flame Following Mouse Animation</title> <style> @import url(https://fonts.googleapis.com/css?family=Amatic+SC); html, body { margin:0; padding:0; height: 100%; } </style> </head> <body> <div style="text-align:center;clear:both;"> </div> <canvas id="fire"></canvas> <script> var Fire = function(){ this.canvas = document.getElementById('fire'); this.ctx = this.canvas.getContext('2d'); this.canvas.height = window.innerHeight; this.canvas.width = window.innerWidth; this.aFires = []; this.aSpark = []; this.aSpark2 = []; this.mouse = { x : this.canvas.width * .5, y : this.canvas.height * .75, } //Once the mouse moves, update this.mouse.x and this.mouse.y, because false is set, so the first-come events are handled first. this.canvas.addEventListener('mousemove', this.updateMouse.bind( this ), false); } Fire.prototype.run = function(){ //Renew Flames and this.update(); this.draw(); //Steady redrawing requestAnimationFrame( this.run.bind(this)); } Fire.prototype.update = function(){ //Draw new flames (red circles) and sparks this.aFires.push( new Flame( this.mouse ) ); this.aSpark.push( new Spark( this.mouse ) ); this.aSpark2.push( new Spark( this.mouse ) ); //If the previous element, the new flame (red circle) and the spark's life cycle are not complete, keep updating it or delete it. for (var i = this.aFires.length - 1; i >= 0; i--) { if( this.aFires[i].alive ) this.aFires[i].update(); else this.aFires.splice( i, 1 ); } for (var i = this.aSpark.length - 1; i >= 0; i--) { if( this.aSpark[i].alive ) this.aSpark[i].update(); else this.aSpark.splice( i, 1 ); } for (var i = this.aSpark2.length - 1; i >= 0; i--) { if( this.aSpark2[i].alive ) this.aSpark2[i].update(); else this.aSpark2.splice( i, 1 ); } } Fire.prototype.draw = function(){ //Drawing Background this.ctx.globalCompositeOperation = "source-over"; this.ctx.fillStyle = "rgba( 15, 5, 2, 1 )"; this.ctx.fillRect( 0, 0, window.innerWidth, window.innerHeight ); //Define Gradient Color Styles this.grd = this.ctx.createRadialGradient( this.mouse.x, this.mouse.y - 200,200,this.mouse.x, this.mouse.y - 100,0 ); this.grd.addColorStop(0,"rgb( 15, 5, 2 )"); this.grd.addColorStop(1,"rgb( 30, 10, 2 )"); //Draw a circle and use the color gradient style. This circle is a big dark red circle. //The circle following the flame is more like a projection on the wall. It needs to be carefully observed. this.ctx.beginPath(); this.ctx.arc( this.mouse.x, this.mouse.y - 100, 200, 0, 2*Math.PI ); this.ctx.fillStyle= this.grd; this.ctx.fill(); //Drawing text Fire this.ctx.font = "15em Amatic SC"; this.ctx.textAlign = "center"; this.ctx.strokeStyle = "rgb(50, 20, 0)"; this.ctx.fillStyle = "rgb(120, 10, 0)"; this.ctx.lineWidth = 2; this.ctx.strokeText("Fire",this.canvas.width/2, this.canvas.height * .72 ); this.ctx.fillText("Fire",this.canvas.width/2, this.canvas.height * .72 ); //Drawing Flames this.ctx.globalCompositeOperation = "overlay"; for (var i = this.aFires.length - 1; i >= 0; i--)this.aFires[i].draw( this.ctx ); //Drawing Particles this.ctx.globalCompositeOperation = "soft-light"; for (var i = this.aSpark.length - 1; i >= 0; i--) if( ( i % 2 ) === 0 ) this.aSpark[i].draw( this.ctx ); //Drawing Particle 2 this.ctx.globalCompositeOperation = "color-dodge"; for (var i = this.aSpark2.length - 1; i >= 0; i--) this.aSpark2[i].draw( this.ctx ); } Fire.prototype.updateMouse = function( e ){ this.mouse.x = e.clientX; this.mouse.y = e.clientY; } var Flame = function( mouse ){ //Mouse coordinates this.cx = mouse.x; this.cy = mouse.y; //Random generation around the mouse this.x = rand( this.cx - 25, this.cx + 25); this.y = rand( this.cy - 5, this.cy + 5); //Random Variables, Transverse and Longitudinal Axis and Radius Changes this.vy = rand( 1, 3 ); this.vx = rand( -1, 1 ); this.r = rand( 20, 30 ); //life cycle this.life = rand( 3, 6 ); this.alive = true; //Used to draw flame color this.c = { h : Math.floor( rand( 2, 40) ), s : 100, l : rand( 80, 100 ), a : 0, ta : rand( 0.8, 0.9 ) } } Flame.prototype.update = function() { //y coordinate change this.y -= this.vy; this.vy += 0.05; //x coordinate change this.x += this.vx; if( this.x < this.cx ) this.vx += 0.1; else this.vx -= 0.1; //Radius change if( this.r > 0 ) this.r -= 0.1; if( this.r <= 0 ) this.r = 0; //Calculate the life cycle, calculate the flame color according to the life cycle this.life -= 0.15; if( this.life <= 0 ){ this.c.a -= 0.05; if( this.c.a <= 0 ) this.alive = false; }else if( this.life > 0 && this.c.a < this.c.ta ){ this.c.a += .08; } } Flame.prototype.draw = function( ctx ){ ctx.beginPath(); ctx.arc( this.x, this.y, this.r * 3, 0, 2*Math.PI ); ctx.fillStyle = "hsla( " + this.c.h + ", " + this.c.s + "%, " + this.c.l + "%, " + (this.c.a/20) + ")"; ctx.fill(); ctx.beginPath(); ctx.arc( this.x, this.y, this.r, 0, 2*Math.PI ); ctx.fillStyle = "hsla( " + this.c.h + ", " + this.c.s + "%, " + this.c.l + "%, " + this.c.a + ")"; ctx.fill(); } var Spark = function( mouse ){ this.cx = mouse.x; this.cy = mouse.y; this.x = rand( this.cx -40, this.cx + 40); this.y = rand( this.cy, this.cy + 5); this.lx = this.x; this.ly = this.y; this.vy = rand( 1, 3 ); this.vx = rand( -4, 4 ); this.r = rand( 0, 1 ); this.life = rand( 4, 5 ); this.alive = true; this.c = { h : Math.floor( rand( 2, 40) ), s : 100, l : rand( 40, 100 ), a : rand( 0.8, 0.9 ) } } Spark.prototype.update = function() { this.lx = this.x; this.ly = this.y; this.y -= this.vy; this.x += this.vx; if( this.x < this.cx ) this.vx += 0.2; else this.vx -= 0.2; this.vy += 0.08; this.life -= 0.1; if( this.life <= 0 ){ this.c.a -= 0.05; if( this.c.a <= 0 ) this.alive = false; } } Spark.prototype.draw = function( ctx ){ ctx.beginPath(); ctx.moveTo( this.lx , this.ly); ctx.lineTo( this.x, this.y); ctx.strokeStyle = "hsla( " + this.c.h + ", " + this.c.s + "%, " + this.c.l + "%, " + (this.c.a / 2) + ")"; ctx.lineWidth = this.r * 2; ctx.lineCap = 'round'; ctx.stroke(); ctx.closePath(); ctx.beginPath(); ctx.moveTo( this.lx , this.ly); ctx.lineTo( this.x, this.y); ctx.strokeStyle = "hsla( " + this.c.h + ", " + this.c.s + "%, " + this.c.l + "%, " + this.c.a + ")"; ctx.lineWidth = this.r; ctx.stroke(); ctx.closePath(); } rand = function( min, max ){ return Math.random() * ( max - min) + min; }; onresize = function () { oCanvas.canvas.width = window.innerWidth; oCanvas.canvas.height = window.innerHeight; }; var oCanvas; init = function() { oCanvas = new Fire(); oCanvas.run(); } window.onload = init; </script> </body> </html>