mirror of
https://github.com/flutter/samples.git
synced 2025-11-10 23:08:59 +00:00
Next Gen UI demo (#1778)
First pass at a Next Generation UI demo app. The UI needs work, feedback gratefully accepted. ## Pre-launch Checklist - [x] I read the [Flutter Style Guide] _recently_, and have followed its advice. - [x] I signed the [CLA]. - [x] I read the [Contributors Guide]. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-devrel channel on [Discord]. <!-- Links --> [Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo [CLA]: https://cla.developers.google.com/ [Discord]: https://github.com/flutter/flutter/wiki/Chat [Contributors Guide]: https://github.com/flutter/samples/blob/main/CONTRIBUTING.md
This commit is contained in:
41
next_gen_ui_demo/lib/assets.dart
Normal file
41
next_gen_ui_demo/lib/assets.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
// 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 'dart:ui';
|
||||
|
||||
class AssetPaths {
|
||||
/// Images
|
||||
static const String _images = 'assets/images';
|
||||
|
||||
static const String titleBgBase = '$_images/bg-base.jpg';
|
||||
static const String titleBgReceive = '$_images/bg-light-receive.png';
|
||||
static const String titleFgEmit = '$_images/fg-light-emit.png';
|
||||
static const String titleFgReceive = '$_images/fg-light-receive.png';
|
||||
static const String titleFgBase = '$_images/fg-base.png';
|
||||
static const String titleMgEmit = '$_images/mg-light-emit.png';
|
||||
static const String titleMgReceive = '$_images/mg-light-receive.png';
|
||||
static const String titleMgBase = '$_images/mg-base.png';
|
||||
static const String titleStartBtn = '$_images/button-start.png';
|
||||
static const String titleStartBtnHover = '$_images/button-start-hover.png';
|
||||
static const String titleStartArrow = '$_images/button-start-arrow.png';
|
||||
static const String titleSelectedLeft = '$_images/select-left.png';
|
||||
static const String titleSelectedRight = '$_images/select-right.png';
|
||||
static const String pulseParticle = '$_images/particle3.png';
|
||||
|
||||
/// Shaders
|
||||
static const String _shaders = 'assets/shaders';
|
||||
static const String orbShader = '$_shaders/orb_shader.frag';
|
||||
static const String uiShader = '$_shaders/ui_glitch.frag';
|
||||
}
|
||||
|
||||
typedef Shaders = ({FragmentShader orb, FragmentShader ui});
|
||||
|
||||
Future<Shaders> loadShaders() async => (
|
||||
orb: (await _loadShader(AssetPaths.orbShader)),
|
||||
ui: (await _loadShader(AssetPaths.uiShader)),
|
||||
);
|
||||
|
||||
Future<FragmentShader> _loadShader(String path) async {
|
||||
return (await FragmentProgram.fromAsset(path)).fragmentShader();
|
||||
}
|
||||
37
next_gen_ui_demo/lib/common/reactive_widget.dart
Normal file
37
next_gen_ui_demo/lib/common/reactive_widget.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
// 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:flutter/material.dart';
|
||||
|
||||
import 'ticking_builder.dart';
|
||||
|
||||
typedef ReactiveWidgetBuilder = Widget Function(
|
||||
BuildContext context, double time, Size bounds);
|
||||
|
||||
/// ReactiveWidget forces repainting a subtree on
|
||||
/// each frame for ambient animation.
|
||||
class ReactiveWidget extends StatefulWidget {
|
||||
const ReactiveWidget({
|
||||
super.key,
|
||||
required this.builder,
|
||||
});
|
||||
final ReactiveWidgetBuilder builder;
|
||||
@override
|
||||
State<ReactiveWidget> createState() => _ReactiveWidgetState();
|
||||
}
|
||||
|
||||
class _ReactiveWidgetState extends State<ReactiveWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TickingBuilder(
|
||||
builder: (_, time) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return widget.builder(context, time, constraints.biggest);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
359
next_gen_ui_demo/lib/common/shader_effect.dart
Normal file
359
next_gen_ui_demo/lib/common/shader_effect.dart
Normal file
@@ -0,0 +1,359 @@
|
||||
// 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 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
|
||||
/**
|
||||
* This is an unfinished, pre-release effect for Flutter Animate:
|
||||
* https://pub.dev/packages/flutter_animate
|
||||
*
|
||||
* It includes a copy of `AnimatedSampler` from Flutter Shaders:
|
||||
* https://github.com/jonahwilliams/flutter_shaders
|
||||
*
|
||||
* Once `AnimatedSampler` (or equivalent) is stable, or included in the core
|
||||
* SDK, this effect will be updated, tested, refined, and added to the
|
||||
* effects.dart file.
|
||||
*/
|
||||
|
||||
// TODO: document.
|
||||
|
||||
/// An effect that lets you apply an animated fragment shader to a target.
|
||||
@immutable
|
||||
class ShaderEffect extends Effect<double> {
|
||||
const ShaderEffect({
|
||||
super.delay,
|
||||
super.duration,
|
||||
super.curve,
|
||||
this.shader,
|
||||
this.update,
|
||||
ShaderLayer? layer,
|
||||
}) : layer = layer ?? ShaderLayer.replace,
|
||||
super(
|
||||
begin: 0,
|
||||
end: 1,
|
||||
);
|
||||
|
||||
final ui.FragmentShader? shader;
|
||||
final ShaderUpdateCallback? update;
|
||||
final ShaderLayer layer;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
Widget child,
|
||||
AnimationController controller,
|
||||
EffectEntry entry,
|
||||
) {
|
||||
double ratio = 1 / MediaQuery.of(context).devicePixelRatio;
|
||||
Animation<double> animation = buildAnimation(controller, entry);
|
||||
return getOptimizedBuilder<double>(
|
||||
animation: animation,
|
||||
builder: (_, __) {
|
||||
return AnimatedSampler(
|
||||
(image, size, canvas) {
|
||||
EdgeInsets? insets;
|
||||
if (update != null) {
|
||||
insets = update!(shader!, animation.value, size, image);
|
||||
}
|
||||
Rect rect = Rect.fromLTWH(0, 0, size.width, size.height);
|
||||
rect = insets?.inflateRect(rect) ?? rect;
|
||||
|
||||
void drawImage() {
|
||||
canvas.save();
|
||||
canvas.scale(ratio, ratio);
|
||||
canvas.drawImage(image, Offset.zero, Paint());
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
if (layer == ShaderLayer.foreground) drawImage();
|
||||
if (shader != null) canvas.drawRect(rect, Paint()..shader = shader);
|
||||
if (layer == ShaderLayer.background) drawImage();
|
||||
},
|
||||
enabled: shader != null,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension ShaderEffectExtensions<T> on AnimateManager<T> {
|
||||
/// Adds a [shader] extension to [AnimateManager] ([Animate] and [AnimateList]).
|
||||
T shader({
|
||||
Duration? delay,
|
||||
Duration? duration,
|
||||
Curve? curve,
|
||||
ui.FragmentShader? shader,
|
||||
ShaderUpdateCallback? update,
|
||||
ShaderLayer? layer,
|
||||
}) =>
|
||||
addEffect(ShaderEffect(
|
||||
delay: delay,
|
||||
duration: duration,
|
||||
curve: curve,
|
||||
shader: shader,
|
||||
update: update,
|
||||
layer: layer,
|
||||
));
|
||||
}
|
||||
|
||||
enum ShaderLayer { foreground, background, replace }
|
||||
|
||||
/// Function signature for [ShaderEffect] update handlers.
|
||||
typedef ShaderUpdateCallback = EdgeInsets? Function(
|
||||
ui.FragmentShader shader, double value, Size size, ui.Image image);
|
||||
|
||||
/******************************************************************************/
|
||||
// TODO: add this as a dependency instead of copying it in once it is stable:
|
||||
// https://github.com/jonahwilliams/flutter_shaders
|
||||
|
||||
// Copyright 2013 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.
|
||||
|
||||
/// A callback for the [AnimatedSamplerBuilder] widget.
|
||||
typedef AnimatedSamplerBuilder = void Function(
|
||||
ui.Image image,
|
||||
Size size,
|
||||
ui.Canvas canvas,
|
||||
);
|
||||
|
||||
/// A widget that allows access to a snapshot of the child widgets for painting
|
||||
/// with a sampler applied to a [FragmentProgram].
|
||||
///
|
||||
/// When [enabled] is true, the child widgets will be painted into a texture
|
||||
/// exposed as a [ui.Image]. This can then be passed to a [FragmentShader]
|
||||
/// instance via [FragmentShader.setSampler].
|
||||
///
|
||||
/// If [enabled] is false, then the child widgets are painted as normal.
|
||||
///
|
||||
/// Caveats:
|
||||
/// * Platform views cannot be captured in a texture. If any are present they
|
||||
/// will be excluded from the texture. Texture-based platform views are OK.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// Providing an image to a fragment shader using
|
||||
/// [FragmentShader.setImageSampler].
|
||||
///
|
||||
/// ```dart
|
||||
/// Widget build(BuildContext context) {
|
||||
/// return AnimatedSampler(
|
||||
/// (ui.Image image, Size size, Canvas canvas) {
|
||||
/// shader
|
||||
/// ..setFloat(0, size.width)
|
||||
/// ..setFloat(1, size.height)
|
||||
/// ..setImageSampler(0, image);
|
||||
/// canvas.drawRect(Offset.zero & size, Paint()..shader = shader);
|
||||
/// },
|
||||
/// child: widget.child,
|
||||
/// );
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// See also:
|
||||
/// * [SnapshotWidget], which provides a similar API for the purpose of
|
||||
/// caching during expensive animations.
|
||||
class AnimatedSampler extends StatelessWidget {
|
||||
/// Create a new [AnimatedSampler].
|
||||
const AnimatedSampler(
|
||||
this.builder, {
|
||||
required this.child,
|
||||
super.key,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
/// A callback used by this widget to provide the children captured in
|
||||
/// a texture.
|
||||
final AnimatedSamplerBuilder builder;
|
||||
|
||||
/// Whether the children should be captured in a texture or displayed as
|
||||
/// normal.
|
||||
final bool enabled;
|
||||
|
||||
/// The child widget.
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _ShaderSamplerBuilder(
|
||||
builder,
|
||||
enabled: enabled,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ShaderSamplerBuilder extends SingleChildRenderObjectWidget {
|
||||
const _ShaderSamplerBuilder(
|
||||
this.builder, {
|
||||
super.child,
|
||||
required this.enabled,
|
||||
});
|
||||
|
||||
final AnimatedSamplerBuilder builder;
|
||||
final bool enabled;
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return _RenderShaderSamplerBuilderWidget(
|
||||
devicePixelRatio: MediaQuery.of(context).devicePixelRatio,
|
||||
builder: builder,
|
||||
enabled: enabled,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context, covariant RenderObject renderObject) {
|
||||
(renderObject as _RenderShaderSamplerBuilderWidget)
|
||||
..devicePixelRatio = MediaQuery.of(context).devicePixelRatio
|
||||
..builder = builder
|
||||
..enabled = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
// A render object that conditionally converts its child into a [ui.Image]
|
||||
// and then paints it in place of the child.
|
||||
class _RenderShaderSamplerBuilderWidget extends RenderProxyBox {
|
||||
// Create a new [_RenderSnapshotWidget].
|
||||
_RenderShaderSamplerBuilderWidget({
|
||||
required double devicePixelRatio,
|
||||
required AnimatedSamplerBuilder builder,
|
||||
required bool enabled,
|
||||
}) : _devicePixelRatio = devicePixelRatio,
|
||||
_builder = builder,
|
||||
_enabled = enabled;
|
||||
|
||||
@override
|
||||
OffsetLayer updateCompositedLayer(
|
||||
{required covariant _ShaderSamplerBuilderLayer? oldLayer}) {
|
||||
final _ShaderSamplerBuilderLayer layer =
|
||||
oldLayer ?? _ShaderSamplerBuilderLayer(builder);
|
||||
layer
|
||||
..callback = builder
|
||||
..size = size
|
||||
..devicePixelRatio = devicePixelRatio;
|
||||
return layer;
|
||||
}
|
||||
|
||||
/// The device pixel ratio used to create the child image.
|
||||
double get devicePixelRatio => _devicePixelRatio;
|
||||
double _devicePixelRatio;
|
||||
set devicePixelRatio(double value) {
|
||||
if (value == devicePixelRatio) {
|
||||
return;
|
||||
}
|
||||
_devicePixelRatio = value;
|
||||
markNeedsCompositedLayerUpdate();
|
||||
}
|
||||
|
||||
/// The painter used to paint the child snapshot or child widgets.
|
||||
AnimatedSamplerBuilder get builder => _builder;
|
||||
AnimatedSamplerBuilder _builder;
|
||||
set builder(AnimatedSamplerBuilder value) {
|
||||
if (value == builder) {
|
||||
return;
|
||||
}
|
||||
_builder = value;
|
||||
markNeedsCompositedLayerUpdate();
|
||||
}
|
||||
|
||||
bool get enabled => _enabled;
|
||||
bool _enabled;
|
||||
set enabled(bool value) {
|
||||
if (value == enabled) {
|
||||
return;
|
||||
}
|
||||
_enabled = value;
|
||||
markNeedsPaint();
|
||||
markNeedsCompositingBitsUpdate();
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isRepaintBoundary => alwaysNeedsCompositing;
|
||||
|
||||
@override
|
||||
bool get alwaysNeedsCompositing => enabled;
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
if (size.isEmpty || !_enabled) {
|
||||
return;
|
||||
}
|
||||
assert(offset == Offset.zero);
|
||||
return super.paint(context, offset);
|
||||
}
|
||||
}
|
||||
|
||||
/// A [Layer] that uses an [AnimatedSamplerBuilder] to create a [ui.Picture]
|
||||
/// every time it is added to a scene.
|
||||
class _ShaderSamplerBuilderLayer extends OffsetLayer {
|
||||
_ShaderSamplerBuilderLayer(this._callback);
|
||||
|
||||
Size get size => _size;
|
||||
Size _size = Size.zero;
|
||||
set size(Size value) {
|
||||
if (value == size) {
|
||||
return;
|
||||
}
|
||||
_size = value;
|
||||
markNeedsAddToScene();
|
||||
}
|
||||
|
||||
double get devicePixelRatio => _devicePixelRatio;
|
||||
double _devicePixelRatio = 1.0;
|
||||
set devicePixelRatio(double value) {
|
||||
if (value == devicePixelRatio) {
|
||||
return;
|
||||
}
|
||||
_devicePixelRatio = value;
|
||||
markNeedsAddToScene();
|
||||
}
|
||||
|
||||
AnimatedSamplerBuilder get callback => _callback;
|
||||
AnimatedSamplerBuilder _callback;
|
||||
set callback(AnimatedSamplerBuilder value) {
|
||||
if (value == callback) {
|
||||
return;
|
||||
}
|
||||
_callback = value;
|
||||
markNeedsAddToScene();
|
||||
}
|
||||
|
||||
ui.Image _buildChildScene(Rect bounds, double pixelRatio) {
|
||||
final ui.SceneBuilder builder = ui.SceneBuilder();
|
||||
final Matrix4 transform =
|
||||
Matrix4.diagonal3Values(pixelRatio, pixelRatio, 1);
|
||||
builder.pushTransform(transform.storage);
|
||||
addChildrenToScene(builder);
|
||||
builder.pop();
|
||||
return builder.build().toImageSync(
|
||||
(pixelRatio * bounds.width).ceil(),
|
||||
(pixelRatio * bounds.height).ceil(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void addToScene(ui.SceneBuilder builder) {
|
||||
if (size.isEmpty) return;
|
||||
final ui.Image image = _buildChildScene(
|
||||
offset & size,
|
||||
devicePixelRatio,
|
||||
);
|
||||
final ui.PictureRecorder pictureRecorder = ui.PictureRecorder();
|
||||
final Canvas canvas = Canvas(pictureRecorder);
|
||||
try {
|
||||
callback(image, size, canvas);
|
||||
} finally {
|
||||
image.dispose();
|
||||
}
|
||||
final ui.Picture picture = pictureRecorder.endRecording();
|
||||
builder.addPicture(offset, picture);
|
||||
}
|
||||
}
|
||||
28
next_gen_ui_demo/lib/common/shader_painter.dart
Normal file
28
next_gen_ui_demo/lib/common/shader_painter.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
// 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 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ShaderPainter extends CustomPainter {
|
||||
ShaderPainter(this.shader, {this.update});
|
||||
|
||||
final FragmentShader shader;
|
||||
final void Function(FragmentShader, Size)? update;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
update?.call(shader, size);
|
||||
canvas.drawRect(
|
||||
Rect.fromLTWH(0, 0, size.width, size.height),
|
||||
Paint()..shader = shader,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant ShaderPainter oldDelegate) {
|
||||
return oldDelegate.shader != shader || oldDelegate.update != update;
|
||||
}
|
||||
}
|
||||
40
next_gen_ui_demo/lib/common/ticking_builder.dart
Normal file
40
next_gen_ui_demo/lib/common/ticking_builder.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
// 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:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
/// TickingBuilder is for ambient animation to be run
|
||||
/// on each frame.
|
||||
class TickingBuilder extends StatefulWidget {
|
||||
const TickingBuilder({super.key, required this.builder});
|
||||
final Widget Function(BuildContext context, double time) builder;
|
||||
@override
|
||||
State<TickingBuilder> createState() => _TickingBuilderState();
|
||||
}
|
||||
|
||||
class _TickingBuilderState extends State<TickingBuilder>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final Ticker _ticker;
|
||||
double _time = 0.0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_ticker = createTicker(_handleTick)..start();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ticker.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleTick(Duration elapsed) {
|
||||
setState(() => _time = elapsed.inMilliseconds.toDouble() / 1000.0);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.builder.call(context, _time);
|
||||
}
|
||||
31
next_gen_ui_demo/lib/common/ui_scaler.dart
Normal file
31
next_gen_ui_demo/lib/common/ui_scaler.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
// 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 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class UiScaler extends StatelessWidget {
|
||||
const UiScaler({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.alignment,
|
||||
this.referenceHeight = 1080,
|
||||
});
|
||||
|
||||
final int referenceHeight;
|
||||
final Widget child;
|
||||
final Alignment alignment;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
final double scale = min(screenSize.height / referenceHeight, 1.0);
|
||||
return Transform.scale(
|
||||
scale: scale,
|
||||
alignment: alignment,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
145
next_gen_ui_demo/lib/main.dart
Normal file
145
next_gen_ui_demo/lib/main.dart
Normal file
@@ -0,0 +1,145 @@
|
||||
// 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 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:window_size/window_size.dart';
|
||||
|
||||
import 'assets.dart';
|
||||
import 'title_screen/title_screen.dart';
|
||||
import 'title_screen_1a/title_screen.dart' as title_screen_1a;
|
||||
import 'title_screen_1b/title_screen.dart' as title_screen_1b;
|
||||
import 'title_screen_2a/title_screen.dart' as title_screen_2a;
|
||||
import 'title_screen_2c/title_screen.dart' as title_screen_2c;
|
||||
import 'title_screen_3a/title_screen.dart' as title_screen_3a;
|
||||
import 'title_screen_3b/title_screen.dart' as title_screen_3b;
|
||||
import 'title_screen_3c/title_screen.dart' as title_screen_3c;
|
||||
import 'title_screen_4a/title_screen.dart' as title_screen_4a;
|
||||
import 'title_screen_4b/title_screen.dart' as title_screen_4b;
|
||||
import 'title_screen_4c/title_screen.dart' as title_screen_4c;
|
||||
import 'title_screen_4d/title_screen.dart' as title_screen_4d;
|
||||
import 'title_screen_4e/title_screen.dart' as title_screen_4e;
|
||||
import 'title_screen_5a/title_screen.dart' as title_screen_5a;
|
||||
import 'title_screen_5b/title_screen.dart' as title_screen_5b;
|
||||
import 'title_screen_6/title_screen.dart' as title_screen_6;
|
||||
|
||||
void main() {
|
||||
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
setWindowMinSize(const Size(800, 500));
|
||||
}
|
||||
Animate.restartOnHotReload = true;
|
||||
runApp(
|
||||
FutureProvider<Shaders?>(
|
||||
create: (context) => loadShaders(),
|
||||
initialData: null,
|
||||
child: const NextGenApp(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class NextGenApp extends StatefulWidget {
|
||||
const NextGenApp({super.key});
|
||||
|
||||
@override
|
||||
State<NextGenApp> createState() => _NextGenAppState();
|
||||
}
|
||||
|
||||
List<
|
||||
TitleScreenBase Function(
|
||||
{required void Function(Color) callback, Key? key})> steps = [
|
||||
title_screen_1a.TitleScreen.new,
|
||||
title_screen_1b.TitleScreen.new,
|
||||
title_screen_2a.TitleScreen.new,
|
||||
title_screen_2c.TitleScreen.new,
|
||||
title_screen_3a.TitleScreen.new,
|
||||
title_screen_3b.TitleScreen.new,
|
||||
title_screen_3c.TitleScreen.new,
|
||||
title_screen_4a.TitleScreen.new,
|
||||
title_screen_4b.TitleScreen.new,
|
||||
title_screen_4c.TitleScreen.new,
|
||||
title_screen_4d.TitleScreen.new,
|
||||
title_screen_4e.TitleScreen.new,
|
||||
title_screen_5a.TitleScreen.new,
|
||||
title_screen_5b.TitleScreen.new,
|
||||
title_screen_6.TitleScreen.new,
|
||||
];
|
||||
|
||||
typedef ColorCallback = void Function(Color colorSchemeSeed);
|
||||
|
||||
class _NextGenAppState extends State<NextGenApp> {
|
||||
int step = 0;
|
||||
Color? colorSchemeSeed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
themeMode: ThemeMode.dark,
|
||||
darkTheme: ThemeData(
|
||||
brightness: Brightness.dark,
|
||||
useMaterial3: true,
|
||||
colorSchemeSeed: colorSchemeSeed,
|
||||
),
|
||||
home: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
'Step ${step + 1} of ${steps.length}',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.copyWith(color: colorSchemeSeed),
|
||||
),
|
||||
backgroundColor: Colors.black38,
|
||||
),
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
|
||||
floatingActionButton: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Visibility(
|
||||
visible: step > 0,
|
||||
child: FloatingActionButton(
|
||||
child: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
if (step > 0) step--;
|
||||
debugPrint('Step = $step');
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: step > 0 && step + 1 < steps.length,
|
||||
child: const Gap(24),
|
||||
),
|
||||
Visibility(
|
||||
visible: step + 1 < steps.length,
|
||||
child: FloatingActionButton(
|
||||
child: const Icon(Icons.arrow_forward),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
if (step + 1 < steps.length) step++;
|
||||
debugPrint('Step = $step');
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: Colors.black,
|
||||
body: steps[step](
|
||||
callback: (color) {
|
||||
setState(() {
|
||||
colorSchemeSeed = color;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
131
next_gen_ui_demo/lib/orb_shader/orb_shader_config.dart
Normal file
131
next_gen_ui_demo/lib/orb_shader/orb_shader_config.dart
Normal file
@@ -0,0 +1,131 @@
|
||||
// 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:flutter/material.dart';
|
||||
|
||||
class OrbShaderConfig {
|
||||
const OrbShaderConfig({
|
||||
this.zoom = 0.3,
|
||||
this.exposure = 0.4,
|
||||
this.roughness = 0.3,
|
||||
this.metalness = 0.3,
|
||||
this.materialColor = const Color.fromARGB(255, 242, 163, 138),
|
||||
this.lightRadius = 0.75,
|
||||
this.lightColor = const Color(0xFFFFFFFF),
|
||||
this.lightBrightness = 15.00,
|
||||
this.ior = 0.5,
|
||||
this.lightAttenuation = 0.5,
|
||||
this.ambientLightColor = const Color(0xFFFFFFFF),
|
||||
this.ambientLightBrightness = 0.2,
|
||||
this.ambientLightDepthFactor = 0.3,
|
||||
this.lightOffsetX = 0,
|
||||
this.lightOffsetY = 0.1,
|
||||
this.lightOffsetZ = -0.66,
|
||||
}) : assert(zoom >= 0 && zoom <= 1),
|
||||
assert(exposure >= 0),
|
||||
assert(metalness >= 0 && metalness <= 1),
|
||||
assert(lightRadius >= 0),
|
||||
assert(lightBrightness >= 1),
|
||||
assert(ior >= 0 && ior <= 2),
|
||||
assert(lightAttenuation >= 0 && lightAttenuation <= 1),
|
||||
assert(ambientLightBrightness >= 0);
|
||||
|
||||
final double zoom;
|
||||
|
||||
/// Camera exposure value, higher is brighter, 0 is black
|
||||
final double exposure;
|
||||
|
||||
/// How rough the surface is, somewhat translates to the intensity/radius
|
||||
/// of specular highlights
|
||||
final double roughness;
|
||||
|
||||
/// 0 for a dielectric material (plastic, wood, grass, water, etc...),
|
||||
/// 1 for a metal (iron, copper, aluminum, gold, etc...), a value in between
|
||||
/// blends the two materials (not really physically accurate, has minor
|
||||
/// artistic value)
|
||||
final double metalness;
|
||||
|
||||
/// any color, alpha ignored, for metal materials doesn't correspond to
|
||||
/// surface color but to a reflectivity index based off of a 0 degree viewing
|
||||
/// angle (can look these values up online for various actual metals)
|
||||
final Color materialColor;
|
||||
|
||||
/// The following light properties model a disk shaped light pointing
|
||||
/// at the sphere
|
||||
final double lightRadius;
|
||||
|
||||
/// alpha ignored
|
||||
final Color lightColor;
|
||||
|
||||
/// Light Brightness measured in luminous power (perceived total
|
||||
/// brightness of light, the larger the radius the more diffused the light
|
||||
/// power is for a given area)
|
||||
final double lightBrightness;
|
||||
|
||||
/// 0..2, Index of refraction, higher value = more refraction,
|
||||
final double ior;
|
||||
|
||||
/// Light attenuation factor, 0 for no attenuation, 1 is very fast attenuation
|
||||
final double lightAttenuation;
|
||||
|
||||
/// alpha ignored
|
||||
final Color ambientLightColor;
|
||||
|
||||
final double ambientLightBrightness;
|
||||
|
||||
/// Modulates the ambient light brightness based off of the depth of the
|
||||
/// pixel. 1 means the ambient brightness factor at the front of the orb is 0,
|
||||
/// brightness factor at the back is 1. 0 means there's no change to the
|
||||
/// brightness factor based on depth
|
||||
final double ambientLightDepthFactor;
|
||||
|
||||
/// Offset of the light relative to the center of the orb, +x is to the right
|
||||
final double lightOffsetX;
|
||||
|
||||
/// Offset of the light relative to the center of the orb, +y is up
|
||||
final double lightOffsetY;
|
||||
|
||||
/// Offset of the light relative to the center of the orb, +z is facing the camera
|
||||
final double lightOffsetZ;
|
||||
|
||||
OrbShaderConfig copyWith({
|
||||
double? zoom,
|
||||
double? exposure,
|
||||
double? roughness,
|
||||
double? metalness,
|
||||
Color? materialColor,
|
||||
double? lightRadius,
|
||||
Color? lightColor,
|
||||
double? lightBrightness,
|
||||
double? ior,
|
||||
double? lightAttenuation,
|
||||
Color? ambientLightColor,
|
||||
double? ambientLightBrightness,
|
||||
double? ambientLightDepthFactor,
|
||||
double? lightOffsetX,
|
||||
double? lightOffsetY,
|
||||
double? lightOffsetZ,
|
||||
}) {
|
||||
return OrbShaderConfig(
|
||||
zoom: zoom ?? this.zoom,
|
||||
exposure: exposure ?? this.exposure,
|
||||
roughness: roughness ?? this.roughness,
|
||||
metalness: metalness ?? this.metalness,
|
||||
materialColor: materialColor ?? this.materialColor,
|
||||
lightRadius: lightRadius ?? this.lightRadius,
|
||||
lightColor: lightColor ?? this.lightColor,
|
||||
lightBrightness: lightBrightness ?? this.lightBrightness,
|
||||
ior: ior ?? this.ior,
|
||||
lightAttenuation: lightAttenuation ?? this.lightAttenuation,
|
||||
ambientLightColor: ambientLightColor ?? this.ambientLightColor,
|
||||
ambientLightBrightness:
|
||||
ambientLightBrightness ?? this.ambientLightBrightness,
|
||||
ambientLightDepthFactor:
|
||||
ambientLightDepthFactor ?? this.ambientLightDepthFactor,
|
||||
lightOffsetX: lightOffsetX ?? this.lightOffsetX,
|
||||
lightOffsetY: lightOffsetY ?? this.lightOffsetY,
|
||||
lightOffsetZ: lightOffsetZ ?? this.lightOffsetZ,
|
||||
);
|
||||
}
|
||||
}
|
||||
85
next_gen_ui_demo/lib/orb_shader/orb_shader_painter.dart
Normal file
85
next_gen_ui_demo/lib/orb_shader/orb_shader_painter.dart
Normal file
@@ -0,0 +1,85 @@
|
||||
// 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 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:vector_math/vector_math_64.dart' as v64;
|
||||
|
||||
import 'orb_shader_config.dart';
|
||||
|
||||
class OrbShaderPainter extends CustomPainter {
|
||||
OrbShaderPainter(
|
||||
this.shader, {
|
||||
required this.config,
|
||||
required this.time,
|
||||
required this.mousePos,
|
||||
required this.energy,
|
||||
});
|
||||
final FragmentShader shader;
|
||||
final OrbShaderConfig config;
|
||||
final double time;
|
||||
final Offset mousePos;
|
||||
final double energy;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
double fov = v64.mix(pi / 4.3, pi / 2.0, config.zoom.clamp(0.0, 1.0));
|
||||
|
||||
v64.Vector3 colorToVector3(Color c) =>
|
||||
v64.Vector3(
|
||||
c.red.toDouble(),
|
||||
c.green.toDouble(),
|
||||
c.blue.toDouble(),
|
||||
) /
|
||||
255.0;
|
||||
|
||||
v64.Vector3 lightLumP = colorToVector3(config.lightColor).normalized() *
|
||||
max(0.0, config.lightBrightness);
|
||||
v64.Vector3 albedo = colorToVector3(config.materialColor);
|
||||
|
||||
v64.Vector3 ambientLight = colorToVector3(config.ambientLightColor) *
|
||||
max(0.0, config.ambientLightBrightness);
|
||||
|
||||
shader.setFloat(0, size.width);
|
||||
shader.setFloat(1, size.height);
|
||||
shader.setFloat(2, time);
|
||||
shader.setFloat(3, max(0.0, config.exposure));
|
||||
shader.setFloat(4, fov);
|
||||
shader.setFloat(5, config.roughness.clamp(0.0, 1.0));
|
||||
shader.setFloat(6, config.metalness.clamp(0.0, 1.0));
|
||||
shader.setFloat(7, config.lightOffsetX);
|
||||
shader.setFloat(8, config.lightOffsetY);
|
||||
shader.setFloat(9, config.lightOffsetZ);
|
||||
shader.setFloat(10, config.lightRadius);
|
||||
shader.setFloat(11, lightLumP.x);
|
||||
shader.setFloat(12, lightLumP.y);
|
||||
shader.setFloat(13, lightLumP.z);
|
||||
shader.setFloat(14, albedo.x);
|
||||
shader.setFloat(15, albedo.y);
|
||||
shader.setFloat(16, albedo.z);
|
||||
shader.setFloat(17, config.ior.clamp(0.0, 2.0));
|
||||
shader.setFloat(18, config.lightAttenuation.clamp(0.0, 1.0));
|
||||
shader.setFloat(19, ambientLight.x);
|
||||
shader.setFloat(20, ambientLight.y);
|
||||
shader.setFloat(21, ambientLight.z);
|
||||
shader.setFloat(22, config.ambientLightDepthFactor.clamp(0.0, 1.0));
|
||||
shader.setFloat(23, energy);
|
||||
|
||||
canvas.drawRect(
|
||||
Rect.fromLTWH(0, 0, size.width, size.height),
|
||||
Paint()..shader = shader,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant OrbShaderPainter oldDelegate) {
|
||||
return oldDelegate.shader != shader ||
|
||||
oldDelegate.config != config ||
|
||||
oldDelegate.time != time ||
|
||||
oldDelegate.mousePos != mousePos ||
|
||||
oldDelegate.energy != energy;
|
||||
}
|
||||
}
|
||||
123
next_gen_ui_demo/lib/orb_shader/orb_shader_widget.dart
Normal file
123
next_gen_ui_demo/lib/orb_shader/orb_shader_widget.dart
Normal file
@@ -0,0 +1,123 @@
|
||||
// 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 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../assets.dart';
|
||||
import '../common/reactive_widget.dart';
|
||||
import 'orb_shader_config.dart';
|
||||
import 'orb_shader_painter.dart';
|
||||
|
||||
class OrbShaderWidget extends StatefulWidget {
|
||||
const OrbShaderWidget({
|
||||
super.key,
|
||||
required this.config,
|
||||
this.onUpdate,
|
||||
required this.mousePos,
|
||||
required this.minEnergy,
|
||||
});
|
||||
|
||||
final double minEnergy;
|
||||
final OrbShaderConfig config;
|
||||
final Offset mousePos;
|
||||
final void Function(double energy)? onUpdate;
|
||||
|
||||
@override
|
||||
State<OrbShaderWidget> createState() => OrbShaderWidgetState();
|
||||
}
|
||||
|
||||
class OrbShaderWidgetState extends State<OrbShaderWidget>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final _heartbeatSequence = TweenSequence(
|
||||
[
|
||||
TweenSequenceItem(tween: ConstantTween(0), weight: 40),
|
||||
TweenSequenceItem(
|
||||
tween: Tween(begin: 0.0, end: 1.0)
|
||||
.chain(CurveTween(curve: Curves.easeInOutCubic)),
|
||||
weight: 8),
|
||||
TweenSequenceItem(
|
||||
tween: Tween(begin: 1.0, end: 0.2)
|
||||
.chain(CurveTween(curve: Curves.easeInOutCubic)),
|
||||
weight: 12),
|
||||
TweenSequenceItem(
|
||||
tween: Tween(begin: 0.2, end: 0.8)
|
||||
.chain(CurveTween(curve: Curves.easeInOutCubic)),
|
||||
weight: 6),
|
||||
TweenSequenceItem(
|
||||
tween: Tween(begin: 0.8, end: 0.0)
|
||||
.chain(CurveTween(curve: Curves.easeInOutCubic)),
|
||||
weight: 10),
|
||||
],
|
||||
);
|
||||
|
||||
late final AnimationController _heartbeatAnim;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_heartbeatAnim = AnimationController(vsync: this, duration: 3000.ms)
|
||||
..repeat();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_heartbeatAnim.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Consumer<Shaders?>(
|
||||
builder: (context, shaders, _) {
|
||||
if (shaders == null) return const SizedBox.expand();
|
||||
return ListenableBuilder(
|
||||
listenable: _heartbeatAnim,
|
||||
builder: (_, __) {
|
||||
final heartbeatEnergy =
|
||||
_heartbeatAnim.drive(_heartbeatSequence).value;
|
||||
return TweenAnimationBuilder(
|
||||
tween: Tween<double>(
|
||||
begin: widget.minEnergy, end: widget.minEnergy),
|
||||
duration: 300.ms,
|
||||
curve: Curves.easeOutCubic,
|
||||
builder: (context, minEnergy, child) {
|
||||
return ReactiveWidget(
|
||||
builder: (context, time, size) {
|
||||
double energyLevel = 0;
|
||||
if (size.shortestSide != 0) {
|
||||
final d = (Offset(size.width, size.height) / 2 -
|
||||
widget.mousePos)
|
||||
.distance;
|
||||
final hitSize = size.shortestSide * .5;
|
||||
energyLevel = 1 - min(1, (d / hitSize));
|
||||
scheduleMicrotask(
|
||||
() => widget.onUpdate?.call(energyLevel));
|
||||
}
|
||||
energyLevel +=
|
||||
(1.3 - energyLevel) * heartbeatEnergy * 0.1;
|
||||
energyLevel = lerpDouble(minEnergy, 1, energyLevel)!;
|
||||
return CustomPaint(
|
||||
size: size,
|
||||
painter: OrbShaderPainter(
|
||||
shaders.orb,
|
||||
config: widget.config,
|
||||
time: time,
|
||||
mousePos: widget.mousePos,
|
||||
energy: energyLevel,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
35
next_gen_ui_demo/lib/styles.dart
Normal file
35
next_gen_ui_demo/lib/styles.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
// 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:flutter/material.dart';
|
||||
|
||||
class TextStyles {
|
||||
static const _font1 = TextStyle(fontFamily: 'Exo', color: Colors.white);
|
||||
|
||||
static TextStyle get h1 => _font1.copyWith(
|
||||
fontSize: 75, letterSpacing: 35, fontWeight: FontWeight.w700);
|
||||
static TextStyle get h2 => h1.copyWith(fontSize: 40, letterSpacing: 0);
|
||||
static TextStyle get h3 =>
|
||||
h1.copyWith(fontSize: 24, letterSpacing: 20, fontWeight: FontWeight.w400);
|
||||
static TextStyle get body => _font1.copyWith(fontSize: 16);
|
||||
static TextStyle get btn => _font1.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 10,
|
||||
);
|
||||
}
|
||||
|
||||
abstract class AppColors {
|
||||
static const orbColors = [
|
||||
Color(0xFF71FDBF),
|
||||
Color(0xFFCE33FF),
|
||||
Color(0xFFFF5033),
|
||||
];
|
||||
|
||||
static const emitColors = [
|
||||
Color(0xFF96FF33),
|
||||
Color(0xFF00FFFF),
|
||||
Color(0xFFFF993E),
|
||||
];
|
||||
}
|
||||
8
next_gen_ui_demo/lib/title_screen/title_screen.dart
Normal file
8
next_gen_ui_demo/lib/title_screen/title_screen.dart
Normal file
@@ -0,0 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../main.dart';
|
||||
|
||||
abstract class TitleScreenBase extends StatefulWidget {
|
||||
const TitleScreenBase({super.key, required this.callback});
|
||||
final ColorCallback callback;
|
||||
}
|
||||
36
next_gen_ui_demo/lib/title_screen_1a/title_screen.dart
Normal file
36
next_gen_ui_demo/lib/title_screen_1a/title_screen.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
// 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:flutter/material.dart';
|
||||
|
||||
import '../assets.dart';
|
||||
import '../styles.dart';
|
||||
import '../title_screen/title_screen.dart';
|
||||
|
||||
class TitleScreen extends TitleScreenBase {
|
||||
const TitleScreen({super.key, required super.callback});
|
||||
|
||||
@override
|
||||
State<TitleScreen> createState() => _TitleScreenState();
|
||||
}
|
||||
|
||||
class _TitleScreenState extends State<TitleScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(duration) => widget.callback(AppColors.orbColors[0]));
|
||||
|
||||
return Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
/// Bg-Base
|
||||
Image.asset(AssetPaths.titleBgBase),
|
||||
|
||||
/// Bg-Receive
|
||||
Image.asset(AssetPaths.titleBgReceive),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
45
next_gen_ui_demo/lib/title_screen_1b/title_screen.dart
Normal file
45
next_gen_ui_demo/lib/title_screen_1b/title_screen.dart
Normal file
@@ -0,0 +1,45 @@
|
||||
// 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:flutter/material.dart';
|
||||
|
||||
import '../assets.dart';
|
||||
import '../styles.dart';
|
||||
import '../title_screen/title_screen.dart';
|
||||
|
||||
class TitleScreen extends TitleScreenBase {
|
||||
const TitleScreen({super.key, required super.callback});
|
||||
|
||||
@override
|
||||
State<TitleScreen> createState() => _TitleScreenState();
|
||||
}
|
||||
|
||||
class _TitleScreenState extends State<TitleScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(duration) => widget.callback(AppColors.orbColors[0]));
|
||||
|
||||
return Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
/// Bg-Base
|
||||
Image.asset(AssetPaths.titleBgBase),
|
||||
|
||||
/// Bg-Receive
|
||||
Image.asset(AssetPaths.titleBgReceive),
|
||||
|
||||
/// Mg-Base
|
||||
Image.asset(AssetPaths.titleMgBase),
|
||||
|
||||
/// Mg-Receive
|
||||
Image.asset(AssetPaths.titleMgReceive),
|
||||
|
||||
/// Mg-Emit
|
||||
Image.asset(AssetPaths.titleMgEmit),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
54
next_gen_ui_demo/lib/title_screen_2a/title_screen.dart
Normal file
54
next_gen_ui_demo/lib/title_screen_2a/title_screen.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
// 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:flutter/material.dart';
|
||||
|
||||
import '../assets.dart';
|
||||
import '../styles.dart';
|
||||
import '../title_screen/title_screen.dart';
|
||||
|
||||
class TitleScreen extends TitleScreenBase {
|
||||
const TitleScreen({super.key, required super.callback});
|
||||
|
||||
@override
|
||||
State<TitleScreen> createState() => _TitleScreenState();
|
||||
}
|
||||
|
||||
class _TitleScreenState extends State<TitleScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(duration) => widget.callback(AppColors.orbColors[0]));
|
||||
|
||||
return Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
/// Bg-Base
|
||||
Image.asset(AssetPaths.titleBgBase),
|
||||
|
||||
/// Bg-Receive
|
||||
Image.asset(AssetPaths.titleBgReceive),
|
||||
|
||||
/// Mg-Base
|
||||
Image.asset(AssetPaths.titleMgBase),
|
||||
|
||||
/// Mg-Receive
|
||||
Image.asset(AssetPaths.titleMgReceive),
|
||||
|
||||
/// Mg-Emit
|
||||
Image.asset(AssetPaths.titleMgEmit),
|
||||
|
||||
/// Fg-Rocks
|
||||
Image.asset(AssetPaths.titleFgBase),
|
||||
|
||||
/// Fg-Receive
|
||||
Image.asset(AssetPaths.titleFgReceive),
|
||||
|
||||
/// Fg-Emit
|
||||
Image.asset(AssetPaths.titleFgEmit),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
106
next_gen_ui_demo/lib/title_screen_2c/title_screen.dart
Normal file
106
next_gen_ui_demo/lib/title_screen_2c/title_screen.dart
Normal file
@@ -0,0 +1,106 @@
|
||||
// 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:flutter/material.dart';
|
||||
|
||||
import '../assets.dart';
|
||||
import '../styles.dart';
|
||||
import '../title_screen/title_screen.dart';
|
||||
|
||||
class TitleScreen extends TitleScreenBase {
|
||||
const TitleScreen({super.key, required super.callback});
|
||||
|
||||
final _finalReceiveLightAmt = 0.7;
|
||||
final _finalEmitLightAmt = 0.5;
|
||||
|
||||
@override
|
||||
State<TitleScreen> createState() => _TitleScreenState();
|
||||
}
|
||||
|
||||
class _TitleScreenState extends State<TitleScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final orbColor = AppColors.orbColors[0];
|
||||
final emitColor = AppColors.emitColors[0];
|
||||
WidgetsBinding.instance
|
||||
.addPostFrameCallback((duration) => widget.callback(orbColor));
|
||||
|
||||
return Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
/// Bg-Base
|
||||
Image.asset(AssetPaths.titleBgBase),
|
||||
|
||||
/// Bg-Receive
|
||||
_LitImage(
|
||||
color: orbColor,
|
||||
imgSrc: AssetPaths.titleBgReceive,
|
||||
lightAmt: widget._finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Base
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgBase,
|
||||
color: orbColor,
|
||||
lightAmt: widget._finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Receive
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgReceive,
|
||||
color: orbColor,
|
||||
lightAmt: widget._finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Emit
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgEmit,
|
||||
color: emitColor,
|
||||
lightAmt: widget._finalEmitLightAmt,
|
||||
),
|
||||
|
||||
/// Fg-Rocks
|
||||
Image.asset(AssetPaths.titleFgBase),
|
||||
|
||||
/// Fg-Receive
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleFgReceive,
|
||||
color: orbColor,
|
||||
lightAmt: widget._finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Fg-Emit
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleFgEmit,
|
||||
color: emitColor,
|
||||
lightAmt: widget._finalEmitLightAmt,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LitImage extends StatelessWidget {
|
||||
const _LitImage({
|
||||
required this.color,
|
||||
required this.imgSrc,
|
||||
required this.lightAmt,
|
||||
});
|
||||
final Color color;
|
||||
final String imgSrc;
|
||||
final double lightAmt;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hsl = HSLColor.fromColor(color);
|
||||
return ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(
|
||||
hsl.withLightness(hsl.lightness * lightAmt).toColor(),
|
||||
BlendMode.modulate,
|
||||
),
|
||||
child: Image.asset(imgSrc),
|
||||
);
|
||||
}
|
||||
}
|
||||
112
next_gen_ui_demo/lib/title_screen_3a/title_screen.dart
Normal file
112
next_gen_ui_demo/lib/title_screen_3a/title_screen.dart
Normal file
@@ -0,0 +1,112 @@
|
||||
// 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:flutter/material.dart';
|
||||
|
||||
import '../assets.dart';
|
||||
import '../styles.dart';
|
||||
import '../title_screen/title_screen.dart';
|
||||
import 'title_screen_ui.dart';
|
||||
|
||||
class TitleScreen extends TitleScreenBase {
|
||||
const TitleScreen({super.key, required super.callback});
|
||||
|
||||
final _finalReceiveLightAmt = 0.7;
|
||||
final _finalEmitLightAmt = 0.5;
|
||||
|
||||
@override
|
||||
State<TitleScreen> createState() => _TitleScreenState();
|
||||
}
|
||||
|
||||
class _TitleScreenState extends State<TitleScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final orbColor = AppColors.orbColors[0];
|
||||
final emitColor = AppColors.emitColors[0];
|
||||
WidgetsBinding.instance
|
||||
.addPostFrameCallback((duration) => widget.callback(orbColor));
|
||||
|
||||
return Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
/// Bg-Base
|
||||
Image.asset(AssetPaths.titleBgBase),
|
||||
|
||||
/// Bg-Receive
|
||||
_LitImage(
|
||||
color: orbColor,
|
||||
imgSrc: AssetPaths.titleBgReceive,
|
||||
lightAmt: widget._finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Base
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgBase,
|
||||
color: orbColor,
|
||||
lightAmt: widget._finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Receive
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgReceive,
|
||||
color: orbColor,
|
||||
lightAmt: widget._finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Emit
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgEmit,
|
||||
color: emitColor,
|
||||
lightAmt: widget._finalEmitLightAmt,
|
||||
),
|
||||
|
||||
/// Fg-Rocks
|
||||
Image.asset(AssetPaths.titleFgBase),
|
||||
|
||||
/// Fg-Receive
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleFgReceive,
|
||||
color: orbColor,
|
||||
lightAmt: widget._finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Fg-Emit
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleFgEmit,
|
||||
color: emitColor,
|
||||
lightAmt: widget._finalEmitLightAmt,
|
||||
),
|
||||
|
||||
/// UI
|
||||
const Positioned.fill(
|
||||
child: TitleScreenUi(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LitImage extends StatelessWidget {
|
||||
const _LitImage({
|
||||
required this.color,
|
||||
required this.imgSrc,
|
||||
required this.lightAmt,
|
||||
});
|
||||
final Color color;
|
||||
final String imgSrc;
|
||||
final double lightAmt;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hsl = HSLColor.fromColor(color);
|
||||
return ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(
|
||||
hsl.withLightness(hsl.lightness * lightAmt).toColor(),
|
||||
BlendMode.modulate,
|
||||
),
|
||||
child: Image.asset(imgSrc),
|
||||
);
|
||||
}
|
||||
}
|
||||
62
next_gen_ui_demo/lib/title_screen_3a/title_screen_ui.dart
Normal file
62
next_gen_ui_demo/lib/title_screen_3a/title_screen_ui.dart
Normal file
@@ -0,0 +1,62 @@
|
||||
// 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:gap/gap.dart';
|
||||
|
||||
import '../assets.dart';
|
||||
import '../common/ui_scaler.dart';
|
||||
import '../styles.dart';
|
||||
|
||||
class TitleScreenUi extends StatelessWidget {
|
||||
const TitleScreenUi({
|
||||
super.key,
|
||||
});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 40, horizontal: 50),
|
||||
child: Stack(
|
||||
children: [
|
||||
/// Title Text
|
||||
TopLeft(
|
||||
child: UiScaler(
|
||||
alignment: Alignment.topLeft,
|
||||
child: _TitleText(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TitleText extends StatelessWidget {
|
||||
const _TitleText();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return 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),
|
||||
],
|
||||
),
|
||||
Text('INTO THE UNKNOWN', style: TextStyles.h3),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
139
next_gen_ui_demo/lib/title_screen_3b/title_screen.dart
Normal file
139
next_gen_ui_demo/lib/title_screen_3b/title_screen.dart
Normal file
@@ -0,0 +1,139 @@
|
||||
// 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:flutter/material.dart';
|
||||
|
||||
import '../assets.dart';
|
||||
import '../styles.dart';
|
||||
import '../title_screen/title_screen.dart';
|
||||
import 'title_screen_ui.dart';
|
||||
|
||||
class TitleScreen extends TitleScreenBase {
|
||||
const TitleScreen({super.key, required super.callback});
|
||||
|
||||
@override
|
||||
State<TitleScreen> createState() => _TitleScreenState();
|
||||
}
|
||||
|
||||
class _TitleScreenState extends State<TitleScreen> {
|
||||
Color get _emitColor =>
|
||||
AppColors.emitColors[_difficultyOverride ?? _difficulty];
|
||||
Color get _orbColor =>
|
||||
AppColors.orbColors[_difficultyOverride ?? _difficulty];
|
||||
|
||||
/// Currently selected difficulty
|
||||
int _difficulty = 0;
|
||||
|
||||
/// Currently focused difficulty (if any)
|
||||
int? _difficultyOverride;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance
|
||||
.addPostFrameCallback((duration) => widget.callback(_orbColor));
|
||||
}
|
||||
|
||||
void _handleDifficultyPressed(int value) {
|
||||
setState(() => _difficulty = value);
|
||||
widget.callback(_orbColor);
|
||||
}
|
||||
|
||||
void _handleDifficultyFocused(int? value) {
|
||||
setState(() => _difficultyOverride = value);
|
||||
widget.callback(_orbColor);
|
||||
}
|
||||
|
||||
final _finalReceiveLightAmt = 0.7;
|
||||
final _finalEmitLightAmt = 0.5;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
/// Bg-Base
|
||||
Image.asset(AssetPaths.titleBgBase),
|
||||
|
||||
/// Bg-Receive
|
||||
_LitImage(
|
||||
color: _orbColor,
|
||||
imgSrc: AssetPaths.titleBgReceive,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Base
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgBase,
|
||||
color: _orbColor,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Receive
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgReceive,
|
||||
color: _orbColor,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Emit
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgEmit,
|
||||
color: _emitColor,
|
||||
lightAmt: _finalEmitLightAmt,
|
||||
),
|
||||
|
||||
/// Fg-Rocks
|
||||
Image.asset(AssetPaths.titleFgBase),
|
||||
|
||||
/// Fg-Receive
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleFgReceive,
|
||||
color: _orbColor,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Fg-Emit
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleFgEmit,
|
||||
color: _emitColor,
|
||||
lightAmt: _finalEmitLightAmt,
|
||||
),
|
||||
|
||||
/// UI
|
||||
Positioned.fill(
|
||||
child: TitleScreenUi(
|
||||
difficulty: _difficulty,
|
||||
onDifficultyFocused: _handleDifficultyFocused,
|
||||
onDifficultyPressed: _handleDifficultyPressed,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LitImage extends StatelessWidget {
|
||||
const _LitImage({
|
||||
required this.color,
|
||||
required this.imgSrc,
|
||||
required this.lightAmt,
|
||||
});
|
||||
final Color color;
|
||||
final String imgSrc;
|
||||
final double lightAmt;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hsl = HSLColor.fromColor(color);
|
||||
return ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(
|
||||
hsl.withLightness(hsl.lightness * lightAmt).toColor(),
|
||||
BlendMode.modulate,
|
||||
),
|
||||
child: Image.asset(imgSrc),
|
||||
);
|
||||
}
|
||||
}
|
||||
187
next_gen_ui_demo/lib/title_screen_3b/title_screen_ui.dart
Normal file
187
next_gen_ui_demo/lib/title_screen_3b/title_screen_ui.dart
Normal file
@@ -0,0 +1,187 @@
|
||||
// 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:focusable_control_builder/focusable_control_builder.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
|
||||
import '../assets.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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TitleText extends StatelessWidget {
|
||||
const _TitleText();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return 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),
|
||||
],
|
||||
),
|
||||
Text('INTO THE UNKNOWN', style: TextStyles.h3),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
),
|
||||
_DifficultyBtn(
|
||||
label: 'Normal',
|
||||
selected: difficulty == 1,
|
||||
onPressed: () => onDifficultyPressed(1),
|
||||
onHover: (over) => onDifficultyFocused(over ? 1 : null),
|
||||
),
|
||||
_DifficultyBtn(
|
||||
label: 'Hardcore',
|
||||
selected: difficulty == 2,
|
||||
onPressed: () => onDifficultyPressed(2),
|
||||
onHover: (over) => onDifficultyFocused(over ? 2 : null),
|
||||
),
|
||||
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
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
139
next_gen_ui_demo/lib/title_screen_3c/title_screen.dart
Normal file
139
next_gen_ui_demo/lib/title_screen_3c/title_screen.dart
Normal file
@@ -0,0 +1,139 @@
|
||||
// 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:flutter/material.dart';
|
||||
|
||||
import '../assets.dart';
|
||||
import '../styles.dart';
|
||||
import '../title_screen/title_screen.dart';
|
||||
import 'title_screen_ui.dart';
|
||||
|
||||
class TitleScreen extends TitleScreenBase {
|
||||
const TitleScreen({super.key, required super.callback});
|
||||
|
||||
@override
|
||||
State<TitleScreen> createState() => _TitleScreenState();
|
||||
}
|
||||
|
||||
class _TitleScreenState extends State<TitleScreen> {
|
||||
Color get _emitColor =>
|
||||
AppColors.emitColors[_difficultyOverride ?? _difficulty];
|
||||
Color get _orbColor =>
|
||||
AppColors.orbColors[_difficultyOverride ?? _difficulty];
|
||||
|
||||
/// Currently selected difficulty
|
||||
int _difficulty = 0;
|
||||
|
||||
/// Currently focused difficulty (if any)
|
||||
int? _difficultyOverride;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance
|
||||
.addPostFrameCallback((duration) => widget.callback(_orbColor));
|
||||
}
|
||||
|
||||
void _handleDifficultyPressed(int value) {
|
||||
setState(() => _difficulty = value);
|
||||
widget.callback(_orbColor);
|
||||
}
|
||||
|
||||
void _handleDifficultyFocused(int? value) {
|
||||
setState(() => _difficultyOverride = value);
|
||||
widget.callback(_orbColor);
|
||||
}
|
||||
|
||||
final _finalReceiveLightAmt = 0.7;
|
||||
final _finalEmitLightAmt = 0.5;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
/// Bg-Base
|
||||
Image.asset(AssetPaths.titleBgBase),
|
||||
|
||||
/// Bg-Receive
|
||||
_LitImage(
|
||||
color: _orbColor,
|
||||
imgSrc: AssetPaths.titleBgReceive,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Base
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgBase,
|
||||
color: _orbColor,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Receive
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgReceive,
|
||||
color: _orbColor,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Emit
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgEmit,
|
||||
color: _emitColor,
|
||||
lightAmt: _finalEmitLightAmt,
|
||||
),
|
||||
|
||||
/// Fg-Rocks
|
||||
Image.asset(AssetPaths.titleFgBase),
|
||||
|
||||
/// Fg-Receive
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleFgReceive,
|
||||
color: _orbColor,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Fg-Emit
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleFgEmit,
|
||||
color: _emitColor,
|
||||
lightAmt: _finalEmitLightAmt,
|
||||
),
|
||||
|
||||
/// UI
|
||||
Positioned.fill(
|
||||
child: TitleScreenUi(
|
||||
difficulty: _difficulty,
|
||||
onDifficultyFocused: _handleDifficultyFocused,
|
||||
onDifficultyPressed: _handleDifficultyPressed,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LitImage extends StatelessWidget {
|
||||
const _LitImage({
|
||||
required this.color,
|
||||
required this.imgSrc,
|
||||
required this.lightAmt,
|
||||
});
|
||||
final Color color;
|
||||
final String imgSrc;
|
||||
final double lightAmt;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hsl = HSLColor.fromColor(color);
|
||||
return ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(
|
||||
hsl.withLightness(hsl.lightness * lightAmt).toColor(),
|
||||
BlendMode.modulate,
|
||||
),
|
||||
child: Image.asset(imgSrc),
|
||||
);
|
||||
}
|
||||
}
|
||||
250
next_gen_ui_demo/lib/title_screen_3c/title_screen_ui.dart
Normal file
250
next_gen_ui_demo/lib/title_screen_3c/title_screen_ui.dart
Normal file
@@ -0,0 +1,250 @@
|
||||
// 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:focusable_control_builder/focusable_control_builder.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
|
||||
import '../assets.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) {
|
||||
return 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),
|
||||
],
|
||||
),
|
||||
Text('INTO THE UNKNOWN', style: TextStyles.h3),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
),
|
||||
_DifficultyBtn(
|
||||
label: 'Normal',
|
||||
selected: difficulty == 1,
|
||||
onPressed: () => onDifficultyPressed(1),
|
||||
onHover: (over) => onDifficultyFocused(over ? 1 : null),
|
||||
),
|
||||
_DifficultyBtn(
|
||||
label: 'Hardcore',
|
||||
selected: difficulty == 2,
|
||||
onPressed: () => onDifficultyPressed(2),
|
||||
onHover: (over) => onDifficultyFocused(over ? 2 : null),
|
||||
),
|
||||
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
|
||||
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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
139
next_gen_ui_demo/lib/title_screen_4a/title_screen.dart
Normal file
139
next_gen_ui_demo/lib/title_screen_4a/title_screen.dart
Normal file
@@ -0,0 +1,139 @@
|
||||
// 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:flutter/material.dart';
|
||||
|
||||
import '../assets.dart';
|
||||
import '../styles.dart';
|
||||
import '../title_screen/title_screen.dart';
|
||||
import 'title_screen_ui.dart';
|
||||
|
||||
class TitleScreen extends TitleScreenBase {
|
||||
const TitleScreen({super.key, required super.callback});
|
||||
|
||||
@override
|
||||
State<TitleScreen> createState() => _TitleScreenState();
|
||||
}
|
||||
|
||||
class _TitleScreenState extends State<TitleScreen> {
|
||||
Color get _emitColor =>
|
||||
AppColors.emitColors[_difficultyOverride ?? _difficulty];
|
||||
Color get _orbColor =>
|
||||
AppColors.orbColors[_difficultyOverride ?? _difficulty];
|
||||
|
||||
/// Currently selected difficulty
|
||||
int _difficulty = 0;
|
||||
|
||||
/// Currently focused difficulty (if any)
|
||||
int? _difficultyOverride;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance
|
||||
.addPostFrameCallback((duration) => widget.callback(_orbColor));
|
||||
}
|
||||
|
||||
void _handleDifficultyPressed(int value) {
|
||||
setState(() => _difficulty = value);
|
||||
widget.callback(_orbColor);
|
||||
}
|
||||
|
||||
void _handleDifficultyFocused(int? value) {
|
||||
setState(() => _difficultyOverride = value);
|
||||
widget.callback(_orbColor);
|
||||
}
|
||||
|
||||
final _finalReceiveLightAmt = 0.7;
|
||||
final _finalEmitLightAmt = 0.5;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
/// Bg-Base
|
||||
Image.asset(AssetPaths.titleBgBase),
|
||||
|
||||
/// Bg-Receive
|
||||
_LitImage(
|
||||
color: _orbColor,
|
||||
imgSrc: AssetPaths.titleBgReceive,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Base
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgBase,
|
||||
color: _orbColor,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Receive
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgReceive,
|
||||
color: _orbColor,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Emit
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgEmit,
|
||||
color: _emitColor,
|
||||
lightAmt: _finalEmitLightAmt,
|
||||
),
|
||||
|
||||
/// Fg-Rocks
|
||||
Image.asset(AssetPaths.titleFgBase),
|
||||
|
||||
/// Fg-Receive
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleFgReceive,
|
||||
color: _orbColor,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Fg-Emit
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleFgEmit,
|
||||
color: _emitColor,
|
||||
lightAmt: _finalEmitLightAmt,
|
||||
),
|
||||
|
||||
/// UI
|
||||
Positioned.fill(
|
||||
child: TitleScreenUi(
|
||||
difficulty: _difficulty,
|
||||
onDifficultyFocused: _handleDifficultyFocused,
|
||||
onDifficultyPressed: _handleDifficultyPressed,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LitImage extends StatelessWidget {
|
||||
const _LitImage({
|
||||
required this.color,
|
||||
required this.imgSrc,
|
||||
required this.lightAmt,
|
||||
});
|
||||
final Color color;
|
||||
final String imgSrc;
|
||||
final double lightAmt;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hsl = HSLColor.fromColor(color);
|
||||
return ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(
|
||||
hsl.withLightness(hsl.lightness * lightAmt).toColor(),
|
||||
BlendMode.modulate,
|
||||
),
|
||||
child: Image.asset(imgSrc),
|
||||
);
|
||||
}
|
||||
}
|
||||
253
next_gen_ui_demo/lib/title_screen_4a/title_screen_ui.dart
Normal file
253
next_gen_ui_demo/lib/title_screen_4a/title_screen_ui.dart
Normal file
@@ -0,0 +1,253 @@
|
||||
// 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 '../assets.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) {
|
||||
return 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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
),
|
||||
_DifficultyBtn(
|
||||
label: 'Normal',
|
||||
selected: difficulty == 1,
|
||||
onPressed: () => onDifficultyPressed(1),
|
||||
onHover: (over) => onDifficultyFocused(over ? 1 : null),
|
||||
),
|
||||
_DifficultyBtn(
|
||||
label: 'Hardcore',
|
||||
selected: difficulty == 2,
|
||||
onPressed: () => onDifficultyPressed(2),
|
||||
onHover: (over) => onDifficultyFocused(over ? 2 : null),
|
||||
),
|
||||
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
|
||||
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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
139
next_gen_ui_demo/lib/title_screen_4b/title_screen.dart
Normal file
139
next_gen_ui_demo/lib/title_screen_4b/title_screen.dart
Normal file
@@ -0,0 +1,139 @@
|
||||
// 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:flutter/material.dart';
|
||||
|
||||
import '../assets.dart';
|
||||
import '../styles.dart';
|
||||
import '../title_screen/title_screen.dart';
|
||||
import 'title_screen_ui.dart';
|
||||
|
||||
class TitleScreen extends TitleScreenBase {
|
||||
const TitleScreen({super.key, required super.callback});
|
||||
|
||||
@override
|
||||
State<TitleScreen> createState() => _TitleScreenState();
|
||||
}
|
||||
|
||||
class _TitleScreenState extends State<TitleScreen> {
|
||||
Color get _emitColor =>
|
||||
AppColors.emitColors[_difficultyOverride ?? _difficulty];
|
||||
Color get _orbColor =>
|
||||
AppColors.orbColors[_difficultyOverride ?? _difficulty];
|
||||
|
||||
/// Currently selected difficulty
|
||||
int _difficulty = 0;
|
||||
|
||||
/// Currently focused difficulty (if any)
|
||||
int? _difficultyOverride;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance
|
||||
.addPostFrameCallback((duration) => widget.callback(_orbColor));
|
||||
}
|
||||
|
||||
void _handleDifficultyPressed(int value) {
|
||||
setState(() => _difficulty = value);
|
||||
widget.callback(_orbColor);
|
||||
}
|
||||
|
||||
void _handleDifficultyFocused(int? value) {
|
||||
setState(() => _difficultyOverride = value);
|
||||
widget.callback(_orbColor);
|
||||
}
|
||||
|
||||
final _finalReceiveLightAmt = 0.7;
|
||||
final _finalEmitLightAmt = 0.5;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
/// Bg-Base
|
||||
Image.asset(AssetPaths.titleBgBase),
|
||||
|
||||
/// Bg-Receive
|
||||
_LitImage(
|
||||
color: _orbColor,
|
||||
imgSrc: AssetPaths.titleBgReceive,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Base
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgBase,
|
||||
color: _orbColor,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Receive
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgReceive,
|
||||
color: _orbColor,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Emit
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgEmit,
|
||||
color: _emitColor,
|
||||
lightAmt: _finalEmitLightAmt,
|
||||
),
|
||||
|
||||
/// Fg-Rocks
|
||||
Image.asset(AssetPaths.titleFgBase),
|
||||
|
||||
/// Fg-Receive
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleFgReceive,
|
||||
color: _orbColor,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Fg-Emit
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleFgEmit,
|
||||
color: _emitColor,
|
||||
lightAmt: _finalEmitLightAmt,
|
||||
),
|
||||
|
||||
/// UI
|
||||
Positioned.fill(
|
||||
child: TitleScreenUi(
|
||||
difficulty: _difficulty,
|
||||
onDifficultyFocused: _handleDifficultyFocused,
|
||||
onDifficultyPressed: _handleDifficultyPressed,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LitImage extends StatelessWidget {
|
||||
const _LitImage({
|
||||
required this.color,
|
||||
required this.imgSrc,
|
||||
required this.lightAmt,
|
||||
});
|
||||
final Color color;
|
||||
final String imgSrc;
|
||||
final double lightAmt;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hsl = HSLColor.fromColor(color);
|
||||
return ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(
|
||||
hsl.withLightness(hsl.lightness * lightAmt).toColor(),
|
||||
BlendMode.modulate,
|
||||
),
|
||||
child: Image.asset(imgSrc),
|
||||
);
|
||||
}
|
||||
}
|
||||
262
next_gen_ui_demo/lib/title_screen_4b/title_screen_ui.dart
Normal file
262
next_gen_ui_demo/lib/title_screen_4b/title_screen_ui.dart
Normal file
@@ -0,0 +1,262 @@
|
||||
// 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 '../assets.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) {
|
||||
return 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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
139
next_gen_ui_demo/lib/title_screen_4c/title_screen.dart
Normal file
139
next_gen_ui_demo/lib/title_screen_4c/title_screen.dart
Normal file
@@ -0,0 +1,139 @@
|
||||
// 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:flutter/material.dart';
|
||||
|
||||
import '../assets.dart';
|
||||
import '../styles.dart';
|
||||
import '../title_screen/title_screen.dart';
|
||||
import 'title_screen_ui.dart';
|
||||
|
||||
class TitleScreen extends TitleScreenBase {
|
||||
const TitleScreen({super.key, required super.callback});
|
||||
|
||||
@override
|
||||
State<TitleScreen> createState() => _TitleScreenState();
|
||||
}
|
||||
|
||||
class _TitleScreenState extends State<TitleScreen> {
|
||||
Color get _emitColor =>
|
||||
AppColors.emitColors[_difficultyOverride ?? _difficulty];
|
||||
Color get _orbColor =>
|
||||
AppColors.orbColors[_difficultyOverride ?? _difficulty];
|
||||
|
||||
/// Currently selected difficulty
|
||||
int _difficulty = 0;
|
||||
|
||||
/// Currently focused difficulty (if any)
|
||||
int? _difficultyOverride;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance
|
||||
.addPostFrameCallback((duration) => widget.callback(_orbColor));
|
||||
}
|
||||
|
||||
void _handleDifficultyPressed(int value) {
|
||||
setState(() => _difficulty = value);
|
||||
widget.callback(_orbColor);
|
||||
}
|
||||
|
||||
void _handleDifficultyFocused(int? value) {
|
||||
setState(() => _difficultyOverride = value);
|
||||
widget.callback(_orbColor);
|
||||
}
|
||||
|
||||
final _finalReceiveLightAmt = 0.7;
|
||||
final _finalEmitLightAmt = 0.5;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
/// Bg-Base
|
||||
Image.asset(AssetPaths.titleBgBase),
|
||||
|
||||
/// Bg-Receive
|
||||
_LitImage(
|
||||
color: _orbColor,
|
||||
imgSrc: AssetPaths.titleBgReceive,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Base
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgBase,
|
||||
color: _orbColor,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Receive
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgReceive,
|
||||
color: _orbColor,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Emit
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgEmit,
|
||||
color: _emitColor,
|
||||
lightAmt: _finalEmitLightAmt,
|
||||
),
|
||||
|
||||
/// Fg-Rocks
|
||||
Image.asset(AssetPaths.titleFgBase),
|
||||
|
||||
/// Fg-Receive
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleFgReceive,
|
||||
color: _orbColor,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Fg-Emit
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleFgEmit,
|
||||
color: _emitColor,
|
||||
lightAmt: _finalEmitLightAmt,
|
||||
),
|
||||
|
||||
/// UI
|
||||
Positioned.fill(
|
||||
child: TitleScreenUi(
|
||||
difficulty: _difficulty,
|
||||
onDifficultyFocused: _handleDifficultyFocused,
|
||||
onDifficultyPressed: _handleDifficultyPressed,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LitImage extends StatelessWidget {
|
||||
const _LitImage({
|
||||
required this.color,
|
||||
required this.imgSrc,
|
||||
required this.lightAmt,
|
||||
});
|
||||
final Color color;
|
||||
final String imgSrc;
|
||||
final double lightAmt;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hsl = HSLColor.fromColor(color);
|
||||
return ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(
|
||||
hsl.withLightness(hsl.lightness * lightAmt).toColor(),
|
||||
BlendMode.modulate,
|
||||
),
|
||||
child: Image.asset(imgSrc),
|
||||
);
|
||||
}
|
||||
}
|
||||
267
next_gen_ui_demo/lib/title_screen_4c/title_screen_ui.dart
Normal file
267
next_gen_ui_demo/lib/title_screen_4c/title_screen_ui.dart
Normal file
@@ -0,0 +1,267 @@
|
||||
// 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 '../assets.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) {
|
||||
return 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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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) => _btnAnim = c)
|
||||
.shimmer(duration: .7.seconds, color: Colors.black),
|
||||
)
|
||||
.animate()
|
||||
.fadeIn(delay: 2.3.seconds)
|
||||
.slide(begin: const Offset(0, .2));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
139
next_gen_ui_demo/lib/title_screen_4d/title_screen.dart
Normal file
139
next_gen_ui_demo/lib/title_screen_4d/title_screen.dart
Normal file
@@ -0,0 +1,139 @@
|
||||
// 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:flutter/material.dart';
|
||||
|
||||
import '../assets.dart';
|
||||
import '../styles.dart';
|
||||
import '../title_screen/title_screen.dart';
|
||||
import 'title_screen_ui.dart';
|
||||
|
||||
class TitleScreen extends TitleScreenBase {
|
||||
const TitleScreen({super.key, required super.callback});
|
||||
|
||||
@override
|
||||
State<TitleScreen> createState() => _TitleScreenState();
|
||||
}
|
||||
|
||||
class _TitleScreenState extends State<TitleScreen> {
|
||||
Color get _emitColor =>
|
||||
AppColors.emitColors[_difficultyOverride ?? _difficulty];
|
||||
Color get _orbColor =>
|
||||
AppColors.orbColors[_difficultyOverride ?? _difficulty];
|
||||
|
||||
/// Currently selected difficulty
|
||||
int _difficulty = 0;
|
||||
|
||||
/// Currently focused difficulty (if any)
|
||||
int? _difficultyOverride;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance
|
||||
.addPostFrameCallback((duration) => widget.callback(_orbColor));
|
||||
}
|
||||
|
||||
void _handleDifficultyPressed(int value) {
|
||||
setState(() => _difficulty = value);
|
||||
widget.callback(_orbColor);
|
||||
}
|
||||
|
||||
void _handleDifficultyFocused(int? value) {
|
||||
setState(() => _difficultyOverride = value);
|
||||
widget.callback(_orbColor);
|
||||
}
|
||||
|
||||
final _finalReceiveLightAmt = 0.7;
|
||||
final _finalEmitLightAmt = 0.5;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
/// Bg-Base
|
||||
Image.asset(AssetPaths.titleBgBase),
|
||||
|
||||
/// Bg-Receive
|
||||
_LitImage(
|
||||
color: _orbColor,
|
||||
imgSrc: AssetPaths.titleBgReceive,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Base
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgBase,
|
||||
color: _orbColor,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Receive
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgReceive,
|
||||
color: _orbColor,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Emit
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgEmit,
|
||||
color: _emitColor,
|
||||
lightAmt: _finalEmitLightAmt,
|
||||
),
|
||||
|
||||
/// Fg-Rocks
|
||||
Image.asset(AssetPaths.titleFgBase),
|
||||
|
||||
/// Fg-Receive
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleFgReceive,
|
||||
color: _orbColor,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Fg-Emit
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleFgEmit,
|
||||
color: _emitColor,
|
||||
lightAmt: _finalEmitLightAmt,
|
||||
),
|
||||
|
||||
/// UI
|
||||
Positioned.fill(
|
||||
child: TitleScreenUi(
|
||||
difficulty: _difficulty,
|
||||
onDifficultyFocused: _handleDifficultyFocused,
|
||||
onDifficultyPressed: _handleDifficultyPressed,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LitImage extends StatelessWidget {
|
||||
const _LitImage({
|
||||
required this.color,
|
||||
required this.imgSrc,
|
||||
required this.lightAmt,
|
||||
});
|
||||
final Color color;
|
||||
final String imgSrc;
|
||||
final double lightAmt;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hsl = HSLColor.fromColor(color);
|
||||
return ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(
|
||||
hsl.withLightness(hsl.lightness * lightAmt).toColor(),
|
||||
BlendMode.modulate,
|
||||
),
|
||||
child: Image.asset(imgSrc),
|
||||
);
|
||||
}
|
||||
}
|
||||
273
next_gen_ui_demo/lib/title_screen_4d/title_screen_ui.dart
Normal file
273
next_gen_ui_demo/lib/title_screen_4d/title_screen_ui.dart
Normal file
@@ -0,0 +1,273 @@
|
||||
// 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 '../assets.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) {
|
||||
return 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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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) => _btnAnim = c)
|
||||
.shimmer(duration: .7.seconds, color: Colors.black),
|
||||
)
|
||||
.animate()
|
||||
.fadeIn(delay: 2.3.seconds)
|
||||
.slide(begin: const Offset(0, .2));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
178
next_gen_ui_demo/lib/title_screen_4e/title_screen.dart
Normal file
178
next_gen_ui_demo/lib/title_screen_4e/title_screen.dart
Normal file
@@ -0,0 +1,178 @@
|
||||
// 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:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
|
||||
import '../assets.dart';
|
||||
import '../styles.dart';
|
||||
import '../title_screen/title_screen.dart';
|
||||
import 'title_screen_ui.dart';
|
||||
|
||||
class TitleScreen extends TitleScreenBase {
|
||||
const TitleScreen({super.key, required super.callback});
|
||||
|
||||
@override
|
||||
State<TitleScreen> createState() => _TitleScreenState();
|
||||
}
|
||||
|
||||
class _TitleScreenState extends State<TitleScreen> {
|
||||
Color get _emitColor =>
|
||||
AppColors.emitColors[_difficultyOverride ?? _difficulty];
|
||||
Color get _orbColor =>
|
||||
AppColors.orbColors[_difficultyOverride ?? _difficulty];
|
||||
|
||||
/// Currently selected difficulty
|
||||
int _difficulty = 0;
|
||||
|
||||
/// Currently focused difficulty (if any)
|
||||
int? _difficultyOverride;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance
|
||||
.addPostFrameCallback((duration) => widget.callback(_orbColor));
|
||||
}
|
||||
|
||||
void _handleDifficultyPressed(int value) {
|
||||
setState(() => _difficulty = value);
|
||||
widget.callback(_orbColor);
|
||||
}
|
||||
|
||||
void _handleDifficultyFocused(int? value) {
|
||||
setState(() => _difficultyOverride = value);
|
||||
widget.callback(_orbColor);
|
||||
}
|
||||
|
||||
final _finalReceiveLightAmt = 0.7;
|
||||
final _finalEmitLightAmt = 0.5;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: _AnimatedColors(
|
||||
orbColor: _orbColor,
|
||||
emitColor: _emitColor,
|
||||
builder: (_, orbColor, emitColor) {
|
||||
return Stack(
|
||||
children: [
|
||||
/// Bg-Base
|
||||
Image.asset(AssetPaths.titleBgBase),
|
||||
|
||||
/// Bg-Receive
|
||||
_LitImage(
|
||||
color: orbColor,
|
||||
imgSrc: AssetPaths.titleBgReceive,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Base
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgBase,
|
||||
color: orbColor,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Receive
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgReceive,
|
||||
color: orbColor,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Emit
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgEmit,
|
||||
color: emitColor,
|
||||
lightAmt: _finalEmitLightAmt,
|
||||
),
|
||||
|
||||
/// Fg-Rocks
|
||||
Image.asset(AssetPaths.titleFgBase),
|
||||
|
||||
/// Fg-Receive
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleFgReceive,
|
||||
color: orbColor,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Fg-Emit
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleFgEmit,
|
||||
color: emitColor,
|
||||
lightAmt: _finalEmitLightAmt,
|
||||
),
|
||||
|
||||
/// UI
|
||||
Positioned.fill(
|
||||
child: TitleScreenUi(
|
||||
difficulty: _difficulty,
|
||||
onDifficultyFocused: _handleDifficultyFocused,
|
||||
onDifficultyPressed: _handleDifficultyPressed,
|
||||
),
|
||||
),
|
||||
],
|
||||
).animate().fadeIn(duration: 1.seconds, delay: .3.seconds);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LitImage extends StatelessWidget {
|
||||
const _LitImage({
|
||||
required this.color,
|
||||
required this.imgSrc,
|
||||
required this.lightAmt,
|
||||
});
|
||||
final Color color;
|
||||
final String imgSrc;
|
||||
final double lightAmt;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hsl = HSLColor.fromColor(color);
|
||||
return ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(
|
||||
hsl.withLightness(hsl.lightness * lightAmt).toColor(),
|
||||
BlendMode.modulate,
|
||||
),
|
||||
child: Image.asset(imgSrc),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AnimatedColors extends StatelessWidget {
|
||||
const _AnimatedColors({
|
||||
required this.emitColor,
|
||||
required this.orbColor,
|
||||
required this.builder,
|
||||
});
|
||||
|
||||
final Color emitColor;
|
||||
final Color orbColor;
|
||||
|
||||
final Widget Function(BuildContext context, Color orbColor, Color emitColor)
|
||||
builder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final duration = .5.seconds;
|
||||
return TweenAnimationBuilder(
|
||||
tween: ColorTween(begin: emitColor, end: emitColor),
|
||||
duration: duration,
|
||||
builder: (_, emitColor, __) {
|
||||
return TweenAnimationBuilder(
|
||||
tween: ColorTween(begin: orbColor, end: orbColor),
|
||||
duration: duration,
|
||||
builder: (context, orbColor, __) {
|
||||
return builder(context, orbColor!, emitColor!);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
273
next_gen_ui_demo/lib/title_screen_4e/title_screen_ui.dart
Normal file
273
next_gen_ui_demo/lib/title_screen_4e/title_screen_ui.dart
Normal file
@@ -0,0 +1,273 @@
|
||||
// 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 '../assets.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) {
|
||||
return 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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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) => _btnAnim = c)
|
||||
.shimmer(duration: .7.seconds, color: Colors.black),
|
||||
)
|
||||
.animate()
|
||||
.fadeIn(delay: 2.3.seconds)
|
||||
.slide(begin: const Offset(0, .2));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
178
next_gen_ui_demo/lib/title_screen_5a/title_screen.dart
Normal file
178
next_gen_ui_demo/lib/title_screen_5a/title_screen.dart
Normal file
@@ -0,0 +1,178 @@
|
||||
// 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:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
|
||||
import '../assets.dart';
|
||||
import '../styles.dart';
|
||||
import '../title_screen/title_screen.dart';
|
||||
import 'title_screen_ui.dart';
|
||||
|
||||
class TitleScreen extends TitleScreenBase {
|
||||
const TitleScreen({super.key, required super.callback});
|
||||
|
||||
@override
|
||||
State<TitleScreen> createState() => _TitleScreenState();
|
||||
}
|
||||
|
||||
class _TitleScreenState extends State<TitleScreen> {
|
||||
Color get _emitColor =>
|
||||
AppColors.emitColors[_difficultyOverride ?? _difficulty];
|
||||
Color get _orbColor =>
|
||||
AppColors.orbColors[_difficultyOverride ?? _difficulty];
|
||||
|
||||
/// Currently selected difficulty
|
||||
int _difficulty = 0;
|
||||
|
||||
/// Currently focused difficulty (if any)
|
||||
int? _difficultyOverride;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance
|
||||
.addPostFrameCallback((duration) => widget.callback(_orbColor));
|
||||
}
|
||||
|
||||
void _handleDifficultyPressed(int value) {
|
||||
setState(() => _difficulty = value);
|
||||
widget.callback(_orbColor);
|
||||
}
|
||||
|
||||
void _handleDifficultyFocused(int? value) {
|
||||
setState(() => _difficultyOverride = value);
|
||||
widget.callback(_orbColor);
|
||||
}
|
||||
|
||||
final _finalReceiveLightAmt = 0.7;
|
||||
final _finalEmitLightAmt = 0.5;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: _AnimatedColors(
|
||||
orbColor: _orbColor,
|
||||
emitColor: _emitColor,
|
||||
builder: (_, orbColor, emitColor) {
|
||||
return Stack(
|
||||
children: [
|
||||
/// Bg-Base
|
||||
Image.asset(AssetPaths.titleBgBase),
|
||||
|
||||
/// Bg-Receive
|
||||
_LitImage(
|
||||
color: orbColor,
|
||||
imgSrc: AssetPaths.titleBgReceive,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Base
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgBase,
|
||||
color: orbColor,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Receive
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgReceive,
|
||||
color: orbColor,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Emit
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgEmit,
|
||||
color: emitColor,
|
||||
lightAmt: _finalEmitLightAmt,
|
||||
),
|
||||
|
||||
/// Fg-Rocks
|
||||
Image.asset(AssetPaths.titleFgBase),
|
||||
|
||||
/// Fg-Receive
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleFgReceive,
|
||||
color: orbColor,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Fg-Emit
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleFgEmit,
|
||||
color: emitColor,
|
||||
lightAmt: _finalEmitLightAmt,
|
||||
),
|
||||
|
||||
/// UI
|
||||
Positioned.fill(
|
||||
child: TitleScreenUi(
|
||||
difficulty: _difficulty,
|
||||
onDifficultyFocused: _handleDifficultyFocused,
|
||||
onDifficultyPressed: _handleDifficultyPressed,
|
||||
),
|
||||
),
|
||||
],
|
||||
).animate().fadeIn(duration: 1.seconds, delay: .3.seconds);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LitImage extends StatelessWidget {
|
||||
const _LitImage({
|
||||
required this.color,
|
||||
required this.imgSrc,
|
||||
required this.lightAmt,
|
||||
});
|
||||
final Color color;
|
||||
final String imgSrc;
|
||||
final double lightAmt;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hsl = HSLColor.fromColor(color);
|
||||
return ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(
|
||||
hsl.withLightness(hsl.lightness * lightAmt).toColor(),
|
||||
BlendMode.modulate,
|
||||
),
|
||||
child: Image.asset(imgSrc),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AnimatedColors extends StatelessWidget {
|
||||
const _AnimatedColors({
|
||||
required this.emitColor,
|
||||
required this.orbColor,
|
||||
required this.builder,
|
||||
});
|
||||
|
||||
final Color emitColor;
|
||||
final Color orbColor;
|
||||
|
||||
final Widget Function(BuildContext context, Color orbColor, Color emitColor)
|
||||
builder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final duration = .5.seconds;
|
||||
return TweenAnimationBuilder(
|
||||
tween: ColorTween(begin: emitColor, end: emitColor),
|
||||
duration: duration,
|
||||
builder: (_, emitColor, __) {
|
||||
return TweenAnimationBuilder(
|
||||
tween: ColorTween(begin: orbColor, end: orbColor),
|
||||
duration: duration,
|
||||
builder: (context, orbColor, __) {
|
||||
return builder(context, orbColor!, emitColor!);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
299
next_gen_ui_demo/lib/title_screen_5a/title_screen_ui.dart
Normal file
299
next_gen_ui_demo/lib/title_screen_5a/title_screen_ui.dart
Normal file
@@ -0,0 +1,299 @@
|
||||
// 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) => _btnAnim = c)
|
||||
.shimmer(duration: .7.seconds, color: Colors.black),
|
||||
)
|
||||
.animate()
|
||||
.fadeIn(delay: 2.3.seconds)
|
||||
.slide(begin: const Offset(0, .2));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
312
next_gen_ui_demo/lib/title_screen_5b/title_screen.dart
Normal file
312
next_gen_ui_demo/lib/title_screen_5b/title_screen.dart
Normal file
@@ -0,0 +1,312 @@
|
||||
// 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 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
|
||||
import '../assets.dart';
|
||||
import '../orb_shader/orb_shader_config.dart';
|
||||
import '../orb_shader/orb_shader_widget.dart';
|
||||
import '../styles.dart';
|
||||
import '../title_screen/title_screen.dart';
|
||||
import 'title_screen_ui.dart';
|
||||
|
||||
class TitleScreen extends TitleScreenBase {
|
||||
const TitleScreen({super.key, required super.callback});
|
||||
|
||||
@override
|
||||
State<TitleScreen> createState() => _TitleScreenState();
|
||||
}
|
||||
|
||||
class _TitleScreenState extends State<TitleScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final _orbKey = GlobalKey<OrbShaderWidgetState>();
|
||||
|
||||
/// Editable Settings
|
||||
/// 0-1, receive lighting strength
|
||||
final _minReceiveLightAmt = .35;
|
||||
final _maxReceiveLightAmt = .7;
|
||||
|
||||
/// 0-1, emit lighting strength
|
||||
final _minEmitLightAmt = .5;
|
||||
final _maxEmitLightAmt = 1;
|
||||
|
||||
/// Internal
|
||||
var _mousePos = Offset.zero;
|
||||
|
||||
Color get _emitColor =>
|
||||
AppColors.emitColors[_difficultyOverride ?? _difficulty];
|
||||
Color get _orbColor =>
|
||||
AppColors.orbColors[_difficultyOverride ?? _difficulty];
|
||||
|
||||
/// Currently selected difficulty
|
||||
int _difficulty = 0;
|
||||
|
||||
/// Currently focused difficulty (if any)
|
||||
int? _difficultyOverride;
|
||||
double _orbEnergy = 0;
|
||||
double _minOrbEnergy = 0;
|
||||
|
||||
double get _finalReceiveLightAmt {
|
||||
final light =
|
||||
lerpDouble(_minReceiveLightAmt, _maxReceiveLightAmt, _orbEnergy) ?? 0;
|
||||
return light + _pulseEffect.value * .05 * _orbEnergy;
|
||||
}
|
||||
|
||||
double get _finalEmitLightAmt {
|
||||
return lerpDouble(_minEmitLightAmt, _maxEmitLightAmt, _orbEnergy) ?? 0;
|
||||
}
|
||||
|
||||
late final AnimationController _pulseEffect;
|
||||
|
||||
Duration _getRndPulseDuration() => 100.ms + 200.ms * Random().nextDouble();
|
||||
|
||||
double _getMinEnergyForDifficulty(int difficulty) {
|
||||
if (difficulty == 1) {
|
||||
return .3;
|
||||
} else if (difficulty == 2) {
|
||||
return .6;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pulseEffect = AnimationController(
|
||||
vsync: this,
|
||||
duration: _getRndPulseDuration(),
|
||||
lowerBound: -1,
|
||||
upperBound: 1,
|
||||
);
|
||||
_pulseEffect.forward();
|
||||
_pulseEffect.addListener(_handlePulseEffectUpdate);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pulseEffect.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handlePulseEffectUpdate() {
|
||||
if (_pulseEffect.status == AnimationStatus.completed) {
|
||||
_pulseEffect.reverse();
|
||||
_pulseEffect.duration = _getRndPulseDuration();
|
||||
} else if (_pulseEffect.status == AnimationStatus.dismissed) {
|
||||
_pulseEffect.duration = _getRndPulseDuration();
|
||||
_pulseEffect.forward();
|
||||
}
|
||||
}
|
||||
|
||||
void _handleDifficultyPressed(int value) {
|
||||
setState(() => _difficulty = value);
|
||||
_bumpMinEnergy();
|
||||
}
|
||||
|
||||
Future<void> _bumpMinEnergy([double amount = 0.1]) async {
|
||||
setState(() {
|
||||
_minOrbEnergy = _getMinEnergyForDifficulty(_difficulty) + amount;
|
||||
});
|
||||
await Future<void>.delayed(.2.seconds);
|
||||
setState(() {
|
||||
_minOrbEnergy = _getMinEnergyForDifficulty(_difficulty);
|
||||
});
|
||||
}
|
||||
|
||||
void _handleStartPressed() => _bumpMinEnergy(0.3);
|
||||
|
||||
void _handleDifficultyFocused(int? value) {
|
||||
setState(() {
|
||||
_difficultyOverride = value;
|
||||
if (value == null) {
|
||||
_minOrbEnergy = _getMinEnergyForDifficulty(_difficulty);
|
||||
} else {
|
||||
_minOrbEnergy = _getMinEnergyForDifficulty(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Update mouse position so the orbWidget can use it, doing it here prevents
|
||||
/// btns from blocking the mouse-move events in the widget itself.
|
||||
void _handleMouseMove(PointerHoverEvent e) {
|
||||
setState(() {
|
||||
_mousePos = e.localPosition;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: MouseRegion(
|
||||
onHover: _handleMouseMove,
|
||||
child: _AnimatedColors(
|
||||
orbColor: _orbColor,
|
||||
emitColor: _emitColor,
|
||||
builder: (_, orbColor, emitColor) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
widget.callback(orbColor);
|
||||
});
|
||||
return Stack(
|
||||
children: [
|
||||
/// Bg-Base
|
||||
Image.asset(AssetPaths.titleBgBase),
|
||||
|
||||
/// Bg-Receive
|
||||
_LitImage(
|
||||
color: orbColor,
|
||||
imgSrc: AssetPaths.titleBgReceive,
|
||||
pulseEffect: _pulseEffect,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Orb
|
||||
Positioned.fill(
|
||||
child: Stack(
|
||||
children: [
|
||||
// Orb
|
||||
OrbShaderWidget(
|
||||
key: _orbKey,
|
||||
mousePos: _mousePos,
|
||||
minEnergy: _minOrbEnergy,
|
||||
config: OrbShaderConfig(
|
||||
ambientLightColor: orbColor,
|
||||
materialColor: orbColor,
|
||||
lightColor: orbColor,
|
||||
),
|
||||
onUpdate: (energy) => setState(() {
|
||||
_orbEnergy = energy;
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
/// Mg-Base
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgBase,
|
||||
color: orbColor,
|
||||
pulseEffect: _pulseEffect,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Receive
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgReceive,
|
||||
color: orbColor,
|
||||
pulseEffect: _pulseEffect,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Emit
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgEmit,
|
||||
color: emitColor,
|
||||
pulseEffect: _pulseEffect,
|
||||
lightAmt: _finalEmitLightAmt,
|
||||
),
|
||||
|
||||
/// Fg-Rocks
|
||||
Image.asset(AssetPaths.titleFgBase),
|
||||
|
||||
/// Fg-Receive
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleFgReceive,
|
||||
color: orbColor,
|
||||
pulseEffect: _pulseEffect,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Fg-Emit
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleFgEmit,
|
||||
color: emitColor,
|
||||
pulseEffect: _pulseEffect,
|
||||
lightAmt: _finalEmitLightAmt,
|
||||
),
|
||||
|
||||
/// UI
|
||||
Positioned.fill(
|
||||
child: TitleScreenUi(
|
||||
difficulty: _difficulty,
|
||||
onDifficultyFocused: _handleDifficultyFocused,
|
||||
onDifficultyPressed: _handleDifficultyPressed,
|
||||
onStartPressed: _handleStartPressed,
|
||||
),
|
||||
),
|
||||
],
|
||||
).animate().fadeIn(duration: 1.seconds, delay: .3.seconds);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LitImage extends StatelessWidget {
|
||||
const _LitImage({
|
||||
required this.color,
|
||||
required this.imgSrc,
|
||||
required this.pulseEffect,
|
||||
required this.lightAmt,
|
||||
});
|
||||
final Color color;
|
||||
final String imgSrc;
|
||||
final AnimationController pulseEffect;
|
||||
final double lightAmt;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hsl = HSLColor.fromColor(color);
|
||||
return ListenableBuilder(
|
||||
listenable: pulseEffect,
|
||||
child: Image.asset(imgSrc),
|
||||
builder: (context, child) {
|
||||
return ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(
|
||||
hsl.withLightness(hsl.lightness * lightAmt).toColor(),
|
||||
BlendMode.modulate,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AnimatedColors extends StatelessWidget {
|
||||
const _AnimatedColors({
|
||||
required this.emitColor,
|
||||
required this.orbColor,
|
||||
required this.builder,
|
||||
});
|
||||
|
||||
final Color emitColor;
|
||||
final Color orbColor;
|
||||
|
||||
final Widget Function(BuildContext context, Color orbColor, Color emitColor)
|
||||
builder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final duration = .5.seconds;
|
||||
return TweenAnimationBuilder(
|
||||
tween: ColorTween(begin: emitColor, end: emitColor),
|
||||
duration: duration,
|
||||
builder: (_, emitColor, __) {
|
||||
return TweenAnimationBuilder(
|
||||
tween: ColorTween(begin: orbColor, end: orbColor),
|
||||
duration: duration,
|
||||
builder: (context, orbColor, __) {
|
||||
return builder(context, orbColor!, emitColor!);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
301
next_gen_ui_demo/lib/title_screen_5b/title_screen_ui.dart
Normal file
301
next_gen_ui_demo/lib/title_screen_5b/title_screen_ui.dart
Normal file
@@ -0,0 +1,301 @@
|
||||
// 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,
|
||||
required this.onStartPressed,
|
||||
});
|
||||
|
||||
final int difficulty;
|
||||
final void Function(int difficulty) onDifficultyPressed;
|
||||
final void Function(int? difficulty) onDifficultyFocused;
|
||||
final VoidCallback onStartPressed;
|
||||
|
||||
@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: onStartPressed),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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) => _btnAnim = c)
|
||||
.shimmer(duration: .7.seconds, color: Colors.black),
|
||||
)
|
||||
.animate()
|
||||
.fadeIn(delay: 2.3.seconds)
|
||||
.slide(begin: const Offset(0, .2));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
67
next_gen_ui_demo/lib/title_screen_6/particle_overlay.dart
Normal file
67
next_gen_ui_demo/lib/title_screen_6/particle_overlay.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
// 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 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:particle_field/particle_field.dart';
|
||||
import 'package:rnd/rnd.dart';
|
||||
|
||||
class ParticleOverlay extends StatelessWidget {
|
||||
const ParticleOverlay({super.key, required this.color, required this.energy});
|
||||
|
||||
final Color color;
|
||||
final double energy;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ParticleField(
|
||||
spriteSheet: SpriteSheet(
|
||||
image: const AssetImage('assets/images/particle-wave.png'),
|
||||
),
|
||||
// blend the image's alpha with the specified color:
|
||||
blendMode: BlendMode.dstIn,
|
||||
|
||||
// this runs every tick:
|
||||
onTick: (controller, _, size) {
|
||||
List<Particle> particles = controller.particles;
|
||||
|
||||
// add a new particle with random angle, distance & velocity:
|
||||
double a = rnd(pi * 2);
|
||||
double dist = rnd(1, 4) * 35 + 150 * energy;
|
||||
double vel = rnd(1, 2) * (1 + energy * 1.8);
|
||||
particles.add(Particle(
|
||||
// how many ticks this particle will live:
|
||||
lifespan: rnd(1, 2) * 20 + energy * 15,
|
||||
// starting distance from center:
|
||||
x: cos(a) * dist,
|
||||
y: sin(a) * dist,
|
||||
// starting velocity:
|
||||
vx: cos(a) * vel,
|
||||
vy: sin(a) * vel,
|
||||
// other starting values:
|
||||
rotation: a,
|
||||
scale: rnd(1, 2) * 0.6 + energy * 0.5,
|
||||
));
|
||||
|
||||
// update all of the particles:
|
||||
for (int i = particles.length - 1; i >= 0; i--) {
|
||||
Particle p = particles[i];
|
||||
if (p.lifespan <= 0) {
|
||||
// particle is expired, remove it:
|
||||
particles.removeAt(i);
|
||||
continue;
|
||||
}
|
||||
p.update(
|
||||
scale: p.scale * 1.025,
|
||||
vx: p.vx * 1.025,
|
||||
vy: p.vy * 1.025,
|
||||
color: color.withOpacity(p.lifespan * 0.001 + 0.01),
|
||||
lifespan: p.lifespan - 1,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
323
next_gen_ui_demo/lib/title_screen_6/title_screen.dart
Normal file
323
next_gen_ui_demo/lib/title_screen_6/title_screen.dart
Normal file
@@ -0,0 +1,323 @@
|
||||
// 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 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
|
||||
import '../assets.dart';
|
||||
import '../orb_shader/orb_shader_config.dart';
|
||||
import '../orb_shader/orb_shader_widget.dart';
|
||||
import '../styles.dart';
|
||||
import '../title_screen/title_screen.dart';
|
||||
import 'particle_overlay.dart';
|
||||
import 'title_screen_ui.dart';
|
||||
|
||||
class TitleScreen extends TitleScreenBase {
|
||||
const TitleScreen({super.key, required super.callback});
|
||||
|
||||
@override
|
||||
State<TitleScreen> createState() => _TitleScreenState();
|
||||
}
|
||||
|
||||
class _TitleScreenState extends State<TitleScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final _orbKey = GlobalKey<OrbShaderWidgetState>();
|
||||
|
||||
/// Editable Settings
|
||||
/// 0-1, receive lighting strength
|
||||
final _minReceiveLightAmt = .35;
|
||||
final _maxReceiveLightAmt = .7;
|
||||
|
||||
/// 0-1, emit lighting strength
|
||||
final _minEmitLightAmt = .5;
|
||||
final _maxEmitLightAmt = 1;
|
||||
|
||||
/// Internal
|
||||
var _mousePos = Offset.zero;
|
||||
|
||||
Color get _emitColor =>
|
||||
AppColors.emitColors[_difficultyOverride ?? _difficulty];
|
||||
Color get _orbColor =>
|
||||
AppColors.orbColors[_difficultyOverride ?? _difficulty];
|
||||
|
||||
/// Currently selected difficulty
|
||||
int _difficulty = 0;
|
||||
|
||||
/// Currently focused difficulty (if any)
|
||||
int? _difficultyOverride;
|
||||
double _orbEnergy = 0;
|
||||
double _minOrbEnergy = 0;
|
||||
|
||||
double get _finalReceiveLightAmt {
|
||||
final light =
|
||||
lerpDouble(_minReceiveLightAmt, _maxReceiveLightAmt, _orbEnergy) ?? 0;
|
||||
return light + _pulseEffect.value * .05 * _orbEnergy;
|
||||
}
|
||||
|
||||
double get _finalEmitLightAmt {
|
||||
return lerpDouble(_minEmitLightAmt, _maxEmitLightAmt, _orbEnergy) ?? 0;
|
||||
}
|
||||
|
||||
late final AnimationController _pulseEffect;
|
||||
|
||||
Duration _getRndPulseDuration() => 100.ms + 200.ms * Random().nextDouble();
|
||||
|
||||
double _getMinEnergyForDifficulty(int difficulty) {
|
||||
if (difficulty == 1) {
|
||||
return .3;
|
||||
} else if (difficulty == 2) {
|
||||
return .6;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pulseEffect = AnimationController(
|
||||
vsync: this,
|
||||
duration: _getRndPulseDuration(),
|
||||
lowerBound: -1,
|
||||
upperBound: 1,
|
||||
);
|
||||
_pulseEffect.forward();
|
||||
_pulseEffect.addListener(_handlePulseEffectUpdate);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pulseEffect.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handlePulseEffectUpdate() {
|
||||
if (_pulseEffect.status == AnimationStatus.completed) {
|
||||
_pulseEffect.reverse();
|
||||
_pulseEffect.duration = _getRndPulseDuration();
|
||||
} else if (_pulseEffect.status == AnimationStatus.dismissed) {
|
||||
_pulseEffect.duration = _getRndPulseDuration();
|
||||
_pulseEffect.forward();
|
||||
}
|
||||
}
|
||||
|
||||
void _handleDifficultyPressed(int value) {
|
||||
setState(() => _difficulty = value);
|
||||
_bumpMinEnergy();
|
||||
}
|
||||
|
||||
Future<void> _bumpMinEnergy([double amount = 0.1]) async {
|
||||
setState(() {
|
||||
_minOrbEnergy = _getMinEnergyForDifficulty(_difficulty) + amount;
|
||||
});
|
||||
await Future<void>.delayed(.2.seconds);
|
||||
setState(() {
|
||||
_minOrbEnergy = _getMinEnergyForDifficulty(_difficulty);
|
||||
});
|
||||
}
|
||||
|
||||
void _handleStartPressed() => _bumpMinEnergy(0.3);
|
||||
|
||||
void _handleDifficultyFocused(int? value) {
|
||||
setState(() {
|
||||
_difficultyOverride = value;
|
||||
if (value == null) {
|
||||
_minOrbEnergy = _getMinEnergyForDifficulty(_difficulty);
|
||||
} else {
|
||||
_minOrbEnergy = _getMinEnergyForDifficulty(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Update mouse position so the orbWidget can use it, doing it here prevents
|
||||
/// btns from blocking the mouse-move events in the widget itself.
|
||||
void _handleMouseMove(PointerHoverEvent e) {
|
||||
setState(() {
|
||||
_mousePos = e.localPosition;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: MouseRegion(
|
||||
onHover: _handleMouseMove,
|
||||
child: _AnimatedColors(
|
||||
orbColor: _orbColor,
|
||||
emitColor: _emitColor,
|
||||
builder: (_, orbColor, emitColor) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
widget.callback(orbColor);
|
||||
});
|
||||
return Stack(
|
||||
children: [
|
||||
/// Bg-Base
|
||||
Image.asset(AssetPaths.titleBgBase),
|
||||
|
||||
/// Bg-Receive
|
||||
_LitImage(
|
||||
color: orbColor,
|
||||
imgSrc: AssetPaths.titleBgReceive,
|
||||
pulseEffect: _pulseEffect,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Orb
|
||||
Positioned.fill(
|
||||
child: Stack(
|
||||
children: [
|
||||
// Orb
|
||||
OrbShaderWidget(
|
||||
key: _orbKey,
|
||||
mousePos: _mousePos,
|
||||
minEnergy: _minOrbEnergy,
|
||||
config: OrbShaderConfig(
|
||||
ambientLightColor: orbColor,
|
||||
materialColor: orbColor,
|
||||
lightColor: orbColor,
|
||||
),
|
||||
onUpdate: (energy) => setState(() {
|
||||
_orbEnergy = energy;
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
/// Mg-Base
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgBase,
|
||||
color: orbColor,
|
||||
pulseEffect: _pulseEffect,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Receive
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgReceive,
|
||||
color: orbColor,
|
||||
pulseEffect: _pulseEffect,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Emit
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgEmit,
|
||||
color: emitColor,
|
||||
pulseEffect: _pulseEffect,
|
||||
lightAmt: _finalEmitLightAmt,
|
||||
),
|
||||
|
||||
/// Particle Field
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: ParticleOverlay(
|
||||
color: orbColor,
|
||||
energy: _orbEnergy,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
/// Fg-Rocks
|
||||
Image.asset(AssetPaths.titleFgBase),
|
||||
|
||||
/// Fg-Receive
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleFgReceive,
|
||||
color: orbColor,
|
||||
pulseEffect: _pulseEffect,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Fg-Emit
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleFgEmit,
|
||||
color: emitColor,
|
||||
pulseEffect: _pulseEffect,
|
||||
lightAmt: _finalEmitLightAmt,
|
||||
),
|
||||
|
||||
/// UI
|
||||
Positioned.fill(
|
||||
child: TitleScreenUi(
|
||||
difficulty: _difficulty,
|
||||
onDifficultyFocused: _handleDifficultyFocused,
|
||||
onDifficultyPressed: _handleDifficultyPressed,
|
||||
onStartPressed: _handleStartPressed,
|
||||
),
|
||||
),
|
||||
],
|
||||
).animate().fadeIn(duration: 1.seconds, delay: .3.seconds);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LitImage extends StatelessWidget {
|
||||
const _LitImage({
|
||||
required this.color,
|
||||
required this.imgSrc,
|
||||
required this.pulseEffect,
|
||||
required this.lightAmt,
|
||||
});
|
||||
final Color color;
|
||||
final String imgSrc;
|
||||
final AnimationController pulseEffect;
|
||||
final double lightAmt;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hsl = HSLColor.fromColor(color);
|
||||
return ListenableBuilder(
|
||||
listenable: pulseEffect,
|
||||
child: Image.asset(imgSrc),
|
||||
builder: (context, child) {
|
||||
return ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(
|
||||
hsl.withLightness(hsl.lightness * lightAmt).toColor(),
|
||||
BlendMode.modulate,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AnimatedColors extends StatelessWidget {
|
||||
const _AnimatedColors({
|
||||
required this.emitColor,
|
||||
required this.orbColor,
|
||||
required this.builder,
|
||||
});
|
||||
|
||||
final Color emitColor;
|
||||
final Color orbColor;
|
||||
|
||||
final Widget Function(BuildContext context, Color orbColor, Color emitColor)
|
||||
builder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final duration = .5.seconds;
|
||||
return TweenAnimationBuilder(
|
||||
tween: ColorTween(begin: emitColor, end: emitColor),
|
||||
duration: duration,
|
||||
builder: (_, emitColor, __) {
|
||||
return TweenAnimationBuilder(
|
||||
tween: ColorTween(begin: orbColor, end: orbColor),
|
||||
duration: duration,
|
||||
builder: (context, orbColor, __) {
|
||||
return builder(context, orbColor!, emitColor!);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
301
next_gen_ui_demo/lib/title_screen_6/title_screen_ui.dart
Normal file
301
next_gen_ui_demo/lib/title_screen_6/title_screen_ui.dart
Normal file
@@ -0,0 +1,301 @@
|
||||
// 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,
|
||||
required this.onStartPressed,
|
||||
});
|
||||
|
||||
final int difficulty;
|
||||
final void Function(int difficulty) onDifficultyPressed;
|
||||
final void Function(int? difficulty) onDifficultyFocused;
|
||||
final VoidCallback onStartPressed;
|
||||
|
||||
@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: onStartPressed),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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) => _btnAnim = c)
|
||||
.shimmer(duration: .7.seconds, color: Colors.black),
|
||||
)
|
||||
.animate()
|
||||
.fadeIn(delay: 2.3.seconds)
|
||||
.slide(begin: const Offset(0, .2));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user