利用HTML canvas制作酷炫星星坠地粒子特效

去年在电影院看过的电影,印象最深刻的,算是电影《你的名字》了,而且被其中的画面深深吸引了,尤其是陨石划过天空的场景,太美啦!所以想着哪天做一个canvas的流星效果。最近刚好看到油管上的一个视频,作者的主页就是陨石坠落的粒子效果为背景,虽然没有《你的名字》中那么写实,但也是很漂亮了,效果大概长这样,附上链接https://codepen.io/christopher4lis/pen/PzONKR

在这个基础上,我做了一些修改,将圆形粒子换成五角星,背景星空无限右移,且随机产生流星。

下面我把制作过程详细讲解一下。

初始设置

生成canvas标签,设置canvas长宽

const canvas = document.createElement(‘canvas‘);
document.body.appendChild(canvas);
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

因为要将canvas设置成背景,所以要绝对定位canvas

const canvas = document.createElement(‘canvas‘);
canvas.style.position = ‘absolute‘;
canvas.style.top = 0;
canvas.style.left = 0;
canvas.style.zIndex = -1;
document.body.appendChild(canvas);
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const c = canvas.getContext(‘2d‘);

先画个渐变色作为背景

//createLinearGradient的4个参数:startX,startY,EndX,EndY
const backgroundGradient = c.createLinearGradient(0, 0, 0, canvas.height);
backgroundGradient.addColorStop(0, ‘rgba(23, 30, 38, 0.7)‘);
backgroundGradient.addColorStop(1, ‘rgba(63, 88, 107, 0.7)‘);
c.fillStyle = backgroundGradient;
c.fillRect(0, 0, canvas.width, canvas.height);

看下效果

接下去画几座山,用三角形来模拟

画山要有几个设置参数:1.位置 2.颜色 3.画几个

从简单开始,我们将山的坐标点设在三角形的最左边那个角,先画三角形的底边,然后画上顶角

c.beginPath();
c.moveTo(0, canvas.height / 2);
c.lineTo(300, canvas.height / 2);
c.lineTo(150, canvas.height / 4);
c.closePath();
c.fill();

接下去,我们想画连起来的山

显然这山画得...我们要想有那种峰峦叠嶂的感觉,那就必须把山脚和山脚之间重叠起来,像这样

That‘s it! 下面我们来分析一下如何用一个通用的方法来画出这样的峰峦叠嶂

首先,定义一个函数drawMountains(),要传入的参数:1、山的数量number,2、山的坐标y(坐标x直接根据山的数量计算出来,3、山的高度height,4、山的颜色color),5、重叠偏移量offset,好了就这些,让我们来实现它,go

这里来说一下x坐标如何计算,我们假设绘制出的山横跨整个canvas,那么每座山的宽度就是canvas.width / number了,如果都按这个x坐标来绘制,结果就是画出没重叠的山。那么如何解决重叠问题呢?我们可以在绘制底边时给起点和终点加一个偏移量:在起点加一个负的偏移量,在终点加一个相同大小的正的偏移量

function drawMountains(number, y, height, color, offset) {
  c.save();
  c.fillStyle = color;
  const width = canvas.width / number;
  // 循环绘制
  for (let i = 0; i < number; i++) {
    c.beginPath();
    c.moveTo(width * i - offset, y);
    c.lineTo(width * i + width + offset, y);
    c.lineTo(width * i + width / 2, y - height);
    c.closePath();
    c.fill();
  }
  c.restore();
}
drawMountains(3, canvas.height / 2, 200, ‘#384551‘, 100);

看下画成什么样了

Bingo!成功了。接着画剩下的山

drawMountains(1, canvas.height, canvas.height * 0.78, ‘#384551‘, 300);
drawMountains(2, canvas.height, canvas.height * 0.64, ‘#2B3843‘, 400);
drawMountains(3, canvas.height, canvas.height * 0.42, ‘#26333E‘, 150);

绘制背景星空

我们先从简单的开始,就只是在背景上绘制星空。这里的思路是:定义一个星空星星的类Skystar,然后通过循环创建出一堆的skystar,这些skystar实例拥有各自独立的位置和半径大小,最后将这些skystar一个个画到canvas上。Let‘s code!

定义Skystar及一些实例方法

function Skystar() {
  this.x = Math.random() * canvas.width;
  this.y = Math.random() * canvas.height;
  this.color = ‘#ccc‘;
  this.shadowColor = ‘#E3EAEF‘;
  this.radius = Math.random() * 3;
}

Skystar.prototype.draw = function() {
  c.save();
  c.beginPath();
  c.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
  c.shadowColor = this.shadowColor;
  c.shadowBlur = Math.random() * 10 + 10;
  c.shadowOffsetX = 0;
  c.shadowOffsetY = 0;
  c.fillStyle = this.color;
  c.fill();
  c.closePath();
  c.restore();
};

Skystar.prototype.update = function() {
  this.draw();
};

接着定义一个全局数组skystarsArray,通过循环将多个skystar实例存入数组

const skyStarsArray = []; // 星空星星数组
const skyStarsCount = 400; // 星空初始生成星星数量
function drawSkyStars() {
  for (let i = 0; i < skyStarsCount; i++) {
      skyStarsArray.push(new Skystar());
  }
}

drawSkyStars();
skyStarsArray.forEach(skyStar => skyStar.update());

这样就画好静止不动的背景星星。

接着我们来思考下如何让星星动起来。这里定义一个animation函数,每一帧的绘制都在这个函数中进行,最后用requestAnimationFrame来重复调用animation函数,达到动画的效果。

背景星星要动起来,就要在每一帧根据星星的位置对星星进行重新绘制。每个星星的实例中,都定义了一个update方法,用来对星星的一些属性进行更新,所以我们现在修改update方法

const skyStarsVelocity = 0.1; // 星空平移速度
Skystar.prototype.update = function() {
  this.draw();
  // 星空一直连续不断向右移
  this.x += skyStarsVelocity;
};

然后在animation中将之前画山和画星空的方法加进去

function animation() {
  requestAnimationFrame(animation);
  // 画背景
  c.fillStyle = backgroundGradient;
  c.fillRect(0, 0, canvas.width, canvas.height);
  // 画星星
  skyStarsArray.forEach(skyStar => skyStar.update());
  // 画山
  drawMountains(1, canvas.height, canvas.height * 0.78, ‘#384551‘, 300);
  drawMountains(2, canvas.height, canvas.height * 0.64, ‘#2B3843‘, 400);
  drawMountains(3, canvas.height, canvas.height * 0.42, ‘#26333E‘, 150);
}

在此之前,还有一些初始化的工作,这里定义一个init函数,专门用来初始化一些数据

function init() {
  drawSkyStars();    // 初始化背景星星
}

完整代码:

const canvas = document.createElement(‘canvas‘);
canvas.style.position = ‘absolute‘;
canvas.style.top = 0;
canvas.style.left = 0;
canvas.style.zIndex = -1;
document.body.appendChild(canvas);
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const c = canvas.getContext(‘2d‘);
const skyStarsArray = []; // 星空星星数组
const skyStarsCount = 400; // 星空初始生成星星数量
const skyStarsVelocity = 0.1; // 星空平移速度
const backgroundGradient = c.createLinearGradient(0, 0, 0, canvas.height);    //4个参数:startX,startY,EndX,EndY
backgroundGradient.addColorStop(0, ‘rgba(23, 30, 38, 0.7)‘);
backgroundGradient.addColorStop(1, ‘rgba(63, 88, 107, 0.7)‘);

function init() {
  drawSkyStars();    // 初始化背景星星
}

// 画山
function drawMountains(number, y, height, color, offset) {
  c.save();
  c.fillStyle = color;
  const width = canvas.width / number;
  // 循环绘制
  for (let i = 0; i < number; i++) {
    c.beginPath();
    c.moveTo(width * i - offset, y);
    c.lineTo(width * i + width + offset, y);
    c.lineTo(width * i + width / 2, y - height);
    c.closePath();
    c.fill();
  }
  c.restore();
}

function Skystar() {
  this.x = Math.random() * canvas.width;
  this.y = Math.random() * canvas.height;
  this.color = ‘#ccc‘;
  this.shadowColor = ‘#E3EAEF‘;
  this.radius = Math.random() * 3;
}

Skystar.prototype.draw = function() {
  c.save();
  c.beginPath();
  c.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
  c.shadowColor = this.shadowColor;
  c.shadowBlur = Math.random() * 10 + 10;
  c.shadowOffsetX = 0;
  c.shadowOffsetY = 0;
  c.fillStyle = this.color;
  c.fill();
  c.closePath();
  c.restore();
};

Skystar.prototype.update = function() {
  this.draw();
  // 星空一直连续不断向右移
  this.x += skyStarsVelocity;
};

function drawSkyStars() {
  for (let i = 0; i < skyStarsCount; i++) {
    skyStarsArray.push(new Skystar());
  }
}

function animation() {
  requestAnimationFrame(animation);
  // 画背景
  c.fillStyle = backgroundGradient;
  c.fillRect(0, 0, canvas.width, canvas.height);
  // 画星星
  skyStarsArray.forEach(skyStar => skyStar.update());
  // 画山
  drawMountains(1, canvas.height, canvas.height * 0.78, ‘#384551‘, 300);
  drawMountains(2, canvas.height, canvas.height * 0.64, ‘#2B3843‘, 400);
  drawMountains(3, canvas.height, canvas.height * 0.42, ‘#26333E‘, 150);
}

init();
animation();

嗯,效果有了,可是...

一段时间后,左边没有星星了。。。原因是,星星都移出canvas了,所以做个修正,扩大星星的绘制宽度为两倍的canvas宽度,在超出canvas的右侧区域也绘制星星,然后让星星向左移,每当星星的x坐标超出canvas宽度,马上在skystarArray数组中将其移除,同时新创建一个skystar存入数组中,这个新生成的星星必须位于canvas左侧看不见的区域,这样就会有无限个星星一直向右移的效果了。

修改Skystar构造函数:

function Skystar(x) {
  this.x = x || (Math.random() - 0.5) * 2 * canvas.width;
  this.y = Math.random() * canvas.height;
  this.color = ‘#ccc‘;
  this.shadowColor = ‘#E3EAEF‘;
  this.radius = Math.random() * 3;
}

修改animation函数:

function animation() {
  ...
  // 画星星
  skyStarsArray.forEach((skyStar, index) => {
      // 如果超出canvas,则去除这颗星星,在canvas左侧重新生成一颗
      if (skyStar.x - skyStar.radius - 20 > canvas.width ) {
        skyStarsArray.splice(index, 1);
        skyStarsArray.push(new Skystar(-Math.random() * canvas.width));
        return;
    }
    skyStar.update()
  });
  ...
}

目前背景星星是平行地向右移动,但是现实中,由于地球自转,才让我们看到星星在移动,所以现实中的星星的移动应该是一个绕着地球移动的效果。因此,我们加一个更真实的效果:星星从左向右移动期间,它的轨迹先是慢慢远离地面,然后再慢慢接近地面。

修改Skystar的update方法:

Skystar.prototype.update = function() {
  this.draw();
  // 星空一直连续不断向右移
  this.x += skyStarsVelocity;
  // y方向上有一个从上到下的偏移量,这里用cos函数来表示,模拟地球自转时看到的星空
  let angle = Math.PI / (canvas.width / skyStarsVelocity) * (this.x / skyStarsVelocity);
  this.y += this.x > 0 ? -Math.cos(angle) * 0.03 : 0;
};

绘制坠落的星星

接下来,我们绘制掉到地板上的星星,首先,我们画一个地板,修改animation函数

function animation() {
  ...
  // 画山
  drawMountains(1, canvas.height, canvas.height * 0.78, ‘#384551‘, 300);
  drawMountains(2, canvas.height, canvas.height * 0.64, ‘#2B3843‘, 400);
  drawMountains(3, canvas.height, canvas.height * 0.42, ‘#26333E‘, 150);
  // 画地面
  c.fillStyle = ‘#182028‘;
  c.fillRect(0, canvas.height * 0.85, canvas.width, canvas.height * 0.15);
}

接着来定义坠落的星星,方法跟画背景星星一样,先定义一个构造函数Star,然后添加构造函数的方法,最后用一个数组来存放Star的实例。一步一步来

构造函数Star

function Star() {
  this.radius = Math.random() * 10 + 5;
  this.x = Math.random() * (canvas.width - this.radius * 2) + this.radius;
  this.y = -Math.random() * canvas.height;
  this.velocity = {
    x: (Math.random() - 0.5) * 20,
    y: 5,
    rotate: 5
  };
  this.rotate = Math.sign(this.velocity.x) * Math.random() * Math.PI * 2;
  this.friction = 0.7;
  this.gravity = 0.5;
  this.opacity = 1;
  this.shadowColor = ‘#E3EAEF‘;
  this.shadowBlur = 20;
  this.timeToLive = 200;
  this.die = false;
}

解释一下这些属性:x,y表示坠星初始的位置,我们把这些星星定义在canvas的上方,星星会从天而降;velocity表示星星各个方向的初始速度,包括星星自转的初始速度;rotate就是星星的角度;gravity表示重力作用,起到一个模拟重力加速的物理效果;timeToLive表示当星星被标识为die时(也就是当星星经过碰撞损失后,半径已经不能再小的时候),经过多长时间才消失

Star绘制五角星的方法draw:

Star.prototype.draw = function() {
  c.save();
  c.beginPath();
  // 画五角星
  for (let i = 0; i < 5; i++) {
    c.lineTo(Math.cos((18 + i * 72 - this.rotate) / 180 * Math.PI) * this.radius + this.x, -Math.sin((18 + i * 72 - this.rotate) / 180 * Math.PI) * this.radius + this.y );
      c.lineTo(Math.cos((54 + i * 72 - this.rotate) / 180 * Math.PI) * this.radius * 0.5 + this.x, -Math.sin((54 + i * 72 - this.rotate) / 180 * Math.PI) * this.radius * 0.5 + this.y);
  }
  c.shadowColor = this.shadowColor;
  c.shadowBlur = this.shadowBlur;
  c.shadowOffsetX = 0;
  c.shadowOffsetY = 0;
  c.fillStyle = ‘rgba(255,255,255,‘ + this.opacity + ‘)‘;
  c.fill();
  c.closePath();
  c.restore();
};

五角星的画法,参看下图

Star的update方法:

Star.prototype.update = function() {
  this.draw();
  // 碰到两边墙壁
  if (
    this.x + this.radius + this.velocity.x > canvas.width ||
    this.x - this.radius + this.velocity.x < 0
  ) {
    this.velocity.x *= -this.friction;    // 碰到两边墙壁,横向速度损失,同时方向反转
    this.velocity.rotate *= -this.friction;    // 旋转速度也损失,同时方向反转
  }
  // 碰到地面
  if (this.y + this.radius + this.velocity.y > canvas.height) {
    this.velocity.y *= -this.friction;    // 每次碰撞,速度都损失,方向反转
    this.velocity.rotate *= (Math.random() - 0.5) * 20;    // 每次碰到地面旋转速度都随机
    this.radius -= 3;
    // 修正如果半径小等于1,直接定为1
    if (this.radius <= 1) {
      this.radius = 1;
    }
  } else {
    this.velocity.y += this.gravity;    // 没碰到地面,速度增加
  }

  this.x += this.velocity.x;
  this.y += this.velocity.y;
  this.rotate += this.velocity.rotate;

  // 进入消失倒计时
  if (this.radius - 1 <= 0 && !this.die) {
    this.timeToLive--;
    this.opacity -= 1 / Math.max(1, this.timeToLive);    // 不透明从慢到快
    if (this.timeToLive < 0) {
      this.die = true;
    }
  }
};

每次碰撞到地面,星星的半径都会少,当半径小到小于1时,我们就认为这颗星星可以进入消失倒计时了,同时也将慢慢变透明

现在,定义一个初始化函数drawStars,将坠落的星星存在starsArray中

// 先画5个星星
function drawStars() {
  for (let i = 0; i < 5; i++) {
    starsArray.push(new Star());
  }
}

修改init函数

function init() {
  drawSkyStars(); // 初始化背景星星
  drawStars(); // 初始化坠落的星星
}

修改animation函数

function animation() {
  ...
  // 画坠落的星星
  starsArray.forEach((star, index) => {
    if (star.die) {
      starsArray.splice(index, 1);
      return;
    }
    star.update();
  });
}

同时还要记得加一个全局的数组starsArray

const starsArray = []; // 坠落星星数组

完整代码

const canvas = document.createElement(‘canvas‘);
canvas.style.position = ‘absolute‘;
canvas.style.top = 0;
canvas.style.left = 0;
canvas.style.zIndex = -1;
document.body.appendChild(canvas);
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const c = canvas.getContext(‘2d‘);
const skyStarsArray = []; // 星空星星数组
const starsArray = []; // 坠落星星数组
const skyStarsCount = 400; // 星空初始生成星星数量
const skyStarsVelocity = 0.1; // 星空平移速度
const backgroundGradient = c.createLinearGradient(0, 0, 0, canvas.height);    //4个参数:startX,startY,EndX,EndY
backgroundGradient.addColorStop(0, ‘rgba(23, 30, 38, 0.7)‘);
backgroundGradient.addColorStop(1, ‘rgba(63, 88, 107, 0.7)‘);

function init() {
  drawSkyStars(); // 初始化背景星星
  drawStars(); // 初始化坠落的星星
}

// 画山
function drawMountains(number, y, height, color, offset) {
  c.save();
  c.fillStyle = color;
  const width = canvas.width / number;
  // 循环绘制
  for (let i = 0; i < number; i++) {
    c.beginPath();
    c.moveTo(width * i - offset, y);
    c.lineTo(width * i + width + offset, y);
    c.lineTo(width * i + width / 2, y - height);
    c.closePath();
    c.fill();
  }
  c.restore();
}

function Skystar(x) {
  this.x = x || (Math.random() - 0.5) * 2 * canvas.width;
  this.y = Math.random() * canvas.height;
  this.color = ‘#ccc‘;
  this.shadowColor = ‘#E3EAEF‘;
  this.radius = Math.random() * 3;
}

Skystar.prototype.draw = function() {
  c.save();
  c.beginPath();
  c.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
  c.shadowColor = this.shadowColor;
  c.shadowBlur = Math.random() * 10 + 10;
  c.shadowOffsetX = 0;
  c.shadowOffsetY = 0;
  c.fillStyle = this.color;
  c.fill();
  c.closePath();
  c.restore();
};

Skystar.prototype.update = function() {
  this.draw();
  // 星空一直连续不断向右移
  this.x += skyStarsVelocity;
  // y方向上有一个从上到下的偏移量,这里用cos函数来表示,模拟地球自转时看到的星空
  let angle = Math.PI / (canvas.width / skyStarsVelocity) * (this.x / skyStarsVelocity);
  this.y += this.x > 0 ? -Math.cos(angle) * 0.03 : 0;
};

function drawSkyStars() {
  for (let i = 0; i < skyStarsCount; i++) {
    skyStarsArray.push(new Skystar());
  }
}

function Star() {
  this.radius = Math.random() * 10 + 5;
  this.x = Math.random() * (canvas.width - this.radius * 2) + this.radius;
  this.y = -Math.random() * canvas.height;
  this.velocity = {
    x: (Math.random() - 0.5) * 20,
    y: 5,
    rotate: 5
  };
  this.rotate = Math.sign(this.velocity.x) * Math.random() * Math.PI * 2;
  this.friction = 0.7;
  this.gravity = 0.5;
  this.opacity = 1;
  this.shadowColor = ‘#E3EAEF‘;
  this.shadowBlur = 20;
  this.timeToLive = 200;
  this.die = false;
}

Star.prototype.draw = function() {
  c.save();
  c.beginPath();
  // 画五角星
  for (let i = 0; i < 5; i++) {
    c.lineTo(
      Math.cos((18 + i * 72 - this.rotate) / 180 * Math.PI) *
      this.radius +
      this.x,
      -Math.sin((18 + i * 72 - this.rotate) / 180 * Math.PI) *
      this.radius +
      this.y
    );
    c.lineTo(
      Math.cos((54 + i * 72 - this.rotate) / 180 * Math.PI) *
      this.radius *
      0.5 +
      this.x,
      -Math.sin((54 + i * 72 - this.rotate) / 180 * Math.PI) *
      this.radius *
      0.5 +
      this.y
    );
  }
  c.shadowColor = this.shadowColor;
  c.shadowBlur = this.shadowBlur;
  c.shadowOffsetX = 0;
  c.shadowOffsetY = 0;
  c.fillStyle = ‘rgba(255,255,255,‘ + this.opacity + ‘)‘;
  c.fill();
  c.closePath();
  c.restore();
};

Star.prototype.update = function() {
  this.draw();
  // 碰到两边墙壁
  if (
    this.x + this.radius + this.velocity.x > canvas.width ||
    this.x - this.radius + this.velocity.x < 0
  ) {
    this.velocity.x *= -this.friction; // 碰到两边墙壁,横向速度损失,同时方向反转
    this.velocity.rotate *= -this.friction; // 旋转速度也损失,同时方向反转反
  }
  // 碰到地面
  if (this.y + this.radius + this.velocity.y > canvas.height) {
    this.velocity.y *= -this.friction; // 每次碰撞,速度都损失,方向反转
    this.velocity.rotate *= (Math.random() - 0.5) * 20; // 每次碰到地面旋转速度都随机
    this.radius -= 3;
    // 修正如果半径小等于1,直接定为1
    if (this.radius <= 1) {
      this.radius = 1;
    }
  } else {
    this.velocity.y += this.gravity; // 没碰到地面,速度增加
  }

  this.x += this.velocity.x;
  this.y += this.velocity.y;
  this.rotate += this.velocity.rotate;

  // 进入消失倒计时
  if (this.radius - 1 <= 0 && !this.die) {
    this.timeToLive--;
    this.opacity -= 1 / Math.max(1, this.timeToLive); // 不透明从慢到快
    if (this.timeToLive < 0) {
      this.die = true;
    }
  }
};

// 先画5个星星
function drawStars() {
  for (let i = 0; i < 5; i++) {
    starsArray.push(new Star());
  }
}

function animation() {
  requestAnimationFrame(animation);
  // 画背景
  c.fillStyle = backgroundGradient;
  c.fillRect(0, 0, canvas.width, canvas.height);
  // 画星星
  skyStarsArray.forEach((skyStar, index) => {
    // 如果超出canvas,则去除这颗星星,在canvas左侧重新生成一颗
    if (skyStar.x - skyStar.radius - 20 > canvas.width) {
      skyStarsArray.splice(index, 1);
      skyStarsArray.push(new Skystar(-Math.random() * canvas.width));
      return;
    }
    skyStar.update();
  });
  // 画山
  drawMountains(1, canvas.height, canvas.height * 0.78, ‘#384551‘, 300);
  drawMountains(2, canvas.height, canvas.height * 0.64, ‘#2B3843‘, 400);
  drawMountains(3, canvas.height, canvas.height * 0.42, ‘#26333E‘, 150);
  // 画地面
  c.fillStyle = ‘#182028‘;
  c.fillRect(0, canvas.height * 0.85, canvas.width, canvas.height * 0.15);
  // 画坠落的星星
  starsArray.forEach((star, index) => {
    if (star.die) {
      starsArray.splice(index, 1);
      return;
    }
    star.update();
  });
}

init();
animation();

绘制碰撞粒子

接下来分析一下如何产生碰撞后粒子飞出去的效果。首先,触发碰撞的条件是在星星坠落碰到地面的那个时刻,此时,生成了一个爆炸点。由于单个星星能产生多个爆炸点,因此我们用一个数组explosionsArray来保存爆炸点。这里沿用之前产生坠星的方法,我们定义爆炸点的构造函数及其相关的方法,然后在碰撞的时候,创建一个爆炸点实例。

先定义一个全局的变量

const explosionsArray = []; // 爆炸点数组

爆炸点的构造函数Explosion

function Explosion(star) {
  // ...
}

因为爆炸点的位置和坠星的位置有密切关系,我们在构造爆炸点的实例时传入一个star实例,从而获取到相关的信息。

爆炸的时候,会飞出小颗粒,也就是生成了新的物体,所以这里还要定义粒子的构造函数。而且在创建爆炸点的那个时候,就要生成几个粒子实例。

粒子的构造函数Particle

function Particle() {
  // ...
}

现在来捋一捋过程:坠星碰到地面的那一时刻 -> 生成爆炸点实例 -> 生成几个粒子实例 -> 循环绘制粒子,更新粒子位置

坠星碰到地面的那一时刻 -> 生成爆炸点实例

坠星碰到地面的那一时刻是在Star的update方法中判断,故这里要修改update方法

Star.prototype.update = function() {
  ...
  // 碰到地面
  if (this.y + this.radius + this.velocity.y > canvas.height) {
    // 如果没到最小半径,则产生爆炸效果
    if (this.radius > 1) {
      explosionsArray.push(new Explosion(this));
    }
    this.velocity.y *= -this.friction; // 每次碰撞,速度都损失,方向反转
    this.velocity.rotate *= (Math.random() - 0.5) * 20; // 每次碰到地面旋转速度都随机
    ...
};
生成爆炸点实例 -> 生成几个粒子实例

这里就要对爆炸点的构造函数进行完善和补充。因为在生成爆炸点的同时,也要同时生成爆炸粒子,所以这里定义一个init方法,在此方法中生成爆炸粒子实例,然后这个init方法在爆炸点实例化时立即执行。

function Explosion(star) {
  this.init(star);
}

假设在碰撞的瞬间生成随机个粒子,因此需要在各个爆炸点设置一个存放爆炸粒子的数组

function Explosion(star) {
  this.particles=[];    // 用来存放爆炸粒子
  this.init(star);
}

init函数

Explosion.prototype.init = function(star) {
  for (let i = 0; i < 4 + Math.random() * 10; i++) {
    const dx = (Math.random() - 0.5) * 8;    // 随机生成的x方向速度
    const dy = (Math.random() - 0.5) * 20;    // 随机生成的y方向速度
    this.particles.push(new Particle(star.x, star.y, dx, dy));    // 把坐标和速度传给Particle构造函数
  }
};
循环绘制粒子,更新粒子位置

除了在碰撞时要生成粒子之外,在每次animation函数执行时,也要更新每个粒子的位置,因此,还要给Explosion定义一个update函数来更新粒子状态

Explosion.prototype.update = function() {
  this.particles.forEach(particle => {
    particle.update();
  });
};

接着来完善Particle构造函数,跟前面坠星的构造函数类似,不同的是粒子用一个正方形表示

function Particle(x, y, dx, dy) {
  this.x = x;
  this.y = y;
  this.dx = dx;
  this.dy = dy;
  this.size = {
    width: 2,
    height: 2
  };
  this.friction = 0.7;
  this.gravity = 0.5;
  this.opacity = 1;
  this.timeToLive = 200;
  this.shadowColor = ‘#E3EAEF‘;
}

Particle的draw方法

Particle.prototype.draw = function() {
  c.save();
  c.fillStyle = ‘rgba(227, 234, 239,‘ + this.opacity + ‘)‘;
  c.shadowColor = this.shadowColor;
  c.shadowBlur = 20;
  c.shadowOffsetX = 0;
  c.shadowOffsetY = 0;
  c.fillRect(this.x, this.y, this.size.width, this.size.height);
  c.restore();
};

Particle的update函数,与坠星不同的是,碰撞粒子一产生就开始消失倒计时

Particle.prototype.update = function() {
  this.draw();
  // 碰到两边墙壁
  if (
    this.x + this.size.width + this.dx > canvas.width ||
    this.x + this.dx < 0
  ) {
    this.dx *= -this.friction;
  }
  // 碰到地面
  if (this.y + this.size.height + this.dy > canvas.height) {
    this.dy *= -this.friction;
  } else {
    this.dy += this.gravity;
  }
  this.x += this.dx;
  this.y += this.dy;

  this.timeToLive--;
  this.opacity -= 1 / this.timeToLive; //不透明度ease-in效果
};

倒计时结束后,要及时把这个爆炸粒子从particles数组中移除。修改Explosion的update方法

Explosion.prototype.update = function() {
  this.particles.forEach((particle, index, particles) => {
    if (particle.timeToLive <= 0) {    // 生命周期结束
      particles.splice(index, 1);
      return;
    }
    particle.update();
  });
};

同理,如果当前爆炸点中所有的粒子都已经超过生命周期,那么就要从explosionsArray中移除。修改animation函数

function animation() {
  ...
  // 画坠落的星星
  starsArray.forEach((star, index) => {
    if (star.die) {
      starsArray.splice(index, 1);
      return;
    }
    star.update();
  });
  // 循环更新爆炸点
  explosionsArray.forEach((explosion, index) => {
    if (explosion.particles.length === 0) {
      explosionsArray.splice(index, 1);
      return;
    }
    explosion.update();
  });
}

完整代码

const canvas = document.createElement(‘canvas‘);
canvas.style.position = ‘absolute‘;
canvas.style.top = 0;
canvas.style.left = 0;
canvas.style.zIndex = -1;
document.body.appendChild(canvas);
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const c = canvas.getContext(‘2d‘);
const skyStarsArray = []; // 星空星星数组
const starsArray = []; // 坠落星星数组
const explosionsArray = []; // 爆炸粒子数组
const skyStarsCount = 400; // 星空初始生成星星数量
const skyStarsVelocity = 0.1; // 星空平移速度
const backgroundGradient = c.createLinearGradient(0, 0, 0, canvas.height); //4个参数:startX,startY,EndX,EndY
backgroundGradient.addColorStop(0, ‘rgba(23, 30, 38, 0.7)‘);
backgroundGradient.addColorStop(1, ‘rgba(63, 88, 107, 0.7)‘);

function init() {
  drawSkyStars(); // 初始化背景星星
  drawStars(); // 初始化坠落的星星
}

// 画山
function drawMountains(number, y, height, color, offset) {
  c.save();
  c.fillStyle = color;
  const width = canvas.width / number;
  // 循环绘制
  for (let i = 0; i < number; i++) {
    c.beginPath();
    c.moveTo(width * i - offset, y);
    c.lineTo(width * i + width + offset, y);
    c.lineTo(width * i + width / 2, y - height);
    c.closePath();
    c.fill();
  }
  c.restore();
}

function Skystar(x) {
  this.x = x || (Math.random() - 0.5) * 2 * canvas.width;
  this.y = Math.random() * canvas.height;
  this.color = ‘#ccc‘;
  this.shadowColor = ‘#E3EAEF‘;
  this.radius = Math.random() * 3;
}

Skystar.prototype.draw = function() {
  c.save();
  c.beginPath();
  c.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
  c.shadowColor = this.shadowColor;
  c.shadowBlur = Math.random() * 10 + 10;
  c.shadowOffsetX = 0;
  c.shadowOffsetY = 0;
  c.fillStyle = this.color;
  c.fill();
  c.closePath();
  c.restore();
};

Skystar.prototype.update = function() {
  this.draw();
  // 星空一直连续不断向右移
  this.x += skyStarsVelocity;
  // y方向上有一个从上到下的偏移量,这里用cos函数来表示,模拟地球自转时看到的星空
  let angle = Math.PI / (canvas.width / skyStarsVelocity) * (this.x / skyStarsVelocity);
  this.y += this.x > 0 ? -Math.cos(angle) * 0.03 : 0;
};

function drawSkyStars() {
  for (let i = 0; i < skyStarsCount; i++) {
    skyStarsArray.push(new Skystar());
  }
}

function Star() {
  this.radius = Math.random() * 10 + 5;
  this.x = Math.random() * (canvas.width - this.radius * 2) + this.radius;
  this.y = -Math.random() * canvas.height;
  this.velocity = {
    x: (Math.random() - 0.5) * 20,
    y: 5,
    rotate: 5
  };
  this.rotate = Math.sign(this.velocity.x) * Math.random() * Math.PI * 2;
  this.friction = 0.7;
  this.gravity = 0.5;
  this.opacity = 1;
  this.shadowColor = ‘#E3EAEF‘;
  this.shadowBlur = 20;
  this.timeToLive = 200;
  this.die = false;
}

Star.prototype.draw = function() {
  c.save();
  c.beginPath();
  // 画五角星
  for (let i = 0; i < 5; i++) {
    c.lineTo(
      Math.cos((18 + i * 72 - this.rotate) / 180 * Math.PI) *
      this.radius +
      this.x,
      -Math.sin((18 + i * 72 - this.rotate) / 180 * Math.PI) *
      this.radius +
      this.y
    );
    c.lineTo(
      Math.cos((54 + i * 72 - this.rotate) / 180 * Math.PI) *
      this.radius *
      0.5 +
      this.x,
      -Math.sin((54 + i * 72 - this.rotate) / 180 * Math.PI) *
      this.radius *
      0.5 +
      this.y
    );
  }
  c.shadowColor = this.shadowColor;
  c.shadowBlur = this.shadowBlur;
  c.shadowOffsetX = 0;
  c.shadowOffsetY = 0;
  c.fillStyle = ‘rgba(255,255,255,‘ + this.opacity + ‘)‘;
  c.fill();
  c.closePath();
  c.restore();
};

Star.prototype.update = function() {
  this.draw();
  // 碰到两边墙壁
  if (
    this.x + this.radius + this.velocity.x > canvas.width ||
    this.x - this.radius + this.velocity.x < 0
  ) {
    this.velocity.x *= -this.friction; // 碰到两边墙壁,横向速度损失,同时方向反转
    this.velocity.rotate *= -this.friction; // 旋转速度也损失,同时方向反转
  }
  // 碰到地面
  if (this.y + this.radius + this.velocity.y > canvas.height) {
    // 如果没到最小半径,则产生爆炸效果
    if (this.radius > 1) {
      explosionsArray.push(new Explosion(this));
    }
    this.velocity.y *= -this.friction; // 每次碰撞,速度都损失,同时方向反转
    this.velocity.rotate *= (Math.random() - 0.5) * 20; // 每次碰到地面旋转速度都随机
    this.radius -= 3;
    // 修正如果半径小等于1,直接定为1
    if (this.radius <= 1) {
      this.radius = 1;
    }
  } else {
    this.velocity.y += this.gravity; // 没碰到地面,速度增加
  }

  this.x += this.velocity.x;
  this.y += this.velocity.y;
  this.rotate += this.velocity.rotate;

  // 进入消失倒计时
  if (this.radius - 1 <= 0 && !this.die) {
    this.timeToLive--;
    this.opacity -= 1 / Math.max(1, this.timeToLive); // 不透明从慢到快
    if (this.timeToLive < 0) {
      this.die = true;
    }
  }
};

// 先画5个星星
function drawStars() {
  for (let i = 0; i < 5; i++) {
    starsArray.push(new Star());
  }
}

function Explosion(star) {
  this.particles = []; // 用来存放爆炸粒子
  this.init(star);
}

Explosion.prototype.init = function(star) {
  for (let i = 0; i < 4 + Math.random() * 10; i++) {
    const dx = (Math.random() - 0.5) * 8; // 随机生成的x方向速度
    const dy = (Math.random() - 0.5) * 20; // 随机生成的y方向速度
    this.particles.push(new Particle(star.x, star.y, dx, dy)); // 把坐标和速度传给Particle构造函数
  }
};

Explosion.prototype.update = function() {
  this.particles.forEach((particle, index, particles) => {
    if (particle.timeToLive <= 0) {
      // 生命周期结束
      particles.splice(index, 1);
      return;
    }
    particle.update();
  });
};

function Particle(x, y, dx, dy) {
  this.x = x;
  this.y = y;
  this.dx = dx;
  this.dy = dy;
  this.size = {
    width: 2,
    height: 2
  };
  this.friction = 0.7;
  this.gravity = 0.5;
  this.opacity = 1;
  this.timeToLive = 200;
  this.shadowColor = ‘#E3EAEF‘;
}

Particle.prototype.draw = function() {
  c.save();
  c.fillStyle = ‘rgba(227, 234, 239,‘ + this.opacity + ‘)‘;
  c.shadowColor = this.shadowColor;
  c.shadowBlur = 20;
  c.shadowOffsetX = 0;
  c.shadowOffsetY = 0;
  c.fillRect(this.x, this.y, this.size.width, this.size.height);
  c.restore();
};

Particle.prototype.update = function() {
  this.draw();
  // 碰到两边墙壁
  if (
    this.x + this.size.width + this.dx > canvas.width ||
    this.x + this.dx < 0
  ) {
    this.dx *= -this.friction;
  }
  // 碰到地面
  if (this.y + this.size.height + this.dy > canvas.height) {
    this.dy *= -this.friction;
  } else {
    this.dy += this.gravity;
  }
  this.x += this.dx;
  this.y += this.dy;

  this.timeToLive--;
  this.opacity -= 1 / this.timeToLive; //不透明度ease-in效果
};

function animation() {
  requestAnimationFrame(animation);
  // 画背景
  c.fillStyle = backgroundGradient;
  c.fillRect(0, 0, canvas.width, canvas.height);
  // 画星星
  skyStarsArray.forEach((skyStar, index) => {
    // 如果超出canvas,则去除这颗星星,在canvas左侧重新生成一颗
    if (skyStar.x - skyStar.radius - 20 > canvas.width) {
      skyStarsArray.splice(index, 1);
      skyStarsArray.push(new Skystar(-Math.random() * canvas.width));
      return;
    }
    skyStar.update();
  });
  // 画山
  drawMountains(1, canvas.height, canvas.height * 0.78, ‘#384551‘, 300);
  drawMountains(2, canvas.height, canvas.height * 0.64, ‘#2B3843‘, 400);
  drawMountains(3, canvas.height, canvas.height * 0.42, ‘#26333E‘, 150);
  // 画地面
  c.fillStyle = ‘#182028‘;
  c.fillRect(0, canvas.height * 0.85, canvas.width, canvas.height * 0.15);
  // 画坠落的星星
  starsArray.forEach((star, index) => {
    if (star.die) {
      starsArray.splice(index, 1);
      return;
    }
    star.update();
  });
  // 循环更新爆炸点
  explosionsArray.forEach((explosion, index) => {
    if (explosion.particles.length === 0) {
      explosionsArray.splice(index, 1);
      return;
    }
    explosion.update();
  });
}

init();
animation();

随机掉落和随机产生背景流星

现在效果已经基本完成了,不过目前是一开始就掉5颗,我们想随机一段时间后掉落一颗,看看如何修改。

定义一个全局变量spawnTimer来存放随机生成时间

let spawnTimer = Math.random() * 500; // 随机生成坠落星星的时间

接着在animation循环时,对spawnTimer递减,然后判断spawnTimer的值,如果小于0,表示可以生成新的坠星了。修改animation函数

function animation() {
  ...
  // 循环更新爆炸点
  explosionsArray.forEach((explosion, index) => {
    if (explosion.particles.length === 0) {
      explosionsArray.splice(index, 1);
      return;
    }
    explosion.update();
  });
  // 控制随机生成坠星
  spawnTimer--;
  if (spawnTimer < 0) {
    spawnTimer = Math.random() * 500;
    starsArray.push(new Star());
  }
}

一开始改成画2颗星星

// 画2个星星
function drawStars() {
  for (let i = 0; i < 2; i++) {
    starsArray.push(new Star());
  }
}

突然感觉背景星空只能移动似乎有点单调,那就加上几颗流星吧

修改Skystar构造函数,增加几个属性,falling表示是否触发流星划破天际的效果

function Skystar(x) {
  ...
  // 流星属性
  this.falling = false;
  this.dx = Math.random() * 4 + 4;
  this.dy = 2;
  this.timeToLive = 200;
}

以上定义了变成流星后的速度、加速度、生命周期等属性

除了修改Skystar构造函数,在animation函数中也要加上一些流星的判断

function animation() {
  requestAnimationFrame(animation);
  // 画背景
  c.fillStyle = backgroundGradient;
  c.fillRect(0, 0, canvas.width, canvas.height);
  // 画背景星星
  // 随机将一个背景星星定义成流星,这里利用坠星的随机生成时间来随机生成流星
  if (~~spawnTimer % 103 === 0) {
    skyStarsArray[
      ~~(Math.random() * skyStarsArray.length)
    ].falling = true;
  }
  skyStarsArray.forEach((skyStar, index) => {
    // 如果超出canvas或者作为流星滑落结束,则去除这颗星星,在canvas左侧重新生成一颗
    if (skyStar.x - skyStar.radius - 20 > canvas.width || skyStar.timeToLive < 0) {
      skyStarsArray.splice(index, 1);
      skyStarsArray.push(new Skystar(-Math.random() * canvas.width));
      return;
    }
    // 星空随机产生流星
    if (skyStar.falling) {
      skyStar.x += skyStar.dx;
      skyStar.y += skyStar.dy;
      skyStar.color = ‘#fff‘;
      // 半径慢慢变小
      if (skyStar.radius > 0.05) {
        skyStar.radius -= 0.05;
      } else {
        skyStar.radius = 0.05;
      }
      skyStar.timeToLive--;
    }
    skyStar.update();
  });
  ...
}

最后,我们加上一个resize事件,当画面大小改变时,重新绘制

window.addEventListener(‘resize‘,() => {
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
  skyStarsArray = [];
  starsArray = [];
  explosionsArray = [];
  spawnTimer = Math.random() * 500;
  init();
}, false);

最终代码

const canvas = document.createElement(‘canvas‘);
canvas.style.position = ‘absolute‘;
canvas.style.top = 0;
canvas.style.left = 0;
canvas.style.zIndex = -1;
document.body.appendChild(canvas);
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const c = canvas.getContext(‘2d‘);
const skyStarsArray = []; // 星空星星数组
const starsArray = []; // 坠落星星数组
const explosionsArray = []; // 爆炸粒子数组
const skyStarsCount = 400; // 星空初始生成星星数量
const skyStarsVelocity = 0.1; // 星空平移速度
const backgroundGradient = c.createLinearGradient(0, 0, 0, canvas.height); //4个参数:startX,startY,EndX,EndY
backgroundGradient.addColorStop(0, ‘rgba(23, 30, 38, 0.7)‘);
backgroundGradient.addColorStop(1, ‘rgba(63, 88, 107, 0.7)‘);
let spawnTimer = Math.random() * 500; // 随机生成坠落星星的时间

window.addEventListener(‘resize‘,() => {
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
  skyStarsArray = [];
  starsArray = [];
  explosionsArray = [];
  spawnTimer = Math.random() * 500;
  init();
}, false);

function init() {
  drawSkyStars(); // 初始化背景星星
  drawStars(); // 初始化坠落的星星
}

// 画山
function drawMountains(number, y, height, color, offset) {
  c.save();
  c.fillStyle = color;
  const width = canvas.width / number;
  // 循环绘制
  for (let i = 0; i < number; i++) {
    c.beginPath();
    c.moveTo(width * i - offset, y);
    c.lineTo(width * i + width + offset, y);
    c.lineTo(width * i + width / 2, y - height);
    c.closePath();
    c.fill();
  }
  c.restore();
}

function Skystar(x) {
  this.x = x || (Math.random() - 0.5) * 2 * canvas.width;
  this.y = Math.random() * canvas.height;
  this.color = ‘#ccc‘;
  this.shadowColor = ‘#E3EAEF‘;
  this.radius = Math.random() * 3;
  // 流星属性
  this.falling = false;
  this.dx = Math.random() * 4 + 4;
  this.dy = 2;
  this.timeToLive = 200;
}

Skystar.prototype.draw = function() {
  c.save();
  c.beginPath();
  c.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
  c.shadowColor = this.shadowColor;
  c.shadowBlur = Math.random() * 10 + 10;
  c.shadowOffsetX = 0;
  c.shadowOffsetY = 0;
  c.fillStyle = this.color;
  c.fill();
  c.closePath();
  c.restore();
};

Skystar.prototype.update = function() {
  this.draw();
  // 星空一直连续不断向右移
  this.x += skyStarsVelocity;
  // y方向上有一个从上到下的偏移量,这里用cos函数来表示,模拟地球自转时看到的星空
  let angle =
      Math.PI /
      (canvas.width / skyStarsVelocity) *
      (this.x / skyStarsVelocity);
  this.y += this.x > 0 ? -Math.cos(angle) * 0.03 : 0;
};

function drawSkyStars() {
  for (let i = 0; i < skyStarsCount; i++) {
    skyStarsArray.push(new Skystar());
  }
}

function Star() {
  this.radius = Math.random() * 10 + 5;
  this.x = Math.random() * (canvas.width - this.radius * 2) + this.radius;
  this.y = -Math.random() * canvas.height;
  this.velocity = {
    x: (Math.random() - 0.5) * 20,
    y: 5,
    rotate: 5
  };
  this.rotate = Math.sign(this.velocity.x) * Math.random() * Math.PI * 2;
  this.friction = 0.7;
  this.gravity = 0.5;
  this.opacity = 1;
  this.shadowColor = ‘#E3EAEF‘;
  this.shadowBlur = 20;
  this.timeToLive = 200;
  this.die = false;
}

Star.prototype.draw = function() {
  c.save();
  c.beginPath();
  // 画五角星
  for (let i = 0; i < 5; i++) {
    c.lineTo(
      Math.cos((18 + i * 72 - this.rotate) / 180 * Math.PI) *
      this.radius +
      this.x,
      -Math.sin((18 + i * 72 - this.rotate) / 180 * Math.PI) *
      this.radius +
      this.y
    );
    c.lineTo(
      Math.cos((54 + i * 72 - this.rotate) / 180 * Math.PI) *
      this.radius *
      0.5 +
      this.x,
      -Math.sin((54 + i * 72 - this.rotate) / 180 * Math.PI) *
      this.radius *
      0.5 +
      this.y
    );
  }
  c.shadowColor = this.shadowColor;
  c.shadowBlur = this.shadowBlur;
  c.shadowOffsetX = 0;
  c.shadowOffsetY = 0;
  c.fillStyle = ‘rgba(255,255,255,‘ + this.opacity + ‘)‘;
  c.fill();
  c.closePath();
  c.restore();
};

Star.prototype.update = function() {
  this.draw();
  // 碰到两边墙壁
  if (
    this.x + this.radius + this.velocity.x > canvas.width ||
    this.x - this.radius + this.velocity.x < 0
  ) {
    this.velocity.x *= -this.friction; // 碰到两边墙壁,横向速度损失,同时方向反转
    this.velocity.rotate *= -this.friction; // 旋转速度也损失,同时方向反转
  }
  // 碰到地面
  if (this.y + this.radius + this.velocity.y > canvas.height) {
    // 如果没到最小半径,则产生爆炸效果
    if (this.radius > 1) {
      explosionsArray.push(new Explosion(this));
    }
    this.velocity.y *= -this.friction; // 每次碰撞,速度都损失,同时方向反转
    this.velocity.rotate *= (Math.random() - 0.5) * 20; // 每次碰到地面旋转速度都随机
    this.radius -= 3;
    // 修正如果半径小等于1,直接定为1
    if (this.radius <= 1) {
      this.radius = 1;
    }
  } else {
    this.velocity.y += this.gravity; // 没碰到地面,速度增加
  }

  this.x += this.velocity.x;
  this.y += this.velocity.y;
  this.rotate += this.velocity.rotate;

  // 进入消失倒计时
  if (this.radius - 1 <= 0 && !this.die) {
    this.timeToLive--;
    this.opacity -= 1 / Math.max(1, this.timeToLive); // 不透明从慢到快
    if (this.timeToLive < 0) {
      this.die = true;
    }
  }
};

// 画2个星星
function drawStars() {
  for (let i = 0; i < 2; i++) {
    starsArray.push(new Star());
  }
}

function Explosion(star) {
  this.particles = []; // 用来存放爆炸粒子
  this.init(star);
}

Explosion.prototype.init = function(star) {
  for (let i = 0; i < 4 + Math.random() * 10; i++) {
    const dx = (Math.random() - 0.5) * 8; // 随机生成的x方向速度
    const dy = (Math.random() - 0.5) * 20; // 随机生成的y方向速度
    this.particles.push(new Particle(star.x, star.y, dx, dy)); // 把坐标和速度传给Particle构造函数
  }
};

Explosion.prototype.update = function() {
  this.particles.forEach((particle, index, particles) => {
    if (particle.timeToLive <= 0) {
      // 生命周期结束
      particles.splice(index, 1);
      return;
    }
    particle.update();
  });
};

function Particle(x, y, dx, dy) {
  this.x = x;
  this.y = y;
  this.dx = dx;
  this.dy = dy;
  this.size = {
    width: 2,
    height: 2
  };
  this.friction = 0.7;
  this.gravity = 0.5;
  this.opacity = 1;
  this.timeToLive = 200;
  this.shadowColor = ‘#E3EAEF‘;
}

Particle.prototype.draw = function() {
  c.save();
  c.fillStyle = ‘rgba(227, 234, 239,‘ + this.opacity + ‘)‘;
  c.shadowColor = this.shadowColor;
  c.shadowBlur = 20;
  c.shadowOffsetX = 0;
  c.shadowOffsetY = 0;
  c.fillRect(this.x, this.y, this.size.width, this.size.height);
  c.restore();
};

Particle.prototype.update = function() {
  this.draw();
  // 碰到两边墙壁
  if (
    this.x + this.size.width + this.dx > canvas.width ||
    this.x + this.dx < 0
  ) {
    this.dx *= -this.friction;
  }
  // 碰到地面
  if (this.y + this.size.height + this.dy > canvas.height) {
    this.dy *= -this.friction;
  } else {
    this.dy += this.gravity;
  }
  this.x += this.dx;
  this.y += this.dy;

  this.timeToLive--;
  this.opacity -= 1 / this.timeToLive; //不透明度ease-in效果
};

function animation() {
  requestAnimationFrame(animation);
  // 画背景
  c.fillStyle = backgroundGradient;
  c.fillRect(0, 0, canvas.width, canvas.height);
  // 画背景星星
  // 随机将一个背景星星定义成流星
  if (~~spawnTimer % 103 === 0) {    // 这里选择一个质数来求余,使得一个生成周期内最多触发一次
    skyStarsArray[
      ~~(Math.random() * skyStarsArray.length)
    ].falling = true;
  }
  skyStarsArray.forEach((skyStar, index) => {
    // 如果超出canvas或者作为流星滑落结束,则去除这颗星星,在canvas左侧重新生成一颗
    if (
      skyStar.x - skyStar.radius - 20 > canvas.width ||
      skyStar.timeToLive < 0
    ) {
      skyStarsArray.splice(index, 1);
      skyStarsArray.push(new Skystar(-Math.random() * canvas.width));
      return;
    }
    // 星空随机产生流星
    if (skyStar.falling) {
      skyStar.x += skyStar.dx;
      skyStar.y += skyStar.dy;
      skyStar.color = ‘#fff‘;
      // 半径慢慢变小
      if (skyStar.radius > 0.05) {
        skyStar.radius -= 0.05;
      } else {
        skyStar.radius = 0.05;
      }
      skyStar.timeToLive--;
    }
    skyStar.update();
  });
  // 画山
  drawMountains(1, canvas.height, canvas.height * 0.78, ‘#384551‘, 300);
  drawMountains(2, canvas.height, canvas.height * 0.64, ‘#2B3843‘, 400);
  drawMountains(3, canvas.height, canvas.height * 0.42, ‘#26333E‘, 150);
  // 画地面
  c.fillStyle = ‘#182028‘;
  c.fillRect(0, canvas.height * 0.85, canvas.width, canvas.height * 0.15);
  // 画坠落的星星
  starsArray.forEach((star, index) => {
    if (star.die) {
      starsArray.splice(index, 1);
      return;
    }
    star.update();
  });
  // 循环更新爆炸点
  explosionsArray.forEach((explosion, index) => {
    if (explosion.particles.length === 0) {
      explosionsArray.splice(index, 1);
      return;
    }
    explosion.update();
  });
  // 控制随机生成坠星
  spawnTimer--;
  if (spawnTimer < 0) {
    spawnTimer = Math.random() * 500;
    starsArray.push(new Star());
  }
}

init();
animation();

整个效果和制作流程就是这样,希望你们能喜欢。快过年了,提前祝大家春节快乐,过年要放烟花,接下去也想研究一下制作烟花的效果,有兴趣的朋友一起交流吧~~

  

原文地址:https://www.cnblogs.com/zealingstar/p/8277177.html

时间: 2024-08-28 05:24:05

利用HTML canvas制作酷炫星星坠地粒子特效的相关文章

用canvas制作酷炫射击游戏--part3

今天介绍下 游戏中的sprite模块,也就是构建玩家及怪物的模块.有了这个模块,就可以在咱们的游戏里加入人物了. 想必用过css的朋友都知道sprite,一种将需要加载的图片拼接在一张图里以减少请求的css性能优化的方法.在游戏中的sprite,指游戏里有行为.有模样的对象.也就是人物或怪物. 源码见game-engine第598行.这里截取下目前需要的部分 加以说明. // 精灵构造器 var Sprite= function (name,context,painter,behaviors)

Qt Widget 利用 Qt4.5 实现酷炫透明窗体

本文讲述的是Qt Widget 利用 Qt4.5 实现酷炫透明窗体,QWidget类中的每一个窗口部件都是矩形,并且它们按Z轴顺序排列的.一个窗口部件可以被它的父窗口部件或者它前面的窗口部件盖住一部分. 先来看内容吧. Qt4.2引入了QWidget::setWindowOpacity函数, 可以为窗体设置透明度, 从0.0到1.0之间, 值越小越透明. 经过设置的窗体可以整体呈现透明的效果. 但这种设置比较粗糙, 只能设一个整体的效果,大概只有比如像拖动的时候能用一下,大多数时候都不太实用.在

tree.js 制作酷炫照片墙

原创hangGe0111 最后发布于2019-03-28 16:10:35 阅读数 561 收藏展开1.tree.js 是一个用来 制作流畅效果,如果学习的比较深入的话,用来做 游戏 或者 动画片 都是可以的,是一个很不错的js插件: 2.使用教程参考 2.1  http://www.hewebgl.com/article/articledir/1  (比较推荐) 这是一个中文网站,资料也比较齐全 2.2  https://techbrood.com/threejs/docs/#%E4%BD%B

Impress.js制作酷炫Presentation PPT

可以先看一个demo:http://dwqs.github.io/resume 昨天,我写了一些关于Impress.js的东西,对于创建在线的自我展示,这是一个非常不错的JavaScript库.由于是线上发布,所有有部分人问我怎么正确的使用它.因为没有在实际的项目页面设置帮助文档.这一篇文章将帮助你开始创建一个简单的幻灯片,但是之后你一定要完成它,可以用它来做很多酷炫的效果,唯一限制你的就是你的创造力. 需求 为了看到效果,请使用Google Chrome or Safari (or Firef

站在巨人的肩膀上——制作酷炫web幻灯片

在线演示1 本地下载 你是否还在用ppt做一些毫无意思的幻灯片?你是否在做ppt的时候绞尽脑汁想把效果做的吸引大家?你是否想通过一次ppt吸引领导的注意?那好吧!来学学怎么制作一款炫酷的web幻灯片~ ps:如果看到这里还不感兴趣就请先看看素材演示,这个演示是前些时候给组里新人介绍HTML5的时候自己做的一款幻灯片!   工具 一款最近版本的chrome浏览器(火狐,safari也可) 一款趁手的IDE工具 impress.js君(您也可下载本页素材进行自己需求的修改) 优点 几乎不需要写任何J

使用reveal.js制作酷炫的页面

几天前遇到一个制作3D幻灯片效果的插件,试着用到自己的在线简历页面中,展示效果蛮好的.不过查阅了很多文档,几乎都是英文,于是想着整理一下写一篇博客当总结. 准备工作: 1.首先当然是下载相应reveal的资源,下载链接:https://github.com/hakimel/reveal.js 2.将相应的文件夹(css.is.lib.plugin)引入到自己的工作目录,目录层次如下:(忽略我的imgs文件夹~~) 下面就可以着手写自己的页面啦~ <!doctype html> <html

Html + Css3 制作酷炫的导航栏

主要亮点: 1 ul 水平显示 2 li 去掉圆点 3 li中字体水平.竖直居中 4 li控制边框样式 5 使用html + css3 渐变画图 制作背景图片 6 更改颜色透明度 7 DIV制作边框阴影 先看效果图: 实现代码: <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <!----编码-----> <meta nam

PS如何制作酷炫个性字母人像海报

整体效果还是很棒棒哒,本人还是比较喜欢这种感觉,这里我们用(创意文字拼贴人物海报效果PS动作)来快速制作出这种效果,下面是一些效果: 01.打开软件,载入笔刷.动作直接双击可以载入或者编辑,预设,预设管理器载入笔刷和动作面板载入动作,关闭软件. 02.把软件切换成英文,我一般是直接改tw10428_Photoshop_zh_CN.dat文件名为Atw10428_Photoshop_zh_CN.dat,详细路径可以看一下图片. 03.打开软件,打开我们需要制作效果的图片,用钢笔工具等做出如下选区.

使用CSS3制作酷炫防苹果复选框 自行测试!

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <style> body{ background:#eee; } .lbl{ /*复选框背景色*/ height:20px; width:50px; display:block;/*元素将显示为块级元素*/ ba