
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
🌀 一、混沌理论:确定性与不可预测性的统一
📚 1.1 混沌理论的诞生
混沌理论是20世纪最重要的科学发现之一,它揭示了看似随机的现象背后可能隐藏着确定性的规律。
历史里程碑:
| 年份 | 人物 | 贡献 |
|---|---|---|
| 1890 | 庞加莱 | 三体问题中的混沌现象 |
| 1963 | 洛伦兹 | 发现洛伦兹吸引子,蝴蝶效应 |
| 1975 | 李天岩-约克 | 首次提出"混沌"术语 |
| 1976 | 罗斯勒 | 罗斯勒吸引子 |
| 1976 | 费根鲍姆 | 普适常数发现 |
| 1983 | 格拉斯伯格-普罗卡西亚 | GP算法,吸引子重构 |
洛伦兹的发现:
1963年,气象学家爱德华·洛伦兹在研究大气对流时,
发现了一个惊人的现象:
初始条件的微小差异,会导致结果的巨大不同。
这就是著名的"蝴蝶效应":
"一只南美洲亚马逊河流域热带雨林中的蝴蝶,
偶尔扇动几下翅膀,可以在两周以后引起
美国得克萨斯州的一场龙卷风。"
------ 爱德华·洛伦兹(1917-2008)
🔬 1.2 混沌系统的特征
混沌系统具有以下核心特征:
1. 对初始条件的敏感依赖性
两个初始条件极其接近的点,随着时间演化,
它们的轨迹会指数级分离。
数学描述:
|δZ(t)| ≈ e^(λt)|δZ(0)|
其中 λ 是李雅普诺夫指数:
- λ > 0:混沌系统
- λ < 0:稳定系统
- λ = 0:临界状态
2. 确定性与不可预测性
混沌系统是完全确定性的(由确定性方程描述),
但长期行为不可预测。
这不是因为方程有随机性,
而是因为初始条件无法无限精确测量。
示例:
双摆系统 - 完全确定,但长期不可预测
3. 奇异吸引子
混沌系统在相空间中的轨迹会被吸引到一个
复杂的几何结构上,称为奇异吸引子。
特点:
- 分形结构(自相似性)
- 非整数维数
- 轨迹永不重复
- 有界但非周期
4. 分岔与倍周期
随着参数变化,系统会经历分岔:
周期1 → 周期2 → 周期4 → ... → 混沌
费根鲍姆常数:
δ = 4.669201609...
这是自然界的一个普适常数!
📐 1.3 相空间与吸引子
相空间的概念:
相空间是描述系统状态的多维空间。
对于三维系统,相空间中的每个点代表:
(x, y, z) → 系统的一个状态
轨迹:系统状态随时间的演化路径
吸引子:轨迹最终收敛到的区域
吸引子类型:
| 类型 | 维数 | 特征 | 示例 |
|---|---|---|---|
| 固定点 | 0 | 稳定平衡态 | 阻尼振子 |
| 极限环 | 1 | 周期运动 | 心跳 |
| 极限环面 | 2 | 准周期运动 | 耦合振子 |
| 奇异吸引子 | 分数 | 混沌运动 | 洛伦兹 |
🦋 二、洛伦兹吸引子:混沌的标志
📊 2.1 洛伦兹方程
洛伦兹方程是混沌理论的基石,描述了大气对流的简化模型:
方程组:
dx/dt = σ(y - x)
dy/dt = x(ρ - z) - y
dz/dt = xy - βz
经典参数:
σ = 10 (普朗特数)
ρ = 28 (瑞利数)
β = 8/3 (几何因子)
当 ρ > 24.74 时,系统进入混沌状态!
参数的物理意义:
σ (sigma) - 普朗特数:
描述动量扩散与热扩散的比值
σ = ν/α (粘性系数/热扩散系数)
ρ (rho) - 瑞利数:
描述对流强度
ρ = Ra/Rac (实际瑞利数/临界瑞利数)
β (beta) - 几何因子:
与对流层的纵横比相关
🎨 2.2 洛伦兹吸引子的几何特征
吸引子的形状:
洛伦兹吸引子呈蝴蝶状,有两个"翅膀":
左翼 右翼
╱ ╲ ╱ ╲
╱ ╲ ╱ ╲
╱ ● ╲ ╱ ● ╲
╱ ╲ ╱ ╲
╱ ╲╱ ╲
╱ ● ╲
╱ ╱╲ ╲
╱ ╱ ╲ ╲
轨迹在两个翼之间随机跳跃,
永远不会重复相同的路径!
分形维数:
洛伦兹吸引子的维数约为 2.06
这意味着它比二维平面"厚"一点,
但比三维空间"薄"很多。
计算方法(盒计数法):
D = lim(log(N(ε))/log(1/ε))
ε→0
其中 N(ε) 是覆盖吸引子所需的边长为 ε 的盒子数
💻 2.3 洛伦兹吸引子的数值求解
四阶龙格-库塔方法:
dart
// 洛伦兹方程的数值求解
class LorenzSolver {
final double sigma, rho, beta;
LorenzSolver({
this.sigma = 10,
this.rho = 28,
this.beta = 8 / 3,
});
// 计算导数
List<double> derivatives(List<double> state) {
final x = state[0], y = state[1], z = state[2];
return [
sigma * (y - x), // dx/dt
x * (rho - z) - y, // dy/dt
x * y - beta * z, // dz/dt
];
}
// 四阶龙格-库塔积分
List<double> step(List<double> state, double dt) {
final k1 = derivatives(state);
final k2 = derivatives([
state[0] + k1[0] * dt / 2,
state[1] + k1[1] * dt / 2,
state[2] + k1[2] * dt / 2,
]);
final k3 = derivatives([
state[0] + k2[0] * dt / 2,
state[1] + k2[1] * dt / 2,
state[2] + k2[2] * dt / 2,
]);
final k4 = derivatives([
state[0] + k3[0] * dt,
state[1] + k3[1] * dt,
state[2] + k3[2] * dt,
]);
return [
state[0] + (k1[0] + 2 * k2[0] + 2 * k3[0] + k4[0]) * dt / 6,
state[1] + (k1[1] + 2 * k2[1] + 2 * k3[1] + k4[1]) * dt / 6,
state[2] + (k1[2] + 2 * k2[2] + 2 * k3[2] + k4[2]) * dt / 6,
];
}
}
🔄 三、其他经典奇异吸引子
🌀 3.1 罗斯勒吸引子
罗斯勒吸引子是比洛伦兹更简单的混沌系统:
方程组:
dx/dt = -y - z
dy/dt = x + ay
dz/dt = b + z(x - c)
经典参数:
a = 0.2
b = 0.2
c = 5.7
特点:
- 只有一个非线性项
- 单螺旋结构
- 更容易分析
几何特征:
罗斯勒吸引子呈螺旋带状:
╱╲
╱ ╲
╱ ╲
╱ ╲
╱ ╲
╱ ╲
╱ ╲
╱ ╲
╱ ╲
╱ ╲
轨迹在螺旋上旋转,同时向外扩展,
然后突然跳回内部,重新开始循环。
💫 3.2 陈氏吸引子
陈氏吸引子是洛伦兹系统的变体:
方程组:
dx/dt = a(y - x)
dy/dt = (c - a)x - xz + cy
dz/dt = xy - bz
经典参数:
a = 40
b = 3
c = 28
特点:
- 双螺旋结构
- 比洛伦兹更复杂的混沌行为
- 由陈关荣于1999年发现
🎭 3.3 埃农映射
埃农映射是二维离散混沌系统:
迭代方程:
x(n+1) = 1 - a·x(n)² + y(n)
y(n+1) = b·x(n)
经典参数:
a = 1.4
b = 0.3
特点:
- 离散系统
- 分形结构
- 可视化简单
分形结构:
埃农映射产生的点集具有自相似性:
放大任意局部,都能看到相似的结构:
┌────────────────┐
│ · · · · │
│ · · · · │
│ · · · │
│ · · · · │
│ · · · · │
└────────────────┘
每次放大都会发现新的细节!
🌊 3.4 双摆混沌
双摆是经典的物理混沌系统:
运动方程:
θ₁'' = [−g(2m₁+m₂)sinθ₁ − m₂g sin(θ₁−2θ₂)
− 2sin(θ₁−θ₂)m₂(θ₂'²L₂ + θ₁'²L₁cos(θ₁−θ₂))]
/ [L₁(2m₁+m₂−m₂cos(2θ₁−2θ₂))]
θ₂'' = [2sin(θ₁−θ₂)(θ₁'²L₁(m₁+m₂)
+ g(m₁+m₂)cosθ₁ + θ₂'²L₂m₂cos(θ₁−θ₂))]
/ [L₂(2m₁+m₂−m₂cos(2θ₁−2θ₂))]
特点:
- 真实物理系统
- 对初始条件极度敏感
- 轨迹永不重复
🎵 四、混沌与音乐的结合
🎼 4.1 混沌音乐生成
混沌系统可以用来生成音乐:
音高映射:
将混沌系统的输出映射到音高:
x(t) → 音高 (MIDI 音符)
范围:[−20, 20] → [48, 84] (C3 到 C6)
映射公式:
note = 48 + (x + 20) / 40 * 36
示例:
x = -20 → note = 48 (C3)
x = 0 → note = 66 (F4)
x = 20 → note = 84 (C6)
节奏映射:
将混沌系统的输出映射到节奏:
z(t) → 音符持续时间
范围:[0, 50] → [八分音符, 全音符]
映射公式:
duration = 0.25 + z / 50 * 0.75 (拍)
示例:
z = 0 → 0.25 拍 (八分音符)
z = 25 → 0.625 拍
z = 50 → 1.0 拍 (全音符)
力度映射:
将混沌系统的输出映射到力度:
y(t) → 力度 (MIDI velocity)
范围:[−30, 30] → [30, 127]
映射公式:
velocity = 30 + (y + 30) / 60 * 97
示例:
y = -30 → velocity = 30 (弱)
y = 0 → velocity = 78 (中)
y = 30 → velocity = 127 (强)
🎚️ 4.2 音频驱动的混沌可视化
将音频特征映射到混沌系统参数:
能量映射:
音频能量 → 混沌参数
能量低:ρ = 20 (周期行为)
能量高:ρ = 35 (强混沌)
映射公式:
ρ = 20 + energy * 15
效果:
- 低能量时轨迹稳定
- 高能量时轨迹剧烈变化
频率映射:
音频频率 → 混沌参数
低频主导:σ = 8 (缓慢变化)
高频主导:σ = 15 (快速变化)
映射公式:
σ = 8 + treble * 7
效果:
- 低频时轨迹平滑
- 高频时轨迹抖动
🌈 4.3 颜色与混沌
轨迹颜色映射:
dart
// 根据混沌状态计算颜色
Color getChaosColor(double x, double y, double z, double time) {
// 使用状态变量计算色相
final hue = (atan2(y, x) * 180 / pi + 180 + time * 10) % 360;
// 使用 z 坐标计算饱和度
final saturation = 0.5 + (z / 50) * 0.5;
// 使用速度计算亮度
final speed = sqrt(x * x + y * y + z * z);
final brightness = 0.5 + (speed / 50) * 0.5;
return HSVColor.fromAHSV(1, hue, saturation, brightness).toColor();
}
📊 五、完整代码实现
以下是完整的可运行代码,可以直接替换到 main.dart 中使用:
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 ChaosApp());
}
class ChaosApp extends StatelessWidget {
const ChaosApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '混沌吸引子',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.cyan, brightness: Brightness.dark),
useMaterial3: true,
),
home: const ChaosHomePage(),
debugShowCheckedModeBanner: false,
);
}
}
class ChaosHomePage extends StatelessWidget {
const ChaosHomePage({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.flutter_dash, color: Colors.cyan,
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const LorenzAttractorDemo()))),
_buildCard(context, title: '罗斯勒吸引子', description: '单螺旋混沌系统', icon: Icons.all_inclusive, color: Colors.purple,
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const RosslerAttractorDemo()))),
_buildCard(context, title: '陈氏吸引子', description: '双螺旋混沌变体', icon: Icons.sync_alt, color: Colors.orange,
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const ChenAttractorDemo()))),
_buildCard(context, title: '埃农映射', description: '离散混沌分形', icon: Icons.scatter_plot, color: Colors.pink,
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const HenonMapDemo()))),
_buildCard(context, title: '音乐混沌', description: '音频驱动的混沌可视化', icon: Icons.music_note, color: Colors.teal,
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const MusicChaosDemo()))),
_buildCard(context, title: '双摆模拟', description: '物理混沌系统', icon: Icons.accessibility, color: Colors.amber,
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const DoublePendulumDemo()))),
]),
);
}
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 LorenzAttractorDemo extends StatefulWidget {
const LorenzAttractorDemo({super.key});
@override
State<LorenzAttractorDemo> createState() => _LorenzAttractorDemoState();
}
class _LorenzAttractorDemoState extends State<LorenzAttractorDemo> with TickerProviderStateMixin {
late AnimationController _animController;
late AudioPlayer _audioPlayer;
double _sigma = 10, _rho = 28, _beta = 8 / 3;
double _x = 1, _y = 1, _z = 1;
final List<Offset> _trajectory = [];
final List<double> _zValues = [];
int _maxPoints = 5000;
bool _isPlaying = false;
Duration _position = Duration.zero;
Duration _duration = Duration.zero;
double _energy = 0, _bass = 0;
double _rotationY = 0;
double _scale = 15;
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: 16))..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));
_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 effectiveRho = _rho + _energy * 10;
final effectiveSigma = _sigma + _bass * 5;
final dt = 0.005;
for (int i = 0; i < 3; i++) {
final k1x = effectiveSigma * (_y - _x);
final k1y = _x * (effectiveRho - _z) - _y;
final k1z = _x * _y - _beta * _z;
final k2x = effectiveSigma * ((_y + k1y * dt / 2) - (_x + k1x * dt / 2));
final k2y = (_x + k1x * dt / 2) * (effectiveRho - (_z + k1z * dt / 2)) - (_y + k1y * dt / 2);
final k2z = (_x + k1x * dt / 2) * (_y + k1y * dt / 2) - _beta * (_z + k1z * dt / 2);
final k3x = effectiveSigma * ((_y + k2y * dt / 2) - (_x + k2x * dt / 2));
final k3y = (_x + k2x * dt / 2) * (effectiveRho - (_z + k2z * dt / 2)) - (_y + k2y * dt / 2);
final k3z = (_x + k2x * dt / 2) * (_y + k2y * dt / 2) - _beta * (_z + k2z * dt / 2);
final k4x = effectiveSigma * ((_y + k3y * dt) - (_x + k3x * dt));
final k4y = (_x + k3x * dt) * (effectiveRho - (_z + k3z * dt)) - (_y + k3y * dt);
final k4z = (_x + k3x * dt) * (_y + k3y * dt) - _beta * (_z + k3z * dt);
_x += (k1x + 2 * k2x + 2 * k3x + k4x) * dt / 6;
_y += (k1y + 2 * k2y + 2 * k3y + k4y) * dt / 6;
_z += (k1z + 2 * k2z + 2 * k3z + k4z) * dt / 6;
}
_rotationY += 0.003;
final px = _x * _scale;
final py = _z * _scale * 0.5 - _y * _scale * 0.3;
_trajectory.add(Offset(px, py));
_zValues.add(_z);
if (_trajectory.length > _maxPoints) {
_trajectory.removeAt(0);
_zValues.removeAt(0);
}
if (_isPlaying) {
_energy = _energy * 0.95 + (sin(DateTime.now().millisecondsSinceEpoch * 0.001) * 0.5 + 0.5) * 0.05;
_bass = _bass * 0.95 + (sin(DateTime.now().millisecondsSinceEpoch * 0.0005) * 0.5 + 0.5) * 0.05;
} else {
_energy *= 0.98;
_bass *= 0.98;
}
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: LorenzPainter(_trajectory, _zValues, _energy), 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: [
IconButton(icon: Icon(_isPlaying ? Icons.pause : Icons.play_arrow, color: Colors.cyan, size: 36),
onPressed: () => _isPlaying ? _audioPlayer.pause() : _audioPlayer.play()),
const SizedBox(width: 16),
Expanded(child: 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(children: [
Expanded(child: _buildParamSlider('ρ', _rho, 15, 40, (v) => _rho = v, Colors.orange)),
Expanded(child: _buildParamSlider('σ', _sigma, 5, 15, (v) => _sigma = v, Colors.purple)),
Expanded(child: _buildParamSlider('β', _beta, 1, 5, (v) => _beta = v, Colors.teal)),
]),
],
),
);
}
Widget _buildParamSlider(String label, double value, double min, double max, Function(double) onChanged, Color color) {
return Column(children: [
Text('$label: ${value.toStringAsFixed(1)}', style: const TextStyle(color: Colors.white70, fontSize: 11)),
Slider(value: value, min: min, max: max, onChanged: (v) => setState(() => onChanged(v)), activeColor: color),
]);
}
}
class LorenzPainter extends CustomPainter {
final List<Offset> trajectory;
final List<double> zValues;
final double energy;
LorenzPainter(this.trajectory, this.zValues, this.energy);
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final bgPaint = Paint()..shader = RadialGradient(
colors: [const Color(0xFF0a0a20), const Color(0xFF050510)],
).createShader(Rect.fromLTWH(0, 0, size.width, size.height));
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), bgPaint);
if (trajectory.length < 2) return;
for (int i = 1; i < trajectory.length; i++) {
final p1 = trajectory[i - 1] + center;
final p2 = trajectory[i] + center;
final z = zValues[i];
final hue = (z * 3 + i * 0.02) % 360;
final alpha = (i / trajectory.length * 0.8 + 0.2).clamp(0.0, 1.0);
final paint = Paint()
..color = HSVColor.fromAHSV(alpha, hue, 0.8, 1).toColor()
..strokeWidth = 1 + energy * 2
..strokeCap = StrokeCap.round;
canvas.drawLine(p1, p2, paint);
}
if (trajectory.isNotEmpty) {
final lastPoint = trajectory.last + center;
canvas.drawCircle(lastPoint, 4 + energy * 3, Paint()..color = Colors.white);
canvas.drawCircle(lastPoint, 2 + energy * 2, Paint()..color = Colors.cyan);
}
}
@override
bool shouldRepaint(covariant LorenzPainter old) => true;
}
/// 罗斯勒吸引子演示
class RosslerAttractorDemo extends StatefulWidget {
const RosslerAttractorDemo({super.key});
@override
State<RosslerAttractorDemo> createState() => _RosslerAttractorDemoState();
}
class _RosslerAttractorDemoState extends State<RosslerAttractorDemo> with SingleTickerProviderStateMixin {
late AnimationController _controller;
double _a = 0.2, _b = 0.2, _c = 5.7;
double _x = 1, _y = 1, _z = 1;
final List<Offset> _trajectory = [];
final List<double> _zValues = [];
double _time = 0;
double _scale = 8;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 16))..repeat();
_controller.addListener(() {
_time += 0.016;
_updateAttractor();
setState(() {});
});
}
void _updateAttractor() {
final dt = 0.01;
for (int i = 0; i < 3; i++) {
final k1x = -_y - _z;
final k1y = _x + _a * _y;
final k1z = _b + _z * (_x - _c);
final k2x = -(_y + k1y * dt / 2) - (_z + k1z * dt / 2);
final k2y = (_x + k1x * dt / 2) + _a * (_y + k1y * dt / 2);
final k2z = _b + (_z + k1z * dt / 2) * ((_x + k1x * dt / 2) - _c);
final k3x = -(_y + k2y * dt / 2) - (_z + k2z * dt / 2);
final k3y = (_x + k2x * dt / 2) + _a * (_y + k2y * dt / 2);
final k3z = _b + (_z + k2z * dt / 2) * ((_x + k2x * dt / 2) - _c);
final k4x = -(_y + k3y * dt) - (_z + k3z * dt);
final k4y = (_x + k3x * dt) + _a * (_y + k3y * dt);
final k4z = _b + (_z + k3z * dt) * ((_x + k3x * dt) - _c);
_x += (k1x + 2 * k2x + 2 * k3x + k4x) * dt / 6;
_y += (k1y + 2 * k2y + 2 * k3y + k4y) * dt / 6;
_z += (k1z + 2 * k2z + 2 * k3z + k4z) * dt / 6;
}
final px = _x * _scale;
final py = _z * _scale * 0.5 - _y * _scale * 0.3;
_trajectory.add(Offset(px, py));
_zValues.add(_z);
if (_trajectory.length > 4000) {
_trajectory.removeAt(0);
_zValues.removeAt(0);
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('罗斯勒吸引子')),
body: Stack(children: [
CustomPaint(painter: RosslerPainter(_trajectory, _zValues, _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: Row(children: [
Expanded(child: _buildSlider('a', _a, 0.1, 0.4, (v) => setState(() => _a = v))),
Expanded(child: _buildSlider('b', _b, 0.1, 0.4, (v) => setState(() => _b = v))),
Expanded(child: _buildSlider('c', _c, 4, 8, (v) => setState(() => _c = v))),
]),
);
}
Widget _buildSlider(String label, double value, double min, double max, Function(double) onChanged) {
return Column(children: [
Text('$label: ${value.toStringAsFixed(2)}', style: const TextStyle(color: Colors.white70, fontSize: 11)),
Slider(value: value, min: min, max: max, onChanged: onChanged),
]);
}
}
class RosslerPainter extends CustomPainter {
final List<Offset> trajectory;
final List<double> zValues;
final double time;
RosslerPainter(this.trajectory, this.zValues, this.time);
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = const Color(0xFF0a0a15));
if (trajectory.length < 2) return;
for (int i = 1; i < trajectory.length; i++) {
final p1 = trajectory[i - 1] + center;
final p2 = trajectory[i] + center;
final hue = (zValues[i] * 20 + time * 10) % 360;
final alpha = (i / trajectory.length * 0.7 + 0.3).clamp(0.0, 1.0);
canvas.drawLine(p1, p2, Paint()..color = HSVColor.fromAHSV(alpha, hue, 0.8, 1).toColor()..strokeWidth = 1.5);
}
if (trajectory.isNotEmpty) {
canvas.drawCircle(trajectory.last + center, 3, Paint()..color = Colors.white);
}
}
@override
bool shouldRepaint(covariant RosslerPainter old) => true;
}
/// 陈氏吸引子演示
class ChenAttractorDemo extends StatefulWidget {
const ChenAttractorDemo({super.key});
@override
State<ChenAttractorDemo> createState() => _ChenAttractorDemoState();
}
class _ChenAttractorDemoState extends State<ChenAttractorDemo> with SingleTickerProviderStateMixin {
late AnimationController _controller;
double _a = 40, _b = 3, _c = 28;
double _x = -0.1, _y = 0.5, _z = -0.6;
final List<Offset> _trajectory = [];
final List<double> _zValues = [];
double _time = 0;
double _scale = 6;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 16))..repeat();
_controller.addListener(() {
_time += 0.016;
_updateAttractor();
setState(() {});
});
}
void _updateAttractor() {
final dt = 0.002;
for (int i = 0; i < 5; i++) {
final k1x = _a * (_y - _x);
final k1y = (_c - _a) * _x - _x * _z + _c * _y;
final k1z = _x * _y - _b * _z;
_x += k1x * dt;
_y += k1y * dt;
_z += k1z * dt;
}
final px = _x * _scale;
final py = _z * _scale * 0.5 - _y * _scale * 0.3;
_trajectory.add(Offset(px, py));
_zValues.add(_z);
if (_trajectory.length > 4000) {
_trajectory.removeAt(0);
_zValues.removeAt(0);
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('陈氏吸引子')),
body: Stack(children: [
CustomPaint(painter: ChenPainter(_trajectory, _zValues, _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: Row(children: [
Expanded(child: _buildSlider('a', _a, 30, 50, (v) => setState(() => _a = v))),
Expanded(child: _buildSlider('b', _b, 1, 5, (v) => setState(() => _b = v))),
Expanded(child: _buildSlider('c', _c, 20, 35, (v) => setState(() => _c = v))),
]),
);
}
Widget _buildSlider(String label, double value, double min, double max, Function(double) onChanged) {
return Column(children: [
Text('$label: ${value.toStringAsFixed(1)}', style: const TextStyle(color: Colors.white70, fontSize: 11)),
Slider(value: value, min: min, max: max, onChanged: onChanged),
]);
}
}
class ChenPainter extends CustomPainter {
final List<Offset> trajectory;
final List<double> zValues;
final double time;
ChenPainter(this.trajectory, this.zValues, this.time);
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = const Color(0xFF0a0510));
if (trajectory.length < 2) return;
for (int i = 1; i < trajectory.length; i++) {
final p1 = trajectory[i - 1] + center;
final p2 = trajectory[i] + center;
final hue = (zValues[i] * 5 + time * 15) % 360;
final alpha = (i / trajectory.length * 0.7 + 0.3).clamp(0.0, 1.0);
canvas.drawLine(p1, p2, Paint()..color = HSVColor.fromAHSV(alpha, hue, 0.9, 1).toColor()..strokeWidth = 1.5);
}
if (trajectory.isNotEmpty) {
canvas.drawCircle(trajectory.last + center, 3, Paint()..color = Colors.white);
}
}
@override
bool shouldRepaint(covariant ChenPainter old) => true;
}
/// 埃农映射演示
class HenonMapDemo extends StatefulWidget {
const HenonMapDemo({super.key});
@override
State<HenonMapDemo> createState() => _HenonMapDemoState();
}
class _HenonMapDemoState extends State<HenonMapDemo> with SingleTickerProviderStateMixin {
late AnimationController _controller;
double _a = 1.4, _b = 0.3;
double _x = 0.1, _y = 0.1;
final List<Offset> _points = [];
int _iterations = 0;
double _time = 0;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 16))..repeat();
_controller.addListener(() {
_time += 0.016;
_iterate();
setState(() {});
});
}
void _iterate() {
for (int i = 0; i < 50; i++) {
final newX = 1 - _a * _x * _x + _y;
final newY = _b * _x;
_x = newX;
_y = newY;
_points.add(Offset(_x * 150, _y * 150));
_iterations++;
}
if (_points.length > 50000) {
_points.removeRange(0, 1000);
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('埃农映射')),
body: Stack(children: [
CustomPaint(painter: HenonPainter(_points, _time), size: Size.infinite),
Positioned(top: 20, left: 20, child: Text('迭代: $_iterations', style: const TextStyle(color: Colors.white70))),
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: Row(children: [
Expanded(child: Column(children: [
Text('a: ${_a.toStringAsFixed(2)}', style: const TextStyle(color: Colors.white70, fontSize: 11)),
Slider(value: _a, min: 1, max: 1.5, onChanged: (v) => setState(() { _a = v; _points.clear(); _x = 0.1; _y = 0.1; _iterations = 0; })),
])),
Expanded(child: Column(children: [
Text('b: ${_b.toStringAsFixed(2)}', style: const TextStyle(color: Colors.white70, fontSize: 11)),
Slider(value: _b, min: 0.1, max: 0.5, onChanged: (v) => setState(() { _b = v; _points.clear(); _x = 0.1; _y = 0.1; _iterations = 0; })),
])),
]),
);
}
}
class HenonPainter extends CustomPainter {
final List<Offset> points;
final double time;
HenonPainter(this.points, this.time);
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = const Color(0xFF050510));
for (int i = 0; i < points.length; i++) {
final p = points[i] + center;
final hue = (i * 0.01 + time * 5) % 360;
canvas.drawCircle(p, 0.5, Paint()..color = HSVColor.fromAHSV(0.7, hue, 0.9, 1).toColor());
}
}
@override
bool shouldRepaint(covariant HenonPainter old) => true;
}
/// 音乐混沌演示
class MusicChaosDemo extends StatefulWidget {
const MusicChaosDemo({super.key});
@override
State<MusicChaosDemo> createState() => _MusicChaosDemoState();
}
class _MusicChaosDemoState extends State<MusicChaosDemo> with TickerProviderStateMixin {
late AnimationController _animController;
late AudioPlayer _audioPlayer;
double _x = 1, _y = 1, _z = 1;
final List<Offset> _trajectory = [];
final List<double> _zValues = [];
bool _isPlaying = false;
Duration _position = Duration.zero;
Duration _duration = Duration.zero;
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: 16))..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));
_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() {
_time += 0.016;
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 sigma = 10 + _treble * 10;
final rho = 28 + _energy * 15;
final beta = 8 / 3 + _bass * 2;
final dt = 0.005;
for (int i = 0; i < 3; i++) {
final k1x = sigma * (_y - _x);
final k1y = _x * (rho - _z) - _y;
final k1z = _x * _y - beta * _z;
_x += k1x * dt;
_y += k1y * dt;
_z += k1z * dt;
}
final scale = 12 + _energy * 5;
final px = _x * scale;
final py = _z * scale * 0.5 - _y * scale * 0.3;
_trajectory.add(Offset(px, py));
_zValues.add(_z);
if (_trajectory.length > 3000) {
_trajectory.removeAt(0);
_zValues.removeAt(0);
}
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: MusicChaosPainter(_trajectory, _zValues, _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.teal),
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.teal : Colors.grey[800],
borderRadius: BorderRadius.circular(12),
),
child: Text(_isPlaying ? '播放中' : '暂停', style: const TextStyle(color: Colors.white, fontSize: 12)),
),
]),
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())),
activeColor: Colors.teal,
),
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
IconButton(icon: Icon(_isPlaying ? Icons.pause : Icons.play_arrow, color: Colors.teal, size: 36),
onPressed: () => _isPlaying ? _audioPlayer.pause() : _audioPlayer.play()),
]),
_buildBandMeters(),
],
),
);
}
Widget _buildBandMeters() {
return 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)),
const SizedBox(width: 8),
Expanded(child: _buildMeter('能量', _energy, Colors.teal)),
]);
}
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: 40,
decoration: BoxDecoration(color: Colors.grey[800], borderRadius: BorderRadius.circular(4)),
child: Align(
alignment: Alignment.bottomCenter,
child: AnimatedContainer(
duration: const Duration(milliseconds: 50),
height: (value * 40).clamp(2.0, 40.0),
decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(4)),
),
),
),
]);
}
}
class MusicChaosPainter extends CustomPainter {
final List<Offset> trajectory;
final List<double> zValues;
final double energy;
final double time;
MusicChaosPainter(this.trajectory, this.zValues, this.energy, this.time);
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final bgColor = Color.lerp(const Color(0xFF050510), const Color(0xFF0a1520), energy)!;
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = bgColor);
if (trajectory.length < 2) return;
for (int i = 1; i < trajectory.length; i++) {
final p1 = trajectory[i - 1] + center;
final p2 = trajectory[i] + center;
final z = zValues[i];
final hue = (z * 5 + time * 20) % 360;
final alpha = (i / trajectory.length * 0.8 + 0.2).clamp(0.0, 1.0);
canvas.drawLine(p1, p2, Paint()..color = HSVColor.fromAHSV(alpha, hue, 0.9, 1).toColor()..strokeWidth = 1.5 + energy);
}
if (trajectory.isNotEmpty) {
final lastPoint = trajectory.last + center;
canvas.drawCircle(lastPoint, 5 + energy * 3, Paint()..color = Colors.white.withOpacity(0.9));
canvas.drawCircle(lastPoint, 3 + energy * 2, Paint()..color = Colors.teal);
}
}
@override
bool shouldRepaint(covariant MusicChaosPainter old) => true;
}
/// 双摆模拟演示
class DoublePendulumDemo extends StatefulWidget {
const DoublePendulumDemo({super.key});
@override
State<DoublePendulumDemo> createState() => _DoublePendulumDemoState();
}
class _DoublePendulumDemoState extends State<DoublePendulumDemo> with SingleTickerProviderStateMixin {
late AnimationController _controller;
// 双摆参数
double _L1 = 150, _L2 = 150;
double _m1 = 10, _m2 = 10;
double _theta1 = pi / 2, _theta2 = pi / 2;
double _omega1 = 0, _omega2 = 0;
double _g = 9.81;
final List<Offset> _trail = [];
double _time = 0;
bool _showTrail = true;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 16))..repeat();
_controller.addListener(() {
_time += 0.016;
_updatePendulum();
setState(() {});
});
}
void _updatePendulum() {
final dt = 0.05;
for (int i = 0; i < 3; i++) {
final delta = _theta1 - _theta2;
final den1 = (_m1 + _m2) * _L1 - _m2 * _L1 * cos(delta) * cos(delta);
final den2 = (_L2 / _L1) * den1;
final alpha1 = (_m2 * _L1 * _omega1 * _omega1 * sin(delta) * cos(delta)
+ _m2 * _g * sin(_theta2) * cos(delta)
+ _m2 * _L2 * _omega2 * _omega2 * sin(delta)
- (_m1 + _m2) * _g * sin(_theta1)) / den1;
final alpha2 = (-_m2 * _L2 * _omega2 * _omega2 * sin(delta) * cos(delta)
+ (_m1 + _m2) * _g * sin(_theta1) * cos(delta)
- (_m1 + _m2) * _L1 * _omega1 * _omega1 * sin(delta)
- (_m1 + _m2) * _g * sin(_theta2)) / den2;
_omega1 += alpha1 * dt;
_omega2 += alpha2 * dt;
_theta1 += _omega1 * dt;
_theta2 += _omega2 * dt;
}
// 记录轨迹
final x2 = _L1 * sin(_theta1) + _L2 * sin(_theta2);
final y2 = _L1 * cos(_theta1) + _L2 * cos(_theta2);
_trail.add(Offset(x2, y2));
if (_trail.length > 2000) {
_trail.removeAt(0);
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('双摆模拟')),
body: Stack(children: [
CustomPaint(
painter: DoublePendulumPainter(
_L1, _L2, _theta1, _theta2, _trail, _time, _showTrail,
),
size: Size.infinite,
),
Positioned(top: 20, right: 20, child: _buildToggle()),
Positioned(bottom: 20, left: 20, right: 20, child: _buildControls()),
]),
);
}
Widget _buildToggle() {
return GestureDetector(
onTap: () => setState(() => _showTrail = !_showTrail),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(color: Colors.black.withOpacity(0.7), borderRadius: BorderRadius.circular(8)),
child: Text(_showTrail ? '隐藏轨迹' : '显示轨迹', style: const TextStyle(color: Colors.white70, fontSize: 12)),
),
);
}
Widget _buildControls() {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(color: Colors.black.withOpacity(0.7), borderRadius: BorderRadius.circular(12)),
child: Row(children: [
Expanded(child: Column(children: [
Text('长度1: ${_L1.toStringAsFixed(0)}', style: const TextStyle(color: Colors.white70, fontSize: 11)),
Slider(value: _L1, min: 50, max: 200, onChanged: (v) => setState(() => _L1 = v)),
])),
Expanded(child: Column(children: [
Text('长度2: ${_L2.toStringAsFixed(0)}', style: const TextStyle(color: Colors.white70, fontSize: 11)),
Slider(value: _L2, min: 50, max: 200, onChanged: (v) => setState(() => _L2 = v)),
])),
Expanded(child: Column(children: [
Text('重置', style: const TextStyle(color: Colors.white70, fontSize: 11)),
IconButton(icon: const Icon(Icons.refresh, color: Colors.amber), onPressed: () {
setState(() {
_theta1 = pi / 2;
_theta2 = pi / 2;
_omega1 = 0;
_omega2 = 0;
_trail.clear();
});
}),
])),
]),
);
}
}
class DoublePendulumPainter extends CustomPainter {
final double L1, L2, theta1, theta2;
final List<Offset> trail;
final double time;
final bool showTrail;
DoublePendulumPainter(this.L1, this.L2, this.theta1, this.theta2, this.trail, this.time, this.showTrail);
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 3);
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = const Color(0xFF0a0a15));
// 绘制轨迹
if (showTrail && trail.length > 1) {
for (int i = 1; i < trail.length; i++) {
final p1 = trail[i - 1] + center;
final p2 = trail[i] + center;
final hue = (i / trail.length * 120 + time * 10) % 360;
final alpha = i / trail.length * 0.8;
canvas.drawLine(p1, p2, Paint()..color = HSVColor.fromAHSV(alpha, hue, 0.9, 1).toColor()..strokeWidth = 1);
}
}
// 计算摆球位置
final x1 = L1 * sin(theta1);
final y1 = L1 * cos(theta1);
final x2 = x1 + L2 * sin(theta2);
final y2 = y1 + L2 * cos(theta2);
// 绘制支点
canvas.drawCircle(center, 8, Paint()..color = Colors.grey);
// 绘制第一根杆
canvas.drawLine(center, center + Offset(x1, y1), Paint()..color = Colors.white..strokeWidth = 3);
// 绘制第一个摆球
canvas.drawCircle(center + Offset(x1, y1), 15, Paint()..color = Colors.cyan);
// 绘制第二根杆
canvas.drawLine(center + Offset(x1, y1), center + Offset(x2, y2), Paint()..color = Colors.white..strokeWidth = 3);
// 绘制第二个摆球
canvas.drawCircle(center + Offset(x2, y2), 12, Paint()..color = Colors.orange);
}
@override
bool shouldRepaint(covariant DoublePendulumPainter old) => true;
}
📝 六、总结
本篇文章深入探讨了混沌理论在音乐可视化中的应用,从洛伦兹吸引子到各种奇异吸引子,构建了音乐驱动的动态混沌艺术系统。
✅ 核心知识点回顾
| 知识点 | 说明 |
|---|---|
| 🌀混沌理论 | 确定性系统中的不可预测行为 |
| 🦋洛伦兹吸引子 | 蝴蝶效应的经典模型 |
| 🔄罗斯勒吸引子 | 单螺旋混沌系统 |
| 💫陈氏吸引子 | 双螺旋混沌变体 |
| 🎭埃农映射 | 离散混沌分形 |
| 🌊双摆系统 | 物理混沌实例 |
| 🎵音频驱动 | 音乐参数映射混沌 |
⭐ 最佳实践要点
- ✅ 使用龙格-库塔方法进行数值积分
- ✅ 音频能量映射到混沌参数实现动态效果
- ✅ 轨迹颜色基于状态变量动态计算
- ✅ 合理设置轨迹长度避免性能问题
- ✅ 提供参数调节交互增强用户体验