Harmony Flutter 跨平台开发实战:鸿蒙与音乐律动艺术、粒子系统与流体模拟:动态粒子的视觉盛宴

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net


🌌 一、粒子系统基础

📚 1.1 粒子系统概述

粒子系统(Particle System)是一种计算机图形技术,通过大量简单元素的组合来模拟复杂现象。它最初由 William T. Reeves 于 1983 年提出,用于电影《星际迷航II:可汗之怒》中的火焰效果。

粒子系统的核心概念

复制代码
粒子系统的基本组成:

1. 粒子(Particle)
   - 位置(Position)
   - 速度(Velocity)
   - 加速度(Acceleration)
   - 生命周期(Lifespan)
   - 颜色/透明度(Color/Alpha)
   - 大小(Size)

2. 发射器(Emitter)
   - 发射位置
   - 发射速率
   - 发射方向
   - 初始速度范围

3. 更新器(Updater)
   - 物理模拟
   - 生命周期管理
   - 碰撞检测

4. 渲染器(Renderer)
   - 绘制方式
   - 纹理/颜色
   - 混合模式

粒子系统应用场景

复制代码
粒子系统可以模拟的现象:

自然现象:
- 火焰与烟雾
- 雨雪天气
- 瀑布与喷泉
- 星空与流星

特效效果:
- 爆炸与碎片
- 魔法光效
- 烟花绽放
- 闪电与电弧

音乐可视化:
- 音频粒子喷泉
- 节奏脉冲波
- 频谱粒子云
- 声波涟漪

交互效果:
- 触摸反馈
- 拖尾效果
- 磁场吸引
- 流体交互

🔬 1.2 粒子物理基础

牛顿运动定律在粒子系统中的应用

复制代码
基本运动方程:

位置更新:
position = position + velocity * dt

速度更新:
velocity = velocity + acceleration * dt

加速度计算:
acceleration = force / mass

常见力的计算:

1. 重力
   F = m * g
   g ≈ 9.8 m/s²(向下)

2. 阻力
   F = -k * velocity
   k 是阻力系数

3. 弹簧力(胡克定律)
   F = -k * (position - restPosition)
   k 是弹性系数

4. 风力
   F = windDirection * windStrength

5. 湍流
   F = noise(position) * turbulenceStrength

粒子生命周期管理

复制代码
粒子生命周期状态:

    ┌──────────┐
    │   出生   │ ← 发射器创建
    └────┬─────┘
         ↓
    ┌──────────┐
    │   存活   │ ← 更新物理、渲染
    └────┬─────┘
         ↓
    ┌──────────┐
    │   衰老   │ ← 透明度/大小变化
    └────┬─────┘
         ↓
    ┌──────────┐
    │   死亡   │ ← 回收或删除
    └──────────┘

生命周期参数:
- 初始生命值(Initial Life)
- 当前生命值(Current Life)
- 衰减速率(Decay Rate)
- 死亡条件(Death Condition)

🎨 1.3 粒子渲染技术

基本渲染方式

复制代码
粒子渲染方法:

1. 点精灵(Point Sprites)
   - 每个粒子一个点
   - GPU 自动扩展为正方形
   - 高效但功能有限

2. 四边形(Quads)
   - 每个粒子两个三角形
   - 支持旋转和拉伸
   - 更灵活但开销更大

3. 实例化渲染(Instanced Rendering)
   - 一次绘制调用渲染所有粒子
   - 最高效的现代方法
   - 需要着色器支持

4. 几何着色器(Geometry Shader)
   - 在 GPU 上生成粒子几何
   - 灵活但性能开销大
   - 适合动态形状

混合模式

复制代码
常用混合模式:

1. 加法混合(Additive)
   最终颜色 = 源颜色 + 目标颜色
   效果:发光、火焰、光效
   公式:src + dst

2. Alpha 混合(Alpha Blending)
   最终颜色 = src * alpha + dst * (1 - alpha)
   效果:透明、烟雾、云
   公式:src * src.a + dst * (1 - src.a)

3. 乘法混合(Multiplicative)
   最终颜色 = 源颜色 * 目标颜色
   效果:阴影、变暗
   公式:src * dst

4. 屏幕混合(Screen)
   最终颜色 = 1 - (1-src) * (1-dst)
   效果:明亮叠加
   公式:1 - (1-src) * (1-dst)

🌊 二、流体模拟基础

💧 2.1 流体动力学概述

流体运动的基本方程

复制代码
纳维-斯托克斯方程(Navier-Stokes):

∂v/∂t + (v·∇)v = -∇p/ρ + ν∇²v + f

其中:
- v:速度场
- p:压力
- ρ:密度
- ν:粘度
- f:外力

简化理解:
- 左边:流体加速度
- 右边:压力梯度 + 粘性力 + 外力

在粒子系统中的简化:
1. 欧拉方法(网格法)
   - 固定网格计算流体属性
   - 适合烟雾、火焰

2. 拉格朗日方法(粒子法)
   - 粒子随流体移动
   - 适合水、液体

3. SPH(光滑粒子流体动力学)
   - 粒子间相互作用
   - 平衡精度与性能

SPH 流体模拟原理

复制代码
SPH(Smoothed Particle Hydrodynamics):

核心思想:
- 流体由粒子组成
- 粒子间通过核函数相互作用
- 计算密度、压力、粘性力

密度计算:
ρᵢ = Σⱼ mⱼ W(rᵢ - rⱼ, h)

其中:
- ρᵢ:粒子 i 的密度
- mⱼ:粒子 j 的质量
- W:核函数
- h:光滑长度

核函数(Smoothing Kernel):
常用的 Poly6 核:
W(r, h) = 315 / (64πh⁹) * (h² - |r|²)³  当 |r| ≤ h

压力计算:
p = k(ρ - ρ₀)
其中 k 是刚度系数,ρ₀ 是静止密度

压力力:
Fᵢᵖʳᵉˢˢᵘʳᵉ = -Σⱼ mⱼ (pᵢ + pⱼ) / (2ρⱼ) ∇W

粘性力:
Fᵢᵛⁱˢᶜ = μ Σⱼ mⱼ (vⱼ - vᵢ) / ρⱼ ∇²W

🌀 2.2 涡旋与湍流

涡旋运动

复制代码
涡旋(Vortex)特征:

1. 涡度(Vorticity)
   ω = ∇ × v
   描述流体旋转程度

2. 环量(Circulation)
   Γ = ∮ v · dl
   沿闭合路径的速度积分

3. 兰金涡旋模型
   内核(刚体旋转):
   vᵣ = Γr / (2πr₀²)  当 r ≤ r₀
   
   外核(自由涡):
   vᵣ = Γ / (2πr)  当 r > r₀

粒子涡旋实现:
- 创建涡旋中心点
- 计算粒子到中心距离
- 应用切向速度
- 添加衰减效果

湍流模拟

复制代码
湍流(Turbulence)模拟方法:

1. Perlin 噪声湍流
   - 多层噪声叠加
   - 不同频率和振幅
   - 适合烟雾、云

2. Curl 噪声
   - 无散度的速度场
   - 自然旋转效果
   - 适合流体、火焰

3. 简化湍流模型
   turbulence = noise(position * frequency) * amplitude
   velocity += turbulence * dt

4. 科尔莫哥罗夫尺度
   - 能量级联理论
   - 大涡旋传递能量给小涡旋
   - 最小尺度耗散能量

🎵 三、音频驱动的粒子系统

🎼 3.1 音频特征到粒子参数的映射

参数映射策略

复制代码
音频特征 → 粒子参数映射:

1. 能量(Energy)
   → 发射速率:能量高 → 更多粒子
   → 粒子大小:能量高 → 更大粒子
   → 粒子速度:能量高 → 更快粒子

2. 低频(Bass)
   → 粒子位置:低频强 → 向下偏移
   → 重力方向:低频强 → 重力增强
   → 粒子颜色:低频强 → 红色系

3. 中频(Mid)
   → 发射角度:中频强 → 扩散角度大
   → 涡旋强度:中频强 → 涡旋增强
   → 粒子颜色:中频强 → 绿色系

4. 高频(Treble)
   → 粒子数量:高频强 → 更多小粒子
   → 闪烁频率:高频强 → 闪烁加快
   → 粒子颜色:高频强 → 蓝色系

5. 节拍(Beat)
   → 爆发效果:节拍 → 粒子爆发
   → 脉冲波:节拍 → 波纹扩散
   → 颜色闪烁:节拍 → 颜色变化

实时音频处理流程

复制代码
音频粒子系统流程:

┌─────────────┐
│  音频输入   │
└──────┬──────┘
       ↓
┌─────────────┐
│  FFT 分析   │ → 频谱数据
└──────┬──────┘
       ↓
┌─────────────┐
│  特征提取   │ → 能量、频率、节拍
└──────┬──────┘
       ↓
┌─────────────┐
│  参数映射   │ → 粒子系统参数
└──────┬──────┘
       ↓
┌─────────────┐
│  粒子更新   │ → 物理模拟
└──────┬──────┘
       ↓
┌─────────────┐
│  渲染输出   │ → 视觉效果
└─────────────┘

🎚️ 3.2 节拍驱动的粒子爆发

节拍检测与粒子爆发

复制代码
节拍检测算法:

1. 能量差异法
   beat = (currentEnergy - averageEnergy) > threshold

2. 频谱通量法
   flux = Σ |spectrum[t] - spectrum[t-1]|
   beat = flux > threshold

3. 自适应阈值
   threshold = averageFlux + sensitivity * stdDev

粒子爆发效果:

爆发参数:
- 爆发数量:50-500 粒子
- 初始速度:随机方向 + 向外
- 生命周期:0.5-2 秒
- 颜色渐变:亮色 → 暗色

爆发模式:
1. 球形爆发
   方向 = normalize(randomPoint - center)
   速度 = random(minSpeed, maxSpeed)

2. 环形爆发
   角度 = random(0, 2π)
   方向 = (cos(angle), sin(angle))

3. 定向爆发
   方向 = beatDirection + randomSpread

🔧 四、Dart/Flutter 中的粒子系统实现

🌟 4.1 基础粒子类

dart 复制代码
import 'dart:math';
import 'dart:typed_data';

class Particle {
  double x;
  double y;
  double vx;
  double vy;
  double ax;
  double ay;
  double life;
  double maxLife;
  double size;
  double rotation;
  double rotationSpeed;
  double hue;
  double saturation;
  double brightness;
  double alpha;
  
  Particle({
    required this.x,
    required this.y,
    this.vx = 0,
    this.vy = 0,
    this.ax = 0,
    this.ay = 0,
    required this.life,
    required this.maxLife,
    this.size = 5,
    this.rotation = 0,
    this.rotationSpeed = 0,
    this.hue = 0,
    this.saturation = 1,
    this.brightness = 1,
    this.alpha = 1,
  });
  
  bool get isDead => life <= 0;
  
  double get lifeRatio => life / maxLife;
  
  void update(double dt) {
    vx += ax * dt;
    vy += ay * dt;
    x += vx * dt;
    y += vy * dt;
    rotation += rotationSpeed * dt;
    life -= dt;
    alpha = lifeRatio.clamp(0.0, 1.0);
  }
  
  void applyForce(double fx, double fy) {
    ax += fx;
    ay += fy;
  }
  
  void applyGravity(double g) {
    ay += g;
  }
  
  void applyDrag(double drag) {
    vx *= (1 - drag);
    vy *= (1 - drag);
  }
}

🎯 4.2 粒子发射器

dart 复制代码
class ParticleEmitter {
  double x;
  double y;
  double emitRate;
  double emitCount;
  double minSpeed;
  double maxSpeed;
  double minAngle;
  double maxAngle;
  double minLife;
  double maxLife;
  double minSize;
  double maxSize;
  double gravity;
  double drag;
  bool isEmitting;
  
  final List<Particle> particles = [];
  double _accumulator = 0;
  final Random _random = Random();
  
  ParticleEmitter({
    required this.x,
    required this.y,
    this.emitRate = 10,
    this.emitCount = 1,
    this.minSpeed = 50,
    this.maxSpeed = 100,
    this.minAngle = 0,
    this.maxAngle = 2 * pi,
    this.minLife = 1,
    this.maxLife = 3,
    this.minSize = 3,
    this.maxSize = 8,
    this.gravity = 98,
    this.drag = 0.01,
    this.isEmitting = true,
  });
  
  void update(double dt) {
    _accumulator += dt;
    
    if (isEmitting) {
      final particlesToEmit = (_accumulator * emitRate).floor();
      _accumulator -= particlesToEmit / emitRate;
      
      for (int i = 0; i < particlesToEmit * emitCount; i++) {
        particles.add(_emitParticle());
      }
    }
    
    for (final particle in particles) {
      particle.applyGravity(gravity);
      particle.applyDrag(drag);
      particle.update(dt);
    }
    
    particles.removeWhere((p) => p.isDead);
  }
  
  Particle _emitParticle() {
    final speed = minSpeed + _random.nextDouble() * (maxSpeed - minSpeed);
    final angle = minAngle + _random.nextDouble() * (maxAngle - minAngle);
    final life = minLife + _random.nextDouble() * (maxLife - minLife);
    final size = minSize + _random.nextDouble() * (maxSize - minSize);
    
    return Particle(
      x: x,
      y: y,
      vx: speed * cos(angle),
      vy: speed * sin(angle),
      life: life,
      maxLife: life,
      size: size,
      hue: _random.nextDouble() * 360,
    );
  }
  
  void burst(int count) {
    for (int i = 0; i < count; i++) {
      particles.add(_emitParticle());
    }
  }
  
  void clear() {
    particles.clear();
  }
}

🌊 4.3 流体粒子系统

dart 复制代码
class FluidParticle extends Particle {
  double density = 0;
  double pressure = 0;
  double fx = 0;
  double fy = 0;
  
  FluidParticle({
    required super.x,
    required super.y,
    super.vx,
    super.vy,
    super.life,
    super.maxLife,
  });
}

class SPHFluidSimulator {
  final List<FluidParticle> particles = [];
  final double smoothingRadius;
  final double stiffness;
  final double viscosity;
  final double restDensity;
  final double particleMass;
  
  SPHFluidSimulator({
    this.smoothingRadius = 20,
    this.stiffness = 1000,
    this.viscosity = 0.1,
    this.restDensity = 1,
    this.particleMass = 1,
  });
  
  void addParticle(double x, double y) {
    particles.add(FluidParticle(x: x, y: y, life: 100, maxLife: 100));
  }
  
  void update(double dt) {
    _computeDensity();
    _computePressure();
    _computeForces();
    _integrate(dt);
  }
  
  void _computeDensity() {
    for (final p in particles) {
      p.density = 0;
      for (final q in particles) {
        final dx = p.x - q.x;
        final dy = p.y - q.y;
        final r2 = dx * dx + dy * dy;
        if (r2 < smoothingRadius * smoothingRadius) {
          final r = sqrt(r2);
          p.density += particleMass * _poly6Kernel(r, smoothingRadius);
        }
      }
    }
  }
  
  void _computePressure() {
    for (final p in particles) {
      p.pressure = stiffness * (p.density - restDensity);
    }
  }
  
  void _computeForces() {
    for (final p in particles) {
      p.fx = 0;
      p.fy = 0;
      
      for (final q in particles) {
        if (p == q) continue;
        
        final dx = q.x - p.x;
        final dy = q.y - p.y;
        final r = sqrt(dx * dx + dy * dy);
        
        if (r < smoothingRadius && r > 0.001) {
          final pressureForce = -particleMass * (p.pressure + q.pressure) / 
              (2 * q.density) * _spikyGradient(r, smoothingRadius);
          
          p.fx += pressureForce * dx / r;
          p.fy += pressureForce * dy / r;
          
          final viscForce = viscosity * particleMass * 
              ((q.vx - p.vx) * dx + (q.vy - p.vy) * dy) / 
              (q.density * r) * _viscosityLaplacian(r, smoothingRadius);
          
          p.fx += viscForce * dx / r;
          p.fy += viscForce * dy / r;
        }
      }
    }
  }
  
  void _integrate(double dt) {
    for (final p in particles) {
      if (p.density > 0.001) {
        p.vx += p.fx / p.density * dt;
        p.vy += p.fy / p.density * dt;
      }
      p.x += p.vx * dt;
      p.y += p.vy * dt;
    }
  }
  
  double _poly6Kernel(double r, double h) {
    if (r >= h) return 0;
    final factor = 315 / (64 * pi * pow(h, 9));
    return factor * pow(h * h - r * r, 3);
  }
  
  double _spikyGradient(double r, double h) {
    if (r >= h) return 0;
    final factor = -45 / (pi * pow(h, 6));
    return factor * pow(h - r, 2);
  }
  
  double _viscosityLaplacian(double r, double h) {
    if (r >= h) return 0;
    final factor = 45 / (pi * pow(h, 6));
    return factor * (h - r);
  }
}

💻 五、完整代码实现

dart 复制代码
import 'package:flutter/material.dart';
import 'package:just_audio_ohos/just_audio_ohos.dart';
import 'package:audio_session/audio_session.dart';
import 'dart:math';
import 'dart:typed_data';

void main() {
  runApp(const ParticleApp());
}

class ParticleApp extends StatelessWidget {
  const ParticleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '粒子系统',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.orange, brightness: Brightness.dark),
        useMaterial3: true,
      ),
      home: const ParticleHomePage(),
      debugShowCheckedModeBanner: false,
    );
  }
}

class ParticleHomePage extends StatelessWidget {
  const ParticleHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('粒子系统'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          _buildCard(context, title: '基础粒子', description: '简单粒子发射器', icon: Icons.grain, color: Colors.orange,
              onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const BasicParticleDemo()))),
          _buildCard(context, title: '音乐粒子', description: '音频驱动的粒子系统', icon: Icons.music_note, color: Colors.deepOrange,
              onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const MusicParticleDemo()))),
          _buildCard(context, title: '烟花效果', description: '节拍驱动的烟花', icon: Icons.celebration, color: Colors.pink,
              onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const FireworkDemo()))),
          _buildCard(context, title: '喷泉效果', description: '重力驱动的喷泉', icon: Icons.water_drop, color: Colors.blue,
              onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const FountainDemo()))),
          _buildCard(context, title: '涡旋效果', description: '粒子涡旋运动', icon: Icons.cyclone, color: Colors.purple,
              onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const VortexDemo()))),
          _buildCard(context, title: '流体模拟', description: 'SPH 流体粒子', icon: Icons.waves, color: Colors.cyan,
              onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const FluidDemo()))),
          _buildCard(context, title: '触摸粒子', description: '交互式粒子效果', icon: Icons.touch_app, color: Colors.green,
              onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const TouchParticleDemo()))),
          _buildCard(context, title: '星系模拟', description: '引力粒子系统', icon: Icons.public, color: Colors.indigo,
              onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const GalaxyDemo()))),
        ],
      ),
    );
  }

  Widget _buildCard(BuildContext context, {required String title, required String description, required IconData icon, 
      required Color color, required VoidCallback onTap}) {
    return Card(
      margin: const EdgeInsets.only(bottom: 12),
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
      child: InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(16),
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Row(children: [
            Container(width: 56, height: 56, decoration: BoxDecoration(color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(12)),
                child: Icon(icon, color: color, size: 28)),
            const SizedBox(width: 16),
            Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
              Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
              const SizedBox(height: 4),
              Text(description, style: TextStyle(color: Colors.grey[600], fontSize: 14)),
            ])),
            Icon(Icons.chevron_right, color: Colors.grey[400]),
          ]),
        ),
      ),
    );
  }
}

class Particle {
  double x, y, vx, vy, ax, ay;
  double life, maxLife, size;
  double hue, alpha;
  
  Particle({required this.x, required this.y, this.vx = 0, this.vy = 0, this.ax = 0, this.ay = 0,
      required this.life, required this.maxLife, this.size = 5, this.hue = 0, this.alpha = 1});
  
  bool get isDead => life <= 0;
  double get lifeRatio => (life / maxLife).clamp(0.0, 1.0);
  
  void update(double dt, {double gravity = 0, double drag = 0}) {
    ax += 0;
    ay += gravity;
    vx = (vx + ax * dt) * (1 - drag);
    vy = (vy + ay * dt) * (1 - drag);
    x += vx * dt;
    y += vy * dt;
    life -= dt;
    alpha = lifeRatio;
    ax = 0;
    ay = 0;
  }
}

class BasicParticleDemo extends StatefulWidget {
  const BasicParticleDemo({super.key});
  @override
  State<BasicParticleDemo> createState() => _BasicParticleDemoState();
}

class _BasicParticleDemoState extends State<BasicParticleDemo> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  final List<Particle> _particles = [];
  final Random _random = Random();
  double _emitRate = 50;
  double _gravity = 200;
  double _speed = 150;
  double _time = 0;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 16))..repeat();
    _controller.addListener(_update);
  }
  
  void _update() {
    final dt = 0.016;
    _time += dt;
    
    for (int i = 0; i < _emitRate * dt; i++) {
      _particles.add(Particle(
        x: 200 + _random.nextDouble() * 10 - 5,
        y: 100,
        vx: (_random.nextDouble() - 0.5) * _speed,
        vy: _random.nextDouble() * _speed * 0.5 + 50,
        life: 2 + _random.nextDouble() * 2,
        maxLife: 4,
        size: 3 + _random.nextDouble() * 5,
        hue: _random.nextDouble() * 60,
      ));
    }
    
    for (final p in _particles) {
      p.update(dt, gravity: _gravity, drag: 0.01);
    }
    
    _particles.removeWhere((p) => p.isDead);
    setState(() {});
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('基础粒子')),
      body: Stack(children: [
        CustomPaint(painter: ParticlePainter(_particles, _time), size: Size.infinite),
        Positioned(bottom: 20, left: 20, right: 20, child: _buildControls()),
      ]),
    );
  }
  
  Widget _buildControls() {
    return Container(
      padding: const EdgeInsets.all(12),
      decoration: BoxDecoration(color: Colors.black.withOpacity(0.7), borderRadius: BorderRadius.circular(12)),
      child: Column(children: [
        Row(children: [
          Expanded(child: Column(children: [
            Text('发射率: ${_emitRate.toStringAsFixed(0)}', style: const TextStyle(color: Colors.white70, fontSize: 11)),
            Slider(value: _emitRate, min: 10, max: 200, onChanged: (v) => setState(() => _emitRate = v), activeColor: Colors.orange),
          ])),
          Expanded(child: Column(children: [
            Text('重力: ${_gravity.toStringAsFixed(0)}', style: const TextStyle(color: Colors.white70, fontSize: 11)),
            Slider(value: _gravity, min: 0, max: 500, onChanged: (v) => setState(() => _gravity = v), activeColor: Colors.orange),
          ])),
        ]),
        Text('速度: ${_speed.toStringAsFixed(0)}', style: const TextStyle(color: Colors.white70, fontSize: 11)),
        Slider(value: _speed, min: 50, max: 300, onChanged: (v) => setState(() => _speed = v), activeColor: Colors.orange),
      ]),
    );
  }
}

class ParticlePainter extends CustomPainter {
  final List<Particle> particles;
  final double time;
  
  ParticlePainter(this.particles, this.time);
  
  @override
  void paint(Canvas canvas, Size size) {
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = const Color(0xFF0a0a15));
    
    for (final p in particles) {
      final hue = (p.hue + time * 20) % 360;
      canvas.drawCircle(Offset(p.x, p.y), p.size * p.lifeRatio, 
          Paint()..color = HSVColor.fromAHSV(p.alpha * 0.8, hue, 0.9, 1).toColor());
    }
  }
  
  @override
  bool shouldRepaint(covariant ParticlePainter old) => true;
}

class MusicParticleDemo extends StatefulWidget {
  const MusicParticleDemo({super.key});
  @override
  State<MusicParticleDemo> createState() => _MusicParticleDemoState();
}

class _MusicParticleDemoState extends State<MusicParticleDemo> with TickerProviderStateMixin {
  late AnimationController _animController;
  late AudioPlayer _audioPlayer;
  final List<Particle> _particles = [];
  final Random _random = Random();
  
  bool _isPlaying = false;
  Float32List _audioData = Float32List(64);
  double _energy = 0, _bass = 0, _mid = 0, _treble = 0;
  double _time = 0;
  
  static const String _audioUrl = 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3';

  @override
  void initState() {
    super.initState();
    _initAudio();
    _animController = AnimationController(vsync: this, duration: const Duration(milliseconds: 33))..repeat();
    _animController.addListener(_update);
  }
  
  Future<void> _initAudio() async {
    _audioPlayer = AudioPlayer();
    final session = await AudioSession.instance;
    await session.configure(const AudioSessionConfiguration.music());
    _audioPlayer.playerStateStream.listen((s) => setState(() => _isPlaying = s.playing));
    try { await _audioPlayer.setUrl(_audioUrl); } catch (e) { debugPrint('加载失败: $e'); }
  }
  
  void _update() {
    final dt = 0.033;
    _time += dt;
    
    for (int i = 0; i < 64; i++) {
      if (_isPlaying) {
        final freq = (i / 64) * 8 + 1;
        _audioData[i] = _audioData[i] * 0.85 + (sin(_time * freq) * 0.5 + 0.5) * 0.15;
      } else {
        _audioData[i] *= 0.95;
      }
    }
    
    double total = 0, bassE = 0, midE = 0, trebleE = 0;
    for (int i = 0; i < 64; i++) {
      total += _audioData[i];
      if (i < 16) bassE += _audioData[i];
      else if (i < 40) midE += _audioData[i];
      else trebleE += _audioData[i];
    }
    _energy = total / 64;
    _bass = bassE / 16;
    _mid = midE / 24;
    _treble = trebleE / 24;
    
    final emitCount = (10 + _energy * 50).toInt();
    for (int i = 0; i < emitCount; i++) {
      final angle = _random.nextDouble() * 2 * pi;
      final speed = 50 + _energy * 200;
      _particles.add(Particle(
        x: 200,
        y: 300,
        vx: cos(angle) * speed * (0.5 + _random.nextDouble()),
        vy: sin(angle) * speed * (0.5 + _random.nextDouble()) - 100,
        life: 1 + _random.nextDouble() * 2,
        maxLife: 3,
        size: 2 + _energy * 8,
        hue: _bass * 60 + _mid * 120 + _treble * 180,
      ));
    }
    
    for (final p in _particles) {
      p.update(dt, gravity: 100 + _bass * 200, drag: 0.02);
    }
    
    _particles.removeWhere((p) => p.isDead);
    setState(() {});
  }

  @override
  void dispose() {
    _animController.dispose();
    _audioPlayer.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('音乐粒子')),
      body: Stack(children: [
        CustomPaint(painter: MusicParticlePainter(_particles, _audioData, _energy, _time), size: Size.infinite),
        Positioned(bottom: 30, left: 20, right: 20, child: _buildControls()),
      ]),
    );
  }
  
  Widget _buildControls() {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(color: Colors.black.withOpacity(0.8), borderRadius: BorderRadius.circular(16)),
      child: Column(mainAxisSize: MainAxisSize.min, children: [
        Row(children: [
          const Icon(Icons.music_note, color: Colors.deepOrange),
          const SizedBox(width: 8),
          const Expanded(child: Text('SoundHelix - Song 1', style: TextStyle(color: Colors.white, fontSize: 14))),
          Container(padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
              decoration: BoxDecoration(color: _isPlaying ? Colors.deepOrange : Colors.grey[800], borderRadius: BorderRadius.circular(12)),
              child: Text(_isPlaying ? '播放中' : '暂停', style: const TextStyle(color: Colors.white, fontSize: 12))),
        ]),
        const SizedBox(height: 12),
        Row(mainAxisAlignment: MainAxisAlignment.center, children: [
          IconButton(icon: Icon(_isPlaying ? Icons.pause : Icons.play_arrow, color: Colors.deepOrange, size: 36),
              onPressed: () => _isPlaying ? _audioPlayer.pause() : _audioPlayer.play()),
        ]),
        Row(children: [
          Expanded(child: _buildMeter('低频', _bass, Colors.red)),
          const SizedBox(width: 8),
          Expanded(child: _buildMeter('中频', _mid, Colors.yellow)),
          const SizedBox(width: 8),
          Expanded(child: _buildMeter('高频', _treble, Colors.cyan)),
        ]),
      ]),
    );
  }
  
  Widget _buildMeter(String label, double value, Color color) {
    return Column(children: [
      Text(label, style: const TextStyle(color: Colors.white70, fontSize: 10)),
      const SizedBox(height: 4),
      Container(height: 30, decoration: BoxDecoration(color: Colors.grey[800], borderRadius: BorderRadius.circular(4)),
          child: Align(alignment: Alignment.bottomCenter,
              child: AnimatedContainer(duration: const Duration(milliseconds: 50), height: (value * 30).clamp(2.0, 30.0),
                  decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(4))))),
    ]);
  }
}

class MusicParticlePainter extends CustomPainter {
  final List<Particle> particles;
  final Float32List audioData;
  final double energy;
  final double time;
  
  MusicParticlePainter(this.particles, this.audioData, this.energy, this.time);
  
  @override
  void paint(Canvas canvas, Size size) {
    final bgColor = Color.lerp(const Color(0xFF0a0a15), const Color(0xFF1a0a20), energy)!;
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = bgColor);
    
    for (final p in particles) {
      final hue = (p.hue + time * 30) % 360;
      canvas.drawCircle(Offset(p.x, p.y), p.size * p.lifeRatio, 
          Paint()..color = HSVColor.fromAHSV(p.alpha * 0.8, hue, 0.9, 1).toColor());
    }
    
    final barWidth = size.width / audioData.length;
    for (int i = 0; i < audioData.length; i++) {
      final barHeight = audioData[i] * 100;
      final hue = (i / audioData.length * 180 + time * 20) % 360;
      canvas.drawRect(Rect.fromLTWH(i * barWidth, size.height - barHeight, barWidth - 1, barHeight),
          Paint()..color = HSVColor.fromAHSV(0.5, hue, 0.8, 1).toColor());
    }
  }
  
  @override
  bool shouldRepaint(covariant MusicParticlePainter old) => true;
}

class FireworkDemo extends StatefulWidget {
  const FireworkDemo({super.key});
  @override
  State<FireworkDemo> createState() => _FireworkDemoState();
}

class _FireworkDemoState extends State<FireworkDemo> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  final List<Particle> _particles = [];
  final Random _random = Random();
  double _time = 0;
  double _nextBurst = 0;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 16))..repeat();
    _controller.addListener(_update);
  }
  
  void _update() {
    final dt = 0.016;
    _time += dt;
    
    if (_time > _nextBurst) {
      _burst();
      _nextBurst = _time + 0.5 + _random.nextDouble() * 1.5;
    }
    
    for (final p in _particles) {
      p.update(dt, gravity: 150, drag: 0.02);
    }
    
    _particles.removeWhere((p) => p.isDead);
    setState(() {});
  }
  
  void _burst() {
    final cx = 50 + _random.nextDouble() * 300;
    final cy = 100 + _random.nextDouble() * 200;
    final count = 50 + _random.nextInt(100);
    final hue = _random.nextDouble() * 360;
    
    for (int i = 0; i < count; i++) {
      final angle = _random.nextDouble() * 2 * pi;
      final speed = 100 + _random.nextDouble() * 150;
      _particles.add(Particle(
        x: cx,
        y: cy,
        vx: cos(angle) * speed,
        vy: sin(angle) * speed,
        life: 1 + _random.nextDouble() * 1.5,
        maxLife: 2.5,
        size: 2 + _random.nextDouble() * 3,
        hue: hue + _random.nextDouble() * 30 - 15,
      ));
    }
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('烟花效果')),
      body: GestureDetector(
        onTap: () => _burst(),
        child: Stack(children: [
          CustomPaint(painter: FireworkPainter(_particles, _time), size: Size.infinite),
          Positioned(bottom: 20, left: 20, child: Container(
            padding: const EdgeInsets.all(12),
            decoration: BoxDecoration(color: Colors.black.withOpacity(0.7), borderRadius: BorderRadius.circular(8)),
            child: const Text('点击屏幕发射烟花', style: TextStyle(color: Colors.white70, fontSize: 12)),
          )),
        ]),
      ),
    );
  }
}

class FireworkPainter extends CustomPainter {
  final List<Particle> particles;
  final double time;
  
  FireworkPainter(this.particles, this.time);
  
  @override
  void paint(Canvas canvas, Size size) {
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = const Color(0xFF050510));
    
    for (final p in particles) {
      final hue = p.hue % 360;
      final paint = Paint()
        ..color = HSVColor.fromAHSV(p.alpha, hue, 0.9, 1).toColor()
        ..maskFilter = MaskFilter.blur(BlurStyle.normal, 3);
      canvas.drawCircle(Offset(p.x, p.y), p.size * p.lifeRatio, paint);
    }
  }
  
  @override
  bool shouldRepaint(covariant FireworkPainter old) => true;
}

class FountainDemo extends StatefulWidget {
  const FountainDemo({super.key});
  @override
  State<FountainDemo> createState() => _FountainDemoState();
}

class _FountainDemoState extends State<FountainDemo> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  final List<Particle> _particles = [];
  final Random _random = Random();
  double _time = 0;
  double _spread = 0.3;
  double _power = 300;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 16))..repeat();
    _controller.addListener(_update);
  }
  
  void _update() {
    final dt = 0.016;
    _time += dt;
    
    for (int i = 0; i < 5; i++) {
      final angle = -pi / 2 + (_random.nextDouble() - 0.5) * _spread;
      _particles.add(Particle(
        x: 200 + _random.nextDouble() * 20 - 10,
        y: 500,
        vx: cos(angle) * _power * (0.8 + _random.nextDouble() * 0.4),
        vy: sin(angle) * _power * (0.8 + _random.nextDouble() * 0.4),
        life: 2 + _random.nextDouble(),
        maxLife: 3,
        size: 3 + _random.nextDouble() * 4,
        hue: 200 + _random.nextDouble() * 40,
      ));
    }
    
    for (final p in _particles) {
      p.update(dt, gravity: 300, drag: 0.01);
    }
    
    _particles.removeWhere((p) => p.isDead);
    setState(() {});
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('喷泉效果')),
      body: Stack(children: [
        CustomPaint(painter: FountainPainter(_particles, _time), size: Size.infinite),
        Positioned(bottom: 20, left: 20, right: 20, child: _buildControls()),
      ]),
    );
  }
  
  Widget _buildControls() {
    return Container(
      padding: const EdgeInsets.all(12),
      decoration: BoxDecoration(color: Colors.black.withOpacity(0.7), borderRadius: BorderRadius.circular(12)),
      child: Column(children: [
        Row(children: [
          Expanded(child: Column(children: [
            Text('扩散: ${(_spread * 180 / pi).toStringAsFixed(0)}°', style: const TextStyle(color: Colors.white70, fontSize: 11)),
            Slider(value: _spread, min: 0.1, max: 1.5, onChanged: (v) => setState(() => _spread = v), activeColor: Colors.blue),
          ])),
          Expanded(child: Column(children: [
            Text('功率: ${_power.toStringAsFixed(0)}', style: const TextStyle(color: Colors.white70, fontSize: 11)),
            Slider(value: _power, min: 100, max: 500, onChanged: (v) => setState(() => _power = v), activeColor: Colors.blue),
          ])),
        ]),
      ]),
    );
  }
}

class FountainPainter extends CustomPainter {
  final List<Particle> particles;
  final double time;
  
  FountainPainter(this.particles, this.time);
  
  @override
  void paint(Canvas canvas, Size size) {
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = const Color(0xFF0a1520));
    
    for (final p in particles) {
      final hue = (p.hue + p.y * 0.1) % 360;
      canvas.drawCircle(Offset(p.x, p.y), p.size * p.lifeRatio, 
          Paint()..color = HSVColor.fromAHSV(p.alpha * 0.7, hue, 0.8, 1).toColor());
    }
  }
  
  @override
  bool shouldRepaint(covariant FountainPainter old) => true;
}

class VortexDemo extends StatefulWidget {
  const VortexDemo({super.key});
  @override
  State<VortexDemo> createState() => _VortexDemoState();
}

class _VortexDemoState extends State<VortexDemo> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  final List<Particle> _particles = [];
  final Random _random = Random();
  double _time = 0;
  double _vortexStrength = 200;
  int _vortexCount = 2;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 16))..repeat();
    _controller.addListener(_update);
    
    for (int i = 0; i < 500; i++) {
      _particles.add(Particle(
        x: _random.nextDouble() * 400,
        y: _random.nextDouble() * 600,
        life: 100,
        maxLife: 100,
        size: 2 + _random.nextDouble() * 2,
        hue: _random.nextDouble() * 360,
      ));
    }
  }
  
  void _update() {
    final dt = 0.016;
    _time += dt;
    
    final vortices = <Offset>[];
    for (int i = 0; i < _vortexCount; i++) {
      final angle = _time * 0.5 + i * pi;
      final radius = 100;
      vortices.add(Offset(200 + cos(angle) * radius, 300 + sin(angle) * radius));
    }
    
    for (final p in _particles) {
      for (final v in vortices) {
        final dx = p.x - v.dx;
        final dy = p.y - v.dy;
        final dist = sqrt(dx * dx + dy * dy);
        if (dist > 10 && dist < 200) {
          final strength = _vortexStrength / dist;
          p.vx += -dy / dist * strength * dt;
          p.vy += dx / dist * strength * dt;
        }
      }
      
      p.vx *= 0.99;
      p.vy *= 0.99;
      p.x += p.vx * dt;
      p.y += p.vy * dt;
      
      if (p.x < 0) p.x = 400;
      if (p.x > 400) p.x = 0;
      if (p.y < 0) p.y = 600;
      if (p.y > 600) p.y = 0;
    }
    
    setState(() {});
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('涡旋效果')),
      body: Stack(children: [
        CustomPaint(painter: VortexPainter(_particles, _time), size: Size.infinite),
        Positioned(bottom: 20, left: 20, right: 20, child: _buildControls()),
      ]),
    );
  }
  
  Widget _buildControls() {
    return Container(
      padding: const EdgeInsets.all(12),
      decoration: BoxDecoration(color: Colors.black.withOpacity(0.7), borderRadius: BorderRadius.circular(12)),
      child: Column(children: [
        Row(children: [
          Expanded(child: Column(children: [
            Text('涡旋强度: ${_vortexStrength.toStringAsFixed(0)}', style: const TextStyle(color: Colors.white70, fontSize: 11)),
            Slider(value: _vortexStrength, min: 50, max: 500, onChanged: (v) => setState(() => _vortexStrength = v), activeColor: Colors.purple),
          ])),
          Expanded(child: Column(children: [
            Text('涡旋数量: $_vortexCount', style: const TextStyle(color: Colors.white70, fontSize: 11)),
            Slider(value: _vortexCount.toDouble(), min: 1, max: 5, divisions: 4, 
                onChanged: (v) => setState(() => _vortexCount = v.toInt()), activeColor: Colors.purple),
          ])),
        ]),
      ]),
    );
  }
}

class VortexPainter extends CustomPainter {
  final List<Particle> particles;
  final double time;
  
  VortexPainter(this.particles, this.time);
  
  @override
  void paint(Canvas canvas, Size size) {
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = const Color(0xFF0a0a15));
    
    for (final p in particles) {
      final hue = (p.hue + time * 10) % 360;
      canvas.drawCircle(Offset(p.x, p.y), p.size, 
          Paint()..color = HSVColor.fromAHSV(0.6, hue, 0.8, 1).toColor());
    }
  }
  
  @override
  bool shouldRepaint(covariant VortexPainter old) => true;
}

class FluidDemo extends StatefulWidget {
  const FluidDemo({super.key});
  @override
  State<FluidDemo> createState() => _FluidDemoState();
}

class _FluidDemoState extends State<FluidDemo> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  final List<_FluidParticle> _particles = [];
  final Random _random = Random();
  double _time = 0;
  double _viscosity = 0.1;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 16))..repeat();
    _controller.addListener(_update);
    
    for (int i = 0; i < 200; i++) {
      _particles.add(_FluidParticle(
        x: 100 + _random.nextDouble() * 200,
        y: 100 + _random.nextDouble() * 200,
      ));
    }
  }
  
  void _update() {
    final dt = 0.016;
    _time += dt;
    
    for (final p in _particles) {
      p.vy += 200 * dt;
      
      for (final q in _particles) {
        if (p == q) continue;
        final dx = q.x - p.x;
        final dy = q.y - p.y;
        final dist = sqrt(dx * dx + dy * dy);
        if (dist < 20 && dist > 0.1) {
          final force = (20 - dist) * 5;
          p.vx -= dx / dist * force * dt;
          p.vy -= dy / dist * force * dt;
        }
      }
      
      p.vx *= (1 - _viscosity * dt);
      p.vy *= (1 - _viscosity * dt);
      p.x += p.vx * dt;
      p.y += p.vy * dt;
      
      if (p.x < 20) { p.x = 20; p.vx *= -0.5; }
      if (p.x > 380) { p.x = 380; p.vx *= -0.5; }
      if (p.y < 20) { p.y = 20; p.vy *= -0.5; }
      if (p.y > 580) { p.y = 580; p.vy *= -0.5; }
    }
    
    setState(() {});
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('流体模拟')),
      body: GestureDetector(
        onPanUpdate: (details) {
          for (final p in _particles) {
            final dx = details.localPosition.dx - p.x;
            final dy = details.localPosition.dy - p.y;
            final dist = sqrt(dx * dx + dy * dy);
            if (dist < 50) {
              p.vx += dx / dist * 50;
              p.vy += dy / dist * 50;
            }
          }
        },
        child: Stack(children: [
          CustomPaint(painter: FluidPainter(_particles, _time), size: Size.infinite),
          Positioned(bottom: 20, left: 20, right: 20, child: _buildControls()),
        ]),
      ),
    );
  }
  
  Widget _buildControls() {
    return Container(
      padding: const EdgeInsets.all(12),
      decoration: BoxDecoration(color: Colors.black.withOpacity(0.7), borderRadius: BorderRadius.circular(12)),
      child: Column(children: [
        Text('粘度: ${_viscosity.toStringAsFixed(2)}', style: const TextStyle(color: Colors.white70, fontSize: 11)),
        Slider(value: _viscosity, min: 0.01, max: 0.5, onChanged: (v) => setState(() => _viscosity = v), activeColor: Colors.cyan),
        const Text('拖动屏幕推动流体', style: TextStyle(color: Colors.white54, fontSize: 10)),
      ]),
    );
  }
}

class _FluidParticle {
  double x, y, vx = 0, vy = 0;
  _FluidParticle({required this.x, required this.y});
}

class FluidPainter extends CustomPainter {
  final List<_FluidParticle> particles;
  final double time;
  
  FluidPainter(this.particles, this.time);
  
  @override
  void paint(Canvas canvas, Size size) {
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = const Color(0xFF051520));
    
    for (final p in particles) {
      final speed = sqrt(p.vx * p.vx + p.vy * p.vy);
      final hue = (200 + speed * 0.5) % 360;
      canvas.drawCircle(Offset(p.x, p.y), 6, 
          Paint()..color = HSVColor.fromAHSV(0.7, hue, 0.8, 1).toColor());
    }
  }
  
  @override
  bool shouldRepaint(covariant FluidPainter old) => true;
}

class TouchParticleDemo extends StatefulWidget {
  const TouchParticleDemo({super.key});
  @override
  State<TouchParticleDemo> createState() => _TouchParticleDemoState();
}

class _TouchParticleDemoState extends State<TouchParticleDemo> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  final List<Particle> _particles = [];
  final Random _random = Random();
  double _time = 0;
  Offset? _touchPos;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 16))..repeat();
    _controller.addListener(_update);
  }
  
  void _update() {
    final dt = 0.016;
    _time += dt;
    
    if (_touchPos != null) {
      for (int i = 0; i < 10; i++) {
        final angle = _random.nextDouble() * 2 * pi;
        final speed = 50 + _random.nextDouble() * 100;
        _particles.add(Particle(
          x: _touchPos!.dx,
          y: _touchPos!.dy,
          vx: cos(angle) * speed,
          vy: sin(angle) * speed,
          life: 0.5 + _random.nextDouble(),
          maxLife: 1.5,
          size: 3 + _random.nextDouble() * 5,
          hue: _time * 100 % 360,
        ));
      }
    }
    
    for (final p in _particles) {
      p.update(dt, drag: 0.05);
    }
    
    _particles.removeWhere((p) => p.isDead);
    setState(() {});
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('触摸粒子')),
      body: GestureDetector(
        onPanStart: (details) => setState(() => _touchPos = details.localPosition),
        onPanUpdate: (details) => setState(() => _touchPos = details.localPosition),
        onPanEnd: (_) => setState(() => _touchPos = null),
        child: Stack(children: [
          CustomPaint(painter: TouchParticlePainter(_particles, _touchPos, _time), size: Size.infinite),
          Positioned(bottom: 20, left: 20, child: Container(
            padding: const EdgeInsets.all(12),
            decoration: BoxDecoration(color: Colors.black.withOpacity(0.7), borderRadius: BorderRadius.circular(8)),
            child: const Text('在屏幕上滑动', style: TextStyle(color: Colors.white70, fontSize: 12)),
          )),
        ]),
      ),
    );
  }
}

class TouchParticlePainter extends CustomPainter {
  final List<Particle> particles;
  final Offset? touchPos;
  final double time;
  
  TouchParticlePainter(this.particles, this.touchPos, this.time);
  
  @override
  void paint(Canvas canvas, Size size) {
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = const Color(0xFF0a0a15));
    
    for (final p in particles) {
      final hue = (p.hue + time * 50) % 360;
      canvas.drawCircle(Offset(p.x, p.y), p.size * p.lifeRatio, 
          Paint()..color = HSVColor.fromAHSV(p.alpha * 0.8, hue, 0.9, 1).toColor());
    }
    
    if (touchPos != null) {
      canvas.drawCircle(touchPos!, 20, Paint()..color = Colors.green.withOpacity(0.3));
    }
  }
  
  @override
  bool shouldRepaint(covariant TouchParticlePainter old) => true;
}

class GalaxyDemo extends StatefulWidget {
  const GalaxyDemo({super.key});
  @override
  State<GalaxyDemo> createState() => _GalaxyDemoState();
}

class _GalaxyDemoState extends State<GalaxyDemo> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  final List<_Star> _stars = [];
  final Random _random = Random();
  double _time = 0;
  double _gravity = 500;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 16))..repeat();
    _controller.addListener(_update);
    
    for (int i = 0; i < 300; i++) {
      final angle = _random.nextDouble() * 2 * pi;
      final dist = 20 + _random.nextDouble() * 180;
      _stars.add(_Star(
        x: 200 + cos(angle) * dist,
        y: 300 + sin(angle) * dist,
        vx: -sin(angle) * sqrt(_gravity * 0.5 / dist) * 20,
        vy: cos(angle) * sqrt(_gravity * 0.5 / dist) * 20,
        mass: 1 + _random.nextDouble() * 2,
        hue: _random.nextDouble() * 60 + 200,
      ));
    }
  }
  
  void _update() {
    final dt = 0.016;
    _time += dt;
    
    final centerX = 200.0;
    final centerY = 300.0;
    
    for (final s in _stars) {
      final dx = centerX - s.x;
      final dy = centerY - s.y;
      final dist = sqrt(dx * dx + dy * dy);
      if (dist > 10) {
        final force = _gravity / (dist * dist);
        s.vx += dx / dist * force * dt;
        s.vy += dy / dist * force * dt;
      }
      
      s.vx *= 0.999;
      s.vy *= 0.999;
      s.x += s.vx * dt;
      s.y += s.vy * dt;
    }
    
    setState(() {});
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('星系模拟')),
      body: Stack(children: [
        CustomPaint(painter: GalaxyPainter(_stars, _time), size: Size.infinite),
        Positioned(bottom: 20, left: 20, right: 20, child: _buildControls()),
      ]),
    );
  }
  
  Widget _buildControls() {
    return Container(
      padding: const EdgeInsets.all(12),
      decoration: BoxDecoration(color: Colors.black.withOpacity(0.7), borderRadius: BorderRadius.circular(12)),
      child: Column(children: [
        Text('引力强度: ${_gravity.toStringAsFixed(0)}', style: const TextStyle(color: Colors.white70, fontSize: 11)),
        Slider(value: _gravity, min: 100, max: 1000, onChanged: (v) => setState(() => _gravity = v), activeColor: Colors.indigo),
        Text('恒星数: ${_stars.length}', style: const TextStyle(color: Colors.white54, fontSize: 10)),
      ]),
    );
  }
}

class _Star {
  double x, y, vx, vy, mass, hue;
  _Star({required this.x, required this.y, required this.vx, required this.vy, required this.mass, required this.hue});
}

class GalaxyPainter extends CustomPainter {
  final List<_Star> stars;
  final double time;
  
  GalaxyPainter(this.stars, this.time);
  
  @override
  void paint(Canvas canvas, Size size) {
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = const Color(0xFF020208));
    
    for (final s in stars) {
      final speed = sqrt(s.vx * s.vx + s.vy * s.vy);
      final hue = (s.hue + speed * 0.2) % 360;
      final alpha = 0.5 + s.mass * 0.1;
      canvas.drawCircle(Offset(s.x, s.y), s.mass, 
          Paint()..color = HSVColor.fromAHSV(alpha, hue, 0.7, 1).toColor());
    }
    
    canvas.drawCircle(const Offset(200, 300), 8, Paint()..color = Colors.white.withOpacity(0.9));
  }
  
  @override
  bool shouldRepaint(covariant GalaxyPainter old) => true;
}

🎯 六、总结与展望

📝 6.1 本文要点回顾

复制代码
粒子系统核心概念:

1. 粒子基础
   - 位置、速度、加速度
   - 生命周期管理
   - 颜色与大小变化

2. 发射器设计
   - 发射位置与方向
   - 发射速率控制
   - 爆发与持续模式

3. 物理模拟
   - 重力与阻力
   - 涡旋与湍流
   - 粒子间相互作用

4. 流体模拟
   - SPH 方法
   - 密度与压力计算
   - 粘性力模拟

5. 音频驱动
   - 能量到粒子数量
   - 频率到颜色映射
   - 节拍到爆发效果

🚀 6.2 性能优化建议

复制代码
粒子系统优化策略:

1. 对象池
   - 重用粒子对象
   - 减少内存分配
   - 避免 GC 压力

2. 空间分区
   - 网格划分
   - 四叉树/八叉树
   - 减少碰撞检测

3. LOD(细节层次)
   - 远距离减少粒子
   - 简化物理计算
   - 降低渲染质量

4. GPU 加速
   - 顶点着色器
   - 计算着色器
   - 实例化渲染

📚 6.3 扩展阅读

  • 《GPU Gems》- 粒子系统章节
  • 《Fluid Simulation for Computer Graphics》
  • SPH 流体模拟论文
  • 实时渲染技术

💡 提示:粒子系统是游戏和视觉效果的核心技术,通过合理的物理模拟和参数映射,可以创造出令人惊叹的动态效果。

相关推荐
空白诗1 小时前
Harmony Flutter 跨平台开发实战:鸿蒙与音乐律动艺术、混沌理论与奇异吸引子:从洛伦兹到音乐的动态艺术
flutter·harmonyos
lbb 小魔仙2 小时前
鸿蒙跨平台实战:React Native在OpenHarmony上的Font字体降级策略详解
react native·华为·harmonyos
2501_921930832 小时前
进阶实战 Flutter for OpenHarmony:高性能列表虚拟化系统 - 大数据量渲染优化实现
flutter
hqk2 小时前
鸿蒙项目实战:手把手带你从零架构 WanAndroid 鸿蒙版
前端·架构·harmonyos
2501_921930833 小时前
进阶实战 Flutter for OpenHarmony:自定义渲染引擎系统 - RenderObject 底层绘制实现
flutter
早點睡3903 小时前
Harmony Flutter 跨平台开发实战:鸿蒙与音乐律动艺术、粒子物理引力场:万有引力与排斥逻辑
flutter·华为·harmonyos
九狼3 小时前
Riverpod 2.0 代码生成与依赖注入
flutter·设计模式·github
空白诗3 小时前
Harmony Flutter 跨平台开发实战:鸿蒙与音乐律动艺术、傅里叶变换与频谱:从时域到频域的视觉翻译
flutter
lbb 小魔仙4 小时前
鸿蒙跨平台实战:React Native在OpenHarmony上的Font字体加载管理详解
react native·华为·harmonyos