Harmony Flutter 跨平台开发实战:鸿蒙与音乐律动艺术、粒子物理引力场:万有引力与排斥逻辑
本篇文章深入探讨了粒子物理系统在音乐可视化中的应用,从牛顿万有引力定律到多种力场的实现,构建了具有"宇宙感"的粒子动画效果。
·

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
🌌 一、粒子物理系统:从牛顿定律到视觉艺术
📚 1.1 粒子系统的历史与应用
粒子系统(Particle System)是由计算机图形学家**威廉·里夫斯(William Reeves)**在 1983 年提出的,最初用于电影《星际迷航II:可汗之怒》中的火焰效果。如今,粒子系统已成为游戏开发、视觉特效和交互设计中的核心技术。
粒子系统的核心思想是:用大量简单个体的集体行为来模拟复杂现象。每个粒子遵循简单的物理规则,但整体却能产生令人惊叹的视觉效果。
| 应用领域 | 具体用途 | 效果描述 |
|---|---|---|
| 🔥火焰与烟雾 | 篝火、烟囱、爆炸 | 粒子上升、扩散、消散 |
| 🌊水流效果 | 喷泉、瀑布、雨滴 | 重力下落、碰撞溅射 |
| ✨星空效果 | 星云、流星、星系 | 引力聚集、轨道运动 |
| 🎆烟花绽放 | 节日烟花、礼花 | 爆炸扩散、拖尾效果 |
| 🎵音乐可视化 | 音频驱动的粒子舞动 | 随节奏律动、能量爆发 |
📐 1.2 牛顿万有引力定律
牛顿万有引力定律是粒子引力系统的基础:
F = G × (m₁ × m₂) / r²
| 符号 | 含义 | 说明 |
|---|---|---|
| F | 引力大小 | 两物体间的相互吸引力 |
| G | 万有引力常数 | 6.674×10⁻¹¹ N·m²/kg² |
| m₁, m₂ | 物体质量 | 两物体的质量 |
| r | 距离 | 两物体质心之间的距离 |
引力方向:沿着两物体连线方向,指向对方。
加速度计算:
a = F / m = G × M / r²
其中 M 是对方的质量。
🔬 1.3 力场类型与特性
在粒子系统中,我们可以定义多种力场:
| 力场类型 | 公式 | 特性 | 视觉效果 |
|---|---|---|---|
| 🔵引力场 | F ∝ 1/r² | 远距离衰减 | 聚集、轨道 |
| 🔴斥力场 | F ∝ -1/r² | 远距离衰减 | 扩散、排斥 |
| 🟢弹簧力 | F = -kx | 线性恢复 | 弹性、振荡 |
| 🟡阻力 | F = -bv | 速度衰减 | 阻尼、稳定 |
| 🟣涡流场 | F ⊥ r | 切向力 | 旋转、螺旋 |
力场方向示意:
引力场: ● ←───────→ ● (相互吸引)
斥力场: ● ──────→ ←────── ● (相互排斥)
涡流场: ↻ ↺
●
🎯 1.4 粒子系统的核心组件
一个完整的粒子系统包含以下核心组件:
| 组件 | 职责 | 关键属性 |
|---|---|---|
| 🎯粒子 | 基本单元 | 位置、速度、加速度、质量、颜色、生命周期 |
| ⚡力场 | 施加力 | 类型、强度、范围、衰减 |
| 🔄发射器 | 生成粒子 | 位置、速率、方向、初始速度 |
| 🎨渲染器 | 绘制粒子 | 形状、大小、透明度、混合模式 |
| ⏱️更新器 | 状态更新 | 积分方法、碰撞检测、边界处理 |
🔧 二、粒子物理系统的 Dart 实现
🧮 2.1 粒子基础类
import 'dart:math';
import 'dart:typed_data';
/// 二维向量
class Vector2D {
double x;
double y;
Vector2D(this.x, this.y);
factory Vector2D.zero() => Vector2D(0, 0);
factory Vector2D.fromAngle(double angle, double magnitude) =>
Vector2D(cos(angle) * magnitude, sin(angle) * magnitude);
Vector2D operator +(Vector2D other) => Vector2D(x + other.x, y + other.y);
Vector2D operator -(Vector2D other) => Vector2D(x - other.x, y - other.y);
Vector2D operator *(double scalar) => Vector2D(x * scalar, y * scalar);
Vector2D operator /(double scalar) => Vector2D(x / scalar, y / scalar);
void add(Vector2D other) {
x += other.x;
y += other.y;
}
void multiply(double scalar) {
x *= scalar;
y *= scalar;
}
double get magnitude => sqrt(x * x + y * y);
double get angle => atan2(y, x);
Vector2D get normalized {
final mag = magnitude;
return mag > 0 ? this / mag : Vector2D.zero();
}
Vector2D get perpendicular => Vector2D(-y, x);
double dot(Vector2D other) => x * other.x + y * other.y;
void limit(double max) {
final mag = magnitude;
if (mag > max) {
multiply(max / mag);
}
}
void setMagnitude(double mag) {
final currentMag = magnitude;
if (currentMag > 0) {
multiply(mag / currentMag);
}
}
}
/// 粒子类
class Particle {
Vector2D position;
Vector2D velocity;
Vector2D acceleration;
double mass;
double radius;
Color color;
double life;
double maxLife;
bool isAlive;
Particle({
required this.position,
Vector2D? velocity,
Vector2D? acceleration,
this.mass = 1.0,
this.radius = 5.0,
this.color = Colors.white,
this.maxLife = 5.0,
}) : velocity = velocity ?? Vector2D.zero(),
acceleration = acceleration ?? Vector2D.zero(),
life = maxLife,
isAlive = true;
/// 施加力
void applyForce(Vector2D force) {
// F = ma, a = F/m
acceleration.add(force / mass);
}
/// 更新状态
void update(double dt) {
// 速度积分
velocity.add(acceleration * dt);
// 位置积分
position.add(velocity * dt);
// 重置加速度(每帧重新计算)
acceleration = Vector2D.zero();
// 生命周期
life -= dt;
if (life <= 0) {
isAlive = false;
}
}
/// 获取生命比例
double get lifeRatio => life / maxLife;
/// 获取透明度
double get opacity => lifeRatio.clamp(0.0, 1.0);
}
/// 粒子系统
class ParticleSystem {
final List<Particle> particles = [];
final List<ForceField> forceFields = [];
final Random random = Random();
int maxParticles;
Vector2D gravity;
double damping;
ParticleSystem({
this.maxParticles = 500,
this.gravity = const Vector2D(0, 0),
this.damping = 0.99,
});
/// 添加粒子
void addParticle(Particle particle) {
if (particles.length < maxParticles) {
particles.add(particle);
}
}
/// 添加力场
void addForceField(ForceField field) {
forceFields.add(field);
}
/// 更新所有粒子
void update(double dt) {
// 计算并施加力
for (final particle in particles) {
// 重力
particle.applyForce(gravity * particle.mass);
// 力场作用
for (final field in forceFields) {
final force = field.calculateForce(particle);
particle.applyForce(force);
}
// 阻尼
particle.velocity.multiply(damping);
}
// 更新粒子状态
for (final particle in particles) {
particle.update(dt);
}
// 移除死亡粒子
particles.removeWhere((p) => !p.isAlive);
}
/// 清除所有粒子
void clear() {
particles.clear();
}
}
⚡ 2.2 力场实现
/// 力场基类
abstract class ForceField {
Vector2D position;
double strength;
double range;
bool enabled;
ForceField({
required this.position,
required this.strength,
this.range = double.infinity,
this.enabled = true,
});
Vector2D calculateForce(Particle particle);
}
/// 引力场
class GravityField extends ForceField {
double mass;
GravityField({
required super.position,
required this.mass,
super.range,
}) : super(strength: mass);
Vector2D calculateForce(Particle particle) {
if (!enabled) return Vector2D.zero();
final direction = position - particle.position;
final distance = direction.magnitude;
// 避免距离过近导致力过大
final minDistance = 10.0;
final effectiveDistance = max(distance, minDistance);
// 检查范围
if (range != double.infinity && effectiveDistance > range) {
return Vector2D.zero();
}
// F = G * m1 * m2 / r^2
// 这里简化为 F = strength * m / r^2
final forceMagnitude = strength * particle.mass / (effectiveDistance * effectiveDistance);
// 限制最大力
final maxForce = 100.0;
final clampedForce = min(forceMagnitude, maxForce);
return direction.normalized * clampedForce;
}
}
/// 斥力场
class RepulsionField extends ForceField {
RepulsionField({
required super.position,
required super.strength,
super.range,
});
Vector2D calculateForce(Particle particle) {
if (!enabled) return Vector2D.zero();
final direction = particle.position - position;
final distance = direction.magnitude;
// 避免距离过近导致力过大
final minDistance = 5.0;
final effectiveDistance = max(distance, minDistance);
// 检查范围
if (range != double.infinity && effectiveDistance > range) {
return Vector2D.zero();
}
// 斥力与距离平方成反比
final forceMagnitude = strength / (effectiveDistance * effectiveDistance);
// 限制最大力
final maxForce = 200.0;
final clampedForce = min(forceMagnitude, maxForce);
return direction.normalized * clampedForce;
}
}
/// 涡流场
class VortexField extends ForceField {
double angularStrength;
VortexField({
required super.position,
required super.strength,
this.angularStrength = 1.0,
super.range,
});
Vector2D calculateForce(Particle particle) {
if (!enabled) return Vector2D.zero();
final direction = position - particle.position;
final distance = direction.magnitude;
// 检查范围
if (range != double.infinity && distance > range) {
return Vector2D.zero();
}
// 切向力(旋转)
final tangent = direction.perpendicular.normalized;
// 径向力(吸引/排斥)
final radial = direction.normalized;
// 组合力
final radialStrength = strength / max(distance, 10);
final tangentialStrength = angularStrength / max(distance, 10);
return tangent * tangentialStrength + radial * radialStrength;
}
}
/// 均匀力场(如风力)
class UniformField extends ForceField {
Vector2D direction;
UniformField({
required this.direction,
required super.strength,
}) : super(position: Vector2D.zero());
Vector2D calculateForce(Particle particle) {
if (!enabled) return Vector2D.zero();
return direction.normalized * strength;
}
}
🎨 2.3 粒子发射器
/// 粒子发射器
class ParticleEmitter {
Vector2D position;
double emissionRate;
double initialSpeed;
double speedVariation;
double direction;
double spread;
double initialLife;
double lifeVariation;
double initialMass;
double massVariation;
double initialRadius;
double radiusVariation;
Color color;
List<Color> colorVariations;
final Random _random = Random();
ParticleEmitter({
required this.position,
this.emissionRate = 10,
this.initialSpeed = 50,
this.speedVariation = 20,
this.direction = 0,
this.spread = pi / 4,
this.initialLife = 3,
this.lifeVariation = 1,
this.initialMass = 1,
this.massVariation = 0.5,
this.initialRadius = 5,
this.radiusVariation = 2,
this.color = Colors.white,
this.colorVariations = const [],
});
/// 发射粒子
List<Particle> emit(double dt) {
final particles = <Particle>[];
final count = (emissionRate * dt).floor();
for (int i = 0; i < count; i++) {
particles.add(_createParticle());
}
// 概率性发射额外粒子
if (_random.nextDouble() < (emissionRate * dt) % 1) {
particles.add(_createParticle());
}
return particles;
}
Particle _createParticle() {
// 随机角度
final angle = direction + (_random.nextDouble() - 0.5) * spread;
// 随机速度
final speed = initialSpeed + (_random.nextDouble() - 0.5) * speedVariation;
// 随机生命周期
final life = initialLife + (_random.nextDouble() - 0.5) * lifeVariation;
// 随机质量
final mass = initialMass + (_random.nextDouble() - 0.5) * massVariation;
// 随机半径
final radius = initialRadius + (_random.nextDouble() - 0.5) * radiusVariation;
// 随机颜色
Color particleColor = color;
if (colorVariations.isNotEmpty) {
particleColor = colorVariations[_random.nextInt(colorVariations.length)];
}
return Particle(
position: Vector2D(position.x, position.y),
velocity: Vector2D.fromAngle(angle, speed),
mass: mass,
radius: radius,
color: particleColor,
maxLife: life,
);
}
/// 爆发式发射
List<Particle> burst(int count) {
return List.generate(count, (_) => _createParticle());
}
}
🌬️ 三、音乐驱动的引力场动画
🎵 3.1 音频响应的引力系统
import 'package:flutter/material.dart';
import 'package:just_audio_ohos/just_audio_ohos.dart';
import 'package:audio_session/audio_session.dart';
/// 音频驱动的引力场控制器
class AudioGravityController extends ChangeNotifier {
final AudioPlayer _player = AudioPlayer();
final ParticleSystem _particleSystem = ParticleSystem(
maxParticles: 800,
damping: 0.98,
);
bool _isPlaying = false;
Duration _position = Duration.zero;
Duration _duration = Duration.zero;
// 音频特征
Float32List _audioData = Float32List(128);
double _energy = 0;
double _bass = 0;
double _mid = 0;
double _treble = 0;
// 引力场参数
final List<GravityField> _gravityFields = [];
final List<RepulsionField> _repulsionFields = [];
final List<VortexField> _vortexFields = [];
bool get isPlaying => _isPlaying;
Duration get position => _position;
Duration get duration => _duration;
ParticleSystem get particleSystem => _particleSystem;
Float32List get audioData => _audioData;
double get energy => _energy;
double get bass => _bass;
double get mid => _mid;
double get treble => _treble;
List<GravityField> get gravityFields => _gravityFields;
List<RepulsionField> get repulsionFields => _repulsionFields;
List<VortexField> get vortexFields => _vortexFields;
AudioPlayer get player => _player;
/// 初始化
Future<void> initialize() async {
final session = await AudioSession.instance;
await session.configure(const AudioSessionConfiguration.music());
_player.playerStateStream.listen((state) {
_isPlaying = state.playing;
notifyListeners();
});
_player.positionStream.listen((position) {
_position = position;
notifyListeners();
});
_player.durationStream.listen((duration) {
_duration = duration ?? Duration.zero;
notifyListeners();
});
}
/// 加载网络音频
Future<void> loadAudio(String url) async {
try {
await _player.setUrl(url);
} catch (e) {
debugPrint('加载音频失败: $e');
}
}
/// 设置力场
void setupForceFields(Size size) {
_gravityFields.clear();
_repulsionFields.clear();
_vortexFields.clear();
final centerX = size.width / 2;
final centerY = size.height / 2;
// 中心引力场
_gravityFields.add(GravityField(
position: Vector2D(centerX, centerY),
mass: 500,
range: 300,
));
// 四角斥力场
_repulsionFields.add(RepulsionField(
position: Vector2D(50, 50),
strength: 1000,
range: 100,
));
_repulsionFields.add(RepulsionField(
position: Vector2D(size.width - 50, 50),
strength: 1000,
range: 100,
));
_repulsionFields.add(RepulsionField(
position: Vector2D(50, size.height - 50),
strength: 1000,
range: 100,
));
_repulsionFields.add(RepulsionField(
position: Vector2D(size.width - 50, size.height - 50),
strength: 1000,
range: 100,
));
// 涡流场
_vortexFields.add(VortexField(
position: Vector2D(centerX - 100, centerY),
strength: 50,
angularStrength: 100,
range: 150,
));
_vortexFields.add(VortexField(
position: Vector2D(centerX + 100, centerY),
strength: 50,
angularStrength: -100,
range: 150,
));
// 添加到粒子系统
_particleSystem.forceFields.clear();
for (final field in _gravityFields) {
_particleSystem.addForceField(field);
}
for (final field in _repulsionFields) {
_particleSystem.addForceField(field);
}
for (final field in _vortexFields) {
_particleSystem.addForceField(field);
}
}
/// 更新音频数据
void update(double dt, double time, Size size) {
final random = Random();
// 更新音频数据
for (int i = 0; i < 128; i++) {
if (_isPlaying) {
final freq = (i / 128) * 8 + 1;
final wave1 = sin(time * freq) * 0.4;
final wave2 = sin(time * freq * 1.5 + pi / 3) * 0.3;
final wave3 = cos(time * freq * 0.5 + pi / 6) * 0.2;
final noise = (random.nextDouble() - 0.5) * 0.15;
final bassBoost = i < 32 ? 0.3 : 0;
_audioData[i] = _audioData[i] * 0.85 +
(wave1 + wave2 + wave3 + noise + bassBoost) * 0.15;
} else {
_audioData[i] = _audioData[i] * 0.95;
}
}
// 计算音频特征
_calculateAudioFeatures();
// 更新力场参数
_updateForceFields(time, size);
// 发射粒子
_emitParticles(time, size);
// 更新粒子系统
_particleSystem.update(dt);
notifyListeners();
}
/// 计算音频特征
void _calculateAudioFeatures() {
double totalEnergy = 0;
double bassEnergy = 0;
double midEnergy = 0;
double trebleEnergy = 0;
for (int i = 0; i < 128; i++) {
final value = _audioData[i].abs();
totalEnergy += value;
if (i < 32) {
bassEnergy += value;
} else if (i < 96) {
midEnergy += value;
} else {
trebleEnergy += value;
}
}
_energy = totalEnergy / 128;
_bass = bassEnergy / 32;
_mid = midEnergy / 64;
_treble = trebleEnergy / 32;
}
/// 更新力场参数
void _updateForceFields(double time, Size size) {
final centerX = size.width / 2;
final centerY = size.height / 2;
// 中心引力随低音变化
if (_gravityFields.isNotEmpty) {
_gravityFields[0].mass = 300 + _bass * 500;
_gravityFields[0].position = Vector2D(
centerX + sin(time) * 20 * _energy,
centerY + cos(time) * 20 * _energy,
);
}
// 涡流场随中频变化
if (_vortexFields.length >= 2) {
_vortexFields[0].angularStrength = 50 + _mid * 150;
_vortexFields[1].angularStrength = -50 - _mid * 150;
}
}
/// 发射粒子
void _emitParticles(double time, Size size) {
final centerX = size.width / 2;
final centerY = size.height / 2;
// 发射速率随能量变化
final emissionRate = 10 + _energy * 50;
// 创建发射器
final emitter = ParticleEmitter(
position: Vector2D(centerX, centerY),
emissionRate: emissionRate,
initialSpeed: 30 + _energy * 100,
speedVariation: 20,
direction: time * 0.5,
spread: pi * 2,
initialLife: 4,
lifeVariation: 2,
initialRadius: 3 + _bass * 4,
radiusVariation: 2,
colorVariations: [
HSVColor.fromAHSV(1, ((time * 30) % 360).abs(), 0.8, 1).toColor(),
HSVColor.fromAHSV(1, ((time * 30 + 60) % 360).abs(), 0.8, 1).toColor(),
HSVColor.fromAHSV(1, ((time * 30 + 120) % 360).abs(), 0.8, 1).toColor(),
],
);
// 发射粒子
final newParticles = emitter.emit(0.016);
for (final particle in newParticles) {
_particleSystem.addParticle(particle);
}
}
/// 播放/暂停
Future<void> togglePlay() async {
if (_isPlaying) {
await _player.pause();
} else {
await _player.play();
}
}
/// 跳转
Future<void> seek(Duration position) async {
await _player.seek(position);
}
void dispose() {
_player.dispose();
super.dispose();
}
}
🎨 3.2 粒子引力场绘制器
/// 粒子引力场绘制器
class ParticleGravityPainter extends CustomPainter {
final ParticleSystem particleSystem;
final List<GravityField> gravityFields;
final List<RepulsionField> repulsionFields;
final List<VortexField> vortexFields;
final double time;
final double energy;
final bool isPlaying;
ParticleGravityPainter({
required this.particleSystem,
required this.gravityFields,
required this.repulsionFields,
required this.vortexFields,
required this.time,
required this.energy,
required this.isPlaying,
});
void paint(Canvas canvas, Size size) {
// 绘制背景
_drawBackground(canvas, size);
// 绘制力场可视化
_drawForceFields(canvas, size);
// 绘制粒子轨迹
_drawParticleTrails(canvas, size);
// 绘制粒子
_drawParticles(canvas, size);
// 绘制力场中心
_drawForceFieldCenters(canvas, size);
}
void _drawBackground(Canvas canvas, Size size) {
final gradient = RadialGradient(
center: Alignment.center,
radius: 1.0,
colors: [
Color.lerp(const Color(0xFF0a0a1a), const Color(0xFF1a0a2a), energy)!,
const Color(0xFF050510),
],
);
canvas.drawRect(
Rect.fromLTWH(0, 0, size.width, size.height),
Paint()..shader = gradient.createShader(Rect.fromLTWH(0, 0, size.width, size.height)),
);
}
void _drawForceFields(Canvas canvas, Size size) {
// 绘制引力场范围
for (final field in gravityFields) {
final paint = Paint()
..color = Colors.blue.withOpacity(0.1 + energy * 0.1)
..style = PaintingStyle.stroke
..strokeWidth = 1;
canvas.drawCircle(
Offset(field.position.x, field.position.y),
field.range,
paint,
);
}
// 绘制涡流场
for (final field in vortexFields) {
final paint = Paint()
..color = Colors.purple.withOpacity(0.15)
..style = PaintingStyle.stroke
..strokeWidth = 1;
final path = Path();
final segments = 50;
for (int i = 0; i <= segments; i++) {
final angle = (i / segments) * pi * 4;
final r = (i / segments) * field.range;
final x = field.position.x + cos(angle) * r;
final y = field.position.y + sin(angle) * r;
if (i == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
}
canvas.drawPath(path, paint);
}
}
void _drawParticleTrails(Canvas canvas, Size size) {
for (final particle in particleSystem.particles) {
if (particle.velocity.magnitude > 5) {
final trailLength = particle.velocity.magnitude * 0.3;
final trailEnd = particle.position - particle.velocity.normalized * trailLength;
final paint = Paint()
..color = particle.color.withOpacity(particle.opacity * 0.3)
..strokeWidth = particle.radius * 0.5
..strokeCap = StrokeCap.round;
canvas.drawLine(
Offset(particle.position.x, particle.position.y),
Offset(trailEnd.x, trailEnd.y),
paint,
);
}
}
}
void _drawParticles(Canvas canvas, Size size) {
for (final particle in particleSystem.particles) {
final paint = Paint()
..color = particle.color.withOpacity(particle.opacity)
..style = PaintingStyle.fill;
// 发光效果
if (isPlaying && particle.velocity.magnitude > 20) {
paint.maskFilter = MaskFilter.blur(BlurStyle.normal, particle.radius * 0.5);
}
canvas.drawCircle(
Offset(particle.position.x, particle.position.y),
particle.radius * (0.5 + particle.lifeRatio * 0.5),
paint,
);
}
}
void _drawForceFieldCenters(Canvas canvas, Size size) {
// 引力场中心
for (final field in gravityFields) {
final paint = Paint()
..shader = RadialGradient(
colors: [
Colors.blue.withOpacity(0.8),
Colors.blue.withOpacity(0),
],
).createShader(Rect.fromCircle(
center: Offset(field.position.x, field.position.y),
radius: 30,
));
canvas.drawCircle(
Offset(field.position.x, field.position.y),
30,
paint,
);
// 核心点
canvas.drawCircle(
Offset(field.position.x, field.position.y),
5,
Paint()..color = Colors.white,
);
}
// 斥力场中心
for (final field in repulsionFields) {
final paint = Paint()
..color = Colors.red.withOpacity(0.5 + energy * 0.3)
..style = PaintingStyle.stroke
..strokeWidth = 2;
canvas.drawCircle(
Offset(field.position.x, field.position.y),
10,
paint,
);
}
// 涡流场中心
for (final field in vortexFields) {
final paint = Paint()
..color = Colors.purple.withOpacity(0.8)
..style = PaintingStyle.fill;
canvas.drawCircle(
Offset(field.position.x, field.position.y),
8,
paint,
);
}
}
bool shouldRepaint(covariant ParticleGravityPainter oldDelegate) => true;
}
📦 四、完整示例代码
以下是完整的粒子物理引力场音乐可视化示例代码,包含网络 MP3 播放功能:
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 ParticleGravityApp());
}
class ParticleGravityApp extends StatelessWidget {
const ParticleGravityApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: '粒子引力场',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.indigo,
brightness: Brightness.dark,
),
useMaterial3: true,
),
home: const ParticleHomePage(),
debugShowCheckedModeBanner: false,
);
}
}
class ParticleHomePage extends StatelessWidget {
const ParticleHomePage({super.key});
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: [
_buildSectionCard(
context,
title: '基础粒子',
description: '简单粒子运动演示',
icon: Icons.blur_circular,
color: Colors.blue,
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const BasicParticleDemo()),
),
),
_buildSectionCard(
context,
title: '引力场',
description: '万有引力模拟',
icon: Icons.grain,
color: Colors.indigo,
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const GravityFieldDemo()),
),
),
_buildSectionCard(
context,
title: '斥力场',
description: '粒子排斥效果',
icon: Icons.bubble_chart,
color: Colors.red,
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const RepulsionFieldDemo()),
),
),
_buildSectionCard(
context,
title: '涡流场',
description: '旋转粒子系统',
icon: Icons.refresh,
color: Colors.purple,
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const VortexFieldDemo()),
),
),
_buildSectionCard(
context,
title: '音乐引力场',
description: '音频驱动的粒子动画',
icon: Icons.music_note,
color: Colors.orange,
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const MusicGravityDemo()),
),
),
_buildSectionCard(
context,
title: '综合演示',
description: '完整粒子宇宙体验',
icon: Icons.public,
color: Colors.cyan,
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const FullGravityDemo()),
),
),
],
),
);
}
Widget _buildSectionCard(
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 Vector2D {
double x, y;
Vector2D(this.x, this.y);
factory Vector2D.zero() => Vector2D(0, 0);
factory Vector2D.fromAngle(double angle, double magnitude) =>
Vector2D(cos(angle) * magnitude, sin(angle) * magnitude);
Vector2D operator +(Vector2D o) => Vector2D(x + o.x, y + o.y);
Vector2D operator -(Vector2D o) => Vector2D(x - o.x, y - o.y);
Vector2D operator *(double s) => Vector2D(x * s, y * s);
Vector2D operator /(double s) => Vector2D(x / s, y / s);
void add(Vector2D o) { x += o.x; y += o.y; }
void multiply(double s) { x *= s; y *= s; }
double get magnitude => sqrt(x * x + y * y);
double get angle => atan2(y, x);
Vector2D get normalized => magnitude > 0 ? this / magnitude : Vector2D.zero();
Vector2D get perpendicular => Vector2D(-y, x);
void limit(double max) {
final mag = magnitude;
if (mag > max) multiply(max / mag);
}
}
/// 粒子类
class Particle {
Vector2D position;
Vector2D velocity;
Vector2D acceleration;
double mass;
double radius;
Color color;
double life;
double maxLife;
bool isAlive;
Particle({
required this.position,
Vector2D? velocity,
this.mass = 1,
this.radius = 5,
this.color = Colors.white,
this.maxLife = 5,
}) : velocity = velocity ?? Vector2D.zero(),
acceleration = Vector2D.zero(),
life = maxLife,
isAlive = true;
void applyForce(Vector2D force) => acceleration.add(force / mass);
void update(double dt) {
velocity.add(acceleration * dt);
position.add(velocity * dt);
acceleration = Vector2D.zero();
life -= dt;
if (life <= 0) isAlive = false;
}
double get lifeRatio => (life / maxLife).clamp(0.0, 1.0);
}
/// 力场
abstract class ForceField {
Vector2D position;
double strength;
double range;
ForceField(this.position, this.strength, this.range);
Vector2D calculateForce(Particle p);
}
/// 引力场
class GravityField extends ForceField {
GravityField(Vector2D pos, double mass, double range)
: super(pos, mass, range);
Vector2D calculateForce(Particle p) {
final dir = position - p.position;
final dist = max(dir.magnitude, 10);
if (range != double.infinity && dist > range) return Vector2D.zero();
final force = min(strength * p.mass / (dist * dist), 100).toDouble();
return dir.normalized * force;
}
}
/// 斥力场
class RepulsionField extends ForceField {
RepulsionField(Vector2D pos, double strength, double range)
: super(pos, strength, range);
Vector2D calculateForce(Particle p) {
final dir = p.position - position;
final dist = max(dir.magnitude, 5);
if (range != double.infinity && dist > range) return Vector2D.zero();
final force = min(strength / (dist * dist), 200).toDouble();
return dir.normalized * force;
}
}
/// 涡流场
class VortexField extends ForceField {
double angularStrength;
VortexField(Vector2D pos, double strength, this.angularStrength, double range)
: super(pos, strength, range);
Vector2D calculateForce(Particle p) {
final dir = position - p.position;
final dist = dir.magnitude;
if (range != double.infinity && dist > range) return Vector2D.zero();
final tangent = dir.perpendicular.normalized;
final radial = dir.normalized;
return tangent * (angularStrength / max(dist, 10)) +
radial * (strength / max(dist, 10));
}
}
/// 粒子系统
class ParticleSystem {
final List<Particle> particles = [];
final List<ForceField> forceFields = [];
int maxParticles;
double damping;
ParticleSystem({this.maxParticles = 500, this.damping = 0.99});
void addParticle(Particle p) {
if (particles.length < maxParticles) particles.add(p);
}
void addForceField(ForceField f) => forceFields.add(f);
void update(double dt) {
for (final p in particles) {
for (final f in forceFields) {
p.applyForce(f.calculateForce(p));
}
p.velocity.multiply(damping);
p.update(dt);
}
particles.removeWhere((p) => !p.isAlive);
}
}
/// 基础粒子演示
class BasicParticleDemo extends StatefulWidget {
const BasicParticleDemo({super.key});
State<BasicParticleDemo> createState() => _BasicParticleDemoState();
}
class _BasicParticleDemoState extends State<BasicParticleDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
final List<Particle> _particles = [];
final Random _random = Random();
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
)..repeat();
for (int i = 0; i < 100; i++) {
_particles.add(Particle(
position: Vector2D(_random.nextDouble() * 400, _random.nextDouble() * 400),
velocity: Vector2D.fromAngle(_random.nextDouble() * pi * 2, 50),
color: HSVColor.fromAHSV(1, _random.nextDouble() * 360, 0.8, 1).toColor(),
maxLife: 5 + _random.nextDouble() * 5,
));
}
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('基础粒子')),
body: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
for (final p in _particles) {
p.update(0.016);
if (p.position.x < 0 || p.position.x > 400) p.velocity.x *= -1;
if (p.position.y < 0 || p.position.y > 400) p.velocity.y *= -1;
}
return CustomPaint(
painter: BasicParticlePainter(_particles),
size: Size.infinite,
);
},
),
);
}
}
class BasicParticlePainter extends CustomPainter {
final List<Particle> particles;
BasicParticlePainter(this.particles);
void paint(Canvas canvas, Size size) {
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height),
Paint()..color = const Color(0xFF0a0a1a));
for (final p in particles) {
canvas.drawCircle(
Offset(p.position.x, p.position.y),
p.radius * p.lifeRatio,
Paint()..color = p.color.withOpacity(p.lifeRatio),
);
}
}
bool shouldRepaint(covariant BasicParticlePainter old) => true;
}
/// 引力场演示
class GravityFieldDemo extends StatefulWidget {
const GravityFieldDemo({super.key});
State<GravityFieldDemo> createState() => _GravityFieldDemoState();
}
class _GravityFieldDemoState extends State<GravityFieldDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
final ParticleSystem _system = ParticleSystem(maxParticles: 300);
final Random _random = Random();
late GravityField _gravityField;
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
)..repeat();
_gravityField = GravityField(Vector2D(200, 200), 300, 300);
_system.addForceField(_gravityField);
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('引力场')),
body: GestureDetector(
onPanUpdate: (details) {
_gravityField.position = Vector2D(
details.localPosition.dx,
details.localPosition.dy,
);
},
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
if (_system.particles.length < 200) {
_system.addParticle(Particle(
position: Vector2D(
_random.nextDouble() * 400,
_random.nextDouble() * 400,
),
velocity: Vector2D.fromAngle(_random.nextDouble() * pi * 2, 20),
color: HSVColor.fromAHSV(1, _random.nextDouble() * 360, 0.8, 1).toColor(),
maxLife: 8,
));
}
_system.update(0.016);
return CustomPaint(
painter: GravityPainter(_system, _gravityField),
size: Size.infinite,
);
},
),
),
);
}
}
class GravityPainter extends CustomPainter {
final ParticleSystem system;
final GravityField field;
GravityPainter(this.system, this.field);
void paint(Canvas canvas, Size size) {
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height),
Paint()..color = const Color(0xFF050510));
canvas.drawCircle(
Offset(field.position.x, field.position.y),
field.range,
Paint()..color = Colors.blue.withOpacity(0.1)..style = PaintingStyle.stroke,
);
canvas.drawCircle(
Offset(field.position.x, field.position.y),
10,
Paint()..color = Colors.blue,
);
for (final p in system.particles) {
canvas.drawCircle(
Offset(p.position.x, p.position.y),
p.radius * p.lifeRatio,
Paint()..color = p.color.withOpacity(p.lifeRatio * 0.8),
);
}
}
bool shouldRepaint(covariant GravityPainter old) => true;
}
/// 斥力场演示
class RepulsionFieldDemo extends StatefulWidget {
const RepulsionFieldDemo({super.key});
State<RepulsionFieldDemo> createState() => _RepulsionFieldDemoState();
}
class _RepulsionFieldDemoState extends State<RepulsionFieldDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
final ParticleSystem _system = ParticleSystem(maxParticles: 200);
final Random _random = Random();
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
)..repeat();
_system.addForceField(RepulsionField(Vector2D(100, 200), 5000, 80));
_system.addForceField(RepulsionField(Vector2D(300, 200), 5000, 80));
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('斥力场')),
body: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
if (_system.particles.length < 150) {
_system.addParticle(Particle(
position: Vector2D(200, 100),
velocity: Vector2D.fromAngle(_random.nextDouble() * pi * 2, 30),
color: HSVColor.fromAHSV(1, (_random.nextDouble() * 60 + 330) % 360, 0.8, 1).toColor(),
maxLife: 6,
));
}
_system.update(0.016);
return CustomPaint(
painter: RepulsionPainter(_system),
size: Size.infinite,
);
},
),
);
}
}
class RepulsionPainter extends CustomPainter {
final ParticleSystem system;
RepulsionPainter(this.system);
void paint(Canvas canvas, Size size) {
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height),
Paint()..color = const Color(0xFF100510));
for (final f in system.forceFields) {
if (f is RepulsionField) {
canvas.drawCircle(
Offset(f.position.x, f.position.y),
f.range,
Paint()..color = Colors.red.withOpacity(0.2)..style = PaintingStyle.stroke,
);
canvas.drawCircle(
Offset(f.position.x, f.position.y),
8,
Paint()..color = Colors.red,
);
}
}
for (final p in system.particles) {
canvas.drawCircle(
Offset(p.position.x, p.position.y),
p.radius * p.lifeRatio,
Paint()..color = p.color.withOpacity(p.lifeRatio * 0.8),
);
}
}
bool shouldRepaint(covariant RepulsionPainter old) => true;
}
/// 涡流场演示
class VortexFieldDemo extends StatefulWidget {
const VortexFieldDemo({super.key});
State<VortexFieldDemo> createState() => _VortexFieldDemoState();
}
class _VortexFieldDemoState extends State<VortexFieldDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
final ParticleSystem _system = ParticleSystem(maxParticles: 300, damping: 0.995);
final Random _random = Random();
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
)..repeat();
_system.addForceField(VortexField(Vector2D(150, 200), 30, 80, 150));
_system.addForceField(VortexField(Vector2D(250, 200), 30, -80, 150));
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('涡流场')),
body: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
if (_system.particles.length < 200) {
_system.addParticle(Particle(
position: Vector2D(_random.nextDouble() * 400, _random.nextDouble() * 400),
velocity: Vector2D.fromAngle(_random.nextDouble() * pi * 2, 10),
color: HSVColor.fromAHSV(1, (_random.nextDouble() * 120 + 260) % 360, 0.8, 1).toColor(),
maxLife: 10,
));
}
_system.update(0.016);
return CustomPaint(
painter: VortexPainter(_system),
size: Size.infinite,
);
},
),
);
}
}
class VortexPainter extends CustomPainter {
final ParticleSystem system;
VortexPainter(this.system);
void paint(Canvas canvas, Size size) {
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height),
Paint()..color = const Color(0xFF0a0515));
for (final f in system.forceFields) {
if (f is VortexField) {
canvas.drawCircle(
Offset(f.position.x, f.position.y),
f.range,
Paint()..color = Colors.purple.withOpacity(0.15)..style = PaintingStyle.stroke,
);
canvas.drawCircle(
Offset(f.position.x, f.position.y),
6,
Paint()..color = Colors.purple,
);
}
}
for (final p in system.particles) {
canvas.drawCircle(
Offset(p.position.x, p.position.y),
p.radius * p.lifeRatio,
Paint()..color = p.color.withOpacity(p.lifeRatio * 0.7),
);
}
}
bool shouldRepaint(covariant VortexPainter old) => true;
}
/// 音乐引力场演示
class MusicGravityDemo extends StatefulWidget {
const MusicGravityDemo({super.key});
State<MusicGravityDemo> createState() => _MusicGravityDemoState();
}
class _MusicGravityDemoState extends State<MusicGravityDemo> with TickerProviderStateMixin {
late AnimationController _animController;
late AudioPlayer _audioPlayer;
final ParticleSystem _system = ParticleSystem(maxParticles: 500);
final Random _random = Random();
Float32List _audioData = Float32List(128);
bool _isPlaying = false;
Duration _position = Duration.zero;
Duration _duration = Duration.zero;
double _energy = 0;
double _bass = 0;
late GravityField _centerGravity;
static const String _audioUrl =
'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3';
void initState() {
super.initState();
_initAudio();
_animController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 16),
)..repeat();
_animController.addListener(_update);
_centerGravity = GravityField(Vector2D(200, 200), 300, 300);
_system.addForceField(_centerGravity);
}
Future<void> _initAudio() async {
_audioPlayer = AudioPlayer();
final session = await AudioSession.instance;
await session.configure(const AudioSessionConfiguration.music());
_audioPlayer.playerStateStream.listen((state) {
setState(() => _isPlaying = state.playing);
});
_audioPlayer.positionStream.listen((pos) {
setState(() => _position = pos);
});
_audioPlayer.durationStream.listen((dur) {
setState(() => _duration = dur ?? Duration.zero);
});
try {
await _audioPlayer.setUrl(_audioUrl);
} catch (e) {
debugPrint('加载音频失败: $e');
}
}
void _update() {
final time = DateTime.now().millisecondsSinceEpoch / 1000.0;
for (int i = 0; i < 128; i++) {
if (_isPlaying) {
final freq = (i / 128) * 8 + 1;
final wave = sin(time * freq) * 0.4 + sin(time * freq * 1.5) * 0.3;
final bassBoost = i < 32 ? 0.3 : 0;
_audioData[i] = _audioData[i] * 0.85 + (wave + bassBoost) * 0.15;
} else {
_audioData[i] *= 0.95;
}
}
double totalEnergy = 0, bassEnergy = 0;
for (int i = 0; i < 128; i++) {
totalEnergy += _audioData[i].abs();
if (i < 32) bassEnergy += _audioData[i].abs();
}
_energy = totalEnergy / 128;
_bass = bassEnergy / 32;
_centerGravity.strength = 200 + _bass * 500;
if (_system.particles.length < 300) {
final hue = ((time * 30) % 360).abs();
_system.addParticle(Particle(
position: Vector2D(200 + sin(time * 2) * 50, 200 + cos(time * 2) * 50),
velocity: Vector2D.fromAngle(time * 3, 30 + _energy * 80),
color: HSVColor.fromAHSV(1, hue, 0.8, 1).toColor(),
radius: 3 + _bass * 4,
maxLife: 5,
));
}
_system.update(0.016);
setState(() {});
}
void dispose() {
_animController.dispose();
_audioPlayer.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('音乐引力场')),
body: Stack(
children: [
CustomPaint(
painter: MusicGravityPainter(_system, _centerGravity, _energy, _isPlaying),
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.7),
borderRadius: BorderRadius.circular(16),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('🎵 SoundHelix - Song 1',
style: TextStyle(color: Colors.white, fontSize: 14)),
const SizedBox(height: 12),
Slider(
value: _duration.inMilliseconds > 0
? _position.inMilliseconds.toDouble().clamp(0, _duration.inMilliseconds.toDouble())
: 0,
max: _duration.inMilliseconds > 0 ? _duration.inMilliseconds.toDouble() : 1,
onChanged: (v) => _audioPlayer.seek(Duration(milliseconds: v.toInt())),
),
IconButton(
icon: Icon(_isPlaying ? Icons.pause : Icons.play_arrow,
color: Colors.indigo, size: 40),
onPressed: () => _isPlaying ? _audioPlayer.pause() : _audioPlayer.play(),
),
],
),
);
}
}
class MusicGravityPainter extends CustomPainter {
final ParticleSystem system;
final GravityField field;
final double energy;
final bool isPlaying;
MusicGravityPainter(this.system, this.field, this.energy, this.isPlaying);
void paint(Canvas canvas, Size size) {
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height),
Paint()..color = Color.lerp(const Color(0xFF050510), const Color(0xFF100520), energy)!);
canvas.drawCircle(
Offset(field.position.x, field.position.y),
field.range,
Paint()..color = Colors.indigo.withOpacity(0.1 + energy * 0.1)
..style = PaintingStyle.stroke,
);
final centerPaint = Paint()
..shader = RadialGradient(
colors: [Colors.indigo.withOpacity(0.8), Colors.indigo.withOpacity(0)],
).createShader(Rect.fromCircle(
center: Offset(field.position.x, field.position.y),
radius: 30,
));
canvas.drawCircle(Offset(field.position.x, field.position.y), 30, centerPaint);
for (final p in system.particles) {
final paint = Paint()..color = p.color.withOpacity(p.lifeRatio * 0.8);
if (isPlaying && p.velocity.magnitude > 30) {
paint.maskFilter = const MaskFilter.blur(BlurStyle.normal, 2);
}
canvas.drawCircle(
Offset(p.position.x, p.position.y),
p.radius * p.lifeRatio,
paint,
);
}
}
bool shouldRepaint(covariant MusicGravityPainter old) => true;
}
/// 综合演示
class FullGravityDemo extends StatefulWidget {
const FullGravityDemo({super.key});
State<FullGravityDemo> createState() => _FullGravityDemoState();
}
class _FullGravityDemoState extends State<FullGravityDemo> with TickerProviderStateMixin {
late AnimationController _animController;
late AudioPlayer _audioPlayer;
final ParticleSystem _system = ParticleSystem(maxParticles: 600, damping: 0.98);
final Random _random = Random();
Float32List _audioData = Float32List(128);
bool _isPlaying = false;
Duration _position = Duration.zero;
Duration _duration = Duration.zero;
double _energy = 0, _bass = 0, _mid = 0, _treble = 0;
late GravityField _centerGravity;
late VortexField _vortex1, _vortex2;
static const String _audioUrl =
'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3';
void initState() {
super.initState();
_initAudio();
_animController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 16),
)..repeat();
_animController.addListener(_update);
_centerGravity = GravityField(Vector2D(200, 200), 300, 250);
_vortex1 = VortexField(Vector2D(120, 200), 20, 60, 120);
_vortex2 = VortexField(Vector2D(280, 200), 20, -60, 120);
_system.addForceField(_centerGravity);
_system.addForceField(_vortex1);
_system.addForceField(_vortex2);
_system.addForceField(RepulsionField(Vector2D(30, 30), 3000, 60));
_system.addForceField(RepulsionField(Vector2D(370, 30), 3000, 60));
_system.addForceField(RepulsionField(Vector2D(30, 370), 3000, 60));
_system.addForceField(RepulsionField(Vector2D(370, 370), 3000, 60));
}
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));
_audioPlayer.positionStream.listen((p) => setState(() => _position = p));
_audioPlayer.durationStream.listen((d) => setState(() => _duration = d ?? Duration.zero));
try {
await _audioPlayer.setUrl(_audioUrl);
} catch (e) {
debugPrint('加载失败: $e');
}
}
void _update() {
final time = DateTime.now().millisecondsSinceEpoch / 1000.0;
for (int i = 0; i < 128; i++) {
if (_isPlaying) {
final freq = (i / 128) * 8 + 1;
final wave = sin(time * freq) * 0.4 + sin(time * freq * 1.5) * 0.3;
final bass = i < 32 ? 0.3 : 0;
_audioData[i] = _audioData[i] * 0.85 + (wave + bass) * 0.15;
} else {
_audioData[i] *= 0.95;
}
}
double total = 0, bassE = 0, midE = 0, trebleE = 0;
for (int i = 0; i < 128; i++) {
final v = _audioData[i].abs();
total += v;
if (i < 32) bassE += v;
else if (i < 96) midE += v;
else trebleE += v;
}
_energy = total / 128;
_bass = bassE / 32;
_mid = midE / 64;
_treble = trebleE / 32;
_centerGravity.strength = 200 + _bass * 400;
_centerGravity.position = Vector2D(
200 + sin(time * 0.5) * 30 * _energy,
200 + cos(time * 0.5) * 30 * _energy,
);
_vortex1.angularStrength = 40 + _mid * 100;
_vortex2.angularStrength = -40 - _mid * 100;
if (_system.particles.length < 400) {
final hue = (time * 25) % 360;
_system.addParticle(Particle(
position: Vector2D(
200 + sin(time * 2) * 60,
200 + cos(time * 2) * 60,
),
velocity: Vector2D.fromAngle(time * 3, 20 + _energy * 60),
color: HSVColor.fromAHSV(1, hue, 0.8, 1).toColor(),
radius: 2 + _bass * 3,
maxLife: 6,
));
}
_system.update(0.016);
setState(() {});
}
void dispose() {
_animController.dispose();
_audioPlayer.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
CustomPaint(
painter: FullGravityPainter(_system, _centerGravity, _vortex1, _vortex2,
_energy, _isPlaying),
size: Size.infinite,
),
Positioned(
top: 40,
left: 20,
child: SafeArea(
child: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
),
),
Positioned(
bottom: 40,
left: 20,
right: 20,
child: _buildControls(),
),
],
),
);
}
Widget _buildControls() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.7),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.indigo.withOpacity(0.3)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('🌌 粒子引力场',
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
const Text('SoundHelix - Song 1',
style: TextStyle(color: Colors.white70, fontSize: 14)),
const SizedBox(height: 16),
Slider(
value: _duration.inMilliseconds > 0
? _position.inMilliseconds.toDouble().clamp(0, _duration.inMilliseconds.toDouble())
: 0,
max: _duration.inMilliseconds > 0 ? _duration.inMilliseconds.toDouble() : 1,
onChanged: (v) => _audioPlayer.seek(Duration(milliseconds: v.toInt())),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.replay_10, color: Colors.white70),
onPressed: () {
final newPos = _position - const Duration(seconds: 10);
_audioPlayer.seek(newPos.isNegative ? Duration.zero : newPos);
},
),
const SizedBox(width: 20),
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [Colors.indigo.shade400, Colors.indigo.shade700],
),
),
child: IconButton(
icon: Icon(_isPlaying ? Icons.pause : Icons.play_arrow,
color: Colors.white, size: 36),
onPressed: () => _isPlaying ? _audioPlayer.pause() : _audioPlayer.play(),
),
),
const SizedBox(width: 20),
IconButton(
icon: const Icon(Icons.forward_10, color: Colors.white70),
onPressed: () {
final newPos = _position + const Duration(seconds: 10);
_audioPlayer.seek(newPos > _duration ? _duration : newPos);
},
),
],
),
],
),
);
}
}
class FullGravityPainter extends CustomPainter {
final ParticleSystem system;
final GravityField gravity;
final VortexField vortex1, vortex2;
final double energy;
final bool isPlaying;
FullGravityPainter(this.system, this.gravity, this.vortex1, this.vortex2,
this.energy, this.isPlaying);
void paint(Canvas canvas, Size size) {
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height),
Paint()..color = Color.lerp(const Color(0xFF050510), const Color(0xFF150525), energy)!);
canvas.drawCircle(
Offset(gravity.position.x, gravity.position.y),
gravity.range,
Paint()..color = Colors.indigo.withOpacity(0.08 + energy * 0.08)
..style = PaintingStyle.stroke,
);
final centerPaint = Paint()
..shader = RadialGradient(
colors: [Colors.indigo.withOpacity(0.6), Colors.indigo.withOpacity(0)],
).createShader(Rect.fromCircle(
center: Offset(gravity.position.x, gravity.position.y),
radius: 25,
));
canvas.drawCircle(Offset(gravity.position.x, gravity.position.y), 25, centerPaint);
canvas.drawCircle(Offset(gravity.position.x, gravity.position.y), 5,
Paint()..color = Colors.white);
for (final v in [vortex1, vortex2]) {
canvas.drawCircle(
Offset(v.position.x, v.position.y),
v.range,
Paint()..color = Colors.purple.withOpacity(0.1)..style = PaintingStyle.stroke,
);
canvas.drawCircle(
Offset(v.position.x, v.position.y),
6,
Paint()..color = Colors.purple.withOpacity(0.8),
);
}
for (final f in system.forceFields) {
if (f is RepulsionField) {
canvas.drawCircle(
Offset(f.position.x, f.position.y),
f.range,
Paint()..color = Colors.red.withOpacity(0.1 + energy * 0.1)
..style = PaintingStyle.stroke,
);
canvas.drawCircle(
Offset(f.position.x, f.position.y),
5,
Paint()..color = Colors.red.withOpacity(0.6),
);
}
}
for (final p in system.particles) {
final paint = Paint()..color = p.color.withOpacity(p.lifeRatio * 0.75);
if (isPlaying && p.velocity.magnitude > 40) {
paint.maskFilter = const MaskFilter.blur(BlurStyle.normal, 2);
}
canvas.drawCircle(
Offset(p.position.x, p.position.y),
p.radius * (0.5 + p.lifeRatio * 0.5),
paint,
);
}
}
bool shouldRepaint(covariant FullGravityPainter old) => true;
}
📝 五、总结
本篇文章深入探讨了粒子物理系统在音乐可视化中的应用,从牛顿万有引力定律到多种力场的实现,构建了具有"宇宙感"的粒子动画效果。
✅ 核心知识点回顾
| 知识点 | 说明 |
|---|---|
| 🌌粒子系统 | 位置、速度、加速度、生命周期 |
| 🔵引力场 | F ∝ 1/r²,聚集效果 |
| 🔴斥力场 | F ∝ -1/r²,扩散效果 |
| 🟣涡流场 | 切向力,旋转效果 |
| 🎵音频驱动 | 低音控制引力,中频控制涡流 |
| 🔊网络音乐 | just_audio_ohos 在线播放 |
⭐ 最佳实践要点
- ✅ 使用欧拉积分更新粒子状态
- ✅ 设置最小距离避免力过大
- ✅ 添加阻尼使系统稳定
- ✅ 粒子生命周期管理避免内存泄漏
🚀 进阶方向
- 🔮 使用 Verlet 积分提升精度
- ✨ 添加粒子间相互作用力
- 👆 触摸交互创建临时力场
- ⚡ 使用 Isolate 并行计算
更多推荐




所有评论(0)