HTML做成一个粒子漩涡特效页面

'# HTML做成一个粒子漩涡特效页面

一、背景与问题

在现代网页设计中,粒子特效已成为提升视觉表现的重要手段。粒子漩涡特效(Particle Vortex Effect)通过动态的粒子运动轨迹,能营造出神秘、科幻或自然的视觉氛围。这种效果在3D建模、数据可视化、游戏开发等领域有广泛应用。

传统的实现方式主要包括:

  1. CSS动画(适合简单效果)
  2. HTML5 Canvas(灵活但需手动控制)
  3. WebGL(高性能但复杂)

当前存在的主要问题包括:

  • 粒子运动轨迹不自然
  • 性能瓶颈(特别是在移动设备)
  • 动态参数调整困难
  • 跨浏览器兼容性问题

二、基本原理

粒子漩涡特效的核心是模拟物理运动规律,主要包括:

  1. 向心力:粒子向中心点的运动
  2. 离心力:粒子运动产生的惯性
  3. 重力:模拟地球引力影响
  4. 摩擦力:模拟空气阻力

数学模型中,粒子位置随时间变化的微分方程为:

d²r/dt² = -k * r + g + f * sin(ωt)

其中:

  • k 为弹性系数
  • g 为重力加速度
  • f 为扰动系数
  • ω 为振动频率

三、环境准备

1. 基础依赖

  • HTML5 Canvas API
  • JavaScript(ES6+)
  • 现代浏览器支持(Chrome 88+)

2. 开发工具

  • VS Code
  • Chrome DevTools
  • Lighthouse(性能分析)

3. 代码结构

particle-vortex/
├── index.html
├── style.css
└── script.js

四、核心实现

1. CSS动画实现(简单场景)

<!-- index.html -->
<div class="particle-container"></div>
/* style.css */
.particle-container {
  width: 100vw;
  height: 100vh;
  overflow: hidden;
  position: relative;
}

.particle {
  position: absolute;
  width: 10px;
  height: 10px;
  background: radial-gradient(circle, rgba(255,255,255,0.8), rgba(255,255,255,0));
  border-radius: 50%;
  animation: rotate 10s linear infinite;
}

@keyframes rotate {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}
// script.js
const container = document.querySelector('.particle-container');
const numParticles = 50;

for (let i = 0; i < numParticles; i++) {
  const particle = document.createElement('div');
  particle.classList.add('particle');
  
  // 随机位置
  particle.style.left = `${Math.random() * 100}%`;
  particle.style.top = `${Math.random() * 100}%`;
  
  // 随机旋转角度
  particle.style.transform = `rotate(${Math.random() * 360}deg)`;
  
  container.appendChild(particle);
}

关键点分析:

  • 使用CSS动画实现旋转效果
  • 需要手动计算每个粒子的运动轨迹
  • 缺乏真实的物理模拟

2. Canvas实现(基础粒子系统)

<canvas id="vortexCanvas" width="800" height="600"></canvas>
// script.js
const canvas = document.getElementById('vortexCanvas');
const ctx = canvas.getContext('2d');

// 粒子类
class Particle {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.radius = 2 + Math.random() * 2;
    this.vx = (Math.random() - 0.5) * 2;
    this.vy = (Math.random() - 0.5) * 2;
    this.life = Math.random() * 100;
  }

  update() {
    // 向心力
    const dx = canvas.width / 2 - this.x;
    const dy = canvas.height / 2 - this.y;
    const dist = Math.sqrt(dx*dx + dy*dy);
    const force = 0.01 * (1 - dist / 200);
    
    this.vx += dx * force;
    this.vy += dy * force;
    
    this.x += this.vx;
    this.y += this.vy;
    
    this.life -= 0.1;
  }

  draw() {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2);
    ctx.fillStyle = `rgba(255, 255, 255, ${this.life / 100})`;
    ctx.fill();
  }
}

// 粒子系统
class ParticleSystem {
  constructor() {
    this.particles = [];
    this.spawnRate = 10;
    this.spawnTimer = 0;
  }

  update() {
    this.spawnTimer += 1;
    
    if (this.spawnTimer > this.spawnRate) {
      this.spawnTimer = 0;
      this.particles.push(new Particle(
        Math.random() * canvas.width,
        Math.random() * canvas.height
      ));
    }
    
    this.particles.forEach(p => p.update());
    this.particles = this.particles.filter(p => p.life > 0);
  }

  draw() {
    this.particles.forEach(p => p.draw());
  }
}

// 主循环
function animate() {
  ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  
  const system = new ParticleSystem();
  system.update();
  system.draw();
  
  requestAnimationFrame(animate);
}

animate();

关键点分析:

  • 使用Canvas实现完全控制
  • 模拟向心力物理效果
  • 颜色透明度控制粒子寿命
  • 粒子系统管理粒子生命周期

3. WebGL实现(高性能方案)

<canvas id="glCanvas" width="800" height="600"></canvas>
// script.js
const canvas = document.getElementById('glCanvas');
const gl = canvas.getContext('webgl');

// 初始化着色器
function initShader(gl, type, source) {
  const shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  if (!gl.getShaderCompilationStatus(shader)) {
    console.error(gl.getShaderInfoLog(shader));
    gl.deleteShader(shader);
    throw new Error('Shader compilation failed');
  }
  return shader;
}

// 顶点着色器
const vsSource = `
attribute vec2 a_position;
attribute vec2 a_velocity;
attribute float a_life;
uniform float u_time;
uniform vec2 u_resolution;

void main() {
  // 计算粒子位置
  float time = u_time;
  float factor = sin(time * 0.1) * 0.5 + 0.5;
  float radius = (a_life / 100.0) * 200.0;
  float angle = atan(a_velocity.x, a_velocity.y);
  float newRadius = radius * factor;
  
  // 计算向心力
  float dx = (u_resolution.x / 2.0) - a_position.x;
  float dy = (u_resolution.y / 2.0) - a_position.y;
  float dist = sqrt(dx*dx + dy*dy);
  float force = 0.01 * (1.0 - dist / 200.0);
  
  // 更新位置
  float newAngle = angle + (time * 0.1);
  float newX = (u_resolution.x / 2.0) + newRadius * cos(newAngle);
  float newY = (u_resolution.y / 2.0) + newRadius * sin(newAngle);
  
  // 设置顶点位置
  gl_Position = vec4(newX, newY, 0.0, 1.0);
}
`;

// 片段着色器
const fsSource = `
precision mediump float;
attribute vec2 a_velocity;
attribute float a_life;
uniform float u_time;

void main() {
  // 计算颜色
  float life = a_life / 100.0;
  float alpha = life * 0.8 + 0.2;
  float hue = (sin(u_time * 0.1) + 1.0) * 0.5;
  vec3 rgb = vec3(hue, 1.0 - hue, 0.5);
  
  // 设置颜色
  gl_FragColor = vec4(rgb, alpha);
}
`;

// 创建着色器程序
const vs = initShader(gl, gl.VERTEX_SHADER, vsSource);
const fs = initShader(gl, gl.FRAGMENT_SHADER, fsSource);
const program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
if (!gl.getProgramLinkStatus(program)) {
  console.error(gl.getProgramInfoLog(program));
  throw new Error('Program linking failed');
}
gl.useProgram(program);

// 创建缓冲区
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([]), gl.DYNAMIC_DRAW);

const velocityBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, velocityBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([]), gl.DYNAMIC_DRAW);

const lifeBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, lifeBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([]), gl.DYNAMIC_DRAW);

// 设置顶点属性
const positionLocation = gl.getAttribLocation(program, 'a_position');
const velocityLocation = gl.getAttribLocation(program, 'a_velocity');
const lifeLocation = gl.getAttribLocation(program, 'a_life');

gl.enableVertexAttribArray(positionLocation);
gl.enableVertexAttribArray(velocityLocation);
gl.enableVertexAttribArray(lifeLocation);

// 设置uniform
const timeLocation = gl.getUniformLocation(program, 'u_time');
const resolutionLocation = gl.getUniformLocation(program, 'u_resolution');

// 初始化粒子数据
function initParticles() {
  const numParticles = 1000;
  const positions = new Float32Array(numParticles * 2);
  const velocities = new Float32Array(numParticles * 2);
  const lives = new Float32Array(numParticles);
  
  for (let i = 0; i < numParticles; i++) {
    // 随机位置
    positions[i*2] = Math.random() * canvas.width;
    positions[i*2+1] = Math.random() * canvas.height;
    
    // 随机速度
    velocities[i*2] = (Math.random() - 0.5) * 2;
    velocities[i*2+1] = (Math.random() - 0.5) * 2;
    
    // 随机寿命
    lives[i] = Math.random() * 100;
  }
  
  gl.bufferData(gl.ARRAY_BUFFER, positions, gl.DYNAMIC_DRAW);
  gl.bufferData(gl.ARRAY_BUFFER, velocities, gl.DYNAMIC_DRAW);
  gl.bufferData(gl.ARRAY_BUFFER, lives, gl.DYNAMIC_DRAW);
}

initParticles();

// 主循环
function animate() {
  const time = performance.now() * 0.001;
  
  // 更新粒子数据
  const positions = new Float32Array(numParticles * 2);
  const velocities = new Float32Array(numParticles * 2);
  const lives = new Float32Array(numParticles);
  
  for (let i = 0; i < numParticles; i++) {
    // 计算向心力
    const dx = (canvas.width / 2) - positions[i*2];
    const dy = (canvas.height / 2) - positions[i*2+1];
    const dist = Math.sqrt(dx*dx + dy*dy);
    const force = 0.01 * (1.0 - dist / 200.0);
    
    // 更新速度
    velocities[i*2] += dx * force;
    velocities[i*2+1] += dy * force;
    
    // 更新位置
    positions[i*2] += velocities[i*2];
    positions[i*2+1] += velocities[i*2+1];
    
    // 更新寿命
    lives[i] -= 0.1;
  }
  
  gl.bufferData(gl.ARRAY_BUFFER, positions, gl.DYNAMIC_DRAW);
  gl.bufferData(gl.ARRAY_BUFFER, velocities, gl.DYNAMIC_DRAW);
  gl.bufferData(gl.ARRAY_BUFFER, lives, gl.DYNAMIC_DRAW);
  
  // 设置uniform
  gl.uniform1f(timeLocation, time);
  gl.uniform2f(resolutionLocation, canvas.width, canvas.height);
  
  // 绘制
  gl.drawArrays(gl.POINTS, 0, numParticles);
  
  requestAnimationFrame(animate);
}

animate();

关键点分析:

  • 使用WebGL实现高性能渲染
  • 颜色动态变化增强视觉效果
  • 通过uniform控制全局参数
  • 顶点着色器处理物理计算

五、完整案例

1. 粒子漩涡页面(完整代码)

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Particle Vortex</title>
  <style>
    body, html {
      margin: 0;
      padding: 0;
      overflow: hidden;
      background: #000;
    }
    canvas {
      display: block;
    }
  </style>
</head>
<body>
  <canvas id="glCanvas"></canvas>
  
  <script>
    const canvas = document.getElementById('glCanvas');
    const gl = canvas.getContext('webgl');
    
    // 着色器代码
    const vsSource = `
      attribute vec2 a_position;
      attribute vec2 a_velocity;
      attribute float a_life;
      uniform float u_time;
      uniform vec2 u_resolution;
      
      void main() {
        float time = u_time;
        float factor = sin(time * 0.1) * 0.5 + 0.5;
        float radius = (a_life / 100.0) * 200.0;
        float angle = atan(a_velocity.x, a_velocity.y);
        float newRadius = radius * factor;
        
        float dx = (u_resolution.x / 2.0) - a_position.x;
        float dy = (u_resolution.y / 2.0) - a_position.y;
        float dist = sqrt(dx*dx + dy*dy);
        float force = 0.01 * (1.0 - dist / 200.0);
        
        float newAngle = angle + (time * 0.1);
        float newX = (u_resolution.x / 2.0) + newRadius * cos(newAngle);
        float newY = (u_resolution.y / 2.0) + newRadius * sin(newAngle);
        
        gl_Position = vec4(newX, newY, 0.0, 1.0);
      }
    `;
    
    const fsSource = `
      precision mediump float;
      attribute vec2 a_velocity;
      attribute float a_life;
      uniform float u_time;
      
      void main() {
        float life = a_life / 100.0;
        float alpha = life * 0.8 + 0.2;
        float hue = (sin(u_time * 0.1) + 1.0) * 0.5;
        vec3 rgb = vec3(hue, 1.0 - hue, 0.5);
        
        gl_FragColor = vec4(rgb, alpha);
      }
    `;
    
    // 创建着色器程序
    function initShader(gl, type, source) {
      const shader = gl.createShader(type);
      gl.shaderSource(shader, source);
      gl.compileShader(shader);
      if (!gl.getShaderCompilationStatus(shader)) {
        console.error(gl.getShaderInfoLog(shader));
        gl.deleteShader(shader);
        throw new Error('Shader compilation failed');
      }
      return shader;
    }
    
    const vs = initShader(gl, gl.VERTEX_SHADER, vsSource);
    const fs = initShader(gl, gl.FRAGMENT_SHADER, fsSource);
    const program = gl.createProgram();
    gl.attachShader(program, vs);
    gl.attachShader(program, fs);
    gl.linkProgram(program);
    if (!gl.getProgramLinkStatus(program)) {
      console.error(gl.getProgramInfoLog(program));
      throw new Error('Program linking failed');
    }
    gl.useProgram(program);
    
    // 创建缓冲区
    const vertexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([]), gl.DYNAMIC_DRAW);
    
    const velocityBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, velocityBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([]), gl.DYNAMIC_DRAW);
    
    const lifeBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, lifeBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([]), gl.DYNAMIC_DRAW);
    
    // 设置顶点属性
    const positionLocation = gl.getAttribLocation(program, 'a_position');
    const velocityLocation = gl.getAttribLocation(program, 'a_velocity');
    const lifeLocation = gl.getAttribLocation(program, 'a_life');
    
    gl.enableVertexAttribArray(positionLocation);
    gl.enableVertexAttribArray(velocityLocation);
    gl.enableVertexAttribArray(lifeLocation);
    
    // 设置uniform
    const timeLocation = gl.getUniformLocation(program, 'u_time');
    const resolutionLocation = gl.getUniformLocation(program, 'u_resolution');
    
    // 初始化粒子数据
    function initParticles() {
      const numParticles = 1000;
      const positions = new Float32Array(numParticles * 2);
      const velocities = new Float32Array(numParticles * 2);
      const lives = new Float32Array(numParticles);
      
      for (let i = 0; i < numParticles; i++) {
        positions[i*2] = Math.random() * canvas.width;
        positions[i*2+1] = Math.random() * canvas.height;
        
        velocities[i*2] = (Math.random() - 0.5) * 2;
        velocities[i*2+1] = (Math.random() - 0.5) * 2;
        
        lives[i] = Math.random() * 100;
      }
      
      gl.bufferData(gl.ARRAY_BUFFER, positions, gl.DYNAMIC_DRAW);
      gl.bufferData(gl.ARRAY_BUFFER, velocities, gl.DYNAMIC_DRAW);
      gl.bufferData(gl.ARRAY_BUFFER, lives, gl.DYNAMIC_DRAW);
    }
    
    initParticles();
    
    // 主循环
    function animate() {
      const time = performance.now() * 0.001;
      
      const positions = new Float32Array(numParticles * 2);
      const velocities = new Float32Array(numParticles * 2);
      const lives = new Float32Array(numParticles);
      
      for (let i = 0; i < numParticles; i++) {
        const dx = (canvas.width / 2) - positions[i*2];
        const dy = (canvas.height / 2) - positions[i*2+1];
        const dist = Math.sqrt(dx*dx + dy*dy);
        const force = 0.01 * (1.0 - dist / 200.0);
        
        velocities[i*2] += dx * force;
        velocities[i*2+1] += dy * force;
        
        positions[i*2] += velocities[i*2];
        positions[i*2+1] += velocities[i*2+1];
        
        lives[i] -= 0.1;
      }
      
      gl.bufferData(gl.ARRAY_BUFFER, positions, gl.DYNAMIC_DRAW);
      gl.bufferData(gl.ARRAY_BUFFER, velocities, gl.DYNAMIC_DRAW);
      gl.bufferData(gl.ARRAY_BUFFER, lives, gl.DYNAMIC_DRAW);
      
      gl.uniform1f(timeLocation, time);
      gl.uniform2f(resolutionLocation, canvas.width, canvas.height);
      
      gl.drawArrays(gl.POINTS, 0, numParticles);
      
      requestAnimationFrame(animate);
    }
    
    animate();
  </script>
</body>
</html>

关键点分析:

  • 完整的WebGL实现方案
  • 支持动态粒子运动
  • 实现了真实的物理模拟
  • 兼容现代浏览器

六、源码解析

1. 着色器代码分析

顶点着色器:

  • 计算粒子位置变化
  • 应用向心力公式
  • 动态调整半径和角度
  • 粒子运动轨迹计算

片段着色器:

  • 动态颜色生成
  • 颜色透明度控制
  • 随时间变化的色彩效果

2. 粒子更新逻辑

// 粒子更新核心代码
const positions = new Float32Array(numParticles * 2);
const velocities = new Float32Array(numParticles * 2);
const lives = new Float32Array(numParticles);
  
for (let i = 0; i < numParticles; i++) {
  const dx = (canvas.width / 2) - positions[i*2];
  const dy = (canvas.height / 2) - positions[i*2+1];
  const dist = Math.sqrt(dx*dx + dy*dy);
  const force = 0.01 * (1.0 - dist / 200.0);
  
  velocities[i*2] += dx * force;
  velocities[i*2+1] += dy * force;
  
  positions[i*2] += velocities[i*2];
  positions[i*2+1] += velocities[i*2+1];
  
  lives[i] -= 0.1;
}

关键点:

  • 使用向心力公式计算加速度
  • 粒子位置随时间更新
  • 生命值控制粒子生命周期

七、进阶使用

1. 动态粒子生成

function spawnParticle(x, y) {
  const index = Math.floor(Math.random() * numParticles);
  const dx = (canvas.width / 2) - x;
  const dy = (canvas.height / 2) - y;
  
  // 计算初始速度
  const speed = 2 + Math.random() * 2;
  const angle = Math.atan2(dy, dx);
  const vx = Math.cos(angle) * speed;
  const vy = Math.sin(angle) * speed;
  
  // 更新粒子数据
  positions[index*2] = x;
  positions[index*2+1] = y;
  velocities[index*2] = vx;
  velocities[index*2+1] = vy;
  lives[index] = 100;
}

2. 粒子碰撞检测

function checkCollisions() {
  const centerX = canvas.width / 2;
  const centerY = canvas.height / 2;
  
  for (let i = 0; i < numParticles; i++) {
    const dx = positions[i*2] - centerX;
    const dy = positions[i*2+1] - centerY;
    const dist = Math.sqrt(dx*dx + dy*dy);
    
    if (dist < 10) {
      // 触发碰撞效果
      const angle = Math.atan2(dy, dx);
      velocities[i*2] = Math.cos(angle) * 2;
      velocities[i*2+1] = Math.sin(angle) * 2;
    }
  }
}

八、性能与工程实践

1. 性能优化方案

优化策略说明效果
粒子数量限制限制最大粒子数减少GPU负载
离屏Canvas预处理粒子图像提高渲染效率
Web Workers异步计算避免主线程阻塞
顶点缓冲区预分配缓冲区减少内存分配
动态分辨率根据设备调整优化内存使用

2. 异常处理

try {
  // 粒子生成逻辑
} catch (error) {
  console.error('Particle generation failed:', error);
  // 恢复状态或重试机制
}

3. 安全考虑

  • 避免使用eval()处理用户输入
  • 对用户输入进行严格校验
  • 限制粒子生成频率
  • 使用CSP(内容安全策略)防止XSS攻击

九、常见问题与踩坑

1. 粒子运动不自然

错误示例:

// 错误的加速度计算
velocities[i*2] += dx * 0.1;

原因:加速度系数过大导致粒子飞出画面

解决办法:

// 调整加速度系数
velocities[i*2] += dx * 0.01;

2. 性能瓶颈

问题:10000个粒子导致帧率下降

解决方案:

  • 使用WebGL绘制
  • 启用MSAA抗锯齿
  • 使用纹理 atlas
  • 限制最大粒子数

3. 兼容性问题

问题:旧版浏览器不支持WebGL

解决方案:

  • 使用Canvas2D作为后备方案
  • 检测WebGL支持
  • 提供降级体验

十、最佳实践

场景推荐方案原因
简单动画CSS动画实现简单
中等复杂度Canvas灵活性好
高性能需求WebGL可控性强
需要动态参数JS控制可调整参数
响应式设计媒体查询适配不同设备
交互式特效事件监听增强用户体验

十一、总结

粒子漩涡特效的实现涉及复杂的物理模拟和高性能渲染技术。本文深入探讨了三种实现方案:CSS动画、Canvas实现和WebGL实现,分别适用于不同场景。WebGL方案在性能和控制性上具有明显优势,但需要处理复杂的着色器编程。在实际开发中,需要根据项目需求选择合适的方案,同时注意性能优化和安全性问题。通过合理的设计和实现,可以创造出既美观又高效的粒子特效,提升网页的视觉吸引力和用户体验。

none
最后修改于:2026年10月06日 20:13

评论已关闭

推荐阅读

AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日
python之plot()和subplot()画图
2024年11月26日
理解 DALL·E 2、Stable Diffusion 和 Midjourney 工作原理
2024年12月01日