好看漂亮的html5网页特效学习笔记4canvas实现火焰跟随鼠标

8次阅读

共计 9899 个字符,预计需要花费 25 分钟才能阅读完成。

效果:

  • 逼真的火焰跟随鼠标,还冒出火花,照亮背景文字
  • 使用 canvas 绘制
  • 使用 javascript,但并无复杂逻辑。上手程度:简单
  • 欢迎来我的博客看此文章:https://clatterrr.com/archive…

源码:

  • 演示地址:https://clatterrr.github.io/f…
  • 源码已在下方给出, 或者访问 github 代码仓库 https://github.com/clatterrr/…

学习笔记:

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/…


    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/xiaoh…

并用 bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被 bind 的第一个参数指定,其余的参数将作为新函数的参数供调用时使用。参考:https://developer.mozilla.org…

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/va…。
火花的构造参数和更新参数也是一样的。

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/t…,自己一个函数一个函数慢慢去查才能加深印象。
注意 globalCompositeOperation 的参数 soft-light 和 color-dodge 似乎国内网站都没找到解释,只在英文 mdn 上看到了样例。参考:
https://developer.mozilla.org…

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>

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>

正文完
 0