
效果:
- 逼真的火焰跟随鼠标,还冒出火花,照亮背景文字
- 使用canvas绘制
- 使用javascript,但并无复杂逻辑。上手程度:简单
- 欢迎来我的博客看此文章: https://clatterrr.com/archives/1606
源码:
- 演示地址:https://clatterrr.github.io/flamecursor/index.html
- 源码已在下方给出,或者访问github代码仓库https://github.com/clatterrr/flamecursor/settings
学习笔记:
google字体
在上一篇中已讲过。好看漂亮的html5网页特效学习笔记(3)_猜猜下一个颜色是什么?
javascript分步详细解释
第一步:
很简单的初始化函数。
var oCanvas; init = function() { oCanvas = new Fire(); oCanvas.run(); } window.onload = init;
第二步:
初始化canvas,定义各种基础的东西,以及为鼠标添加事件监测。addEventListener第三个参数的意思是,若为false,则为事件处理顺序为先进先处理,为true则为先进后处理。具体请看https://www.runoob.com/jsref/met-element-addeventlistener.html
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, } //一旦鼠标移动,就更新this.mouse.x和this.mouse.y,因为设置了false,所以先进来的事件先处理 this.canvas.addEventListener('mousemove', this.updateMouse.bind( this ), false); } Fire.prototype.updateMouse = function( e ){ this.mouse.x = e.clientX; this.mouse.y = e.clientY; }
第三步:
使用requestAnimationFrame使下一帧重新运行一遍run函数。为什么不用setInterval呢?因为内在运行机制会让它运行速度随机器性能变化而变化,导致时间控制不精确。而requestAnimationFrame使用系统时间,保证每秒执行60次。参考:https://www.cnblogs.com/xiaohuochai/p/5777186.html
并bind()
方法创建一个新的函数,在bind()
被调用时,这个新函数的this
被bind的第一个参数指定,其余的参数将作为新函数的参数供调用时使用。参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
Fire.prototype.run = function(){ //重新新绘制火焰和火花 this.update(); this.draw(); //稳定重绘画面 requestAnimationFrame( this.run.bind( this ) ); }
第四步:
重新绘制一个火焰(实际上是红色的圆)和两个火花(实际上是小长方形)。若有火焰或火花的生命周期完了,那么就用删除它。否则继续更新它。
注意火焰火花原本被存储在数组里,所以删除用slice就好了。
Fire.prototype.update = function(){ //绘制新的火焰(红色的圆)以及火花 this.aFires.push( new Flame( this.mouse ) ); this.aSpark.push( new Spark( this.mouse ) ); this.aSpark2.push( new Spark( this.mouse ) ); //之前元素,即新的火焰(红色的圆)以及火花的生命周期未完的话,就继续更新它,否则就删除它 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 ); } }
第五步:
以火焰为粒子。部分注释在代码中。先使用构造函数确定火焰的各项参数。若火焰被更新,那么就更新它的坐标,生命周期,以及用生命周期计算颜色。这里的颜色使用的是HSLA颜色,参考:http://caibaojian.com/css3/values/color/hsla.htm。
火花的构造参数和更新参数也是一样的。
var Flame = function( mouse ){ //鼠标坐标 this.cx = mouse.x; this.cy = mouse.y; //随机在鼠标周围产生 this.x = rand( this.cx - 25, this.cx + 25); this.y = rand( this.cy - 5, this.cy + 5); //随机变量,横轴纵轴以及半径的变化 this.vy = rand( 1, 3 ); this.vx = rand( -1, 1 ); this.r = rand( 20, 30 ); //生命周期 this.life = rand( 3, 6 ); this.alive = true; //用于绘制火焰颜色 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坐标变化 this.y -= this.vy; this.vy += 0.05; //x坐标变化 this.x += this.vx; if( this.x < this.cx ) this.vx += 0.1; else this.vx -= 0.1; //半径变化 if( this.r > 0 ) this.r -= 0.1; if( this.r <= 0 ) this.r = 0; //计算生命周期,根据生命周期计算火焰颜色 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; } }
第六步:
canvas的主场,绘制背景,包括黑色背景,文字FIRE,以及一个跟随在火焰后的暗红色大圆。这个大圆大圆,更像是墙壁的上的投影,需要仔细一点才能发现。canvas参考https://www.w3school.com.cn/tags/html_ref_canvas.asp,自己一个函数一个函数慢慢去查才能加深印象。
注意globalCompositeOperation的参数soft-light和color-dodge似乎国内网站都没找到解释,只在英文mdn上看到了样例。参考:
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation
Fire.prototype.draw = function(){ //绘制背景 this.ctx.globalCompositeOperation = "source-over"; this.ctx.fillStyle = "rgba( 15, 5, 2, 1 )"; this.ctx.fillRect( 0, 0, window.innerWidth, window.innerHeight ); //定义渐变颜色样式 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 )"); //绘制一个圆形,并使用颜色渐变样式。这个圆是一个暗红色的大圆 //跟随在火焰后的大圆,更像是墙壁的上的投影,需要仔细一点才能发现 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(); //绘制文字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 ); //绘制火焰 this.ctx.globalCompositeOperation = "overlay"; for (var i = this.aFires.length - 1; i >= 0; i--)this.aFires[i].draw( this.ctx ); //绘制粒子 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 ); //绘制粒子2 this.ctx.globalCompositeOperation = "color-dodge"; for (var i = this.aSpark2.length - 1; i >= 0; i--) this.aSpark2[i].draw( this.ctx ); }
最后一步:
最关键但也很简单的地方,就是真正的绘制火焰和火花啦。beginpath(),arc(),fillstyle()和fill()都是canvas的方法。慢慢查找资料吧。只是注意这儿的颜色用的是hsla颜色而不是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(); }
完整源码:
<!doctype html> <html> <head> <meta charset="utf-8"> <title>Canvas实现火焰跟随鼠标动画</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, } //一旦鼠标移动,就更新this.mouse.x和this.mouse.y,因为设置了false,所以先进来的事件先处理 this.canvas.addEventListener('mousemove', this.updateMouse.bind( this ), false); } Fire.prototype.run = function(){ //重新新绘制火焰和 this.update(); this.draw(); //稳定重绘画面 requestAnimationFrame( this.run.bind(this)); } Fire.prototype.update = function(){ //绘制新的火焰(红色的圆)以及火花 this.aFires.push( new Flame( this.mouse ) ); this.aSpark.push( new Spark( this.mouse ) ); this.aSpark2.push( new Spark( this.mouse ) ); //之前元素,即新的火焰(红色的圆)以及火花的生命周期未完的话,就继续更新它,否则就删除它 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(){ //绘制背景 this.ctx.globalCompositeOperation = "source-over"; this.ctx.fillStyle = "rgba( 15, 5, 2, 1 )"; this.ctx.fillRect( 0, 0, window.innerWidth, window.innerHeight ); //定义渐变颜色样式 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 )"); //绘制一个圆形,并使用颜色渐变样式。这个圆是一个暗红色的大圆 //跟随在火焰后的大圆,更像是墙壁的上的投影,需要仔细一点才能发现 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(); //绘制文字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 ); //绘制火焰 this.ctx.globalCompositeOperation = "overlay"; for (var i = this.aFires.length - 1; i >= 0; i--)this.aFires[i].draw( this.ctx ); //绘制粒子 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 ); //绘制粒子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 ){ //鼠标坐标 this.cx = mouse.x; this.cy = mouse.y; //随机在鼠标周围产生 this.x = rand( this.cx - 25, this.cx + 25); this.y = rand( this.cy - 5, this.cy + 5); //随机变量,横轴纵轴以及半径的变化 this.vy = rand( 1, 3 ); this.vx = rand( -1, 1 ); this.r = rand( 20, 30 ); //生命周期 this.life = rand( 3, 6 ); this.alive = true; //用于绘制火焰颜色 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坐标变化 this.y -= this.vy; this.vy += 0.05; //x坐标变化 this.x += this.vx; if( this.x < this.cx ) this.vx += 0.1; else this.vx -= 0.1; //半径变化 if( this.r > 0 ) this.r -= 0.1; if( this.r <= 0 ) this.r = 0; //计算生命周期,根据生命周期计算火焰颜色 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>
学习了。