浏览这个网站后,发现它的动态粒子背景效果真不错。
起初我以为这个效果是用视频背景实现的,后来发现其粒子可以根据滚动条的位置而加速,并不简单。F12查看后发现其使用了Canvas。于是,想着编写一个JS库来实现类似的“动态粒子背景”效果。
动态粒子是通过Canvas绘制的,因此需要了解Canvas的一些常用的API方法:
<canvas id="bg">canvas>
const canvas = document.getElementById("bg");
const ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.arc(100, 100, 50, 0, Math.PI * 2);
ctx.stroke();
ctx.fillStyle = "#ff0000"; // 设置填充颜色为红色
ctx.strokeStyle = "#0000ff"; // 设置描边颜色为蓝色
ctx.lineWidth = 5; // 设置线条宽度为5像素
ctx.font = "24px Arial"; // 设置字体、大小和字体族
节流(throttle)用于减少函数的执行次数。Canvas元素的宽度高度要跟随浏览器宽度高度变化,若不使用节流则会频繁触发绘制的相关参数,影响到浏览器性能。
通过闭包封装好内部变量,使其不影响外部的变量。
//Canvas元素
let canvasElement;
//Canvas 2D对象
let canvasContext;
//Canvas 宽度
let canvasWidth;
//Canvas 高度
let canvasHeight;
//星星列表
let starList;
//星星颜色列表,rgb格式:"255, 255, 255"
let starColorList;
//星星半径大小
let starRadius;
//焦距等级,与canvasWidth相乘,必须大于0
let focalDistanceLevel;
//星星数量等级,与canvasWidth相乘,必须大于0
let starCountLevel;
//星星速度等级,与焦距相乘,必须大于0
let starSpeedLevel;
//焦距
let focalDistance;
//星星数量
let starCount;
//执行动画
let rAF;
定义初始化方法,初始化的参数必须是一个Canvas元素对象。初始化的内容是设置Canvas元素的初始背景色、Canvas画布的宽度和高度、粒子的颜色列表、粒子的半径、焦距的等级、粒子的数量等级、粒子的速度等级。
canvasElement = canvas_element;
canvasElement.width = canvasElement.clientWidth;
canvasElement.height = canvasElement.clientHeight;
canvasElement.style.backgroundColor = "black";
canvasContext = canvasElement.getContext("2d");
canvasWidth = canvasElement.clientWidth;
canvasHeight = canvasElement.clientHeight;
starColorList = ["255, 255, 255"];
starRadius = 1;
focalDistanceLevel = 0.4;
starCountLevel = 0.2;
starSpeedLevel = 0.0005;
focalDistance = canvasWidth * focalDistanceLevel;
starCount = Math.ceil(canvasWidth * starCountLevel);
生成每个粒子的属性
starList = [];
for (let i = 0; i < starCount; i++) {
starList[i] = {
x: canvasWidth * (0.1 + 0.8 * Math.random()),
y: canvasHeight * (0.1 + 0.8 * Math.random()),
z: focalDistance * Math.random(),
color: starColorList[Math.ceil(Math.random() * 1000) % starColorList.length]
}
}}
粒子的x、y、z属性是随机确定的。x、y设置初次渲染在距离屏幕边界的10%的范围内,而不至于刚刚渲染出现就要离开屏幕范围。z表示粒子距离屏幕面的距离。
注册浏览器窗口尺寸变化事件
const self = this;
window.addEventListener("resize", self.throttle(function () {
canvasElement.width = canvasElement.clientWidth;
canvasElement.height = canvasElement.clientHeight;
canvasWidth = canvasElement.clientWidth;
canvasHeight = canvasElement.clientHeight;
focalDistance = canvasWidth * focalDistanceLevel;
const starCount2 = Math.ceil(canvasWidth * starCountLevel);
if (starCount > starCount2) {
starList.splice(starCount2);
} else {
let num = starCount2 - starCount;
while (num--) {
starList.push({
x: canvasWidth * (0.1 + 0.8 * Math.random()),
y: canvasHeight * (0.1 + 0.8 * Math.random()),
z: focalDistance * Math.random(),
color: starColorList[Math.ceil(Math.random() * 1000) % starColorList.length]
});
}
}
starCount = Math.ceil(canvasWidth * starCountLevel);
}, 200));
const starSpeed = canvasWidth * focalDistanceLevel * starSpeedLevel;
//清空画布
canvasContext.clearRect(0, 0, canvasWidth, canvasHeight);
//计算位置
for (let i = 0; i < starList.length; i++) {
const star = starList[i];
const star_x = (star["x"] - canvasWidth / 2) * (focalDistance / star["z"]) + canvasWidth / 2;
const star_y = (star["y"] - canvasHeight / 2) * (focalDistance / star["z"]) + canvasHeight / 2;
star["z"] -= starSpeed;
if (star["z"] > 0 && star["z"] <= focalDistance && star_x >= -20 && star_x <= canvasWidth + 20 && star_y >= -20 && star_y <= canvasHeight + 20) {
const star_radius = starRadius * (focalDistance / star["z"] * 0.8);
const star_opacity = 1 - 0.8 * (star["z"] / focalDistance);
canvasContext.fillStyle = "rgba(" + star["color"] + ", " + star_opacity + ")";
canvasContext.shadowOffsetX = 0;
canvasContext.shadowOffsetY = 0;
canvasContext.shadowColor = "rgb(" + star["color"] + ")";
canvasContext.shadowBlur = 10;
canvasContext.beginPath();
canvasContext.arc(star_x, star_y, star_radius, 0, 2 * Math.PI);
canvasContext.fill();
} else {
const z = focalDistance * Math.random();
star["x"] = canvasWidth * (0.1 + 0.8 * Math.random());
star["y"] = canvasHeight * (0.1 + 0.8 * Math.random());
star["z"] = z;
star["color"] = starColorList[Math.ceil(Math.random() * 1000) % starColorList.length];
}
}
const self = this;
rAF = window.requestAnimationFrame(function () {
self.render();
});
粒子的动态变化是由于z值的变化而引起的,根据焦距与z的比值等于实时xx与初始xx比值计算出此时粒子的位置。
速度是根据粒子的z减少值确定,速度越快则z值变小得越快。
粒子是否在屏幕范围是根据粒子实时的x、y、z确定,x、y、z若不满足设定条件则判定此粒子已离开屏幕范围,需要重置粒子的属性。
粒子持续变化需要递推调用render方法,为什么不使用setInterval或setTimeout?使用这两个页面会存在卡顿,效果不好,而requestAnimationFrame
是一个浏览器的 API,用于创建平滑的动画效果。它允许你请求浏览器在下一次重绘之前调用指定的函数来更新动画。这比使用 setInterval
或 setTimeout
更高效,因为它会在屏幕刷新的时刻执行回调,从而确保动画的流畅性。requestAnimationFrame
的一个重要特性是,它会在页面不可见或者在后台标签页时暂停,这可以节省CPU资源。
window.cancelAnimationFrame(rAF);
starList = [];
canvasContext.clearRect(0, 0, canvasWidth, canvasHeight);
canvasElement.width = 0;
canvasElement.height = 0;
需要将starList清空释放内存,同时取消动画更新,释放资源。
setStarColorList: function (color, mode = false) {
if (typeof color === 'object') {
starColorList = color;
} else if (typeof color === 'string') {
starColorList.push(color);
}
if (mode) {
for (let i = 0; i < starList.length; i++) {
starList[i]["color"] = starColorList[Math.ceil(Math.random() * 1000) % starColorList.length];
}
}
}
此方法接受两个参数,第一个参数表示颜色,第二个参数表示是否立刻同步颜色。
setStarSpeedLevel: function (star_speed_level = 0.0005) {
starSpeedLevel = star_speed_level
}
演示地址
StarrySky