You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
164 lines
4.3 KiB
164 lines
4.3 KiB
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;
|
|
}
|