From 90884e905f0ede26815091d3fd588ec07de77095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=AD=90=E8=B4=A4?= Date: Fri, 30 Jan 2026 11:24:01 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E4=BC=AA3d=E7=90=83=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/components/sphere_cloud.dart | 164 +++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 lib/components/sphere_cloud.dart diff --git a/lib/components/sphere_cloud.dart b/lib/components/sphere_cloud.dart new file mode 100644 index 0000000..e50bbd8 --- /dev/null +++ b/lib/components/sphere_cloud.dart @@ -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 items; + final double radius; + final double itemSize; + final double cameraZ; + final double focal; + final double minScale; + final double maxScale; + + @override + State createState() => _SphereCloudState(); +} + +class _SphereCloudState extends State { + 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; +}