
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
🌀 一、双曲几何:数学之美
📚 1.1 非欧几何的诞生
双曲几何是非欧几何的重要分支,由高斯、波尔约和罗巴切夫斯基在19世纪初独立发现。它打破了欧几里得几何中"过直线外一点有且只有一条平行线"的公理。
平行公理的演变:
欧几里得几何(第五公设):
过直线外一点,有且只有一条直线与已知直线平行
双曲几何(罗巴切夫斯基):
过直线外一点,至少有两条直线与已知直线平行
椭圆几何(黎曼):
过直线外一点,没有直线与已知直线平行
三种几何的比较:
┌─────────────┬─────────────┬─────────────┐
│ 欧氏几何 │ 双曲几何 │ 椭圆几何 │
├─────────────┼─────────────┼─────────────┤
│ 平行线:1条 │ 平行线:∞条 │ 平行线:0条 │
│ 曲率:0 │ 曲率:< 0 │ 曲率:> 0 │
│ 三角形内角和│ 三角形内角和│ 三角形内角和│
│ = 180° │ < 180° │ > 180° │
└─────────────┴─────────────┴─────────────┘
历史背景:
公元前300年:欧几里得《几何原本》
1829年:罗巴切夫斯基发表双曲几何
1832年:波尔约独立发现双曲几何
1854年:黎曼提出黎曼几何
1868年:贝尔特拉米给出双曲几何模型
1882年:庞加莱提出圆盘模型
📐 1.2 庞加莱圆盘模型
庞加莱圆盘模型是双曲几何的一种直观表示方法,将无限的双曲平面映射到有限的单位圆盘内。
模型定义:
庞加莱圆盘:
D = {z ∈ ℂ : |z| < 1}
点:单位圆盘内的复数或点
直线:与边界圆正交的圆弧或直径
距离:双曲距离公式
双曲距离公式:
对于两点 z₁, z₂ ∈ D
d(z₁, z₂) = arccosh(1 + 2·|z₁ - z₂|² / ((1 - |z₁|²)(1 - |z₂|²)))
简化形式:
d(0, z) = arctanh(|z|) = ½ ln((1 + |z|) / (1 - |z|))
双曲直线:
庞加莱圆盘中的"直线":
1. 过圆心的直径(欧氏直线)
│
─────┼─────
│
2. 与边界正交的圆弧
╭───────╮
╱ ╲
│ ● │
╲ ╱
╰───────╯
特点:
- 所有双曲直线无限延伸
- 两点确定唯一双曲直线
- 双曲直线在边界上"相交于无穷远"
🔬 1.3 双曲几何的性质
三角形内角和:
双曲三角形内角和 < 180°
面积公式:
Area = π - (α + β + γ)
其中 α, β, γ 是三角形的三个内角
特点:
- 内角和越小,面积越大
- 最大面积趋近于 π
- "理想三角形":三个顶点都在边界上,内角和 = 0
理想三角形示意:
╭─────────╮
╱ ╲
│ │
│ │
╲ ╱
╰─────────╯
三个顶点在边界圆上
内角都是 0°
面积 = π
双曲圆:
双曲圆的定义:
到定点(圆心)距离相等的点的集合
性质:
- 双曲圆也是欧氏圆
- 但圆心位置不同
- 双曲圆心比欧氏圆心更靠近原点
示意:
╭─────╮
╱ ● ╲ ●:双曲圆心
│ ○ │ ○:欧氏圆心
╲ ╱
╰─────╯
极限圆:
极限圆(Horocycle):
当圆心趋向边界时的极限情况
性质:
- 通过边界上一点
- 与边界圆相切
- 是双曲几何特有的曲线
示意:
╭─────────╮
╱ ╲
│ │
╲ ● ╱ ●:边界点
╰─────────╯ 极限圆在此点与边界相切
🎯 1.4 双曲镶嵌
双曲平面可以进行无限多种正多边形镶嵌,这是双曲几何最迷人的特性之一。
正镶嵌分类:
正镶嵌 {p, q}:
p 边形,每个顶点连接 q 个多边形
欧氏镶嵌(曲率 = 0):
{3, 6}:三角形镶嵌
{4, 4}:正方形镶嵌
{6, 3}:六边形镶嵌
双曲镶嵌(曲率 < 0):
{5, 4}:五边形镶嵌
{7, 3}:七边形镶嵌
{8, 3}:八边形镶嵌
... 无限多种
判定条件:
(p - 2)(q - 2) < 4:球面镶嵌
(p - 2)(q - 2) = 4:欧氏镶嵌
(p - 2)(q - 2) > 4:双曲镶嵌
双曲镶嵌示意:
{7, 3} 镶嵌(七边形,每顶点3个):
╱╲
╱ ╲
╱ ╲
╱ ╲
╱ ╲
╱──────────╲
╱ ╲
每个七边形的内角 = 120°
七边形内角和 = 840° < 900°(欧氏七边形)
🔧 二、双曲几何的 Dart 实现
🧮 2.1 庞加莱圆盘模型
dart
import 'dart:math';
/// 复数类
class Complex {
final double real;
final double imag;
const Complex(this.real, this.imag);
factory Complex.fromPolar(double r, double theta) =>
Complex(r * cos(theta), r * sin(theta));
double get modulus => sqrt(real * real + imag * imag);
double get argument => atan2(imag, real);
Complex get conjugate => Complex(real, -imag);
Complex get inverse => conjugate / (modulus * modulus);
Complex operator +(Complex other) => Complex(real + other.real, imag + other.imag);
Complex operator -(Complex other) => Complex(real - other.real, imag - other.imag);
Complex operator *(Complex other) => Complex(real * other.real - imag * other.imag, real * other.imag + imag * other.real);
Complex operator /(Complex other) => this * other.inverse;
Offset toOffset() => Offset(real, imag);
}
/// 庞加莱圆盘模型
class PoincareDisk {
static const double radius = 1.0;
static bool isInside(Complex z) => z.modulus < radius;
static double distance(Complex z1, Complex z2) {
final diff = z1 - z2;
final numerator = diff.modulus * diff.modulus;
final denominator = (1 - z1.modulus * z1.modulus) * (1 - z2.modulus * z2.modulus);
if (denominator <= 0) return double.infinity;
return acosh(1 + 2 * numerator / denominator);
}
static double distanceFromOrigin(Complex z) {
return atanh(z.modulus);
}
static double acosh(double x) => log(x + sqrt(x * x - 1));
static double atanh(double x) => 0.5 * log((1 + x) / (1 - x));
static Complex mobiusAdd(Complex z, Complex w) {
final numerator = z + w;
final denominator = Complex(1, 0) + z.conjugate * w;
return numerator / denominator;
}
static Complex mobiusMultiply(Complex z, Complex w) {
return z * w;
}
static Complex translate(Complex z, Complex delta) {
return mobiusAdd(delta, z);
}
static Complex rotate(Complex z, double angle) {
return z * Complex.fromPolar(1, angle);
}
static Complex scale(Complex z, double factor) {
final dist = distanceFromOrigin(z);
final newDist = dist + factor;
final newMod = tanh(newDist);
return Complex.fromPolar(newMod, z.argument);
}
static double metricTensor(Complex z) {
final r = z.modulus;
return 4 / ((1 - r * r) * (1 - r * r));
}
static double getGeodesicCurvature(Complex z1, Complex z2) {
if (z1.modulus < 0.001 && z2.modulus < 0.001) {
return 0;
}
final mid = Complex((z1.real + z2.real) / 2, (z1.imag + z2.imag) / 2);
final chordLength = (z1 - z2).modulus;
if (mid.modulus < 0.001) {
return 0;
}
return chordLength / (2 * mid.modulus * sqrt(1 - mid.modulus * mid.modulus));
}
}
/// 双曲直线(测地线)
class HyperbolicLine {
final Complex p1;
final Complex p2;
HyperbolicLine(this.p1, this.p2);
bool get isDiameter {
final cross = p1.real * p2.imag - p1.imag * p2.real;
return cross.abs() < 1e-10;
}
List<Complex> getPoints(int count) {
if (isDiameter) {
return List.generate(count, (i) {
final t = i / (count - 1);
return Complex(p1.real + t * (p2.real - p1.real), p1.imag + t * (p2.imag - p1.imag));
});
}
final center = _computeCircleCenter();
final radius = (center - p1).modulus;
final angle1 = (p1 - center).argument;
final angle2 = (p2 - center).argument;
var deltaAngle = angle2 - angle1;
if (deltaAngle > pi) deltaAngle -= 2 * pi;
if (deltaAngle < -pi) deltaAngle += 2 * pi;
return List.generate(count, (i) {
final t = i / (count - 1);
final angle = angle1 + t * deltaAngle;
return center + Complex.fromPolar(radius, angle);
});
}
Complex _computeCircleCenter() {
final mid = Complex((p1.real + p2.real) / 2, (p1.imag + p2.imag) / 2);
final dx = p2.real - p1.real;
final dy = p2.imag - p1.imag;
final perpX = -dy;
final perpY = dx;
final t = (mid.real * mid.real + mid.imag * mid.imag - 1) / (2 * (mid.real * perpX + mid.imag * perpY));
return Complex(mid.real + t * perpX, mid.imag + t * perpY);
}
double get length => PoincareDisk.distance(p1, p2);
}
⚡ 2.2 双曲镶嵌生成
dart
/// 双曲镶嵌生成器
class HyperbolicTiling {
final int p;
final int q;
final int layers;
HyperbolicTiling({required this.p, required this.q, this.layers = 3});
bool get isHyperbolic => (p - 2) * (q - 2) > 4;
double get interiorAngle => 2 * pi / q;
double get centerToVertex => acosh(cos(pi / p) / sin(pi / q));
List<List<Complex>> generatePolygons() {
final polygons = <List<Complex>>[];
final visited = <String>{};
_generateLayer(Complex(0, 0), 0, polygons, visited);
return polygons;
}
void _generateLayer(
Complex center,
int currentLayer,
List<List<Complex>> polygons,
Set<String> visited,
) {
if (currentLayer > layers) return;
final key = '${center.real.toStringAsFixed(6)}_${center.imag.toStringAsFixed(6)}';
if (visited.contains(key)) return;
visited.add(key);
final polygon = _generatePolygon(center);
polygons.add(polygon);
if (currentLayer < layers) {
for (int i = 0; i < p; i++) {
final nextCenter = _computeNeighborCenter(center, i);
if (nextCenter.modulus < 0.99) {
_generateLayer(nextCenter, currentLayer + 1, polygons, visited);
}
}
}
}
List<Complex> _generatePolygon(Complex center) {
final vertices = <Complex>[];
final r = tanh(centerToVertex / 2);
for (int i = 0; i < p; i++) {
final angle = 2 * pi * i / p;
final localVertex = Complex.fromPolar(r, angle);
vertices.add(PoincareDisk.mobiusAdd(center, localVertex));
}
return vertices;
}
Complex _computeNeighborCenter(Complex center, int edgeIndex) {
final r = tanh(centerToVertex);
final angle = 2 * pi * edgeIndex / p + pi / p;
final direction = Complex.fromPolar(r, angle);
return PoincareDisk.mobiusAdd(center, direction);
}
}
/// 双曲三角形
class HyperbolicTriangle {
final Complex a;
final Complex b;
final Complex c;
HyperbolicTriangle(this.a, this.b, this.c);
double get angleA => _computeAngle(b, a, c);
double get angleB => _computeAngle(a, b, c);
double get angleC => _computeAngle(a, c, b);
double get angleSum => angleA + angleB + angleC;
double get area => pi - angleSum;
double get defect => pi - angleSum;
double _computeAngle(Complex p1, Complex vertex, Complex p2) {
final v1 = Complex(p1.real - vertex.real, p1.imag - vertex.imag);
final v2 = Complex(p2.real - vertex.real, p2.imag - vertex.imag);
final dot = v1.real * v2.real + v1.imag * v2.imag;
final cross = v1.real * v2.imag - v1.imag * v2.real;
return atan2(cross.abs(), dot);
}
List<Complex> getVertices() => [a, b, c];
List<HyperbolicLine> getEdges() => [
HyperbolicLine(a, b),
HyperbolicLine(b, c),
HyperbolicLine(c, a),
];
}
🎨 2.3 可视化组件
dart
import 'package:flutter/material.dart';
/// 双曲几何可视化绘制器
class HyperbolicPainter extends CustomPainter {
final List<List<Complex>> polygons;
final double scale;
final Offset center;
final ColorScheme colorScheme;
HyperbolicPainter({
required this.polygons,
required this.scale,
required this.center,
required this.colorScheme,
});
@override
void paint(Canvas canvas, Size size) {
canvas.drawRect(
Rect.fromLTWH(0, 0, size.width, size.height),
Paint()..color = const Color(0xFF0a0a15),
);
_drawBoundaryCircle(canvas, size);
for (int i = 0; i < polygons.length; i++) {
_drawPolygon(canvas, polygons[i], i);
}
}
void _drawBoundaryCircle(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.white.withOpacity(0.3)
..style = PaintingStyle.stroke
..strokeWidth = 2;
canvas.drawCircle(center, scale, paint);
}
void _drawPolygon(Canvas canvas, List<Complex> vertices, int index) {
if (vertices.isEmpty) return;
final path = Path();
final points = <Offset>[];
for (int i = 0; i < vertices.length; i++) {
final v = vertices[i];
final nextV = vertices[(i + 1) % vertices.length];
final line = HyperbolicLine(v, nextV);
final linePoints = line.getPoints(20);
for (final p in linePoints) {
points.add(Offset(center.dx + p.real * scale, center.dy + p.imag * scale));
}
}
if (points.isNotEmpty) {
path.moveTo(points[0].dx, points[0].dy);
for (int i = 1; i < points.length; i++) {
path.lineTo(points[i].dx, points[i].dy);
}
path.close();
}
final hue = (index * 137.5) % 360;
final fillPaint = Paint()
..color = HSVColor.fromAHSV(0.3, hue, 0.7, 0.9).toColor()
..style = PaintingStyle.fill;
final strokePaint = Paint()
..color = HSVColor.fromAHSV(1, hue, 0.8, 1).toColor()
..style = PaintingStyle.stroke
..strokeWidth = 1;
canvas.drawPath(path, fillPaint);
canvas.drawPath(path, strokePaint);
}
@override
bool shouldRepaint(covariant HyperbolicPainter old) =>
polygons.length != old.polygons.length;
}
📦 三、完整示例代码
以下是完整的双曲几何可视化示例代码:
dart
import 'package:flutter/material.dart';
import 'dart:math';
void main() {
runApp(const HyperbolicApp());
}
class HyperbolicApp extends StatelessWidget {
const HyperbolicApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '双曲几何',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple, brightness: Brightness.dark),
useMaterial3: true,
),
home: const HyperbolicHomePage(),
debugShowCheckedModeBanner: false,
);
}
}
class HyperbolicHomePage extends StatelessWidget {
const HyperbolicHomePage({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.deepPurple,
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const PoincareDiskDemo()))),
_buildCard(context, title: '双曲镶嵌', description: '正多边形镶嵌图案', icon: Icons.grid_on, color: Colors.indigo,
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const TilingDemo()))),
_buildCard(context, title: '测地线', description: '双曲直线与距离', icon: Icons.show_chart, color: Colors.purple,
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const GeodesicDemo()))),
_buildCard(context, title: '双曲三角形', description: '内角和与面积', icon: Icons.change_history, color: Colors.pink,
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const TriangleDemo()))),
],
),
);
}
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 C {
final double r, i;
const C(this.r, this.i);
factory C.polar(double mod, double arg) => C(mod * cos(arg), mod * sin(arg));
double get mod => sqrt(r * r + i * i);
double get arg => atan2(i, r);
C get conj => C(r, -i);
C operator +(C o) => C(r + o.r, i + o.i);
C operator -(C o) => C(r - o.r, i - o.i);
C operator *(C o) => C(r * o.r - i * o.i, r * o.i + i * o.r);
C operator /(C o) => this * C(o.r / (o.r*o.r + o.i*o.i), -o.i / (o.r*o.r + o.i*o.i));
Offset toOff(double s, Offset c) => Offset(c.dx + r * s, c.dy + i * s);
}
/// 庞加莱圆盘
double hDist(C z1, C z2) {
final d = (z1 - z2).mod;
final den = (1 - z1.mod * z1.mod) * (1 - z2.mod * z2.mod);
if (den <= 0) return double.infinity;
return acosh(1 + 2 * d * d / den);
}
double acosh(double x) => log(x + sqrt(x * x - 1));
double tanh(double x) => (exp(x) - exp(-x)) / (exp(x) + exp(-x));
double atanh(double x) => 0.5 * log((1 + x) / (1 - x));
C mobiusAdd(C z, C w) => (z + w) / (C(1, 0) + z.conj * w);
/// 庞加莱圆盘演示
class PoincareDiskDemo extends StatefulWidget {
const PoincareDiskDemo({super.key});
@override
State<PoincareDiskDemo> createState() => _PoincareDiskDemoState();
}
class _PoincareDiskDemoState extends State<PoincareDiskDemo> with SingleTickerProviderStateMixin {
late AnimationController _ctrl;
double _time = 0;
final List<C> _points = [];
double _rotation = 0;
@override
void initState() {
super.initState();
for (int i = 0; i < 12; i++) {
_points.add(C.polar(0.5, 2 * pi * i / 12));
}
_ctrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 16))..repeat();
_ctrl.addListener(_update);
}
void _update() { _time += 0.016; _rotation += 0.005; setState(() {}); }
@override
void dispose() { _ctrl.removeListener(_update); _ctrl.dispose(); super.dispose(); }
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('庞加莱圆盘')),
body: CustomPaint(painter: DiskPainter(_points, _rotation, _time), size: Size.infinite),
);
}
}
class DiskPainter extends CustomPainter {
final List<C> pts;
final double rot, t;
DiskPainter(this.pts, this.rot, this.t);
@override
void paint(Canvas canvas, Size size) {
final c = Offset(size.width / 2, size.height / 2);
final s = min(size.width, size.height) * 0.45;
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = const Color(0xFF0a0a15));
// 边界圆
canvas.drawCircle(c, s, Paint()..color = Colors.white.withOpacity(0.2)..style = PaintingStyle.stroke..strokeWidth = 2);
// 同心圆(双曲等距线)
for (int i = 1; i <= 5; i++) {
final r = tanh(i * 0.3);
canvas.drawCircle(c, r * s, Paint()..color = Colors.white.withOpacity(0.1)..style = PaintingStyle.stroke);
}
// 径向线(测地线)
for (int i = 0; i < 12; i++) {
final angle = rot + 2 * pi * i / 12;
canvas.drawLine(c, Offset(c.dx + s * cos(angle), c.dy + s * sin(angle)), Paint()..color = Colors.white.withOpacity(0.1));
}
// 点和连线
for (int i = 0; i < pts.length; i++) {
final p = pts[i];
final rotatedP = C.polar(p.mod, p.arg + rot);
final off = rotatedP.toOff(s, c);
canvas.drawCircle(off, 6, Paint()..color = HSVColor.fromAHSV(1, i * 30, 0.8, 1).toColor());
final next = pts[(i + 1) % pts.length];
final rotatedNext = C.polar(next.mod, next.arg + rot);
_drawGeodesic(canvas, c, s, rotatedP, rotatedNext, HSVColor.fromAHSV(0.5, i * 30, 0.8, 1).toColor());
}
// 距离标注
final d = hDist(pts[0], pts[1]);
final tp = TextPainter(text: TextSpan(text: '双曲距离: ${d.toStringAsFixed(2)}', style: const TextStyle(color: Colors.white70, fontSize: 12)), textDirection: TextDirection.ltr)..layout();
tp.paint(canvas, const Offset(10, 10));
}
void _drawGeodesic(Canvas canvas, Offset c, double s, C p1, C p2, Color color) {
final off1 = p1.toOff(s, c);
final off2 = p2.toOff(s, c);
final cross = p1.r * p2.i - p1.i * p2.r;
if (cross.abs() < 0.01) {
canvas.drawLine(off1, off2, Paint()..color = color..strokeWidth = 2);
return;
}
final mid = C((p1.r + p2.r) / 2, (p1.i + p2.i) / 2);
final dx = p2.r - p1.r, dy = p2.i - p1.i;
final px = -dy, py = dx;
final t = (mid.r * mid.r + mid.i * mid.i - 1) / (2 * (mid.r * px + mid.i * py));
final center = C(mid.r + t * px, mid.i + t * py);
final radius = (center - p1).mod;
final path = Path();
final angle1 = (p1 - center).arg;
var angle2 = (p2 - center).arg;
var delta = angle2 - angle1;
if (delta > pi) delta -= 2 * pi;
if (delta < -pi) delta += 2 * pi;
path.arcTo(Rect.fromCircle(center: center.toOff(s, c), radius: radius * s), angle1, delta, false);
canvas.drawPath(path, Paint()..color = color..style = PaintingStyle.stroke..strokeWidth = 2);
}
@override
bool shouldRepaint(covariant DiskPainter old) => true;
}
/// 双曲镶嵌演示
class TilingDemo extends StatefulWidget {
const TilingDemo({super.key});
@override
State<TilingDemo> createState() => _TilingDemoState();
}
class _TilingDemoState extends State<TilingDemo> {
int _p = 7, _q = 3, _layers = 3;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('双曲镶嵌')),
body: Column(children: [
Expanded(child: CustomPaint(painter: TilingPainter(_p, _q, _layers), size: Size.infinite)),
_buildControls(),
]),
);
}
Widget _buildControls() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(color: Colors.grey[900], borderRadius: const BorderRadius.vertical(top: Radius.circular(20))),
child: Column(mainAxisSize: MainAxisSize.min, children: [
Row(children: [
const Text('p (边数): ', style: TextStyle(color: Colors.white70)),
Expanded(child: Slider(value: _p.toDouble(), min: 3, max: 12, divisions: 9, onChanged: (v) => setState(() => _p = v.toInt()))),
Text('$_p', style: const TextStyle(color: Colors.indigo)),
]),
Row(children: [
const Text('q (连接数): ', style: TextStyle(color: Colors.white70)),
Expanded(child: Slider(value: _q.toDouble(), min: 3, max: 8, divisions: 5, onChanged: (v) => setState(() => _q = v.toInt()))),
Text('$_q', style: const TextStyle(color: Colors.indigo)),
]),
Row(children: [
const Text('层数: ', style: TextStyle(color: Colors.white70)),
Expanded(child: Slider(value: _layers.toDouble(), min: 1, max: 5, divisions: 4, onChanged: (v) => setState(() => _layers = v.toInt()))),
Text('$_layers', style: const TextStyle(color: Colors.indigo)),
]),
Text(_getTypeText(), style: TextStyle(color: _getTypeColor())),
]),
);
}
String _getTypeText() {
final val = (_p - 2) * (_q - 2);
if (val < 4) return '球面镶嵌';
if (val == 4) return '欧氏镶嵌';
return '双曲镶嵌 {$_p, $_q}';
}
Color _getTypeColor() {
final val = (_p - 2) * (_q - 2);
if (val < 4) return Colors.blue;
if (val == 4) return Colors.green;
return Colors.purple;
}
}
class TilingPainter extends CustomPainter {
final int p, q, layers;
TilingPainter(this.p, this.q, this.layers);
@override
void paint(Canvas canvas, Size size) {
final c = Offset(size.width / 2, size.height / 2);
final s = min(size.width, size.height) * 0.45;
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = const Color(0xFF0a0a15));
canvas.drawCircle(c, s, Paint()..color = Colors.white.withOpacity(0.2)..style = PaintingStyle.stroke..strokeWidth = 2);
final polygons = _generateTiling();
for (int i = 0; i < polygons.length; i++) {
_drawPolygon(canvas, c, s, polygons[i], i);
}
}
List<List<C>> _generateTiling() {
final polys = <List<C>>[];
final visited = <String>{};
_generateLayer(C(0, 0), 0, polys, visited);
return polys;
}
void _generateLayer(C center, int layer, List<List<C>> polys, Set<String> visited) {
if (layer > layers) return;
final key = '${center.r.toStringAsFixed(4)}_${center.i.toStringAsFixed(4)}';
if (visited.contains(key)) return;
visited.add(key);
final r = tanh(_centerToVertex() / 2);
final verts = <C>[];
for (int i = 0; i < p; i++) {
final local = C.polar(r, 2 * pi * i / p);
verts.add(mobiusAdd(center, local));
}
polys.add(verts);
if (layer < layers) {
for (int i = 0; i < p; i++) {
final nr = tanh(_centerToVertex());
final angle = 2 * pi * i / p + pi / p;
final next = mobiusAdd(center, C.polar(nr, angle));
if (next.mod < 0.95) {
_generateLayer(next, layer + 1, polys, visited);
}
}
}
}
double _centerToVertex() => acosh(cos(pi / p) / sin(pi / q));
void _drawPolygon(Canvas canvas, Offset c, double s, List<C> verts, int idx) {
final path = Path();
final points = <Offset>[];
for (int i = 0; i < verts.length; i++) {
final v = verts[i];
final nv = verts[(i + 1) % verts.length];
final linePts = _getGeodesicPoints(v, nv, 15);
for (final p in linePts) {
points.add(p.toOff(s, c));
}
}
if (points.isNotEmpty) {
path.moveTo(points[0].dx, points[0].dy);
for (int i = 1; i < points.length; i++) {
path.lineTo(points[i].dx, points[i].dy);
}
path.close();
}
final hue = (idx * 137.5) % 360;
canvas.drawPath(path, Paint()..color = HSVColor.fromAHSV(0.25, hue, 0.7, 0.9).toColor()..style = PaintingStyle.fill);
canvas.drawPath(path, Paint()..color = HSVColor.fromAHSV(1, hue, 0.8, 1).toColor()..style = PaintingStyle.stroke..strokeWidth = 1);
}
List<C> _getGeodesicPoints(C p1, C p2, int count) {
final cross = p1.r * p2.i - p1.i * p2.r;
if (cross.abs() < 0.01) {
return List.generate(count, (i) {
final t = i / (count - 1);
return C(p1.r + t * (p2.r - p1.r), p1.i + t * (p2.i - p1.i));
});
}
final mid = C((p1.r + p2.r) / 2, (p1.i + p2.i) / 2);
final dx = p2.r - p1.r, dy = p2.i - p1.i;
final px = -dy, py = dx;
final t = (mid.r * mid.r + mid.i * mid.i - 1) / (2 * (mid.r * px + mid.i * py));
final center = C(mid.r + t * px, mid.i + t * py);
final radius = (center - p1).mod;
final angle1 = (p1 - center).arg;
var angle2 = (p2 - center).arg;
var delta = angle2 - angle1;
if (delta > pi) delta -= 2 * pi;
if (delta < -pi) delta += 2 * pi;
return List.generate(count, (i) {
final angle = angle1 + delta * i / (count - 1);
return center + C.polar(radius, angle);
});
}
@override
bool shouldRepaint(covariant TilingPainter old) => p != old.p || q != old.q || layers != old.layers;
}
/// 测地线演示
class GeodesicDemo extends StatefulWidget {
const GeodesicDemo({super.key});
@override
State<GeodesicDemo> createState() => _GeodesicDemoState();
}
class _GeodesicDemoState extends State<GeodesicDemo> {
final List<C> _pts = [C(0.3, 0.2), C(-0.2, 0.4), C(0.1, -0.3)];
int _selectedIdx = -1;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('测地线')),
body: GestureDetector(
onPanStart: (d) {
final size = MediaQuery.of(context).size;
final c = Offset(size.width / 2, size.height / 2);
final s = min(size.width, size.height) * 0.45;
final x = (d.localPosition.dx - c.dx) / s;
final y = (d.localPosition.dy - c.dy) / s;
for (int i = 0; i < _pts.length; i++) {
if ((_pts[i].r - x).abs() < 0.05 && (_pts[i].i - y).abs() < 0.05) {
setState(() => _selectedIdx = i);
return;
}
}
if (x * x + y * y < 0.95) {
setState(() => _pts.add(C(x, y)));
}
},
onPanUpdate: (d) {
if (_selectedIdx >= 0) {
final size = MediaQuery.of(context).size;
final c = Offset(size.width / 2, size.height / 2);
final s = min(size.width, size.height) * 0.45;
final x = (d.localPosition.dx - c.dx) / s;
final y = (d.localPosition.dy - c.dy) / s;
if (x * x + y * y < 0.95) {
setState(() => _pts[_selectedIdx] = C(x, y));
}
}
},
onPanEnd: (_) => setState(() => _selectedIdx = -1),
child: CustomPaint(painter: GeodesicPainter(_pts), size: Size.infinite),
),
);
}
}
class GeodesicPainter extends CustomPainter {
final List<C> pts;
GeodesicPainter(this.pts);
@override
void paint(Canvas canvas, Size size) {
final c = Offset(size.width / 2, size.height / 2);
final s = min(size.width, size.height) * 0.45;
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = const Color(0xFF0a0a15));
canvas.drawCircle(c, s, Paint()..color = Colors.white.withOpacity(0.3)..style = PaintingStyle.stroke..strokeWidth = 2);
for (int i = 0; i < pts.length; i++) {
for (int j = i + 1; j < pts.length; j++) {
_drawGeodesic(canvas, c, s, pts[i], pts[j], Colors.purple.withOpacity(0.5));
final d = hDist(pts[i], pts[j]);
final mid = C((pts[i].r + pts[j].r) / 2, (pts[i].i + pts[j].i) / 2);
final tp = TextPainter(text: TextSpan(text: d.toStringAsFixed(2), style: const TextStyle(color: Colors.white54, fontSize: 10)), textDirection: TextDirection.ltr)..layout();
tp.paint(canvas, mid.toOff(s, c) - const Offset(10, 5));
}
}
for (int i = 0; i < pts.length; i++) {
canvas.drawCircle(pts[i].toOff(s, c), 8, Paint()..color = Colors.white);
canvas.drawCircle(pts[i].toOff(s, c), 5, Paint()..color = Colors.purple);
}
}
void _drawGeodesic(Canvas canvas, Offset c, double s, C p1, C p2, Color color) {
final cross = p1.r * p2.i - p1.i * p2.r;
if (cross.abs() < 0.01) {
canvas.drawLine(p1.toOff(s, c), p2.toOff(s, c), Paint()..color = color..strokeWidth = 2);
return;
}
final mid = C((p1.r + p2.r) / 2, (p1.i + p2.i) / 2);
final dx = p2.r - p1.r, dy = p2.i - p1.i;
final px = -dy, py = dx;
final t = (mid.r * mid.r + mid.i * mid.i - 1) / (2 * (mid.r * px + mid.i * py));
final center = C(mid.r + t * px, mid.i + t * py);
final radius = (center - p1).mod;
final path = Path();
final angle1 = (p1 - center).arg;
var angle2 = (p2 - center).arg;
var delta = angle2 - angle1;
if (delta > pi) delta -= 2 * pi;
if (delta < -pi) delta += 2 * pi;
path.arcTo(Rect.fromCircle(center: center.toOff(s, c), radius: radius * s), angle1, delta, false);
canvas.drawPath(path, Paint()..color = color..style = PaintingStyle.stroke..strokeWidth = 2);
}
@override
bool shouldRepaint(covariant GeodesicPainter old) => true;
}
/// 双曲三角形演示
class TriangleDemo extends StatefulWidget {
const TriangleDemo({super.key});
@override
State<TriangleDemo> createState() => _TriangleDemoState();
}
class _TriangleDemoState extends State<TriangleDemo> {
List<C> _pts = [C(0, 0), C(0.5, 0), C(0.25, 0.4)];
int _selected = -1;
@override
Widget build(BuildContext context) {
final angles = _computeAngles();
final sum = angles.reduce((a, b) => a + b);
final area = pi - sum;
return Scaffold(
appBar: AppBar(title: const Text('双曲三角形')),
body: GestureDetector(
onPanStart: (d) {
final size = MediaQuery.of(context).size;
final c = Offset(size.width / 2, size.height / 2);
final s = min(size.width, size.height) * 0.4;
final x = (d.localPosition.dx - c.dx) / s;
final y = (d.localPosition.dy - c.dy) / s;
for (int i = 0; i < _pts.length; i++) {
if ((_pts[i].r - x).abs() < 0.08 && (_pts[i].i - y).abs() < 0.08) {
setState(() => _selected = i);
return;
}
}
},
onPanUpdate: (d) {
if (_selected >= 0) {
final size = MediaQuery.of(context).size;
final c = Offset(size.width / 2, size.height / 2);
final s = min(size.width, size.height) * 0.4;
final x = (d.localPosition.dx - c.dx) / s;
final y = (d.localPosition.dy - c.dy) / s;
if (x * x + y * y < 0.95) {
setState(() => _pts[_selected] = C(x, y));
}
}
},
onPanEnd: (_) => setState(() => _selected = -1),
child: Column(children: [
Expanded(child: CustomPaint(painter: TrianglePainter(_pts), size: Size.infinite)),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(color: Colors.grey[900], borderRadius: const BorderRadius.vertical(top: Radius.circular(20))),
child: Column(children: [
Text('内角: α=${(angles[0] * 180 / pi).toStringAsFixed(1)}° β=${(angles[1] * 180 / pi).toStringAsFixed(1)}° γ=${(angles[2] * 180 / pi).toStringAsFixed(1)}°', style: const TextStyle(color: Colors.white70)),
Text('内角和: ${(sum * 180 / pi).toStringAsFixed(1)}° < 180°', style: const TextStyle(color: Colors.pink)),
Text('亏量: ${((pi - sum) * 180 / pi).toStringAsFixed(1)}° = 面积: ${area.toStringAsFixed(3)}', style: const TextStyle(color: Colors.pink)),
]),
),
]),
),
);
}
List<double> _computeAngles() {
final angles = <double>[];
for (int i = 0; i < 3; i++) {
final p1 = _pts[i];
final v = _pts[(i + 1) % 3];
final p2 = _pts[(i + 2) % 3];
final v1 = C(v.r - p1.r, v.i - p1.i);
final v2 = C(p2.r - p1.r, p2.i - p1.i);
final dot = v1.r * v2.r + v1.i * v2.i;
final cross = (v1.r * v2.i - v1.i * v2.r).abs();
angles.add(atan2(cross, dot));
}
return angles;
}
}
class TrianglePainter extends CustomPainter {
final List<C> pts;
TrianglePainter(this.pts);
@override
void paint(Canvas canvas, Size size) {
final c = Offset(size.width / 2, size.height / 2);
final s = min(size.width, size.height) * 0.4;
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = const Color(0xFF0a0a15));
canvas.drawCircle(c, s, Paint()..color = Colors.white.withOpacity(0.3)..style = PaintingStyle.stroke..strokeWidth = 2);
final path = Path();
for (int i = 0; i < 3; i++) {
final p1 = pts[i];
final p2 = pts[(i + 1) % 3];
final cross = p1.r * p2.i - p1.i * p2.r;
if (cross.abs() < 0.01) {
if (i == 0) path.moveTo(p1.toOff(s, c).dx, p1.toOff(s, c).dy);
path.lineTo(p2.toOff(s, c).dx, p2.toOff(s, c).dy);
} else {
final mid = C((p1.r + p2.r) / 2, (p1.i + p2.i) / 2);
final dx = p2.r - p1.r, dy = p2.i - p1.i;
final px = -dy, py = dx;
final t = (mid.r * mid.r + mid.i * mid.i - 1) / (2 * (mid.r * px + mid.i * py));
final center = C(mid.r + t * px, mid.i + t * py);
final radius = (center - p1).mod;
final angle1 = (p1 - center).arg;
var angle2 = (p2 - center).arg;
var delta = angle2 - angle1;
if (delta > pi) delta -= 2 * pi;
if (delta < -pi) delta += 2 * pi;
path.arcTo(Rect.fromCircle(center: center.toOff(s, c), radius: radius * s), angle1, delta, false);
}
}
path.close();
canvas.drawPath(path, Paint()..color = Colors.pink.withOpacity(0.3)..style = PaintingStyle.fill);
canvas.drawPath(path, Paint()..color = Colors.pink..style = PaintingStyle.stroke..strokeWidth = 2);
for (final p in pts) {
canvas.drawCircle(p.toOff(s, c), 8, Paint()..color = Colors.white);
}
}
@override
bool shouldRepaint(covariant TrianglePainter old) => true;
}
📝 四、数学原理深入解析
📐 4.1 双曲距离公式推导
从度量张量出发:
庞加莱圆盘的度量张量:
ds² = 4(dx² + dy²) / (1 - x² - y²)²
极坐标形式:
ds² = 4(dr² + r²dθ²) / (1 - r²)²
从原点到点 z 的距离:
d(0, z) = ∫₀^|z| 2dr / (1 - r²) = atanh(|z|)
两点间的距离:
d(z₁, z₂) = arccosh(1 + 2|z₁ - z₂|² / ((1 - |z₁|²)(1 - |z₂|²)))
推导:
利用莫比乌斯变换的不变性
将两点变换到原点和实轴上
🔄 4.2 莫比乌斯变换
莫比乌斯变换定义:
莫比乌斯变换:
f(z) = (az + b) / (cz + d)
其中 ad - bc ≠ 0
保持庞加莱圆盘的变换:
f(z) = e^(iθ) · (z - z₀) / (1 - z̄₀z)
性质:
- 保角:保持角度
- 保圆:将圆映射为圆
- 等距:保持双曲距离
- 可逆:有逆变换
变换类型:
1. 旋转:
f(z) = e^(iθ) · z
2. 平移:
f(z) = (z + a) / (1 + āz)
3. 缩放(沿测地线):
f(z) = e^t · z(在特定方向)
任何等距变换都可以表示为:
f(z) = e^(iθ) · (z - z₀) / (1 - z̄₀z)
🌸 4.3 高斯-博内定理
双曲几何中的高斯-博内定理:
对于双曲三角形:
∫∫_T K dA + ∫_∂T κ_g ds = 2π
其中:
- K = -1(双曲曲率)
- κ_g = 0(测地线曲率)
简化为:
-A + (π - α - β - γ) = 0
即:
Area = π - (α + β + γ)
三角形面积 = π - 内角和
推广到多边形:
对于 n 边形:
Area = (n - 2)π - 内角和
对于正 n 边形(内角 = α):
Area = (n - 2)π - nα
当 α → 0 时(理想多边形):
Area → (n - 2)π
🎯 4.4 双曲函数
双曲函数定义:
sinh(x) = (e^x - e^(-x)) / 2
cosh(x) = (e^x + e^(-x)) / 2
tanh(x) = sinh(x) / cosh(x)
反函数:
arsinh(x) = ln(x + √(x² + 1))
arcosh(x) = ln(x + √(x² - 1))
artanh(x) = ½ ln((1 + x) / (1 - x))
恒等式:
cosh²(x) - sinh²(x) = 1
在双曲几何中的应用:
双曲距离与 tanh 的关系:
r = tanh(d/2)
其中 r 是欧氏半径,d 是双曲距离
圆周长:
C = 2π sinh(r)
圆面积:
A = 2π (cosh(r) - 1) = 4π sinh²(r/2)
🔬 五、高级应用场景
🎨 5.1 艺术与设计
埃舍尔的双曲木刻:
dart
class EscherHyperbolic {
static List<List<C>> generatePattern(int p, int q, int layers) {
final tiling = HyperbolicTiling(p: p, q: q, layers: layers);
return tiling.generatePolygons();
}
static void applyFishPattern(Canvas canvas, List<C> polygon, int index) {
// 在多边形内绘制鱼形图案
}
}
🌐 5.2 数据可视化
双曲树布局:
dart
class HyperbolicTreeLayout {
final TreeNode root;
HyperbolicTreeLayout(this.root);
Map<TreeNode, C> computeLayout() {
final positions = <TreeNode, C>{};
_layoutSubtree(root, C(0, 0), 0, 2 * pi, positions);
return positions;
}
void _layoutSubtree(TreeNode node, C center, double startAngle, double endAngle, Map<TreeNode, C> positions) {
positions[node] = center;
final children = node.children;
if (children.isEmpty) return;
final angleStep = (endAngle - startAngle) / children.length;
final childDist = 0.3;
for (int i = 0; i < children.length; i++) {
final angle = startAngle + angleStep * (i + 0.5);
final childCenter = mobiusAdd(center, C.polar(childDist, angle));
_layoutSubtree(children[i], childCenter, angle - angleStep / 2, angle + angleStep / 2, positions);
}
}
}
📱 5.3 鸿蒙多端适配
性能优化配置:
dart
class HyperbolicConfig {
static int getMaxLayers(BuildContext context) {
final performance = DevicePerformance.getLevel();
switch (performance) {
case PerformanceLevel.high: return 5;
case PerformanceLevel.medium: return 3;
case PerformanceLevel.low: return 2;
}
}
}
📊 六、性能优化策略
⚡ 6.1 镶嵌缓存
预计算优化:
dart
class TilingCache {
static final Map<String, List<List<C>>> _cache = {};
static List<List<C>> getTiling(int p, int q, int layers) {
final key = '${p}_$q\_$layers';
return _cache.putIfAbsent(key, () {
final tiling = HyperbolicTiling(p: p, q: q, layers: layers);
return tiling.generatePolygons();
});
}
}
💾 6.2 层级渲染
渐进式绘制:
dart
class ProgressiveRenderer {
int _currentLayer = 0;
final int maxLayers;
ProgressiveRenderer(this.maxLayers);
List<List<C>> getNextBatch() {
if (_currentLayer >= maxLayers) return [];
_currentLayer++;
// 返回当前层的多边形
return [];
}
}
🎓 七、学习资源与拓展
📚 推荐阅读
| 主题 | 资源 | 难度 |
|---|---|---|
| 双曲几何 | 《非欧几何》 | ⭐⭐ |
| 庞加莱圆盘 | 《几何拓扑》 | ⭐⭐⭐ |
| 双曲镶嵌 | 《埃舍尔的几何艺术》 | ⭐⭐ |
| 黎曼几何 | 《黎曼几何引论》 | ⭐⭐⭐ |
🔗 相关项目
- HyperRogue:双曲几何游戏
- D. E. Shaw 的双曲浏览器:数据可视化
- M.C. Escher 作品集:艺术应用
📝 八、总结
本篇文章深入探讨了双曲几何与庞加莱圆盘模型的数学原理及其在 Flutter 中的可视化实现。
✅ 核心知识点回顾
| 知识点 | 说明 |
|---|---|
| 🌀 非欧几何 | 打破平行公理 |
| ⭕ 庞加莱圆盘 | 双曲平面的有限表示 |
| 📐 双曲距离 | atanh 公式 |
| 🔺 双曲三角形 | 内角和 < 180° |
| 🎨 双曲镶嵌 | 无限多种正镶嵌 |
⭐ 最佳实践要点
- ✅ 使用莫比乌斯变换进行坐标变换
- ✅ 注意边界附近的数值精度
- ✅ 缓存镶嵌计算结果
- ✅ 渐进式渲染提高性能
🚀 进阶方向
- 🔮 三维双曲空间
- ✨ 双曲网络可视化
- 📊 双曲机器学习
- 🎨 生成艺术设计
💡 提示:本文代码基于 Flutter for Harmony 开发,可在鸿蒙设备上流畅运行。