2022-12-08 16:24:43 -07:00
|
|
|
import 'package:particle_field/particle_field.dart';
|
2022-08-29 20:38:28 -06:00
|
|
|
import 'package:wonders/common_libs.dart';
|
|
|
|
import 'package:wonders/logic/data/collectible_data.dart';
|
2022-12-19 14:13:25 -07:00
|
|
|
import 'package:wonders/ui/common/app_backdrop.dart';
|
2022-11-14 15:36:34 -07:00
|
|
|
import 'package:wonders/ui/common/centered_box.dart';
|
2022-12-19 14:13:25 -07:00
|
|
|
import 'package:wonders/ui/common/pop_navigator_underlay.dart';
|
2022-08-29 20:38:28 -06:00
|
|
|
|
|
|
|
part 'widgets/_animated_ribbon.dart';
|
|
|
|
part 'widgets/_celebration_particles.dart';
|
|
|
|
|
|
|
|
class CollectibleFoundScreen extends StatelessWidget {
|
|
|
|
// CollectibleItem passes in a (theoretically) pre-loaded imageProvider.
|
|
|
|
// we could check for load completion, and hold after introT, but that shouldn't be necessary in a real-world scenario.
|
|
|
|
const CollectibleFoundScreen({required this.collectible, required this.imageProvider, Key? key}) : super(key: key);
|
|
|
|
|
|
|
|
final CollectibleData collectible;
|
|
|
|
final ImageProvider imageProvider;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return RepaintBoundary(
|
|
|
|
child: _buildIntro(context).animate().swap(
|
|
|
|
delay: $styles.times.fast * 3.5,
|
|
|
|
builder: (_, __) => _buildDetail(context),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget _buildIntro(BuildContext context) {
|
|
|
|
Duration t = $styles.times.fast;
|
|
|
|
return Stack(children: [
|
|
|
|
Animate().custom(duration: t * 5, builder: (context, ratio, _) => _buildGradient(context, ratio, 0)),
|
|
|
|
|
|
|
|
// icon is handled by Hero initially, then scales slowly:
|
|
|
|
Center(
|
|
|
|
child: FractionallySizedBox(
|
|
|
|
widthFactor: 0.33,
|
|
|
|
heightFactor: 0.33,
|
|
|
|
child: Hero(
|
|
|
|
tag: 'collectible_icon_${collectible.id}',
|
|
|
|
child: Image(
|
|
|
|
image: collectible.icon,
|
|
|
|
fit: BoxFit.contain,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
).animate().scale(begin: 1.5, end: 3, curve: Curves.easeInExpo, delay: t, duration: t * 3).fadeOut(),
|
|
|
|
)
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget _buildDetail(BuildContext context) {
|
|
|
|
Duration t = $styles.times.fast;
|
|
|
|
return Stack(key: ValueKey('detail'), children: [
|
2022-12-19 14:13:25 -07:00
|
|
|
/// Background
|
|
|
|
AppBackdrop(
|
|
|
|
strength: .5,
|
|
|
|
child: Container(color: $styles.colors.greyStrong.withOpacity(.96)),
|
|
|
|
).animate().fadeIn(),
|
|
|
|
|
|
|
|
/// Particles
|
2022-08-29 20:38:28 -06:00
|
|
|
_CelebrationParticles(fadeMs: (t * 6).inMilliseconds),
|
2022-12-19 14:13:25 -07:00
|
|
|
|
|
|
|
/// invisible close btn
|
|
|
|
PopNavigatorUnderlay(),
|
|
|
|
|
|
|
|
/// Content
|
2022-08-29 20:38:28 -06:00
|
|
|
SafeArea(
|
2022-11-14 15:36:34 -07:00
|
|
|
child: CenteredBox(
|
|
|
|
width: $styles.sizes.maxContentWidth3,
|
|
|
|
child: Column(
|
|
|
|
children: [
|
|
|
|
Gap($styles.insets.lg),
|
|
|
|
Spacer(),
|
|
|
|
SizedBox(
|
|
|
|
height: context.heightPx * .35,
|
|
|
|
child: Center(child: Hero(tag: 'collectible_image_${collectible.id}', child: _buildImage(context))),
|
|
|
|
),
|
|
|
|
Gap($styles.insets.lg),
|
|
|
|
_buildRibbon(context),
|
|
|
|
Gap($styles.insets.sm),
|
|
|
|
_buildTitle(context, collectible.title, $styles.text.h2, $styles.colors.offWhite, t * 1.5),
|
|
|
|
Gap($styles.insets.xs),
|
|
|
|
_buildTitle(
|
|
|
|
context, collectible.subtitle.toUpperCase(), $styles.text.title2, $styles.colors.accent1, t * 2),
|
|
|
|
Spacer(),
|
|
|
|
Gap($styles.insets.lg),
|
|
|
|
_buildCollectionButton(context),
|
|
|
|
Gap($styles.insets.lg),
|
|
|
|
Spacer(),
|
|
|
|
],
|
2022-08-29 20:38:28 -06:00
|
|
|
),
|
2022-10-27 10:24:12 -06:00
|
|
|
),
|
2022-08-29 20:38:28 -06:00
|
|
|
),
|
|
|
|
BackBtn.close().safe().animate().fade(delay: t * 4, duration: t * 2),
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget _buildGradient(BuildContext context, double ratioIn, double ratioOut) {
|
|
|
|
// used by both intro and detail animations to ensure they share a mid-point.
|
|
|
|
ratioIn = Curves.easeOutExpo.transform(ratioIn);
|
|
|
|
final double opacity = 1.0 * (ratioIn * 0.8 + ratioOut * 0.2);
|
|
|
|
final Color light = $styles.colors.offWhite;
|
|
|
|
final Color dark = $styles.colors.black;
|
|
|
|
|
|
|
|
// final state is a solid fill, so optimize that case:
|
|
|
|
if (ratioOut == 1) return Container(color: dark.withOpacity(opacity));
|
|
|
|
|
|
|
|
return Container(
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
gradient: RadialGradient(
|
|
|
|
colors: [Color.lerp(light, dark, ratioOut)!.withOpacity(opacity), dark.withOpacity(opacity)],
|
|
|
|
stops: [0.2, min(1, 0.25 + ratioIn * 0.5 + ratioOut * 0.5)],
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget _buildImage(BuildContext context) {
|
|
|
|
Duration t = $styles.times.fast;
|
|
|
|
// build an image with animated shadows and scaling
|
|
|
|
return AppImage(image: imageProvider, scale: 1.0)
|
|
|
|
.animate()
|
|
|
|
.custom(
|
|
|
|
duration: t * 6,
|
|
|
|
builder: (_, ratio, child) => Container(
|
|
|
|
padding: EdgeInsets.all($styles.insets.xxs),
|
|
|
|
margin: EdgeInsets.symmetric(horizontal: $styles.insets.xl),
|
|
|
|
decoration: BoxDecoration(color: $styles.colors.offWhite, boxShadow: [
|
|
|
|
BoxShadow(
|
|
|
|
color: $styles.colors.accent1.withOpacity(ratio * 0.75),
|
|
|
|
blurRadius: $styles.insets.xl * 2,
|
|
|
|
),
|
|
|
|
BoxShadow(
|
|
|
|
color: $styles.colors.black.withOpacity(ratio * 0.75),
|
|
|
|
offset: Offset(0, $styles.insets.xxs),
|
|
|
|
blurRadius: $styles.insets.sm,
|
|
|
|
),
|
|
|
|
]),
|
|
|
|
child: child,
|
|
|
|
),
|
|
|
|
)
|
|
|
|
.scale(begin: 0.3, duration: t * 2, curve: Curves.easeOutExpo, alignment: Alignment(0, 0.7));
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget _buildRibbon(BuildContext context) {
|
|
|
|
Duration t = $styles.times.fast;
|
|
|
|
return _AnimatedRibbon($strings.collectibleFoundTitleArtifactDiscovered.toUpperCase())
|
|
|
|
.animate()
|
|
|
|
.scale(begin: 0.3, duration: t * 2, curve: Curves.easeOutExpo, alignment: Alignment(0, -1));
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget _buildTitle(BuildContext context, String text, TextStyle style, Color color, Duration delay) {
|
|
|
|
Duration t = $styles.times.fast;
|
|
|
|
// because this is a performance-sensitive screen, we are fading in the text by adjusting color:
|
|
|
|
return Container(
|
|
|
|
padding: EdgeInsets.symmetric(horizontal: $styles.insets.lg),
|
|
|
|
child: Animate().custom(
|
|
|
|
delay: delay,
|
|
|
|
duration: t * 2,
|
|
|
|
builder: (_, m, __) => Text(
|
|
|
|
text,
|
|
|
|
maxLines: 2,
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
style: style.copyWith(color: color.withOpacity(m)),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget _buildCollectionButton(BuildContext context) {
|
2022-12-03 12:36:06 -07:00
|
|
|
Duration t = $styles.times.med;
|
2022-10-27 10:24:12 -06:00
|
|
|
return AppBtn.from(
|
|
|
|
text: $strings.collectibleFoundButtonViewCollection,
|
|
|
|
isSecondary: true,
|
|
|
|
onPressed: () => _handleViewCollectionPressed(context),
|
2022-12-03 12:36:06 -07:00
|
|
|
).animate().fadeIn(delay: t).move(
|
|
|
|
begin: Offset(0, 50),
|
|
|
|
duration: t,
|
|
|
|
curve: Curves.easeOutCubic,
|
|
|
|
);
|
2022-08-29 20:38:28 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
void _handleViewCollectionPressed(BuildContext context) {
|
|
|
|
Navigator.pop(context);
|
|
|
|
context.push(ScreenPaths.collection(collectible.id));
|
|
|
|
}
|
|
|
|
}
|