Animate the splash and fix the web preview hanging on the logo
The splash now raises the sun in the mark and over a painted valley, floats four feature chips around the mark, shimmers the wordmark, shows a three-beat "how it works" card, drifts light motes upward and walks a status line through the session check. A tap skips it. The web build hung on the native splash because the asset manifest could not be published: "assets/icons/splash_orb.png" is 27 characters, so its length prefix in AssetManifest.bin is an ESC byte. Renamed it to splash_mark.png and removed the unused anchor assets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KhQh614DMeYoQEH9SoMRUw
This commit is contained in:
@@ -1,17 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
|
|
||||||
<!-- Grounded mark: a horizon line with a weight resting on it. The bar is
|
|
||||||
the ground; the block is what you are carrying. -->
|
|
||||||
<rect width="512" height="512" fill="#141414"/>
|
|
||||||
|
|
||||||
<!-- The block -->
|
|
||||||
<rect x="176" y="146" width="160" height="160" rx="34" fill="#EDEEEA"/>
|
|
||||||
|
|
||||||
<!-- The notch — a debt taken out of the block -->
|
|
||||||
<rect x="236" y="146" width="40" height="86" rx="20" fill="#141414"/>
|
|
||||||
|
|
||||||
<!-- The ground line -->
|
|
||||||
<rect x="120" y="348" width="272" height="26" rx="13" fill="#2F6F4E"/>
|
|
||||||
|
|
||||||
<!-- Shadow beneath, shortened: the weight sits on the line -->
|
|
||||||
<rect x="168" y="396" width="176" height="14" rx="7" fill="#EDEEEA" opacity="0.22"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 760 B |
Binary file not shown.
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
@@ -26,6 +26,9 @@ class SceneArt extends StatelessWidget {
|
|||||||
/// Darkens the lower half so white type laid over it stays legible.
|
/// Darkens the lower half so white type laid over it stays legible.
|
||||||
final bool shade;
|
final bool shade;
|
||||||
|
|
||||||
|
/// Lowers the sun behind the ridges; animate it for a sunrise.
|
||||||
|
final double sunLift;
|
||||||
|
|
||||||
const SceneArt({
|
const SceneArt({
|
||||||
super.key,
|
super.key,
|
||||||
required this.palette,
|
required this.palette,
|
||||||
@@ -34,6 +37,7 @@ class SceneArt extends StatelessWidget {
|
|||||||
this.figureScale = 0.5,
|
this.figureScale = 0.5,
|
||||||
this.figureAlignment = const Alignment(0.35, 0.15),
|
this.figureAlignment = const Alignment(0.35, 0.15),
|
||||||
this.shade = false,
|
this.shade = false,
|
||||||
|
this.sunLift = 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -50,7 +54,8 @@ class SceneArt extends StatelessWidget {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
CustomPaint(painter: ScenePainter(palette, seed)),
|
CustomPaint(
|
||||||
|
painter: ScenePainter(palette, seed, sunLift: sunLift)),
|
||||||
if (figure != null)
|
if (figure != null)
|
||||||
Align(
|
Align(
|
||||||
alignment: figureAlignment,
|
alignment: figureAlignment,
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ class ScenePainter extends CustomPainter {
|
|||||||
|
|
||||||
final int seed;
|
final int seed;
|
||||||
|
|
||||||
ScenePainter(this.palette, this.seed);
|
/// 1 → the sun sits in its place; 0 → it is down behind the ridges.
|
||||||
|
final double sunLift;
|
||||||
|
|
||||||
|
ScenePainter(this.palette, this.seed, {this.sunLift = 1});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void paint(Canvas canvas, Size size) {
|
void paint(Canvas canvas, Size size) {
|
||||||
@@ -36,7 +39,7 @@ class ScenePainter extends CustomPainter {
|
|||||||
final double radius = math.min(w, h) * (0.13 + random.nextDouble() * 0.05);
|
final double radius = math.min(w, h) * (0.13 + random.nextDouble() * 0.05);
|
||||||
final Offset sun = Offset(
|
final Offset sun = Offset(
|
||||||
w * (0.18 + random.nextDouble() * 0.64),
|
w * (0.18 + random.nextDouble() * 0.64),
|
||||||
h * (0.26 + random.nextDouble() * 0.12),
|
h * (0.26 + random.nextDouble() * 0.12) + h * 0.5 * (1 - sunLift),
|
||||||
);
|
);
|
||||||
canvas.drawCircle(
|
canvas.drawCircle(
|
||||||
sun,
|
sun,
|
||||||
@@ -109,6 +112,8 @@ class ScenePainter extends CustomPainter {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool shouldRepaint(covariant ScenePainter oldDelegate) {
|
bool shouldRepaint(covariant ScenePainter oldDelegate) {
|
||||||
return oldDelegate.seed != seed || oldDelegate.palette != palette;
|
return oldDelegate.seed != seed ||
|
||||||
|
oldDelegate.palette != palette ||
|
||||||
|
oldDelegate.sunLift != sunLift;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
|
|
||||||
import '../../about/internal/application/MeDescription.dart';
|
import '../../about/internal/application/MeDescription.dart';
|
||||||
import '../../about/internal/application/NavigatorType.dart';
|
import '../../about/internal/application/NavigatorType.dart';
|
||||||
|
import '../../about/internal/application/RoutineKind.dart';
|
||||||
import '../../about/internal/application/TextType.dart';
|
import '../../about/internal/application/TextType.dart';
|
||||||
import '../../configs/Navigator.dart';
|
import '../../configs/Navigator.dart';
|
||||||
import '../../designs/AmbientBackground.dart';
|
import '../../designs/AmbientBackground.dart';
|
||||||
@@ -15,6 +17,7 @@ import '../../designs/Responsive.dart';
|
|||||||
import '../../designs/scene/SceneArt.dart';
|
import '../../designs/scene/SceneArt.dart';
|
||||||
import '../../designs/text/Text.dart';
|
import '../../designs/text/Text.dart';
|
||||||
import '../../utils/Colors.dart';
|
import '../../utils/Colors.dart';
|
||||||
|
import '../../utils/CommonUtils.dart';
|
||||||
import '../home/Home.dart';
|
import '../home/Home.dart';
|
||||||
import '../login/Login.dart';
|
import '../login/Login.dart';
|
||||||
import 'ConnectSplash.dart';
|
import 'ConnectSplash.dart';
|
||||||
@@ -24,6 +27,28 @@ import 'ViewSplash.dart';
|
|||||||
class SplashState extends State<Splash> implements ConnectSplash {
|
class SplashState extends State<Splash> implements ConnectSplash {
|
||||||
ViewSplash? _model;
|
ViewSplash? _model;
|
||||||
|
|
||||||
|
Timer? _statusTimer;
|
||||||
|
|
||||||
|
int _status = 0;
|
||||||
|
|
||||||
|
/// Set once the session check has been asked for, so a tap and the timer
|
||||||
|
/// never both fire it.
|
||||||
|
bool _launched = false;
|
||||||
|
|
||||||
|
/// What the loader says while it fills — the app's three jobs, in order.
|
||||||
|
static const List<String> _statuses = <String>[
|
||||||
|
"Loading your plan",
|
||||||
|
"Checking what's overdue",
|
||||||
|
"Lining up today's routines",
|
||||||
|
"Ready",
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_statusTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ViewModelBuilder<ViewSplash>.reactive(
|
return ViewModelBuilder<ViewSplash>.reactive(
|
||||||
@@ -51,136 +76,329 @@ class SplashState extends State<Splash> implements ConnectSplash {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initiate() async {
|
Future<void> _initiate() async {
|
||||||
// Long enough for the mark to land and take one breath, then the session
|
_statusTimer = Timer.periodic(
|
||||||
// decides where we land.
|
const Duration(milliseconds: 1000), _onStatusTick);
|
||||||
await Future.delayed(const Duration(milliseconds: 2200));
|
|
||||||
|
// Long enough to watch the sun come up and read the screen once; a tap
|
||||||
|
// skips straight on.
|
||||||
|
await Future.delayed(const Duration(milliseconds: 4200));
|
||||||
|
_launch();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onStatusTick(Timer timer) {
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_status = (_status + 1).clamp(0, _statuses.length - 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSkip() {
|
||||||
|
_launch();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _launch() {
|
||||||
|
if (_launched || !mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_launched = true;
|
||||||
|
_statusTimer?.cancel();
|
||||||
_model?.initialize();
|
_model?.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _greeting() {
|
||||||
|
final int hour = DateTime.now().hour;
|
||||||
|
if (hour < 5) {
|
||||||
|
return "Still up";
|
||||||
|
}
|
||||||
|
if (hour < 12) {
|
||||||
|
return "Good morning";
|
||||||
|
}
|
||||||
|
if (hour < 18) {
|
||||||
|
return "Good afternoon";
|
||||||
|
}
|
||||||
|
return "Good evening";
|
||||||
|
}
|
||||||
|
|
||||||
Widget _mobileView(BoxConstraints constraints) {
|
Widget _mobileView(BoxConstraints constraints) {
|
||||||
return AmbientBackground(
|
return GestureDetector(
|
||||||
child: Stack(
|
onTap: _onSkip,
|
||||||
children: [
|
behavior: HitTestBehavior.opaque,
|
||||||
// The landscape rising from the bottom edge — the same painted world
|
child: AmbientBackground(
|
||||||
// the routine cards live in.
|
child: Stack(
|
||||||
Positioned(
|
children: [
|
||||||
left: 0,
|
_landscape(constraints),
|
||||||
right: 0,
|
..._motes(constraints),
|
||||||
bottom: 0,
|
SafeArea(
|
||||||
height: constraints.maxHeight * 0.34,
|
child: Padding(
|
||||||
child: ShaderMask(
|
padding: const EdgeInsets.fromLTRB(24, 14, 24, 22),
|
||||||
shaderCallback: (Rect bounds) => const LinearGradient(
|
child: Column(
|
||||||
begin: Alignment.topCenter,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
end: Alignment.bottomCenter,
|
children: [
|
||||||
colors: <Color>[Color(0x00000000), Color(0xFF000000)],
|
_topRow(),
|
||||||
stops: <double>[0.0, 0.45],
|
const Spacer(flex: 2),
|
||||||
).createShader(bounds),
|
_hero(),
|
||||||
blendMode: BlendMode.dstIn,
|
const SizedBox(height: 26),
|
||||||
child: const SceneArt(palette: sceneWorkout, seed: 11),
|
_wordmark(),
|
||||||
)
|
const Spacer(flex: 2),
|
||||||
.animate()
|
_howItWorks(),
|
||||||
.fadeIn(duration: 1200.ms, curve: Curves.easeOut)
|
const Spacer(flex: 1),
|
||||||
.slideY(begin: 0.25, end: 0, duration: 1400.ms,
|
_loader(),
|
||||||
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.
|
/// The painted valley rising from the bottom edge while its sun comes up.
|
||||||
Widget _mark() {
|
Widget _landscape(BoxConstraints constraints) {
|
||||||
|
return Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
height: constraints.maxHeight * 0.42,
|
||||||
|
child: ShaderMask(
|
||||||
|
shaderCallback: (Rect bounds) => const LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: <Color>[Color(0x00000000), Color(0xFF000000)],
|
||||||
|
stops: <double>[0.0, 0.4],
|
||||||
|
).createShader(bounds),
|
||||||
|
blendMode: BlendMode.dstIn,
|
||||||
|
child: TweenAnimationBuilder<double>(
|
||||||
|
tween: Tween<double>(begin: 0, end: 1),
|
||||||
|
duration: const Duration(milliseconds: 3200),
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
builder: (context, lift, child) => SceneArt(
|
||||||
|
palette: sceneWorkout,
|
||||||
|
seed: 11,
|
||||||
|
sunLift: lift,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.animate()
|
||||||
|
.fadeIn(duration: 900.ms)
|
||||||
|
.slideY(begin: 0.3, end: 0, duration: 1400.ms,
|
||||||
|
curve: Curves.easeOutCubic),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Specks of light drifting up out of the valley.
|
||||||
|
List<Widget> _motes(BoxConstraints constraints) {
|
||||||
|
const List<double> xs = <double>[0.12, 0.27, 0.41, 0.58, 0.7, 0.84, 0.93];
|
||||||
|
|
||||||
|
return List<Widget>.generate(xs.length, (index) {
|
||||||
|
final double size = 4.0 + (index % 3) * 2;
|
||||||
|
|
||||||
|
return Positioned(
|
||||||
|
left: constraints.maxWidth * xs[index],
|
||||||
|
bottom: constraints.maxHeight * (0.08 + (index % 4) * 0.05),
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: Container(
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: colorWhite,
|
||||||
|
boxShadow: <BoxShadow>[
|
||||||
|
BoxShadow(
|
||||||
|
color: colorWhite.withValues(alpha: 0.8), blurRadius: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.animate(onPlay: (controller) => controller.repeat())
|
||||||
|
.fadeIn(duration: 600.ms, delay: (index * 260).ms)
|
||||||
|
.moveY(
|
||||||
|
begin: 0,
|
||||||
|
end: -140 - index * 12.0,
|
||||||
|
duration: (2600 + index * 240).ms,
|
||||||
|
delay: (index * 260).ms,
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
)
|
||||||
|
.fadeOut(
|
||||||
|
duration: 900.ms, delay: (2000 + index * 240).ms),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _topRow() {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const GroundedMark(size: 26),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
text("Grounded", 16, TextType.Medium, color: colorInk),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
GlassSurface(
|
||||||
|
radius: BorderRadius.circular(99),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(CupertinoIcons.sun_max_fill,
|
||||||
|
size: 13, color: colorStandingWarned),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
text(_greeting(), 12, TextType.Medium, color: colorInk),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).animate().fadeIn(duration: 600.ms, delay: 150.ms).slideY(
|
||||||
|
begin: -0.4, end: 0, duration: 600.ms, delay: 150.ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The mark in breathing rings, with the four things the app holds you to
|
||||||
|
/// floating around it.
|
||||||
|
Widget _hero() {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: 230,
|
height: 270,
|
||||||
height: 230,
|
|
||||||
child: Stack(
|
child: Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
for (int ring = 0; ring < 3; ring++)
|
for (int ring = 0; ring < 3; ring++)
|
||||||
Container(
|
Container(
|
||||||
width: 120,
|
width: 140,
|
||||||
height: 120,
|
height: 140,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: colorWhite.withValues(alpha: 0.9), width: 1.4),
|
color: colorWhite.withValues(alpha: 0.95), width: 1.4),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.animate(onPlay: (controller) => controller.repeat())
|
.animate(onPlay: (controller) => controller.repeat())
|
||||||
.scale(
|
.scale(
|
||||||
begin: const Offset(1, 1),
|
begin: const Offset(1, 1),
|
||||||
end: const Offset(1.9, 1.9),
|
end: const Offset(1.85, 1.85),
|
||||||
duration: 2400.ms,
|
duration: 2700.ms,
|
||||||
delay: (ring * 800).ms,
|
delay: (ring * 900).ms,
|
||||||
curve: Curves.easeOutCubic,
|
curve: Curves.easeOutCubic,
|
||||||
)
|
)
|
||||||
.fadeOut(duration: 2400.ms, delay: (ring * 800).ms),
|
.fadeOut(duration: 2700.ms, delay: (ring * 900).ms),
|
||||||
GlassSurface(
|
_mark(),
|
||||||
radius: BorderRadius.circular(44),
|
_chip(const Alignment(-1, -1), sceneWorkout,
|
||||||
padding: const EdgeInsets.all(10),
|
kindIcon(RoutineKind.Workout), "Workout routines", 0),
|
||||||
child: Container(
|
_chip(const Alignment(1, -0.62), sceneMeditation,
|
||||||
decoration: BoxDecoration(
|
kindIcon(RoutineKind.Meditation), "Breathing sessions", 1),
|
||||||
borderRadius: BorderRadius.circular(32),
|
_chip(const Alignment(-1, 0.62), sceneMustDo,
|
||||||
boxShadow: <BoxShadow>[
|
kindIcon(RoutineKind.MustDo), "Must-do chains", 2),
|
||||||
BoxShadow(
|
_chip(const Alignment(1, 1), goalScene(goalAccents[4]),
|
||||||
color: colorPrimary.withValues(alpha: 0.35),
|
CupertinoIcons.flag_fill, "Goals with deadlines", 3),
|
||||||
blurRadius: 40,
|
|
||||||
offset: const Offset(0, 16),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
// The sun rises out of the ground line, then the arcs light.
|
|
||||||
child: TweenAnimationBuilder<double>(
|
|
||||||
tween: Tween<double>(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 _mark() {
|
||||||
|
return GlassSurface(
|
||||||
|
radius: BorderRadius.circular(46),
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(34),
|
||||||
|
boxShadow: <BoxShadow>[
|
||||||
|
BoxShadow(
|
||||||
|
color: colorPrimary.withValues(alpha: 0.4),
|
||||||
|
blurRadius: 40,
|
||||||
|
offset: const Offset(0, 16),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
// The sun rises out of the ground line, then the arcs light.
|
||||||
|
child: TweenAnimationBuilder<double>(
|
||||||
|
tween: Tween<double>(begin: 0, end: 1),
|
||||||
|
duration: const Duration(milliseconds: 1800),
|
||||||
|
builder: (context, t, child) => GroundedMark(
|
||||||
|
size: 128,
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A frosted chip with a painted thumbnail. It drifts in, then bobs.
|
||||||
|
Widget _chip(Alignment alignment, List<Color> palette, IconData icon,
|
||||||
|
String label, int index) {
|
||||||
|
return Align(
|
||||||
|
alignment: alignment,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(99),
|
||||||
|
boxShadow: const [
|
||||||
|
BoxShadow(
|
||||||
|
color: Color(0x142A3A30),
|
||||||
|
blurRadius: 16,
|
||||||
|
offset: Offset(0, 6),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: GlassSurface(
|
||||||
|
radius: BorderRadius.circular(99),
|
||||||
|
tint: colorGlassStrong,
|
||||||
|
padding: const EdgeInsets.fromLTRB(4, 4, 12, 4),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
child: ClipOval(
|
||||||
|
child: SceneArt(
|
||||||
|
palette: palette,
|
||||||
|
seed: 5 + index * 9,
|
||||||
|
figure: icon,
|
||||||
|
figureScale: 0.52,
|
||||||
|
figureAlignment: const Alignment(0, 0.2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
text(label, 12, TextType.Medium, color: colorInk),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.animate()
|
||||||
|
.fadeIn(duration: 500.ms, delay: (900 + index * 180).ms)
|
||||||
|
.slideX(
|
||||||
|
begin: alignment.x < 0 ? -0.3 : 0.3,
|
||||||
|
end: 0,
|
||||||
|
duration: 600.ms,
|
||||||
|
delay: (900 + index * 180).ms,
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
)
|
||||||
|
.then()
|
||||||
|
.animate(onPlay: (controller) => controller.repeat(reverse: true))
|
||||||
|
.moveY(
|
||||||
|
begin: -3,
|
||||||
|
end: 3,
|
||||||
|
duration: (1800 + index * 260).ms,
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Two tones, like the greeting on the home screen; the name catches the
|
||||||
|
/// light once as it lands.
|
||||||
Widget _wordmark() {
|
Widget _wordmark() {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -189,9 +407,11 @@ class SplashState extends State<Splash> implements ConnectSplash {
|
|||||||
.animate()
|
.animate()
|
||||||
.fadeIn(duration: 700.ms, delay: 350.ms)
|
.fadeIn(duration: 700.ms, delay: 350.ms)
|
||||||
.slideY(begin: 0.3, end: 0, duration: 700.ms, delay: 350.ms,
|
.slideY(begin: 0.3, end: 0, duration: 700.ms, delay: 350.ms,
|
||||||
curve: Curves.easeOutCubic),
|
curve: Curves.easeOutCubic)
|
||||||
const SizedBox(height: 10),
|
.then(delay: 300.ms)
|
||||||
text("A to-do app that\ndoesn't believe you.", 22, TextType.Light,
|
.shimmer(duration: 1200.ms, color: colorLime),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
text("A to-do app that\ndoesn't believe you.", 21, TextType.Regular,
|
||||||
color: colorInkSoft, align: TextAlign.center, height: 1.3)
|
color: colorInkSoft, align: TextAlign.center, height: 1.3)
|
||||||
.animate()
|
.animate()
|
||||||
.fadeIn(duration: 700.ms, delay: 650.ms)
|
.fadeIn(duration: 700.ms, delay: 650.ms)
|
||||||
@@ -201,7 +421,80 @@ class SplashState extends State<Splash> implements ConnectSplash {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A glass capsule that fills while the session resolves.
|
/// The whole idea in three beats, on glass over the valley.
|
||||||
|
Widget _howItWorks() {
|
||||||
|
return GlassSurface(
|
||||||
|
radius: BorderRadius.circular(24),
|
||||||
|
tint: colorGlassStrong,
|
||||||
|
padding: const EdgeInsets.fromLTRB(14, 14, 14, 14),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _beat(CupertinoIcons.hand_raised_fill, "Commit",
|
||||||
|
"Say it and set a window", 0)),
|
||||||
|
_beatArrow(0),
|
||||||
|
Expanded(
|
||||||
|
child: _beat(CupertinoIcons.timer, "Do it",
|
||||||
|
"Inside the window, with proof", 1)),
|
||||||
|
_beatArrow(1),
|
||||||
|
Expanded(
|
||||||
|
child: _beat(CupertinoIcons.exclamationmark_triangle_fill,
|
||||||
|
"Or owe it", "Misses turn into debt", 2)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).animate().fadeIn(duration: 600.ms, delay: 1500.ms).slideY(
|
||||||
|
begin: 0.25, end: 0, duration: 700.ms, delay: 1500.ms,
|
||||||
|
curve: Curves.easeOutCubic);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _beat(IconData icon, String title, String caption, int index) {
|
||||||
|
final List<Color> tones = <Color>[
|
||||||
|
colorPrimary,
|
||||||
|
colorKindMeditation,
|
||||||
|
colorStandingGrounded,
|
||||||
|
];
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: tones[index].withValues(alpha: 0.12),
|
||||||
|
),
|
||||||
|
child: Icon(icon, size: 16, color: tones[index]),
|
||||||
|
)
|
||||||
|
.animate()
|
||||||
|
.scale(
|
||||||
|
begin: const Offset(0.4, 0.4),
|
||||||
|
end: const Offset(1, 1),
|
||||||
|
duration: 500.ms,
|
||||||
|
delay: (1700 + index * 250).ms,
|
||||||
|
curve: Curves.easeOutBack,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
text(title, 13, TextType.Medium, color: colorInk),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
text(caption, 10, TextType.Regular,
|
||||||
|
color: colorInkSoft, align: TextAlign.center, maxLines: 2),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _beatArrow(int index) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 12),
|
||||||
|
child: Icon(CupertinoIcons.chevron_right, size: 12, color: colorInkSoft)
|
||||||
|
.animate()
|
||||||
|
.fadeIn(duration: 400.ms, delay: (1900 + index * 250).ms),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A glass capsule that fills while the session resolves, and a status
|
||||||
|
/// line that walks through what is being checked.
|
||||||
Widget _loader() {
|
Widget _loader() {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -209,33 +502,50 @@ class SplashState extends State<Splash> implements ConnectSplash {
|
|||||||
radius: BorderRadius.circular(99),
|
radius: BorderRadius.circular(99),
|
||||||
padding: const EdgeInsets.all(3),
|
padding: const EdgeInsets.all(3),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 140,
|
width: 150,
|
||||||
height: 6,
|
height: 6,
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 140,
|
width: 150,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: colorNight,
|
color: colorNight,
|
||||||
borderRadius: BorderRadius.circular(99),
|
borderRadius: BorderRadius.circular(99),
|
||||||
),
|
),
|
||||||
)
|
).animate().scaleX(
|
||||||
.animate()
|
|
||||||
.scaleX(
|
|
||||||
begin: 0.04,
|
begin: 0.04,
|
||||||
end: 1,
|
end: 1,
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
duration: 2000.ms,
|
duration: 4000.ms,
|
||||||
curve: Curves.easeInOutCubic,
|
curve: Curves.easeInOutCubic,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 10),
|
||||||
text("Checking what you promised", 12, TextType.Regular,
|
AnimatedSwitcher(
|
||||||
color: colorWhite.withValues(alpha: 0.9)),
|
duration: const Duration(milliseconds: 300),
|
||||||
|
transitionBuilder: (child, animation) => FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: SlideTransition(
|
||||||
|
position: Tween<Offset>(
|
||||||
|
begin: const Offset(0, 0.4),
|
||||||
|
end: Offset.zero,
|
||||||
|
).animate(animation),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: KeyedSubtree(
|
||||||
|
key: ValueKey<int>(_status),
|
||||||
|
child: text(_statuses[_status], 12, TextType.Medium,
|
||||||
|
color: colorWhite),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
text("Tap anywhere to skip", 10, TextType.Regular,
|
||||||
|
color: colorWhite.withValues(alpha: 0.7)),
|
||||||
],
|
],
|
||||||
).animate().fadeIn(duration: 500.ms, delay: 900.ms);
|
).animate().fadeIn(duration: 500.ms, delay: 600.ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const String imagePath = "assets/images";
|
|||||||
const String iconPath = "assets/icons";
|
const String iconPath = "assets/icons";
|
||||||
|
|
||||||
/// The mark on a night disc, as the native splash shows it.
|
/// The mark on a night disc, as the native splash shows it.
|
||||||
const String splashMark = "$iconPath/splash_orb.png";
|
const String splashMark = "$iconPath/splash_mark.png";
|
||||||
|
|
||||||
const String logoMark = "$iconPath/icon.png";
|
const String logoMark = "$iconPath/icon.png";
|
||||||
|
|
||||||
|
|||||||
@@ -122,10 +122,10 @@ flutter_launcher_icons:
|
|||||||
# the OS screen to the app is invisible.
|
# the OS screen to the app is invisible.
|
||||||
flutter_native_splash:
|
flutter_native_splash:
|
||||||
color: "#EAEFE6"
|
color: "#EAEFE6"
|
||||||
image: "assets/icons/splash_orb.png"
|
image: "assets/icons/splash_mark.png"
|
||||||
android_12:
|
android_12:
|
||||||
color: "#EAEFE6"
|
color: "#EAEFE6"
|
||||||
image: "assets/icons/splash_orb.png"
|
image: "assets/icons/splash_mark.png"
|
||||||
icon_background_color: "#EAEFE6"
|
icon_background_color: "#EAEFE6"
|
||||||
android: true
|
android: true
|
||||||
ios: true
|
ios: true
|
||||||
|
|||||||
@@ -34,6 +34,8 @@
|
|||||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
|
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<style id="splash-screen-style">
|
<style id="splash-screen-style">
|
||||||
html {
|
html {
|
||||||
height: 100%
|
height: 100%
|
||||||
@@ -108,6 +110,7 @@
|
|||||||
</picture>
|
</picture>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
You can customize the "flutter_bootstrap.js" script.
|
You can customize the "flutter_bootstrap.js" script.
|
||||||
This is useful to provide a custom configuration to the Flutter loader
|
This is useful to provide a custom configuration to the Flutter loader
|
||||||
|
|||||||
Reference in New Issue
Block a user