Harmony Flutter 跨平台开发实战:鸿蒙与音乐律动艺术、柏林噪声场:有色噪声下的“视觉震动“
柏林噪声(Perlin Noise)是由美国计算机科学家**肯·柏林(Ken Perlin)**在 1983 年发明的。当时他正在为电影《电子世界争霸战》(Tron)制作特效,发现传统的随机噪声过于杂乱,无法产生自然、有机的视觉效果。柏林噪声的核心创新在于:它不是纯粹的随机,而是一种"平滑的随机"。这种特性使得柏林噪声能够模拟自然界中许多有机的现象:在信号处理领域,噪声根据其频谱特性可以分为不同类

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
🌫️ 一、柏林噪声:从电影特效到程序化生成
📚 1.1 柏林噪声的诞生
柏林噪声(Perlin Noise)是由美国计算机科学家**肯·柏林(Ken Perlin)**在 1983 年发明的。当时他正在为电影《电子世界争霸战》(Tron)制作特效,发现传统的随机噪声过于杂乱,无法产生自然、有机的视觉效果。
柏林噪声的核心创新在于:它不是纯粹的随机,而是一种"平滑的随机"。这种特性使得柏林噪声能够模拟自然界中许多有机的现象:
| 应用领域 | 具体用途 | 效果描述 |
|---|---|---|
| 🏔️ 地形生成 | 高度图、山脉、峡谷 | 连续起伏的自然地貌 |
| ☁️ 云彩模拟 | 动态云层、烟雾 | 流动飘逸的大气效果 |
| 🌊 水面波纹 | 海洋、湖泊表面 | 自然的波动纹理 |
| 🔥 火焰效果 | 篝火、熔岩 | 跳动的火焰动态 |
| 🪨 纹理生成 | 大理石、木材、岩石 | 真实的材质纹理 |
| 🎵 音乐可视化 | 有机流动的视觉效果 | 随音乐律动的噪声场 |
🎨 设计哲学:柏林噪声的魅力在于它介于"完全有序"和"完全随机"之间,这种"受控的混沌"正是自然界美的本质。
📐 1.2 噪声的分类:从白噪声到有色噪声
在信号处理领域,噪声根据其频谱特性可以分为不同类型:
| 噪声类型 | 频谱特性 | 视觉特征 | 应用场景 |
|---|---|---|---|
| ⚪ 白噪声 | 所有频率能量相等 | 完全随机、杂乱无章 | 电视雪花、静电噪声 |
| 🟤 布朗噪声 | 能量与频率平方成反比 | 极度平滑、缓慢变化 | 瀑布声、低频隆隆声 |
| 🟣 粉红噪声 | 能量与频率成反比 | 自然、舒适 | 雨声、树叶沙沙声 |
| 🔵 柏林噪声 | 能量集中在低频 | 有机、连续、平滑 | 地形、云彩、纹理 |
频谱能量分布示意:
白噪声: ████████████████████ (均匀分布)
粉红噪声: ████████████████ (低频增强)
布朗噪声: ██████████████ (低频主导)
柏林噪声: ████████████ (平滑低频)
🔬 1.3 柏林噪声的数学原理
🔹 1.3.1 梯度噪声的核心思想
柏林噪声是一种梯度噪声(Gradient Noise),其核心思想是:
- 定义网格点:在空间中定义一个规则的整数网格
- 分配梯度向量:为每个网格点分配一个伪随机梯度向量
- 计算距离向量:对于任意点,计算它到周围网格点的距离向量
- 点积与插值:将梯度向量与距离向量点积,然后平滑插值
示意图:
●─────────●─────────●
│ ↖ │ ↗ │
│ │ │
│ P │ │
│ │ │
●─────────●─────────●
│ ↙ │ ↘ │
│ │ │
│ │ │
│ │ │
●─────────●─────────●
● = 网格点(有梯度向量)
P = 待计算点
↖↗↙↘ = 梯度向量方向
🔹 1.3.2 一维柏林噪声
一维柏林噪声是最简单的形式,用于生成沿一条线的平滑随机值:
noise(x) = lerp(
grad(p[floor(x)], x - floor(x)),
grad(p[floor(x) + 1], x - floor(x) - 1),
fade(x - floor(x))
)
其中:
grad(p, d)= 梯度值 = 排列表值 × 距离lerp(a, b, t)= 线性插值 = a + t × (b - a)fade(t)= 平滑过渡 = 6t⁵ - 15t⁴ + 10t³
🔹 1.3.3 二维柏林噪声
二维柏林噪声需要考虑四个角点的梯度:
noise(x, y) = lerp(
lerp(dot(g00, d00), dot(g10, d10), u),
lerp(dot(g01, d01), dot(g11, d11), u),
v
)
| 符号 | 含义 |
|---|---|
| g00, g10, g01, g11 | 四个角点的梯度向量 |
| d00, d10, d01, d11 | 点到四个角点的距离向量 |
| u, v | 平滑插值参数 |
🎯 1.4 柏林噪声的关键特性
| 特性 | 说明 | 可视化效果 |
|---|---|---|
| 🔄 连续性 | 相邻点的噪声值相近 | 平滑过渡,无突变 |
| 🎲 确定性 | 相同输入总是产生相同输出 | 可重复生成 |
| 🌊 带限性 | 能量集中在特定频率范围 | 无高频噪声 |
| 🎨 可调节性 | 通过参数控制噪声特征 | 多样化效果 |
| 🔗 可叠加性 | 多层噪声叠加产生复杂效果 | 分形细节 |
🔧 二、柏林噪声的 Dart 实现
🧮 2.1 基础柏林噪声生成器
import 'dart:math';
import 'dart:typed_data';
/// 柏林噪声生成器
class PerlinNoise {
final List<int> _permutation;
final List<int> _p;
final int seed;
PerlinNoise({this.seed = 0})
: _permutation = _generatePermutation(seed),
_p = List.filled(512, 0) {
// 复制排列表两次,避免边界检查
for (int i = 0; i < 256; i++) {
_p[i] = _permutation[i];
_p[256 + i] = _permutation[i];
}
}
/// 生成伪随机排列表
static List<int> _generatePermutation(int seed) {
final random = Random(seed);
final perm = List<int>.generate(256, (i) => i);
// Fisher-Yates 洗牌算法
for (int i = 255; i > 0; i--) {
final j = random.nextInt(i + 1);
final temp = perm[i];
perm[i] = perm[j];
perm[j] = temp;
}
return perm;
}
/// 平滑过渡函数(Quintic interpolation)
double _fade(double t) {
return t * t * t * (t * (t * 6 - 15) + 10);
}
/// 线性插值
double _lerp(double a, double b, double t) {
return a + t * (b - a);
}
/// 计算梯度
double _grad(int hash, double x, double y) {
final h = hash & 7; // 取低3位
final u = h < 4 ? x : y;
final v = h < 4 ? y : x;
return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v);
}
/// 二维柏林噪声
double noise2D(double x, double y) {
// 找到单位正方形
final xi = x.floor() & 255;
final yi = y.floor() & 255;
// 相对位置
final xf = x - x.floor();
final yf = y - y.floor();
// 平滑曲线
final u = _fade(xf);
final v = _fade(yf);
// 四个角点的哈希值
final aa = _p[_p[xi] + yi];
final ab = _p[_p[xi] + yi + 1];
final ba = _p[_p[xi + 1] + yi];
final bb = _p[_p[xi + 1] + yi + 1];
// 梯度点积
final x1 = _lerp(
_grad(aa, xf, yf),
_grad(ba, xf - 1, yf),
u,
);
final x2 = _lerp(
_grad(ab, xf, yf - 1),
_grad(bb, xf - 1, yf - 1),
u,
);
return _lerp(x1, x2, v);
}
/// 一维柏林噪声
double noise1D(double x) {
return noise2D(x, 0);
}
/// 三维柏林噪声
double noise3D(double x, double y, double z) {
final xi = x.floor() & 255;
final yi = y.floor() & 255;
final zi = z.floor() & 255;
final xf = x - x.floor();
final yf = y - y.floor();
final zf = z - z.floor();
final u = _fade(xf);
final v = _fade(yf);
final w = _fade(zf);
final aaa = _p[_p[_p[xi] + yi] + zi];
final aba = _p[_p[_p[xi] + yi + 1] + zi];
final aab = _p[_p[_p[xi] + yi] + zi + 1];
final abb = _p[_p[_p[xi] + yi + 1] + zi + 1];
final baa = _p[_p[_p[xi + 1] + yi] + zi];
final bba = _p[_p[_p[xi + 1] + yi + 1] + zi];
final bab = _p[_p[_p[xi + 1] + yi] + zi + 1];
final bbb = _p[_p[_p[xi + 1] + yi + 1] + zi + 1];
double grad(int hash, double x, double y, double z) {
final h = hash & 15;
final u = h < 8 ? x : y;
final v = h < 4 ? y : (h == 12 || h == 14 ? x : z);
return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v);
}
final x1 = _lerp(grad(aaa, xf, yf, zf), grad(baa, xf - 1, yf, zf), u);
final x2 = _lerp(grad(aba, xf, yf - 1, zf), grad(bba, xf - 1, yf - 1, zf), u);
final y1 = _lerp(x1, x2, v);
final x3 = _lerp(grad(aab, xf, yf, zf - 1), grad(bab, xf - 1, yf, zf - 1), u);
final x4 = _lerp(grad(abb, xf, yf - 1, zf - 1), grad(bbb, xf - 1, yf - 1, zf - 1), u);
final y2 = _lerp(x3, x4, v);
return _lerp(y1, y2, w);
}
}
🌊 2.2 分形布朗运动(FBM)
分形布朗运动(Fractional Brownian Motion)是柏林噪声的进阶应用,通过叠加多个频率的噪声产生更丰富的细节:
/// 分形布朗运动
class FractalBrownianMotion {
final PerlinNoise noise;
final int octaves; // 叠加层数
final double persistence; // 持续度(振幅衰减因子)
final double lacunarity; // 间隙度(频率增长因子)
final double scale; // 基础缩放
FractalBrownianMotion({
int? seed,
this.octaves = 6,
this.persistence = 0.5,
this.lacunarity = 2.0,
this.scale = 0.01,
}) : noise = PerlinNoise(seed: seed ?? DateTime.now().millisecondsSinceEpoch);
/// 计算分形噪声值
double fbm(double x, double y) {
double value = 0;
double amplitude = 1;
double frequency = scale;
double maxValue = 0;
for (int i = 0; i < octaves; i++) {
value += amplitude * noise.noise2D(x * frequency, y * frequency);
maxValue += amplitude;
amplitude *= persistence;
frequency *= lacunarity;
}
return value / maxValue;
}
/// 带时间参数的动态 FBM
double fbmAnimated(double x, double y, double time) {
double value = 0;
double amplitude = 1;
double frequency = scale;
double maxValue = 0;
for (int i = 0; i < octaves; i++) {
// 每层噪声有不同的时间偏移
final timeOffset = time * (0.1 + i * 0.05);
value += amplitude * noise.noise3D(
x * frequency,
y * frequency,
timeOffset,
);
maxValue += amplitude;
amplitude *= persistence;
frequency *= lacunarity;
}
return value / maxValue;
}
/// 湍流效果(取绝对值)
double turbulence(double x, double y, double time) {
double value = 0;
double amplitude = 1;
double frequency = scale;
double maxValue = 0;
for (int i = 0; i < octaves; i++) {
final timeOffset = time * (0.1 + i * 0.05);
value += amplitude * noise.noise3D(
x * frequency,
y * frequency,
timeOffset,
).abs();
maxValue += amplitude;
amplitude *= persistence;
frequency *= lacunarity;
}
return value / maxValue;
}
}
🎨 2.3 噪声场可视化
import 'package:flutter/material.dart';
/// 噪声场可视化绘制器
class NoiseFieldPainter extends CustomPainter {
final FractalBrownianMotion fbm;
final double time;
final Color baseColor;
final Color accentColor;
final int resolution;
NoiseFieldPainter({
required this.fbm,
required this.time,
required this.baseColor,
required this.accentColor,
this.resolution = 50,
});
void paint(Canvas canvas, Size size) {
final cellWidth = size.width / resolution;
final cellHeight = size.height / resolution;
for (int x = 0; x < resolution; x++) {
for (int y = 0; y < resolution; y++) {
// 计算噪声值
final noiseValue = fbm.fbmAnimated(
x * cellWidth,
y * cellHeight,
time,
);
// 映射到 [0, 1] 范围
final normalizedValue = (noiseValue + 1) / 2;
// 插值颜色
final color = Color.lerp(baseColor, accentColor, normalizedValue)!;
// 绘制单元格
final rect = Rect.fromLTWH(
x * cellWidth,
y * cellHeight,
cellWidth + 1,
cellHeight + 1,
);
canvas.drawRect(rect, Paint()..color = color);
}
}
}
bool shouldRepaint(covariant NoiseFieldPainter oldDelegate) {
return time != oldDelegate.time;
}
}
🌬️ 三、音乐驱动的噪声场动画
🎵 3.1 音频响应的噪声参数
我们可以将音频特征映射到噪声参数,创造出随音乐律动的视觉效果:
import 'package:flutter/material.dart';
import 'package:just_audio_ohos/just_audio_ohos.dart';
import 'package:audio_session/audio_session.dart';
import 'dart:typed_data';
import 'dart:math';
/// 音频驱动的噪声场控制器
class AudioNoiseController extends ChangeNotifier {
final AudioPlayer _player = AudioPlayer();
final FractalBrownianMotion _fbm = FractalBrownianMotion(octaves: 4);
bool _isPlaying = false;
Duration _position = Duration.zero;
Duration _duration = Duration.zero;
// 音频特征
double _energy = 0;
double _bass = 0;
double _treble = 0;
Float32List _audioData = Float32List(128);
// 噪声参数
double _noiseScale = 0.005;
double _noiseSpeed = 1.0;
double _colorShift = 0;
bool get isPlaying => _isPlaying;
Duration get position => _position;
Duration get duration => _duration;
double get energy => _energy;
double get bass => _bass;
double get treble => _treble;
Float32List get audioData => _audioData;
FractalBrownianMotion get fbm => _fbm;
double get noiseScale => _noiseScale;
double get noiseSpeed => _noiseSpeed;
double get colorShift => _colorShift;
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 updateAudioData(double time) {
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.7 +
(wave1 + wave2 + noise + bassBoost) * 0.3;
} else {
_audioData[i] = _audioData[i] * 0.95;
}
}
// 计算音频特征
_calculateFeatures();
// 更新噪声参数
_updateNoiseParams(time);
notifyListeners();
}
/// 计算音频特征
void _calculateFeatures() {
// 总能量
double totalEnergy = 0;
for (int i = 0; i < 128; i++) {
totalEnergy += _audioData[i].abs();
}
_energy = totalEnergy / 128;
// 低频能量(低音)
double bassEnergy = 0;
for (int i = 0; i < 32; i++) {
bassEnergy += _audioData[i].abs();
}
_bass = bassEnergy / 32;
// 高频能量(高音)
double trebleEnergy = 0;
for (int i = 96; i < 128; i++) {
trebleEnergy += _audioData[i].abs();
}
_treble = trebleEnergy / 32;
}
/// 更新噪声参数
void _updateNoiseParams(double time) {
// 低音影响噪声缩放
_noiseScale = 0.003 + _bass * 0.01;
// 能量影响动画速度
_noiseSpeed = 0.5 + _energy * 2;
// 高音影响颜色偏移
_colorShift = time * 10 + _treble * 60;
}
/// 播放/暂停
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 FlowingNoisePainter extends CustomPainter {
final AudioNoiseController controller;
final double time;
final Size size;
FlowingNoisePainter({
required this.controller,
required this.time,
required this.size,
});
void paint(Canvas canvas, Size size) {
// 绘制背景渐变
_drawBackground(canvas, size);
// 绘制噪声场
_drawNoiseField(canvas, size);
// 绘制流动线条
_drawFlowLines(canvas, size);
// 绘制粒子
if (controller.isPlaying) {
_drawParticles(canvas, size);
}
}
void _drawBackground(Canvas canvas, Size size) {
final gradient = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
const Color(0xFF0A0A1A),
const Color(0xFF1A1A3A),
const Color(0xFF0A0A1A),
],
);
final paint = Paint()
..shader = gradient.createShader(Rect.fromLTWH(0, 0, size.width, size.height));
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), paint);
}
void _drawNoiseField(Canvas canvas, Size size) {
final fbm = controller.fbm;
final resolution = 40;
final cellWidth = size.width / resolution;
final cellHeight = size.height / resolution;
for (int x = 0; x < resolution; x++) {
for (int y = 0; y < resolution; y++) {
final px = x * cellWidth;
final py = y * cellHeight;
// 计算噪声值
final noiseValue = fbm.fbmAnimated(
px * controller.noiseScale,
py * controller.noiseScale,
time * controller.noiseSpeed,
);
// 基于音频能量调整亮度
final brightness = 0.3 + controller.energy * 0.7;
final normalizedValue = (noiseValue + 1) / 2 * brightness;
// 颜色计算
final hue = (controller.colorShift + normalizedValue * 60) % 360;
final color = HSVColor.fromAHSV(
normalizedValue * 0.6,
hue,
0.8,
normalizedValue,
).toColor();
final rect = Rect.fromLTWH(px, py, cellWidth + 1, cellHeight + 1);
canvas.drawRect(rect, Paint()..color = color);
}
}
}
void _drawFlowLines(Canvas canvas, Size size) {
final fbm = controller.fbm;
final lineCount = 20;
final pointsPerLine = 50;
for (int i = 0; i < lineCount; i++) {
final startY = size.height * (i / lineCount);
final path = Path();
var x = 0.0;
var y = startY;
path.moveTo(x, y);
for (int j = 0; j < pointsPerLine; j++) {
// 使用噪声计算流动方向
final noiseX = fbm.noise3D(x * 0.01, y * 0.01, time * 0.5);
final noiseY = fbm.noise3D(x * 0.01 + 100, y * 0.01 + 100, time * 0.5);
// 根据音频能量调整步长
final step = 10 + controller.energy * 20;
x += noiseX * step;
y += noiseY * step;
path.lineTo(x, y);
}
final hue = (controller.colorShift + i * 15) % 360;
final paint = Paint()
..color = HSVColor.fromAHSV(0.5, hue, 0.8, 0.8).toColor()
..strokeWidth = 1.5 + controller.bass * 2
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
canvas.drawPath(path, paint);
}
}
void _drawParticles(Canvas canvas, Size size) {
final random = Random(time.toInt());
final particleCount = 50;
for (int i = 0; i < particleCount; i++) {
final x = random.nextDouble() * size.width;
final y = random.nextDouble() * size.height;
final radius = random.nextDouble() * 4 + 1;
final hue = (controller.colorShift + i * 7) % 360;
final color = HSVColor.fromAHSV(0.7, hue, 1, 1).toColor();
final paint = Paint()
..color = color
..style = PaintingStyle.fill
..maskFilter = MaskFilter.blur(BlurStyle.normal, 3 + controller.treble * 5);
canvas.drawCircle(Offset(x, y), radius, paint);
}
}
bool shouldRepaint(covariant FlowingNoisePainter 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:typed_data';
import 'dart:math';
void main() {
runApp(const NoiseFieldApp());
}
class NoiseFieldApp extends StatelessWidget {
const NoiseFieldApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: '柏林噪声场',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.deepPurple,
brightness: Brightness.dark,
),
useMaterial3: true,
),
home: const NoiseFieldHomePage(),
debugShowCheckedModeBanner: false,
);
}
}
class NoiseFieldHomePage extends StatelessWidget {
const NoiseFieldHomePage({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.grid_on,
color: Colors.blue,
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const BasicNoiseDemo()),
),
),
_buildSectionCard(
context,
title: '分形噪声',
description: 'FBM 多层叠加效果',
icon: Icons.layers,
color: Colors.purple,
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const FractalNoiseDemo()),
),
),
_buildSectionCard(
context,
title: '流动噪声',
description: '动态流场可视化',
icon: Icons.air,
color: Colors.cyan,
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const FlowNoiseDemo()),
),
),
_buildSectionCard(
context,
title: '音乐噪声场',
description: '音频驱动的噪声动画',
icon: Icons.music_note,
color: Colors.orange,
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const MusicNoiseDemo()),
),
),
_buildSectionCard(
context,
title: '综合演示',
description: '完整噪声场体验',
icon: Icons.blur_on,
color: Colors.pink,
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const FullNoiseDemo()),
),
),
],
),
);
}
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 PerlinNoise {
final List<int> _p;
PerlinNoise({int seed = 0}) : _p = _initPermutation(seed);
static List<int> _initPermutation(int seed) {
final random = Random(seed);
final perm = List<int>.generate(256, (i) => i);
for (int i = 255; i > 0; i--) {
final j = random.nextInt(i + 1);
final temp = perm[i];
perm[i] = perm[j];
perm[j] = temp;
}
return [...perm, ...perm];
}
double _fade(double t) => t * t * t * (t * (t * 6 - 15) + 10);
double _lerp(double a, double b, double t) => a + t * (b - a);
double _grad(int hash, double x, double y) {
final h = hash & 7;
final u = h < 4 ? x : y;
final v = h < 4 ? y : x;
return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v);
}
double noise2D(double x, double y) {
final xi = x.floor() & 255;
final yi = y.floor() & 255;
final xf = x - x.floor();
final yf = y - y.floor();
final u = _fade(xf);
final v = _fade(yf);
final aa = _p[_p[xi] + yi];
final ab = _p[_p[xi] + yi + 1];
final ba = _p[_p[xi + 1] + yi];
final bb = _p[_p[xi + 1] + yi + 1];
return _lerp(
_lerp(_grad(aa, xf, yf), _grad(ba, xf - 1, yf), u),
_lerp(_grad(ab, xf, yf - 1), _grad(bb, xf - 1, yf - 1), u),
v,
);
}
double noise3D(double x, double y, double z) {
final xi = x.floor() & 255;
final yi = y.floor() & 255;
final zi = z.floor() & 255;
final xf = x - x.floor();
final yf = y - y.floor();
final zf = z - z.floor();
final u = _fade(xf);
final v = _fade(yf);
final w = _fade(zf);
double grad(int hash, double x, double y, double z) {
final h = hash & 15;
final u = h < 8 ? x : y;
final v = h < 4 ? y : (h == 12 || h == 14 ? x : z);
return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v);
}
final aaa = _p[_p[_p[xi] + yi] + zi];
final aba = _p[_p[_p[xi] + yi + 1] + zi];
final aab = _p[_p[_p[xi] + yi] + zi + 1];
final abb = _p[_p[_p[xi] + yi + 1] + zi + 1];
final baa = _p[_p[_p[xi + 1] + yi] + zi];
final bba = _p[_p[_p[xi + 1] + yi + 1] + zi];
final bab = _p[_p[_p[xi + 1] + yi] + zi + 1];
final bbb = _p[_p[_p[xi + 1] + yi + 1] + zi + 1];
return _lerp(
_lerp(
_lerp(grad(aaa, xf, yf, zf), grad(baa, xf - 1, yf, zf), u),
_lerp(grad(aba, xf, yf - 1, zf), grad(bba, xf - 1, yf - 1, zf), u),
v,
),
_lerp(
_lerp(grad(aab, xf, yf, zf - 1), grad(bab, xf - 1, yf, zf - 1), u),
_lerp(grad(abb, xf, yf - 1, zf - 1), grad(bbb, xf - 1, yf - 1, zf - 1), u),
v,
),
w,
);
}
}
/// 分形布朗运动
class FBM {
final PerlinNoise noise;
final int octaves;
final double persistence;
final double lacunarity;
FBM({
int? seed,
this.octaves = 6,
this.persistence = 0.5,
this.lacunarity = 2.0,
}) : noise = PerlinNoise(seed: seed ?? DateTime.now().microsecondsSinceEpoch);
double get(double x, double y, double time) {
double value = 0;
double amplitude = 1;
double frequency = 1;
double maxValue = 0;
for (int i = 0; i < octaves; i++) {
value += amplitude * noise.noise3D(x * frequency, y * frequency, time);
maxValue += amplitude;
amplitude *= persistence;
frequency *= lacunarity;
}
return value / maxValue;
}
}
/// 基础噪声演示
class BasicNoiseDemo extends StatefulWidget {
const BasicNoiseDemo({super.key});
State<BasicNoiseDemo> createState() => _BasicNoiseDemoState();
}
class _BasicNoiseDemoState extends State<BasicNoiseDemo> with SingleTickerProviderStateMixin {
late AnimationController _controller;
final PerlinNoise _noise = PerlinNoise(seed: 42);
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 10),
)..repeat();
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('基础噪声')),
body: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return CustomPaint(
painter: BasicNoisePainter(
noise: _noise,
time: _controller.value * 10,
),
size: Size.infinite,
);
},
),
);
}
}
class BasicNoisePainter extends CustomPainter {
final PerlinNoise noise;
final double time;
BasicNoisePainter({required this.noise, required this.time});
void paint(Canvas canvas, Size size) {
final resolution = 60;
final cellW = size.width / resolution;
final cellH = size.height / resolution;
for (int x = 0; x < resolution; x++) {
for (int y = 0; y < resolution; y++) {
final value = noise.noise2D(x * 0.1 + time * 0.5, y * 0.1);
final normalized = (value + 1) / 2;
final hue = normalized * 240;
final color = HSVColor.fromAHSV(1, hue, 0.8, normalized).toColor();
canvas.drawRect(
Rect.fromLTWH(x * cellW, y * cellH, cellW + 1, cellH + 1),
Paint()..color = color,
);
}
}
}
bool shouldRepaint(covariant BasicNoisePainter oldDelegate) => time != oldDelegate.time;
}
/// 分形噪声演示
class FractalNoiseDemo extends StatefulWidget {
const FractalNoiseDemo({super.key});
State<FractalNoiseDemo> createState() => _FractalNoiseDemoState();
}
class _FractalNoiseDemoState extends State<FractalNoiseDemo> with SingleTickerProviderStateMixin {
late AnimationController _controller;
final FBM _fbm = FBM(octaves: 6, persistence: 0.5);
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 15),
)..repeat();
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('分形噪声')),
body: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return CustomPaint(
painter: FractalNoisePainter(
fbm: _fbm,
time: _controller.value * 15,
),
size: Size.infinite,
);
},
),
);
}
}
class FractalNoisePainter extends CustomPainter {
final FBM fbm;
final double time;
FractalNoisePainter({required this.fbm, required this.time});
void paint(Canvas canvas, Size size) {
final resolution = 50;
final cellW = size.width / resolution;
final cellH = size.height / resolution;
for (int x = 0; x < resolution; x++) {
for (int y = 0; y < resolution; y++) {
final value = fbm.get(x * cellW * 0.01, y * cellH * 0.01, time * 0.3);
final normalized = (value + 1) / 2;
final gradient = LinearGradient(
colors: [
const Color(0xFF1a1a2e),
const Color(0xFF16213e),
const Color(0xFF0f3460),
const Color(0xFFe94560),
],
);
final colorIndex = normalized * 3;
Color color;
if (colorIndex < 1) {
color = Color.lerp(const Color(0xFF1a1a2e), const Color(0xFF16213e), colorIndex)!;
} else if (colorIndex < 2) {
color = Color.lerp(const Color(0xFF16213e), const Color(0xFF0f3460), colorIndex - 1)!;
} else {
color = Color.lerp(const Color(0xFF0f3460), const Color(0xFFe94560), colorIndex - 2)!;
}
canvas.drawRect(
Rect.fromLTWH(x * cellW, y * cellH, cellW + 1, cellH + 1),
Paint()..color = color,
);
}
}
}
bool shouldRepaint(covariant FractalNoisePainter oldDelegate) => time != oldDelegate.time;
}
/// 流动噪声演示
class FlowNoiseDemo extends StatefulWidget {
const FlowNoiseDemo({super.key});
State<FlowNoiseDemo> createState() => _FlowNoiseDemoState();
}
class _FlowNoiseDemoState extends State<FlowNoiseDemo> with SingleTickerProviderStateMixin {
late AnimationController _controller;
final FBM _fbm = FBM(octaves: 4);
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 20),
)..repeat();
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('流动噪声')),
body: Container(
color: Colors.black,
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return CustomPaint(
painter: FlowNoisePainter(
fbm: _fbm,
time: _controller.value * 20,
),
size: Size.infinite,
);
},
),
),
);
}
}
class FlowNoisePainter extends CustomPainter {
final FBM fbm;
final double time;
FlowNoisePainter({required this.fbm, required this.time});
void paint(Canvas canvas, Size size) {
// 背景
canvas.drawRect(
Rect.fromLTWH(0, 0, size.width, size.height),
Paint()..color = const Color(0xFF050510),
);
// 流线
final lineCount = 30;
for (int i = 0; i < lineCount; i++) {
final path = Path();
var x = 0.0;
var y = size.height * (i / lineCount);
path.moveTo(x, y);
for (int j = 0; j < 100; j++) {
final angle = fbm.get(x * 0.005, y * 0.005, time * 0.2) * pi * 2;
x += cos(angle) * 8;
y += sin(angle) * 8;
path.lineTo(x, y);
}
final hue = (i / lineCount * 180 + time * 10) % 360;
final paint = Paint()
..color = HSVColor.fromAHSV(0.6, hue, 0.8, 0.9).toColor()
..strokeWidth = 1.5
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
canvas.drawPath(path, paint);
}
}
bool shouldRepaint(covariant FlowNoisePainter oldDelegate) => time != oldDelegate.time;
}
/// 音乐噪声演示
class MusicNoiseDemo extends StatefulWidget {
const MusicNoiseDemo({super.key});
State<MusicNoiseDemo> createState() => _MusicNoiseDemoState();
}
class _MusicNoiseDemoState extends State<MusicNoiseDemo> with TickerProviderStateMixin {
late AnimationController _animController;
late AudioPlayer _audioPlayer;
final FBM _fbm = FBM(octaves: 4);
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;
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((state) {
setState(() {
_isPlaying = state.playing;
});
});
_audioPlayer.positionStream.listen((position) {
setState(() {
_position = position;
});
});
_audioPlayer.durationStream.listen((duration) {
setState(() {
_duration = duration ?? 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 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.7 + (wave1 + wave2 + noise + bassBoost) * 0.3;
} else {
_audioData[i] = _audioData[i] * 0.95;
}
}
// 计算特征
double totalEnergy = 0;
double 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;
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: MusicNoisePainter(
fbm: _fbm,
time: DateTime.now().millisecondsSinceEpoch / 1000.0,
audioData: _audioData,
energy: _energy,
bass: _bass,
isPlaying: _isPlaying,
),
size: Size.infinite,
),
Positioned(
bottom: 30,
left: 20,
right: 20,
child: _buildPlayerControls(),
),
],
),
);
}
Widget _buildPlayerControls() {
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),
SliderTheme(
data: SliderTheme.of(context).copyWith(
activeTrackColor: Colors.deepPurple,
inactiveTrackColor: Colors.grey.shade800,
thumbColor: Colors.deepPurple,
),
child: Slider(
value: _duration.inMilliseconds > 0
? _position.inMilliseconds.toDouble().clamp(0, _duration.inMilliseconds.toDouble())
: 0,
max: _duration.inMilliseconds > 0
? _duration.inMilliseconds.toDouble()
: 1,
onChanged: (value) {
_audioPlayer.seek(Duration(milliseconds: value.toInt()));
},
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_formatDuration(_position),
style: const TextStyle(color: Colors.white70, fontSize: 12)),
const SizedBox(width: 20),
IconButton(
icon: Icon(_isPlaying ? Icons.pause : Icons.play_arrow,
color: Colors.deepPurple, size: 40),
onPressed: () {
if (_isPlaying) {
_audioPlayer.pause();
} else {
_audioPlayer.play();
}
},
),
const SizedBox(width: 20),
Text(_formatDuration(_duration),
style: const TextStyle(color: Colors.white70, fontSize: 12)),
],
),
],
),
);
}
String _formatDuration(Duration d) {
final minutes = d.inMinutes;
final seconds = d.inSeconds.remainder(60);
return '$minutes:${seconds.toString().padLeft(2, '0')}';
}
}
class MusicNoisePainter extends CustomPainter {
final FBM fbm;
final double time;
final Float32List audioData;
final double energy;
final double bass;
final bool isPlaying;
MusicNoisePainter({
required this.fbm,
required this.time,
required this.audioData,
required this.energy,
required this.bass,
required this.isPlaying,
});
void paint(Canvas canvas, Size size) {
// 背景
final bgGradient = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
const Color(0xFF0a0a1a),
Color.lerp(const Color(0xFF1a1a3a), const Color(0xFF3a1a5a), bass)!,
const Color(0xFF0a0a1a),
],
);
canvas.drawRect(
Rect.fromLTWH(0, 0, size.width, size.height),
Paint()..shader = bgGradient.createShader(Rect.fromLTWH(0, 0, size.width, size.height)),
);
// 噪声场
final resolution = 35;
final cellW = size.width / resolution;
final cellH = size.height / resolution;
final scale = 0.003 + bass * 0.01;
final speed = 0.3 + energy * 0.5;
for (int x = 0; x < resolution; x++) {
for (int y = 0; y < resolution; y++) {
final value = fbm.get(
x * cellW * scale,
y * cellH * scale,
time * speed,
);
final normalized = (value + 1) / 2;
final brightness = 0.3 + energy * 0.7;
final hue = (time * 20 + normalized * 60) % 360;
final color = HSVColor.fromAHSV(
normalized * brightness * 0.7,
hue,
0.8,
normalized * brightness,
).toColor();
canvas.drawRect(
Rect.fromLTWH(x * cellW, y * cellH, cellW + 1, cellH + 1),
Paint()..color = color,
);
}
}
// 流线
final lineCount = 15;
for (int i = 0; i < lineCount; i++) {
final path = Path();
var x = 0.0;
var y = size.height * (i / lineCount);
path.moveTo(x, y);
for (int j = 0; j < 80; j++) {
final angle = fbm.get(x * 0.003, y * 0.003, time * 0.2) * pi * 2;
final step = 8 + energy * 15;
x += cos(angle) * step;
y += sin(angle) * step;
path.lineTo(x, y);
}
final hue = (i / lineCount * 180 + time * 15) % 360;
final paint = Paint()
..color = HSVColor.fromAHSV(0.4 + energy * 0.3, hue, 0.8, 0.8).toColor()
..strokeWidth = 1 + bass * 3
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
canvas.drawPath(path, paint);
}
// 粒子
if (isPlaying) {
final random = Random(time.toInt());
for (int i = 0; i < 40; i++) {
final px = random.nextDouble() * size.width;
final py = random.nextDouble() * size.height;
final radius = random.nextDouble() * 3 + 1;
final hue = (time * 30 + i * 9) % 360;
final paint = Paint()
..color = HSVColor.fromAHSV(0.6, hue, 1, 1).toColor()
..maskFilter = MaskFilter.blur(BlurStyle.normal, 3);
canvas.drawCircle(Offset(px, py), radius, paint);
}
}
}
bool shouldRepaint(covariant MusicNoisePainter oldDelegate) => true;
}
/// 综合演示
class FullNoiseDemo extends StatefulWidget {
const FullNoiseDemo({super.key});
State<FullNoiseDemo> createState() => _FullNoiseDemoState();
}
class _FullNoiseDemoState extends State<FullNoiseDemo> with TickerProviderStateMixin {
late AnimationController _animController;
late AudioPlayer _audioPlayer;
final FBM _fbm = FBM(octaves: 5, persistence: 0.6);
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;
double _treble = 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((state) {
setState(() {
_isPlaying = state.playing;
});
});
_audioPlayer.positionStream.listen((position) {
setState(() {
_position = position;
});
});
_audioPlayer.durationStream.listen((duration) {
setState(() {
_duration = duration ?? 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 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;
}
}
double totalEnergy = 0;
double bassEnergy = 0;
double trebleEnergy = 0;
for (int i = 0; i < 128; i++) {
totalEnergy += _audioData[i].abs();
if (i < 32) bassEnergy += _audioData[i].abs();
if (i >= 96) trebleEnergy += _audioData[i].abs();
}
_energy = totalEnergy / 128;
_bass = bassEnergy / 32;
_treble = trebleEnergy / 32;
setState(() {});
}
void dispose() {
_animController.dispose();
_audioPlayer.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
CustomPaint(
painter: FullNoisePainter(
fbm: _fbm,
time: DateTime.now().millisecondsSinceEpoch / 1000.0,
audioData: _audioData,
energy: _energy,
bass: _bass,
treble: _treble,
isPlaying: _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: _buildPlayerControls(),
),
],
),
);
}
Widget _buildPlayerControls() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.7),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.deepPurple.withOpacity(0.3), width: 1),
boxShadow: [
BoxShadow(
color: Colors.deepPurple.withOpacity(0.2),
blurRadius: 20,
spreadRadius: 5,
),
],
),
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),
SliderTheme(
data: SliderTheme.of(context).copyWith(
activeTrackColor: Colors.deepPurple,
inactiveTrackColor: Colors.grey.shade800,
thumbColor: Colors.deepPurple,
overlayColor: Colors.deepPurple.withOpacity(0.2),
),
child: Slider(
value: _duration.inMilliseconds > 0
? _position.inMilliseconds.toDouble().clamp(0, _duration.inMilliseconds.toDouble())
: 0,
max: _duration.inMilliseconds > 0
? _duration.inMilliseconds.toDouble()
: 1,
onChanged: (value) {
_audioPlayer.seek(Duration(milliseconds: value.toInt()));
},
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(_formatDuration(_position),
style: const TextStyle(color: Colors.white70, fontSize: 12)),
Text(_formatDuration(_duration),
style: const TextStyle(color: Colors.white70, fontSize: 12)),
],
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.replay_10, color: Colors.white70, size: 28),
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(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Colors.deepPurple.shade400, Colors.deepPurple.shade700],
),
boxShadow: [
BoxShadow(color: Colors.deepPurple.withOpacity(0.4), blurRadius: 15, spreadRadius: 2),
],
),
child: IconButton(
icon: Icon(_isPlaying ? Icons.pause : Icons.play_arrow, color: Colors.white, size: 36),
onPressed: () {
if (_isPlaying) {
_audioPlayer.pause();
} else {
_audioPlayer.play();
}
},
),
),
const SizedBox(width: 20),
IconButton(
icon: const Icon(Icons.forward_10, color: Colors.white70, size: 28),
onPressed: () {
final newPos = _position + const Duration(seconds: 10);
_audioPlayer.seek(newPos > _duration ? _duration : newPos);
},
),
],
),
],
),
);
}
String _formatDuration(Duration d) {
final minutes = d.inMinutes;
final seconds = d.inSeconds.remainder(60);
return '$minutes:${seconds.toString().padLeft(2, '0')}';
}
}
class FullNoisePainter extends CustomPainter {
final FBM fbm;
final double time;
final Float32List audioData;
final double energy;
final double bass;
final double treble;
final bool isPlaying;
FullNoisePainter({
required this.fbm,
required this.time,
required this.audioData,
required this.energy,
required this.bass,
required this.treble,
required this.isPlaying,
});
void paint(Canvas canvas, Size size) {
// 动态背景
final bgGradient = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color.lerp(const Color(0xFF050510), const Color(0xFF100520), bass)!,
Color.lerp(const Color(0xFF0a0a2a), const Color(0xFF2a0a4a), energy)!,
Color.lerp(const Color(0xFF050510), const Color(0xFF100520), treble)!,
],
);
canvas.drawRect(
Rect.fromLTWH(0, 0, size.width, size.height),
Paint()..shader = bgGradient.createShader(Rect.fromLTWH(0, 0, size.width, size.height)),
);
// 多层噪声场
_drawNoiseLayer(canvas, size, 0.002, 0.3, 0.3);
_drawNoiseLayer(canvas, size, 0.004, 0.5, 0.5);
_drawNoiseLayer(canvas, size, 0.008, 0.7, 0.7);
// 流线
_drawFlowLines(canvas, size);
// 波纹效果
_drawRipples(canvas, size);
// 粒子
if (isPlaying) {
_drawParticles(canvas, size);
}
}
void _drawNoiseLayer(Canvas canvas, Size size, double scale, double alpha, double speed) {
final resolution = 30;
final cellW = size.width / resolution;
final cellH = size.height / resolution;
final actualScale = scale + bass * 0.005;
final actualSpeed = speed + energy * 0.3;
for (int x = 0; x < resolution; x++) {
for (int y = 0; y < resolution; y++) {
final value = fbm.get(
x * cellW * actualScale,
y * cellH * actualScale,
time * actualSpeed,
);
final normalized = (value + 1) / 2;
final hue = (time * 15 + normalized * 90 + treble * 30) % 360;
final color = HSVColor.fromAHSV(
normalized * alpha * (0.5 + energy * 0.5),
hue,
0.7 + energy * 0.3,
normalized,
).toColor();
canvas.drawRect(
Rect.fromLTWH(x * cellW, y * cellH, cellW + 1, cellH + 1),
Paint()..color = color,
);
}
}
}
void _drawFlowLines(Canvas canvas, Size size) {
final lineCount = 20;
for (int i = 0; i < lineCount; i++) {
final path = Path();
var x = 0.0;
var y = size.height * (i / lineCount);
path.moveTo(x, y);
for (int j = 0; j < 100; j++) {
final angle = fbm.get(x * 0.003, y * 0.003, time * 0.15) * pi * 2;
final step = 6 + energy * 12;
x += cos(angle) * step;
y += sin(angle) * step;
path.lineTo(x, y);
}
final hue = (i / lineCount * 180 + time * 12) % 360;
final paint = Paint()
..color = HSVColor.fromAHSV(0.35 + energy * 0.25, hue, 0.8, 0.85).toColor()
..strokeWidth = 0.8 + bass * 2.5
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
canvas.drawPath(path, paint);
}
}
void _drawRipples(Canvas canvas, Size size) {
final rippleCount = 5;
final centerX = size.width / 2;
final centerY = size.height / 2;
for (int i = 0; i < rippleCount; i++) {
final phase = (time * 0.5 + i * 0.2) % 1;
final radius = phase * size.width * 0.6;
final alpha = (1 - phase) * 0.3 * energy;
final hue = (time * 20 + i * 40) % 360;
final paint = Paint()
..color = HSVColor.fromAHSV(alpha, hue, 0.6, 1).toColor()
..strokeWidth = 2 + bass * 3
..style = PaintingStyle.stroke;
canvas.drawCircle(Offset(centerX, centerY), radius, paint);
}
}
void _drawParticles(Canvas canvas, Size size) {
final random = Random(time.toInt());
final particleCount = 60;
for (int i = 0; i < particleCount; i++) {
final px = random.nextDouble() * size.width;
final py = random.nextDouble() * size.height;
final radius = random.nextDouble() * 4 + 1;
final hue = (time * 25 + i * 6) % 360;
final paint = Paint()
..color = HSVColor.fromAHSV(0.65, hue, 1, 1).toColor()
..maskFilter = MaskFilter.blur(BlurStyle.normal, 2 + treble * 4);
canvas.drawCircle(Offset(px, py), radius, paint);
}
}
bool shouldRepaint(covariant FullNoisePainter oldDelegate) => true;
}
📝 五、总结
本篇文章深入探讨了柏林噪声在音乐可视化中的应用,从数学原理到代码实现,涵盖了以下核心内容:
✅ 核心知识点回顾
| 知识点 | 说明 |
|---|---|
| 📐 柏林噪声原理 | 梯度噪声、平滑插值、排列表 |
| 🌊 分形布朗运动 | 多层叠加、八度、持续度、间隙度 |
| 🎵 音频驱动参数 | 能量映射噪声缩放和速度 |
| 🎨 流动场可视化 | 噪声驱动的流线绘制 |
| 🔊 网络音乐播放 | just_audio_ohos 实现在线 MP3 |
⭐ 最佳实践要点
- ✅ 使用 FBM 叠加多层噪声产生丰富细节
- ✅ 将音频特征映射到噪声参数实现动态效果
- ✅ 结合流线和粒子增强视觉层次
- ✅ 低音影响缩放,高音影响颜色
🚀 进阶方向
- 🔮 使用 Simplex Noise 提升计算效率
- ✨ 实现 3D 噪声场体积渲染
- 👆 添加触摸交互控制噪声参数
- ⚡ 使用 Isolate 进行并行噪声计算
更多推荐
所有评论(0)