
欢迎加入开源鸿蒙跨平台社区: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 流体模拟论文
- 实时渲染技术
💡 提示:粒子系统是游戏和视觉效果的核心技术,通过合理的物理模拟和参数映射,可以创造出令人惊叹的动态效果。