import 'dart:math' as math; import 'package:flutter/material.dart'; /// PAILot animated splash screen. /// /// Phase 1 (~0-1500ms): The P logo reveals itself progressively from top to /// bottom, as if being drawn by an invisible pen. /// /// Phase 2 (~1000-1700ms): A paper airplane flies in from the left, arcing /// across the P. /// /// After animation completes the [onComplete] callback fires. class SplashScreen extends StatefulWidget { final VoidCallback onComplete; const SplashScreen({super.key, required this.onComplete}); @override State createState() => _SplashScreenState(); } class _SplashScreenState extends State with TickerProviderStateMixin { late final AnimationController _drawController; late final AnimationController _planeController; late final AnimationController _fadeController; static const Duration _drawDuration = Duration(milliseconds: 1500); static const Duration _planeDuration = Duration(milliseconds: 700); static const Duration _fadeDuration = Duration(milliseconds: 300); @override void initState() { super.initState(); _drawController = AnimationController(vsync: this, duration: _drawDuration); _planeController = AnimationController(vsync: this, duration: _planeDuration); _fadeController = AnimationController(vsync: this, duration: _fadeDuration); _startSequence(); } Future _startSequence() async { await Future.delayed(const Duration(milliseconds: 50)); if (!mounted) return; _drawController.forward(); await Future.delayed(const Duration(milliseconds: 1000)); if (!mounted) return; _planeController.forward(); await Future.delayed(const Duration(milliseconds: 800)); if (!mounted) return; await _fadeController.forward(); if (!mounted) return; widget.onComplete(); } @override void dispose() { _drawController.dispose(); _planeController.dispose(); _fadeController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFF111217), body: FadeTransition( opacity: Tween(begin: 1.0, end: 0.0).animate( CurvedAnimation(parent: _fadeController, curve: Curves.easeIn), ), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ AnimatedBuilder( animation: Listenable.merge([_drawController, _planeController]), builder: (context, child) { return CustomPaint( size: const Size(300, 300), painter: _PLogoWithPlanePainter( drawProgress: _drawController.value, planeProgress: _planeController.value, ), ); }, ), const SizedBox(height: 24), AnimatedBuilder( animation: _drawController, builder: (context, child) { return Opacity( opacity: _drawController.value.clamp(0.0, 1.0), child: const Text( 'PAILot', style: TextStyle( color: Color(0xFF00C7FF), fontSize: 28, fontWeight: FontWeight.w300, letterSpacing: 6, ), ), ); }, ), ], ), ), ), ); } } class _PLogoWithPlanePainter extends CustomPainter { final double drawProgress; final double planeProgress; const _PLogoWithPlanePainter({ required this.drawProgress, required this.planeProgress, }); @override void paint(Canvas canvas, Size size) { const double svgW = 519.2; const double svgH = 519.3; final double scale = math.min(size.width / svgW, size.height / svgH); final double offsetX = (size.width - svgW * scale) / 2; final double offsetY = (size.height - svgH * scale) / 2; canvas.save(); canvas.translate(offsetX, offsetY); canvas.scale(scale, scale); _drawPLogo(canvas); _drawPlane(canvas); canvas.restore(); } void _drawPLogo(Canvas canvas) { if (drawProgress <= 0) return; final paths = _buildPPaths(); Rect bounds = paths.first.getBounds(); for (final p in paths.skip(1)) { bounds = bounds.expandToInclude(p.getBounds()); } final double revealY = bounds.top + bounds.height * Curves.easeInOut.transform(drawProgress); canvas.save(); canvas.clipRect(Rect.fromLTRB( bounds.left - 10, bounds.top - 10, bounds.right + 10, revealY + 6, )); final Paint gradientPaint = Paint() ..shader = const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFF0000FF), Color(0xFF00C7FF)], ).createShader(Rect.fromLTWH(0, 0, 519.2, 519.3)) ..style = PaintingStyle.fill ..isAntiAlias = true; for (final p in paths) { canvas.drawPath(p, gradientPaint); } canvas.restore(); } List _buildPPaths() { final path1 = Path(); path1.moveTo(149.4, 68.3); path1.lineTo(191.1, 155.4); path1.lineTo(300.6, 151.2); path1.cubicTo(301.3, 151.2, 302.0, 151.1, 302.7, 151.1); path1.cubicTo(324.8, 151.1, 343.0, 169.0, 343.0, 191.4); path1.cubicTo(343.0, 201.3, 339.4, 210.4, 333.5, 217.4); path1.cubicTo(366.8, 193.1, 389.3, 153.8, 389.3, 109.5); path1.cubicTo(389.3, 69.5, 371.7, 24.8, 343.9, 0.3); path1.cubicTo(339.9, 0.0, 335.8, -0.1, 331.7, -0.1); path1.lineTo(0, -0.1); path1.lineTo(0, 5.4); path1.cubicTo(0, 81.7, 59.8, 152.7, 134.9, 156.8); path1.lineTo(107.3, 68.3); path1.close(); final path2 = Path(); path2.moveTo(518.9, 175.6); path2.cubicTo(515.9, 128.6, 495.7, 86.3, 464.4, 55.0); path2.cubicTo(433.2, 23.8, 391.1, 3.6, 344.4, 0.5); path2.cubicTo(344.3, 0.5, 344.2, 0.6, 344.3, 0.7); path2.cubicTo(372.0, 25.2, 389.5, 69.8, 389.5, 109.6); path2.cubicTo(389.5, 151.9, 364.4, 195.1, 333.7, 217.5); path2.cubicTo(327.9, 224.3, 319.9, 229.2, 310.9, 231.0); path2.cubicTo(293.9, 238.9, 275.2, 243.4, 255.9, 243.4); path2.cubicTo(274.9, 243.4, 293.3, 239.1, 310.1, 231.4); path2.cubicTo(310.2, 231.3, 310.2, 231.1, 310.0, 231.2); path2.cubicTo(307.1, 231.7, 304.0, 231.9, 300.9, 231.8); path2.lineTo(191.5, 227.6); path2.lineTo(191.4, 227.7); path2.lineTo(149.7, 314.7); path2.lineTo(149.6, 314.8); path2.lineTo(107.7, 314.8); path2.cubicTo(107.6, 314.8, 107.6, 314.7, 107.6, 314.6); path2.lineTo(135.1, 226.2); path2.cubicTo(135.1, 226.1, 135.1, 226.0, 135.0, 226.0); path2.cubicTo(59.7, 230.2, 0, 283.6, 0, 359.9); path2.lineTo(0, 375.0); path2.lineTo(0, 516.6); path2.cubicTo(0, 516.7, 0.2, 516.8, 0.2, 516.6); path2.cubicTo(29.6, 429.6, 111.9, 374.7, 208.9, 374.7); path2.lineTo(298.2, 374.7); path2.lineTo(298.4, 375.0); path2.lineTo(329.8, 375.0); path2.cubicTo(439.7, 375.0, 525.7, 285.2, 518.9, 175.6); path2.close(); final path3 = Path(); path3.moveTo(208.9, 374.5); path3.cubicTo(111.7, 374.5, 29.2, 429.6, 0, 517.0); path3.lineTo(0, 519.2); path3.lineTo(158.7, 519.2); path3.lineTo(159.0, 417.6); path3.cubicTo(158.8, 393.9, 178.0, 374.6, 201.7, 374.6); path3.lineTo(298.4, 374.6); return [path1, path2, path3]; } void _drawPlane(Canvas canvas) { if (planeProgress <= 0) return; const double startX = -80.0, startY = 290.0; const double ctrlX = 260.0, ctrlY = 100.0; const double endX = 480.0, endY = 250.0; final double t = planeProgress; final double mt = 1.0 - t; final double px = mt * mt * startX + 2 * mt * t * ctrlX + t * t * endX; final double py = mt * mt * startY + 2 * mt * t * ctrlY + t * t * endY; final double dx = 2 * mt * (ctrlX - startX) + 2 * t * (endX - ctrlX); final double dy = 2 * mt * (ctrlY - startY) + 2 * t * (endY - ctrlY); final double angle = math.atan2(dy, dx); double alpha = 1.0; if (t < 0.15) { alpha = t / 0.15; } else if (t > 0.9) { alpha = (1.0 - t) / 0.1; } canvas.save(); canvas.translate(px, py); canvas.rotate(angle); const double s = 30.0; final Paint bodyPaint = Paint() ..color = Color.fromRGBO(0, 199, 255, alpha) ..style = PaintingStyle.fill ..isAntiAlias = true; final Paint edgePaint = Paint() ..color = Color.fromRGBO(255, 255, 255, alpha * 0.6) ..style = PaintingStyle.stroke ..strokeWidth = 1.5 ..isAntiAlias = true; final Path plane = Path() ..moveTo(s, 0) ..lineTo(-s * 0.6, -s * 0.55) ..lineTo(-s * 0.2, 0) ..lineTo(-s * 0.6, s * 0.55) ..close(); canvas.drawPath(plane, bodyPaint); canvas.drawPath(plane, edgePaint); canvas.drawLine( Offset(s, 0), Offset(-s * 0.2, 0), Paint() ..color = Color.fromRGBO(255, 255, 255, alpha * 0.4) ..strokeWidth = 1.0, ); canvas.restore(); } @override bool shouldRepaint(_PLogoWithPlanePainter old) => old.drawProgress != drawProgress || old.planeProgress != planeProgress; }