import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:stacked/stacked.dart'; import '../../about/internal/application/MeDescription.dart'; import '../../about/internal/application/NavigatorType.dart'; import '../../about/internal/application/TextType.dart'; import '../../configs/Navigator.dart'; import '../../designs/AmbientBackground.dart'; import '../../designs/GlassSurface.dart'; import '../../designs/GroundedMark.dart'; import '../../designs/Responsive.dart'; import '../../designs/scene/SceneArt.dart'; import '../../designs/text/Text.dart'; import '../../utils/Colors.dart'; import '../home/Home.dart'; import '../login/Login.dart'; import 'ConnectSplash.dart'; import 'Splash.dart'; import 'ViewSplash.dart'; class SplashState extends State implements ConnectSplash { ViewSplash? _model; @override Widget build(BuildContext context) { return ViewModelBuilder.reactive( viewModelBuilder: () => ViewSplash(context, this), onViewModelReady: (viewModel) { _model = viewModel; _initiate(); }, builder: (context, viewModel, child) => PopScope( canPop: false, child: Scaffold( backgroundColor: colorAmbientMid, body: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return Responsive( mobile: _mobileView(constraints), tablet: _mobileView(constraints), desktop: _mobileView(constraints), ); }, ), ), ), ); } Future _initiate() async { // Long enough for the mark to land and take one breath, then the session // decides where we land. await Future.delayed(const Duration(milliseconds: 2200)); _model?.initialize(); } Widget _mobileView(BoxConstraints constraints) { return AmbientBackground( child: Stack( children: [ // The landscape rising from the bottom edge — the same painted world // the routine cards live in. Positioned( left: 0, right: 0, bottom: 0, height: constraints.maxHeight * 0.34, child: ShaderMask( shaderCallback: (Rect bounds) => const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Color(0x00000000), Color(0xFF000000)], stops: [0.0, 0.45], ).createShader(bounds), blendMode: BlendMode.dstIn, child: const SceneArt(palette: sceneWorkout, seed: 11), ) .animate() .fadeIn(duration: 1200.ms, curve: Curves.easeOut) .slideY(begin: 0.25, end: 0, duration: 1400.ms, curve: Curves.easeOutCubic), ), SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(28, 18, 28, 32), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ text("Grounded", 17, TextType.Regular, color: colorInk), text("Daily discipline", 17, TextType.Regular, color: colorInkSoft), ], ).animate().fadeIn(duration: 600.ms, delay: 200.ms), const Spacer(flex: 3), Center(child: _mark()), const SizedBox(height: 40), _wordmark(), const Spacer(flex: 4), _loader(), ], ), ), ), ], ), ); } /// The mark on glass, inside rings that breathe outward. Widget _mark() { return SizedBox( width: 230, height: 230, child: Stack( alignment: Alignment.center, children: [ for (int ring = 0; ring < 3; ring++) Container( width: 120, height: 120, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: colorWhite.withValues(alpha: 0.9), width: 1.4), ), ) .animate(onPlay: (controller) => controller.repeat()) .scale( begin: const Offset(1, 1), end: const Offset(1.9, 1.9), duration: 2400.ms, delay: (ring * 800).ms, curve: Curves.easeOutCubic, ) .fadeOut(duration: 2400.ms, delay: (ring * 800).ms), GlassSurface( radius: BorderRadius.circular(44), padding: const EdgeInsets.all(10), child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(32), boxShadow: [ BoxShadow( color: colorPrimary.withValues(alpha: 0.35), blurRadius: 40, offset: const Offset(0, 16), ), ], ), // The sun rises out of the ground line, then the arcs light. child: TweenAnimationBuilder( tween: Tween(begin: 0, end: 1), duration: const Duration(milliseconds: 1700), builder: (context, t, child) => GroundedMark( size: 132, rise: Curves.easeOutCubic .transform((t / 0.6).clamp(0.0, 1.0)), arcs: Curves.easeOut .transform(((t - 0.45) / 0.55).clamp(0.0, 1.0)), ), ), ), ) .animate() .scale( begin: const Offset(0.6, 0.6), end: const Offset(1, 1), duration: 900.ms, curve: Curves.easeOutBack, ) .fadeIn(duration: 500.ms), ], ), ); } /// Two-tone, like the greeting on the home screen. Widget _wordmark() { return Column( children: [ text("Grounded", 46, TextType.Medium, color: colorInk, align: TextAlign.center, letterSpacing: -1.2) .animate() .fadeIn(duration: 700.ms, delay: 350.ms) .slideY(begin: 0.3, end: 0, duration: 700.ms, delay: 350.ms, curve: Curves.easeOutCubic), const SizedBox(height: 10), text("A to-do app that\ndoesn't believe you.", 22, TextType.Light, color: colorInkSoft, align: TextAlign.center, height: 1.3) .animate() .fadeIn(duration: 700.ms, delay: 650.ms) .slideY(begin: 0.3, end: 0, duration: 700.ms, delay: 650.ms, curve: Curves.easeOutCubic), ], ); } /// A glass capsule that fills while the session resolves. Widget _loader() { return Column( children: [ GlassSurface( radius: BorderRadius.circular(99), padding: const EdgeInsets.all(3), child: SizedBox( width: 140, height: 6, child: Align( alignment: Alignment.centerLeft, child: Container( width: 140, decoration: BoxDecoration( color: colorNight, borderRadius: BorderRadius.circular(99), ), ) .animate() .scaleX( begin: 0.04, end: 1, alignment: Alignment.centerLeft, duration: 2000.ms, curve: Curves.easeInOutCubic, ), ), ), ), const SizedBox(height: 12), text("Checking what you promised", 12, TextType.Regular, color: colorWhite.withValues(alpha: 0.9)), ], ).animate().fadeIn(duration: 500.ms, delay: 900.ms); } @override void launchHome(MeDescription value) { GroundedNavigation() .navigateToPage(NavigatorType.makeNewMain, const Home(), context); } @override void launchLogin() { GroundedNavigation() .navigateToPage(NavigatorType.makeNewMain, const Login(), context); } }