mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
556 lines
15 KiB
Dart
556 lines
15 KiB
Dart
part of skysprites;
|
|
|
|
typedef void ActionCallback();
|
|
|
|
/// Actions are used to animate properties of nodes or any other type of
|
|
/// objects. The actions are powered by an [ActionController], typically
|
|
/// associated with a [Node]. The most commonly used action is the
|
|
/// [ActionTween] which interpolates a property between two values over time.
|
|
///
|
|
/// Actions can be nested in different ways; played in sequence using the
|
|
/// [ActionSequence], or looped using the [ActionRepeat].
|
|
///
|
|
/// You should typically not override this class directly, instead override
|
|
/// [ActionInterval] or [ActionInstant] if you need to create a new action
|
|
/// class.
|
|
abstract class Action {
|
|
Object _tag;
|
|
bool _finished = false;
|
|
bool _added = false;
|
|
|
|
/// Moves to the next time step in an action, [dt] is the delta time since
|
|
/// the last time step in seconds. Typically this method is called from the
|
|
/// [ActionController].
|
|
void step(double dt);
|
|
|
|
/// Sets the action to a specific point in time. The [t] value that is passed
|
|
/// in is a normalized value 0.0 to 1.0 of the duration of the action. Every
|
|
/// action will always recieve a callback with the end time point (1.0),
|
|
/// unless it is cancelled.
|
|
void update(double t) {
|
|
}
|
|
|
|
void _reset() {
|
|
_finished = false;
|
|
}
|
|
|
|
double get duration => 0.0;
|
|
}
|
|
|
|
/// The abstract class for an action that changes properties over a time
|
|
/// interval, optionally using an easing curve.
|
|
abstract class ActionInterval extends Action {
|
|
double _duration;
|
|
|
|
bool _firstTick = true;
|
|
double _elapsed = 0.0;
|
|
|
|
/// The duration, in seconds, of the action.
|
|
///
|
|
/// double myTime = myAction.duration;
|
|
double get duration => _duration;
|
|
|
|
/// The animation curve used to ease the animation.
|
|
///
|
|
/// myAction.curve = bounceOut;
|
|
Curve curve;
|
|
|
|
ActionInterval([this._duration = 0.0, this.curve]);
|
|
|
|
void step(double dt) {
|
|
if (_firstTick) {
|
|
_firstTick = false;
|
|
} else {
|
|
_elapsed += dt;
|
|
}
|
|
|
|
double t;
|
|
if (this._duration == 0.0) {
|
|
t = 1.0;
|
|
} else {
|
|
t = (_elapsed / _duration).clamp(0.0, 1.0);
|
|
}
|
|
|
|
if (curve == null) {
|
|
update(t);
|
|
} else {
|
|
update(curve.transform(t));
|
|
}
|
|
|
|
if (t >= 1.0) _finished = true;
|
|
}
|
|
}
|
|
|
|
/// An action that repeats an action a fixed number of times.
|
|
class ActionRepeat extends ActionInterval {
|
|
final int numRepeats;
|
|
final ActionInterval action;
|
|
int _lastFinishedRepeat = -1;
|
|
|
|
/// Creates a new action that is repeats the passed in action a fixed number
|
|
/// of times.
|
|
///
|
|
/// var myLoop = new ActionRepeat(myAction);
|
|
ActionRepeat(this.action, this.numRepeats) {
|
|
_duration = action.duration * numRepeats;
|
|
}
|
|
|
|
void update(double t) {
|
|
int currentRepeat = math.min((t * numRepeats.toDouble()).toInt(), numRepeats - 1);
|
|
for (int i = math.max(_lastFinishedRepeat, 0); i < currentRepeat; i++) {
|
|
if (!action._finished) action.update(1.0);
|
|
action._reset();
|
|
}
|
|
_lastFinishedRepeat = currentRepeat;
|
|
|
|
double ta = (t * numRepeats.toDouble()) % 1.0;
|
|
action.update(ta);
|
|
|
|
if (t >= 1.0) {
|
|
action.update(1.0);
|
|
action._finished = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// An action that repeats an action an indefinite number of times.
|
|
class ActionRepeatForever extends Action {
|
|
final ActionInterval action;
|
|
double _elapsedInAction = 0.0;
|
|
|
|
/// Creates a new action with the action that is passed in.
|
|
///
|
|
/// var myInifiniteLoop = new ActionRepeatForever(myAction);
|
|
ActionRepeatForever(this.action);
|
|
|
|
step(double dt) {
|
|
_elapsedInAction += dt;
|
|
while (_elapsedInAction > action.duration) {
|
|
_elapsedInAction -= action.duration;
|
|
if (!action._finished) action.update(1.0);
|
|
action._reset();
|
|
}
|
|
_elapsedInAction = math.max(_elapsedInAction, 0.0);
|
|
|
|
double t;
|
|
if (action._duration == 0.0) {
|
|
t = 1.0;
|
|
} else {
|
|
t = (_elapsedInAction / action._duration).clamp(0.0, 1.0);
|
|
}
|
|
|
|
action.update(t);
|
|
}
|
|
}
|
|
|
|
/// An action that plays a number of supplied actions in sequence. The duration
|
|
/// of the [ActionSequence] with be the sum of the durations of the actions
|
|
/// passed in to the constructor.
|
|
class ActionSequence extends ActionInterval {
|
|
Action _a;
|
|
Action _b;
|
|
double _split;
|
|
|
|
/// Creates a new action with the list of actions passed in.
|
|
///
|
|
/// var mySequence = new ActionSequence([myAction0, myAction1, myAction2]);
|
|
ActionSequence(List<Action> actions) {
|
|
assert(actions.length >= 2);
|
|
|
|
if (actions.length == 2) {
|
|
// Base case
|
|
_a = actions[0];
|
|
_b = actions[1];
|
|
} else {
|
|
_a = actions[0];
|
|
_b = new ActionSequence(actions.sublist(1));
|
|
}
|
|
|
|
// Calculate split and duration
|
|
_duration = _a.duration + _b.duration;
|
|
if (_duration > 0) {
|
|
_split = _a.duration / _duration;
|
|
} else {
|
|
_split = 1.0;
|
|
}
|
|
}
|
|
|
|
void update(double t) {
|
|
if (t < _split) {
|
|
// Play first action
|
|
double ta;
|
|
if (_split > 0.0) {
|
|
ta = (t / _split).clamp(0.0, 1.0);
|
|
} else {
|
|
ta = 1.0;
|
|
}
|
|
_updateWithCurve(_a, ta);
|
|
} else if (t >= 1.0) {
|
|
// Make sure everything is finished
|
|
if (!_a._finished) _finish(_a);
|
|
if (!_b._finished) _finish(_b);
|
|
} else {
|
|
// Play second action, but first make sure the first has finished
|
|
if (!_a._finished) _finish(_a);
|
|
double tb;
|
|
if (_split < 1.0) {
|
|
tb = (1.0 - (1.0 - t) / (1.0 - _split)).clamp(0.0, 1.0);
|
|
} else {
|
|
tb = 1.0;
|
|
}
|
|
_updateWithCurve(_b, tb);
|
|
}
|
|
}
|
|
|
|
void _updateWithCurve(Action action, double t) {
|
|
if (action is ActionInterval) {
|
|
ActionInterval actionInterval = action;
|
|
if (actionInterval.curve == null) {
|
|
action.update(t);
|
|
} else {
|
|
action.update(actionInterval.curve.transform(t));
|
|
}
|
|
} else {
|
|
action.update(t);
|
|
}
|
|
|
|
if (t >= 1.0) {
|
|
action._finished = true;
|
|
}
|
|
}
|
|
|
|
void _finish(Action action) {
|
|
action.update(1.0);
|
|
action._finished = true;
|
|
}
|
|
|
|
void _reset() {
|
|
super._reset();
|
|
_a._reset();
|
|
_b._reset();
|
|
}
|
|
}
|
|
|
|
/// An action that plays the supplied actions in parallell. The duration of the
|
|
/// [ActionGroup] will be the maximum of the durations of the actions used to
|
|
/// compose this action.
|
|
class ActionGroup extends ActionInterval {
|
|
List<Action> _actions;
|
|
|
|
/// Creates a new action with the list of actions passed in.
|
|
///
|
|
/// var myGroup = new ActionGroup([myAction0, myAction1, myAction2]);
|
|
ActionGroup(this._actions) {
|
|
for (Action action in _actions) {
|
|
if (action.duration > _duration) {
|
|
_duration = action.duration;
|
|
}
|
|
}
|
|
}
|
|
|
|
void update(double t) {
|
|
if (t >= 1.0) {
|
|
// Finish all unfinished actions
|
|
for (Action action in _actions) {
|
|
if (!action._finished) {
|
|
action.update(1.0);
|
|
action._finished = true;
|
|
}
|
|
}
|
|
} else {
|
|
for (Action action in _actions) {
|
|
if (action.duration == 0.0) {
|
|
// Fire all instant actions immediately
|
|
if (!action._finished) {
|
|
action.update(1.0);
|
|
action._finished = true;
|
|
}
|
|
} else {
|
|
// Update child actions
|
|
double ta = (t / (action.duration / duration)).clamp(0.0, 1.0);
|
|
if (ta < 1.0) {
|
|
if (action is ActionInterval) {
|
|
ActionInterval actionInterval = action;
|
|
if (actionInterval.curve == null) {
|
|
action.update(ta);
|
|
} else {
|
|
action.update(actionInterval.curve.transform(ta));
|
|
}
|
|
} else {
|
|
action.update(ta);
|
|
}
|
|
} else if (!action._finished){
|
|
action.update(1.0);
|
|
action._finished = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void _reset() {
|
|
for (Action action in _actions) {
|
|
action._reset();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// An action that doesn't perform any other task than taking time. This action
|
|
/// is typically used in a sequence to space out other events.
|
|
class ActionDelay extends ActionInterval {
|
|
/// Creates a new action with the specified [delay]
|
|
ActionDelay(double delay) : super(delay);
|
|
}
|
|
|
|
/// An action that doesn't have a duration. If this class is overridden to
|
|
/// create custom instant actions, only the [fire] method should be overriden.
|
|
abstract class ActionInstant extends Action {
|
|
|
|
void step(double dt) {
|
|
}
|
|
|
|
void update(double t) {
|
|
fire();
|
|
_finished = true;
|
|
}
|
|
|
|
void fire();
|
|
}
|
|
|
|
/// An action that calls a custom function when it is fired.
|
|
class ActionCallFunction extends ActionInstant {
|
|
ActionCallback _function;
|
|
|
|
/// Creates a new callback action with the supplied callback.
|
|
///
|
|
/// var myAction = new ActionCallFunction(() { print("Hello!";) });
|
|
ActionCallFunction(this._function);
|
|
|
|
void fire() {
|
|
_function();
|
|
}
|
|
}
|
|
|
|
/// An action that removes the supplied node from its parent when it's fired.
|
|
class ActionRemoveNode extends ActionInstant {
|
|
Node _node;
|
|
|
|
/// Creates a new action with the node to remove as its argument.
|
|
///
|
|
/// var myAction = new ActionRemoveNode(myNode);
|
|
ActionRemoveNode(this._node);
|
|
|
|
void fire() {
|
|
_node.removeFromParent();
|
|
}
|
|
}
|
|
|
|
/// An action that tweens a property between two values, optionally using an
|
|
/// animation curve. This is one of the most common building blocks when
|
|
/// creating actions. The tween class can be used to animate properties of the
|
|
/// type [Point], [Size], [Rect], [double], or [Color].
|
|
class ActionTween extends ActionInterval {
|
|
|
|
/// The setter method used to set the property being animated.
|
|
final Function setter;
|
|
|
|
/// The start value of the animation.
|
|
final startVal;
|
|
|
|
/// The end value of the animation.
|
|
final endVal;
|
|
|
|
var _delta;
|
|
|
|
/// Creates a new tween action. The [setter] will be called to update the
|
|
/// animated property from [startVal] to [endVal] over the [duration] time in
|
|
/// seconds. Optionally an animation [curve] can be passed in for easing the
|
|
/// animation.
|
|
///
|
|
/// // Animate myNode from its current position to 100.0, 100.0 during
|
|
/// // 1.0 second and a bounceOut easing
|
|
/// var myTween = new ActionTween(
|
|
/// (a) => myNode.position = a,
|
|
/// myNode.position,
|
|
/// new Point(100.0, 100.0,
|
|
/// 1.0,
|
|
/// bounceOut
|
|
/// );
|
|
/// myNode.actions.run(myTween);
|
|
ActionTween(this.setter, this.startVal, this.endVal, double duration, [Curve curve]) : super(duration, curve) {
|
|
_computeDelta();
|
|
}
|
|
|
|
void _computeDelta() {
|
|
if (startVal is Point) {
|
|
// Point
|
|
double xStart = startVal.x;
|
|
double yStart = startVal.y;
|
|
double xEnd = endVal.x;
|
|
double yEnd = endVal.y;
|
|
_delta = new Point(xEnd - xStart, yEnd - yStart);
|
|
} else if (startVal is Size) {
|
|
// Size
|
|
double wStart = startVal.width;
|
|
double hStart = startVal.height;
|
|
double wEnd = endVal.width;
|
|
double hEnd = endVal.height;
|
|
_delta = new Size(wEnd - wStart, hEnd - hStart);
|
|
} else if (startVal is Rect) {
|
|
// Rect
|
|
double lStart = startVal.left;
|
|
double tStart = startVal.top;
|
|
double rStart = startVal.right;
|
|
double bStart = startVal.bottom;
|
|
double lEnd = endVal.left;
|
|
double tEnd = endVal.top;
|
|
double rEnd = endVal.right;
|
|
double bEnd = endVal.bottom;
|
|
_delta = new Rect.fromLTRB(lEnd - lStart, tEnd - tStart, rEnd - rStart, bEnd - bStart);
|
|
} else if (startVal is double) {
|
|
// Double
|
|
_delta = endVal - startVal;
|
|
} else if (startVal is Color) {
|
|
// Color
|
|
int aDelta = endVal.alpha - startVal.alpha;
|
|
int rDelta = endVal.red - startVal.red;
|
|
int gDelta = endVal.green - startVal.green;
|
|
int bDelta = endVal.blue - startVal.blue;
|
|
_delta = new _ColorDiff(aDelta, rDelta, gDelta, bDelta);
|
|
} else {
|
|
assert(false);
|
|
}
|
|
}
|
|
|
|
void update(double t) {
|
|
var newVal;
|
|
|
|
if (startVal is Point) {
|
|
// Point
|
|
double xStart = startVal.x;
|
|
double yStart = startVal.y;
|
|
double xDelta = _delta.x;
|
|
double yDelta = _delta.y;
|
|
newVal = new Point(xStart + xDelta * t, yStart + yDelta * t);
|
|
} else if (startVal is Size) {
|
|
// Size
|
|
double wStart = startVal.width;
|
|
double hStart = startVal.height;
|
|
double wDelta = _delta.width;
|
|
double hDelta = _delta.height;
|
|
newVal = new Size(wStart + wDelta * t, hStart + hDelta * t);
|
|
} else if (startVal is Rect) {
|
|
// Rect
|
|
double lStart = startVal.left;
|
|
double tStart = startVal.top;
|
|
double rStart = startVal.right;
|
|
double bStart = startVal.bottom;
|
|
double lDelta = _delta.left;
|
|
double tDelta = _delta.top;
|
|
double rDelta = _delta.right;
|
|
double bDelta = _delta.bottom;
|
|
newVal = new Rect.fromLTRB(lStart + lDelta * t, tStart + tDelta * t, rStart + rDelta * t, bStart + bDelta * t);
|
|
} else if (startVal is double) {
|
|
// Doubles
|
|
newVal = startVal + _delta * t;
|
|
} else if (startVal is Color) {
|
|
// Colors
|
|
int aNew = (startVal.alpha + (_delta.alpha * t).toInt()).clamp(0, 255);
|
|
int rNew = (startVal.red + (_delta.red * t).toInt()).clamp(0, 255);
|
|
int gNew = (startVal.green + (_delta.green * t).toInt()).clamp(0, 255);
|
|
int bNew = (startVal.blue + (_delta.blue * t).toInt()).clamp(0, 255);
|
|
newVal = new Color.fromARGB(aNew, rNew, gNew, bNew);
|
|
} else {
|
|
// Oopses
|
|
assert(false);
|
|
}
|
|
|
|
setter(newVal);
|
|
}
|
|
}
|
|
|
|
/// A class the controls the playback of actions. To play back an action it is
|
|
/// passed to the [ActionController]'s [run] method. The [ActionController]
|
|
/// itself is typically a property of a [Node] and powered by the [SpriteBox].
|
|
class ActionController {
|
|
|
|
List<Action> _actions = [];
|
|
|
|
/// Creates a new [ActionController]. However, for most uses a reference to
|
|
/// an [ActionController] is acquired through the [Node.actions] property.
|
|
ActionController();
|
|
|
|
/// Runs an [action], can optionally be passed a [tag]. The [tag] can be used
|
|
/// to reference the action or a set of actions with the same tag.
|
|
///
|
|
/// myNode.actions.run(myAction, "myActionGroup");
|
|
void run(Action action, [Object tag]) {
|
|
assert(!action._added);
|
|
|
|
action._tag = tag;
|
|
action._added = true;
|
|
action.update(0.0);
|
|
_actions.add(action);
|
|
}
|
|
|
|
/// Stops an [action] and removes it from the controller.
|
|
///
|
|
/// myNode.actions.stop(myAction);
|
|
void stop(Action action) {
|
|
if (_actions.remove(action)) {
|
|
action._added = false;
|
|
action._reset();
|
|
}
|
|
}
|
|
|
|
void _stopAtIndex(int i) {
|
|
Action action = _actions[i];
|
|
action._added = false;
|
|
action._reset();
|
|
_actions.removeAt(i);
|
|
}
|
|
|
|
/// Stops all actions with the specified tag and removes them from the
|
|
/// controller.
|
|
///
|
|
/// myNode.actions.stopWithTag("myActionGroup");
|
|
void stopWithTag(Object tag) {
|
|
for (int i = _actions.length - 1; i >= 0; i--) {
|
|
Action action = _actions[i];
|
|
if (action._tag == tag) {
|
|
_stopAtIndex(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Stops all actions currently being run by the controller and removes them.
|
|
///
|
|
/// myNode.actions.stopAll();
|
|
void stopAll() {
|
|
for (int i = _actions.length - 1; i >= 0; i--) {
|
|
_stopAtIndex(i);
|
|
}
|
|
}
|
|
|
|
void step(double dt) {
|
|
for (int i = _actions.length - 1; i >= 0; i--) {
|
|
Action action = _actions[i];
|
|
action.step(dt);
|
|
|
|
if (action._finished) {
|
|
action._added = false;
|
|
_actions.removeAt(i);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class _ColorDiff {
|
|
final int alpha;
|
|
final int red;
|
|
final int green;
|
|
final int blue;
|
|
|
|
_ColorDiff(this.alpha, this.red, this.green, this.blue);
|
|
}
|