在这里插入图片描述

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


🧬 一、Voronoi 图:自然界的空间分割艺术

📚 1.1 Voronoi 图的历史与发现

Voronoi 图(Voronoi Diagram),又称泰森多边形(Thiessen Polygon)或狄利克雷镶嵌(Dirichlet Tessellation),是一种将空间分割成多个区域的方法。

历史发展

年份 人物 贡献
1644 勒内·笛卡尔 首次描述类似概念
1850 古斯塔夫·狄利克雷 正式研究二次型
1908 格奥尔基·沃罗诺伊 给出 n 维空间的一般定义
1911 阿尔弗雷德·泰森 应用于气象学

自然界中的 Voronoi 图案

  • 🐢 长颈鹿斑纹
  • 🌿 龟背竹叶脉
  • 🧬 细胞分裂
  • 🌊 干裂的土地
  • 🐝 蜂巢结构

📐 1.2 Voronoi 图的数学定义

给定平面上的 n 个点(称为站点或种子点)P = {p₁, p₂, …, pₙ},Voronoi 图将平面分割成 n 个区域,使得:

对于每个区域 V(pᵢ)

V(pᵢ) = {x ∈ ℝ² : d(x, pᵢ) < d(x, pⱼ), ∀j ≠ i}

其中 d(x, y) 表示两点间的欧几里得距离。

关键概念

概念 定义 说明
📍 站点 种子点 pᵢ Voronoi 区域的中心
📐 Voronoi 区域 V(pᵢ) 离该站点最近的点集
📏 Voronoi 边 两区域边界 到两站点等距的点
Voronoi 顶点 多边交点 到多个站点等距的点
🔗 Delaunay 三角剖分 Voronoi 对偶 连接相邻站点的三角形
Voronoi 图示意:

    ●─────────●─────────●
    │╲        │        ╱│
    │  ╲      │      ╱  │
    │    ╲    │    ╱    │
    ●──────●──┼──●──────●
    │    ╱    │    ╲    │
    │  ╱      │      ╲  │
    │╱        │        ╲│
    ●─────────●─────────●
    
    ● = 站点
    ─ = Voronoi 边

🔬 1.3 Delaunay 三角剖分

Delaunay 三角剖分是 Voronoi 图的对偶图,具有以下重要性质:

空圆性质(Empty Circle Property)
对于 Delaunay 三角剖分中的任意三角形,其外接圆内部不包含任何其他站点。

最大化最小角性质
在所有可能的三角剖分中,Delaunay 三角剖分最大化最小角,避免产生"瘦长"三角形。

性质 说明
🔵 空圆性质 外接圆不含其他点
📐 最大最小角 避免退化三角形
🔗 对偶关系 与 Voronoi 图互为对偶
🌐 唯一性 一般位置点集唯一

🎯 1.4 Voronoi 图的应用领域

领域 应用 效果
🗺️ 地理信息系统 最近设施分析 找最近的医院/学校
📶 通信网络 基站覆盖范围 信号最优分配
🤖 机器人路径规划 避障导航 安全路径生成
🎨 计算机图形学 纹理生成、图像分割 自然图案模拟
🎵 音乐可视化 动态空间分割 节奏驱动的细胞演化

🔧 二、Voronoi 图的 Dart 实现

🧮 2.1 基础数据结构

import 'dart:math';
import 'dart:typed_data';

/// 二维点
class Point2D {
  final double x;
  final double y;
  
  const Point2D(this.x, this.y);
  
  factory Point2D.zero() => const Point2D(0, 0);
  
  Point2D operator +(Point2D other) => Point2D(x + other.x, y + other.y);
  Point2D operator -(Point2D other) => Point2D(x - other.x, y - other.y);
  Point2D operator *(double scalar) => Point2D(x * scalar, y * scalar);
  
  double distanceTo(Point2D other) {
    final dx = x - other.x;
    final dy = y - other.y;
    return sqrt(dx * dx + dy * dy);
  }
  
  double distanceSquaredTo(Point2D other) {
    final dx = x - other.x;
    final dy = y - other.y;
    return dx * dx + dy * dy;
  }
  
  double get magnitude => sqrt(x * x + y * y);
  
  Point2D get normalized {
    final mag = magnitude;
    return mag > 0 ? Point2D(x / mag, y / mag) : Point2D.zero();
  }
  
  
  String toString() => 'Point2D($x, $y)';
  
  
  bool operator ==(Object other) =>
      other is Point2D && x == other.x && y == other.y;
  
  
  int get hashCode => Object.hash(x, y);
}

/// 线段
class LineSegment {
  final Point2D start;
  final Point2D end;
  
  const LineSegment(this.start, this.end);
  
  double get length => start.distanceTo(end);
  
  Point2D get midpoint => Point2D(
    (start.x + end.x) / 2,
    (start.y + end.y) / 2,
  );
  
  Point2D get direction => (end - start).normalized;
  
  Point2D get perpendicular => Point2D(
    -(end.y - start.y),
    end.x - start.x,
  ).normalized;
}

/// 三角形
class Triangle {
  final Point2D a;
  final Point2D b;
  final Point2D c;
  
  const Triangle(this.a, this.b, this.c);
  
  /// 计算外接圆圆心和半径
  Circumcircle getCircumcircle() {
    final ax = a.x, ay = a.y;
    final bx = b.x, by = b.y;
    final cx = c.x, cy = c.y;
    
    final d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
    
    if (d.abs() < 1e-10) {
      return Circumcircle(Point2D.zero(), double.infinity);
    }
    
    final ux = ((ax * ax + ay * ay) * (by - cy) +
                (bx * bx + by * by) * (cy - ay) +
                (cx * cx + cy * cy) * (ay - by)) / d;
    
    final uy = ((ax * ax + ay * ay) * (cx - bx) +
                (bx * bx + by * by) * (ax - cx) +
                (cx * cx + cy * cy) * (bx - ax)) / d;
    
    final center = Point2D(ux, uy);
    final radius = center.distanceTo(a);
    
    return Circumcircle(center, radius);
  }
  
  /// 判断点是否在外接圆内
  bool isPointInCircumcircle(Point2D point) {
    final circle = getCircumcircle();
    return point.distanceSquaredTo(circle.center) < circle.radius * circle.radius;
  }
}

/// 外接圆
class Circumcircle {
  final Point2D center;
  final double radius;
  
  const Circumcircle(this.center, this.radius);
}

⚡ 2.2 Fortune 算法实现

Fortune 算法是构建 Voronoi 图的经典算法,时间复杂度为 O(n log n):

/// Voronoi 单元
class VoronoiCell {
  final Point2D site;
  final List<Point2D> vertices;
  final List<int> neighborIndices;
  
  VoronoiCell({
    required this.site,
    List<Point2D>? vertices,
    List<int>? neighborIndices,
  })  : vertices = vertices ?? [],
        neighborIndices = neighborIndices ?? [];
  
  void addVertex(Point2D vertex) {
    vertices.add(vertex);
  }
  
  void sortVertices() {
    if (vertices.isEmpty) return;
    
    // 按角度排序顶点
    vertices.sort((a, b) {
      final angleA = atan2(a.y - site.y, a.x - site.x);
      final angleB = atan2(b.y - site.y, b.x - site.x);
      return angleA.compareTo(angleB);
    });
  }
  
  Path toPath() {
    if (vertices.isEmpty) return Path();
    
    final path = Path();
    path.moveTo(vertices.first.x, vertices.first.y);
    
    for (int i = 1; i < vertices.length; i++) {
      path.lineTo(vertices[i].x, vertices[i].y);
    }
    
    path.close();
    return path;
  }
}

/// Voronoi 图
class VoronoiDiagram {
  final List<Point2D> sites;
  final List<VoronoiCell> cells;
  final List<LineSegment> edges;
  final Rect bounds;
  
  VoronoiDiagram({
    required this.sites,
    required this.cells,
    required this.edges,
    required this.bounds,
  });
  
  /// 使用暴力法构建 Voronoi 图(适合小规模数据)
  factory VoronoiDiagram.fromSites(List<Point2D> sites, Rect bounds) {
    final cells = <VoronoiCell>[];
    final edges = <LineSegment>[];
    
    for (int i = 0; i < sites.length; i++) {
      final cell = VoronoiCell(site: sites[i]);
      cells.add(cell);
    }
    
    // 使用采样点确定每个单元的边界
    final resolution = 200;
    final cellPixels = List<List<Point2D>>.generate(
      sites.length,
      (_) => <Point2D>[],
    );
    
    for (int py = 0; py < resolution; py++) {
      for (int px = 0; px < resolution; px++) {
        final x = bounds.left + (px / resolution) * bounds.width;
        final y = bounds.top + (py / resolution) * bounds.height;
        final point = Point2D(x, y);
        
        int nearestIndex = 0;
        double nearestDist = point.distanceSquaredTo(sites[0]);
        
        for (int i = 1; i < sites.length; i++) {
          final dist = point.distanceSquaredTo(sites[i]);
          if (dist < nearestDist) {
            nearestDist = dist;
            nearestIndex = i;
          }
        }
        
        cellPixels[nearestIndex].add(point);
      }
    }
    
    // 提取边界点
    for (int i = 0; i < sites.length; i++) {
      final pixels = cellPixels[i];
      if (pixels.isEmpty) continue;
      
      // 找到边界点
      final boundaryPoints = <Point2D>[];
      for (final pixel in pixels) {
        final px = ((pixel.x - bounds.left) / bounds.width * resolution).round();
        final py = ((pixel.y - bounds.top) / bounds.height * resolution).round();
        
        // 检查是否为边界点
        bool isBoundary = false;
        for (final neighbor in _getNeighbors(px, py, resolution)) {
          final nx = bounds.left + (neighbor.$1 / resolution) * bounds.width;
          final ny = bounds.top + (neighbor.$2 / resolution) * bounds.height;
          final neighborPoint = Point2D(nx, ny);
          
          int neighborCell = 0;
          double neighborDist = neighborPoint.distanceSquaredTo(sites[0]);
          for (int j = 1; j < sites.length; j++) {
            final dist = neighborPoint.distanceSquaredTo(sites[j]);
            if (dist < neighborDist) {
              neighborDist = dist;
              neighborCell = j;
            }
          }
          
          if (neighborCell != i) {
            isBoundary = true;
            break;
          }
        }
        
        if (isBoundary) {
          boundaryPoints.add(pixel);
        }
      }
      
      // 简化边界点
      cells[i].vertices.addAll(_simplifyBoundary(boundaryPoints, sites[i]));
      cells[i].sortVertices();
    }
    
    return VoronoiDiagram(
      sites: sites,
      cells: cells,
      edges: edges,
      bounds: bounds,
    );
  }
  
  static List<(int, int)> _getNeighbors(int x, int y, int resolution) {
    return [
      (x - 1, y), (x + 1, y),
      (x, y - 1), (x, y + 1),
    ].where((p) => p.$1 >= 0 && p.$1 < resolution && 
                   p.$2 >= 0 && p.$2 < resolution).toList();
  }
  
  static List<Point2D> _simplifyBoundary(List<Point2D> points, Point2D center) {
    if (points.isEmpty) return [];
    
    // 按角度排序
    points.sort((a, b) {
      final angleA = atan2(a.y - center.y, a.x - center.x);
      final angleB = atan2(b.y - center.y, b.x - center.x);
      return angleA.compareTo(angleB);
    });
    
    // 简化:每隔几个点取一个
    final simplified = <Point2D>[];
    final step = max(1, points.length ~/ 20);
    for (int i = 0; i < points.length; i += step) {
      simplified.add(points[i]);
    }
    
    return simplified;
  }
}

🎨 2.3 Delaunay 三角剖分实现

/// Delaunay 三角剖分(Bowyer-Watson 算法)
class DelaunayTriangulation {
  final List<Point2D> points;
  final List<Triangle> triangles;
  
  DelaunayTriangulation({
    required this.points,
    required this.triangles,
  });
  
  /// 使用 Bowyer-Watson 算法构建 Delaunay 三角剖分
  factory DelaunayTriangulation.fromPoints(List<Point2D> points) {
    if (points.length < 3) {
      return DelaunayTriangulation(points: points, triangles: []);
    }
    
    // 创建超级三角形
    final bounds = _calculateBounds(points);
    final superTriangle = _createSuperTriangle(bounds);
    
    final triangles = <Triangle>[superTriangle];
    
    // 逐点插入
    for (final point in points) {
      final badTriangles = <Triangle>[];
      
      // 找到所有外接圆包含该点的三角形
      for (final triangle in triangles) {
        if (triangle.isPointInCircumcircle(point)) {
          badTriangles.add(triangle);
        }
      }
      
      // 找到多边形边界
      final polygon = <LineSegment>[];
      for (final triangle in badTriangles) {
        final edges = [
          LineSegment(triangle.a, triangle.b),
          LineSegment(triangle.b, triangle.c),
          LineSegment(triangle.c, triangle.a),
        ];
        
        for (final edge in edges) {
          bool isShared = false;
          for (final other in badTriangles) {
            if (other == triangle) continue;
            final otherEdges = [
              LineSegment(other.a, other.b),
              LineSegment(other.b, other.c),
              LineSegment(other.c, other.a),
            ];
            if (otherEdges.any((e) => _edgesEqual(e, edge))) {
              isShared = true;
              break;
            }
          }
          if (!isShared) {
            polygon.add(edge);
          }
        }
      }
      
      // 移除坏三角形
      triangles.removeWhere((t) => badTriangles.contains(t));
      
      // 创建新三角形
      for (final edge in polygon) {
        triangles.add(Triangle(edge.start, edge.end, point));
      }
    }
    
    // 移除包含超级三角形顶点的三角形
    triangles.removeWhere((t) =>
        _triangleContainsPoint(t, superTriangle.a) ||
        _triangleContainsPoint(t, superTriangle.b) ||
        _triangleContainsPoint(t, superTriangle.c));
    
    return DelaunayTriangulation(points: points, triangles: triangles);
  }
  
  static Rect _calculateBounds(List<Point2D> points) {
    double minX = double.infinity, maxX = double.negativeInfinity;
    double minY = double.infinity, maxY = double.negativeInfinity;
    
    for (final p in points) {
      minX = min(minX, p.x);
      maxX = max(maxX, p.x);
      minY = min(minY, p.y);
      maxY = max(maxY, p.y);
    }
    
    return Rect.fromLTRB(minX, minY, maxX, maxY);
  }
  
  static Triangle _createSuperTriangle(Rect bounds) {
    final dx = bounds.width * 10;
    final dy = bounds.height * 10;
    
    return Triangle(
      Point2D(bounds.left - dx, bounds.top - dy),
      Point2D(bounds.right + dx, bounds.top - dy),
      Point2D(bounds.center.dx, bounds.bottom + dy),
    );
  }
  
  static bool _edgesEqual(LineSegment a, LineSegment b) {
    return (a.start == b.start && a.end == b.end) ||
           (a.start == b.end && a.end == b.start);
  }
  
  static bool _triangleContainsPoint(Triangle t, Point2D p) {
    return t.a == p || t.b == p || t.c == p;
  }
  
  /// 获取 Voronoi 边
  List<LineSegment> getVoronoiEdges() {
    final edges = <LineSegment>[];
    
    for (final triangle in triangles) {
      final circle = triangle.getCircumcircle();
      if (circle.radius.isFinite) {
        // 连接相邻三角形的外心
        // 这里简化处理,实际需要更复杂的逻辑
      }
    }
    
    return edges;
  }
}

🌬️ 三、音乐驱动的 Voronoi 动画

🎵 3.1 动态 Voronoi 控制器

import 'package:flutter/material.dart';
import 'package:just_audio_ohos/just_audio_ohos.dart';
import 'package:audio_session/audio_session.dart';

/// 动态 Voronoi 控制器
class DynamicVoronoiController extends ChangeNotifier {
  final AudioPlayer _player = AudioPlayer();
  final Random _random = Random();
  
  List<Point2D> _sites = [];
  List<Point2D> _targetSites = [];
  List<Color> _cellColors = [];
  
  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 _siteCount = 20;
  
  bool get isPlaying => _isPlaying;
  Duration get position => _position;
  Duration get duration => _duration;
  List<Point2D> get sites => _sites;
  List<Color> get cellColors => _cellColors;
  Float32List get audioData => _audioData;
  double get energy => _energy;
  double get bass => _bass;
  double get mid => _mid;
  double get treble => _treble;
  AudioPlayer get player => _player;
  
  /// 初始化
  Future<void> initialize(Size size) 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();
    });
    
    _initializeSites(size);
  }
  
  /// 初始化站点
  void _initializeSites(Size size) {
    _sites = [];
    _targetSites = [];
    _cellColors = [];
    
    for (int i = 0; i < _siteCount; i++) {
      final x = _random.nextDouble() * size.width;
      final y = _random.nextDouble() * size.height;
      _sites.add(Point2D(x, y));
      _targetSites.add(Point2D(x, y));
      _cellColors.add(HSVColor.fromAHSV(
        1,
        _random.nextDouble() * 360,
        0.6 + _random.nextDouble() * 0.3,
        0.8 + _random.nextDouble() * 0.2,
      ).toColor());
    }
  }
  
  /// 加载网络音频
  Future<void> loadAudio(String url) async {
    try {
      await _player.setUrl(url);
    } catch (e) {
      debugPrint('加载音频失败: $e');
    }
  }
  
  /// 更新
  void update(double dt, Size size) {
    _time += dt;
    
    // 更新音频数据
    _updateAudioData();
    
    // 计算音频特征
    _calculateAudioFeatures();
    
    // 更新站点位置
    _updateSites(dt, size);
    
    // 更新颜色
    _updateColors();
    
    notifyListeners();
  }
  
  void _updateAudioData() {
    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 _updateSites(double dt, Size size) {
    // 根据音频更新目标位置
    for (int i = 0; i < _sites.length; i++) {
      final angle = _time * 0.5 + i * 2 * pi / _sites.length;
      final radius = 50 + _energy * 100 + sin(_time * 2 + i) * 30;
      
      _targetSites[i] = Point2D(
        size.width / 2 + cos(angle) * radius,
        size.height / 2 + sin(angle) * radius,
      );
      
      // 平滑移动
      _sites[i] = Point2D(
        _sites[i].x + (_targetSites[i].x - _sites[i].x) * dt * 2,
        _sites[i].y + (_targetSites[i].y - _sites[i].y) * dt * 2,
      );
    }
    
    // 低音脉冲:向外扩散
    if (_bass > 0.5) {
      for (int i = 0; i < _sites.length; i++) {
        final dir = (_sites[i] - Point2D(size.width / 2, size.height / 2)).normalized;
        _sites[i] = Point2D(
          _sites[i].x + dir.x * _bass * 10,
          _sites[i].y + dir.y * _bass * 10,
        );
      }
    }
  }
  
  void _updateColors() {
    for (int i = 0; i < _cellColors.length; i++) {
      final hue = (_time * 20 + i * 360 / _sites.length) % 360;
      _cellColors[i] = HSVColor.fromAHSV(
        0.6 + _energy * 0.3,
        hue,
        0.7 + _mid * 0.3,
        0.8 + _treble * 0.2,
      ).toColor();
    }
  }
  
  /// 添加站点
  void addSite(Point2D site, Size size) {
    if (_sites.length < 50) {
      _sites.add(site);
      _targetSites.add(site);
      _cellColors.add(HSVColor.fromAHSV(
        1,
        _random.nextDouble() * 360,
        0.7,
        0.9,
      ).toColor());
      _siteCount = _sites.length;
    }
  }
  
  /// 移除最近的站点
  void removeNearestSite(Point2D point) {
    if (_sites.isEmpty) return;
    
    int nearestIndex = 0;
    double nearestDist = point.distanceSquaredTo(_sites[0]);
    
    for (int i = 1; i < _sites.length; i++) {
      final dist = point.distanceSquaredTo(_sites[i]);
      if (dist < nearestDist) {
        nearestDist = dist;
        nearestIndex = i;
      }
    }
    
    _sites.removeAt(nearestIndex);
    _targetSites.removeAt(nearestIndex);
    _cellColors.removeAt(nearestIndex);
    _siteCount = _sites.length;
  }
  
  /// 播放/暂停
  Future<void> togglePlay() async {
    if (_isPlaying) {
      await _player.pause();
    } else {
      await _player.play();
    }
  }
  
  /// 跳转
  Future<void> seek(Duration position) async {
    await _player.seek(position);
  }
  
  
  void dispose() {
    _player.dispose();
    super.dispose();
  }
}

🎨 3.2 Voronoi 绘制器

/// Voronoi 绘制器
class VoronoiPainter extends CustomPainter {
  final List<Point2D> sites;
  final List<Color> cellColors;
  final Rect bounds;
  final double energy;
  final bool isPlaying;
  final double time;
  
  VoronoiPainter({
    required this.sites,
    required this.cellColors,
    required this.bounds,
    required this.energy,
    required this.isPlaying,
    required this.time,
  });
  
  
  void paint(Canvas canvas, Size size) {
    // 绘制背景
    _drawBackground(canvas, size);
    
    // 绘制 Voronoi 单元
    _drawVoronoiCells(canvas, size);
    
    // 绘制站点
    _drawSites(canvas, size);
    
    // 绘制边框
    _drawBorders(canvas, size);
  }
  
  void _drawBackground(Canvas canvas, Size size) {
    final gradient = RadialGradient(
      center: Alignment.center,
      radius: 1.2,
      colors: [
        Color.lerp(const Color(0xFF1a1a2e), const Color(0xFF2a1a3e), energy)!,
        const Color(0xFF0a0a15),
      ],
    );
    
    canvas.drawRect(
      Rect.fromLTWH(0, 0, size.width, size.height),
      Paint()..shader = gradient.createShader(Rect.fromLTWH(0, 0, size.width, size.height)),
    );
  }
  
  void _drawVoronoiCells(Canvas canvas, Size size) {
    if (sites.isEmpty) return;
    
    final resolution = 150;
    final cellMap = List<List<int>>.generate(
      resolution,
      (_) => List<int>.filled(resolution, -1),
    );
    
    // 为每个像素分配最近的站点
    for (int py = 0; py < resolution; py++) {
      for (int px = 0; px < resolution; px++) {
        final x = (px / resolution) * size.width;
        final y = (py / resolution) * size.height;
        final point = Point2D(x, y);
        
        int nearestIndex = 0;
        double nearestDist = point.distanceSquaredTo(sites[0]);
        
        for (int i = 1; i < sites.length; i++) {
          final dist = point.distanceSquaredTo(sites[i]);
          if (dist < nearestDist) {
            nearestDist = dist;
            nearestIndex = i;
          }
        }
        
        cellMap[py][px] = nearestIndex;
      }
    }
    
    // 绘制每个单元
    for (int i = 0; i < sites.length; i++) {
      _drawCell(canvas, size, cellMap, i, resolution);
    }
  }
  
  void _drawCell(Canvas canvas, Size size, List<List<int>> cellMap, 
                 int cellIndex, int resolution) {
    final path = Path();
    bool started = false;
    
    // 找到单元边界
    for (int py = 0; py < resolution; py++) {
      for (int px = 0; px < resolution; px++) {
        if (cellMap[py][px] == cellIndex) {
          // 检查是否为边界像素
          bool isBoundary = false;
          
          for (final neighbor in [(px - 1, py), (px + 1, py), (px, py - 1), (px, py + 1)]) {
            final nx = neighbor.$1;
            final ny = neighbor.$2;
            
            if (nx < 0 || nx >= resolution || ny < 0 || ny >= resolution) {
              isBoundary = true;
              break;
            }
            
            if (cellMap[ny][nx] != cellIndex) {
              isBoundary = true;
              break;
            }
          }
          
          if (isBoundary) {
            final x = (px / resolution) * size.width;
            final y = (py / resolution) * size.height;
            
            if (!started) {
              path.moveTo(x, y);
              started = true;
            } else {
              path.lineTo(x, y);
            }
          }
        }
      }
    }
    
    if (started) {
      path.close();
      
      // 绘制填充
      final color = cellIndex < cellColors.length 
          ? cellColors[cellIndex] 
          : Colors.white;
      
      canvas.drawPath(
        path,
        Paint()
          ..color = color.withOpacity(0.4 + energy * 0.3)
          ..style = PaintingStyle.fill,
      );
      
      // 绘制边框
      canvas.drawPath(
        path,
        Paint()
          ..color = Colors.white.withOpacity(0.3 + energy * 0.4)
          ..style = PaintingStyle.stroke
          ..strokeWidth = 1 + energy,
      );
    }
  }
  
  void _drawSites(Canvas canvas, Size size) {
    for (int i = 0; i < sites.length; i++) {
      final site = sites[i];
      
      // 发光效果
      final glowPaint = Paint()
        ..shader = RadialGradient(
          colors: [
            Colors.white.withOpacity(0.6),
            Colors.white.withOpacity(0),
          ],
        ).createShader(Rect.fromCircle(
          center: Offset(site.x, site.y),
          radius: 15 + energy * 10,
        ));
      
      canvas.drawCircle(
        Offset(site.x, site.y),
        15 + energy * 10,
        glowPaint,
      );
      
      // 核心点
      canvas.drawCircle(
        Offset(site.x, site.y),
        3 + energy * 2,
        Paint()..color = Colors.white,
      );
    }
  }
  
  void _drawBorders(Canvas canvas, Size size) {
    // 绘制外边框
    canvas.drawRect(
      Rect.fromLTWH(0, 0, size.width, size.height),
      Paint()
        ..color = Colors.white.withOpacity(0.1)
        ..style = PaintingStyle.stroke
        ..strokeWidth = 2,
    );
  }
  
  
  bool shouldRepaint(covariant VoronoiPainter oldDelegate) => true;
}

📦 四、完整示例代码

以下是完整的 Voronoi 泰森多边形音乐可视化示例代码:

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

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Voronoi 泰森多边形',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.teal,
          brightness: Brightness.dark,
        ),
        useMaterial3: true,
      ),
      home: const VoronoiHomePage(),
      debugShowCheckedModeBanner: false,
    );
  }
}

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('🧬 Voronoi 泰森多边形'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          _buildCard(
            context,
            title: '基础 Voronoi',
            description: '静态泰森多边形演示',
            icon: Icons.grid_on,
            color: Colors.teal,
            onTap: () => Navigator.push(
              context,
              MaterialPageRoute(builder: (_) => const BasicVoronoiDemo()),
            ),
          ),
          _buildCard(
            context,
            title: '动态 Voronoi',
            description: '站点运动与演化',
            icon: Icons.motion_photos_on,
            color: Colors.cyan,
            onTap: () => Navigator.push(
              context,
              MaterialPageRoute(builder: (_) => const DynamicVoronoiDemo()),
            ),
          ),
          _buildCard(
            context,
            title: '音乐 Voronoi',
            description: '音频驱动的细胞演化',
            icon: Icons.music_note,
            color: Colors.purple,
            onTap: () => Navigator.push(
              context,
              MaterialPageRoute(builder: (_) => const MusicVoronoiDemo()),
            ),
          ),
          _buildCard(
            context,
            title: '交互 Voronoi',
            description: '触摸添加/删除站点',
            icon: Icons.touch_app,
            color: Colors.orange,
            onTap: () => Navigator.push(
              context,
              MaterialPageRoute(builder: (_) => const InteractiveVoronoiDemo()),
            ),
          ),
        ],
      ),
    );
  }

  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 Point2D {
  final double x;
  final double y;
  
  const Point2D(this.x, this.y);
  
  Point2D operator +(Point2D o) => Point2D(x + o.x, y + o.y);
  Point2D operator -(Point2D o) => Point2D(x - o.x, y - o.y);
  
  double distanceTo(Point2D o) => sqrt((x - o.x) * (x - o.x) + (y - o.y) * (y - o.y));
  double distanceSquaredTo(Point2D o) => (x - o.x) * (x - o.x) + (y - o.y) * (y - o.y);
  double get magnitude => sqrt(x * x + y * y);
  Point2D get normalized => magnitude > 0 ? Point2D(x / magnitude, y / magnitude) : const Point2D(0, 0);
}

/// 基础 Voronoi 演示
class BasicVoronoiDemo extends StatefulWidget {
  const BasicVoronoiDemo({super.key});

  
  State<BasicVoronoiDemo> createState() => _BasicVoronoiDemoState();
}

class _BasicVoronoiDemoState extends State<BasicVoronoiDemo> {
  final List<Point2D> _sites = [];
  final List<Color> _colors = [];
  final Random _random = Random();
  
  
  void initState() {
    super.initState();
    _generateSites(15);
  }
  
  void _generateSites(int count) {
    _sites.clear();
    _colors.clear();
    
    for (int i = 0; i < count; i++) {
      _sites.add(Point2D(
        50 + _random.nextDouble() * 300,
        50 + _random.nextDouble() * 300,
      ));
      _colors.add(HSVColor.fromAHSV(1, _random.nextDouble() * 360, 0.7, 0.9).toColor());
    }
    setState(() {});
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('基础 Voronoi')),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _generateSites(15),
        child: const Icon(Icons.refresh),
      ),
      body: CustomPaint(
        painter: BasicVoronoiPainter(_sites, _colors),
        size: Size.infinite,
      ),
    );
  }
}

class BasicVoronoiPainter extends CustomPainter {
  final List<Point2D> sites;
  final List<Color> colors;
  
  BasicVoronoiPainter(this.sites, this.colors);
  
  
  void paint(Canvas canvas, Size size) {
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height),
        Paint()..color = const Color(0xFF0a0a15));
    
    if (sites.isEmpty) return;
    
    final resolution = 120;
    
    for (int py = 0; py < resolution; py++) {
      for (int px = 0; px < resolution; px++) {
        final x = (px / resolution) * size.width;
        final y = (py / resolution) * size.height;
        final point = Point2D(x, y);
        
        int nearest = 0;
        double nearestDist = point.distanceSquaredTo(sites[0]);
        
        for (int i = 1; i < sites.length; i++) {
          final dist = point.distanceSquaredTo(sites[i]);
          if (dist < nearestDist) {
            nearestDist = dist;
            nearest = i;
          }
        }
        
        // 检查边界
        bool isBoundary = false;
        for (final n in [(px - 1, py), (px + 1, py), (px, py - 1), (px, py + 1)]) {
          if (n.$1 < 0 || n.$1 >= resolution || n.$2 < 0 || n.$2 >= resolution) continue;
          
          final nx = (n.$1 / resolution) * size.width;
          final ny = (n.$2 / resolution) * size.height;
          final np = Point2D(nx, ny);
          
          int nNearest = 0;
          double nDist = np.distanceSquaredTo(sites[0]);
          for (int i = 1; i < sites.length; i++) {
            final d = np.distanceSquaredTo(sites[i]);
            if (d < nDist) { nDist = d; nNearest = i; }
          }
          
          if (nNearest != nearest) { isBoundary = true; break; }
        }
        
        final color = colors[nearest % colors.length];
        final paint = Paint()..color = isBoundary 
            ? Colors.white.withOpacity(0.8)
            : color.withOpacity(0.5);
        
        canvas.drawRect(
          Rect.fromLTWH(x, y, size.width / resolution, size.height / resolution),
          paint,
        );
      }
    }
    
    // 绘制站点
    for (final site in sites) {
      canvas.drawCircle(Offset(site.x, site.y), 4, Paint()..color = Colors.white);
    }
  }
  
  
  bool shouldRepaint(covariant BasicVoronoiPainter old) => true;
}

/// 动态 Voronoi 演示
class DynamicVoronoiDemo extends StatefulWidget {
  const DynamicVoronoiDemo({super.key});

  
  State<DynamicVoronoiDemo> createState() => _DynamicVoronoiDemoState();
}

class _DynamicVoronoiDemoState extends State<DynamicVoronoiDemo> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  final List<Point2D> _sites = [];
  final List<Color> _colors = [];
  final Random _random = Random();
  double _time = 0;
  
  
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: const Duration(seconds: 1))..repeat();
    _controller.addListener(_update);
    _generateSites(12);
  }
  
  void _generateSites(int count) {
    _sites.clear();
    _colors.clear();
    for (int i = 0; i < count; i++) {
      _sites.add(Point2D(200, 200));
      _colors.add(HSVColor.fromAHSV(1, i * 360 / count, 0.7, 0.9).toColor());
    }
  }
  
  void _update() {
    _time += 0.016;
    setState(() {});
  }

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('动态 Voronoi')),
      body: LayoutBuilder(
        builder: (context, constraints) {
          final size = Size(constraints.maxWidth, constraints.maxHeight);
          
          // 更新站点位置
          for (int i = 0; i < _sites.length; i++) {
            final angle = _time + i * 2 * pi / _sites.length;
            final radius = 80 + sin(_time * 2 + i) * 40;
            _sites[i] = Point2D(
              size.width / 2 + cos(angle) * radius,
              size.height / 2 + sin(angle) * radius,
            );
          }
          
          return CustomPaint(
            painter: DynamicVoronoiPainter(_sites, _colors, _time),
            size: size,
          );
        },
      ),
    );
  }
}

class DynamicVoronoiPainter extends CustomPainter {
  final List<Point2D> sites;
  final List<Color> colors;
  final double time;
  
  DynamicVoronoiPainter(this.sites, this.colors, this.time);
  
  
  void paint(Canvas canvas, Size size) {
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height),
        Paint()..color = const Color(0xFF0a0a15));
    
    if (sites.isEmpty) return;
    
    final resolution = 100;
    
    for (int py = 0; py < resolution; py++) {
      for (int px = 0; px < resolution; px++) {
        final x = (px / resolution) * size.width;
        final y = (py / resolution) * size.height;
        final point = Point2D(x, y);
        
        int nearest = 0;
        double nearestDist = point.distanceSquaredTo(sites[0]);
        for (int i = 1; i < sites.length; i++) {
          final d = point.distanceSquaredTo(sites[i]);
          if (d < nearestDist) { nearestDist = d; nearest = i; }
        }
        
        final hue = (time * 30 + nearest * 30) % 360;
        final color = HSVColor.fromAHSV(1, hue, 0.6, 0.9).toColor();
        
        canvas.drawRect(
          Rect.fromLTWH(x, y, size.width / resolution + 1, size.height / resolution + 1),
          Paint()..color = color.withOpacity(0.4),
        );
      }
    }
    
    // 绘制边框
    for (int py = 0; py < resolution; py++) {
      for (int px = 0; px < resolution; px++) {
        final x = (px / resolution) * size.width;
        final y = (py / resolution) * size.height;
        final point = Point2D(x, y);
        
        int nearest = 0;
        double nearestDist = point.distanceSquaredTo(sites[0]);
        for (int i = 1; i < sites.length; i++) {
          final d = point.distanceSquaredTo(sites[i]);
          if (d < nearestDist) { nearestDist = d; nearest = i; }
        }
        
        for (final n in [(px - 1, py), (px + 1, py), (px, py - 1), (px, py + 1)]) {
          if (n.$1 < 0 || n.$1 >= resolution || n.$2 < 0 || n.$2 >= resolution) continue;
          
          final nx = (n.$1 / resolution) * size.width;
          final ny = (n.$2 / resolution) * size.height;
          final np = Point2D(nx, ny);
          
          int nNearest = 0;
          double nDist = np.distanceSquaredTo(sites[0]);
          for (int i = 1; i < sites.length; i++) {
            final d = np.distanceSquaredTo(sites[i]);
            if (d < nDist) { nDist = d; nNearest = i; }
          }
          
          if (nNearest != nearest) {
            canvas.drawRect(
              Rect.fromLTWH(x, y, size.width / resolution + 1, size.height / resolution + 1),
              Paint()..color = Colors.white.withOpacity(0.3),
            );
            break;
          }
        }
      }
    }
    
    for (final site in sites) {
      canvas.drawCircle(Offset(site.x, site.y), 4, Paint()..color = Colors.white);
    }
  }
  
  
  bool shouldRepaint(covariant DynamicVoronoiPainter old) => true;
}

/// 音乐 Voronoi 演示
class MusicVoronoiDemo extends StatefulWidget {
  const MusicVoronoiDemo({super.key});

  
  State<MusicVoronoiDemo> createState() => _MusicVoronoiDemoState();
}

class _MusicVoronoiDemoState extends State<MusicVoronoiDemo> with TickerProviderStateMixin {
  late AnimationController _animController;
  late AudioPlayer _audioPlayer;
  final List<Point2D> _sites = [];
  final List<Color> _colors = [];
  final Random _random = Random();
  Float32List _audioData = Float32List(128);
  bool _isPlaying = false;
  Duration _position = Duration.zero;
  Duration _duration = Duration.zero;
  double _energy = 0, _bass = 0;
  double _time = 0;
  
  static const String _audioUrl = 
      'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3';

  
  void initState() {
    super.initState();
    _initAudio();
    _animController = AnimationController(vsync: this, duration: const Duration(milliseconds: 16))..repeat();
    _animController.addListener(_update);
    _generateSites(15);
  }
  
  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 _generateSites(int count) {
    _sites.clear();
    _colors.clear();
    for (int i = 0; i < count; i++) {
      _sites.add(Point2D(200, 200));
      _colors.add(HSVColor.fromAHSV(1, i * 360 / count, 0.7, 0.9).toColor());
    }
  }
  
  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;
    for (int i = 0; i < 128; i++) {
      total += _audioData[i].abs();
      if (i < 32) bassE += _audioData[i].abs();
    }
    _energy = total / 128;
    _bass = bassE / 32;
    
    setState(() {});
  }

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('音乐 Voronoi')),
      body: Stack(
        children: [
          LayoutBuilder(
            builder: (context, constraints) {
              final size = Size(constraints.maxWidth, constraints.maxHeight);
              
              for (int i = 0; i < _sites.length; i++) {
                final angle = _time * 0.5 + i * 2 * pi / _sites.length;
                final radius = 60 + _energy * 80 + sin(_time * 2 + i) * 30;
                _sites[i] = Point2D(
                  size.width / 2 + cos(angle) * radius,
                  size.height / 2 + sin(angle) * radius,
                );
              }
              
              return CustomPaint(
                painter: MusicVoronoiPainter(_sites, _time, _energy, _isPlaying),
                size: size,
              );
            },
          ),
          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())),
          ),
          IconButton(
            icon: Icon(_isPlaying ? Icons.pause : Icons.play_arrow, color: Colors.teal, size: 40),
            onPressed: () => _isPlaying ? _audioPlayer.pause() : _audioPlayer.play(),
          ),
        ],
      ),
    );
  }
}

class MusicVoronoiPainter extends CustomPainter {
  final List<Point2D> sites;
  final double time;
  final double energy;
  final bool isPlaying;
  
  MusicVoronoiPainter(this.sites, this.time, this.energy, this.isPlaying);
  
  
  void paint(Canvas canvas, Size size) {
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height),
        Paint()..color = Color.lerp(const Color(0xFF0a0a15), const Color(0xFF150a20), energy)!);
    
    if (sites.isEmpty) return;
    
    final resolution = 80;
    
    for (int py = 0; py < resolution; py++) {
      for (int px = 0; px < resolution; px++) {
        final x = (px / resolution) * size.width;
        final y = (py / resolution) * size.height;
        final point = Point2D(x, y);
        
        int nearest = 0;
        double nearestDist = point.distanceSquaredTo(sites[0]);
        for (int i = 1; i < sites.length; i++) {
          final d = point.distanceSquaredTo(sites[i]);
          if (d < nearestDist) { nearestDist = d; nearest = i; }
        }
        
        final hue = (time * 40 + nearest * 25) % 360;
        final color = HSVColor.fromAHSV(1, hue, 0.5 + energy * 0.3, 0.9).toColor();
        
        canvas.drawRect(
          Rect.fromLTWH(x, y, size.width / resolution + 1, size.height / resolution + 1),
          Paint()..color = color.withOpacity(0.35 + energy * 0.3),
        );
      }
    }
    
    // 边框
    for (int py = 0; py < resolution; py++) {
      for (int px = 0; px < resolution; px++) {
        final x = (px / resolution) * size.width;
        final y = (py / resolution) * size.height;
        final point = Point2D(x, y);
        
        int nearest = 0;
        double nearestDist = point.distanceSquaredTo(sites[0]);
        for (int i = 1; i < sites.length; i++) {
          final d = point.distanceSquaredTo(sites[i]);
          if (d < nearestDist) { nearestDist = d; nearest = i; }
        }
        
        for (final n in [(px - 1, py), (px + 1, py), (px, py - 1), (px, py + 1)]) {
          if (n.$1 < 0 || n.$1 >= resolution || n.$2 < 0 || n.$2 >= resolution) continue;
          
          final nx = (n.$1 / resolution) * size.width;
          final ny = (n.$2 / resolution) * size.height;
          final np = Point2D(nx, ny);
          
          int nNearest = 0;
          double nDist = np.distanceSquaredTo(sites[0]);
          for (int i = 1; i < sites.length; i++) {
            final d = np.distanceSquaredTo(sites[i]);
            if (d < nDist) { nDist = d; nNearest = i; }
          }
          
          if (nNearest != nearest) {
            canvas.drawRect(
              Rect.fromLTWH(x, y, size.width / resolution + 1, size.height / resolution + 1),
              Paint()..color = Colors.white.withOpacity(0.2 + energy * 0.3),
            );
            break;
          }
        }
      }
    }
    
    for (final site in sites) {
      final paint = Paint()..color = Colors.white.withOpacity(0.8);
      if (isPlaying) {
        paint.maskFilter = const MaskFilter.blur(BlurStyle.normal, 3);
      }
      canvas.drawCircle(Offset(site.x, site.y), 3 + energy * 2, paint);
    }
  }
  
  
  bool shouldRepaint(covariant MusicVoronoiPainter old) => true;
}

/// 交互 Voronoi 演示
class InteractiveVoronoiDemo extends StatefulWidget {
  const InteractiveVoronoiDemo({super.key});

  
  State<InteractiveVoronoiDemo> createState() => _InteractiveVoronoiDemoState();
}

class _InteractiveVoronoiDemoState extends State<InteractiveVoronoiDemo> {
  final List<Point2D> _sites = [];
  final List<Color> _colors = [];
  final Random _random = Random();
  
  
  void initState() {
    super.initState();
    _generateInitialSites();
  }
  
  void _generateInitialSites() {
    for (int i = 0; i < 8; i++) {
      _sites.add(Point2D(50 + _random.nextDouble() * 300, 50 + _random.nextDouble() * 300));
      _colors.add(HSVColor.fromAHSV(1, _random.nextDouble() * 360, 0.7, 0.9).toColor());
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('交互 Voronoi')),
      body: GestureDetector(
        onTapDown: (details) {
          setState(() {
            _sites.add(Point2D(details.localPosition.dx, details.localPosition.dy));
            _colors.add(HSVColor.fromAHSV(1, _random.nextDouble() * 360, 0.7, 0.9).toColor());
          });
        },
        onLongPressStart: (details) {
          // 长按删除最近的站点
          final point = Point2D(details.localPosition.dx, details.localPosition.dy);
          int nearest = 0;
          double nearestDist = point.distanceSquaredTo(_sites[0]);
          for (int i = 1; i < _sites.length; i++) {
            final d = point.distanceSquaredTo(_sites[i]);
            if (d < nearestDist) { nearestDist = d; nearest = i; }
          }
          setState(() {
            _sites.removeAt(nearest);
            _colors.removeAt(nearest);
          });
        },
        child: CustomPaint(
          painter: InteractiveVoronoiPainter(_sites, _colors),
          size: Size.infinite,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => setState(() { _sites.clear(); _colors.clear(); }),
        child: const Icon(Icons.clear),
      ),
    );
  }
}

class InteractiveVoronoiPainter extends CustomPainter {
  final List<Point2D> sites;
  final List<Color> colors;
  
  InteractiveVoronoiPainter(this.sites, this.colors);
  
  
  void paint(Canvas canvas, Size size) {
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height),
        Paint()..color = const Color(0xFF0a0a15));
    
    if (sites.isEmpty) {
      final textPainter = TextPainter(
        text: const TextSpan(
          text: '点击添加站点\n长按删除站点',
          style: TextStyle(color: Colors.white54, fontSize: 18),
        ),
        textDirection: TextDirection.ltr,
      )..layout();
      textPainter.paint(canvas, Offset(
        (size.width - textPainter.width) / 2,
        (size.height - textPainter.height) / 2,
      ));
      return;
    }
    
    final resolution = 100;
    
    for (int py = 0; py < resolution; py++) {
      for (int px = 0; px < resolution; px++) {
        final x = (px / resolution) * size.width;
        final y = (py / resolution) * size.height;
        final point = Point2D(x, y);
        
        int nearest = 0;
        double nearestDist = point.distanceSquaredTo(sites[0]);
        for (int i = 1; i < sites.length; i++) {
          final d = point.distanceSquaredTo(sites[i]);
          if (d < nearestDist) { nearestDist = d; nearest = i; }
        }
        
        final color = colors[nearest % colors.length];
        
        canvas.drawRect(
          Rect.fromLTWH(x, y, size.width / resolution + 1, size.height / resolution + 1),
          Paint()..color = color.withOpacity(0.5),
        );
      }
    }
    
    // 边框
    for (int py = 0; py < resolution; py++) {
      for (int px = 0; px < resolution; px++) {
        final x = (px / resolution) * size.width;
        final y = (py / resolution) * size.height;
        final point = Point2D(x, y);
        
        int nearest = 0;
        double nearestDist = point.distanceSquaredTo(sites[0]);
        for (int i = 1; i < sites.length; i++) {
          final d = point.distanceSquaredTo(sites[i]);
          if (d < nearestDist) { nearestDist = d; nearest = i; }
        }
        
        for (final n in [(px - 1, py), (px + 1, py), (px, py - 1), (px, py + 1)]) {
          if (n.$1 < 0 || n.$1 >= resolution || n.$2 < 0 || n.$2 >= resolution) continue;
          
          final nx = (n.$1 / resolution) * size.width;
          final ny = (n.$2 / resolution) * size.height;
          final np = Point2D(nx, ny);
          
          int nNearest = 0;
          double nDist = np.distanceSquaredTo(sites[0]);
          for (int i = 1; i < sites.length; i++) {
            final d = np.distanceSquaredTo(sites[i]);
            if (d < nDist) { nDist = d; nNearest = i; }
          }
          
          if (nNearest != nearest) {
            canvas.drawRect(
              Rect.fromLTWH(x, y, size.width / resolution + 1, size.height / resolution + 1),
              Paint()..color = Colors.white.withOpacity(0.4),
            );
            break;
          }
        }
      }
    }
    
    for (final site in sites) {
      canvas.drawCircle(Offset(site.x, site.y), 5, Paint()..color = Colors.white);
    }
  }
  
  
  bool shouldRepaint(covariant InteractiveVoronoiPainter old) => true;
}

📝 五、总结

本篇文章深入探讨了 Voronoi 泰森多边形在音乐可视化中的应用,从计算几何原理到动态演化算法,构建了具有"细胞分裂感"的空间分割动画效果。

✅ 核心知识点回顾

知识点 说明
🧬 Voronoi 图 空间分割、最近邻域
📐 Delaunay 三角剖分 空圆性质、对偶关系
Bowyer-Watson 算法 逐点插入、增量构建
🎵 音频驱动 能量控制站点运动
🔊 网络音乐 just_audio_ohos 在线播放

⭐ 最佳实践要点

  • ✅ 使用像素采样法简化 Voronoi 计算
  • ✅ 站点平滑移动避免突变
  • ✅ 颜色随音频动态变化
  • ✅ 支持交互添加/删除站点

🚀 进阶方向

  • 🔮 实现 Fortune 算法提升性能
  • ✨ 添加细胞分裂动画效果
  • 👆 多点触控交互
  • ⚡ 使用 GPU 加速渲染

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐