Files
Alternativa3D/src/alternativa/engine3d/animation/AnimationClip.as

490 lines
14 KiB
ActionScript

/**
* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
* If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice.
* You may add additional accurate notices of copyright ownership.
*
* It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/
* */
package alternativa.engine3d.animation {
import alternativa.engine3d.alternativa3d;
import alternativa.engine3d.animation.keys.Track;
import alternativa.engine3d.core.Object3D;
use namespace alternativa3d;
/**
*
* Plays complex animation which consists of a set of animation tracks.
* Every animated property of every model's element presented by separated track.
* Track is somewhat similar to separate animated layer in Flash, but the layer stores animation of all properties for some element at once.
* In opposite, track stores animation for every property (for an example, separate track for a scale and separate track for a coordinates).
* Track also can contain keyframes in arbitrary positions. Frames, contained between keyframes, are linearly interpolated
* (i.e. behave themselves like a timeline frames in flash, for which motion twin was created ).
* Animation clip connects each track with a specific object.
* Animation clip stores information about animation for the whole model, i.e. for any element state at any time moment.
* Animation works handled by <code>AnimationController</code>.
*
* @see alternativa.engine3d.animation.keys.Track
* @see alternativa.engine3d.animation.keys.TransformTrack
* @see alternativa.engine3d.animation.keys.NumberTrack
* @see alternativa.engine3d.animation.AnimationController
*/
public class AnimationClip extends AnimationNode {
/**
* @private
*/
alternativa3d var _objects:Array;
/**
* Name of the animation clip.
*/
public var name:String;
/**
* Defines if animation should be repeated.
*/
public var loop:Boolean = true;
/**
* Length of animation in seconds. If length of any animation track is changed, updateLength()
* method should be called to recalculate the length of the clip.
*
* @see #updateLength()
*/
public var length:Number = 0;
/**
* Handles the active animation execution. Plays animation if value is true.
*
* @see AnimationNode#isActive
*/
public var animated:Boolean = true;
/**
* @private
* Current value of time.
*/
private var _time:Number = 0;
/**
* @private
*/
private var _numTracks:int = 0;
/**
* @private
*/
private var _tracks:Vector.<Track> = new Vector.<Track>();
/**
* @private
*/
private var _notifiersList:AnimationNotify;
/**
* Creates a AnimationClip object.
*
* @param name name of the clip
*/
public function AnimationClip(name:String = null) {
this.name = name;
}
/**
* The list of animated objects. Animation tracks are bound to the objects by object names.
*
* @see Track#object
*/
public function get objects():Array {
return (_objects == null) ? null : [].concat(_objects);
}
/**
* @private
*/
public function set objects(value:Array):void {
updateObjects(_objects, controller, value, controller);
_objects = (value == null) ? null : [].concat(value);
}
/**
* @private
*/
override alternativa3d function setController(value:AnimationController):void {
updateObjects(_objects, controller, _objects, value);
this.controller = value;
}
/**
* @private
*/
private function addObject(object:Object):void {
if (_objects == null) {
_objects = [object];
} else {
_objects.push(object);
}
if (controller != null) {
controller.addObject(object);
}
}
/**
* @private
*/
private function updateObjects(oldObjects:Array, oldController:AnimationController, newObjects:Array, newController:AnimationController):void {
var i:int, count:int;
if (oldController != null && oldObjects != null) {
for (i = 0, count = _objects.length; i < count; i++) {
oldController.removeObject(oldObjects[i]);
}
}
if (newController != null && newObjects != null) {
for (i = 0, count = newObjects.length; i < count; i++) {
newController.addObject(newObjects[i]);
}
}
}
/**
* Updates the length of the clip in order to match with length of longest track.
* Should be called after track was changed.
*/
public function updateLength():void {
for (var i:int = 0; i < _numTracks; i++) {
var track:Track = _tracks[i];
var len:Number = track.length;
if (len > length) {
length = len;
}
}
}
/**
* Adds a new track to the animation clip.
* The total length of the clip is recalculated automatically.
*
* @param track track which should be added.
* @return added track.
*
* @see #length
*/
public function addTrack(track:Track):Track {
if (track == null) {
throw new Error("Track can not be null");
}
_tracks[_numTracks++] = track;
if (track.length > length) {
length = track.length;
}
return track;
}
/**
* Removes the specified track from the clip. The clip length is automatically recalculated.
*
* @param track track which should be removed.
* @return removed track.
*
* @see #length
* @throw Error if the AnimationClip does not include the track.
*/
public function removeTrack(track:Track):Track {
var index:int = _tracks.indexOf(track);
if (index < 0) throw new ArgumentError("Track not found");
_numTracks--;
var j:int = index + 1;
while (index < _numTracks) {
_tracks[index] = _tracks[j];
index++;
j++;
}
_tracks.length = _numTracks;
length = 0;
for (var i:int = 0; i < _numTracks; i++) {
var t:Track = _tracks[i];
if (t.length > length) {
length = t.length;
}
}
return track;
}
/**
* Returns the track object instance that exists at the specified index.
*
* @param index index.
* @return the track object instance that exists at the specified index.
*/
public function getTrackAt(index:int):Track {
return _tracks[index];
}
/**
* Number of tracks in the AnimationClip.
*/
public function get numTracks():int {
return _numTracks;
}
/**
* @private
*/
override alternativa3d function update(interval:Number, weight:Number):void {
var oldTime:Number = _time;
if (animated) {
_time += interval*speed;
if (loop) {
if (_time < 0) {
// TODO: Loop processing
// _position = (length <= 0) ? 0 : _position % length;
_time = 0;
} else {
if (_time >= length) {
collectNotifiers(oldTime, length);
_time = (length <= 0) ? 0 : _time % length;
collectNotifiers(0, _time < oldTime ? _time : oldTime);
} else {
collectNotifiers(oldTime, _time);
}
}
} else {
if (_time < 0) {
_time = 0;
} else if (_time >= length) {
_time = length;
}
collectNotifiers(oldTime, _time);
}
}
if (weight > 0) {
for (var i:int = 0; i < _numTracks; i++) {
var track:Track = _tracks[i];
if (track.object != null) {
var state:AnimationState = controller.getState(track.object);
if (state != null) {
track.blend(_time, weight, state);
}
}
}
}
}
/**
* Current time of animation.
*/
public function get time():Number {
return _time;
}
/**
* @private
*/
public function set time(value:Number):void {
_time = value;
}
/**
* Current normalized time in the interval [0, 1].
*/
public function get normalizedTime():Number {
return (length == 0) ? 0 : _time/length;
}
/**
* @private
*/
public function set normalizedTime(value:Number):void {
_time = value*length;
}
/**
* @private
*/
private function getNumChildren(object:Object):int {
if (object is Object3D) {
return Object3D(object).numChildren;
}
return 0;
}
/**
* @private
*/
private function getChildAt(object:Object, index:int):Object {
if (object is Object3D) {
return Object3D(object).getChildAt(index);
}
return null;
}
/**
* @private
*/
private function addChildren(object:Object):void {
for (var i:int = 0, numChildren:int = getNumChildren(object); i < numChildren; i++) {
var child:Object = getChildAt(object, i);
addObject(child);
addChildren(child);
}
}
/**
* Binds tracks from the animation clip to given object. Only those tracks which have object property equal to the object's name are bound.
*
* @param object The object to which tracks are bound.
* @param includeDescendants If true, the whole tree of the object's children (if any) is processed.
*
* @see #objects
* @see alternativa.engine3d.animation.keys.Track#object
*/
public function attach(object:Object, includeDescendants:Boolean):void {
updateObjects(_objects, controller, null, controller);
_objects = null;
addObject(object);
if (includeDescendants) {
addChildren(object);
}
}
/**
* @private
*/
alternativa3d function collectNotifiers(start:Number, end:Number):void {
for (var notify:AnimationNotify = _notifiersList; notify != null; notify = notify.next) {
if (notify._time > start && notify._time <= end) {
// add notify to dispatched
notify.processNext = controller.nearestNotifyers;
controller.nearestNotifyers = notify;
}
}
}
/**
* Creates an AnimationNotify instance which is capable of firing notification events when playback reaches the specified time on the time line.
*
* @param time The time in seconds to which the notification trigger will be bound.
* @param name The name of AnimationNotify instance.
*
* @return A new instance of AnimationNotify class bound to specified time counting from start of the time line.
*
* @see AnimationNotify
*/
public function addNotify(time:Number, name:String = null):AnimationNotify {
time = (time <= 0) ? 0 : ((time >= length) ? length : time);
var notify:AnimationNotify = new AnimationNotify(name);
notify._time = time;
if (_notifiersList == null) {
_notifiersList = notify;
return notify;
} else {
if (_notifiersList._time > time) {
// Replaces the first key
notify.next = _notifiersList;
_notifiersList = notify;
return notify;
} else {
// Search for appropriate place
var n:AnimationNotify = _notifiersList;
while (n.next != null && n.next._time <= time) {
n = n.next;
}
if (n.next == null) {
// Places at the end
n.next = notify;
} else {
notify.next = n.next;
n.next = notify;
}
}
}
return notify;
}
/**
* Creates an AnimationNotify instance which is capable of firing notification events when playback reaches
* the specified time on the time line. The time is specified as an offset from the end of time line towards its start.
*
* @param offsetFromEnd The offset in seconds from the end of the time line towards its start, where the event object will be set in.
* @param name The name of notification trigger.
*
* @return A new instance of AnimationNotify class bound to specified time.
*
* @see AnimationNotify
*/
// TODO: name of method (addNotifyAtEnd) is incomprehensible. Rename to addNotifyFromFinish (or something else).
public function addNotifyAtEnd(offsetFromEnd:Number = 0, name:String = null):AnimationNotify {
return addNotify(length - offsetFromEnd, name);
}
/**
* Removes specified notification trigger.
*
* @param notify The notification trigger to remove.
* @return The removed notification trigger.
*/
public function removeNotify(notify:AnimationNotify):AnimationNotify {
if (_notifiersList != null) {
if (_notifiersList == notify) {
_notifiersList = _notifiersList.next;
return notify;
}
var n:AnimationNotify = _notifiersList;
while (n.next != null && n.next != notify) {
n = n.next;
}
if (n.next == notify) {
// removes
n.next = notify.next;
return notify;
}
}
throw new Error("Notify not found");
}
/**
* The list of notification triggers.
*/
public function get notifiers():Vector.<AnimationNotify> {
var result:Vector.<AnimationNotify> = new Vector.<AnimationNotify>();
var i:int = 0;
for (var notify:AnimationNotify = _notifiersList; notify != null; notify = notify.next) {
result[i] = notify;
i++;
}
return result;
}
/**
* Returns a fragment of the clip between specified bounds.
*
* @param start The start time of a fragment in seconds.
* @param end The end time of a fragment in seconds.
* @return The clip fragment.
*/
public function slice(start:Number, end:Number = Number.MAX_VALUE):AnimationClip {
var sliced:AnimationClip = new AnimationClip(name);
sliced._objects = (_objects == null) ? null : [].concat(_objects);
for (var i:int = 0; i < _numTracks; i++) {
sliced.addTrack(_tracks[i].slice(start, end));
}
return sliced;
}
/**
* Clones the clip. Both the clone and the original reference the same tracks.
*/
public function clone():AnimationClip {
var cloned:AnimationClip = new AnimationClip(name);
cloned._objects = (_objects == null) ? null : [].concat(_objects);
for (var i:int = 0; i < _numTracks; i++) {
cloned.addTrack(_tracks[i]);
}
cloned.length = length;
return cloned;
}
}
}