Harmony Flutter 跨平台开发实战:鸿蒙与音乐律动艺术、Mandelbrot 分形生长:自相似性的音频映射
分形几何(Fractal Geometry)是由数学家**本华·曼德博(Benoît Mandelbrot)**在 1975 年提出的概念,用于描述自然界中那些传统欧几里得几何无法描述的不规则形状。“分形"一词源自拉丁语 “fractus”,意为"破碎的”、“不规则的”。历史里程碑:分形的核心特征:分形几何是描述自然界的最佳语言,因为自然界中充满了分形结构:植物界的分形:自然分形实例:Mandel

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
🌀 一、Mandelbrot 集:无限复杂的数学之美
📚 1.1 分形几何的历史渊源
分形几何(Fractal Geometry)是由数学家**本华·曼德博(Benoît Mandelbrot)**在 1975 年提出的概念,用于描述自然界中那些传统欧几里得几何无法描述的不规则形状。“分形"一词源自拉丁语 “fractus”,意为"破碎的”、“不规则的”。
历史里程碑:
| 年份 | 人物 | 贡献 |
|---|---|---|
| 1879 | 格奥尔格·康托尔 | 康托尔集,第一个分形 |
| 1904 | 海里格·冯·科赫 | 科赫雪花曲线 |
| 1905 | 皮埃尔·法图 | 研究迭代有理函数 |
| 1915 | 瓦茨瓦夫·谢尔宾斯基 | 谢尔宾斯基三角形 |
| 1918 | 加斯顿·朱利亚 | 朱利亚集合研究 |
| 1918 | 费利克斯·豪斯多夫 | 分形维数概念 |
| 1980 | 本华·曼德博 | 发现 Mandelbrot 集 |
| 1985 | Heinz-Otto Peitgen | 分形可视化普及 |
分形的核心特征:
| 特征 | 描述 | 数学表达 |
|---|---|---|
| 🔄自相似性 | 局部与整体相似 | f(λx) = λ^D · f(x) |
| 📐分数维数 | 维数不是整数 | D = log(N)/log(S) |
| ♾️无限细节 | 放大后仍有结构 | lim(n→∞) detail |
| 🎯简单规则 | 复杂来自简单迭代 | z_{n+1} = f(z_n) |
| 🌊混沌边缘 | 有序与无序之间 | 边界分形 |
🌿 1.2 自然界中的分形
分形几何是描述自然界的最佳语言,因为自然界中充满了分形结构:
植物界的分形:
蕨类植物叶片:
┌─────────────┐
│ ╱╲ │
│ ╱ ╲ │
│ ╱ ╱╲ ╲ │
│ ╱ ╱ ╲ ╲ │
│╱ ╱╱╲╱╲╲ ╲ │
│ ╱ ╲ ╲ │
│ ╱ ╲ ╲ │
└─────────────┘
每个小叶片都是整体的缩小版
罗马花椰菜:
每个小花都是整个花椰菜的缩小版
分形维数约为 2.7
自然分形实例:
| 对象 | 分形维数 | 特点 |
|---|---|---|
| 🌊 海岸线 | 1.1-1.3 | 越精细测量越长 |
| ❄️ 雪花 | ~1.7 | 六重对称分支 |
| ⚡ 闪电 | ~1.6 | 随机分支结构 |
| 🌿 蕨叶 | ~1.5 | 自相似叶片 |
| 🫁 肺部 | ~2.9 | 分支气管网络 |
| 🧬 血管 | ~2.7 | 分形分布 |
| 🏔️ 山脉 | ~2.2 | 表面粗糙度 |
📐 1.3 Mandelbrot 集的数学定义
Mandelbrot 集是复平面上满足特定条件的点集,由一个极其简单的迭代公式定义:
核心迭代公式:
z_{n+1} = z_n² + c
其中:
- z_0 = 0(初始值)
- c 是复平面上的点
- 迭代观察 z_n 的行为
详细数学推导:
设复数 c = a + bi,z_n = x_n + y_n·i,则迭代公式展开为:
x_{n+1} = x_n² - y_n² + a
y_{n+1} = 2·x_n·y_n + b
初始条件:x_0 = 0, y_0 = 0
判定条件:
| 条件 | 判断 | 颜色处理 |
|---|---|---|
| z_n | 保持有界(< 2) | |
| z_n | 趋向无穷(≥ 2) |
逃逸半径定理:
如果 |z_n| > 2,则序列必定趋向无穷
证明:
设 |z| > 2, |c| ≤ 2
|z² + c| ≥ |z|² - |c| > |z|² - |z| = |z|(|z| - 1) > |z|
因此一旦 |z| > 2,序列单调递增,趋向无穷。
Mandelbrot 集的边界:
复平面示意:
Im (虚轴)
↑
1.5 │ ░░░░░░░░░░░░░░░░░░░░░░░░░
│ ░░░░░░░░░░░░░░░░░░░░░░░░░░░
1.0 │ ░░░░░░████████████████░░░░░░
│ ░░░░██████████████████░░░░░░
0.5 │ ░░░████████████████████░░░░░
│ ░░██████████████████████░░░░
0.0 │──░░██████████████████████░░░░───→ Re (实轴)
│ ░░██████████████████████░░░░
-0.5 │ ░░░████████████████████░░░░░
│ ░░░░██████████████████░░░░░░
-1.0 │ ░░░░░░████████████████░░░░░░
│ ░░░░░░░░░░░░░░░░░░░░░░░░░░░
-1.5 │ ░░░░░░░░░░░░░░░░░░░░░░░░░
│
-2.0 -1.5 -1.0 -0.5 0 0.5 1.0
█ = Mandelbrot 集(黑色区域)
░ = 逃逸区域(按迭代次数着色)
🔬 1.4 Mandelbrot 集的著名结构
Mandelbrot 集包含许多著名的子结构,每个都有独特的数学性质:
主心脏区域(Main Cardioid):
形状:心形曲线
方程:r = (1 - cos(θ))/2,中心在 (-1/4, 0)
性质:此区域内的点迭代后收敛到固定点
╱╲
╱ ╲
╱ ╲
╱ ╲
╱ ╲
╲ ╱
╲ ╱
╲ ╱
╲ ╱
╲╱
主圆盘(Main Bulb):
形状:圆形
位置:中心在 (-1, 0),半径 1/4
性质:此区域内的点迭代后收敛到 2-周期轨道
海马谷(Seahorse Valley):
位置:主心脏与主圆盘连接处附近
特点:放大后呈现海马形状的螺旋结构
分形维数:接近 2
╱╲
╱ ╲
╱ ╲
╱ ╲
╱ ╱╲ ╲
╱ ╱ ╲ ╲
╱ ╲
╱ ╲
象谷(Elephant Valley):
位置:主心脏顶部
特点:放大后呈现大象鼻子形状的卷曲结构
著名放大位置:
| 名称 | 坐标 | 放大倍数 | 特点 |
|---|---|---|---|
| 海马谷 | (-0.75, 0.1) | 10^6 | 螺旋海马 |
| 象谷 | (0.28, 0.008) | 10^7 | 卷曲象鼻 |
| 三角谷 | (-0.16, 1.04) | 10^5 | 三角分形 |
| 螺旋臂 | (-0.761, -0.085) | 10^4 | 双螺旋 |
| 卫星 | (-1.75, 0) | 10^2 | 小型复制品 |
🔗 1.5 Julia 集合与 Mandelbrot 集的关系
Julia 集合是 Mandelbrot 集的"亲戚",两者有着深刻的数学联系:
Julia 集的定义:
对于固定的复数 c,Julia 集 J_c 定义为:
- 迭代公式:z_{n+1} = z_n² + c
- 初始值 z_0 在复平面上变化
- 观察 z_n 的行为
与 Mandelbrot 集的区别:
- Mandelbrot:c 变化,z_0 = 0
- Julia:c 固定,z_0 变化
两种集合的关系:
| 特性 | Mandelbrot 集 | Julia 集 |
|---|---|---|
| 📐参数 | c 变化,z₀ = 0 | c 固定,z₀ 变化 |
| 🎯结果 | 一个集合 | 无限多个集合 |
| 🔗关系 | 参数空间 | 动力系统空间 |
| 🎨形状 | 著名的"海马" | 各种对称图案 |
| 📊连通性 | 整体连通 | 取决于 c 的位置 |
关键定理(法图-朱利亚定理):
对于复数 c:
- 如果 c ∈ Mandelbrot 集,则 J_c 是连通的
- 如果 c ∉ Mandelbrot 集,则 J_c 是不连通的(法图尘)
这个定理建立了两种集合之间的深刻联系。
Julia 集示例:
c = -0.4 + 0.6i(连通的 Julia 集) c = -0.8 + 0.156i(法图尘)
╱╲╱╲╱╲╱╲ · · · ·
╱ ╲ ╱ ╲ · · · · ·
╱ ╲╱ ╲ · · · · ·
╱ ╲ ╲ · · · · ·
╱ ╲ ╲ · · · ·
╱ ╲ ╲ · · · ·
╱ · · ·
╱ · · ·
🎯 1.6 分形的应用领域
分形几何在众多领域有着广泛的应用:
| 领域 | 应用 | 效果 |
|---|---|---|
| 🎨计算机图形学 | 纹理生成、地形模拟、云彩渲染 | 自然景观 |
| 📡天线设计 | 分形天线(Sierpinski、Koch) | 多频段接收 |
| 📊数据压缩 | 分形压缩算法 | 高压缩比 |
| 🎵音乐可视化 | 动态分形演化 | 视觉震撼 |
| 🔬科学建模 | 复杂系统模拟 | 自然现象 |
| 💰金融分析 | 股价波动建模 | 风险评估 |
| 🏥医学影像 | 肿瘤边界分析 | 诊断辅助 |
| 🌐网络科学 | 互联网拓扑 | 流量优化 |
分形天线示例:
传统天线 vs 分形天线
传统单极天线: Sierpinski 分形天线:
│ ▲
│ ╱ ╲
│ ╱ ╲
│ ╱ ╲ ╱ ╲
│ ╱ ╱ ╲
│ ╱ ╲ ╱ ╲ ╱ ╲
─┴─ ───────────
分形天线优势:
- 多频段工作
- 尺寸更小
- 性能更好
🎵 1.7 分形与音乐的关联
分形与音乐有着天然的联系,许多音乐作品具有分形特征:
音乐中的分形:
| 音乐元素 | 分形特征 | 例子 |
|---|---|---|
| 🎼节奏 | 不同时间尺度的相似模式 | 非洲鼓乐 |
| 🎹旋律 | 自相似的主题变奏 | 巴赫赋格 |
| 🎸和声 | 分层的声音结构 | 电子音乐 |
| 🔊音色 | 分形的频谱结构 | 钟声、铃声 |
分形音乐可视化:
音频特征 → 分形参数映射
低音 (Bass) → 缩放深度
中频 (Mid) → 迭代次数
高频 (Treble) → 颜色偏移
能量 (Energy) → 动画速度
节拍 (Beat) → 脉冲效果
🔧 二、Mandelbrot 集的 Dart 实现
🧮 2.1 复数运算基础
import 'dart:math';
import 'dart:typed_data';
/// 复数类 - 分形计算的基础
class Complex {
final double real;
final double imaginary;
const Complex(this.real, this.imaginary);
/// 零复数
static const Complex zero = Complex(0, 0);
/// 单位复数
static const Complex one = Complex(1, 0);
/// 虚数单位
static const Complex i = Complex(0, 1);
/// 从极坐标创建
factory Complex.fromPolar(double r, double theta) {
return Complex(r * cos(theta), r * sin(theta));
}
/// 加法
Complex operator +(Complex other) => Complex(
real + other.real,
imaginary + other.imaginary,
);
/// 减法
Complex operator -(Complex other) => Complex(
real - other.real,
imaginary - other.imaginary,
);
/// 乘法
Complex operator *(Complex other) => Complex(
real * other.real - imaginary * other.imaginary,
real * other.imaginary + imaginary * other.real,
);
/// 标量乘法
Complex operator *(double scalar) => Complex(
real * scalar,
imaginary * scalar,
);
/// 除法
Complex operator /(Complex other) {
final denominator = other.real * other.real + other.imaginary * other.imaginary;
return Complex(
(real * other.real + imaginary * other.imaginary) / denominator,
(imaginary * other.real - real * other.imaginary) / denominator,
);
}
/// 模的平方
double get magnitudeSquared => real * real + imaginary * imaginary;
/// 模
double get magnitude => sqrt(magnitudeSquared);
/// 幅角
double get argument => atan2(imaginary, real);
/// 共轭
Complex get conjugate => Complex(real, -imaginary);
/// 平方
Complex get squared => this * this;
/// 立方
Complex get cubed => this * this * this;
/// n 次方
Complex pow(int n) {
if (n == 0) return one;
if (n == 1) return this;
if (n < 0) return one / pow(-n);
Complex result = one;
for (int i = 0; i < n; i++) {
result = result * this;
}
return result;
}
/// 平方根
Complex get sqrt {
final r = magnitude;
final theta = argument / 2;
return Complex.fromPolar(sqrt(r), theta);
}
/// 指数 e^z
Complex get exp => Complex.fromPolar(exp(real), imaginary);
/// 自然对数 ln(z)
Complex get log => Complex(magnitude.log, argument);
String toString() {
if (imaginary == 0) return real.toStringAsFixed(4);
if (real == 0) return '${imaginary.toStringAsFixed(4)}i';
return '$real ${imaginary >= 0 ? '+' : '-'} ${imaginary.abs().toStringAsFixed(4)}i';
}
bool operator ==(Object other) {
if (other is! Complex) return false;
return real == other.real && imaginary == other.imaginary;
}
int get hashCode => Object.hash(real, imaginary);
}
⚡ 2.2 Mandelbrot 迭代计算器
/// Mandelbrot 迭代结果
class MandelbrotResult {
final int iterations;
final bool escaped;
final double smoothValue;
final double finalMagnitude;
const MandelbrotResult({
required this.iterations,
required this.escaped,
required this.smoothValue,
required this.finalMagnitude,
});
}
/// Mandelbrot 计算器
class MandelbrotCalculator {
final int maxIterations;
final double escapeRadius;
final double escapeRadiusSquared;
MandelbrotCalculator({
this.maxIterations = 100,
this.escapeRadius = 2.0,
}) : escapeRadiusSquared = escapeRadius * escapeRadius;
/// 计算单个点的迭代结果
MandelbrotResult calculate(Complex c) {
Complex z = Complex.zero;
int iterations = 0;
while (iterations < maxIterations && z.magnitudeSquared < escapeRadiusSquared) {
z = z.squared + c;
iterations++;
}
final escaped = iterations < maxIterations;
final finalMagnitude = z.magnitude;
// 平滑着色值(使用连续着色算法)
double smoothValue = 0;
if (escaped) {
// 连续着色公式
// smooth = n + 1 - log(log(|z_n|)) / log(2)
final logZn = log(finalMagnitudeSquared);
final nu = log(logZn / log(2)) / log(2);
smoothValue = iterations + 1 - nu;
}
return MandelbrotResult(
iterations: iterations,
escaped: escaped,
smoothValue: smoothValue,
finalMagnitude: finalMagnitude,
);
}
/// 使用周期检测优化计算
MandelbrotResult calculateWithPeriodDetection(Complex c) {
Complex z = Complex.zero;
int iterations = 0;
// 周期检测
Complex zOld = Complex.zero;
int period = 0;
while (iterations < maxIterations && z.magnitudeSquared < escapeRadiusSquared) {
z = z.squared + c;
iterations++;
// 检查是否进入周期轨道
if ((z - zOld).magnitudeSquared < 1e-10) {
// 发现周期轨道,点在集合内
return MandelbrotResult(
iterations: maxIterations,
escaped: false,
smoothValue: 0,
finalMagnitude: z.magnitude,
);
}
period++;
if (period > 20) {
period = 0;
zOld = z;
}
}
final escaped = iterations < maxIterations;
double smoothValue = 0;
if (escaped) {
final logZn = log(z.magnitudeSquared);
final nu = log(logZn / log(2)) / log(2);
smoothValue = iterations + 1 - nu;
}
return MandelbrotResult(
iterations: iterations,
escaped: escaped,
smoothValue: smoothValue,
finalMagnitude: z.magnitude,
);
}
/// 批量计算区域
Float32List calculateRegion({
required int width,
required int height,
required Complex center,
required double scale,
bool usePeriodDetection = false,
}) {
final result = Float32List(width * height);
for (int py = 0; py < height; py++) {
for (int px = 0; px < width; px++) {
final x = center.real + (px - width / 2) * scale;
final y = center.imaginary + (py - height / 2) * scale;
final mandelbrotResult = usePeriodDetection
? calculateWithPeriodDetection(Complex(x, y))
: calculate(Complex(x, y));
if (mandelbrotResult.escaped) {
result[py * width + px] = mandelbrotResult.smoothValue / maxIterations;
} else {
result[py * width + px] = -1; // 属于集合内部
}
}
}
return result;
}
/// 计算边界估计
static bool isInMainCardioid(Complex c) {
// 主心脏区域检测
final q = (c.real - 0.25) * (c.real - 0.25) + c.imaginary * c.imaginary;
return q * (q + (c.real - 0.25)) <= 0.25 * c.imaginary * c.imaginary;
}
static bool isInMainBulb(Complex c) {
// 主圆盘检测
return (c.real + 1) * (c.real + 1) + c.imaginary * c.imaginary <= 0.0625;
}
/// 快速计算(跳过已知在集合内的区域)
MandelbrotResult calculateFast(Complex c) {
// 快速检测是否在主心脏或主圆盘内
if (isInMainCardioid(c) || isInMainBulb(c)) {
return MandelbrotResult(
iterations: maxIterations,
escaped: false,
smoothValue: 0,
finalMagnitude: 0,
);
}
return calculateWithPeriodDetection(c);
}
}
🎨 2.3 分形着色方案
/// 分形着色器
class FractalColorScheme {
final String name;
final List<Color> colors;
final List<double> positions;
const FractalColorScheme({
required this.name,
required this.colors,
required this.positions,
});
/// 经典配色
static const FractalColorScheme classic = FractalColorScheme(
name: 'Classic',
colors: [
Color(0xFF000076),
Color(0xFF0026B3),
Color(0xFF0066FF),
Color(0xFF00B3FF),
Color(0xFF00FFFF),
Color(0xFF00FF80),
Color(0xFF00FF00),
Color(0xFF80FF00),
Color(0xFFFFFF00),
Color(0xFFFF8000),
Color(0xFFFF0000),
Color(0xFF800000),
Color(0xFF000000),
],
positions: [0, 0.05, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.75, 0.85, 0.95, 1.0],
);
/// 火焰配色
static const FractalColorScheme fire = FractalColorScheme(
name: 'Fire',
colors: [
Color(0xFF000000),
Color(0xFF1A0000),
Color(0xFF330000),
Color(0xFF4D0000),
Color(0xFF660000),
Color(0xFF800000),
Color(0xFF990000),
Color(0xFFB30000),
Color(0xFFCC0000),
Color(0xFFE60000),
Color(0xFFFF0000),
Color(0xFFFF3300),
Color(0xFFFF6600),
Color(0xFFFF9900),
Color(0xFFFFCC00),
Color(0xFFFFFF00),
Color(0xFFFFFFFF),
],
positions: [0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.7, 0.8, 0.9, 1.0],
);
/// 海洋配色
static const FractalColorScheme ocean = FractalColorScheme(
name: 'Ocean',
colors: [
Color(0xFF000010),
Color(0xFF000020),
Color(0xFF000040),
Color(0xFF000060),
Color(0xFF000080),
Color(0xFF0020A0),
Color(0xFF0040C0),
Color(0xFF0060E0),
Color(0xFF0080FF),
Color(0xFF00A0FF),
Color(0xFF00C0FF),
Color(0xFF00E0FF),
Color(0xFF40F0FF),
Color(0xFF80FFFF),
Color(0xFFC0FFFF),
],
positions: [0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
);
/// 迷幻配色
static const FractalColorScheme psychedelic = FractalColorScheme(
name: 'Psychedelic',
colors: [
Color(0xFFFF00FF),
Color(0xFF00FFFF),
Color(0xFFFFFF00),
Color(0xFFFF00FF),
Color(0xFF00FF00),
Color(0xFF0000FF),
Color(0xFFFF0000),
Color(0xFF00FF00),
Color(0xFFFF00FF),
],
positions: [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0],
);
/// 极光配色
static const FractalColorScheme aurora = FractalColorScheme(
name: 'Aurora',
colors: [
Color(0xFF000020),
Color(0xFF001040),
Color(0xFF002060),
Color(0xFF004080),
Color(0xFF0080A0),
Color(0xFF00C0C0),
Color(0xFF00FF80),
Color(0xFF40FF40),
Color(0xFF80FF00),
Color(0xFFC0FF00),
Color(0xFFFFFF00),
Color(0xFFFFC000),
Color(0xFFFF8000),
],
positions: [0, 0.08, 0.15, 0.22, 0.3, 0.38, 0.46, 0.54, 0.62, 0.7, 0.78, 0.88, 1.0],
);
/// 所有预设配色
static const List<FractalColorScheme> presets = [
classic,
fire,
ocean,
psychedelic,
aurora,
];
/// 根据值获取颜色
Color getColor(double value) {
if (value < 0) return const Color(0xFF000000);
value = value.clamp(0.0, 1.0);
for (int i = 0; i < positions.length - 1; i++) {
if (value >= positions[i] && value <= positions[i + 1]) {
final t = (value - positions[i]) / (positions[i + 1] - positions[i]);
return Color.lerp(colors[i], colors[i + 1], t)!;
}
}
return colors.last;
}
/// 根据迭代次数和时间获取动态颜色
Color getAnimatedColor(double value, double time, {double speed = 0.1}) {
if (value < 0) return const Color(0xFF000000);
// 添加时间偏移实现动画效果
final animatedValue = (value + time * speed) % 1.0;
return getColor(animatedValue);
}
/// 获取带脉冲效果的颜色
Color getPulsedColor(double value, double time, double pulse) {
if (value < 0) return const Color(0xFF000000);
final baseColor = getColor(value);
final pulseFactor = 0.5 + 0.5 * sin(time * 2 + pulse * pi);
return Color.lerp(baseColor, Colors.white, pulseFactor * 0.3)!;
}
}
🎵 2.4 音频驱动的分形控制器
import 'package:flutter/material.dart';
import 'package:just_audio_ohos/just_audio_ohos.dart';
import 'package:audio_session/audio_session.dart';
/// 分形视图状态
class FractalViewState {
final Complex center;
final double scale;
final int maxIterations;
final FractalColorScheme colorScheme;
final double rotation;
const FractalViewState({
this.center = const Complex(-0.5, 0),
this.scale = 0.004,
this.maxIterations = 100,
this.colorScheme = FractalColorScheme.classic,
this.rotation = 0,
});
FractalViewState copyWith({
Complex? center,
double? scale,
int? maxIterations,
FractalColorScheme? colorScheme,
double? rotation,
}) {
return FractalViewState(
center: center ?? this.center,
scale: scale ?? this.scale,
maxIterations: maxIterations ?? this.maxIterations,
colorScheme: colorScheme ?? this.colorScheme,
rotation: rotation ?? this.rotation,
);
}
}
/// 音频驱动的分形控制器
class AudioFractalController extends ChangeNotifier {
final AudioPlayer _player = AudioPlayer();
final MandelbrotCalculator _calculator = MandelbrotCalculator(maxIterations: 150);
FractalViewState _state = const FractalViewState();
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;
double _time = 0;
double _zoomPhase = 0;
double _colorPhase = 0;
bool get isPlaying => _isPlaying;
Duration get position => _position;
Duration get duration => _duration;
FractalViewState get state => _state;
Float32List get audioData => _audioData;
double get energy => _energy;
double get bass => _bass;
double get mid => _mid;
double get treble => _treble;
AudioPlayer get player => _player;
double get time => _time;
MandelbrotCalculator get calculator => _calculator;
/// 初始化
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 update(double dt) {
_time += dt;
_zoomPhase += dt;
_colorPhase += dt * 2;
// 更新音频数据
_updateAudioData();
// 计算音频特征
_calculateAudioFeatures();
// 更新分形参数
_updateFractalParams();
notifyListeners();
}
void _updateAudioData() {
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 noise = (random.nextDouble() - 0.5) * 0.15;
final bassBoost = i < 32 ? 0.3 : 0;
_audioData[i] = _audioData[i] * 0.85 +
(wave1 + wave2 + noise + bassBoost) * 0.15;
} else {
_audioData[i] *= 0.95;
}
}
}
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 _updateFractalParams() {
// 音频驱动的缩放
final zoomFactor = 1 - _energy * 0.3;
final newScale = 0.004 * zoomFactor;
// 音频驱动的迭代次数
final newIterations = (100 + _energy * 100).toInt();
// 音频驱动的旋转
final newRotation = _time * 0.1 * (1 + _mid);
_state = _state.copyWith(
scale: newScale,
maxIterations: newIterations,
rotation: newRotation,
);
}
/// 缩放
void zoom(double factor) {
_state = _state.copyWith(scale: _state.scale * factor);
notifyListeners();
}
/// 平移
void pan(Offset delta) {
final newCenter = Complex(
_state.center.real - delta.dx * _state.scale,
_state.center.imaginary - delta.dy * _state.scale,
);
_state = _state.copyWith(center: newCenter);
notifyListeners();
}
/// 设置配色方案
void setColorScheme(FractalColorScheme scheme) {
_state = _state.copyWith(colorScheme: scheme);
notifyListeners();
}
/// 设置中心点
void setCenter(Complex center) {
_state = _state.copyWith(center: center);
notifyListeners();
}
/// 播放/暂停
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();
}
}
📦 三、完整示例代码
以下是完整的 Mandelbrot 分形音乐可视化示例代码:
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 FractalApp());
}
class FractalApp extends StatelessWidget {
const FractalApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Mandelbrot 分形',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple, brightness: Brightness.dark),
useMaterial3: true,
),
home: const FractalHomePage(),
debugShowCheckedModeBanner: false,
);
}
}
class FractalHomePage extends StatelessWidget {
const FractalHomePage({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('🌀 Mandelbrot 分形'), backgroundColor: Theme.of(context).colorScheme.inversePrimary),
body: ListView(padding: const EdgeInsets.all(16), children: [
_buildCard(context, title: '基础分形', description: '静态 Mandelbrot 集', icon: Icons.grain, color: Colors.deepPurple,
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const BasicFractalDemo()))),
_buildCard(context, title: '动态着色', description: '颜色动画效果', icon: Icons.palette, color: Colors.pink,
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const AnimatedColorDemo()))),
_buildCard(context, title: '交互探索', description: '缩放和平移', icon: Icons.zoom_in, color: Colors.cyan,
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const InteractiveFractalDemo()))),
_buildCard(context, title: '音乐分形', description: '音频驱动的演化', icon: Icons.music_note, color: Colors.orange,
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const MusicFractalDemo()))),
_buildCard(context, title: 'Julia 集', description: 'Julia 集合探索', icon: Icons.blur_on, color: Colors.indigo,
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const JuliaSetDemo()))),
]),
);
}
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 Complex {
final double real, imaginary;
const Complex(this.real, this.imaginary);
Complex operator +(Complex o) => Complex(real + o.real, imaginary + o.imaginary);
Complex operator *(Complex o) => Complex(real * o.real - imaginary * o.imaginary, real * o.imaginary + imaginary * o.real);
Complex get squared => this * this;
double get magnitudeSquared => real * real + imaginary * imaginary;
}
/// 基础分形演示
class BasicFractalDemo extends StatefulWidget {
const BasicFractalDemo({super.key});
State<BasicFractalDemo> createState() => _BasicFractalDemoState();
}
class _BasicFractalDemoState extends State<BasicFractalDemo> {
int _maxIterations = 100;
double _centerX = -0.5, _centerY = 0, _scale = 0.004;
int _colorSchemeIndex = 0;
final List<FractalColorScheme> _colorSchemes = FractalColorScheme.presets;
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('基础分形')),
body: Column(children: [
Expanded(child: CustomPaint(painter: BasicFractalPainter(_centerX, _centerY, _scale, _maxIterations, _colorSchemes[_colorSchemeIndex]), size: Size.infinite)),
_buildControls(),
]),
);
}
Widget _buildControls() {
return Container(
padding: const EdgeInsets.all(16),
color: Colors.black12,
child: Column(children: [
Row(children: [
const Text('迭代: ', style: TextStyle(color: Colors.white)),
Expanded(child: Slider(value: _maxIterations.toDouble(), min: 20, max: 300, divisions: 28,
onChanged: (v) => setState(() => _maxIterations = v.toInt()))),
Text('$_maxIterations', style: const TextStyle(color: Colors.white)),
]),
Row(children: [
const Text('缩放: ', style: TextStyle(color: Colors.white)),
Expanded(child: Slider(value: -log(_scale), min: 4, max: 12,
onChanged: (v) => setState(() => _scale = exp(-v)))),
]),
const SizedBox(height: 8),
Wrap(spacing: 8, children: List.generate(_colorSchemes.length, (i) =>
ChoiceChip(label: Text(_colorSchemes[i].name), selected: _colorSchemeIndex == i,
onSelected: (_) => setState(() => _colorSchemeIndex = i)))),
]),
);
}
}
class BasicFractalPainter extends CustomPainter {
final double centerX, centerY, scale;
final int maxIterations;
final FractalColorScheme colorScheme;
BasicFractalPainter(this.centerX, this.centerY, this.scale, this.maxIterations, this.colorScheme);
void paint(Canvas canvas, Size size) {
final pixelSize = 2;
for (int py = 0; py < size.height.toInt(); py += pixelSize) {
for (int px = 0; px < size.width.toInt(); px += pixelSize) {
final x = centerX + (px - size.width / 2) * scale;
final y = centerY + (py - size.height / 2) * scale;
final color = _calculatePoint(x, y);
canvas.drawRect(Rect.fromLTWH(px.toDouble(), py.toDouble(), pixelSize.toDouble(), pixelSize.toDouble()), Paint()..color = color);
}
}
}
Color _calculatePoint(double cx, double cy) {
// 快速检测主心脏和主圆盘
final q = (cx - 0.25) * (cx - 0.25) + cy * cy;
if (q * (q + (cx - 0.25)) <= 0.25 * cy * cy) {
return const Color(0xFF000000);
}
if ((cx + 1) * (cx + 1) + cy * cy <= 0.0625) {
return const Color(0xFF000000);
}
var zr = 0.0, zi = 0.0;
int iter = 0;
while (iter < maxIterations && zr * zr + zi * zi < 4) {
final temp = zr * zr - zi * zi + cx;
zi = 2 * zr * zi + cy;
zr = temp;
iter++;
}
if (iter == maxIterations) return const Color(0xFF000000);
// 平滑着色
final logZn = log(zr * zr + zi * zi) / 2;
final nu = log(logZn / log(2)) / log(2);
final smooth = (iter + 1 - nu) / maxIterations;
return colorScheme.getColor(smooth);
}
bool shouldRepaint(covariant BasicFractalPainter old) =>
centerX != old.centerX || centerY != old.centerY || scale != old.scale || maxIterations != old.maxIterations;
}
/// 动态着色演示
class AnimatedColorDemo extends StatefulWidget {
const AnimatedColorDemo({super.key});
State<AnimatedColorDemo> createState() => _AnimatedColorDemoState();
}
class _AnimatedColorDemoState extends State<AnimatedColorDemo> with SingleTickerProviderStateMixin {
late AnimationController _controller;
double _time = 0;
int _colorScheme = 0;
double _colorSpeed = 1.0;
double _zoom = 1.0;
double _centerX = -0.5, _centerY = 0;
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 16))..repeat();
_controller.addListener(() { _time += 0.016 * _colorSpeed; setState(() {}); });
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('动态着色')),
body: Stack(children: [
CustomPaint(
painter: AnimatedColorPainter(
time: _time,
colorScheme: _colorScheme,
zoom: _zoom,
centerX: _centerX,
centerY: _centerY,
),
size: Size.infinite,
),
Positioned(bottom: 20, 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: [
Row(children: [
const Text('配色方案:', style: TextStyle(color: Colors.white)),
const SizedBox(width: 10),
for (int i = 0; i < 4; i++)
GestureDetector(
onTap: () => setState(() => _colorScheme = i),
child: Container(
width: 30, height: 30,
margin: const EdgeInsets.symmetric(horizontal: 4),
decoration: BoxDecoration(
color: _colorScheme == i ? Colors.white : Colors.grey[800],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: _getColorForScheme(i), width: 2),
),
child: Center(child: Text('$i', style: TextStyle(color: _getColorForScheme(i), fontSize: 12))),
),
),
]),
const SizedBox(height: 12),
Row(children: [
const Text('速度:', style: TextStyle(color: Colors.white)),
Expanded(child: Slider(value: _colorSpeed, min: 0.1, max: 3, onChanged: (v) => setState(() => _colorSpeed = v))),
]),
Row(children: [
const Text('缩放:', style: TextStyle(color: Colors.white)),
Expanded(child: Slider(value: _zoom, min: 0.5, max: 5, onChanged: (v) => setState(() => _zoom = v))),
]),
],
),
);
}
Color _getColorForScheme(int scheme) {
switch (scheme) {
case 0: return Colors.deepPurple;
case 1: return Colors.orange;
case 2: return Colors.cyan;
case 3: return Colors.pink;
default: return Colors.white;
}
}
}
class AnimatedColorPainter extends CustomPainter {
final double time;
final int colorScheme;
final double zoom;
final double centerX, centerY;
AnimatedColorPainter({
required this.time,
required this.colorScheme,
required this.zoom,
required this.centerX,
required this.centerY,
});
void paint(Canvas canvas, Size canvasSize) {
final pixelSize = (3 / zoom).floor().clamp(1, 4);
final scale = 0.004 / zoom;
final maxIter = (100 * zoom).floor().clamp(50, 300);
for (int py = 0; py < canvasSize.height; py += pixelSize) {
for (int px = 0; px < canvasSize.width; px += pixelSize) {
final x = centerX + (px - canvasSize.width / 2) * scale;
final y = centerY + (py - canvasSize.height / 2) * scale;
final color = _calculatePoint(x, y, maxIter);
canvas.drawRect(
Rect.fromLTWH(px.toDouble(), py.toDouble(), pixelSize.toDouble(), pixelSize.toDouble()),
Paint()..color = color,
);
}
}
}
Color _calculatePoint(double cx, double cy, int maxIter) {
final q = (cx - 0.25) * (cx - 0.25) + cy * cy;
if (q * (q + (cx - 0.25)) <= 0.25 * cy * cy) return const Color(0xFF000000);
if ((cx + 1) * (cx + 1) + cy * cy <= 0.0625) return const Color(0xFF000000);
var zr = 0.0, zi = 0.0;
int iter = 0;
while (iter < maxIter && zr * zr + zi * zi < 256) {
final temp = zr * zr - zi * zi + cx;
zi = 2 * zr * zi + cy;
zr = temp;
iter++;
}
if (iter == maxIter) return const Color(0xFF000000);
final logZn = log(sqrt(zr * zr + zi * zi));
final nu = log(logZn / log(2)) / log(2);
final smooth = (iter + 1 - nu) / maxIter;
return _getColor(smooth, iter, maxIter);
}
Color _getColor(double smooth, int iter, int maxIter) {
final t = smooth.clamp(0.0, 1.0);
switch (colorScheme) {
case 0:
final hue = (t * 360 + time * 50) % 360;
return HSVColor.fromAHSV(1, hue, 0.8, 1).toColor();
case 1:
final hue = (t * 180 + time * 30) % 360;
return HSVColor.fromAHSV(1, hue, 0.9, 0.8 + sin(t * pi + time) * 0.2).toColor();
case 2:
final r = ((sin(t * pi * 2 + time) * 0.5 + 0.5) * 255).toInt();
final g = ((sin(t * pi * 2 + time + pi * 2 / 3) * 0.5 + 0.5) * 255).toInt();
final b = ((sin(t * pi * 2 + time + pi * 4 / 3) * 0.5 + 0.5) * 255).toInt();
return Color.fromRGBO(r, g, b, 1);
case 3:
final wave = sin(t * pi * 4 + time * 2);
final hue = (t * 300 + wave * 30 + time * 40) % 360;
return HSVColor.fromAHSV(1, hue, 0.7 + wave * 0.3, 1).toColor();
default:
return HSVColor.fromAHSV(1, t * 360, 0.8, 1).toColor();
}
}
bool shouldRepaint(covariant AnimatedColorPainter old) =>
time != old.time || colorScheme != old.colorScheme || zoom != old.zoom;
}
/// 交互探索演示
class InteractiveFractalDemo extends StatefulWidget {
const InteractiveFractalDemo({super.key});
State<InteractiveFractalDemo> createState() => _InteractiveFractalDemoState();
}
class _InteractiveFractalDemoState extends State<InteractiveFractalDemo> {
double _centerX = -0.5, _centerY = 0, _scale = 0.004;
int _maxIterations = 100;
void _handleScaleStart(ScaleStartDetails details) {}
void _handleScaleUpdate(ScaleUpdateDetails details) {
setState(() {
_centerX -= details.focalPointDelta.dx * _scale;
_centerY -= details.focalPointDelta.dy * _scale;
_scale *= 1 / details.scale;
_scale = _scale.clamp(1e-15, 0.01);
});
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('交互探索')),
body: GestureDetector(
onScaleStart: _handleScaleStart,
onScaleUpdate: _handleScaleUpdate,
child: Stack(children: [
CustomPaint(painter: BasicFractalPainter(_centerX, _centerY, _scale, _maxIterations, FractalColorScheme.classic), size: Size.infinite),
Positioned(bottom: 20, left: 20, child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(color: Colors.black.withOpacity(0.7), borderRadius: BorderRadius.circular(8)),
child: Text('缩放: ${_scale.toStringAsExponential(2)}', style: const TextStyle(color: Colors.white70, fontSize: 12)),
)),
]),
),
);
}
}
/// 音乐分形演示
class MusicFractalDemo extends StatefulWidget {
const MusicFractalDemo({super.key});
State<MusicFractalDemo> createState() => _MusicFractalDemoState();
}
class _MusicFractalDemoState extends State<MusicFractalDemo> with TickerProviderStateMixin {
late AnimationController _animController;
late AudioPlayer _audioPlayer;
Float32List _audioData = Float32List(128);
bool _isPlaying = false;
Duration _position = Duration.zero;
Duration _duration = Duration.zero;
double _energy = 0, _bass = 0, _mid = 0, _high = 0;
double _time = 0;
double _centerX = -0.5, _centerY = 0;
double _zoom = 1.0;
int _colorMode = 0;
double _pulseIntensity = 0;
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);
}
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 < 128; i++) {
if (_isPlaying) {
final freq = (i / 128) * 12 + 1;
final phase = _time * freq * 0.5;
final wave = sin(phase) * 0.35 + sin(phase * 1.618) * 0.25 + sin(phase * 0.618) * 0.2;
final bassBoost = i < 20 ? 0.4 * sin(_time * 2) : 0;
final midBoost = i >= 20 && i < 60 ? 0.2 * sin(_time * 4) : 0;
final highBoost = i >= 60 ? 0.15 * sin(_time * 8) : 0;
_audioData[i] = _audioData[i] * 0.7 + (wave + bassBoost + midBoost + highBoost) * 0.3;
} else {
_audioData[i] *= 0.92;
}
}
double total = 0, bassE = 0, midE = 0, highE = 0;
for (int i = 0; i < 128; i++) {
total += _audioData[i].abs();
if (i < 20) bassE += _audioData[i].abs();
else if (i < 60) midE += _audioData[i].abs();
else highE += _audioData[i].abs();
}
final prevEnergy = _energy;
_energy = total / 128;
_bass = bassE / 20;
_mid = midE / 40;
_high = highE / 48;
if (_energy > prevEnergy * 1.3) {
_pulseIntensity = (_energy - prevEnergy).clamp(0.0, 1.0);
}
_pulseIntensity *= 0.9;
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: MusicFractalPainter(
time: _time,
energy: _energy,
bass: _bass,
mid: _mid,
high: _high,
isPlaying: _isPlaying,
zoom: _zoom,
centerX: _centerX,
centerY: _centerY,
colorMode: _colorMode,
pulseIntensity: _pulseIntensity,
audioData: _audioData,
),
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.deepPurple),
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.deepPurple : 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.deepPurple,
),
const SizedBox(height: 8),
Row(children: [
IconButton(
icon: Icon(_isPlaying ? Icons.pause : Icons.play_arrow, color: Colors.deepPurple, size: 36),
onPressed: () => _isPlaying ? _audioPlayer.pause() : _audioPlayer.play(),
),
const SizedBox(width: 16),
Expanded(
child: Column(children: [
Row(children: [
const Text('缩放:', style: TextStyle(color: Colors.white70, fontSize: 12)),
Expanded(child: Slider(value: _zoom, min: 0.5, max: 5, onChanged: (v) => setState(() => _zoom = v), activeColor: Colors.orange)),
]),
Row(children: [
const Text('配色:', style: TextStyle(color: Colors.white70, fontSize: 12)),
for (int i = 0; i < 4; i++)
GestureDetector(
onTap: () => setState(() => _colorMode = i),
child: Container(
width: 24, height: 24,
margin: const EdgeInsets.symmetric(horizontal: 4),
decoration: BoxDecoration(
color: _colorMode == i ? Colors.white : Colors.grey[700],
borderRadius: BorderRadius.circular(6),
border: Border.all(color: [Colors.deepPurple, Colors.orange, Colors.cyan, Colors.pink][i], width: 2),
),
),
),
]),
]),
),
]),
const SizedBox(height: 8),
_buildEnergyBars(),
],
),
);
}
Widget _buildEnergyBars() {
return Row(children: [
Expanded(child: _buildBar('低频', _bass, Colors.red)),
const SizedBox(width: 8),
Expanded(child: _buildBar('中频', _mid, Colors.yellow)),
const SizedBox(width: 8),
Expanded(child: _buildBar('高频', _high, Colors.cyan)),
]);
}
Widget _buildBar(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 MusicFractalPainter extends CustomPainter {
final double time;
final double energy;
final double bass;
final double mid;
final double high;
final bool isPlaying;
final double zoom;
final double centerX, centerY;
final int colorMode;
final double pulseIntensity;
final Float32List audioData;
MusicFractalPainter({
required this.time,
required this.energy,
required this.bass,
required this.mid,
required this.high,
required this.isPlaying,
required this.zoom,
required this.centerX,
required this.centerY,
required this.colorMode,
required this.pulseIntensity,
required this.audioData,
});
void paint(Canvas canvas, Size canvasSize) {
final bgColor = Color.lerp(
const Color(0xFF000015),
Color.lerp(const Color(0xFF150025), const Color(0xFF001525), mid)!,
energy * 0.5,
)!;
canvas.drawRect(Rect.fromLTWH(0, 0, canvasSize.width, canvasSize.height), Paint()..color = bgColor);
final pixelSize = (2 + (1 - energy) * 2).floor().clamp(1, 4);
final scale = 0.004 / zoom * (1 - bass * 0.2);
final maxIter = (80 + energy * 60 + zoom * 20).floor().clamp(50, 200);
final dynamicCenterX = centerX + sin(time * 0.5) * high * 0.1;
final dynamicCenterY = centerY + cos(time * 0.7) * mid * 0.1;
for (int py = 0; py < canvasSize.height; py += pixelSize) {
for (int px = 0; px < canvasSize.width; px += pixelSize) {
final x = dynamicCenterX + (px - canvasSize.width / 2) * scale;
final y = dynamicCenterY + (py - canvasSize.height / 2) * scale;
final color = _calculatePoint(x, y, maxIter, px.toDouble(), py.toDouble(), canvasSize);
canvas.drawRect(
Rect.fromLTWH(px.toDouble(), py.toDouble(), pixelSize.toDouble(), pixelSize.toDouble()),
Paint()..color = color,
);
}
}
if (isPlaying && pulseIntensity > 0.1) {
_drawPulseEffect(canvas, canvasSize);
}
}
Color _calculatePoint(double cx, double cy, int maxIter, double px, double py, Size canvasSize) {
final q = (cx - 0.25) * (cx - 0.25) + cy * cy;
if (q * (q + (cx - 0.25)) <= 0.25 * cy * cy) return const Color(0xFF000000);
if ((cx + 1) * (cx + 1) + cy * cy <= 0.0625) return const Color(0xFF000000);
var zr = 0.0, zi = 0.0;
int iter = 0;
final audioIdx = ((px / canvasSize.width) * 64).floor().clamp(0, 63);
final audioInfluence = audioData[audioIdx] * 0.3;
while (iter < maxIter && zr * zr + zi * zi < 256) {
final temp = zr * zr - zi * zi + cx + audioInfluence * 0.001;
zi = 2 * zr * zi + cy;
zr = temp;
iter++;
}
if (iter == maxIter) return const Color(0xFF000000);
final logZn = log(sqrt(zr * zr + zi * zi));
final nu = log(logZn / log(2)) / log(2);
final smooth = (iter + 1 - nu) / maxIter;
return _getColor(smooth, iter, maxIter);
}
Color _getColor(double smooth, int iter, int maxIter) {
final t = smooth.clamp(0.0, 1.0);
switch (colorMode) {
case 0:
final hue = (t * 360 + time * 40 + energy * 80) % 360;
final saturation = 0.7 + bass * 0.3;
final value = 0.8 + pulseIntensity * 0.2;
return HSVColor.fromAHSV(1, hue, saturation, value).toColor();
case 1:
final hue = (t * 240 + time * 60 + bass * 60) % 360;
final wave = sin(t * pi * 6 + time * 3);
return HSVColor.fromAHSV(1, hue, 0.8 + wave * 0.2, 0.9 + energy * 0.1).toColor();
case 2:
final r = ((sin(t * pi * 2 + time * 2) * 0.5 + 0.5 + bass * 0.3).clamp(0.0, 1.0) * 255).toInt();
final g = ((sin(t * pi * 2 + time * 2 + pi * 2 / 3) * 0.5 + 0.5 + mid * 0.3).clamp(0.0, 1.0) * 255).toInt();
final b = ((sin(t * pi * 2 + time * 2 + pi * 4 / 3) * 0.5 + 0.5 + high * 0.3).clamp(0.0, 1.0) * 255).toInt();
return Color.fromRGBO(r, g, b, 1);
case 3:
final hue = (t * 300 + time * 30 + sin(time + t * 10) * 30) % 360;
final pulse = sin(time * 10 + t * 20) * pulseIntensity;
return HSVColor.fromAHSV(0.9 + pulse * 0.1, hue, 0.85, 1).toColor();
default:
return HSVColor.fromAHSV(1, t * 360, 0.8, 1).toColor();
}
}
void _drawPulseEffect(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final maxRadius = max(size.width, size.height) * 0.6;
for (int i = 0; i < 3; i++) {
final radius = (maxRadius * (0.3 + i * 0.2) * (1 + pulseIntensity * 0.5)).clamp(10.0, maxRadius);
final alpha = (pulseIntensity * 0.3 * (1 - i * 0.3)).clamp(0.0, 1.0);
final paint = Paint()
..color = HSVColor.fromAHSV(alpha, 280 + i * 30, 0.6, 1).toColor()
..style = PaintingStyle.stroke
..strokeWidth = 2 + pulseIntensity * 3;
canvas.drawCircle(center, radius, paint);
}
}
bool shouldRepaint(covariant MusicFractalPainter old) =>
time != old.time || energy != old.energy || bass != old.bass ||
zoom != old.zoom || colorMode != old.colorMode || pulseIntensity != old.pulseIntensity;
}
/// Julia 集演示
class JuliaSetDemo extends StatefulWidget {
const JuliaSetDemo({super.key});
State<JuliaSetDemo> createState() => _JuliaSetDemoState();
}
class _JuliaSetDemoState extends State<JuliaSetDemo> with SingleTickerProviderStateMixin {
late AnimationController _controller;
double _time = 0;
int _presetIndex = 0;
final List<Complex> _presets = [
const Complex(-0.4, 0.6),
const Complex(0.285, 0.01),
const Complex(-0.70176, -0.3842),
const Complex(-0.8, 0.156),
const Complex(0.355, 0.355),
];
final List<String> _presetNames = ['Dendrite', 'Spiral', 'Dragon', 'Lightning', 'Symmetric'];
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 16))..repeat();
_controller.addListener(() { _time += 0.016; setState(() {}); });
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Julia 集')),
body: Column(children: [
Expanded(child: CustomPaint(painter: JuliaSetPainter(_presets[_presetIndex], _time), size: Size.infinite)),
Container(
padding: const EdgeInsets.all(16),
color: Colors.black12,
child: Wrap(spacing: 8, children: List.generate(_presets.length, (i) =>
ChoiceChip(label: Text(_presetNames[i]), selected: _presetIndex == i,
onSelected: (_) => setState(() => _presetIndex = i)))),
),
]),
);
}
}
class JuliaSetPainter extends CustomPainter {
final Complex c;
final double time;
JuliaSetPainter(this.c, this.time);
void paint(Canvas canvas, Size size) {
final pixelSize = 2;
final scale = 0.004;
const maxIter = 100;
for (int py = 0; py < size.height.toInt(); py += pixelSize) {
for (int px = 0; px < size.width.toInt(); px += pixelSize) {
var zr = (px - size.width / 2) * scale;
var zi = (py - size.height / 2) * scale;
int iter = 0;
while (iter < maxIter && zr * zr + zi * zi < 4) {
final temp = zr * zr - zi * zi + c.real;
zi = 2 * zr * zi + c.imaginary;
zr = temp;
iter++;
}
if (iter < maxIter) {
final hue = ((iter + time * 30) % 360).abs();
final color = HSVColor.fromAHSV(1, hue, 0.8, 1).toColor();
canvas.drawRect(Rect.fromLTWH(px.toDouble(), py.toDouble(), pixelSize.toDouble(), pixelSize.toDouble()), Paint()..color = color);
}
}
}
}
bool shouldRepaint(covariant JuliaSetPainter old) => time != old.time || c != old.c;
}
📝 四、总结
本篇文章深入探讨了 Mandelbrot 分形在音乐可视化中的应用,从复数运算到音频驱动的动态演化,构建了具有"无限复杂感"的分形动画效果。
✅ 核心知识点回顾
| 知识点 | 说明 |
|---|---|
| 🌀分形几何 | 自相似性、分数维数 |
| 📐复数运算 | 加减乘除、模、幅角 |
| ⚡迭代计算 | Mandelbrot、Julia 集 |
| 🎨连续着色 | 平滑着色算法 |
| 🎵音频驱动 | 能量映射参数 |
⭐ 最佳实践要点
- ✅ 使用快速检测跳过主心脏和主圆盘
- ✅ 周期检测优化计算
- ✅ 平滑着色避免色带
- ✅ 音频特征控制分形参数
- ✅ 多种配色方案切换
🚀 进阶方向
- 🔮 GPU 着色器加速
- ✨ 3D 分形渲染
- 👆 多点触控缩放
- ⚡ 实时分形视频
- 🌐 分形动画导出
更多推荐



所有评论(0)