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

本文介绍如何利用HTML canvas实现一个酷炫的星星坠地粒子特效,包括初始设置、绘制背景星空、绘制坠落星星、碰撞粒子的生成与绘制,以及随机掉落和背景流星的产生。通过详细讲解和代码示例,展示了制作过程。
摘要由CSDN通过智能技术生成

去年在电影院看过的电影,印象最深刻的,算是电影《你的名字》了,而且被其中的画面深深吸引了,尤其是陨石划过天空的场景,太美啦!所以想着哪天做一个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.opacit
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值