057ac1e4ea
withOpacity/activeColor/cacheExtent/PublicRoomsChunk durch die neuen Flutter-3.44-Äquivalente ersetzt.
191 lines
5.0 KiB
Dart
191 lines
5.0 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);
|
|
|
|
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<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;
|
|
}
|