mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-01 10:38:17 +08:00
Effect controllers restructuring (#1134)
* Update effectController
* move effect controllers into the controllers/ directory
* Add .forward property to EffectController
* SimpleEffectController supports reverse time
* Fixing some compile errors
* rename SimpleEffectController -> LinearEffectController
* minor cleanup
* DurationEffectController and PauseEffectController
* ReverseLinearEffectController
* CurvedEffectController and its reverse
* InfiniteEffectController
* Added EffectController.recede()
* Add EffectController.update()
* Add InfiniteEffectController'
* RepeatedEffectController
* SequenceEffectController
* DelayedEffectController
* Restore the [EffectController.started] property
* minor
* Rename reset() -> setToStart()
* time direction is now managed from the Effect class
* StandardEffectController replaced with function standardController()
* update some doc-comments
* flutter analyze
* flutter format
* fix some tests
* more test fixes
* fix remaining tests
* format
* rename local variable
* minor simplification
* Expand docs in PauseEffectController
* added tests
* Curved controller test
* fix errors
* formatting
* added more tests
* format
* fix RepeatedEffectController
* more tests
* format
* changelog
* increase tolerance
* Replaced standardController with factory EffectController constructor
* Added parameter EffectController({alternate=false})
* Added default for curve= parameter
* rename
* rename tests
* added more exports
* rename tests
* rename src/effects2
Co-authored-by: Lukas Klingsbo <lukas.klingsbo@gmail.com>
This commit is contained in:
@ -188,7 +188,7 @@ class Rock extends SpriteComponent
|
||||
add(
|
||||
ScaleEffect.by(
|
||||
Vector2.all(10),
|
||||
StandardEffectController(duration: 0.3),
|
||||
EffectController(duration: 0.3),
|
||||
),
|
||||
);
|
||||
return true;
|
||||
|
||||
@ -65,7 +65,7 @@ class SimpleShapesExample extends FlameGame with HasTappables {
|
||||
component.add(
|
||||
MoveEffect.to(
|
||||
size / 2,
|
||||
StandardEffectController(
|
||||
EffectController(
|
||||
duration: 5,
|
||||
reverseDuration: 5,
|
||||
infinite: true,
|
||||
@ -75,7 +75,7 @@ class SimpleShapesExample extends FlameGame with HasTappables {
|
||||
component.add(
|
||||
RotateEffect.to(
|
||||
3,
|
||||
StandardEffectController(
|
||||
EffectController(
|
||||
duration: 1,
|
||||
reverseDuration: 1,
|
||||
infinite: true,
|
||||
|
||||
@ -40,7 +40,7 @@ class MoveEffectExample extends FlameGame {
|
||||
)..add(
|
||||
MoveEffect.to(
|
||||
Vector2(380, 50),
|
||||
StandardEffectController(
|
||||
EffectController(
|
||||
duration: 3,
|
||||
reverseDuration: 3,
|
||||
infinite: true,
|
||||
@ -58,7 +58,7 @@ class MoveEffectExample extends FlameGame {
|
||||
..add(
|
||||
MoveEffect.to(
|
||||
Vector2(380, 150),
|
||||
StandardEffectController(
|
||||
EffectController(
|
||||
duration: 3,
|
||||
reverseDuration: 3,
|
||||
infinite: true,
|
||||
@ -68,7 +68,7 @@ class MoveEffectExample extends FlameGame {
|
||||
..add(
|
||||
MoveEffect.by(
|
||||
Vector2(0, -50),
|
||||
StandardEffectController(
|
||||
EffectController(
|
||||
duration: 0.25,
|
||||
reverseDuration: 0.25,
|
||||
startDelay: 1,
|
||||
@ -92,7 +92,7 @@ class MoveEffectExample extends FlameGame {
|
||||
..add(
|
||||
MoveEffect.along(
|
||||
path1,
|
||||
StandardEffectController(
|
||||
EffectController(
|
||||
duration: 10,
|
||||
startDelay: i * 0.2,
|
||||
infinite: true,
|
||||
@ -110,7 +110,7 @@ class MoveEffectExample extends FlameGame {
|
||||
..add(
|
||||
MoveEffect.along(
|
||||
path2,
|
||||
StandardEffectController(
|
||||
EffectController(
|
||||
duration: 6,
|
||||
startDelay: i * 0.3,
|
||||
infinite: true,
|
||||
|
||||
@ -2,7 +2,6 @@ import 'package:flame/components.dart';
|
||||
import 'package:flame/effects.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/input.dart';
|
||||
|
||||
import '../../commons/ember.dart';
|
||||
|
||||
class OpacityEffectExample extends FlameGame with TapDetector {
|
||||
@ -32,7 +31,7 @@ class OpacityEffectExample extends FlameGame with TapDetector {
|
||||
size: Vector2.all(100),
|
||||
)..add(
|
||||
OpacityEffect.fadeOut(
|
||||
StandardEffectController(
|
||||
EffectController(
|
||||
duration: 1.5,
|
||||
reverseDuration: 1.5,
|
||||
infinite: true,
|
||||
@ -46,9 +45,9 @@ class OpacityEffectExample extends FlameGame with TapDetector {
|
||||
void onTap() {
|
||||
final opacity = sprite.paint.color.opacity;
|
||||
if (opacity >= 0.5) {
|
||||
sprite.add(OpacityEffect.fadeOut(StandardEffectController(duration: 1)));
|
||||
sprite.add(OpacityEffect.fadeOut(EffectController(duration: 1)));
|
||||
} else {
|
||||
sprite.add(OpacityEffect.fadeIn(StandardEffectController(duration: 1)));
|
||||
sprite.add(OpacityEffect.fadeIn(EffectController(duration: 1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,7 +25,7 @@ class RotateEffectExample extends FlameGame {
|
||||
compass.rim.add(
|
||||
RotateEffect.by(
|
||||
1.0,
|
||||
StandardEffectController(
|
||||
EffectController(
|
||||
duration: 6,
|
||||
reverseDuration: 3,
|
||||
curve: Curves.ease,
|
||||
@ -37,7 +37,7 @@ class RotateEffectExample extends FlameGame {
|
||||
..add(
|
||||
RotateEffect.to(
|
||||
Transform2D.tau,
|
||||
StandardEffectController(
|
||||
EffectController(
|
||||
duration: 20,
|
||||
infinite: true,
|
||||
),
|
||||
@ -46,7 +46,7 @@ class RotateEffectExample extends FlameGame {
|
||||
..add(
|
||||
RotateEffect.by(
|
||||
Transform2D.tau * 0.015,
|
||||
StandardEffectController(
|
||||
EffectController(
|
||||
duration: 0.1,
|
||||
reverseDuration: 0.1,
|
||||
infinite: true,
|
||||
@ -56,7 +56,7 @@ class RotateEffectExample extends FlameGame {
|
||||
..add(
|
||||
RotateEffect.by(
|
||||
Transform2D.tau * 0.021,
|
||||
StandardEffectController(
|
||||
EffectController(
|
||||
duration: 0.13,
|
||||
reverseDuration: 0.13,
|
||||
infinite: true,
|
||||
|
||||
@ -42,7 +42,7 @@ class ScaleEffectExample extends FlameGame with TapDetector {
|
||||
square.add(
|
||||
ScaleEffect.to(
|
||||
Vector2.all(s),
|
||||
StandardEffectController(
|
||||
EffectController(
|
||||
duration: 1.5,
|
||||
curve: Curves.bounceInOut,
|
||||
),
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/effects.dart';
|
||||
import 'package:flame/extensions.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/input.dart';
|
||||
import 'package:flame/palette.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/animation.dart';
|
||||
|
||||
class SizeEffectExample extends FlameGame with TapDetector {
|
||||
static const String description = '''
|
||||
@ -42,10 +44,7 @@ class SizeEffectExample extends FlameGame with TapDetector {
|
||||
square.add(
|
||||
SizeEffect.to(
|
||||
Vector2.all(s),
|
||||
StandardEffectController(
|
||||
duration: 1.5,
|
||||
curve: Curves.bounceInOut,
|
||||
),
|
||||
EffectController(duration: 1.5, curve: Curves.bounceInOut),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -105,7 +105,7 @@ class JoystickAdvancedExample extends FlameGame
|
||||
onPressed: () => player.add(
|
||||
RotateEffect.by(
|
||||
8 * rng.nextDouble(),
|
||||
StandardEffectController(
|
||||
EffectController(
|
||||
duration: 1,
|
||||
reverseDuration: 1,
|
||||
curve: Curves.bounceOut,
|
||||
@ -131,7 +131,7 @@ class JoystickAdvancedExample extends FlameGame
|
||||
onPressed: () => player.add(
|
||||
ScaleEffect.by(
|
||||
Vector2.all(1.5),
|
||||
StandardEffectController(duration: 1.0, reverseDuration: 1.0),
|
||||
EffectController(duration: 1.0, reverseDuration: 1.0),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -152,7 +152,7 @@ class JoystickAdvancedExample extends FlameGame
|
||||
size: Vector2(185, 50),
|
||||
onPressed: () => player.add(
|
||||
OpacityEffect.fadeOut(
|
||||
StandardEffectController(duration: 0.5, reverseDuration: 0.5),
|
||||
EffectController(duration: 0.5, reverseDuration: 0.5),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -3,10 +3,12 @@
|
||||
## [Next]
|
||||
- Add `ButtonComponent` backed by two `PositionComponent`s
|
||||
- Add `SpriteButtonComponent` backed by two `Sprite`s
|
||||
- Allow more flexible construction of `EffectController`s and make them able to run back in time
|
||||
- Remove old effects system
|
||||
- Export new effects system
|
||||
- Introduce `updateTree` to follow the `renderTree` convention
|
||||
- Fix `Parallax.load` with different loading times
|
||||
- Fix render order of components and add tests
|
||||
- `isHud` renamed to `respectCamera`
|
||||
|
||||
## [1.0.0-releasecandidate.18]
|
||||
@ -15,7 +17,6 @@
|
||||
- Fixed position calculation in `HudMarginComponent` when using a viewport
|
||||
- Add noClip option to `FixedResolutionViewport`
|
||||
- Add a few missing helpers to SpriteAnimation
|
||||
- Fix render order of components and add tests
|
||||
|
||||
## [1.0.0-releasecandidate.17]
|
||||
- Added `StandardEffectController` class
|
||||
|
||||
@ -1,13 +1,21 @@
|
||||
export 'src/effects/component_effect.dart';
|
||||
export 'src/effects/controllers/curved_effect_controller.dart';
|
||||
export 'src/effects/controllers/delayed_effect_controller.dart';
|
||||
export 'src/effects/controllers/duration_effect_controller.dart';
|
||||
export 'src/effects/controllers/effect_controller.dart';
|
||||
export 'src/effects/controllers/infinite_effect_controller.dart';
|
||||
export 'src/effects/controllers/linear_effect_controller.dart';
|
||||
export 'src/effects/controllers/pause_effect_controller.dart';
|
||||
export 'src/effects/controllers/repeated_effect_controller.dart';
|
||||
export 'src/effects/controllers/reverse_curved_effect_controller.dart';
|
||||
export 'src/effects/controllers/reverse_linear_effect_controller.dart';
|
||||
export 'src/effects/controllers/sequence_effect_controller.dart';
|
||||
export 'src/effects/effect.dart';
|
||||
export 'src/effects/effect_controller.dart';
|
||||
export 'src/effects/move_effect.dart';
|
||||
export 'src/effects/opacity_effect.dart';
|
||||
export 'src/effects/remove_effect.dart';
|
||||
export 'src/effects/rotate_effect.dart';
|
||||
export 'src/effects/scale_effect.dart';
|
||||
export 'src/effects/simple_effect_controller.dart';
|
||||
export 'src/effects/size_effect.dart';
|
||||
export 'src/effects/standard_effect_controller.dart';
|
||||
export 'src/effects/transform2d_effect.dart';
|
||||
export 'src/extensions/vector2.dart';
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
import '../../components.dart';
|
||||
import 'controllers/effect_controller.dart';
|
||||
import 'effect.dart';
|
||||
import 'effect_controller.dart';
|
||||
|
||||
/// Base class for effects that target a [Component] of type [T].
|
||||
///
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
import 'package:flutter/animation.dart';
|
||||
|
||||
import 'duration_effect_controller.dart';
|
||||
|
||||
/// A controller that grows non-linearly from 0 to 1 following the provided
|
||||
/// [curve]. The [duration] cannot be 0.
|
||||
class CurvedEffectController extends DurationEffectController {
|
||||
CurvedEffectController(double duration, Curve curve)
|
||||
: assert(duration > 0, 'Duration must be positive: $duration'),
|
||||
_curve = curve,
|
||||
super(duration);
|
||||
|
||||
Curve get curve => _curve;
|
||||
final Curve _curve;
|
||||
|
||||
@override
|
||||
double get progress => _curve.transform(timer / duration);
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
import 'effect_controller.dart';
|
||||
|
||||
/// An effect controller that waits for [delay] seconds before running the
|
||||
/// child controller. While waiting, the progress will be reported at 0.
|
||||
class DelayedEffectController extends EffectController {
|
||||
DelayedEffectController(EffectController child, {required this.delay})
|
||||
: assert(delay >= 0, 'Delay must be non-negative: $delay'),
|
||||
_child = child,
|
||||
_timer = 0,
|
||||
super.empty();
|
||||
|
||||
final EffectController _child;
|
||||
final double delay;
|
||||
double _timer;
|
||||
|
||||
@override
|
||||
bool get isInfinite => _child.isInfinite;
|
||||
|
||||
@override
|
||||
bool get isRandom => _child.isRandom;
|
||||
|
||||
@override
|
||||
bool get started => _timer == delay;
|
||||
|
||||
@override
|
||||
bool get completed => started && _child.completed;
|
||||
|
||||
@override
|
||||
double get progress => started ? _child.progress : 0;
|
||||
|
||||
@override
|
||||
double? get duration {
|
||||
final d = _child.duration;
|
||||
return d == null ? null : d + delay;
|
||||
}
|
||||
|
||||
@override
|
||||
double advance(double dt) {
|
||||
if (_timer == delay) {
|
||||
return _child.advance(dt);
|
||||
}
|
||||
_timer += dt;
|
||||
if (_timer > delay) {
|
||||
final t = _child.advance(_timer - delay);
|
||||
_timer = delay;
|
||||
return t;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
double recede(double dt) {
|
||||
if (_timer == delay) {
|
||||
_timer -= _child.recede(dt);
|
||||
} else {
|
||||
_timer -= dt;
|
||||
}
|
||||
if (_timer < 0) {
|
||||
final leftoverTime = -_timer;
|
||||
_timer = 0;
|
||||
return leftoverTime;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@override
|
||||
void setToStart() {
|
||||
_timer = 0;
|
||||
}
|
||||
|
||||
@override
|
||||
void setToEnd() {
|
||||
_timer = delay;
|
||||
_child.setToEnd();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import 'effect_controller.dart';
|
||||
|
||||
/// Abstract class for an effect controller that has a predefined [duration].
|
||||
///
|
||||
/// This effect controller cannot be used directly, instead it serves as base
|
||||
/// for some other effect controller classes.
|
||||
///
|
||||
/// The primary functionality offered by this class is the [timer] property,
|
||||
/// which keeps track of how much time has passed within this controller. The
|
||||
/// effect controller will be considered [completed] when the timer reaches the
|
||||
/// [duration] value.
|
||||
abstract class DurationEffectController extends EffectController {
|
||||
DurationEffectController(double duration)
|
||||
: assert(duration >= 0, 'Duration cannot be negative: $duration'),
|
||||
_duration = duration,
|
||||
_timer = 0,
|
||||
super.empty();
|
||||
|
||||
double _duration;
|
||||
double _timer;
|
||||
|
||||
@override
|
||||
double get duration => _duration;
|
||||
|
||||
set duration(double value) => _duration = value;
|
||||
|
||||
@protected
|
||||
double get timer => _timer;
|
||||
|
||||
@override
|
||||
bool get completed => _timer == _duration;
|
||||
|
||||
@override
|
||||
double advance(double dt) {
|
||||
_timer += dt;
|
||||
if (_timer > _duration) {
|
||||
final leftoverTime = _timer - _duration;
|
||||
_timer = _duration;
|
||||
return leftoverTime;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@override
|
||||
double recede(double dt) {
|
||||
_timer -= dt;
|
||||
if (_timer < 0) {
|
||||
final leftoverTime = 0 - _timer;
|
||||
_timer = 0;
|
||||
return leftoverTime;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@override
|
||||
void setToStart() {
|
||||
_timer = 0;
|
||||
}
|
||||
|
||||
@override
|
||||
void setToEnd() {
|
||||
_timer = duration;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,171 @@
|
||||
import 'package:flutter/animation.dart';
|
||||
|
||||
import 'curved_effect_controller.dart';
|
||||
import 'delayed_effect_controller.dart';
|
||||
import 'infinite_effect_controller.dart';
|
||||
import 'linear_effect_controller.dart';
|
||||
import 'pause_effect_controller.dart';
|
||||
import 'repeated_effect_controller.dart';
|
||||
import 'reverse_curved_effect_controller.dart';
|
||||
import 'reverse_linear_effect_controller.dart';
|
||||
import 'sequence_effect_controller.dart';
|
||||
|
||||
/// Base "controller" class to facilitate animation of effects.
|
||||
///
|
||||
/// The purpose of an effect controller is to define how an effect or an
|
||||
/// animation should progress over time. To facilitate that, this class provides
|
||||
/// variable [progress], which will grow from 0.0 to 1.0. The value of 0
|
||||
/// corresponds to the beginning of an animation, and the value of 1.0 is
|
||||
/// the end of the animation.
|
||||
///
|
||||
/// The [progress] variable can best be thought of as a "logical time". For
|
||||
/// example, if you want to animate a certain property from value A to value B,
|
||||
/// then you can use [progress] to linearly interpolate between these two
|
||||
/// extremes and obtain variable `x = A*(1 - progress) + B*progress`.
|
||||
///
|
||||
/// The exact behavior of [progress] is determined by subclasses, but the
|
||||
/// following general considerations apply:
|
||||
/// - the progress can also go in negative direction (i.e. from 1 to 0);
|
||||
/// - the progress may oscillate, going from 0 to 1, then back to 0, etc;
|
||||
/// - the progress may change over a finite or infinite period of time;
|
||||
/// - the value of 0 corresponds to the logical start of an animation;
|
||||
/// - the value of 1 is either the end or the "peak" of an animation;
|
||||
/// - the progress may briefly attain values outside of [0; 1] range (for
|
||||
/// example if a "bouncy" easing curve is applied).
|
||||
///
|
||||
/// An [EffectController] can be made to run forward in time (`advance()`), or
|
||||
/// backward in time (`recede()`).
|
||||
///
|
||||
/// Unlike the `dart.ui.AnimationController`, this class does not use a `Ticker`
|
||||
/// to keep track of time. Instead, it must be pushed through time manually, by
|
||||
/// calling the `update()` method within the game loop.
|
||||
abstract class EffectController {
|
||||
/// Factory function for producing common [EffectController]s.
|
||||
///
|
||||
/// In the simplest case, when only `duration` is provided, this will return
|
||||
/// a [LinearEffectController] that grows linearly from 0 to 1 over the period
|
||||
/// of that duration.
|
||||
///
|
||||
/// More generally, the produced effect controller allows to add a delay
|
||||
/// before the beginning of the animation, to animate both forward and in
|
||||
/// reverse, to iterate several times (or infinitely), to apply an arbitrary
|
||||
/// [curve] making the effect progression non-linear, etc.
|
||||
///
|
||||
/// In the most general case, the animation proceeds through the following
|
||||
/// steps:
|
||||
/// 1. wait for [startDelay] seconds,
|
||||
/// 2. repeat the following steps [repeatCount] times (or [infinite]ly):
|
||||
/// a. progress from 0 to 1 over the [duration] seconds,
|
||||
/// b. wait for [atMaxDuration] seconds,
|
||||
/// c. progress from 1 to 0 over the [reverseDuration] seconds,
|
||||
/// d. wait for [atMinDuration] seconds.
|
||||
///
|
||||
/// Setting parameter [alternate] to true is another way to create a
|
||||
/// controller whose [reverseDuration] is the same as the forward [duration].
|
||||
///
|
||||
/// If the animation is finite and there are no "backward" or "atMin" stages
|
||||
/// then the animation will complete at `progress == 1`, otherwise it will
|
||||
/// complete at `progress == 0`.
|
||||
factory EffectController({
|
||||
required double duration,
|
||||
Curve curve = Curves.linear,
|
||||
double? reverseDuration,
|
||||
Curve? reverseCurve,
|
||||
bool infinite = false,
|
||||
bool alternate = false,
|
||||
int? repeatCount,
|
||||
double startDelay = 0.0,
|
||||
double atMaxDuration = 0.0,
|
||||
double atMinDuration = 0.0,
|
||||
}) {
|
||||
final isLinear = curve == Curves.linear;
|
||||
final hasReverse = alternate || (reverseDuration != null);
|
||||
final reverseIsLinear =
|
||||
reverseCurve == Curves.linear || ((reverseCurve == null) && isLinear);
|
||||
final items = [
|
||||
if (isLinear) LinearEffectController(duration),
|
||||
if (!isLinear) CurvedEffectController(duration, curve),
|
||||
if (atMaxDuration != 0) PauseEffectController(atMaxDuration, progress: 1),
|
||||
if (hasReverse && reverseIsLinear)
|
||||
ReverseLinearEffectController(reverseDuration ?? duration),
|
||||
if (hasReverse && !reverseIsLinear)
|
||||
ReverseCurvedEffectController(
|
||||
reverseDuration ?? duration,
|
||||
reverseCurve ?? curve.flipped,
|
||||
),
|
||||
if (atMinDuration != 0) PauseEffectController(atMinDuration, progress: 0),
|
||||
];
|
||||
assert(items.isNotEmpty);
|
||||
var controller =
|
||||
items.length == 1 ? items[0] : SequenceEffectController(items);
|
||||
if (infinite) {
|
||||
assert(
|
||||
repeatCount == null,
|
||||
'An infinite animation cannot have a repeat count',
|
||||
);
|
||||
controller = InfiniteEffectController(controller);
|
||||
}
|
||||
if (repeatCount != null && repeatCount != 1) {
|
||||
assert(repeatCount > 0, 'repeatCount must be positive');
|
||||
controller = RepeatedEffectController(controller, repeatCount);
|
||||
}
|
||||
if (startDelay != 0) {
|
||||
controller = DelayedEffectController(controller, delay: startDelay);
|
||||
}
|
||||
return controller;
|
||||
}
|
||||
|
||||
EffectController.empty();
|
||||
|
||||
/// Will the effect continue to run forever (never completes)?
|
||||
bool get isInfinite => false;
|
||||
|
||||
/// Is the effect's duration random or fixed?
|
||||
bool get isRandom => false;
|
||||
|
||||
/// Total duration of the effect. If the effect is either infinite or random,
|
||||
/// this will return `null`.
|
||||
double? get duration;
|
||||
|
||||
/// Has the effect started running? Some effects use a "delay" parameter to
|
||||
/// postpone the start of an animation. This property then tells you whether
|
||||
/// this delay period has already passed.
|
||||
bool get started => true;
|
||||
|
||||
/// Has the effect already finished?
|
||||
///
|
||||
/// For a finite animation, this property will turn `true` once the animation
|
||||
/// has finished running and the [progress] variable will no longer change
|
||||
/// in the future. For an infinite animation this should always return
|
||||
/// `false`.
|
||||
bool get completed;
|
||||
|
||||
/// The current progress of the effect, a value between 0 and 1.
|
||||
double get progress;
|
||||
|
||||
/// Advances this controller's internal clock by [dt] seconds.
|
||||
///
|
||||
/// If the controller is still running, the return value will be 0. If it
|
||||
/// already finished, then the return value will be the "leftover" part of
|
||||
/// the [dt]. That is, the amount of time [dt] that remains after the
|
||||
/// controller has finished.
|
||||
///
|
||||
/// Normally, this method will be called by the owner of the controller class.
|
||||
/// For example, if the controller is passed to an `Effect` class, then that
|
||||
/// class will take care of calling this method as necessary.
|
||||
double advance(double dt);
|
||||
|
||||
/// Similar to `advance()`, but makes the effect controller move back in time.
|
||||
///
|
||||
/// If the supplied amount of time [dt] would push the effect past its
|
||||
/// starting point, then the effect stops at the start and the "leftover"
|
||||
/// portion of [dt] is returned.
|
||||
double recede(double dt);
|
||||
|
||||
/// Reverts the controller to its initial state, as it was before the start
|
||||
/// of the animation.
|
||||
void setToStart();
|
||||
|
||||
/// Puts the controller into its final "completed" state.
|
||||
void setToEnd();
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
import 'effect_controller.dart';
|
||||
|
||||
/// Effect controller that wraps a [child] effect controller and repeats it
|
||||
/// infinitely.
|
||||
class InfiniteEffectController extends EffectController {
|
||||
InfiniteEffectController(this.child) : super.empty();
|
||||
|
||||
final EffectController child;
|
||||
|
||||
@override
|
||||
bool get isInfinite => true;
|
||||
|
||||
@override
|
||||
bool get completed => false;
|
||||
|
||||
@override
|
||||
double? get duration => null;
|
||||
|
||||
@override
|
||||
double get progress => child.progress;
|
||||
|
||||
@override
|
||||
double advance(double dt) {
|
||||
var t = dt;
|
||||
for (;;) {
|
||||
t = child.advance(t);
|
||||
if (t == 0) {
|
||||
break;
|
||||
}
|
||||
child.setToStart();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@override
|
||||
double recede(double dt) {
|
||||
var t = dt;
|
||||
for (;;) {
|
||||
t = child.recede(t);
|
||||
if (t == 0) {
|
||||
break;
|
||||
}
|
||||
child.setToEnd();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@override
|
||||
void setToStart() {
|
||||
child.setToStart();
|
||||
}
|
||||
|
||||
@override
|
||||
void setToEnd() {
|
||||
child.setToEnd();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
import 'duration_effect_controller.dart';
|
||||
|
||||
/// A controller that grows linearly from 0 to 1 over [duration] seconds.
|
||||
///
|
||||
/// The [duration] can also be 0, in which case the effect will jump from 0 to 1
|
||||
/// instantaneously.
|
||||
class LinearEffectController extends DurationEffectController {
|
||||
LinearEffectController(double duration) : super(duration);
|
||||
|
||||
// If duration is 0, `completed` will be true, and division by 0 avoided.
|
||||
@override
|
||||
double get progress => completed ? 1 : (timer / duration);
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
import 'duration_effect_controller.dart';
|
||||
|
||||
/// A controller that keeps constant [progress] over [duration] seconds.
|
||||
///
|
||||
/// Since "progress" represents the "logical time" of an Effect, keeping the
|
||||
/// progress constant over some time is equivalent to freezing in time or
|
||||
/// pausing the effect for the prescribed duration.
|
||||
///
|
||||
/// This controller is best used in combination with other controllers. For
|
||||
/// example, you can create a repeated controller where the progress changes
|
||||
/// 0->1->0 over a short period of time, then pauses, and this sequence repeats.
|
||||
class PauseEffectController extends DurationEffectController {
|
||||
PauseEffectController(double duration, {required double progress})
|
||||
: _progress = progress,
|
||||
super(duration);
|
||||
|
||||
final double _progress;
|
||||
|
||||
@override
|
||||
double get progress => _progress;
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
import 'effect_controller.dart';
|
||||
|
||||
/// Effect controller that repeats [child] controller a certain number of times.
|
||||
///
|
||||
/// The [repeatCount] must be positive, and [child] controller cannot be
|
||||
/// infinite. The child controller will be reset after each iteration (except
|
||||
/// the last).
|
||||
class RepeatedEffectController extends EffectController {
|
||||
RepeatedEffectController(this.child, this.repeatCount)
|
||||
: assert(repeatCount > 0, 'repeatCount must be positive'),
|
||||
assert(!child.isInfinite, 'child cannot be infinite'),
|
||||
_remainingCount = repeatCount,
|
||||
super.empty();
|
||||
|
||||
final EffectController child;
|
||||
final int repeatCount;
|
||||
|
||||
/// How many iterations this controller has remaining. When this reaches 0
|
||||
/// the controller is considered completed.
|
||||
int get remainingIterationsCount => _remainingCount;
|
||||
int _remainingCount;
|
||||
|
||||
@override
|
||||
double get progress => child.progress;
|
||||
|
||||
@override
|
||||
bool get completed => _remainingCount == 0;
|
||||
|
||||
@override
|
||||
double? get duration {
|
||||
final d = child.duration;
|
||||
return d == null ? null : d * repeatCount;
|
||||
}
|
||||
|
||||
@override
|
||||
double advance(double dt) {
|
||||
var t = child.advance(dt);
|
||||
while (t > 0 && _remainingCount > 0) {
|
||||
_remainingCount--;
|
||||
if (_remainingCount != 0) {
|
||||
child.setToStart();
|
||||
t = child.advance(t);
|
||||
}
|
||||
}
|
||||
if (_remainingCount == 1 && child.completed) {
|
||||
_remainingCount--;
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
@override
|
||||
double recede(double dt) {
|
||||
if (_remainingCount == 0 && dt > 0) {
|
||||
// When advancing, we do not reset the child on last iteration. Hence,
|
||||
// if we recede from the end position the remaining count must be
|
||||
// adjusted.
|
||||
_remainingCount = 1;
|
||||
}
|
||||
var t = child.recede(dt);
|
||||
while (t > 0 && _remainingCount < repeatCount) {
|
||||
_remainingCount++;
|
||||
child.setToEnd();
|
||||
t = child.recede(t);
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
@override
|
||||
void setToStart() {
|
||||
_remainingCount = repeatCount;
|
||||
child.setToStart();
|
||||
}
|
||||
|
||||
@override
|
||||
void setToEnd() {
|
||||
_remainingCount = 0;
|
||||
child.setToEnd();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
import 'package:flutter/animation.dart';
|
||||
|
||||
import 'duration_effect_controller.dart';
|
||||
|
||||
/// A controller that grows non-linearly from 1 to 0 following the provided
|
||||
/// [curve]. The [duration] cannot be 0.
|
||||
class ReverseCurvedEffectController extends DurationEffectController {
|
||||
ReverseCurvedEffectController(double duration, Curve curve)
|
||||
: assert(duration > 0, 'Duration must be positive: $duration'),
|
||||
_curve = curve,
|
||||
super(duration);
|
||||
|
||||
Curve get curve => _curve;
|
||||
final Curve _curve;
|
||||
|
||||
@override
|
||||
double get progress => _curve.transform(1 - timer / duration);
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
import 'duration_effect_controller.dart';
|
||||
|
||||
/// A controller that grows linearly from 1 to 0 over [duration] seconds.
|
||||
///
|
||||
/// The [duration] can also be 0, in which case the effect will jump from 1 to 0
|
||||
/// instantaneously.
|
||||
class ReverseLinearEffectController extends DurationEffectController {
|
||||
ReverseLinearEffectController(double duration) : super(duration);
|
||||
|
||||
// If duration is 0, `completed` will be true, and division by 0 avoided.
|
||||
@override
|
||||
double get progress => completed ? 0 : 1 - (timer / duration);
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
import 'effect_controller.dart';
|
||||
|
||||
/// An effect controller that executes a list of other controllers one after
|
||||
/// another.
|
||||
class SequenceEffectController extends EffectController {
|
||||
SequenceEffectController(List<EffectController> controllers)
|
||||
: assert(controllers.isNotEmpty, 'List of controllers cannot be empty'),
|
||||
assert(
|
||||
!controllers.any((c) => c.isInfinite),
|
||||
'Children controllers cannot be infinite',
|
||||
),
|
||||
children = controllers,
|
||||
_currentIndex = 0,
|
||||
super.empty();
|
||||
|
||||
/// Individual controllers in the sequence.
|
||||
final List<EffectController> children;
|
||||
|
||||
/// The index of the controller currently being executed. This starts with 0,
|
||||
/// and by the end it will be equal to `_children.length - 1`. This variable
|
||||
/// is always a valid index within the `_children` list.
|
||||
int _currentIndex;
|
||||
|
||||
@override
|
||||
bool get completed {
|
||||
return _currentIndex == children.length - 1 &&
|
||||
children[_currentIndex].completed;
|
||||
}
|
||||
|
||||
@override
|
||||
double? get duration {
|
||||
var totalDuration = 0.0;
|
||||
for (final controller in children) {
|
||||
final d = controller.duration;
|
||||
if (d == null) {
|
||||
return null;
|
||||
}
|
||||
totalDuration += d;
|
||||
}
|
||||
return totalDuration;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isRandom => children.any((c) => c.isRandom);
|
||||
|
||||
@override
|
||||
double get progress => children[_currentIndex].progress;
|
||||
|
||||
@override
|
||||
double advance(double dt) {
|
||||
var t = children[_currentIndex].advance(dt);
|
||||
while (t > 0 && _currentIndex < children.length - 1) {
|
||||
_currentIndex++;
|
||||
t = children[_currentIndex].advance(t);
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
@override
|
||||
double recede(double dt) {
|
||||
var t = children[_currentIndex].recede(dt);
|
||||
while (t > 0 && _currentIndex > 0) {
|
||||
_currentIndex--;
|
||||
t = children[_currentIndex].recede(t);
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
@override
|
||||
void setToStart() {
|
||||
_currentIndex = 0;
|
||||
children.forEach((c) => c.setToStart());
|
||||
}
|
||||
|
||||
@override
|
||||
void setToEnd() {
|
||||
_currentIndex = children.length - 1;
|
||||
children.forEach((c) => c.setToEnd());
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import '../../components.dart';
|
||||
import 'effect_controller.dart';
|
||||
import 'controllers/effect_controller.dart';
|
||||
|
||||
/// An [Effect] is a component that changes properties or appearance of another
|
||||
/// component over time.
|
||||
@ -30,7 +30,8 @@ abstract class Effect extends Component {
|
||||
: removeOnFinish = true,
|
||||
_paused = false,
|
||||
_started = false,
|
||||
_finished = false;
|
||||
_finished = false,
|
||||
_reversed = false;
|
||||
|
||||
/// An object that describes how the effect should evolve over time.
|
||||
final EffectController controller;
|
||||
@ -57,6 +58,13 @@ abstract class Effect extends Component {
|
||||
bool get isPaused => _paused;
|
||||
bool _paused;
|
||||
|
||||
/// Whether the effect is currently running back in time.
|
||||
///
|
||||
/// Call `reverse()` in order to change this. When the effect is reset, this
|
||||
/// is set to false.
|
||||
bool get isReversed => _reversed;
|
||||
bool _reversed;
|
||||
|
||||
/// Pause the effect. The effect will not respond to updates while it is
|
||||
/// paused. Calling `resume()` or `reset()` will un-pause it. Pausing an
|
||||
/// already paused effect is a no-op.
|
||||
@ -66,6 +74,9 @@ abstract class Effect extends Component {
|
||||
/// currently paused, this call is a no-op.
|
||||
void resume() => _paused = false;
|
||||
|
||||
/// Cause the effect to run back in time.
|
||||
void reverse() => _reversed = !_reversed;
|
||||
|
||||
/// Restore the effect to its original state as it was when the effect was
|
||||
/// just created.
|
||||
///
|
||||
@ -75,10 +86,11 @@ abstract class Effect extends Component {
|
||||
/// it to the target.
|
||||
@mustCallSuper
|
||||
void reset() {
|
||||
controller.reset();
|
||||
controller.setToStart();
|
||||
_paused = false;
|
||||
_started = false;
|
||||
_finished = false;
|
||||
_reversed = false;
|
||||
}
|
||||
|
||||
/// Implementation of [Component]'s `update()` method. Derived classes are
|
||||
@ -89,7 +101,11 @@ abstract class Effect extends Component {
|
||||
return;
|
||||
}
|
||||
super.update(dt);
|
||||
controller.update(dt);
|
||||
if (_reversed) {
|
||||
controller.recede(dt);
|
||||
} else {
|
||||
controller.advance(dt);
|
||||
}
|
||||
if (!_started && controller.started) {
|
||||
_started = true;
|
||||
onStart();
|
||||
|
||||
@ -1,57 +0,0 @@
|
||||
/// Base "controller" class to facilitate animation of effects.
|
||||
///
|
||||
/// The purpose of an effect controller is to define how an effect or an
|
||||
/// animation should progress over time. To facilitate that, this class provides
|
||||
/// variable [progress], which will grow from 0.0 to 1.0 over time. The value
|
||||
/// of 0 corresponds to the beginning of an animation, and the value of 1.0 is
|
||||
/// the end of the animation.
|
||||
///
|
||||
/// The [progress] variable can be best thought of as a "logical time". For
|
||||
/// example, if you want to animate a certain property from value A to value B,
|
||||
/// then you can use [progress] to linearly interpolate between these two
|
||||
/// extremes and obtain variable `x = A*(1 - progress) + B*progress`.
|
||||
///
|
||||
/// The exact behavior of [progress] is determined by subclasses, but the
|
||||
/// following general considerations apply:
|
||||
/// - the progress may go in negative direction (i.e. from 1 to 0);
|
||||
/// - the progress may oscillate, going from 0 to 1, then back to 0, etc;
|
||||
/// - the progress may change over a finite or infinite period of time;
|
||||
/// - the value of 0 corresponds to logical start of an animation;
|
||||
/// - the value of 1 is either a start or "peak" of an animation;
|
||||
/// - the progress may briefly attain values outside of [0; 1] range (for
|
||||
/// example if a "bouncy" easing curve is applied).
|
||||
///
|
||||
/// Unlike the `dart.ui.AnimationController`, this class does not use a `Ticker`
|
||||
/// to keep track of time. Instead, it must be pushed through time manually, by
|
||||
/// calling the `update()` method within the game loop.
|
||||
abstract class EffectController {
|
||||
/// Will the effect continue to run forever (i.e. has no logical end)?
|
||||
bool get isInfinite;
|
||||
|
||||
/// Has the effect started running? Some effects use a "delay" parameter to
|
||||
/// postpone the start of an animation. This property then tells you whether
|
||||
/// this delay period has already passed.
|
||||
bool get started;
|
||||
|
||||
/// Has the effect already completed running?
|
||||
///
|
||||
/// For a finite animation, this property will turn `true` once the animation
|
||||
/// has finished running and the [progress] variable will no longer change
|
||||
/// in the future. For an infinite animation this should always return
|
||||
/// `false`.
|
||||
bool get completed;
|
||||
|
||||
/// The current value of the effect/animation, a value between 0 and 1.
|
||||
double get progress;
|
||||
|
||||
/// Reverts the controller to its initial state, as it was before the start
|
||||
/// of the animation.
|
||||
void reset();
|
||||
|
||||
/// Advances this controller's internal clock by [dt] seconds.
|
||||
///
|
||||
/// Normally, this method will be called by the owner of the controller class.
|
||||
/// For example, if the controller is passed to an `Effect` class, then that
|
||||
/// class will take care of calling the `update()` method as necessary.
|
||||
void update(double dt);
|
||||
}
|
||||
@ -2,7 +2,7 @@ import 'dart:ui';
|
||||
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
|
||||
import 'effect_controller.dart';
|
||||
import 'controllers/effect_controller.dart';
|
||||
import 'transform2d_effect.dart';
|
||||
|
||||
/// Move a component to a new position.
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import '../../components.dart';
|
||||
import 'component_effect.dart';
|
||||
import 'effect_controller.dart';
|
||||
import 'controllers/effect_controller.dart';
|
||||
|
||||
/// Change the opacity of a component over time.
|
||||
///
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
import 'controllers/linear_effect_controller.dart';
|
||||
import 'effect.dart';
|
||||
import 'simple_effect_controller.dart';
|
||||
|
||||
/// This simple effect, when attached to a component, will cause that component
|
||||
/// to be removed from the game tree after `delay` seconds.
|
||||
class RemoveEffect extends Effect {
|
||||
RemoveEffect({double delay = 0.0})
|
||||
: super(SimpleEffectController(delay: delay));
|
||||
RemoveEffect({double delay = 0.0}) : super(LinearEffectController(delay));
|
||||
|
||||
@override
|
||||
void apply(double progress) {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import 'effect_controller.dart';
|
||||
import 'controllers/effect_controller.dart';
|
||||
import 'transform2d_effect.dart';
|
||||
|
||||
/// Rotate a component around its anchor.
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
|
||||
import 'effect_controller.dart';
|
||||
import 'controllers/effect_controller.dart';
|
||||
import 'transform2d_effect.dart';
|
||||
|
||||
/// Scale a component.
|
||||
|
||||
@ -1,54 +0,0 @@
|
||||
import 'effect_controller.dart';
|
||||
import 'standard_effect_controller.dart';
|
||||
|
||||
/// Simplest possible [EffectController], which supports an effect progressing
|
||||
/// linearly over [duration] seconds.
|
||||
///
|
||||
/// The [duration] can be 0, in which case the effect will jump from 0 to 1
|
||||
/// instantaneously.
|
||||
///
|
||||
/// The [delay] parameter allows to delay the start of the effect by the
|
||||
/// specified number of seconds.
|
||||
///
|
||||
/// See also: [StandardEffectController]
|
||||
class SimpleEffectController extends EffectController {
|
||||
SimpleEffectController({
|
||||
this.duration = 0.0,
|
||||
this.delay = 0.0,
|
||||
}) : assert(duration >= 0, 'duration cannot be negative: $duration'),
|
||||
assert(delay >= 0, 'delay cannot be negative: $delay');
|
||||
|
||||
final double duration;
|
||||
final double delay;
|
||||
double _timer = 0.0;
|
||||
|
||||
@override
|
||||
bool get started => _timer >= delay;
|
||||
|
||||
@override
|
||||
bool get completed => _timer >= delay + duration;
|
||||
|
||||
@override
|
||||
bool get isInfinite => false;
|
||||
|
||||
@override
|
||||
double get progress {
|
||||
// If duration == 0, then `completed == started`, and the middle case
|
||||
// (which divides by duration) cannot occur.
|
||||
return completed
|
||||
? 1
|
||||
: started
|
||||
? (_timer - delay) / duration
|
||||
: 0;
|
||||
}
|
||||
|
||||
@override
|
||||
void update(double dt) {
|
||||
_timer += dt;
|
||||
}
|
||||
|
||||
@override
|
||||
void reset() {
|
||||
_timer = 0;
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import '../../components.dart';
|
||||
import '../../extensions.dart';
|
||||
import 'component_effect.dart';
|
||||
import 'effect_controller.dart';
|
||||
import 'controllers/effect_controller.dart';
|
||||
|
||||
/// Change the size of a component over time.
|
||||
///
|
||||
|
||||
@ -1,234 +0,0 @@
|
||||
import 'package:flutter/animation.dart';
|
||||
|
||||
import 'effect_controller.dart';
|
||||
|
||||
/// A commonly used implementation of a [EffectController].
|
||||
///
|
||||
/// In the simplest case, [StandardEffectController] will have a positive
|
||||
/// `duration` and will change its [progress] linearly from 0 to 1 over the
|
||||
/// period of that duration.
|
||||
///
|
||||
/// More generally, a [StandardEffectController] allows to add a delay before
|
||||
/// the beginning of the animation, to animate both forward and in reverse,
|
||||
/// to iterate several times (or infinitely), to apply an arbitrary [Curve]
|
||||
/// making the effect progression non-linear, etc.
|
||||
///
|
||||
/// In the most general case, the animation proceeds through the following
|
||||
/// steps:
|
||||
/// 1. wait for [startDelay] seconds,
|
||||
/// 2. repeat the following steps [repeatCount] times (or infinitely):
|
||||
/// a. progress from 0 to 1 over the [forwardDuration] seconds,
|
||||
/// b. wait for [atMaxDuration] seconds,
|
||||
/// c. progress from 1 to 0 over the [backwardDuration] seconds,
|
||||
/// d. wait for [atMinDuration] seconds,
|
||||
/// 3. mark the animation as [completed].
|
||||
///
|
||||
/// If the animation is finite and there are no `backward` or `atMin` stages
|
||||
/// then the animation will complete at `progress == 1`, otherwise it will
|
||||
/// complete at `progress == 0`.
|
||||
///
|
||||
/// The animation is "sticky" at the end of the `forward` and `backward` stages.
|
||||
/// This means that within a single [update()] call the animation may complete
|
||||
/// these stages but will not move on to the next ones. Thus, you're guaranteed
|
||||
/// to be able to observe `progress == 1` and `progress == 0` at least once
|
||||
/// within each iteration cycle.
|
||||
class StandardEffectController extends EffectController {
|
||||
StandardEffectController({
|
||||
required double duration,
|
||||
Curve curve = Curves.linear,
|
||||
double reverseDuration = 0.0,
|
||||
Curve? reverseCurve,
|
||||
bool infinite = false,
|
||||
int? repeatCount,
|
||||
this.startDelay = 0.0,
|
||||
this.atMaxDuration = 0.0,
|
||||
this.atMinDuration = 0.0,
|
||||
}) : assert(
|
||||
!infinite || repeatCount == null,
|
||||
'An infinite animation cannot have a repeat count',
|
||||
),
|
||||
assert(
|
||||
infinite || (repeatCount ?? 1) > 0,
|
||||
'repeatCount must be positive',
|
||||
),
|
||||
assert(duration > 0, 'duration must be positive'),
|
||||
assert(reverseDuration >= 0, 'reverseDuration cannot be negative'),
|
||||
assert(startDelay >= 0, 'startDelay cannot be negative'),
|
||||
assert(atMaxDuration >= 0, 'atMaxDuration cannot be negative'),
|
||||
assert(atMinDuration >= 0, 'atMinDuration cannot be negative'),
|
||||
repeatCount = infinite ? -1 : (repeatCount ?? 1),
|
||||
forwardDuration = duration,
|
||||
backwardDuration = reverseDuration,
|
||||
forwardCurve = curve,
|
||||
backwardCurve =
|
||||
reverseCurve ?? (curve == Curves.linear ? curve : curve.flipped),
|
||||
_progress = 0,
|
||||
_remainingIterationsCount = repeatCount ?? (infinite ? -1 : 1),
|
||||
_remainingTimeAtCurrentStage = startDelay,
|
||||
_stage = _AnimationStage.beforeStart;
|
||||
|
||||
/// The current value of the animation.
|
||||
///
|
||||
/// This variable changes from 0 to 1 over time, which can be used by an
|
||||
/// animation to produce the desired transition effect. In essence, you can
|
||||
/// think of this variable as a "logical time".
|
||||
///
|
||||
/// This variable is guaranteed to be 0 during the `beforeStart` and `atMin`
|
||||
/// periods, and to be 1 during the `atMax` period. During the `forward`
|
||||
/// period this variable changes from 0 to 1, and during the `backward` period
|
||||
/// it goes back from 1 to 0. However, during the latter two periods it is
|
||||
/// possible for `progress` to become less than 0 or greater than 1 if either
|
||||
/// the [forwardCurve] or the [backwardCurve] produce values outside of [0; 1]
|
||||
/// range.
|
||||
@override
|
||||
double get progress => _progress;
|
||||
double _progress;
|
||||
|
||||
/// The transformation curve that applies during the `forward` stage. By
|
||||
/// default, the curve is linear.
|
||||
///
|
||||
/// The effect of the curve is that if the animation is normally at x% of its
|
||||
/// progression during the `forward` stage, then the reported [progress] will
|
||||
/// be equal to `forwardCurve.progress(x)`.
|
||||
final Curve forwardCurve;
|
||||
|
||||
/// The transformation curve that applies during the `backward` stage. By
|
||||
/// default, the curve is the flipped to [forwardCurve].
|
||||
///
|
||||
/// The effect of the curve is that if the animation is at normally x% of its
|
||||
/// progression during the `backward` stage, then the reported [progress] will
|
||||
/// be equal to `backwardCurve.progress(x)`.
|
||||
final Curve backwardCurve;
|
||||
|
||||
/// The number of times to play the animation, defaults to 1.
|
||||
///
|
||||
/// If this value is negative, it indicates an infinitely repeating animation.
|
||||
/// This value cannot be zero.
|
||||
final int repeatCount;
|
||||
|
||||
/// Will the effect continue to loop forever?
|
||||
@override
|
||||
bool get isInfinite => repeatCount < 0;
|
||||
|
||||
int _remainingIterationsCount;
|
||||
|
||||
@override
|
||||
bool get started => _stage != _AnimationStage.beforeStart;
|
||||
|
||||
@override
|
||||
bool get completed => _remainingIterationsCount == 0;
|
||||
|
||||
/// The time (in seconds) before the animation begins. During this time
|
||||
/// the property [started] will be returning false, and [progress] will be 0.
|
||||
final double startDelay;
|
||||
|
||||
final double forwardDuration;
|
||||
final double backwardDuration;
|
||||
final double atMaxDuration;
|
||||
final double atMinDuration;
|
||||
|
||||
double get cycleDuration {
|
||||
return forwardDuration + atMaxDuration + backwardDuration + atMinDuration;
|
||||
}
|
||||
|
||||
bool get isSimpleAnimation =>
|
||||
atMaxDuration + backwardDuration + atMinDuration == 0;
|
||||
|
||||
_AnimationStage _stage;
|
||||
double _remainingTimeAtCurrentStage;
|
||||
|
||||
@override
|
||||
void update(double dt) {
|
||||
if (completed) {
|
||||
return;
|
||||
}
|
||||
_remainingTimeAtCurrentStage -= dt;
|
||||
// When remaining time becomes zero or negative, it means we're
|
||||
// transitioning into the next stage.
|
||||
//
|
||||
// In each iteration of the while loop below we transition to the next
|
||||
// stage and add the next stage's duration to the remaining timer. This may
|
||||
// not be enough to make the timer positive (particularly if some stage has
|
||||
// duration 0), which means we need to keep progressing onto the next stage.
|
||||
//
|
||||
// The exceptions to this rule are the "forward" and "backward" stages which
|
||||
// always exit at the end, even if the timer is negative. This allows us to
|
||||
// have "sticky" start and end of an animation, i.e. the controller will
|
||||
// never jump over points with progress=0 or progress=1.
|
||||
while (_remainingTimeAtCurrentStage <= 0) {
|
||||
// In the switch below we are *finishing* each of the indicated stages.
|
||||
switch (_stage) {
|
||||
case _AnimationStage.beforeStart:
|
||||
_remainingTimeAtCurrentStage += forwardDuration;
|
||||
_stage = _AnimationStage.forward;
|
||||
break;
|
||||
case _AnimationStage.forward:
|
||||
_progress = 1;
|
||||
_remainingTimeAtCurrentStage += atMaxDuration;
|
||||
_stage = _AnimationStage.atMax;
|
||||
if (_remainingIterationsCount == 1 && isSimpleAnimation) {
|
||||
_markCompleted();
|
||||
}
|
||||
return;
|
||||
case _AnimationStage.atMax:
|
||||
_remainingTimeAtCurrentStage += backwardDuration;
|
||||
_stage = _AnimationStage.backward;
|
||||
if (_remainingIterationsCount == 1 &&
|
||||
backwardDuration == 0 &&
|
||||
atMinDuration == 0) {
|
||||
_markCompleted();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case _AnimationStage.backward:
|
||||
_progress = 0;
|
||||
_remainingTimeAtCurrentStage += atMinDuration;
|
||||
_stage = _AnimationStage.atMin;
|
||||
return;
|
||||
case _AnimationStage.atMin:
|
||||
_remainingTimeAtCurrentStage += forwardDuration;
|
||||
_stage = _AnimationStage.forward;
|
||||
if (!isInfinite) {
|
||||
_remainingIterationsCount -= 1;
|
||||
if (_remainingIterationsCount == 0) {
|
||||
_markCompleted();
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert(_remainingTimeAtCurrentStage > 0);
|
||||
if (_stage == _AnimationStage.forward) {
|
||||
_progress = forwardCurve.transform(
|
||||
1 - _remainingTimeAtCurrentStage / forwardDuration,
|
||||
);
|
||||
}
|
||||
if (_stage == _AnimationStage.backward) {
|
||||
_progress = backwardCurve.transform(
|
||||
_remainingTimeAtCurrentStage / backwardDuration,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void reset() {
|
||||
_progress = 0;
|
||||
_stage = _AnimationStage.beforeStart;
|
||||
_remainingTimeAtCurrentStage = startDelay;
|
||||
_remainingIterationsCount = repeatCount;
|
||||
}
|
||||
|
||||
void _markCompleted() {
|
||||
_remainingTimeAtCurrentStage = double.infinity;
|
||||
_remainingIterationsCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
enum _AnimationStage {
|
||||
beforeStart,
|
||||
forward,
|
||||
atMax,
|
||||
backward,
|
||||
atMin,
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import '../components/position_component.dart';
|
||||
import '../game/transform2d.dart';
|
||||
import 'component_effect.dart';
|
||||
import 'effect_controller.dart';
|
||||
import 'controllers/effect_controller.dart';
|
||||
|
||||
/// Base class for effects that target a [Transform2D] property.
|
||||
///
|
||||
|
||||
@ -0,0 +1,118 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flame/src/effects/controllers/curved_effect_controller.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter/animation.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('CurvedEffectController', () {
|
||||
const curves = <Curve>[
|
||||
Curves.bounceIn,
|
||||
Curves.bounceInOut,
|
||||
Curves.bounceOut,
|
||||
Curves.decelerate,
|
||||
Curves.ease,
|
||||
Curves.easeIn,
|
||||
Curves.easeInBack,
|
||||
Curves.easeInCirc,
|
||||
Curves.easeInCubic,
|
||||
Curves.easeInExpo,
|
||||
Curves.easeInOut,
|
||||
Curves.easeInOutBack,
|
||||
Curves.easeInOutCirc,
|
||||
Curves.easeInOutCubic,
|
||||
Curves.easeInOutCubicEmphasized,
|
||||
Curves.easeInOutExpo,
|
||||
Curves.easeInOutQuad,
|
||||
Curves.easeInOutQuart,
|
||||
Curves.easeInOutQuint,
|
||||
Curves.easeInOutSine,
|
||||
Curves.easeInQuad,
|
||||
Curves.easeInQuart,
|
||||
Curves.easeInQuint,
|
||||
Curves.easeInSine,
|
||||
Curves.easeInToLinear,
|
||||
Curves.easeOut,
|
||||
Curves.easeOutBack,
|
||||
Curves.easeOutCirc,
|
||||
Curves.easeOutCubic,
|
||||
Curves.easeOutExpo,
|
||||
Curves.easeOutQuad,
|
||||
Curves.easeOutQuart,
|
||||
Curves.easeOutQuint,
|
||||
Curves.easeOutSine,
|
||||
Curves.elasticIn,
|
||||
Curves.elasticInOut,
|
||||
Curves.elasticOut,
|
||||
Curves.fastLinearToSlowEaseIn,
|
||||
Curves.fastOutSlowIn,
|
||||
Curves.linear,
|
||||
Curves.linearToEaseOut,
|
||||
Curves.slowMiddle,
|
||||
];
|
||||
|
||||
testRandom('normal', (Random random) {
|
||||
final duration = random.nextDouble();
|
||||
final curve = curves[random.nextInt(curves.length)];
|
||||
final ec = CurvedEffectController(duration, curve);
|
||||
|
||||
expect(ec.progress, 0);
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, false);
|
||||
expect(ec.curve, curve);
|
||||
expect(ec.isInfinite, false);
|
||||
expect(ec.isRandom, false);
|
||||
expect(ec.duration, duration);
|
||||
|
||||
var totalTime = 0.0;
|
||||
while (totalTime < duration) {
|
||||
final dt = random.nextDouble() * 0.01;
|
||||
totalTime += dt;
|
||||
final leftoverTime = ec.advance(dt);
|
||||
if (totalTime > duration) {
|
||||
expect(leftoverTime, closeTo(totalTime - duration, 1e-15));
|
||||
expect(ec.progress, 1);
|
||||
} else {
|
||||
expect(leftoverTime, 0);
|
||||
expect(
|
||||
ec.progress,
|
||||
closeTo(curve.transform(totalTime / duration), 1e-15),
|
||||
);
|
||||
}
|
||||
}
|
||||
expect(ec.progress, 1);
|
||||
expect(ec.completed, true);
|
||||
|
||||
totalTime = duration;
|
||||
while (totalTime > 0) {
|
||||
final dt = random.nextDouble() * 0.01;
|
||||
totalTime -= dt;
|
||||
final leftoverTime = ec.recede(dt);
|
||||
if (totalTime > 0) {
|
||||
expect(leftoverTime, 0);
|
||||
expect(
|
||||
ec.progress,
|
||||
closeTo(curve.transform(totalTime / duration), 1e-15),
|
||||
);
|
||||
} else {
|
||||
expect(leftoverTime, closeTo(-totalTime, 1e-15));
|
||||
expect(ec.progress, 0);
|
||||
}
|
||||
}
|
||||
expect(ec.progress, 0);
|
||||
expect(ec.completed, false);
|
||||
});
|
||||
|
||||
test('errors', () {
|
||||
expect(
|
||||
() => CurvedEffectController(0, Curves.linear),
|
||||
throwsA(isA<AssertionError>()),
|
||||
);
|
||||
expect(
|
||||
() => CurvedEffectController(-1, Curves.linear),
|
||||
throwsA(isA<AssertionError>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
import 'package:flame/src/effects/controllers/delayed_effect_controller.dart';
|
||||
import 'package:flame/src/effects/controllers/linear_effect_controller.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('DelayedEffectController', () {
|
||||
test('normal', () {
|
||||
final ec = DelayedEffectController(LinearEffectController(1), delay: 3);
|
||||
expect(ec.isInfinite, false);
|
||||
expect(ec.isRandom, false);
|
||||
expect(ec.started, false);
|
||||
expect(ec.completed, false);
|
||||
expect(ec.progress, 0);
|
||||
expect(ec.duration, 4);
|
||||
|
||||
for (var i = 0; i < 6; i++) {
|
||||
expect(ec.advance(0.5), 0);
|
||||
expect(ec.started, i == 5);
|
||||
expect(ec.progress, 0);
|
||||
}
|
||||
expect(ec.advance(1), 0);
|
||||
expect(ec.progress, 1);
|
||||
expect(ec.completed, true);
|
||||
expect(ec.advance(1), 1);
|
||||
});
|
||||
|
||||
test('reset', () {
|
||||
final ec = DelayedEffectController(LinearEffectController(1), delay: 3);
|
||||
ec.setToEnd();
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, true);
|
||||
expect(ec.progress, 1);
|
||||
ec.setToStart();
|
||||
expect(ec.started, false);
|
||||
expect(ec.completed, false);
|
||||
expect(ec.progress, 0);
|
||||
});
|
||||
|
||||
test('advance/recede', () {
|
||||
final ec = DelayedEffectController(LinearEffectController(1), delay: 3);
|
||||
|
||||
expect(ec.advance(1), 0);
|
||||
expect(ec.recede(0.5), 0);
|
||||
expect(ec.advance(0.5), 0);
|
||||
expect(ec.advance(2), 0); // 3/3 + 0/1
|
||||
expect(ec.progress, 0);
|
||||
expect(ec.started, true);
|
||||
expect(ec.recede(0.5), 0); // 2.5/3 + 0/1
|
||||
expect(ec.started, false);
|
||||
expect(ec.advance(1), 0); // 3/3 + 0.5/1
|
||||
expect(ec.started, true);
|
||||
expect(ec.progress, closeTo(0.5, 1e-15));
|
||||
expect(ec.advance(1), closeTo(0.5, 1e-15)); // 3/3 + 1/1
|
||||
expect(ec.completed, true);
|
||||
expect(ec.recede(0.5), 0); // 3/3 + 0.5/1
|
||||
expect(ec.completed, false);
|
||||
expect(ec.started, true);
|
||||
expect(ec.progress, closeTo(0.5, 1e-15));
|
||||
expect(ec.recede(1), 0); // 2.5/3 + 0/1
|
||||
expect(ec.progress, 0);
|
||||
expect(ec.started, false);
|
||||
expect(ec.recede(3), closeTo(0.5, 1e-15));
|
||||
});
|
||||
|
||||
test('errors', () {
|
||||
expect(
|
||||
() => DelayedEffectController(LinearEffectController(1), delay: -1),
|
||||
failsAssert('Delay must be non-negative: -1.0'),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -1,97 +1,90 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flame/src/effects/standard_effect_controller.dart';
|
||||
import 'package:flame/src/effects/controllers/effect_controller.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter/animation.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('StandardEffectController', () {
|
||||
group('EffectController', () {
|
||||
test('forward', () {
|
||||
final ec = StandardEffectController(duration: 1.0);
|
||||
final ec = EffectController(duration: 1.0);
|
||||
expect(ec.isInfinite, false);
|
||||
expect(ec.isSimpleAnimation, true);
|
||||
expect(ec.started, false);
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, false);
|
||||
expect(ec.progress, 0.0);
|
||||
|
||||
ec.update(0.5);
|
||||
ec.advance(0.5);
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, false);
|
||||
expect(ec.progress, 0.5);
|
||||
|
||||
ec.update(0.5);
|
||||
ec.advance(0.5);
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, true);
|
||||
expect(ec.progress, 1.0);
|
||||
|
||||
// Updating controller after it already finished is a no-op
|
||||
ec.update(0.5);
|
||||
ec.advance(0.5);
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, true);
|
||||
expect(ec.progress, 1.0);
|
||||
});
|
||||
|
||||
test('forward x 2', () {
|
||||
final ec = StandardEffectController(duration: 1, repeatCount: 2);
|
||||
expect(ec.isSimpleAnimation, true);
|
||||
expect(ec.started, false);
|
||||
final ec = EffectController(duration: 1, repeatCount: 2);
|
||||
expect(ec.started, true);
|
||||
expect(ec.progress, 0);
|
||||
|
||||
ec.update(1);
|
||||
ec.advance(1);
|
||||
expect(ec.progress, 1);
|
||||
ec.update(0);
|
||||
expect(ec.progress, 0);
|
||||
ec.update(1);
|
||||
ec.advance(1e-10);
|
||||
expect(ec.progress, closeTo(1e-10, 1e-15));
|
||||
ec.advance(1);
|
||||
expect(ec.progress, 1);
|
||||
expect(ec.completed, true);
|
||||
});
|
||||
|
||||
test('forward + delay', () {
|
||||
final ec = StandardEffectController(duration: 1.0, startDelay: 0.2);
|
||||
final ec = EffectController(duration: 1.0, startDelay: 0.2);
|
||||
expect(ec.isInfinite, false);
|
||||
expect(ec.isSimpleAnimation, true);
|
||||
|
||||
// initial delay
|
||||
for (var i = 0; i < 20; i++) {
|
||||
expect(ec.started, false);
|
||||
expect(ec.completed, false);
|
||||
expect(ec.progress, 0);
|
||||
ec.update(0.01);
|
||||
ec.advance(0.01);
|
||||
}
|
||||
expect(ec.progress, closeTo(0, 1e-10));
|
||||
|
||||
// progress from 0 to 1 over 1s
|
||||
for (var i = 0; i < 100; i++) {
|
||||
ec.update(0.01);
|
||||
ec.advance(0.01);
|
||||
expect(ec.started, true);
|
||||
expect(ec.progress, closeTo((i + 1) / 100, 1e-10));
|
||||
}
|
||||
|
||||
// final update, to account for any rounding errors
|
||||
ec.update(1e-10);
|
||||
ec.advance(1e-10);
|
||||
expect(ec.completed, true);
|
||||
expect(ec.progress, 1);
|
||||
});
|
||||
|
||||
test('forward + atMax', () {
|
||||
final ec = StandardEffectController(duration: 1, atMaxDuration: 0.5);
|
||||
expect(ec.cycleDuration, 1.5);
|
||||
expect(ec.isSimpleAnimation, false);
|
||||
final ec = EffectController(duration: 1, atMaxDuration: 0.5);
|
||||
expect(ec.duration, 1.5);
|
||||
expect(ec.isInfinite, false);
|
||||
expect(ec.progress, 0);
|
||||
|
||||
ec.update(1.5);
|
||||
ec.advance(1.5);
|
||||
expect(ec.progress, 1);
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, false);
|
||||
ec.update(0);
|
||||
expect(ec.progress, 1);
|
||||
expect(ec.completed, true);
|
||||
});
|
||||
|
||||
test('(forward + reverse) x 5', () {
|
||||
final ec = StandardEffectController(
|
||||
final ec = EffectController(
|
||||
startDelay: 1.0,
|
||||
duration: 2.0,
|
||||
reverseDuration: 1.0,
|
||||
@ -101,18 +94,11 @@ void main() {
|
||||
);
|
||||
expect(ec.isInfinite, false);
|
||||
expect(ec.progress, 0);
|
||||
expect(ec.repeatCount, 5);
|
||||
expect(ec.started, false);
|
||||
expect(ec.completed, false);
|
||||
expect(ec.forwardDuration, 2.0);
|
||||
expect(ec.backwardDuration, 1.0);
|
||||
expect(ec.atMaxDuration, 0.2);
|
||||
expect(ec.atMinDuration, 0.5);
|
||||
expect(ec.cycleDuration, 3.7);
|
||||
expect(ec.isSimpleAnimation, false);
|
||||
|
||||
// Initial delay
|
||||
ec.update(1.0);
|
||||
ec.advance(1.0);
|
||||
expect(ec.started, true);
|
||||
expect(ec.progress, 0);
|
||||
|
||||
@ -120,60 +106,50 @@ void main() {
|
||||
for (var iteration = 0; iteration < 5; iteration++) {
|
||||
// forward
|
||||
for (var i = 0; i < 200; i++) {
|
||||
ec.update(0.01);
|
||||
ec.advance(0.01);
|
||||
expect(ec.progress, closeTo((i + 1) / 200, 1e-10));
|
||||
}
|
||||
// atPeak
|
||||
for (var i = 0; i < 20; i++) {
|
||||
ec.update(0.01);
|
||||
ec.advance(0.01);
|
||||
expect(ec.progress, closeTo(1, i == 19 ? 1e-10 : 0));
|
||||
}
|
||||
// reverse
|
||||
for (var i = 0; i < 100; i++) {
|
||||
ec.update(0.01);
|
||||
ec.advance(0.01);
|
||||
expect(ec.progress, closeTo((99 - i) / 100, 1e-10));
|
||||
}
|
||||
// atPit
|
||||
for (var i = 0; i < 50; i++) {
|
||||
ec.update(0.01);
|
||||
ec.advance(0.01);
|
||||
expect(ec.progress, closeTo(0, i == 49 ? 1e-10 : 0));
|
||||
}
|
||||
}
|
||||
|
||||
// In the end, the progress will remain at zero
|
||||
ec.update(1e-10);
|
||||
ec.advance(1e-10);
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, true);
|
||||
expect(ec.progress, 0);
|
||||
});
|
||||
|
||||
testRandom('infinite', (Random random) {
|
||||
final ec = StandardEffectController(duration: 1.4, infinite: true);
|
||||
const duration = 1.4;
|
||||
final ec = EffectController(duration: duration, infinite: true);
|
||||
expect(ec.isInfinite, true);
|
||||
expect(ec.isSimpleAnimation, true);
|
||||
expect(ec.progress, 0);
|
||||
expect(ec.started, false);
|
||||
|
||||
ec.update(0);
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, false);
|
||||
expect(ec.progress, 0);
|
||||
|
||||
var stageTime = 0.0;
|
||||
for (var i = 0; i < 100; i++) {
|
||||
final dt = random.nextDouble() * 0.3;
|
||||
ec.update(dt);
|
||||
ec.advance(dt);
|
||||
stageTime += dt;
|
||||
if (stageTime >= ec.forwardDuration) {
|
||||
stageTime -= ec.forwardDuration;
|
||||
// The controller will report once `progress==1`, exactly, and then
|
||||
// once `progress==0`, also exactly.
|
||||
expect(ec.progress, 1);
|
||||
ec.update(0);
|
||||
expect(ec.progress, 0);
|
||||
} else {
|
||||
expect(ec.progress, closeTo(stageTime / ec.forwardDuration, 1e-10));
|
||||
if (stageTime >= duration) {
|
||||
stageTime -= duration;
|
||||
}
|
||||
expect(ec.progress, closeTo(stageTime / duration, 1e-10));
|
||||
}
|
||||
|
||||
expect(ec.started, true);
|
||||
@ -182,32 +158,32 @@ void main() {
|
||||
});
|
||||
|
||||
test('reset', () {
|
||||
final ec = StandardEffectController(duration: 1.23);
|
||||
expect(ec.started, false);
|
||||
final ec = EffectController(duration: 1.23);
|
||||
expect(ec.started, true);
|
||||
expect(ec.progress, 0);
|
||||
|
||||
ec.update(0.4);
|
||||
ec.advance(0.4);
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, false);
|
||||
expect(ec.progress, closeTo(0.4 / 1.23, 1e-10));
|
||||
|
||||
ec.reset();
|
||||
expect(ec.started, false);
|
||||
ec.setToStart();
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, false);
|
||||
expect(ec.progress, 0);
|
||||
|
||||
ec.update(0.5);
|
||||
ec.advance(0.5);
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, false);
|
||||
expect(ec.progress, closeTo(0.5 / 1.23, 1e-10));
|
||||
|
||||
ec.update(1);
|
||||
ec.advance(1);
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, true);
|
||||
expect(ec.progress, 1);
|
||||
|
||||
ec.reset();
|
||||
expect(ec.started, false);
|
||||
ec.setToStart();
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, false);
|
||||
expect(ec.progress, 0);
|
||||
});
|
||||
@ -218,80 +194,83 @@ void main() {
|
||||
}
|
||||
|
||||
expectThrows(
|
||||
() => StandardEffectController(duration: -1),
|
||||
() => EffectController(duration: -1),
|
||||
);
|
||||
expectThrows(
|
||||
() => StandardEffectController(duration: 1, repeatCount: 0),
|
||||
() => EffectController(duration: 1, repeatCount: 0),
|
||||
);
|
||||
expectThrows(
|
||||
() => StandardEffectController(
|
||||
() => EffectController(
|
||||
duration: 1,
|
||||
infinite: true,
|
||||
repeatCount: 3,
|
||||
),
|
||||
);
|
||||
expectThrows(
|
||||
() => StandardEffectController(duration: 1, repeatCount: -1),
|
||||
() => EffectController(duration: 1, repeatCount: -1),
|
||||
);
|
||||
expectThrows(
|
||||
() => StandardEffectController(duration: 1, reverseDuration: -1),
|
||||
() => EffectController(duration: 1, reverseDuration: -1),
|
||||
);
|
||||
expectThrows(
|
||||
() => StandardEffectController(duration: 1, startDelay: -1),
|
||||
() => EffectController(duration: 1, startDelay: -1),
|
||||
);
|
||||
expectThrows(
|
||||
() => StandardEffectController(duration: 1, atMaxDuration: -1),
|
||||
() => EffectController(duration: 1, atMaxDuration: -1),
|
||||
);
|
||||
expectThrows(
|
||||
() => StandardEffectController(duration: 1, atMinDuration: -1),
|
||||
() => EffectController(duration: 1, atMinDuration: -1),
|
||||
);
|
||||
});
|
||||
|
||||
test('curve', () {
|
||||
final curve = Curves.easeIn;
|
||||
final ec = StandardEffectController(
|
||||
const curve = Curves.easeIn;
|
||||
final ec = EffectController(
|
||||
duration: 1,
|
||||
curve: curve,
|
||||
reverseDuration: 0.8,
|
||||
);
|
||||
expect(ec.started, false);
|
||||
expect(ec.cycleDuration, 1.8);
|
||||
expect(ec.started, true);
|
||||
expect(ec.duration, 1.8);
|
||||
|
||||
for (var i = 0; i < 100; i++) {
|
||||
ec.update(0.01);
|
||||
expect(ec.progress, closeTo(curve.transform((i + 1) / 100), 1e-10));
|
||||
ec.advance(0.01);
|
||||
// Precision is less for final iteration, because it may flip over
|
||||
// to the backwards curve.
|
||||
final epsilon = i == 99 ? 1e-6 : 1e-10;
|
||||
expect(ec.progress, closeTo(curve.transform((i + 1) / 100), epsilon));
|
||||
}
|
||||
for (var i = 0; i < 80; i++) {
|
||||
ec.update(0.01);
|
||||
ec.advance(0.01);
|
||||
expect(
|
||||
ec.progress,
|
||||
closeTo(curve.flipped.transform(1 - (i + 1) / 80), 1e-10),
|
||||
);
|
||||
}
|
||||
ec.update(1e-10);
|
||||
ec.advance(1e-10);
|
||||
expect(ec.completed, true);
|
||||
expect(ec.progress, 0);
|
||||
});
|
||||
|
||||
test('reverse curve', () {
|
||||
final curve = Curves.easeInQuad;
|
||||
final ec = StandardEffectController(
|
||||
const curve = Curves.easeInQuad;
|
||||
final ec = EffectController(
|
||||
duration: 1,
|
||||
reverseDuration: 1,
|
||||
reverseCurve: curve,
|
||||
);
|
||||
expect(ec.started, false);
|
||||
expect(ec.cycleDuration, 2);
|
||||
expect(ec.started, true);
|
||||
expect(ec.duration, 2);
|
||||
|
||||
ec.update(1);
|
||||
ec.advance(1);
|
||||
expect(ec.progress, 1);
|
||||
expect(ec.completed, false);
|
||||
|
||||
for (var i = 0; i < 100; i++) {
|
||||
ec.update(0.01);
|
||||
ec.advance(0.01);
|
||||
expect(ec.progress, closeTo(curve.transform(1 - (i + 1) / 100), 1e-10));
|
||||
}
|
||||
ec.update(1e-10);
|
||||
ec.advance(1e-10);
|
||||
expect(ec.completed, true);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,56 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flame/src/effects/controllers/infinite_effect_controller.dart';
|
||||
import 'package:flame/src/effects/controllers/linear_effect_controller.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('InfiniteEffectController', () {
|
||||
test('basic properties', () {
|
||||
final ec = InfiniteEffectController(LinearEffectController(1));
|
||||
expect(ec.isInfinite, true);
|
||||
expect(ec.isRandom, false);
|
||||
expect(ec.duration, null);
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, false);
|
||||
expect(ec.progress, 0);
|
||||
});
|
||||
|
||||
test('reset', () {
|
||||
final ec = InfiniteEffectController(LinearEffectController(1));
|
||||
ec.setToEnd();
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, false);
|
||||
expect(ec.progress, 1);
|
||||
ec.setToStart();
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, false);
|
||||
expect(ec.progress, 0);
|
||||
});
|
||||
|
||||
testRandom('advance', (Random random) {
|
||||
final ec = InfiniteEffectController(LinearEffectController(1));
|
||||
var totalTime = 0.0;
|
||||
while (totalTime < 10) {
|
||||
final dt = random.nextDouble() * 0.1;
|
||||
totalTime += dt;
|
||||
expect(ec.advance(dt), 0);
|
||||
expect(ec.progress, closeTo(totalTime % 1, 5e-14));
|
||||
}
|
||||
expect(ec.completed, false);
|
||||
});
|
||||
|
||||
testRandom('recede', (Random random) {
|
||||
final ec = InfiniteEffectController(LinearEffectController(1));
|
||||
var totalTime = 0.0;
|
||||
while (totalTime < 10) {
|
||||
final dt = random.nextDouble() * 0.1;
|
||||
totalTime += dt;
|
||||
expect(ec.recede(dt), 0);
|
||||
expect(ec.progress, closeTo((11 - totalTime) % 1, 5e-14));
|
||||
}
|
||||
expect(ec.completed, false);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,86 @@
|
||||
import 'package:flame/src/effects/controllers/linear_effect_controller.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('LinearEffectController', () {
|
||||
test('[duration==0]', () {
|
||||
final ec = LinearEffectController(0);
|
||||
expect(ec.duration, 0);
|
||||
expect(ec.isInfinite, false);
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, true);
|
||||
expect(ec.progress, 1);
|
||||
|
||||
expect(ec.advance(0.1), 0.1);
|
||||
expect(ec.progress, 1);
|
||||
});
|
||||
|
||||
test('[duration==0] reset', () {
|
||||
final ec = LinearEffectController(0);
|
||||
ec.setToStart();
|
||||
expect(ec.completed, true);
|
||||
expect(ec.progress, 1);
|
||||
ec.setToEnd();
|
||||
expect(ec.completed, true);
|
||||
expect(ec.progress, 1);
|
||||
});
|
||||
|
||||
test('[duration==1]', () {
|
||||
final ec = LinearEffectController(1);
|
||||
expect(ec.duration, 1);
|
||||
expect(ec.progress, 0);
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, false);
|
||||
expect(ec.isInfinite, false);
|
||||
|
||||
expect(ec.advance(0.5), 0);
|
||||
expect(ec.progress, 0.5);
|
||||
expect(ec.completed, false);
|
||||
|
||||
expect(ec.advance(0.5), 0);
|
||||
expect(ec.progress, 1);
|
||||
expect(ec.completed, true);
|
||||
|
||||
expect(ec.advance(0.00001), closeTo(0.00001, 1e-15));
|
||||
expect(ec.progress, 1);
|
||||
expect(ec.completed, true);
|
||||
|
||||
expect(ec.recede(0.5), 0);
|
||||
expect(ec.progress, 0.5);
|
||||
|
||||
expect(ec.recede(0.5), 0);
|
||||
expect(ec.progress, 0);
|
||||
|
||||
expect(ec.recede(0.00001), closeTo(0.00001, 1e-15));
|
||||
expect(ec.progress, 0);
|
||||
});
|
||||
|
||||
test('[duration==2] reset', () {
|
||||
final ec = LinearEffectController(2);
|
||||
expect(ec.advance(3), 1);
|
||||
expect(ec.completed, true);
|
||||
expect(ec.progress, 1);
|
||||
|
||||
ec.setToStart();
|
||||
expect(ec.completed, false);
|
||||
expect(ec.progress, 0);
|
||||
|
||||
expect(ec.advance(1), 0);
|
||||
expect(ec.progress, closeTo(0.5, 1e-15));
|
||||
expect(ec.completed, false);
|
||||
|
||||
expect(ec.advance(1), 0);
|
||||
expect(ec.progress, 1);
|
||||
expect(ec.completed, true);
|
||||
|
||||
expect(ec.advance(1), 1);
|
||||
expect(ec.progress, 1);
|
||||
expect(ec.completed, true);
|
||||
|
||||
ec.setToStart();
|
||||
ec.setToEnd();
|
||||
expect(ec.progress, 1);
|
||||
expect(ec.completed, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,182 @@
|
||||
import 'package:flame/src/effects/controllers/infinite_effect_controller.dart';
|
||||
import 'package:flame/src/effects/controllers/linear_effect_controller.dart';
|
||||
import 'package:flame/src/effects/controllers/repeated_effect_controller.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('RepeatedEffectController', () {
|
||||
test('basic properties', () {
|
||||
final ec = RepeatedEffectController(LinearEffectController(1), 5);
|
||||
expect(ec.isInfinite, false);
|
||||
expect(ec.isRandom, false);
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, false);
|
||||
expect(ec.duration, 5);
|
||||
expect(ec.progress, 0);
|
||||
expect(ec.repeatCount, 5);
|
||||
expect(ec.remainingIterationsCount, 5);
|
||||
});
|
||||
|
||||
test('reset', () {
|
||||
final ec = RepeatedEffectController(LinearEffectController(1), 5);
|
||||
ec.setToEnd();
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, true);
|
||||
expect(ec.child.completed, true);
|
||||
expect(ec.progress, 1);
|
||||
expect(ec.remainingIterationsCount, 0);
|
||||
|
||||
ec.setToStart();
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, false);
|
||||
expect(ec.child.completed, false);
|
||||
expect(ec.progress, 0);
|
||||
expect(ec.remainingIterationsCount, 5);
|
||||
});
|
||||
|
||||
test('advance', () {
|
||||
final ec = RepeatedEffectController(LinearEffectController(2), 5);
|
||||
expect(ec.remainingIterationsCount, 5);
|
||||
|
||||
// First iteration
|
||||
expect(ec.advance(1), 0);
|
||||
expect(ec.progress, 0.5);
|
||||
expect(ec.remainingIterationsCount, 5);
|
||||
|
||||
expect(ec.advance(1), 0);
|
||||
expect(ec.progress, 1);
|
||||
expect(ec.remainingIterationsCount, 5);
|
||||
|
||||
// Second iteration
|
||||
expect(ec.advance(1), 0);
|
||||
expect(ec.progress, 0.5);
|
||||
expect(ec.remainingIterationsCount, 4);
|
||||
|
||||
expect(ec.advance(1), 0);
|
||||
expect(ec.progress, 1);
|
||||
expect(ec.remainingIterationsCount, 4);
|
||||
|
||||
// Third iteration
|
||||
expect(ec.advance(1), 0);
|
||||
expect(ec.progress, 0.5);
|
||||
expect(ec.remainingIterationsCount, 3);
|
||||
|
||||
expect(ec.advance(1), 0);
|
||||
expect(ec.progress, 1);
|
||||
expect(ec.remainingIterationsCount, 3);
|
||||
|
||||
// Forth iteration
|
||||
expect(ec.advance(1), 0);
|
||||
expect(ec.progress, 0.5);
|
||||
expect(ec.remainingIterationsCount, 2);
|
||||
|
||||
expect(ec.advance(1), 0);
|
||||
expect(ec.progress, 1);
|
||||
expect(ec.remainingIterationsCount, 2);
|
||||
|
||||
// Fifth iteration
|
||||
expect(ec.advance(1), 0);
|
||||
expect(ec.progress, 0.5);
|
||||
expect(ec.remainingIterationsCount, 1);
|
||||
|
||||
expect(ec.advance(1), 0);
|
||||
expect(ec.progress, 1);
|
||||
// last iteration is consumed immediately
|
||||
expect(ec.remainingIterationsCount, 0);
|
||||
expect(ec.completed, true);
|
||||
|
||||
// Any subsequent time will be spilled over
|
||||
expect(ec.advance(1), 1);
|
||||
expect(ec.progress, 1);
|
||||
expect(ec.completed, true);
|
||||
});
|
||||
|
||||
test('advance 2', () {
|
||||
const n = 5;
|
||||
const dt = 0.17;
|
||||
final nIterations = (n / dt).floor();
|
||||
final ec = RepeatedEffectController(LinearEffectController(1), n);
|
||||
for (var i = 0; i < nIterations; i++) {
|
||||
expect(ec.advance(dt), 0);
|
||||
expect(ec.progress, closeTo((i + 1) * dt % 1, 1e-15));
|
||||
}
|
||||
expect(ec.advance(dt), closeTo((nIterations + 1) * dt - n, 1e-15));
|
||||
expect(ec.completed, true);
|
||||
expect(ec.progress, 1);
|
||||
});
|
||||
|
||||
test('recede', () {
|
||||
final ec = RepeatedEffectController(LinearEffectController(2), 5);
|
||||
ec.setToEnd();
|
||||
expect(ec.completed, true);
|
||||
expect(ec.recede(0), 0);
|
||||
expect(ec.completed, true);
|
||||
expect(ec.remainingIterationsCount, 0);
|
||||
|
||||
// Fifth iteration
|
||||
expect(ec.recede(1), 0);
|
||||
expect(ec.progress, 0.5);
|
||||
expect(ec.completed, false);
|
||||
expect(ec.remainingIterationsCount, 1);
|
||||
|
||||
expect(ec.recede(1), 0);
|
||||
expect(ec.progress, 0);
|
||||
expect(ec.remainingIterationsCount, 1);
|
||||
|
||||
// Forth iteration
|
||||
expect(ec.recede(1), 0);
|
||||
expect(ec.progress, 0.5);
|
||||
expect(ec.remainingIterationsCount, 2);
|
||||
|
||||
expect(ec.recede(1), 0);
|
||||
expect(ec.progress, 0);
|
||||
expect(ec.remainingIterationsCount, 2);
|
||||
|
||||
// Third iteration
|
||||
expect(ec.recede(1), 0);
|
||||
expect(ec.progress, 0.5);
|
||||
expect(ec.remainingIterationsCount, 3);
|
||||
|
||||
expect(ec.recede(1), 0);
|
||||
expect(ec.progress, 0);
|
||||
expect(ec.remainingIterationsCount, 3);
|
||||
|
||||
// Second iteration
|
||||
expect(ec.recede(1), 0);
|
||||
expect(ec.progress, 0.5);
|
||||
expect(ec.remainingIterationsCount, 4);
|
||||
|
||||
expect(ec.recede(1), 0);
|
||||
expect(ec.progress, 0);
|
||||
expect(ec.remainingIterationsCount, 4);
|
||||
|
||||
// First iteration
|
||||
expect(ec.recede(1), 0);
|
||||
expect(ec.progress, 0.5);
|
||||
expect(ec.remainingIterationsCount, 5);
|
||||
|
||||
expect(ec.recede(1), 0);
|
||||
expect(ec.progress, 0);
|
||||
expect(ec.remainingIterationsCount, 5);
|
||||
expect(ec.started, true);
|
||||
|
||||
// Extra iterations
|
||||
expect(ec.recede(1), 1);
|
||||
expect(ec.progress, 0);
|
||||
expect(ec.remainingIterationsCount, 5);
|
||||
});
|
||||
|
||||
test('errors', () {
|
||||
final ec = LinearEffectController(1);
|
||||
expect(
|
||||
() => RepeatedEffectController(InfiniteEffectController(ec), 1),
|
||||
failsAssert('child cannot be infinite'),
|
||||
);
|
||||
expect(
|
||||
() => RepeatedEffectController(ec, 0),
|
||||
failsAssert('repeatCount must be positive'),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,117 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flame/src/effects/controllers/infinite_effect_controller.dart';
|
||||
import 'package:flame/src/effects/controllers/linear_effect_controller.dart';
|
||||
import 'package:flame/src/effects/controllers/sequence_effect_controller.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('SequenceEffectController', () {
|
||||
test('basic properties', () {
|
||||
final ec = SequenceEffectController([
|
||||
LinearEffectController(1),
|
||||
LinearEffectController(2),
|
||||
LinearEffectController(3),
|
||||
]);
|
||||
expect(ec.isRandom, false);
|
||||
expect(ec.isInfinite, false);
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, false);
|
||||
expect(ec.duration, 6);
|
||||
expect(ec.progress, 0);
|
||||
expect(ec.children.length, 3);
|
||||
});
|
||||
|
||||
test('reset', () {
|
||||
final ec = SequenceEffectController([
|
||||
LinearEffectController(1),
|
||||
LinearEffectController(2),
|
||||
LinearEffectController(3),
|
||||
]);
|
||||
ec.setToEnd();
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, true);
|
||||
expect(ec.progress, 1);
|
||||
expect(ec.children.every((c) => c.completed), true);
|
||||
|
||||
ec.setToStart();
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, false);
|
||||
expect(ec.progress, 0);
|
||||
expect(ec.children.every((c) => c.progress == 0), true);
|
||||
});
|
||||
|
||||
testRandom('advance', (Random random) {
|
||||
final ec = SequenceEffectController([
|
||||
LinearEffectController(1),
|
||||
LinearEffectController(2),
|
||||
LinearEffectController(3),
|
||||
]);
|
||||
|
||||
var totalTime = 0.0;
|
||||
while (totalTime <= 6) {
|
||||
expect(
|
||||
ec.progress,
|
||||
closeTo(
|
||||
totalTime <= 1
|
||||
? totalTime
|
||||
: totalTime <= 3
|
||||
? (totalTime - 1) / 2
|
||||
: (totalTime - 3) / 3,
|
||||
1e-15,
|
||||
),
|
||||
);
|
||||
final dt = random.nextDouble();
|
||||
totalTime += dt;
|
||||
ec.advance(dt);
|
||||
}
|
||||
expect(ec.completed, true);
|
||||
expect(ec.progress, 1);
|
||||
expect(ec.children.every((c) => c.completed), true);
|
||||
});
|
||||
|
||||
testRandom('recede', (Random random) {
|
||||
final ec = SequenceEffectController([
|
||||
LinearEffectController(1),
|
||||
LinearEffectController(2),
|
||||
LinearEffectController(3),
|
||||
]);
|
||||
ec.setToEnd();
|
||||
|
||||
var totalTime = 6.0;
|
||||
while (totalTime >= 0) {
|
||||
expect(
|
||||
ec.progress,
|
||||
closeTo(
|
||||
totalTime <= 1
|
||||
? totalTime
|
||||
: totalTime <= 3
|
||||
? (totalTime - 1) / 2
|
||||
: (totalTime - 3) / 3,
|
||||
1e-14,
|
||||
),
|
||||
);
|
||||
final dt = random.nextDouble() * 0.1;
|
||||
totalTime -= dt;
|
||||
ec.recede(dt);
|
||||
}
|
||||
expect(ec.completed, false);
|
||||
expect(ec.progress, 0);
|
||||
expect(ec.children.every((c) => c.progress == 0), true);
|
||||
});
|
||||
|
||||
test('errors', () {
|
||||
expect(
|
||||
() => SequenceEffectController([]),
|
||||
failsAssert('List of controllers cannot be empty'),
|
||||
);
|
||||
expect(
|
||||
() => SequenceEffectController(
|
||||
[InfiniteEffectController(LinearEffectController(1))],
|
||||
),
|
||||
failsAssert('Children controllers cannot be infinite'),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
import 'package:flame/src/components/component.dart';
|
||||
import 'package:flame/src/effects/controllers/effect_controller.dart';
|
||||
import 'package:flame/src/effects/effect.dart';
|
||||
import 'package:flame/src/effects/effect_controller.dart';
|
||||
import 'package:flame/src/effects/standard_effect_controller.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@ -39,7 +38,7 @@ class _MyEffect extends Effect {
|
||||
void main() {
|
||||
group('Effect', () {
|
||||
test('pause & resume', () {
|
||||
final effect = _MyEffect(StandardEffectController(duration: 10));
|
||||
final effect = _MyEffect(EffectController(duration: 10));
|
||||
expect(effect.x, -1);
|
||||
expect(effect.isPaused, false);
|
||||
|
||||
@ -81,7 +80,7 @@ void main() {
|
||||
(game) {
|
||||
final obj = Component();
|
||||
game.add(obj);
|
||||
final effect = _MyEffect(StandardEffectController(duration: 1));
|
||||
final effect = _MyEffect(EffectController(duration: 1));
|
||||
obj.add(effect);
|
||||
game.update(0);
|
||||
expect(obj.children.length, 1);
|
||||
@ -102,7 +101,7 @@ void main() {
|
||||
(game) {
|
||||
final obj = Component();
|
||||
game.add(obj);
|
||||
final effect = _MyEffect(StandardEffectController(duration: 1));
|
||||
final effect = _MyEffect(EffectController(duration: 1));
|
||||
effect.removeOnFinish = false;
|
||||
obj.add(effect);
|
||||
game.update(0);
|
||||
@ -130,11 +129,9 @@ void main() {
|
||||
effect.reset();
|
||||
expect(effect.x, -1);
|
||||
expect(effect.controller.completed, false);
|
||||
expect(effect.controller.started, false);
|
||||
|
||||
game.update(0.5);
|
||||
expect(effect.x, 0.5);
|
||||
expect(effect.controller.started, true);
|
||||
expect(effect.controller.completed, false);
|
||||
|
||||
// Now the effect completes once again, but still remains mounted
|
||||
@ -149,7 +146,7 @@ void main() {
|
||||
test('onStart & onFinish', () {
|
||||
var nStarted = 0;
|
||||
var nFinished = 0;
|
||||
final effect = _MyEffect(StandardEffectController(duration: 1))
|
||||
final effect = _MyEffect(EffectController(duration: 1))
|
||||
..onStartCallback = () {
|
||||
nStarted++;
|
||||
}
|
||||
@ -158,19 +155,16 @@ void main() {
|
||||
};
|
||||
|
||||
effect.update(0);
|
||||
expect(effect.controller.started, true);
|
||||
expect(effect.x, 0);
|
||||
expect(nStarted, 1);
|
||||
expect(nFinished, 0);
|
||||
|
||||
effect.update(0.5);
|
||||
expect(effect.controller.started, true);
|
||||
expect(effect.x, 0.5);
|
||||
expect(nStarted, 1);
|
||||
expect(nFinished, 0);
|
||||
|
||||
effect.update(0.5);
|
||||
expect(effect.controller.started, true);
|
||||
expect(effect.controller.completed, true);
|
||||
expect(effect.x, 1);
|
||||
expect(nStarted, 1);
|
||||
|
||||
@ -3,8 +3,8 @@ import 'dart:ui';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/src/effects/controllers/linear_effect_controller.dart';
|
||||
import 'package:flame/src/effects/move_effect.dart';
|
||||
import 'package:flame/src/effects/simple_effect_controller.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
@ -17,7 +17,7 @@ void main() {
|
||||
game.update(0);
|
||||
|
||||
object.add(
|
||||
MoveEffect.by(Vector2(5, -1), SimpleEffectController(duration: 1)),
|
||||
MoveEffect.by(Vector2(5, -1), LinearEffectController(1)),
|
||||
);
|
||||
game.update(0.5);
|
||||
expect(object.position.x, closeTo(3 + 2.5, 1e-15));
|
||||
@ -35,7 +35,7 @@ void main() {
|
||||
game.update(0);
|
||||
|
||||
object.add(
|
||||
MoveEffect.to(Vector2(5, -1), SimpleEffectController(duration: 1)),
|
||||
MoveEffect.to(Vector2(5, -1), LinearEffectController(1)),
|
||||
);
|
||||
game.update(0.5);
|
||||
expect(object.position.x, closeTo(3 * 0.5 + 5 * 0.5, 1e-15));
|
||||
@ -57,7 +57,7 @@ void main() {
|
||||
MoveEffect.along(
|
||||
Path()
|
||||
..addOval(Rect.fromCircle(center: const Offset(6, 10), radius: 50)),
|
||||
SimpleEffectController(duration: 1),
|
||||
LinearEffectController(1),
|
||||
),
|
||||
);
|
||||
game.update(0);
|
||||
@ -73,7 +73,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('#along wrong arguments', () {
|
||||
final controller = SimpleEffectController();
|
||||
final controller = LinearEffectController(0);
|
||||
expect(
|
||||
() => MoveEffect.along(Path(), controller),
|
||||
throwsArgumentError,
|
||||
|
||||
@ -2,8 +2,8 @@ import 'dart:math';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/src/effects/controllers/effect_controller.dart';
|
||||
import 'package:flame/src/effects/opacity_effect.dart';
|
||||
import 'package:flame/src/effects/standard_effect_controller.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@ -19,7 +19,7 @@ void main() {
|
||||
|
||||
component.setOpacity(0.2);
|
||||
component.add(
|
||||
OpacityEffect.by(0.4, StandardEffectController(duration: 1)),
|
||||
OpacityEffect.by(0.4, EffectController(duration: 1)),
|
||||
);
|
||||
game.update(0);
|
||||
expect(component.getOpacity(), 0.2);
|
||||
@ -41,7 +41,7 @@ void main() {
|
||||
|
||||
component.setOpacity(0.2);
|
||||
component.add(
|
||||
OpacityEffect.to(0.4, StandardEffectController(duration: 1)),
|
||||
OpacityEffect.to(0.4, EffectController(duration: 1)),
|
||||
);
|
||||
game.update(0);
|
||||
expect(component.getOpacity(), 0.2);
|
||||
@ -66,7 +66,7 @@ void main() {
|
||||
const step = 10 * 1 / 255;
|
||||
final effect = OpacityEffect.by(
|
||||
-step,
|
||||
StandardEffectController(duration: 1),
|
||||
EffectController(duration: 1),
|
||||
);
|
||||
component.add(effect..removeOnFinish = false);
|
||||
for (var i = 0; i < 5; i++) {
|
||||
@ -89,7 +89,7 @@ void main() {
|
||||
|
||||
final effect = OpacityEffect.to(
|
||||
0.0,
|
||||
StandardEffectController(duration: 1),
|
||||
EffectController(duration: 1),
|
||||
);
|
||||
component.add(effect..removeOnFinish = false);
|
||||
for (var i = 0; i < 5; i++) {
|
||||
@ -111,12 +111,12 @@ void main() {
|
||||
game.update(0);
|
||||
|
||||
component.add(
|
||||
OpacityEffect.by(0.5, StandardEffectController(duration: 10)),
|
||||
OpacityEffect.by(0.5, EffectController(duration: 10)),
|
||||
);
|
||||
component.add(
|
||||
OpacityEffect.by(
|
||||
0.5,
|
||||
StandardEffectController(
|
||||
EffectController(
|
||||
duration: 1,
|
||||
reverseDuration: 1,
|
||||
repeatCount: 5,
|
||||
@ -149,7 +149,7 @@ void main() {
|
||||
game.ensureAdd(component);
|
||||
|
||||
final effect = OpacityEffect.fadeOut(
|
||||
StandardEffectController(
|
||||
EffectController(
|
||||
duration: 1,
|
||||
reverseDuration: 1,
|
||||
infinite: true,
|
||||
|
||||
@ -2,8 +2,8 @@ import 'dart:math';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/src/effects/controllers/effect_controller.dart';
|
||||
import 'package:flame/src/effects/rotate_effect.dart';
|
||||
import 'package:flame/src/effects/standard_effect_controller.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@ -18,7 +18,7 @@ void main() {
|
||||
|
||||
object.angle = 1;
|
||||
object.add(
|
||||
RotateEffect.by(1, StandardEffectController(duration: 1)),
|
||||
RotateEffect.by(1, EffectController(duration: 1)),
|
||||
);
|
||||
game.update(0);
|
||||
expect(object.angle, 1);
|
||||
@ -43,7 +43,7 @@ void main() {
|
||||
|
||||
object.angle = 1;
|
||||
object.add(
|
||||
RotateEffect.to(3, StandardEffectController(duration: 1)),
|
||||
RotateEffect.to(3, EffectController(duration: 1)),
|
||||
);
|
||||
game.update(0);
|
||||
expect(object.angle, 1);
|
||||
@ -65,7 +65,7 @@ void main() {
|
||||
game.add(object);
|
||||
game.update(0);
|
||||
|
||||
final effect = RotateEffect.by(1, StandardEffectController(duration: 1));
|
||||
final effect = RotateEffect.by(1, EffectController(duration: 1));
|
||||
object.add(effect..removeOnFinish = false);
|
||||
for (var i = 0; i < 5; i++) {
|
||||
expect(object.angle, i);
|
||||
@ -83,7 +83,7 @@ void main() {
|
||||
game.add(object);
|
||||
game.update(0);
|
||||
|
||||
final effect = RotateEffect.to(1, StandardEffectController(duration: 1));
|
||||
final effect = RotateEffect.to(1, EffectController(duration: 1));
|
||||
object.add(effect..removeOnFinish = false);
|
||||
for (var i = 0; i < 5; i++) {
|
||||
object.angle = 1 + 4.0 * i;
|
||||
@ -102,12 +102,12 @@ void main() {
|
||||
game.update(0);
|
||||
|
||||
object.add(
|
||||
RotateEffect.by(5, StandardEffectController(duration: 10)),
|
||||
RotateEffect.by(5, EffectController(duration: 10)),
|
||||
);
|
||||
object.add(
|
||||
RotateEffect.by(
|
||||
0.5,
|
||||
StandardEffectController(
|
||||
EffectController(
|
||||
duration: 1,
|
||||
reverseDuration: 1,
|
||||
repeatCount: 5,
|
||||
@ -134,7 +134,7 @@ void main() {
|
||||
|
||||
final effect = RotateEffect.by(
|
||||
1.0,
|
||||
StandardEffectController(
|
||||
EffectController(
|
||||
duration: 1,
|
||||
reverseDuration: 1,
|
||||
infinite: true,
|
||||
|
||||
@ -2,8 +2,8 @@ import 'dart:math';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/src/effects/controllers/effect_controller.dart';
|
||||
import 'package:flame/src/effects/scale_effect.dart';
|
||||
import 'package:flame/src/effects/standard_effect_controller.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@ -15,7 +15,7 @@ void main() {
|
||||
|
||||
component.scale = Vector2.all(1.0);
|
||||
component.add(
|
||||
ScaleEffect.by(Vector2.all(1.0), StandardEffectController(duration: 1)),
|
||||
ScaleEffect.by(Vector2.all(1.0), EffectController(duration: 1)),
|
||||
);
|
||||
game.update(0);
|
||||
expectVector2(component.scale, Vector2.all(1.0));
|
||||
@ -37,7 +37,7 @@ void main() {
|
||||
|
||||
component.scale = Vector2.all(1.0);
|
||||
component.add(
|
||||
ScaleEffect.to(Vector2.all(3.0), StandardEffectController(duration: 1)),
|
||||
ScaleEffect.to(Vector2.all(3.0), EffectController(duration: 1)),
|
||||
);
|
||||
game.update(0);
|
||||
expectVector2(component.scale, Vector2.all(1.0));
|
||||
@ -59,7 +59,7 @@ void main() {
|
||||
|
||||
final effect = ScaleEffect.by(
|
||||
Vector2.all(1.0),
|
||||
StandardEffectController(duration: 1),
|
||||
EffectController(duration: 1),
|
||||
);
|
||||
component.add(effect..removeOnFinish = false);
|
||||
final expectedScale = Vector2.all(1.0);
|
||||
@ -80,7 +80,7 @@ void main() {
|
||||
|
||||
final effect = ScaleEffect.to(
|
||||
Vector2.all(1.0),
|
||||
StandardEffectController(duration: 1),
|
||||
EffectController(duration: 1),
|
||||
);
|
||||
component.add(effect..removeOnFinish = false);
|
||||
for (var i = 0; i < 5; i++) {
|
||||
@ -98,12 +98,12 @@ void main() {
|
||||
game.ensureAdd(component);
|
||||
|
||||
component.add(
|
||||
ScaleEffect.by(Vector2.all(5), StandardEffectController(duration: 10)),
|
||||
ScaleEffect.by(Vector2.all(5), EffectController(duration: 10)),
|
||||
);
|
||||
component.add(
|
||||
ScaleEffect.by(
|
||||
Vector2.all(0.5),
|
||||
StandardEffectController(
|
||||
EffectController(
|
||||
duration: 1,
|
||||
reverseDuration: 1,
|
||||
repeatCount: 5,
|
||||
@ -137,7 +137,7 @@ void main() {
|
||||
|
||||
final effect = ScaleEffect.by(
|
||||
Vector2.all(1.0),
|
||||
StandardEffectController(
|
||||
EffectController(
|
||||
duration: 1,
|
||||
reverseDuration: 1,
|
||||
infinite: true,
|
||||
|
||||
@ -1,129 +0,0 @@
|
||||
import 'package:flame/src/effects/simple_effect_controller.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('SimpleEffectController', () {
|
||||
test('default', () {
|
||||
final ec = SimpleEffectController();
|
||||
expect(ec.duration, 0);
|
||||
expect(ec.delay, 0);
|
||||
expect(ec.isInfinite, false);
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, true);
|
||||
expect(ec.progress, 1);
|
||||
});
|
||||
|
||||
test('simple with duration', () {
|
||||
final ec = SimpleEffectController(duration: 1);
|
||||
expect(ec.delay, 0);
|
||||
expect(ec.duration, 1);
|
||||
expect(ec.progress, 0);
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, false);
|
||||
expect(ec.isInfinite, false);
|
||||
|
||||
ec.update(0.5);
|
||||
expect(ec.progress, 0.5);
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, false);
|
||||
|
||||
ec.update(0.5);
|
||||
expect(ec.progress, 1);
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, true);
|
||||
|
||||
ec.update(0.00001);
|
||||
expect(ec.progress, 1);
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, true);
|
||||
});
|
||||
|
||||
test('simple with delay', () {
|
||||
final ec = SimpleEffectController(delay: 1);
|
||||
expect(ec.isInfinite, false);
|
||||
expect(ec.started, false);
|
||||
expect(ec.completed, false);
|
||||
expect(ec.progress, 0);
|
||||
expect(ec.delay, 1);
|
||||
expect(ec.duration, 0);
|
||||
|
||||
ec.update(0.5);
|
||||
expect(ec.started, false);
|
||||
expect(ec.completed, false);
|
||||
expect(ec.progress, 0);
|
||||
|
||||
ec.update(0.5);
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, true);
|
||||
expect(ec.progress, 1);
|
||||
});
|
||||
|
||||
test('duration + delay', () {
|
||||
final ec = SimpleEffectController(duration: 1, delay: 2);
|
||||
expect(ec.isInfinite, false);
|
||||
expect(ec.started, false);
|
||||
expect(ec.completed, false);
|
||||
expect(ec.duration, 1);
|
||||
expect(ec.delay, 2);
|
||||
expect(ec.progress, 0);
|
||||
|
||||
ec.update(0.5);
|
||||
expect(ec.started, false);
|
||||
expect(ec.progress, 0);
|
||||
|
||||
ec.update(0.5);
|
||||
expect(ec.started, false);
|
||||
expect(ec.progress, 0);
|
||||
|
||||
ec.update(1);
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, false);
|
||||
expect(ec.progress, 0);
|
||||
|
||||
ec.update(0.5);
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, false);
|
||||
expect(ec.progress, 0.5);
|
||||
|
||||
ec.update(0.5);
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, true);
|
||||
expect(ec.progress, 1);
|
||||
});
|
||||
|
||||
test('reset', () {
|
||||
final ec = SimpleEffectController();
|
||||
ec.reset();
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, true);
|
||||
expect(ec.progress, 1);
|
||||
});
|
||||
|
||||
test('reset 2', () {
|
||||
final ec = SimpleEffectController(duration: 2, delay: 1);
|
||||
ec.update(3);
|
||||
expect(ec.completed, true);
|
||||
expect(ec.progress, 1);
|
||||
|
||||
ec.reset();
|
||||
expect(ec.started, false);
|
||||
expect(ec.completed, false);
|
||||
expect(ec.progress, 0);
|
||||
|
||||
ec.update(1);
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, false);
|
||||
expect(ec.progress, 0);
|
||||
|
||||
ec.update(1);
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, false);
|
||||
expect(ec.progress, closeTo(0.5, 1e-15));
|
||||
|
||||
ec.update(1);
|
||||
expect(ec.started, true);
|
||||
expect(ec.completed, true);
|
||||
expect(ec.progress, 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -2,8 +2,8 @@ import 'dart:math';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/src/effects/controllers/effect_controller.dart';
|
||||
import 'package:flame/src/effects/size_effect.dart';
|
||||
import 'package:flame/src/effects/standard_effect_controller.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@ -15,7 +15,7 @@ void main() {
|
||||
|
||||
component.size = Vector2.all(1.0);
|
||||
component.add(
|
||||
SizeEffect.by(Vector2.all(1.0), StandardEffectController(duration: 1)),
|
||||
SizeEffect.by(Vector2.all(1.0), EffectController(duration: 1)),
|
||||
);
|
||||
game.update(0);
|
||||
expectVector2(component.size, Vector2.all(1.0));
|
||||
@ -37,7 +37,7 @@ void main() {
|
||||
|
||||
component.size = Vector2.all(1.0);
|
||||
component.add(
|
||||
SizeEffect.to(Vector2.all(3.0), StandardEffectController(duration: 1)),
|
||||
SizeEffect.to(Vector2.all(3.0), EffectController(duration: 1)),
|
||||
);
|
||||
game.update(0);
|
||||
expectVector2(component.size, Vector2.all(1.0));
|
||||
@ -59,7 +59,7 @@ void main() {
|
||||
|
||||
final effect = SizeEffect.by(
|
||||
Vector2.all(1.0),
|
||||
StandardEffectController(duration: 1),
|
||||
EffectController(duration: 1),
|
||||
);
|
||||
component.add(effect..removeOnFinish = false);
|
||||
final expectedSize = Vector2.zero();
|
||||
@ -80,7 +80,7 @@ void main() {
|
||||
|
||||
final effect = SizeEffect.to(
|
||||
Vector2.all(1.0),
|
||||
StandardEffectController(duration: 1),
|
||||
EffectController(duration: 1),
|
||||
);
|
||||
component.add(effect..removeOnFinish = false);
|
||||
for (var i = 0; i < 5; i++) {
|
||||
@ -98,12 +98,12 @@ void main() {
|
||||
game.ensureAdd(component);
|
||||
|
||||
component.add(
|
||||
SizeEffect.by(Vector2.all(5), StandardEffectController(duration: 10)),
|
||||
SizeEffect.by(Vector2.all(5), EffectController(duration: 10)),
|
||||
);
|
||||
component.add(
|
||||
SizeEffect.by(
|
||||
Vector2.all(0.5),
|
||||
StandardEffectController(
|
||||
EffectController(
|
||||
duration: 1,
|
||||
reverseDuration: 1,
|
||||
repeatCount: 5,
|
||||
@ -137,7 +137,7 @@ void main() {
|
||||
|
||||
final effect = SizeEffect.by(
|
||||
Vector2.all(1.0),
|
||||
StandardEffectController(
|
||||
EffectController(
|
||||
duration: 1,
|
||||
reverseDuration: 1,
|
||||
infinite: true,
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import 'package:flame/src/components/position_component.dart';
|
||||
import 'package:flame/src/effects/effect_controller.dart';
|
||||
import 'package:flame/src/effects/standard_effect_controller.dart';
|
||||
import 'package:flame/src/effects/controllers/effect_controller.dart';
|
||||
import 'package:flame/src/effects/transform2d_effect.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@ -18,12 +17,12 @@ void main() {
|
||||
game.add(component);
|
||||
game.update(0);
|
||||
|
||||
final effect = _MyEffect(StandardEffectController(duration: 1));
|
||||
final effect = _MyEffect(EffectController(duration: 1));
|
||||
component.add(effect);
|
||||
game.update(0);
|
||||
expect(effect.transform, component.transform);
|
||||
|
||||
final effect2 = _MyEffect(StandardEffectController(duration: 1));
|
||||
final effect2 = _MyEffect(EffectController(duration: 1));
|
||||
expect(
|
||||
() async => game.add(effect2),
|
||||
throwsA(isA<UnsupportedError>()),
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
export 'src/expect_double.dart';
|
||||
export 'src/expect_vector2.dart';
|
||||
export 'src/fails_assert.dart';
|
||||
export 'src/flame_test.dart';
|
||||
export 'src/mock_gesture_events.dart';
|
||||
export 'src/mock_image.dart';
|
||||
|
||||
22
packages/flame_test/lib/src/fails_assert.dart
Normal file
22
packages/flame_test/lib/src/fails_assert.dart
Normal file
@ -0,0 +1,22 @@
|
||||
import 'package:test/test.dart';
|
||||
|
||||
/// Matcher that can be used in a test that expects an assertion error.
|
||||
///
|
||||
/// This is similar to standard `throwsAssertionError` matcher, but also
|
||||
/// allows an optional message string to verify that the assertion has the
|
||||
/// expected message.
|
||||
///
|
||||
/// For example:
|
||||
/// ```dart
|
||||
/// expect(
|
||||
/// () => PositionComponent(size: Vector2.all(-1)),
|
||||
/// failsAssert('size of a PositionComponent cannot be negative'),
|
||||
/// )
|
||||
/// ```
|
||||
Matcher failsAssert([String? message]) {
|
||||
var typeMatcher = isA<AssertionError>();
|
||||
if (message != null) {
|
||||
typeMatcher = typeMatcher.having((e) => e.message, 'message', message);
|
||||
}
|
||||
return throwsA(typeMatcher);
|
||||
}
|
||||
24
packages/flame_test/test/expect_fails_assert.dart
Normal file
24
packages/flame_test/test/expect_fails_assert.dart
Normal file
@ -0,0 +1,24 @@
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('failsAssert', () {
|
||||
test('without message', () {
|
||||
expect(
|
||||
() {
|
||||
assert(2 + 2 == 5);
|
||||
},
|
||||
failsAssert(),
|
||||
);
|
||||
});
|
||||
|
||||
test('with message', () {
|
||||
expect(
|
||||
() {
|
||||
assert(2 + 2 == 5, 'Basic arithmetic error');
|
||||
},
|
||||
failsAssert('Basic arithmetic error'),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user