|
|
|
// Copyright 2023 The Flutter Authors. All rights reserved.
|
|
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
|
|
// found in the LICENSE file.
|
|
|
|
|
|
|
|
import 'package:extra_alignments/extra_alignments.dart';
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:flutter_animate/flutter_animate.dart';
|
|
|
|
import 'package:focusable_control_builder/focusable_control_builder.dart';
|
|
|
|
import 'package:gap/gap.dart';
|
|
|
|
import 'package:provider/provider.dart';
|
|
|
|
|
|
|
|
import '../assets.dart';
|
|
|
|
import '../common/shader_effect.dart';
|
|
|
|
import '../common/ticking_builder.dart';
|
|
|
|
import '../common/ui_scaler.dart';
|
|
|
|
import '../styles.dart';
|
|
|
|
|
|
|
|
class TitleScreenUi extends StatelessWidget {
|
|
|
|
const TitleScreenUi({
|
|
|
|
super.key,
|
|
|
|
required this.difficulty,
|
|
|
|
required this.onDifficultyPressed,
|
|
|
|
required this.onDifficultyFocused,
|
|
|
|
});
|
|
|
|
|
|
|
|
final int difficulty;
|
|
|
|
final void Function(int difficulty) onDifficultyPressed;
|
|
|
|
final void Function(int? difficulty) onDifficultyFocused;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return Padding(
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 50),
|
|
|
|
child: Stack(
|
|
|
|
children: [
|
|
|
|
/// Title Text
|
|
|
|
const TopLeft(
|
|
|
|
child: UiScaler(
|
|
|
|
alignment: Alignment.topLeft,
|
|
|
|
child: _TitleText(),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
|
|
|
|
/// Difficulty Btns
|
|
|
|
BottomLeft(
|
|
|
|
child: UiScaler(
|
|
|
|
alignment: Alignment.bottomLeft,
|
|
|
|
child: _DifficultyBtns(
|
|
|
|
difficulty: difficulty,
|
|
|
|
onDifficultyPressed: onDifficultyPressed,
|
|
|
|
onDifficultyFocused: onDifficultyFocused,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
|
|
|
|
/// StartBtn
|
|
|
|
BottomRight(
|
|
|
|
child: UiScaler(
|
|
|
|
alignment: Alignment.bottomRight,
|
|
|
|
child: Padding(
|
|
|
|
padding: const EdgeInsets.only(bottom: 20, right: 40),
|
|
|
|
child: _StartBtn(onPressed: () {}),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _TitleText extends StatelessWidget {
|
|
|
|
const _TitleText();
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
Widget content = Column(
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
children: [
|
|
|
|
const Gap(20),
|
|
|
|
Row(
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
children: [
|
|
|
|
Transform.translate(
|
|
|
|
offset: Offset(-(TextStyles.h1.letterSpacing! * .5), 0),
|
|
|
|
child: Text('OUTPOST', style: TextStyles.h1),
|
|
|
|
),
|
|
|
|
Image.asset(AssetPaths.titleSelectedLeft, height: 65),
|
|
|
|
Text('57', style: TextStyles.h2),
|
|
|
|
Image.asset(AssetPaths.titleSelectedRight, height: 65),
|
|
|
|
],
|
|
|
|
).animate().fadeIn(delay: .8.seconds, duration: .7.seconds),
|
|
|
|
Text('INTO THE UNKNOWN', style: TextStyles.h3)
|
|
|
|
.animate()
|
|
|
|
.fadeIn(delay: 1.seconds, duration: .7.seconds),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
return Consumer<Shaders?>(
|
|
|
|
builder: (context, shaders, _) {
|
|
|
|
if (shaders == null) return content;
|
|
|
|
return TickingBuilder(
|
|
|
|
builder: (context, time) {
|
|
|
|
return AnimatedSampler(
|
|
|
|
(image, size, canvas) {
|
|
|
|
const double overdrawPx = 30;
|
|
|
|
shaders.ui
|
|
|
|
..setFloat(0, size.width)
|
|
|
|
..setFloat(1, size.height)
|
|
|
|
..setFloat(2, time)
|
|
|
|
..setImageSampler(0, image);
|
|
|
|
Rect rect = Rect.fromLTWH(-overdrawPx, -overdrawPx,
|
|
|
|
size.width + overdrawPx, size.height + overdrawPx);
|
|
|
|
canvas.drawRect(rect, Paint()..shader = shaders.ui);
|
|
|
|
},
|
|
|
|
child: content,
|
|
|
|
);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _DifficultyBtns extends StatelessWidget {
|
|
|
|
const _DifficultyBtns({
|
|
|
|
required this.difficulty,
|
|
|
|
required this.onDifficultyPressed,
|
|
|
|
required this.onDifficultyFocused,
|
|
|
|
});
|
|
|
|
|
|
|
|
final int difficulty;
|
|
|
|
final void Function(int difficulty) onDifficultyPressed;
|
|
|
|
final void Function(int? difficulty) onDifficultyFocused;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return Column(
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
children: [
|
|
|
|
_DifficultyBtn(
|
|
|
|
label: 'Casual',
|
|
|
|
selected: difficulty == 0,
|
|
|
|
onPressed: () => onDifficultyPressed(0),
|
|
|
|
onHover: (over) => onDifficultyFocused(over ? 0 : null),
|
|
|
|
)
|
|
|
|
.animate()
|
|
|
|
.fadeIn(delay: 1.3.seconds, duration: .35.seconds)
|
|
|
|
.slide(begin: const Offset(0, .2)),
|
|
|
|
_DifficultyBtn(
|
|
|
|
label: 'Normal',
|
|
|
|
selected: difficulty == 1,
|
|
|
|
onPressed: () => onDifficultyPressed(1),
|
|
|
|
onHover: (over) => onDifficultyFocused(over ? 1 : null),
|
|
|
|
)
|
|
|
|
.animate()
|
|
|
|
.fadeIn(delay: 1.5.seconds, duration: .35.seconds)
|
|
|
|
.slide(begin: const Offset(0, .2)),
|
|
|
|
_DifficultyBtn(
|
|
|
|
label: 'Hardcore',
|
|
|
|
selected: difficulty == 2,
|
|
|
|
onPressed: () => onDifficultyPressed(2),
|
|
|
|
onHover: (over) => onDifficultyFocused(over ? 2 : null),
|
|
|
|
)
|
|
|
|
.animate()
|
|
|
|
.fadeIn(delay: 1.7.seconds, duration: .35.seconds)
|
|
|
|
.slide(begin: const Offset(0, .2)),
|
|
|
|
const Gap(20),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _DifficultyBtn extends StatelessWidget {
|
|
|
|
const _DifficultyBtn({
|
|
|
|
required this.selected,
|
|
|
|
required this.onPressed,
|
|
|
|
required this.onHover,
|
|
|
|
required this.label,
|
|
|
|
});
|
|
|
|
final String label;
|
|
|
|
final bool selected;
|
|
|
|
final VoidCallback onPressed;
|
|
|
|
final void Function(bool hasFocus) onHover;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return FocusableControlBuilder(
|
|
|
|
onPressed: onPressed,
|
|
|
|
onHoverChanged: (_, state) => onHover.call(state.isHovered),
|
|
|
|
builder: (_, state) {
|
|
|
|
return Padding(
|
|
|
|
padding: const EdgeInsets.all(8.0),
|
|
|
|
child: SizedBox(
|
|
|
|
width: 250,
|
|
|
|
height: 60,
|
|
|
|
child: Stack(
|
|
|
|
children: [
|
|
|
|
/// Bg with fill and outline
|
|
|
|
AnimatedOpacity(
|
|
|
|
opacity: (!selected && (state.isHovered || state.isFocused))
|
|
|
|
? 1
|
|
|
|
: 0,
|
|
|
|
duration: .3.seconds,
|
|
|
|
child: Container(
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
color: const Color(0xFF00D1FF).withOpacity(.1),
|
|
|
|
border: Border.all(color: Colors.white, width: 5),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
|
|
|
|
if (state.isHovered || state.isFocused) ...[
|
|
|
|
Container(
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
color: const Color(0xFF00D1FF).withOpacity(.1),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
|
|
|
|
/// cross-hairs (selected state)
|
|
|
|
if (selected) ...[
|
|
|
|
CenterLeft(
|
|
|
|
child: Image.asset(AssetPaths.titleSelectedLeft),
|
|
|
|
),
|
|
|
|
CenterRight(
|
|
|
|
child: Image.asset(AssetPaths.titleSelectedRight),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
|
|
|
|
/// Label
|
|
|
|
Center(
|
|
|
|
child: Text(label.toUpperCase(), style: TextStyles.btn),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _StartBtn extends StatefulWidget {
|
|
|
|
const _StartBtn({required this.onPressed});
|
|
|
|
final VoidCallback onPressed;
|
|
|
|
|
|
|
|
@override
|
|
|
|
State<_StartBtn> createState() => _StartBtnState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class _StartBtnState extends State<_StartBtn> {
|
|
|
|
AnimationController? _btnAnim;
|
|
|
|
bool _wasHovered = false;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return FocusableControlBuilder(
|
|
|
|
cursor: SystemMouseCursors.click,
|
|
|
|
onPressed: widget.onPressed,
|
|
|
|
builder: (_, state) {
|
|
|
|
if ((state.isHovered || state.isFocused) &&
|
|
|
|
!_wasHovered &&
|
|
|
|
_btnAnim?.status != AnimationStatus.forward) {
|
|
|
|
_btnAnim?.forward(from: 0);
|
|
|
|
}
|
|
|
|
_wasHovered = (state.isHovered || state.isFocused);
|
|
|
|
return SizedBox(
|
|
|
|
width: 520,
|
|
|
|
height: 100,
|
|
|
|
child: Stack(
|
|
|
|
children: [
|
|
|
|
Positioned.fill(child: Image.asset(AssetPaths.titleStartBtn)),
|
|
|
|
if (state.isHovered || state.isFocused) ...[
|
|
|
|
Positioned.fill(
|
|
|
|
child: Image.asset(AssetPaths.titleStartBtnHover)),
|
|
|
|
],
|
|
|
|
Center(
|
|
|
|
child: Row(
|
|
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
|
|
children: [
|
|
|
|
Text('START MISSION',
|
|
|
|
style: TextStyles.btn
|
|
|
|
.copyWith(fontSize: 24, letterSpacing: 18)),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
)
|
|
|
|
.animate(
|
|
|
|
autoPlay: false,
|
|
|
|
onInit: (c) {
|
|
|
|
if (_btnAnim != null && _btnAnim != c) {
|
|
|
|
_btnAnim?.dispose();
|
|
|
|
}
|
|
|
|
_btnAnim = c;
|
|
|
|
},
|
|
|
|
)
|
|
|
|
.shimmer(duration: .7.seconds, color: Colors.black),
|
|
|
|
)
|
|
|
|
.animate()
|
|
|
|
.fadeIn(delay: 2.3.seconds)
|
|
|
|
.slide(begin: const Offset(0, .2));
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|