part of flutter_sprites; 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; } typedef void SetterCallback(dynamic value); /// The abstract class for an action that changes properties over a time /// interval, optionally using an easing curve. abstract class ActionInterval extends Action { ActionInterval([this._duration = 0.0, this.curve]); /// The duration, in seconds, of the action. /// /// double myTime = myAction.duration; double get duration => _duration; double _duration; /// The animation curve used to ease the animation. /// /// myAction.curve = bounceOut; Curve curve; bool _firstTick = true; double _elapsed = 0.0; 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); void 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 { /// 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(); } /// The setter method used to set the property being animated. final SetterCallback setter; /// The start value of the animation. final dynamic startVal; /// The end value of the animation. final dynamic endVal; dynamic _delta; 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 = <Action>[]; /// 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); }