Harmony Flutter 跨平台开发实战:鸿蒙与音乐律动艺术、柏林噪声场:有色噪声下的“视觉震动“

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net


🌫️ 一、柏林噪声:从电影特效到程序化生成

📚 1.1 柏林噪声的诞生

柏林噪声(Perlin Noise)是由美国计算机科学家**肯·柏林(Ken Perlin)**在 1983 年发明的。当时他正在为电影《电子世界争霸战》(Tron)制作特效,发现传统的随机噪声过于杂乱,无法产生自然、有机的视觉效果。

柏林噪声的核心创新在于:它不是纯粹的随机,而是一种"平滑的随机"。这种特性使得柏林噪声能够模拟自然界中许多有机的现象:

应用领域 具体用途 效果描述
🏔️ 地形生成 高度图、山脉、峡谷 连续起伏的自然地貌
☁️ 云彩模拟 动态云层、烟雾 流动飘逸的大气效果
🌊 水面波纹 海洋、湖泊表面 自然的波动纹理
🔥 火焰效果 篝火、熔岩 跳动的火焰动态
🪨 纹理生成 大理石、木材、岩石 真实的材质纹理
🎵 音乐可视化 有机流动的视觉效果 随音乐律动的噪声场

🎨 设计哲学:柏林噪声的魅力在于它介于"完全有序"和"完全随机"之间,这种"受控的混沌"正是自然界美的本质。


📐 1.2 噪声的分类:从白噪声到有色噪声

在信号处理领域,噪声根据其频谱特性可以分为不同类型:

噪声类型 频谱特性 视觉特征 应用场景
白噪声 所有频率能量相等 完全随机、杂乱无章 电视雪花、静电噪声
🟤 布朗噪声 能量与频率平方成反比 极度平滑、缓慢变化 瀑布声、低频隆隆声
🟣 粉红噪声 能量与频率成反比 自然、舒适 雨声、树叶沙沙声
🔵 柏林噪声 能量集中在低频 有机、连续、平滑 地形、云彩、纹理
复制代码
频谱能量分布示意:

白噪声:  ████████████████████  (均匀分布)
粉红噪声: ████████████████      (低频增强)
布朗噪声: ██████████████        (低频主导)
柏林噪声: ████████████          (平滑低频)

🔬 1.3 柏林噪声的数学原理

🔹 1.3.1 梯度噪声的核心思想

柏林噪声是一种梯度噪声(Gradient Noise),其核心思想是:

  1. 定义网格点:在空间中定义一个规则的整数网格

  2. 分配梯度向量:为每个网格点分配一个伪随机梯度向量

  3. 计算距离向量:对于任意点,计算它到周围网格点的距离向量

  4. 点积与插值:将梯度向量与距离向量点积,然后平滑插值

    示意图:

    复制代码
     ●─────────●─────────●
     │    ↖    │    ↗    │
     │         │         │
     │  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 基础柏林噪声生成器

dart 复制代码
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)是柏林噪声的进阶应用,通过叠加多个频率的噪声产生更丰富的细节:

dart 复制代码
/// 分形布朗运动
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 噪声场可视化

dart 复制代码
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,
  });
  
  @override
  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);
      }
    }
  }
  
  @override
  bool shouldRepaint(covariant NoiseFieldPainter oldDelegate) {
    return time != oldDelegate.time;
  }
}

🌬️ 三、音乐驱动的噪声场动画

🎵 3.1 音频响应的噪声参数

我们可以将音频特征映射到噪声参数,创造出随音乐律动的视觉效果:

dart 复制代码
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);
  }
  
  @override
  void dispose() {
    _player.dispose();
    super.dispose();
  }
}

🌊 3.2 流动噪声场绘制器

dart 复制代码
/// 流动噪声场绘制器
class FlowingNoisePainter extends CustomPainter {
  final AudioNoiseController controller;
  final double time;
  final Size size;
  
  FlowingNoisePainter({
    required this.controller,
    required this.time,
    required this.size,
  });
  
  @override
  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);
    }
  }
  
  @override
  bool shouldRepaint(covariant FlowingNoisePainter oldDelegate) => true;
}

🎨 四、完整示例代码

以下是完整的柏林噪声场音乐可视化示例代码,包含网络 MP3 播放功能:

dart 复制代码
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});

  @override
  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});

  @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: [
          _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});

  @override
  State<BasicNoiseDemo> createState() => _BasicNoiseDemoState();
}

class _BasicNoiseDemoState extends State<BasicNoiseDemo> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  final PerlinNoise _noise = PerlinNoise(seed: 42);
  
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 10),
    )..repeat();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  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});
  
  @override
  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,
        );
      }
    }
  }
  
  @override
  bool shouldRepaint(covariant BasicNoisePainter oldDelegate) => time != oldDelegate.time;
}

/// 分形噪声演示
class FractalNoiseDemo extends StatefulWidget {
  const FractalNoiseDemo({super.key});

  @override
  State<FractalNoiseDemo> createState() => _FractalNoiseDemoState();
}

class _FractalNoiseDemoState extends State<FractalNoiseDemo> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  final FBM _fbm = FBM(octaves: 6, persistence: 0.5);
  
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 15),
    )..repeat();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  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});
  
  @override
  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,
        );
      }
    }
  }
  
  @override
  bool shouldRepaint(covariant FractalNoisePainter oldDelegate) => time != oldDelegate.time;
}

/// 流动噪声演示
class FlowNoiseDemo extends StatefulWidget {
  const FlowNoiseDemo({super.key});

  @override
  State<FlowNoiseDemo> createState() => _FlowNoiseDemoState();
}

class _FlowNoiseDemoState extends State<FlowNoiseDemo> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  final FBM _fbm = FBM(octaves: 4);
  
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 20),
    )..repeat();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  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});
  
  @override
  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);
    }
  }
  
  @override
  bool shouldRepaint(covariant FlowNoisePainter oldDelegate) => time != oldDelegate.time;
}

/// 音乐噪声演示
class MusicNoiseDemo extends StatefulWidget {
  const MusicNoiseDemo({super.key});

  @override
  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';

  @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((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(() {});
  }

  @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: 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,
  });
  
  @override
  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);
      }
    }
  }
  
  @override
  bool shouldRepaint(covariant MusicNoisePainter oldDelegate) => true;
}

/// 综合演示
class FullNoiseDemo extends StatefulWidget {
  const FullNoiseDemo({super.key});

  @override
  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';

  @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((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(() {});
  }

  @override
  void dispose() {
    _animController.dispose();
    _audioPlayer.dispose();
    super.dispose();
  }

  @override
  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,
  });
  
  @override
  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);
    }
  }
  
  @override
  bool shouldRepaint(covariant FullNoisePainter oldDelegate) => true;
}

📝 五、总结

本篇文章深入探讨了柏林噪声在音乐可视化中的应用,从数学原理到代码实现,涵盖了以下核心内容:

✅ 核心知识点回顾

知识点 说明
📐 柏林噪声原理 梯度噪声、平滑插值、排列表
🌊 分形布朗运动 多层叠加、八度、持续度、间隙度
🎵 音频驱动参数 能量映射噪声缩放和速度
🎨 流动场可视化 噪声驱动的流线绘制
🔊 网络音乐播放 just_audio_ohos 实现在线 MP3

⭐ 最佳实践要点

  • ✅ 使用 FBM 叠加多层噪声产生丰富细节
  • ✅ 将音频特征映射到噪声参数实现动态效果
  • ✅ 结合流线和粒子增强视觉层次
  • ✅ 低音影响缩放,高音影响颜色

🚀 进阶方向

  • 🔮 使用 Simplex Noise 提升计算效率
  • ✨ 实现 3D 噪声场体积渲染
  • 👆 添加触摸交互控制噪声参数
  • ⚡ 使用 Isolate 进行并行噪声计算

相关推荐
lbb 小魔仙3 小时前
【HarmonyOS】RN_of_HarmonyOS实战项目_TextInput表情符号输入
华为·harmonyos
lbb 小魔仙4 小时前
鸿蒙跨平台实战:React Native在OpenHarmony上的AccessibilityInfo无障碍检测
react native·华为·harmonyos
恋猫de小郭7 小时前
Flutter 2026 Roadmap 发布,未来计划是什么?
android·前端·flutter
空白诗8 小时前
Harmony Flutter 跨平台开发实战:鸿蒙与音乐律动艺术、分形几何与自相似性:无限细节的数学之美
flutter
星空22238 小时前
【HarmonyOS】day43:RN_of_HarmonyOS实战项目_电话号码输入
华为·harmonyos
松叶似针8 小时前
Flutter三方库适配OpenHarmony【doc_text】— OpenHarmony 插件工程搭建与配置文件详解
flutter
阿林来了9 小时前
Flutter三方库适配OpenHarmony【flutter_web_auth】— OAuth2 协议基础与认证流程拆解
flutter
早點睡39010 小时前
Harmony Flutter 跨平台开发实战:鸿蒙与音乐律动艺术、贝塞尔流体律动:三阶贝塞尔曲线的“呼吸“感
flutter
2501_9219308310 小时前
进阶实战 Flutter for OpenHarmony:响应式状态机系统 - 复杂状态流转实现
flutter