云计算百科
云计算领域专业知识百科平台

前端小练习 —— 霓虹弹幕雨 (JS 能在浏览器里放烟花!)

前言:初学 JS 时,你可能觉得它就是个 “表单验证工具人”—— 最多弹个 alert 框框。但今天这个案例会让你瞪大眼睛:这玩意儿居然能在浏览器里搞出 “赛博朋克烟花秀”?别眨眼,咱们这就开整! 想抄作业?直接拉到最后,完整代码给你备好,复制粘贴就能看效果!

先上效果图描述:想象一下 —— 漆黑的背景里,成千上万的彩色粒子像被按下播放键的弹幕,有的直线狂飙,有的螺旋绕圈,近处的粒子亮得晃眼,远处的拖着淡淡的霓虹尾巴,鼠标一拖还能 360° 转着看。就像把科幻电影里的飞船尾气搬进了浏览器,关键是:这居然是 JS 写的!

在开始之前,先得知道咱们要 “召唤” 这些粒子需要啥 “咒语”:

  • Three.js:3D 世界的 “包工头”,不用自己搬砖(写底层 3D 逻辑),它帮你把 WebGL 包装得明明白白。
  • WebGL:Three.js 背后的 “底层搬砖工”,负责在网页上画 3D 图形,咱们不用直接指挥它,但得知道有这么个狠角色。
  • 着色器:粒子的 “造型师”,控制每个点的大小、颜色、运动姿势,让它们别长得千篇一律。
  • 动画循环:就像给粒子上了 “永动机”,让它们一直动起来,不会卡成 PPT。
  • 响应式:窗口放大缩小?粒子们会自动调整队形,不会乱成一锅粥。

目录

1. 召唤 “工具人” 库

2. 搭个 “舞台”(场景和相机)

3. 请 “摄影师”(渲染器)

4. 让舞台 “伸缩自如”(窗口适配)

5. 给观众 “操控权”(控制器)

6. 给粒子 “编序号”(全局变量)

7. 生成第一批 “演员”(粒子数组)

8. 加更多 “群演”(补充粒子)

9. 给粒子 “穿衣服”(几何体和材质)

10. 让演员 “上台”(点云进场景)

11. 开始 “表演”(渲染循环)


1. 召唤 “工具人” 库

要搞事情,先把家伙事儿备齐:

import * as THREE from "https://cdn.skypack.dev/three@0.136.0"; // 3D包工头
import { OrbitControls } from "https://cdn.skypack.dev/three@0.136.0/examples/jsm/controls/OrbitControls"; // 观众操控器
console.clear(); // 清掉控制台的垃圾,免得影响装X

吐槽:就像做饭前先把锅碗瓢盆摆出来,这里先把 Three.js 和控制器 “请” 进来,顺便把控制台擦干净 —— 之前调试的console.log就像厨房垃圾,得清掉。

2. 搭个 “舞台”(场景和相机)

没有舞台,粒子们在哪儿表演?

let scene = new THREE.Scene(); // 建个舞台
scene.background = new THREE.Color(0x000000); // 舞台背景:纯黑,像电影院关灯
let camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 1000); // 架个相机
camera.position.set(0, 5, 30); // 相机位置:站在后排看舞台,不远不近

吐槽:背景设成纯黑,才能凸显粒子的霓虹色,就像黑夜里的萤火虫才显眼。相机位置很关键,太近了只能看见几个粒子的 “大脸”,太远了就成芝麻了。

3. 请 “摄影师”(渲染器)

舞台搭好了,得有人把画面拍出来给观众看:

let renderer = new THREE.WebGLRenderer({ antialias: true }); // 摄影师,带抗锯齿(画面更细腻)
renderer.setSize(innerWidth, innerHeight); // 照片尺寸:和窗口一样大
document.body.appendChild(renderer.domElement); // 把照片贴到网页上

吐槽:antialias: true就像给摄影师加了 “美颜滤镜”,粒子边缘不会毛毛糙糙。最后一步是把渲染结果 “钉” 在网页上 —— 不然观众看啥?

4. 让舞台 “伸缩自如”(窗口适配)

观众可能会放大窗口,舞台得跟着变:

window.addEventListener("resize", () => {
camera.aspect = innerWidth / innerHeight; // 相机镜头比例跟着窗口变
camera.updateProjectionMatrix(); // 相机“重调焦距”
renderer.setSize(innerWidth, innerHeight); // 摄影师换个尺寸拍
});

吐槽:就像电影院的幕布,观众把厅拉大了,幕布也得跟着撑大,不然画面会变形。这里的resize事件就是 “幕布调节器”。

5. 给观众 “操控权”(控制器)

总不能只看固定角度吧?得让观众自己转着看:

let controls = new OrbitControls(camera, renderer.domElement); // 给个遥控器
controls.enableDamping = true; // 拖动后慢慢停下,像带刹车的轮椅
controls.enableZoom = true; // 允许缩放,想看细节就凑近点

吐槽:enableDamping是个好东西,不然拖动鼠标一松手,画面 “嗖” 地飞出去,容易晕 3D。就像玩过山车加了缓停,友好!

6. 给粒子 “编序号”(全局变量)

这么多粒子,得有 “点名册” 和 “时间表”:

let globalData = { time: { value: 0 } }; // 全局计时器,给粒子看时间用
let speeds = []; // 每个粒子的“速度表”
let directions = []; // 每个粒子的“运动方向”

// 给粒子随机分配方向和速度
let setDirection = () => {
directions.push(
Math.random() * 2 – 1, // X方向:左(-1)到右(1)
Math.random() * 2 – 1, // Y方向:下(-1)到上(1)
Math.random() * 2 – 1, // Z方向:前(-1)到后(1)
Math.random() * 3 + 1 // 速度:1到4之间
);
};

吐槽:就像给每个学生发 “运动手环”,记录他们跑多快、往哪跑。Math.random() * 2 – 1这招很妙,直接把随机数从 0-1 变成 – 1 到 1,省得再算一遍。

7. 生成第一批 “演员”(粒子数组)

先来 5 万个粒子撑场面:

let particles = new Array(50000).fill().map(() => {
speeds.push(Math.random() * 0.8 + 0.2); // 大小:0.2到1
setDirection(); // 分配方向
// 随机位置:在一个球里分布
return new THREE.Vector3(
(Math.random() * 2 – 1) * 20, // X范围:-20到20
(Math.random() * 2 – 1) * 20, // Y范围:-20到20
(Math.random() * 2 – 1) * 20 // Z范围:-20到20
);
});

吐槽:5 万个粒子听起来多,但 Three.js 扛得住 —— 就像演唱会 5 万人,只要场地够大(浏览器性能够),就不会挤爆。Vector3是给每个粒子 “画坐标”,让它们别扎堆。

8. 加更多 “群演”(补充粒子)

再来 10 万个粒子,让场面更炸:

for (let i = 0; i < 100000; i++) {
let radius = Math.random() * 50 + 30; // 距离中心30到80的环
let angle = Math.random() * Math.PI * 2; // 绕圈角度
// 环形分布:像土星环一样
particles.push(new THREE.Vector3(
Math.cos(angle) * radius, // X坐标
(Math.random() * 2 – 1) * 10, // Y坐标:上下浮动
Math.sin(angle) * radius // Z坐标
));
speeds.push(Math.random() * 0.8 + 0.2);
setDirection();
}

吐槽:这批粒子是 “外围观众”,站在远处当背景。用极坐标算位置(cos和sin),比直接用 XYZ 简单 —— 就像算圆形广场上的座位,用角度比用横纵坐标方便。

9. 给粒子 “穿衣服”(几何体和材质)

粒子光有位置还不行,得有颜色和造型:

// 把粒子坐标装进“几何体”(相当于给演员定站位)
let geometry = new THREE.BufferGeometry().setFromPoints(particles);
// 给每个粒子贴“大小标签”和“方向标签”
geometry.setAttribute("speed", new THREE.Float32BufferAttribute(speeds, 1));
geometry.setAttribute("dir", new THREE.Float32BufferAttribute(directions, 4));

// 给粒子做“衣服”(材质)
let material = new THREE.PointsMaterial({
size: 0.3, // 基础大小
transparent: true, // 允许透明(才有渐变效果)
blending: THREE.AdditiveBlending, // 叠加发光(像霓虹灯)
onBeforeCompile: (shader) => { // 自定义“造型师”(着色器)
shader.uniforms.time = globalData.time; // 给造型师看时间
// 顶点着色器:控制粒子运动和大小
shader.vertexShader = `
uniform float time;
attribute float speed;
attribute vec4 dir;
varying vec3 vColor;
${shader.vertexShader}
`.replace(`gl_PointSize = size;`, `gl_PointSize = size * speed;`) // 大小乘速度(快的粒子更大)
.replace(`#include <begin_vertex>`, `
#include <begin_vertex>
// 按方向移动:时间越久,走得越远
transformed += vec3(dir.x, dir.y, dir.z) * dir.w * time;
`)
.replace(`#include <color_vertex>`, `
#include <color_vertex>
// 颜色渐变:从红到紫
float hue = mod(time * 0.1 + gl_Position.x * 0.01, 1.0);
vColor = hsl2rgb(hue, 0.8, 0.5); // HSL转RGB(方便调颜色)
`);

// 片元着色器:控制粒子边缘渐变
shader.fragmentShader = `
varying vec3 vColor;
${shader.fragmentShader}
`.replace(`vec4 diffuseColor = vec4( diffuse, opacity );`, `
// 中心亮,边缘暗(像光斑)
float distance = length(gl_PointCoord – 0.5);
vec4 diffuseColor = vec4(vColor, smoothstep(0.5, 0.1, distance));
`);

// 加个HSL转RGB的工具函数(着色器里没有自带)
shader.fragmentShader = `
vec3 hsl2rgb(float h, float s, float l) {
vec3 rgb = clamp(abs(mod(h * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) – 3.0) – 1.0, 0.0, 1.0);
return l + s * (rgb – 0.5) * (1.0 – abs(2.0 * l – 1.0));
}
${shader.fragmentShader}
`;
}
});

吐槽:这部分是 “核心魔法”!着色器就像给粒子 “定制动作和妆容”—— 让快的粒子更大,颜色随时间变色,边缘模糊像光斑。hsl2rgb是个小技巧,用 HSL 调颜色比 RGB 方便多了,随便改个 hue 值就能换整套色系。

10. 让演员 “上台”(点云进场景)

把穿好衣服的粒子们请上舞台:

let particleSystem = new THREE.Points(geometry, material); // 组合成“粒子群”
scene.add(particleSystem); // 上台!

吐槽:Points就像 “群演总管”,把所有粒子打包成一个对象,方便统一控制。加进场景后,它们就算正式 “登场” 了。

11. 开始 “表演”(渲染循环)

让粒子们动起来,永不停歇:

let clock = new THREE.Clock(); // 计时器
renderer.setAnimationLoop(() => {
controls.update(); // 刷新控制器(阻尼效果需要)
globalData.time.value = clock.getElapsedTime(); // 更新全局时间
particleSystem.rotation.y = clock.getElapsedTime() * 0.05; // 整体慢慢旋转
renderer.render(scene, camera); // 每帧拍一张照片
});

吐槽:setAnimationLoop是 “导演喊 Action”,让浏览器每帧都刷新画面。整体旋转是个小心机,就算观众不动鼠标,也能看到不同角度的效果。

完整代码(抄作业专用)

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>霓虹弹幕雨</title>
<style> body { margin: 0; overflow: hidden; } </style>
</head>
<body>
<script type="module">
import * as THREE from "https://cdn.skypack.dev/three@0.136.0";
import { OrbitControls } from "https://cdn.skypack.dev/three@0.136.0/examples/jsm/controls/OrbitControls";
console.clear();

// 1. 舞台和相机
let scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000);
let camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 1000);
camera.position.set(0, 5, 30);

// 2. 渲染器
let renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);

// 3. 窗口适配
window.addEventListener("resize", () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});

// 4. 控制器
let controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;

// 5. 粒子数据
let globalData = { time: { value: 0 } };
let speeds = [];
let directions = [];
let setDirection = () => {
directions.push(
Math.random() * 2 – 1,
Math.random() * 2 – 1,
Math.random() * 2 – 1,
Math.random() * 3 + 1
);
};

// 6. 生成粒子
let particles = new Array(50000).fill().map(() => {
speeds.push(Math.random() * 0.8 + 0.2);
setDirection();
return new THREE.Vector3(
(Math.random() * 2 – 1) * 20,
(Math.random() * 2 – 1) * 20,
(Math.random() * 2 – 1) * 20
);
});

// 7. 补充粒子
for (let i = 0; i < 100000; i++) {
let radius = Math.random() * 50 + 30;
let angle = Math.random() * Math.PI * 2;
particles.push(new THREE.Vector3(
Math.cos(angle) * radius,
(Math.random() * 2 – 1) * 10,
Math.sin(angle) * radius
));
speeds.push(Math.random() * 0.8 + 0.2);
setDirection();
}

// 8. 几何体和材质
let geometry = new THREE.BufferGeometry().setFromPoints(particles);
geometry.setAttribute("speed", new THREE.Float32BufferAttribute(speeds, 1));
geometry.setAttribute("dir", new THREE.Float32BufferAttribute(directions, 4));

// HSL转RGB的工具函数,将在着色器中使用
const hsl2rgbFunction = `
vec3 hsl2rgb(float h, float s, float l) {
vec3 rgb = clamp(abs(mod(h * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) – 3.0) – 1.0, 0.0, 1.0);
return l + s * (rgb – 0.5) * (1.0 – abs(2.0 * l – 1.0));
}
`;

let material = new THREE.PointsMaterial({
size: 0.3,
transparent: true,
blending: THREE.AdditiveBlending,
onBeforeCompile: (shader) => {
shader.uniforms.time = globalData.time;
// 顶点着色器:控制粒子运动和大小,现在包含hsl2rgb函数
shader.vertexShader = `
uniform float time;
attribute float speed;
attribute vec4 dir;
varying vec3 vColor;
${hsl2rgbFunction} // 在这里添加HSL转RGB函数
${shader.vertexShader}
`.replace(`gl_PointSize = size;`, `gl_PointSize = size * speed;`)
.replace(`#include <begin_vertex>`, `
#include <begin_vertex>
transformed += vec3(dir.x, dir.y, dir.z) * dir.w * time;
`)
.replace(`#include <color_vertex>`, `
#include <color_vertex>
float hue = mod(time * 0.1 + gl_Position.x * 0.01, 1.0);
vColor = hsl2rgb(hue, 0.8, 0.5);
`);

// 片元着色器:控制粒子边缘渐变
shader.fragmentShader = `
varying vec3 vColor;
${hsl2rgbFunction} // 在这里也添加HSL转RGB函数
${shader.fragmentShader}
`.replace(`vec4 diffuseColor = vec4( diffuse, opacity );`, `
float distance = length(gl_PointCoord – 0.5);
vec4 diffuseColor = vec4(vColor, smoothstep(0.5, 0.1, distance));
`);
}
});

// 9. 加入场景并循环
let particleSystem = new THREE.Points(geometry, material);
scene.add(particleSystem);

let clock = new THREE.Clock();
renderer.setAnimationLoop(() => {
controls.update();
globalData.time.value = clock.getElapsedTime();
particleSystem.rotation.y = clock.getElapsedTime() * 0.05;
renderer.render(scene, camera);
});
</script>
</body>
</html>

总结一下

其实核心就是:用 Three.js 搭 3D 环境,用着色器给粒子 “定制造型和动作”,再用动画循环让它们动起来。看起来复杂,但拆成一步一步,就像搭积木 —— 先拼底座(场景相机),再装零件(粒子),最后拧上马达(渲染循环)。

试试改改代码里的数字:比如把粒子数量改成 10 万,或者把颜色 HSL 的饱和度调低,看看会变成啥样。毕竟 JS 的乐趣就在于:你永远不知道改个数字能玩出什么新花样!

最后说一句:别再说 JS 只能做表单了,它能在浏览器里给你放一整晚的霓虹烟花 —— 这就是前端的浪漫啊!

赞(0)
未经允许不得转载:网硕互联帮助中心 » 前端小练习 —— 霓虹弹幕雨 (JS 能在浏览器里放烟花!)
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!