1 changed files with 164 additions and 0 deletions
Unified View
Diff Options
@ -0,0 +1,164 @@ |
|||||
|
import 'dart:math' as math; |
||||
|
import 'package:flutter/material.dart'; |
||||
|
|
||||
|
class SphereCloud extends StatefulWidget { |
||||
|
const SphereCloud({ |
||||
|
super.key, |
||||
|
required this.items, |
||||
|
this.radius = 140, |
||||
|
this.itemSize = 48, |
||||
|
this.cameraZ = 400, // 相机离球心距离,越大透视越弱 |
||||
|
this.focal = 240, // 焦距,影响投影与缩放 |
||||
|
this.minScale = 0.45, |
||||
|
this.maxScale = 1.25, |
||||
|
}); |
||||
|
|
||||
|
final List<Widget> items; |
||||
|
final double radius; |
||||
|
final double itemSize; |
||||
|
final double cameraZ; |
||||
|
final double focal; |
||||
|
final double minScale; |
||||
|
final double maxScale; |
||||
|
|
||||
|
@override |
||||
|
State<SphereCloud> createState() => _SphereCloudState(); |
||||
|
} |
||||
|
|
||||
|
class _SphereCloudState extends State<SphereCloud> { |
||||
|
late final List<_Point3> _points; |
||||
|
|
||||
|
// 当前旋转角(弧度) |
||||
|
double _rotX = 0.0; |
||||
|
double _rotY = 0.0; |
||||
|
|
||||
|
@override |
||||
|
void initState() { |
||||
|
super.initState(); |
||||
|
_points = _genFibonacciSphere(widget.items.length, widget.radius); |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
void didUpdateWidget(covariant SphereCloud oldWidget) { |
||||
|
super.didUpdateWidget(oldWidget); |
||||
|
if (oldWidget.items.length != widget.items.length || |
||||
|
oldWidget.radius != widget.radius) { |
||||
|
_points |
||||
|
..clear() |
||||
|
..addAll(_genFibonacciSphere(widget.items.length, widget.radius)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
return LayoutBuilder(builder: (_, c) { |
||||
|
final cx = c.maxWidth / 2; |
||||
|
final cy = c.maxHeight / 2; |
||||
|
|
||||
|
// 计算每个点投影后的 2D 信息 |
||||
|
final projected = <_ProjectedItem>[]; |
||||
|
for (int i = 0; i < _points.length; i++) { |
||||
|
final p = _rotate(_points[i], _rotX, _rotY); |
||||
|
|
||||
|
// 透视:避免 cameraZ - z 过小导致爆炸 |
||||
|
final denom = (widget.cameraZ - p.z).clamp(40.0, 1e9); |
||||
|
final perspective = widget.focal / denom; |
||||
|
|
||||
|
final x2 = cx + p.x * perspective; |
||||
|
final y2 = cy + p.y * perspective; |
||||
|
|
||||
|
final scale = perspective.clamp(widget.minScale, widget.maxScale); |
||||
|
|
||||
|
projected.add(_ProjectedItem( |
||||
|
index: i, |
||||
|
x: x2, |
||||
|
y: y2, |
||||
|
z: p.z, |
||||
|
scale: scale, |
||||
|
)); |
||||
|
} |
||||
|
|
||||
|
// 按 z 排序:远->近 |
||||
|
projected.sort((a, b) => a.z.compareTo(b.z)); |
||||
|
|
||||
|
return GestureDetector( |
||||
|
onPanUpdate: (d) { |
||||
|
// 你可以按手感调整系数 |
||||
|
setState(() { |
||||
|
_rotY += d.delta.dx * 0.01; |
||||
|
_rotX -= d.delta.dy * 0.01; |
||||
|
}); |
||||
|
}, |
||||
|
child: Stack( |
||||
|
clipBehavior: Clip.none, |
||||
|
children: [ |
||||
|
for (final it in projected) |
||||
|
Positioned( |
||||
|
left: it.x - widget.itemSize / 2, |
||||
|
top: it.y - widget.itemSize / 2, |
||||
|
child: Transform.scale( |
||||
|
scale: it.scale, |
||||
|
child: SizedBox( |
||||
|
width: widget.itemSize, |
||||
|
height: widget.itemSize, |
||||
|
child: widget.items[it.index], |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// Fibonacci sphere:简单、分布均匀(比随机更像“均匀铺满球面”) |
||||
|
List<_Point3> _genFibonacciSphere(int n, double r) { |
||||
|
if (n <= 0) return []; |
||||
|
final pts = <_Point3>[]; |
||||
|
final offset = 2.0 / n; |
||||
|
final increment = math.pi * (3.0 - math.sqrt(5.0)); // golden angle |
||||
|
for (int i = 0; i < n; i++) { |
||||
|
final y = ((i * offset) - 1) + (offset / 2); |
||||
|
final rr = math.sqrt(1 - y * y); |
||||
|
final phi = i * increment; |
||||
|
final x = math.cos(phi) * rr; |
||||
|
final z = math.sin(phi) * rr; |
||||
|
pts.add(_Point3(x * r, y * r, z * r)); |
||||
|
} |
||||
|
return pts; |
||||
|
} |
||||
|
|
||||
|
// 绕 X/Y 旋转(右手系) |
||||
|
_Point3 _rotate(_Point3 p, double rx, double ry) { |
||||
|
// rotate around X |
||||
|
final cosX = math.cos(rx), sinX = math.sin(rx); |
||||
|
var y1 = p.y * cosX - p.z * sinX; |
||||
|
var z1 = p.y * sinX + p.z * cosX; |
||||
|
|
||||
|
// rotate around Y |
||||
|
final cosY = math.cos(ry), sinY = math.sin(ry); |
||||
|
var x2 = p.x * cosY + z1 * sinY; |
||||
|
var z2 = -p.x * sinY + z1 * cosY; |
||||
|
|
||||
|
return _Point3(x2, y1, z2); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
class _Point3 { |
||||
|
_Point3(this.x, this.y, this.z); |
||||
|
final double x, y, z; |
||||
|
} |
||||
|
|
||||
|
class _ProjectedItem { |
||||
|
_ProjectedItem({ |
||||
|
required this.index, |
||||
|
required this.x, |
||||
|
required this.y, |
||||
|
required this.z, |
||||
|
required this.scale, |
||||
|
}); |
||||
|
|
||||
|
final int index; |
||||
|
final double x, y, z; |
||||
|
final double scale; |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save