wip: Sicherungs-Commit aller Änderungen seit April + Arbeitsstruktur (CLAUDE.md, ROADMAP.md, PROGRESS.md, Autopilot)
6 Wochen uncommittete Arbeit (Voice-Channels, LiveKit-Manager, Settings-Modal u.v.m.) als ein WIP-Commit gesichert, damit nichts verloren geht und der Pi den aktuellen Stand klonen kann. Thematische Aufarbeitung: siehe ROADMAP M0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CPrAGBxBT6GfPXzeWQ4AXb
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
import 'package:pyramid/core/theme.dart';
|
||||
|
||||
class PyramidLoader extends StatefulWidget {
|
||||
final double size;
|
||||
final bool useLottie;
|
||||
|
||||
const PyramidLoader({super.key, this.size = 60, this.useLottie = false});
|
||||
|
||||
@override
|
||||
State<PyramidLoader> createState() => _PyramidLoaderState();
|
||||
}
|
||||
|
||||
class _PyramidLoaderState extends State<PyramidLoader>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _ctrl;
|
||||
|
||||
// Mirror the JS constants exactly
|
||||
static const _hold = 0.2;
|
||||
static const _curveExp = 3.0;
|
||||
static const _tiltDeg = 20.0;
|
||||
|
||||
double _ease(double t, double exp) =>
|
||||
t < 0.5 ? 0.5 * pow(2 * t, exp) : 1 - 0.5 * pow(2 * (1 - t), exp);
|
||||
|
||||
double _yawAt(double t) {
|
||||
final a = 1.0 - _hold;
|
||||
if (t >= a) return pi / 4;
|
||||
return _ease(t / a, _curveExp) * 2 * pi + pi / 4;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_ctrl = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 2000),
|
||||
)..repeat();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ctrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Color _accent(BuildContext ctx) {
|
||||
try {
|
||||
return PyramidTheme.of(ctx).accent;
|
||||
} catch (_) {
|
||||
return const Color(0xFFF5A524);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final accent = _accent(context);
|
||||
|
||||
final painter = AnimatedBuilder(
|
||||
animation: _ctrl,
|
||||
builder: (_, __) => CustomPaint(
|
||||
painter: _PyramidPainter(
|
||||
yaw: _yawAt(_ctrl.value),
|
||||
accent: accent,
|
||||
tiltDeg: _tiltDeg,
|
||||
),
|
||||
size: Size(widget.size, widget.size),
|
||||
),
|
||||
);
|
||||
|
||||
if (widget.useLottie) {
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
width: widget.size,
|
||||
height: widget.size,
|
||||
child: ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(accent, BlendMode.srcIn),
|
||||
child: Lottie.asset(
|
||||
'assets/pyramid-loader.json',
|
||||
fit: BoxFit.contain,
|
||||
repeat: true,
|
||||
animate: true,
|
||||
errorBuilder: (_, __, ___) => painter,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Center(child: painter);
|
||||
}
|
||||
}
|
||||
|
||||
class _PyramidPainter extends CustomPainter {
|
||||
final double yaw;
|
||||
final Color accent;
|
||||
final double tiltDeg;
|
||||
|
||||
const _PyramidPainter({
|
||||
required this.yaw,
|
||||
required this.accent,
|
||||
required this.tiltDeg,
|
||||
});
|
||||
|
||||
List<double> _rotY(List<double> v, double a) {
|
||||
final c = cos(a), s = sin(a);
|
||||
return [v[0] * c + v[2] * s, v[1], -v[0] * s + v[2] * c];
|
||||
}
|
||||
|
||||
List<List<double>> _clipZ(List<List<double>> poly) {
|
||||
final out = <List<double>>[];
|
||||
for (var i = 0; i < poly.length; i++) {
|
||||
final a = poly[i];
|
||||
final b = poly[(i + 1) % poly.length];
|
||||
final ai = a[2] >= 0, bi = b[2] >= 0;
|
||||
if (ai) out.add(a);
|
||||
if (ai != bi) {
|
||||
final t = a[2] / (a[2] - b[2]);
|
||||
out.add([a[0] + t * (b[0] - a[0]), a[1] + t * (b[1] - a[1]), 0.0]);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Offset _proj(List<double> v, double sc, double cx, double cy, double tr) =>
|
||||
Offset(cx + v[0] * sc, cy + (v[1] * cos(tr) + v[2] * sin(tr)) * sc);
|
||||
|
||||
double _frontness(List<List<double>> verts) {
|
||||
final a = verts[0], b = verts[1], c = verts[2];
|
||||
final ux = b[0]-a[0], uy = b[1]-a[1], uz = b[2]-a[2];
|
||||
final vx = c[0]-a[0], vy = c[1]-a[1], vz = c[2]-a[2];
|
||||
final nz = ux * vy - uy * vx;
|
||||
final nLen = sqrt(
|
||||
pow(uy * vz - uz * vy, 2) + pow(uz * vx - ux * vz, 2) + nz * nz,
|
||||
);
|
||||
return nLen == 0 ? 0 : max(0.0, -nz / nLen);
|
||||
}
|
||||
|
||||
Color _darken(Color c, double amount) => Color.fromARGB(
|
||||
c.alpha,
|
||||
max(0, (c.red * (1 - amount)).round()),
|
||||
max(0, (c.green * (1 - amount)).round()),
|
||||
max(0, (c.blue * (1 - amount)).round()),
|
||||
);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final sc = size.width * 0.38;
|
||||
final cx = size.width / 2;
|
||||
final cy = size.height / 2;
|
||||
final tr = -tiltDeg * pi / 180;
|
||||
|
||||
const base = 0.7;
|
||||
final faces = [
|
||||
[[0.0, -1.0, 0.0], [base, 1.0, base], [base, 1.0, -base]],
|
||||
[[0.0, -1.0, 0.0], [base, 1.0, -base], [-base, 1.0, -base]],
|
||||
[[0.0, -1.0, 0.0], [-base, 1.0, -base],[-base, 1.0, base]],
|
||||
[[0.0, -1.0, 0.0], [-base, 1.0, base], [base, 1.0, base]],
|
||||
];
|
||||
|
||||
final items = <({List<Offset> pts, double depth})>[];
|
||||
|
||||
for (final face in faces) {
|
||||
final rot = face.map((v) => _rotY(v, yaw)).toList();
|
||||
final cl = _clipZ(rot);
|
||||
if (cl.length < 3) continue;
|
||||
final depth = cl.fold(0.0, (s, v) => s + v[2]) / cl.length;
|
||||
items.add((
|
||||
pts: cl.map((v) => _proj(v, sc, cx, cy, tr)).toList(),
|
||||
depth: depth,
|
||||
));
|
||||
}
|
||||
|
||||
items.sort((a, b) => a.depth.compareTo(b.depth));
|
||||
|
||||
final fillPaint = Paint()
|
||||
..color = _darken(accent, 0.45)
|
||||
..style = PaintingStyle.fill;
|
||||
final strokePaint = Paint()
|
||||
..color = accent
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1.4 * (size.width / 96)
|
||||
..strokeJoin = StrokeJoin.round
|
||||
..strokeCap = StrokeCap.round;
|
||||
|
||||
for (final item in items) {
|
||||
final path = Path()..moveTo(item.pts[0].dx, item.pts[0].dy);
|
||||
for (var i = 1; i < item.pts.length; i++) {
|
||||
path.lineTo(item.pts[i].dx, item.pts[i].dy);
|
||||
}
|
||||
path.close();
|
||||
canvas.drawPath(path, fillPaint);
|
||||
canvas.drawPath(path, strokePaint);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_PyramidPainter old) =>
|
||||
old.yaw != yaw || old.accent != accent;
|
||||
}
|
||||
Reference in New Issue
Block a user