187 lines
6.0 KiB
Dart
187 lines
6.0 KiB
Dart
|
|
import 'dart:math' as math;
|
||
|
|
import 'package:flutter/material.dart';
|
||
|
|
import 'package:conduit/l10n/app_localizations.dart';
|
||
|
|
|
||
|
|
/// Animated iiEasy logo: 3 rotating dashed rings, exactly as in the reference HTML.
|
||
|
|
/// - Outer: rotate 0° → 360°, 10s
|
||
|
|
/// - Middle: rotate 30° → -330°, 12s
|
||
|
|
/// - Inner: rotate 60° → 420°, 8s
|
||
|
|
class IiEasyLoadingLogo extends StatefulWidget {
|
||
|
|
final bool isDark;
|
||
|
|
|
||
|
|
const IiEasyLoadingLogo({super.key, required this.isDark});
|
||
|
|
|
||
|
|
@override
|
||
|
|
State<IiEasyLoadingLogo> createState() => _IiEasyLoadingLogoState();
|
||
|
|
}
|
||
|
|
|
||
|
|
class _IiEasyLoadingLogoState extends State<IiEasyLoadingLogo>
|
||
|
|
with TickerProviderStateMixin {
|
||
|
|
late AnimationController _outerController;
|
||
|
|
late AnimationController _middleController;
|
||
|
|
late AnimationController _innerController;
|
||
|
|
|
||
|
|
@override
|
||
|
|
void initState() {
|
||
|
|
super.initState();
|
||
|
|
_outerController = AnimationController(
|
||
|
|
vsync: this,
|
||
|
|
duration: const Duration(seconds: 10),
|
||
|
|
)..repeat();
|
||
|
|
|
||
|
|
_middleController = AnimationController(
|
||
|
|
vsync: this,
|
||
|
|
duration: const Duration(seconds: 12),
|
||
|
|
)..repeat();
|
||
|
|
|
||
|
|
_innerController = AnimationController(
|
||
|
|
vsync: this,
|
||
|
|
duration: const Duration(seconds: 8),
|
||
|
|
)..repeat();
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
void dispose() {
|
||
|
|
_outerController.dispose();
|
||
|
|
_middleController.dispose();
|
||
|
|
_innerController.dispose();
|
||
|
|
super.dispose();
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
final strokeColor = widget.isDark ? Colors.white : const Color(0xFF1F2937);
|
||
|
|
final textColor = widget.isDark ? Colors.white : const Color(0xFF1F2937);
|
||
|
|
final sloganColor = widget.isDark ? Colors.grey.shade300 : Colors.grey.shade700;
|
||
|
|
|
||
|
|
return ColoredBox(
|
||
|
|
color: widget.isDark ? const Color(0xFF000000) : Colors.white,
|
||
|
|
child: SafeArea(
|
||
|
|
child: Center(
|
||
|
|
child: Column(
|
||
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
||
|
|
children: [
|
||
|
|
SizedBox(
|
||
|
|
width: 200,
|
||
|
|
height: 200,
|
||
|
|
child: AnimatedBuilder(
|
||
|
|
animation: Listenable.merge([
|
||
|
|
_outerController,
|
||
|
|
_middleController,
|
||
|
|
_innerController,
|
||
|
|
]),
|
||
|
|
builder: (context, _) {
|
||
|
|
return CustomPaint(
|
||
|
|
painter: _RingsPainter(
|
||
|
|
strokeColor: strokeColor,
|
||
|
|
outerTurns: _outerController.value,
|
||
|
|
middleTurns: _middleController.value,
|
||
|
|
innerTurns: _innerController.value,
|
||
|
|
),
|
||
|
|
size: const Size(200, 200),
|
||
|
|
);
|
||
|
|
},
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(height: 32),
|
||
|
|
Text(
|
||
|
|
'iiEasy',
|
||
|
|
style: TextStyle(
|
||
|
|
fontSize: 48,
|
||
|
|
fontWeight: FontWeight.bold,
|
||
|
|
color: textColor,
|
||
|
|
letterSpacing: -1,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(height: 8),
|
||
|
|
Text(
|
||
|
|
AppLocalizations.of(context)?.appSlogan ?? 'Future. Simple.',
|
||
|
|
style: TextStyle(
|
||
|
|
fontSize: 20,
|
||
|
|
fontWeight: FontWeight.w500,
|
||
|
|
color: sloganColor,
|
||
|
|
letterSpacing: 0.5,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Draws 3 dashed circles with rotation exactly as in HTML.
|
||
|
|
/// viewBox 0 0 200 200, center (100, 100).
|
||
|
|
class _RingsPainter extends CustomPainter {
|
||
|
|
_RingsPainter({
|
||
|
|
required this.strokeColor,
|
||
|
|
required this.outerTurns,
|
||
|
|
required this.middleTurns,
|
||
|
|
required this.innerTurns,
|
||
|
|
});
|
||
|
|
|
||
|
|
final Color strokeColor;
|
||
|
|
final double outerTurns; // 0..1 → 0°..360°
|
||
|
|
final double middleTurns; // 0..1 → 30°..-330° (so use 30/360 + (1-t)*2 - 1?)
|
||
|
|
final double innerTurns; // 0..1 → 60°..420°
|
||
|
|
|
||
|
|
static const double cx = 100;
|
||
|
|
static const double cy = 100;
|
||
|
|
|
||
|
|
@override
|
||
|
|
void paint(Canvas canvas, Size size) {
|
||
|
|
final scale = size.width / 200;
|
||
|
|
canvas.save();
|
||
|
|
canvas.scale(scale);
|
||
|
|
|
||
|
|
final paint = Paint()
|
||
|
|
..color = strokeColor
|
||
|
|
..style = PaintingStyle.stroke
|
||
|
|
..strokeWidth = 6
|
||
|
|
..strokeCap = StrokeCap.butt;
|
||
|
|
|
||
|
|
// Outer: 0° → 360° in 10s. angle = outerTurns * 2*pi
|
||
|
|
_drawDashedCircle(canvas, paint, cx, cy, 70, [15, 85], outerTurns * 2 * math.pi);
|
||
|
|
|
||
|
|
// Middle: 30° → -330° in 12s. So angle = 30° + (1 - middleTurns) * 360° going backwards
|
||
|
|
// 30° = 30/360 * 2pi, -330° = -330/360 * 2pi = 30/360 * 2pi - 2pi
|
||
|
|
final middleAngle = (30 / 360) * 2 * math.pi - middleTurns * 2 * math.pi;
|
||
|
|
_drawDashedCircle(canvas, paint, cx, cy, 50, [12, 58], middleAngle);
|
||
|
|
|
||
|
|
// Inner: 60° → 420° in 8s. angle = 60° + innerTurns * 360°
|
||
|
|
final innerAngle = (60 / 360) * 2 * math.pi + innerTurns * 2 * math.pi;
|
||
|
|
_drawDashedCircle(canvas, paint, cx, cy, 30, [8, 32], innerAngle);
|
||
|
|
|
||
|
|
canvas.restore();
|
||
|
|
}
|
||
|
|
|
||
|
|
void _drawDashedCircle(Canvas canvas, Paint paint, double cx, double cy,
|
||
|
|
double r, List<double> dashArray, double startAngleRad) {
|
||
|
|
const twoPi = 2 * math.pi;
|
||
|
|
// SVG stroke-dasharray: dash and gap in user units along the path.
|
||
|
|
// Arc length = r * angle, so angle = length / r.
|
||
|
|
final dashAngle = dashArray[0] / r;
|
||
|
|
final gapAngle = dashArray[1] / r;
|
||
|
|
final periodAngle = dashAngle + gapAngle;
|
||
|
|
double angle = startAngleRad;
|
||
|
|
final rect = Rect.fromCircle(center: Offset(cx, cy), radius: r);
|
||
|
|
while (angle < startAngleRad + twoPi) {
|
||
|
|
final sweep = math.min(dashAngle, startAngleRad + twoPi - angle);
|
||
|
|
if (sweep > 0.001) {
|
||
|
|
final path = Path()..arcTo(rect, angle, sweep, false);
|
||
|
|
canvas.drawPath(path, paint);
|
||
|
|
}
|
||
|
|
angle += periodAngle;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
bool shouldRepaint(_RingsPainter oldDelegate) {
|
||
|
|
return oldDelegate.outerTurns != outerTurns ||
|
||
|
|
oldDelegate.middleTurns != middleTurns ||
|
||
|
|
oldDelegate.innerTurns != innerTurns ||
|
||
|
|
oldDelegate.strokeColor != strokeColor;
|
||
|
|
}
|
||
|
|
}
|