Files
pyramid/lib/widgets/pyramid_loader.dart
T
Bernd Steckmeister 25ed765a03 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
2026-07-03 05:47:18 +02:00

203 lines
5.4 KiB
Dart

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