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 createState() => _PyramidLoaderState(); } class _PyramidLoaderState extends State 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 _rotY(List 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> _clipZ(List> poly) { final out = >[]; 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 v, double sc, double cx, double cy, double tr) => Offset(cx + v[0] * sc, cy + (v[1] * cos(tr) + v[2] * sin(tr)) * sc); Color _darken(Color c, double amount) => c.withValues( red: max(0.0, c.r * (1 - amount)), green: max(0.0, c.g * (1 - amount)), blue: max(0.0, c.b * (1 - amount)), ); @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 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; }