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

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;
}