Harmony Flutter 跨平台开发实战:鸿蒙与音乐律动艺术、Voronoi 泰森多边形:空间分割的动态演化
给定平面上的 n 个点(称为站点或种子点)P = {p₁, p₂, …对于每个区域 V(pᵢ)其中 d(x, y) 表示两点间的欧几里得距离。关键概念概念定义说明📍站点种子点 pᵢVoronoi 区域的中心📐Voronoi 区域V(pᵢ)离该站点最近的点集📏Voronoi 边两区域边界到两站点等距的点⭐Voronoi 顶点多边交点到多个站点等距的点🔗Delaunay 三角剖分Voronoi

欢迎加入开源鸿蒙跨平台社区: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 加速渲染
更多推荐




所有评论(0)