Harmony Flutter 跨平台开发实战:鸿蒙与音乐律动艺术、极坐标对称投影:万花筒般的几何韵律

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


🔮 一、极坐标对称投影:数学之美

📚 1.1 极坐标系统

极坐标是一种二维坐标系统,使用半径 r角度 θ 来表示平面上的点,与笛卡尔坐标(x, y)形成互补。

坐标转换公式

复制代码
极坐标 → 笛卡尔坐标:
x = r × cos(θ)
y = r × sin(θ)

笛卡尔坐标 → 极坐标:
r = √(x² + y²)
θ = atan2(y, x)

示意图

复制代码
        y轴
         ↑
         │    • P(r, θ)
         │   ╱
         │  ╱ r
         │ ╱
         │╱ θ
    ─────┼─────────→ x轴
         │O (原点)

P 点的极坐标:(r, θ)
P 点的直角坐标:(r·cos(θ), r·sin(θ))

📐 1.2 对称性原理

对称是自然界中最基本的美学原则之一。在极坐标系统中,对称性可以通过角度变换来实现:

对称类型 变换规则 特点
🔄 旋转对称 θ → θ + 2π/n n 重旋转对称
↕️ 反射对称 θ → -θ 镜像对称
🔀 滑动对称 θ → θ + π/n, r → r 平移+反射
🌀 螺旋对称 θ → θ + α, r → r + d 旋转+缩放

n 重旋转对称示意

复制代码
n = 3 (三重对称)      n = 6 (六重对称)
      ╱╲                  ╱╲
     ╱  ╲                ╱  ╲
    ╱    ╲              ╱╲  ╱╲
   ────────            ────────
    ╲    ╱              ╲╱  ╲╱
     ╲  ╱
      ╲╱

🔬 1.3 万花筒原理

万花筒利用镜面反射原理,将简单的图案通过多次反射产生复杂的对称图案。

万花筒数学模型

复制代码
对于 n 面镜片的万花筒:

1. 将角度 θ 映射到基本扇形区域:
   θ' = θ mod (2π/n)

2. 根据扇形内的位置决定是否反射:
   如果 θ' > π/n,则 θ' = 2π/n - θ'

3. 应用图案变换:
   图案在基本扇形内绘制,然后复制到所有对称位置

万花筒效果示意

复制代码
原始图案:          6折万花筒:
    ◆                  ◆ ◆ ◆
                       ◆ ◆ ◆
                      ◆ ◆ ◆ ◆ ◆
                       ◆ ◆ ◆
                        ◆ ◆

🎯 1.4 极坐标在艺术中的应用

应用领域 效果 示例
🌸 曼陀罗 圆形对称图案 宗教艺术
❄️ 雪花 六重对称 自然界
🎨 装饰艺术 伊斯兰几何 建筑装饰
🎵 音乐可视化 节奏对称 音频响应
🌊 波纹效果 同心圆扩散 水波模拟

🔧 二、极坐标对称的 Dart 实现

🧮 2.1 极坐标工具类

dart 复制代码
import 'dart:math';
import 'dart:typed_data';

/// 极坐标点
class PolarPoint {
  final double r;
  final double theta;
  
  const PolarPoint(this.r, this.theta);
  
  /// 从笛卡尔坐标创建
  factory PolarPoint.fromCartesian(double x, double y) {
    return PolarPoint(
      sqrt(x * x + y * y),
      atan2(y, x),
    );
  }
  
  /// 转换为笛卡尔坐标
  Point<double> toCartesian() {
    return Point(r * cos(theta), r * sin(theta));
  }
  
  /// 旋转
  PolarPoint rotate(double angle) {
    return PolarPoint(r, theta + angle);
  }
  
  /// 缩放
  PolarPoint scale(double factor) {
    return PolarPoint(r * factor, theta);
  }
}

/// 极坐标变换器
class PolarTransform {
  final double centerX;
  final double centerY;
  final double scale;
  
  const PolarTransform({
    required this.centerX,
    required this.centerY,
    this.scale = 1.0,
  });
  
  /// 笛卡尔坐标转极坐标
  PolarPoint toPolar(double x, double y) {
    final dx = (x - centerX) / scale;
    final dy = (y - centerY) / scale;
    return PolarPoint.fromCartesian(dx, dy);
  }
  
  /// 极坐标转笛卡尔坐标
  Point<double> toCartesian(PolarPoint polar) {
    final cartesian = polar.toCartesian();
    return Point(
      cartesian.x * scale + centerX,
      cartesian.y * scale + centerY,
    );
  }
  
  /// 规范化角度到 [0, 2π)
  static double normalizeAngle(double theta) {
    while (theta < 0) theta += 2 * pi;
    while (theta >= 2 * pi) theta -= 2 * pi;
    return theta;
  }
  
  /// 将角度映射到 n 重对称的基本扇形
  static double foldAngle(double theta, int folds) {
    final sectorAngle = 2 * pi / folds;
    var normalized = normalizeAngle(theta);
    
    // 映射到基本扇形
    final sectorIndex = (normalized / sectorAngle).floor();
    normalized = normalized - sectorIndex * sectorAngle;
    
    // 在扇形内反射
    if (sectorIndex % 2 == 1) {
      normalized = sectorAngle - normalized;
    }
    
    return normalized;
  }
}

⚡ 2.2 对称图案生成器

dart 复制代码
/// 对称图案生成器
class SymmetryGenerator {
  final int foldCount;
  final double radius;
  final PolarTransform transform;
  
  SymmetryGenerator({
    required this.foldCount,
    required this.radius,
    required this.transform,
  });
  
  /// 生成对称点集
  List<Point<double>> generateSymmetricPoints(PolarPoint base) {
    final points = <Point<double>>[];
    final sectorAngle = 2 * pi / foldCount;
    
    for (int i = 0; i < foldCount; i++) {
      // 原始点
      final rotated = base.rotate(i * sectorAngle);
      points.add(transform.toCartesian(rotated));
      
      // 镜像点
      final mirrored = PolarPoint(base.r, -base.theta);
      final mirroredRotated = mirrored.rotate(i * sectorAngle);
      points.add(transform.toCartesian(mirroredRotated));
    }
    
    return points;
  }
  
  /// 生成对称路径
  Path generateSymmetricPath(List<PolarPoint> basePoints) {
    final path = Path();
    final sectorAngle = 2 * pi / foldCount;
    
    for (int i = 0; i < foldCount; i++) {
      // 原始路径
      for (int j = 0; j < basePoints.length; j++) {
        final rotated = basePoints[j].rotate(i * sectorAngle);
        final cartesian = transform.toCartesian(rotated);
        
        if (j == 0 && i == 0) {
          path.moveTo(cartesian.x, cartesian.y);
        } else {
          path.lineTo(cartesian.x, cartesian.y);
        }
      }
      
      // 镜像路径
      for (int j = basePoints.length - 1; j >= 0; j--) {
        final mirrored = PolarPoint(basePoints[j].r, -basePoints[j].theta);
        final rotated = mirrored.rotate(i * sectorAngle);
        final cartesian = transform.toCartesian(rotated);
        path.lineTo(cartesian.x, cartesian.y);
      }
    }
    
    path.close();
    return path;
  }
  
  /// 生成放射状图案
  List<Point<double>> generateRadialPattern({
    required int rings,
    required int pointsPerRing,
    required double innerRadius,
    required double outerRadius,
  }) {
    final points = <Point<double>>[];
    
    for (int ring = 0; ring < rings; ring++) {
      final r = innerRadius + (outerRadius - innerRadius) * ring / (rings - 1);
      
      for (int i = 0; i < pointsPerRing; i++) {
        final theta = 2 * pi * i / pointsPerRing;
        final polar = PolarPoint(r, theta);
        points.add(transform.toCartesian(polar));
      }
    }
    
    return points;
  }
}

/// 万花筒效果生成器
class KaleidoscopeGenerator {
  final int segments;
  final double radius;
  final Offset center;
  
  KaleidoscopeGenerator({
    required this.segments,
    required this.radius,
    required this.center,
  });
  
  /// 应用万花筒变换
  List<Offset> transform(Offset point) {
    final results = <Offset>[];
    final dx = point.dx - center.dx;
    final dy = point.dy - center.dy;
    final r = sqrt(dx * dx + dy * dy);
    var theta = atan2(dy, dx);
    
    final sectorAngle = 2 * pi / segments;
    
    for (int i = 0; i < segments; i++) {
      // 原始
      final newTheta1 = theta + i * sectorAngle;
      results.add(Offset(
        center.dx + r * cos(newTheta1),
        center.dy + r * sin(newTheta1),
      ));
      
      // 镜像
      final newTheta2 = -theta + i * sectorAngle;
      results.add(Offset(
        center.dx + r * cos(newTheta2),
        center.dy + r * sin(newTheta2),
      ));
    }
    
    return results;
  }
  
  /// 生成万花筒路径
  Path generatePath(List<Offset> basePoints, {bool closed = true}) {
    final path = Path();
    
    for (final point in basePoints) {
      final transformed = transform(point);
      for (int i = 0; i < transformed.length; i++) {
        if (i == 0) {
          path.moveTo(transformed[i].dx, transformed[i].dy);
        } else {
          path.lineTo(transformed[i].dx, transformed[i].dy);
        }
      }
    }
    
    if (closed) path.close();
    return path;
  }
}

🎨 2.3 音频驱动的极坐标可视化

dart 复制代码
import 'package:flutter/material.dart';
import 'package:just_audio_ohos/just_audio_ohos.dart';
import 'package:audio_session/audio_session.dart';

/// 音频驱动的极坐标可视化控制器
class AudioPolarController extends ChangeNotifier {
  final AudioPlayer _player = AudioPlayer();
  
  bool _isPlaying = false;
  Duration _position = Duration.zero;
  Duration _duration = Duration.zero;
  
  Float32List _audioData = Float32List(128);
  double _energy = 0;
  double _bass = 0;
  double _mid = 0;
  double _treble = 0;
  
  double _time = 0;
  int _foldCount = 6;
  double _rotation = 0;
  double _pulsePhase = 0;
  
  bool get isPlaying => _isPlaying;
  Duration get position => _position;
  Duration get duration => _duration;
  Float32List get audioData => _audioData;
  double get energy => _energy;
  double get bass => _bass;
  double get mid => _mid;
  double get treble => _treble;
  int get foldCount => _foldCount;
  double get rotation => _rotation;
  double get pulsePhase => _pulsePhase;
  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 update(double dt) {
    _time += dt;
    _pulsePhase += dt * 2;
    
    // 更新音频数据
    _updateAudioData();
    
    // 计算音频特征
    _calculateAudioFeatures();
    
    // 更新旋转
    _rotation += dt * (0.5 + _energy * 2);
    
    // 更新折叠数
    _updateFoldCount();
    
    notifyListeners();
  }
  
  void _updateAudioData() {
    final random = Random();
    
    for (int i = 0; i < 128; i++) {
      if (_isPlaying) {
        final freq = (i / 128) * 8 + 1;
        final wave1 = sin(_time * freq) * 0.4;
        final wave2 = sin(_time * freq * 1.5 + pi / 3) * 0.3;
        final noise = (random.nextDouble() - 0.5) * 0.15;
        final bassBoost = i < 32 ? 0.3 : 0;
        
        _audioData[i] = _audioData[i] * 0.85 + 
            (wave1 + wave2 + noise + bassBoost) * 0.15;
      } else {
        _audioData[i] *= 0.95;
      }
    }
  }
  
  void _calculateAudioFeatures() {
    double totalEnergy = 0;
    double bassEnergy = 0;
    double midEnergy = 0;
    double trebleEnergy = 0;
    
    for (int i = 0; i < 128; i++) {
      final value = _audioData[i].abs();
      totalEnergy += value;
      
      if (i < 32) {
        bassEnergy += value;
      } else if (i < 96) {
        midEnergy += value;
      } else {
        trebleEnergy += value;
      }
    }
    
    _energy = totalEnergy / 128;
    _bass = bassEnergy / 32;
    _mid = midEnergy / 64;
    _treble = trebleEnergy / 32;
  }
  
  void _updateFoldCount() {
    // 根据中频能量调整对称数
    final targetFolds = 4 + (_mid * 8).toInt();
    if (targetFolds != _foldCount && Random().nextDouble() < 0.02) {
      _foldCount = targetFolds.clamp(3, 12);
    }
  }
  
  /// 设置折叠数
  void setFoldCount(int count) {
    _foldCount = count.clamp(3, 16);
    notifyListeners();
  }
  
  /// 播放/暂停音频
  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();
  }
}

📦 三、完整示例代码

以下是完整的极坐标对称投影音乐可视化示例代码:

dart 复制代码
import 'package:flutter/material.dart';
import 'package:just_audio_ohos/just_audio_ohos.dart';
import 'package:audio_session/audio_session.dart';
import 'dart:math';
import 'dart:typed_data';

void main() {
  runApp(const PolarSymmetryApp());
}

class PolarSymmetryApp extends StatelessWidget {
  const PolarSymmetryApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '极坐标对称投影',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.dark),
        useMaterial3: true,
      ),
      home: const PolarHomePage(),
      debugShowCheckedModeBanner: false,
    );
  }
}

class PolarHomePage extends StatelessWidget {
  const PolarHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('🔮 极坐标对称'), backgroundColor: Theme.of(context).colorScheme.inversePrimary),
      body: ListView(padding: const EdgeInsets.all(16), children: [
        _buildCard(context, title: '基础极坐标', description: '极坐标可视化', icon: Icons.circle_outlined, color: Colors.purple,
            onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const BasicPolarDemo()))),
        _buildCard(context, title: '万花筒效果', description: '多折叠对称', icon: Icons.filter_vintage, color: Colors.pink,
            onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const KaleidoscopeDemo()))),
        _buildCard(context, title: '曼陀罗图案', description: '神圣几何', icon: Icons.grain, color: Colors.indigo,
            onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const MandalaDemo()))),
        _buildCard(context, title: '音乐极坐标', description: '音频驱动对称', icon: Icons.music_note, color: Colors.orange,
            onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const MusicPolarDemo()))),
        _buildCard(context, title: '螺旋波纹', description: '阿基米德螺旋', icon: Icons.all_inclusive, color: Colors.teal,
            onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const SpiralDemo()))),
      ]),
    );
  }

  Widget _buildCard(BuildContext context, {required String title, required String description, required IconData icon, 
      required Color color, required VoidCallback onTap}) {
    return Card(
      margin: const EdgeInsets.only(bottom: 12),
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
      child: InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(16),
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Row(children: [
            Container(width: 56, height: 56, decoration: BoxDecoration(color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(12)),
                child: Icon(icon, color: color, size: 28)),
            const SizedBox(width: 16),
            Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
              Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
              const SizedBox(height: 4),
              Text(description, style: TextStyle(color: Colors.grey[600], fontSize: 14)),
            ])),
            Icon(Icons.chevron_right, color: Colors.grey[400]),
          ]),
        ),
      ),
    );
  }
}

/// 极坐标点
class PolarPoint {
  final double r, theta;
  const PolarPoint(this.r, this.theta);
  
  factory PolarPoint.fromCartesian(double x, double y) =>
      PolarPoint(sqrt(x * x + y * y), atan2(y, x));
  
  Point<double> toCartesian() => Point(r * cos(theta), r * sin(theta));
  PolarPoint rotate(double angle) => PolarPoint(r, theta + angle);
  PolarPoint scale(double factor) => PolarPoint(r * factor, theta);
}

/// 基础极坐标演示
class BasicPolarDemo extends StatefulWidget {
  const BasicPolarDemo({super.key});
  @override
  State<BasicPolarDemo> createState() => _BasicPolarDemoState();
}

class _BasicPolarDemoState extends State<BasicPolarDemo> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  double _time = 0;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 16))..repeat();
    _controller.addListener(() { _time += 0.016; setState(() {}); });
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('基础极坐标')),
      body: CustomPaint(painter: BasicPolarPainter(_time), size: Size.infinite),
    );
  }
}

class BasicPolarPainter extends CustomPainter {
  final double time;
  BasicPolarPainter(this.time);
  
  @override
  void paint(Canvas canvas, Size size) {
    final center = Offset(size.width / 2, size.height / 2);
    final maxR = min(size.width, size.height) / 2 - 20;
    
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = const Color(0xFF0a0a15));
    
    // 绘制极坐标网格
    for (int r = 20; r <= maxR; r += 40) {
      canvas.drawCircle(center, r.toDouble(), Paint()..color = Colors.white10..style = PaintingStyle.stroke);
    }
    
    for (int i = 0; i < 12; i++) {
      final angle = i * pi / 6;
      canvas.drawLine(center, Offset(center.dx + maxR * cos(angle), center.dy + maxR * sin(angle)),
          Paint()..color = Colors.white10);
    }
    
    // 绘制极坐标曲线
    final path = Path();
    for (double theta = 0; theta <= 4 * pi; theta += 0.02) {
      final r = maxR * 0.6 * (1 + 0.3 * sin(3 * theta + time * 2));
      final x = center.dx + r * cos(theta);
      final y = center.dy + r * sin(theta);
      
      if (theta == 0) path.moveTo(x, y);
      else path.lineTo(x, y);
    }
    
    canvas.drawPath(path, Paint()..color = Colors.purple..style = PaintingStyle.stroke..strokeWidth = 2);
    
    // 绘制动态点
    for (int i = 0; i < 8; i++) {
      final theta = time + i * pi / 4;
      final r = maxR * 0.8 * (0.5 + 0.5 * sin(time * 2 + i));
      final x = center.dx + r * cos(theta);
      final y = center.dy + r * sin(theta);
      
      canvas.drawCircle(Offset(x, y), 6, Paint()..color = HSVColor.fromAHSV(1, ((i * 45 + time * 50) % 360).abs(), 0.8, 1).toColor());
    }
  }
  
  @override
  bool shouldRepaint(covariant BasicPolarPainter old) => true;
}

/// 万花筒效果演示
class KaleidoscopeDemo extends StatefulWidget {
  const KaleidoscopeDemo({super.key});
  @override
  State<KaleidoscopeDemo> createState() => _KaleidoscopeDemoState();
}

class _KaleidoscopeDemoState extends State<KaleidoscopeDemo> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  double _time = 0;
  int _segments = 8;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 16))..repeat();
    _controller.addListener(() { _time += 0.016; setState(() {}); });
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('万花筒效果')),
      body: Column(children: [
        Expanded(child: CustomPaint(painter: KaleidoscopePainter(_time, _segments), size: Size.infinite)),
        _buildControls(),
      ]),
    );
  }
  
  Widget _buildControls() {
    return Container(
      padding: const EdgeInsets.all(16),
      color: Colors.black12,
      child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
        const Text('对称数: ', style: TextStyle(color: Colors.white)),
        Slider(value: _segments.toDouble(), min: 3, max: 16, divisions: 13,
            onChanged: (v) => setState(() => _segments = v.toInt())),
        Text('$_segments', style: const TextStyle(color: Colors.white)),
      ]),
    );
  }
}

class KaleidoscopePainter extends CustomPainter {
  final double time;
  final int segments;
  
  KaleidoscopePainter(this.time, this.segments);
  
  @override
  void paint(Canvas canvas, Size size) {
    final center = Offset(size.width / 2, size.height / 2);
    final maxR = min(size.width, size.height) / 2 - 10;
    
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = const Color(0xFF0a0a15));
    
    final sectorAngle = 2 * pi / segments;
    
    for (int seg = 0; seg < segments; seg++) {
      canvas.save();
      canvas.translate(center.dx, center.dy);
      canvas.rotate(seg * sectorAngle);
      
      // 绘制扇形内的图案
      _drawSectorPattern(canvas, maxR, sectorAngle);
      
      // 绘制镜像
      canvas.scale(1, -1);
      _drawSectorPattern(canvas, maxR, sectorAngle);
      
      canvas.restore();
    }
  }
  
  void _drawSectorPattern(Canvas canvas, double maxR, double sectorAngle) {
    // 绘制多层花瓣
    for (int layer = 0; layer < 5; layer++) {
      final r = maxR * (0.3 + layer * 0.15);
      final hue = ((layer * 40 + time * 30) % 360).abs();
      
      final path = Path();
      for (double t = 0; t <= sectorAngle; t += 0.02) {
        final wave = r * (0.8 + 0.2 * sin(time * 3 + layer + t * 5));
        final x = wave * cos(t);
        final y = wave * sin(t);
        
        if (t == 0) path.moveTo(x, y);
        else path.lineTo(x, y);
      }
      path.lineTo(0, 0);
      path.close();
      
      canvas.drawPath(path, Paint()..color = HSVColor.fromAHSV(0.6, hue.toDouble(), 0.7, 1).toColor());
    }
    
    // 绘制装饰线条
    for (int i = 0; i < 3; i++) {
      final r1 = maxR * (0.2 + i * 0.25);
      final r2 = maxR * (0.35 + i * 0.25);
      final angle = sectorAngle * (0.3 + i * 0.2);
      
      canvas.drawLine(Offset(r1 * cos(angle * 0.5), r1 * sin(angle * 0.5)),
          Offset(r2 * cos(angle), r2 * sin(angle)),
          Paint()..color = Colors.white.withOpacity(0.5)..strokeWidth = 1);
    }
  }
  
  @override
  bool shouldRepaint(covariant KaleidoscopePainter old) => true;
}

/// 曼陀罗图案演示
class MandalaDemo extends StatefulWidget {
  const MandalaDemo({super.key});
  @override
  State<MandalaDemo> createState() => _MandalaDemoState();
}

class _MandalaDemoState extends State<MandalaDemo> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  double _time = 0;
  int _petals = 12;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 16))..repeat();
    _controller.addListener(() { _time += 0.016; setState(() {}); });
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('曼陀罗图案')),
      body: Column(children: [
        Expanded(child: CustomPaint(painter: MandalaPainter(_time, _petals), size: Size.infinite)),
        _buildControls(),
      ]),
    );
  }
  
  Widget _buildControls() {
    return Container(
      padding: const EdgeInsets.all(16),
      color: Colors.black12,
      child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
        const Text('花瓣数: ', style: TextStyle(color: Colors.white)),
        Slider(value: _petals.toDouble(), min: 4, max: 24, divisions: 20,
            onChanged: (v) => setState(() => _petals = v.toInt())),
        Text('$_petals', style: TextStyle(color: Colors.white)),
      ]),
    );
  }
}

class MandalaPainter extends CustomPainter {
  final double time;
  final int petals;
  
  MandalaPainter(this.time, this.petals);
  
  @override
  void paint(Canvas canvas, Size size) {
    final center = Offset(size.width / 2, size.height / 2);
    final maxR = min(size.width, size.height) / 2 - 20;
    
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = const Color(0xFF0a0a15));
    
    // 绘制多层曼陀罗
    for (int layer = 5; layer >= 0; layer--) {
      final r = maxR * (0.2 + layer * 0.16);
      final hue = (layer * 30 + time * 20) % 360;
      
      _drawPetalRing(canvas, center, r, petals + layer * 2, hue.toDouble(), layer);
    }
    
    // 绘制中心
    canvas.drawCircle(center, maxR * 0.1, Paint()..color = Colors.white.withOpacity(0.8));
    canvas.drawCircle(center, maxR * 0.05, Paint()..color = Colors.purple);
  }
  
  void _drawPetalRing(Canvas canvas, Offset center, double r, int count, double hue, int layer) {
    final angleStep = 2 * pi / count;
    final petalLength = r * 0.3;
    final petalWidth = r * 0.15;
    
    for (int i = 0; i < count; i++) {
      final angle = i * angleStep + time * (layer % 2 == 0 ? 0.2 : -0.2);
      
      final path = Path();
      path.moveTo(center.dx, center.dy);
      
      // 绘制花瓣形状
      final tipX = center.dx + r * cos(angle);
      final tipY = center.dy + r * sin(angle);
      
      final ctrl1X = center.dx + (r - petalLength * 0.5) * cos(angle) + petalWidth * cos(angle + pi / 2);
      final ctrl1Y = center.dy + (r - petalLength * 0.5) * sin(angle) + petalWidth * sin(angle + pi / 2);
      
      final ctrl2X = center.dx + (r - petalLength * 0.5) * cos(angle) - petalWidth * cos(angle + pi / 2);
      final ctrl2Y = center.dy + (r - petalLength * 0.5) * sin(angle) - petalWidth * sin(angle + pi / 2);
      
      path.quadraticBezierTo(ctrl1X, ctrl1Y, tipX, tipY);
      path.quadraticBezierTo(ctrl2X, ctrl2Y, center.dx, center.dy);
      
      final paint = Paint()..color = HSVColor.fromAHSV(0.7, hue, 0.6, 1).toColor();
      canvas.drawPath(path, paint);
      
      // 绘制轮廓
      canvas.drawPath(path, Paint()..color = Colors.white.withOpacity(0.3)..style = PaintingStyle.stroke..strokeWidth = 0.5);
    }
  }
  
  @override
  bool shouldRepaint(covariant MandalaPainter old) => true;
}

/// 音乐极坐标演示
class MusicPolarDemo extends StatefulWidget {
  const MusicPolarDemo({super.key});
  @override
  State<MusicPolarDemo> createState() => _MusicPolarDemoState();
}

class _MusicPolarDemoState extends State<MusicPolarDemo> with TickerProviderStateMixin {
  late AnimationController _animController;
  late AudioPlayer _audioPlayer;
  Float32List _audioData = Float32List(128);
  bool _isPlaying = false;
  Duration _position = Duration.zero;
  Duration _duration = Duration.zero;
  double _energy = 0, _bass = 0, _mid = 0;
  double _time = 0, _rotation = 0;
  int _folds = 6;
  
  static const String _audioUrl = 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3';

  @override
  void initState() {
    super.initState();
    _initAudio();
    _animController = AnimationController(vsync: this, duration: const Duration(milliseconds: 16))..repeat();
    _animController.addListener(_update);
  }
  
  Future<void> _initAudio() async {
    _audioPlayer = AudioPlayer();
    final session = await AudioSession.instance;
    await session.configure(const AudioSessionConfiguration.music());
    
    _audioPlayer.playerStateStream.listen((s) => setState(() => _isPlaying = s.playing));
    _audioPlayer.positionStream.listen((p) => setState(() => _position = p));
    _audioPlayer.durationStream.listen((d) => setState(() => _duration = d ?? Duration.zero));
    
    try { await _audioPlayer.setUrl(_audioUrl); } catch (e) { debugPrint('加载失败: $e'); }
  }
  
  void _update() {
    _time += 0.016;
    
    for (int i = 0; i < 128; i++) {
      if (_isPlaying) {
        final freq = (i / 128) * 8 + 1;
        final wave = sin(_time * freq) * 0.4 + sin(_time * freq * 1.5) * 0.3;
        final bass = i < 32 ? 0.3 : 0;
        _audioData[i] = _audioData[i] * 0.85 + (wave + bass) * 0.15;
      } else {
        _audioData[i] *= 0.95;
      }
    }
    
    double total = 0, bassE = 0, midE = 0;
    for (int i = 0; i < 128; i++) {
      total += _audioData[i].abs();
      if (i < 32) bassE += _audioData[i].abs();
      else if (i < 96) midE += _audioData[i].abs();
    }
    _energy = total / 128;
    _bass = bassE / 32;
    _mid = midE / 64;
    
    _rotation += 0.016 * (0.5 + _energy * 2);
    
    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: MusicPolarPainter(_time, _rotation, _folds, _audioData, _energy, _bass), size: Size.infinite),
        Positioned(bottom: 30, left: 20, right: 20, child: _buildControls()),
      ]),
    );
  }
  
  Widget _buildControls() {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(color: Colors.black.withOpacity(0.7), borderRadius: BorderRadius.circular(16)),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          const Text('🎵 SoundHelix - Song 1', style: TextStyle(color: Colors.white, fontSize: 14)),
          const SizedBox(height: 12),
          Slider(value: _duration.inMilliseconds > 0 ? _position.inMilliseconds.toDouble().clamp(0, _duration.inMilliseconds.toDouble()) : 0,
              max: _duration.inMilliseconds > 0 ? _duration.inMilliseconds.toDouble() : 1,
              onChanged: (v) => _audioPlayer.seek(Duration(milliseconds: v.toInt()))),
          Row(mainAxisAlignment: MainAxisAlignment.center, children: [
            const Text('对称: ', style: TextStyle(color: Colors.white70)),
            Slider(value: _folds.toDouble(), min: 3, max: 12, divisions: 9,
                onChanged: (v) => setState(() => _folds = v.toInt())),
            Text('$_folds', style: const TextStyle(color: Colors.white)),
            const SizedBox(width: 20),
            IconButton(icon: Icon(_isPlaying ? Icons.pause : Icons.play_arrow, color: Colors.orange, size: 36),
                onPressed: () => _isPlaying ? _audioPlayer.pause() : _audioPlayer.play()),
          ]),
        ],
      ),
    );
  }
}

class MusicPolarPainter extends CustomPainter {
  final double time, rotation;
  final int folds;
  final Float32List audioData;
  final double energy, bass;
  
  MusicPolarPainter(this.time, this.rotation, this.folds, this.audioData, this.energy, this.bass);
  
  @override
  void paint(Canvas canvas, Size size) {
    final center = Offset(size.width / 2, size.height / 2);
    final maxR = min(size.width, size.height) / 2 - 30;
    
    final bgColor = Color.lerp(const Color(0xFF0a0a15), const Color(0xFF150a20), energy)!;
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = bgColor);
    
    canvas.save();
    canvas.translate(center.dx, center.dy);
    canvas.rotate(rotation);
    
    final sectorAngle = 2 * pi / folds;
    
    // 绘制音频波形环
    for (int ring = 0; ring < 3; ring++) {
      final baseR = maxR * (0.3 + ring * 0.25);
      final hue = ((ring * 60 + time * 30) % 360).abs();
      
      for (int seg = 0; seg < folds; seg++) {
        canvas.save();
        canvas.rotate(seg * sectorAngle);
        
        final path = Path();
        for (int i = 0; i <= 32; i++) {
          final t = i * sectorAngle / 32;
          final audioIndex = (seg * 32 + i) % audioData.length;
          final audioVal = audioData[audioIndex].abs();
          final r = baseR * (1 + audioVal * 0.5 + bass * 0.3);
          
          final x = r * cos(t);
          final y = r * sin(t);
          
          if (i == 0) path.moveTo(x, y);
          else path.lineTo(x, y);
        }
        
        path.lineTo(0, 0);
        path.close();
        
        final paint = Paint()..color = HSVColor.fromAHSV(0.6 - ring * 0.15, hue, 0.7, 1).toColor();
        if (energy > 0.3) paint.maskFilter = MaskFilter.blur(BlurStyle.normal, 2);
        
        canvas.drawPath(path, paint);
        
        canvas.restore();
      }
    }
    
    // 绘制中心脉冲
    final pulseR = maxR * 0.15 * (1 + bass * 0.5);
    canvas.drawCircle(Offset.zero, pulseR, Paint()..color = Colors.white.withOpacity(0.8));
    canvas.drawCircle(Offset.zero, pulseR * 0.6, Paint()..color = Colors.purple.withOpacity(0.9));
    
    canvas.restore();
  }
  
  @override
  bool shouldRepaint(covariant MusicPolarPainter old) => true;
}

/// 螺旋波纹演示
class SpiralDemo extends StatefulWidget {
  const SpiralDemo({super.key});
  @override
  State<SpiralDemo> createState() => _SpiralDemoState();
}

class _SpiralDemoState extends State<SpiralDemo> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  double _time = 0;
  int _arms = 5;
  double _tightness = 0.3;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 16))..repeat();
    _controller.addListener(() { _time += 0.016; setState(() {}); });
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('螺旋波纹')),
      body: Column(children: [
        Expanded(child: CustomPaint(painter: SpiralPainter(_time, _arms, _tightness), size: Size.infinite)),
        _buildControls(),
      ]),
    );
  }
  
  Widget _buildControls() {
    return Container(
      padding: const EdgeInsets.all(16),
      color: Colors.black12,
      child: Column(children: [
        Row(children: [
          const Text('臂数: ', style: TextStyle(color: Colors.white)),
          Slider(value: _arms.toDouble(), min: 1, max: 12, divisions: 11,
              onChanged: (v) => setState(() => _arms = v.toInt())),
          Text('$_arms', style: const TextStyle(color: Colors.white)),
        ]),
        Row(children: [
          const Text('紧密度: ', style: TextStyle(color: Colors.white)),
          Slider(value: _tightness, min: 0.1, max: 1,
              onChanged: (v) => setState(() => _tightness = v)),
          Text(_tightness.toStringAsFixed(2), style: const TextStyle(color: Colors.white)),
        ]),
      ]),
    );
  }
}

class SpiralPainter extends CustomPainter {
  final double time;
  final int arms;
  final double tightness;
  
  SpiralPainter(this.time, this.arms, this.tightness);
  
  @override
  void paint(Canvas canvas, Size size) {
    final center = Offset(size.width / 2, size.height / 2);
    final maxR = min(size.width, size.height) / 2 - 20;
    
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = const Color(0xFF0a0a15));
    
    for (int arm = 0; arm < arms; arm++) {
      final armAngle = arm * 2 * pi / arms;
      final hue = (arm * 360 / arms + time * 20) % 360;
      
      final path = Path();
      for (double t = 0; t <= 8 * pi; t += 0.05) {
        final r = maxR * tightness * t / (8 * pi);
        final angle = t + armAngle + time * 0.5;
        
        final x = center.dx + r * cos(angle);
        final y = center.dy + r * sin(angle);
        
        if (t == 0) path.moveTo(x, y);
        else path.lineTo(x, y);
      }
      
      final paint = Paint()
        ..color = HSVColor.fromAHSV(0.8, hue, 0.7, 1).toColor()
        ..style = PaintingStyle.stroke
        ..strokeWidth = 2
        ..strokeCap = StrokeCap.round;
      
      canvas.drawPath(path, paint);
    }
    
    // 绘制波纹
    for (int i = 0; i < 5; i++) {
      final phase = (time * 2 + i * 0.5) % 3;
      final r = maxR * phase / 3;
      final alpha = (1 - phase / 3) * 0.5;
      
      canvas.drawCircle(center, r, Paint()..color = Colors.white.withOpacity(alpha)..style = PaintingStyle.stroke..strokeWidth = 1);
    }
  }
  
  @override
  bool shouldRepaint(covariant SpiralPainter old) => true;
}

📝 四、数学原理深入解析

📐 4.1 极坐标曲线方程

极坐标系统中有许多经典的数学曲线,它们在音乐可视化中可以产生独特的视觉效果:

玫瑰曲线(Rose Curves)

复制代码
r = a × cos(n × θ)

当 n 为奇数:n 个花瓣
当 n 为偶数:2n 个花瓣

示例:
n = 3: 三叶玫瑰        n = 4: 八叶玫瑰
     ╱╲                   ╲╱╲╱
    ╱  ╲                  ╱  ╲
   ╱    ╲                ╱    ╲
  ────────              ────────
   ╲    ╱                ╲    ╱
    ╲  ╱                  ╲  ╱
     ╲╱                    ╲╱

阿基米德螺旋

复制代码
r = a + b × θ

特点:相邻两圈之间的距离相等
应用:唱片纹路、弹簧

     ╱╱╱╱╱
    ╱╱╱╱╱╱
   ╱╱╱╱╱╱╱
  ╱╱╱╱╱╱╱╱
 ╱╱╱╱╱╱╱╱╱

对数螺旋

复制代码
r = a × e^(b × θ)

特点:角度等量增加,半径按比例增加
自然界:鹦鹉螺、银河系旋臂

     ╱╱╱╱
    ╱╱╱╱╱
   ╱╱╱╱╱╱
  ╱╱╱╱╱╱╱

心形线(Cardioid)

复制代码
r = a × (1 + cos(θ))

形状:心形
应用:麦克风指向性、声学

    ╱╲
   ╱  ╲
  ╱    ╲
 ╱      ╲
╱        ╲
╲        ╱
 ╲      ╱
  ╲____╱

🔄 4.2 对称群理论

对称性在数学中由群论描述,不同的对称操作构成不同的群:

常见对称群

群名 符号 描述 示例
循环群 Cn n 重旋转对称 雪花 C6
二面体群 Dn n 重旋转 + 反射 正多边形
T 四面体对称 甲烷分子
八面体群 O 立方体对称 正方体
二十面体群 I 二十面体对称 足球

对称操作的数学表示

复制代码
旋转变换矩阵(绕原点旋转角度 θ):
R(θ) = | cos(θ)  -sin(θ) |
       | sin(θ)   cos(θ) |

反射变换矩阵(关于 x 轴反射):
M = | 1   0 |
    | 0  -1 |

n 重旋转对称的组合:
Cn = {R(0), R(2π/n), R(4π/n), ..., R(2π(n-1)/n)}

🌸 4.3 曼陀罗几何学

曼陀罗(Mandala)源自梵语,意为"圆"或"中心",是一种具有深刻精神意义的几何图案。

曼陀罗的数学结构

复制代码
层级结构:
第 1 层:中心点(本源)
第 2 层:内圆(自我)
第 3 层:花瓣(展开)
第 4 层:外环(世界)
第 5 层:边界(界限)

半径公式:
r_n = r_0 × (1 + n × Δr)

花瓣角度分布:
θ_k = 2π × k / petalCount

花瓣形状的数学描述

复制代码
单瓣花瓣(使用极坐标):
r(θ) = r_base + A × cos(p × θ) × exp(-k × θ²)

其中:
- r_base: 基础半径
- A: 花瓣振幅
- p: 花瓣形状参数
- k: 衰减系数

多瓣组合:
for i in 0..petalCount:
    θ_offset = i × 2π / petalCount
    drawPetal(θ_offset)

🎵 4.4 音频特征与极坐标映射

将音频特征映射到极坐标参数,可以创造出丰富的视觉效果:

映射策略

音频特征 极坐标参数 效果
能量 (Energy) 半径 r 整体缩放
低频 (Bass) 中心大小 脉冲效果
中频 (Mid) 花瓣数 形态变化
高频 (Treble) 旋转速度 动态感
音色 (Timbre) 颜色 氛围变化

音频驱动的参数方程

dart 复制代码
// 极坐标半径受音频调制
double r = baseRadius * (1 + energy * 0.5 + bass * 0.3);

// 花瓣数量随中频变化
int petals = 4 + (mid * 8).toInt();

// 旋转速度与高频相关
double rotationSpeed = 0.5 + treble * 2;

// 颜色随音色变化
double hue = (baseHue + timbre * 60) % 360;

🔬 五、高级应用场景

🎨 5.1 实时音乐可视化

极坐标对称投影在实时音乐可视化中有独特优势:

优势分析

  1. 视觉平衡:中心对称天然具有视觉平衡感
  2. 节奏表达:旋转速度可以表达音乐节奏
  3. 层次分明:不同半径环可以表示不同频段
  4. 计算高效:对称性减少计算量

实现架构

复制代码
音频输入 → FFT分析 → 特征提取 → 参数映射 → 极坐标渲染
    ↓           ↓           ↓           ↓           ↓
  PCM数据    频谱数据    能量/频段    半径/角度    对称绘制

🌐 5.2 交互式艺术装置

极坐标对称投影适合创建交互式艺术装置:

交互方式

输入 映射 视觉反馈
触摸位置 极坐标角度 图案变形
触摸压力 半径大小 图案缩放
手势旋转 整体旋转 动态旋转
多点触控 对称数 图案变化

📱 5.3 鸿蒙多端适配

在鸿蒙系统上实现极坐标可视化需要考虑多端适配:

适配策略

dart 复制代码
// 响应式尺寸计算
double calculateRadius(BuildContext context) {
  final size = MediaQuery.of(context).size;
  final minDimension = min(size.width, size.height);
  return minDimension * 0.4; // 留出边距
}

// 设备性能适配
int calculateDetailLevel() {
  // 根据设备性能调整细节层次
  if (DevicePerformance.isHigh) return 128;
  if (DevicePerformance.isMedium) return 64;
  return 32;
}

// 多窗口适配
@override
void didChangeMetrics() {
  setState(() {
    // 窗口尺寸变化时重新计算
    _updateLayout();
  });
}

📊 六、性能优化策略

⚡ 6.1 渲染优化

极坐标对称渲染的性能优化技巧:

减少重绘区域

dart 复制代码
// 使用 RepaintBoundary 隔离重绘
RepaintBoundary(
  child: CustomPaint(
    painter: PolarPainter(...),
  ),
)

预计算对称点

dart 复制代码
// 预计算对称角度,避免每帧重复计算
List<double> _symmetryAngles = [];

void initSymmetry(int folds) {
  _symmetryAngles = List.generate(
    folds * 2, 
    (i) => i * pi / folds,
  );
}

使用 Isolate 进行复杂计算

dart 复制代码
// 在后台 Isolate 中计算复杂图案
Future<List<Point>> computePattern(PatternParams params) async {
  return await compute(_generatePattern, params);
}

💾 6.2 内存优化

对象复用

dart 复制代码
// 复用 Float32List 避免频繁分配
class AudioDataPool {
  Float32List? _buffer;
  
  Float32List getBuffer(int size) {
    _buffer ??= Float32List(size);
    return _buffer!;
  }
}

图片缓存

dart 复制代码
// 缓存静态背景图案
class PatternCache {
  static final Map<String, ui.Image> _cache = {};
  
  static Future<ui.Image> getPattern(String key) async {
    return _cache.putIfAbsent(key, () => _generatePattern(key));
  }
}

🎓 七、学习资源与拓展

📚 推荐阅读

主题 资源 难度
极坐标数学 《极坐标与参数方程》 ⭐⭐
对称群论 《群论与对称性》 ⭐⭐⭐
分形几何 《分形几何:数学基础与应用》 ⭐⭐⭐
音频处理 《数字信号处理》 ⭐⭐⭐
计算机图形学 《计算机图形学原理》 ⭐⭐⭐

🔗 相关项目

  • Processing:极坐标可视化的经典工具
  • p5.js:Web 端创意编程库
  • TouchDesigner:实时视觉编程环境
  • Max/MSP:音频可视化专业工具

📝 八、总结

本篇文章深入探讨了极坐标对称投影在音乐可视化中的应用,从基础极坐标变换到多折叠对称,构建了万花筒般的几何动画效果。

✅ 核心知识点回顾

知识点 说明
📐 极坐标系统 r, θ 表示法,坐标转换
🔄 对称变换 旋转、反射对称,群论基础
🔮 万花筒效果 多折叠对称,镜像复制
🌸 曼陀罗图案 花瓣环绘制,层级结构
🎵 音频驱动 能量映射半径,频率映射形态
性能优化 对象复用,预计算,缓存

⭐ 最佳实践要点

  • ✅ 使用极坐标简化圆形图案
  • ✅ 对称减少计算量
  • ✅ 音频特征控制参数
  • ✅ 添加发光效果增强视觉

🚀 进阶方向

  • 🔮 3D 极坐标投影
  • ✨ 交互式万花筒
  • 👆 触摸绘制图案
  • ⚡ GPU 着色器加速

相关推荐
BackCatK Chen1 小时前
2026智驾决赛圈:洗牌、技术决战与3大生死门槛
算法·华为·gpu算力·vla·世界模型
阿林来了2 小时前
Flutter三方库适配OpenHarmony【flutter_web_auth】— Dart 层源码逐行解析
flutter
lbb 小魔仙2 小时前
鸿蒙跨平台实战:React Native在OpenHarmony上的Font自定义字体注册详解
react native·华为·harmonyos
开开心心就好2 小时前
内存清理软件灵活设置,自动阈值快捷键清
运维·服务器·windows·pdf·harmonyos·risc-v·1024程序员节
lbb 小魔仙2 小时前
鸿蒙跨平台实战:React Native在OpenHarmony上的PixelFormat图片格式处理
react native·华为·harmonyos
心之语歌2 小时前
Flutter Provider 使用教程:Consumer/of/watch/read 全解析
flutter
2601_949593652 小时前
Harmony Flutter 跨平台开发实战:鸿蒙与音乐律动艺术、Voronoi 泰森多边形:空间分割的动态演化
flutter·华为·harmonyos
松叶似针2 小时前
Flutter三方库适配OpenHarmony【doc_text】— Word 文档解析插件功能全景与适配价值
flutter·word·harmonyos
2601_949593652 小时前
Harmony Flutter 跨平台开发实战:鸿蒙与音乐律动艺术、分布式联觉震动:鸿蒙多端同步的节奏共鸣
flutter·harmonyos