Files
iiEsaywebUIapp/lib/shared/widgets/ii_easy_loading_logo.dart

187 lines
6.0 KiB
Dart
Raw Normal View History

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