打造梦幻粒子动画效果:基于 Vue 的 Canvas 实现方案

粒子动画效果在现代网页设计中越来越受欢迎,它能为页面增添动态感和视觉吸引力。本文将分享一个基于 Vue 和 Canvas 实现的粒子动画组件,该组件具有高度可定制性,可轻松集成到各种 Web 项目中。

我们实现的粒子动画具有以下特点:

  • 粒子从底部向上飘动,模拟轻盈上升的效果
  • 粒子带有呼吸式发光效果,增强视觉层次感
  • 每个粒子都有随机的大小、速度和颜色
  • 支持响应式布局,自动适应容器大小变化
  • 所有参数均可通过 props 灵活配置

技术选择

为什么选择 Canvas 而非 DOM 元素来实现粒子效果?

  1. 性能优势:Canvas 在处理大量粒子时性能远优于 DOM 操作
  2. 绘制灵活性:Canvas 提供丰富的绘图 API,便于实现复杂的视觉效果
  3. 资源占用低:相比创建大量 DOM 节点,Canvas 渲染更高效

核心实现步骤

  1. 初始化 Canvas 并设置合适的尺寸
  2. 创建粒子类,定义粒子的属性和行为
  3. 实现粒子的绘制逻辑,包括发光效果
  4. 构建动画循环,更新粒子状态
  5. 添加响应式处理和组件生命周期管理

组件结构

vue 复制代码
<template>
  <div class="particle-container">
    <canvas ref="particleCanvas" class="particle-canvas"></canvas>
  </div>
</template>

模板部分非常简洁,只包含一个容器和 canvas 元素,canvas 将作为我们绘制粒子的画布。

可配置参数

javascript 复制代码
props: {
  // 粒子数量
  particleCount: {
    type: Number,
    default: 50,
    validator: (value) => value >= 0
  },
  // 粒子颜色数组
  particleColors: {
    type: Array,
    default: () => [
      'rgba(255, 255, 255,',    // 白色
      'rgba(153, 204, 255,',   // 淡蓝
      'rgba(255, 204, 255,',   // 淡粉
      'rgba(204, 255, 255,'    // 淡青
    ]
  },
  // 发光强度
  glowIntensity: {
    type: Number,
    default: 1.5
  },
  // 粒子大小控制参数
  minParticleSize: {
    type: Number,
    default: 0.5  // 最小粒子半径
  },
  maxParticleSize: {
    type: Number,
    default: 1.5  // 最大粒子半径
  }
}

这些参数允许开发者根据需求调整粒子效果的密度、颜色、大小和发光强度。

粒子创建与初始化

javascript 复制代码
createParticle() {
  // 根据传入的范围计算粒子半径
  const radius = this.minParticleSize + Math.random() * (this.maxParticleSize - this.minParticleSize)

  return {
    x: Math.random() * this.canvasWidth,
    y: this.canvasHeight + Math.random() * 50,
    radius,  // 使用新的半径范围
    color: this.getRandomColor(),
    speedY: Math.random() * 1.5 + 0.5,  // 垂直速度
    speedX: (Math.random() - 0.5) * 0.3,  // 水平漂移
    alpha: Math.random() * 0.5 + 0.5,
    life: Math.random() * 150 + 150,  // 生命周期
    glow: Math.random() * 0.8 + 0.2,
    glowSpeed: (Math.random() - 0.5) * 0.02,
    shadowBlur: radius * 3 + 1  // 阴影模糊与粒子大小成比例
  }
}

每个粒子都有随机的初始位置(从底部进入)、大小、速度和发光属性,这确保了动画效果的自然和丰富性。

动画循环

动画的核心是animate方法,它使用requestAnimationFrame创建流畅的动画循环:

javascript 复制代码
animate() {
  this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)

  this.particles.forEach((particle, index) => {
    // 更新粒子位置
    particle.y -= particle.speedY
    particle.x += particle.speedX
    particle.life--

    // 处理发光动画
    particle.glow += particle.glowSpeed
    if (particle.glow > 1.2) {
      particle.glow = 1.2
      particle.glowSpeed = -particle.glowSpeed
    } else if (particle.glow < 0.2) {
      particle.glow = 0.2
      particle.glowSpeed = -particle.glowSpeed
    }

    // 粒子生命周期结束,重新创建
    if (particle.y < -particle.radius || particle.life <= 0) {
      this.particles[index] = this.createParticle()
    }

    // 绘制粒子(包括发光效果、核心和高光)
    // ...绘制代码省略
  })

  this.animationId = requestAnimationFrame(this.animate)
}

在每次动画帧中,我们更新所有粒子的位置和状态,当粒子超出画布或生命周期结束时,会创建新的粒子替换它,从而实现循环不断的动画效果。

响应式处理

为了使粒子动画适应不同屏幕尺寸,我们添加了窗口大小变化的监听:

javascript 复制代码
handleResize() {
  this.initCanvas()
  this.particles = this.particles.map(() => this.createParticle())
}

当窗口大小改变时,我们重新初始化 Canvas 尺寸并重新创建所有粒子,确保动画始终充满整个容器

完整代码

vue 复制代码
<template>
  <div class="particle-container">
    <canvas ref="particleCanvas" class="particle-canvas"></canvas>
  </div>
</template>

<script>
export default {
  name: 'ParticleAnimation',
  props: {
    // 粒子数量
    particleCount: {
      type: Number,
      default: 50,
      validator: (value) => value >= 0
    },
    // 粒子颜色数组
    particleColors: {
      type: Array,
      default: () => [
        'rgba(255, 255, 255,',    // 白色
        'rgba(153, 204, 255,',   // 淡蓝
        'rgba(255, 204, 255,',   // 淡粉
        'rgba(204, 255, 255,'    // 淡青
      ]
    },
    // 发光强度
    glowIntensity: {
      type: Number,
      default: 1.5
    },
    // 粒子大小控制参数
    minParticleSize: {
      type: Number,
      default: 0.5  // 最小粒子半径
    },
    maxParticleSize: {
      type: Number,
      default: 1.5  // 最大粒子半径
    }
  },
  data() {
    return {
      canvas: null,
      ctx: null,
      particles: [],
      animationId: null,
      canvasWidth: 0,
      canvasHeight: 0
    }
  },
  watch: {
    particleCount(newVal) {
      this.particles = []
      this.initParticles(newVal)
    },
    particleColors: {
      deep: true,
      handler() {
        this.particles.forEach((particle, index) => {
          this.particles[index].color = this.getRandomColor()
        })
      }
    },
    // 监听粒子大小变化
    minParticleSize() {
      this.resetParticles()
    },
    maxParticleSize() {
      this.resetParticles()
    }
  },
  methods: {
    initCanvas() {
      this.canvas = this.$refs.particleCanvas
      this.ctx = this.canvas.getContext('2d')

      const container = this.canvas.parentElement
      this.canvasWidth = container.clientWidth
      this.canvasHeight = container.clientHeight
      this.canvas.width = this.canvasWidth
      this.canvas.height = this.canvasHeight
    },

    initParticles(count) {
      for (let i = 0; i < count; i++) {
        this.particles.push(this.createParticle())
      }
    },

    createParticle() {
      // 根据传入的范围计算粒子半径
      const radius = this.minParticleSize + Math.random() * (this.maxParticleSize - this.minParticleSize)

      return {
        x: Math.random() * this.canvasWidth,
        y: this.canvasHeight + Math.random() * 50,
        radius,  // 使用新的半径范围
        color: this.getRandomColor(),
        speedY: Math.random() * 1.5 + 0.5,  // 降低速度,配合小粒子
        speedX: (Math.random() - 0.5) * 0.3,  // 减少漂移
        alpha: Math.random() * 0.5 + 0.5,
        life: Math.random() * 150 + 150,  // 延长生命周期,让小粒子存在更久
        glow: Math.random() * 0.8 + 0.2,
        glowSpeed: (Math.random() - 0.5) * 0.02,
        shadowBlur: radius * 3 + 1  // 阴影模糊与粒子大小成比例
      }
    },

    getRandomColor() {
      if (this.particleColors.length === 0) {
        return 'rgba(255, 255, 255,'
      }
      return this.particleColors[Math.floor(Math.random() * this.particleColors.length)]
    },

    animate() {
      this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)

      this.particles.forEach((particle, index) => {
        particle.y -= particle.speedY
        particle.x += particle.speedX
        particle.life--

        // 闪亮动画
        particle.glow += particle.glowSpeed
        if (particle.glow > 1.2) {
          particle.glow = 1.2
          particle.glowSpeed = -particle.glowSpeed
        } else if (particle.glow < 0.2) {
          particle.glow = 0.2
          particle.glowSpeed = -particle.glowSpeed
        }

        if (particle.y < -particle.radius || particle.life <= 0) {
          this.particles[index] = this.createParticle()
        }

        // 绘制粒子(适配小粒子的比例)
        this.ctx.save()

        // 阴影效果
        this.ctx.shadowColor = `${particle.color}${particle.glow * this.glowIntensity})`
        this.ctx.shadowBlur = particle.shadowBlur * particle.glow
        this.ctx.shadowOffsetX = 0
        this.ctx.shadowOffsetY = 0

        // 外发光圈(按粒子大小比例缩放)
        this.ctx.beginPath()
        this.ctx.arc(particle.x, particle.y, particle.radius * (1 + particle.glow * 0.8), 0, Math.PI * 2)
        this.ctx.fillStyle = `${particle.color}${0.2 * particle.glow})`
        this.ctx.fill()

        // 粒子核心
        this.ctx.beginPath()
        this.ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2)
        this.ctx.fillStyle = `${particle.color}${particle.alpha + (particle.glow * 0.3)})`
        this.ctx.fill()

        // 高光点(适配小粒子)
        if (particle.glow > 0.8) {
          this.ctx.beginPath()
          const highlightSize = particle.radius * 0.3 * particle.glow
          this.ctx.arc(
            particle.x - particle.radius * 0.2,
            particle.y - particle.radius * 0.2,
            highlightSize,
            0,
            Math.PI * 2
          )
          this.ctx.fillStyle = `rgba(255, 255, 255, ${0.6 * particle.glow})`
          this.ctx.fill()
        }

        this.ctx.restore()
      })

      this.animationId = requestAnimationFrame(this.animate)
    },

    handleResize() {
      this.initCanvas()
      this.particles = this.particles.map(() => this.createParticle())
    },

    // 重置粒子大小
    resetParticles() {
      this.particles = this.particles.map(() => this.createParticle())
    }
  },
  mounted() {
    this.initCanvas()
    this.initParticles(this.particleCount)
    this.animate()
    window.addEventListener('resize', this.handleResize)
  },
  beforeDestroy() {
    cancelAnimationFrame(this.animationId)
    window.removeEventListener('resize', this.handleResize)
  }
}
</script>

<style scoped>
.particle-container {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
}

.particle-canvas {
  display: block;
  width: 100%;
  height: 100%;
}
</style>

使用方法

vue 复制代码
<template>
  <div class="page-container">
    <ParticleAnimation 
      :particle-count="80"
      :glow-intensity="2"
      :min-particle-size="0.8"
      :max-particle-size="2"
    />
    <!-- 其他内容 -->
  </div>
</template>

<script>
import ParticleAnimation from '@/components/ParticleAnimation.vue'

export default {
  components: {
    ParticleAnimation
  }
}
</script>

<style>
.page-container {
  width: 100vw;
  height: 100vh;
}
</style>
相关推荐
OpenTiny社区3 小时前
如何使用 TinyEditor 快速部署一个协同编辑器?
前端·vue.js
李剑一4 小时前
vite框架下大屏适配方案
前端·vue.js·响应式设计
胖虎2654 小时前
从零搭建 Vue3 富文本编辑器:基于 Quill可扩展方案
vue.js
濑户川5 小时前
Vue3 计算属性与监听器:computed、watch、watchEffect 用法解析
前端·javascript·vue.js
前端九哥5 小时前
我删光了项目里的 try-catch,老板:6
前端·vue.js
顽疲5 小时前
SpringBoot + Vue 集成阿里云OSS直传最佳实践
vue.js·spring boot·阿里云
一 乐6 小时前
车辆管理|校园车辆信息|基于SprinBoot+vue的校园车辆管理系统(源码+数据库+文档)
java·前端·数据库·vue.js·论文·毕设·车辆管理
百锦再6 小时前
Python、Java与Go:AI大模型时代的语言抉择
java·前端·vue.js·人工智能·python·go·1024程序员节
菩提树下的凡夫6 小时前
前端vue的开发流程
前端·javascript·vue.js