# Copyright 2012 United States Government, as represented by the Secretary of Defense, Under # Secretary of Defense (Personnel & Readiness). # # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except # in compliance with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software distributed under the License # is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express # or implied. See the License for the specific language governing permissions and limitations under # the License. ## `animation.vwf` adds standard animation functions to a node. ## ## Animations play over a given `animationDuration` and at a given `animationRate` with respect to ## simulation time. `animationTime` tracks time as the animation plays. Setting `animationTime` ## moves the animation. The animation plays once to the end, or with `animationLoop` repeats ## indefinitely. ## ## Events are sent to indicate `animationStarted`, `animationStopped` and `animationLooped`. Changes ## to the animation time are announced with `animationTimeChanged.` ## ## A node may provide an `animationUpdate` method to calculate its own animation state, or it may ## use a specialized behavior that implements a standard animation. ## ## @name animation.vwf ## @namespace # The duration must be a non-negative number, times are always in the range [ `0`, `duration` ], and # the rate may be any number (although there is little protection against rates very near zero). # # 0 <= animationDuration # 0 <= animationTime <= animationDuration # -Infinity < animationRate < Infinity # # The animation play state and time index are maintained in `animationStartSIM` and # `animationPauseSIM`. `animationPauseSIM` is `null` while the animation is playing. The # distance from `animationStartSIM` to the current time (when playing) or `animationPauseSIM` # (when paused) gives the time index, after accounting for the play rate. # # animationTime = ( ( animationPauseSIM || time ) - ( animationStartSIM || time ) ) * # animationRate (positive rates) # animationTime = ( ( animationPauseSIM || time ) - ( animationStartSIM || time ) ) * # animationRate + animationDuration (negative rates) # # animationPlaying = this.animationStartSIM != null && this.animationPauseSIM == null # # `animationStartSIM` and `animationPauseSIM` are `null` before the animation is first played. # This state is interpreted as `animationPlaying` == `false` and `animationTime` == `0`. # # animationStartSIM == null => animationPauseSIM == null # animationStartSIM == null => animationStopSIM == null # # animationStartSIM != null => animationStartSIM <= time # animationPauseSIM != null => animationStartSIM <= animationPauseSIM <= time # animationStopSIM != null => animationStartSIM + animationDurationSIM == animationStopSIM # # animationDurationSIM == animationDuration / animationRate (positive rates) # animationDurationSIM == -animationDuration / animationRate (negative rates) # # Properties named with a`SIM` suffix refer to times or durations on the simulation timeline. All # other time-related properties refer to the animation timeline. --- properties: ## The current animation position in seconds. ## ## `animationTime` automatically moves with the simulation time while the animation is playing, ## tracking the change in time multiplied by `animationRate`. A negative `animationRate` will ## cause `animationTime` to move backwards. Setting `animationTime` updates the postion whether or ## not the animation is currently playing. animationTime: set: | if(this.animationStartTime == null) { this.animationStartTime = 0; } if(this.animationStopTime == null) { this.animationStopTime = this.animationDuration; } // Save copies to avoid repeated reads. var duration = this.animationStopTime - this.animationStartTime, rate = this.animationRate; // Range limit the incoming value. value = Math.min( Math.max( this.animationStartTime, value ), this.animationStopTime ); // Keep paused if updating start/pause from null/null. Use t=0 instead of `this.time` so that // setting `node.animationTime` during initialization is consistent across multiple clients. if ( this.animationStartSIM == null ) { this.animationPauseSIM = 0; } // Calculate the start and stop times that makes the new time work. this.animationStartSIM = ( this.animationPauseSIM != null ? this.animationPauseSIM : this.time ) - ( rate >= 0 ? value - this.animationStartTime : value - duration ) / rate; this.animationStopSIM = this.animationStartSIM + ( rate >= 0 ? duration : -duration ) / rate; // Update the node and fire the changed event. if ( value !== this.animationTimeUpdated ) { this.animationTimeUpdated = value; this.animationUpdate( value, this.animationDuration ); this.animationTimeChanged( value ); } //@ sourceURL=animation.animationTime.set.vwf get: | // Save copies to avoid repeated reads. var startTime = this.animationStartTime; var stopTime = this.animationStopTime; var rate = this.animationRate; var animationPauseSIM = this.animationPauseSIM; var animationStartSIM = this.animationStartSIM; var time = this.time; // Calculate the time from the start and current/pause times. var value = ( ( animationPauseSIM != null ? animationPauseSIM : time ) - ( animationStartSIM != null ? animationStartSIM : time ) ) * rate + ( rate >= 0 ? startTime : stopTime ); // Range limit the value. value = Math.min( Math.max( startTime, value ), stopTime ); // If changed since last seen, update and fire the changed event. if ( value !== this.animationTimeUpdated ) { this.animationTimeUpdated = value; this.animationUpdate( value, this.animationDuration ); this.animationTimeChanged( value ); } return value; //@ sourceURL=animation.animationTime.get.vwf ## The length of the animation in seconds. ## ## `animationTime` will always be within the range [ `0`, `animationDuration` ]. Animations ## automatically stop playing once `animationTime` reaches `animationDuration` unless ## `animationLoop` is set. Animations with a negative `animationRate` start at ## `animationDuration` and stop at `0`. animationDuration: # TODO: allow changes while playing set: | var duration = value, rate = this.animationRate; this.animationDuration = duration; this.animationDurationSIM = ( rate >= 0 ? duration : -duration ) / rate; value: 1 ## The animation playback rate. ## ## Set `animationRate` to a value greater than `1` to increase the rate. Set a value between `0` ## and `1` to decrease it. Negative rates will cause the animation to play backwards. animationRate: # TODO: allow changes while playing set: | var duration = this.animationDuration, rate = value; this.animationRate = rate; this.animationDurationSIM = ( rate >= 0 ? duration : -duration ) / rate; value: 1 ## Automatically replay the animation from the start after reaching the end. animationLoop: false ## The animation's play/pause control. animationPlaying: set: | if(this.animationStartTime == null) { this.animationStartTime = 0; } if(this.animationStopTime == null) { this.animationStopTime = this.animationDuration; } if ( this.animationStartSIM != null && this.animationPauseSIM == null ) { if ( ! value ) { // Mark as paused at the current time. this.animationPauseSIM = this.time; // Send the `animationStopped` event if stopping at the end. if ( this.time == this.animationStopSIM ) { this.animationStopped(); } } } else { if ( value ) { // Save copies to avoid repeated reads. var duration = this.animationStopTime - this.animationStartTime, rate = this.animationRate; // Start from the beginning if resuming from the end. if ( this.animationPauseSIM == this.animationStopSIM ) { this.animationPauseSIM = this.animationStartSIM; } // Recalculate the start and stop times to keep paused time unchanged, then resume. this.animationStartSIM = ( this.animationStartSIM != null ? this.animationStartSIM : this.time ) - ( this.animationPauseSIM != null ? this.animationPauseSIM : this.time ) + this.time; this.animationStopSIM = this.animationStartSIM + ( rate >= 0 ? duration : -duration ) / rate; this.animationPauseSIM = null; // Send the `animationStarted` event if starting from the beginning. if ( this.time == this.animationStartSIM ) { this.animationStarted(); } // Start the animation worker. this.logger.debug( "scheduling animationTick" ); this.animationTick(); } } //@ sourceURL=animation.animationPlaying.set.vwf get: | return this.animationStartSIM != null && this.animationPauseSIM == null; ## The most recent value of `animationTime` recognized by this behavior. ## ## Each `animationTime` change causes `animationUpdate` to be called. `animationTimeUpdated` ## records the value of `animationTime` from the last time this occurred. ## ## `animationTimeUpdated` is for internal use. Do not set this property. animationTimeUpdated: null ## The simulation time corresponding to the start of the animation. ## ## `animationStartSIM` is `null` until the animation is first played. `animationPlaying` is ## `false` and `animationTime` is `0` while `animationStartSIM` is `null`. ## ## `animationStartSIM` is for internal use. Do not set this property. animationStartSIM: null ## The simulation time corresponding to the animation's pause position. ## ## `animationPauseSIM` is `null` while the animation is playing and before the animation is ## first played. ## ## `animationPauseSIM` is for internal use. Do not set this property. animationPauseSIM: null ## The simulation time corresponding to the end of the animation. The animation worker function ## loops or stops the animation when `time` reaches this value. ## ## `animationStopSIM` is for internal use. Do not set this property. animationStopSIM: null ## The animation's duration in simulation time, after considering `animationRate`. ## ## `animationDurationSIM` is for internal use. Do not set this property. animationDurationSIM: null ## The time that the animation should start at. Used with animationStopTime to play ## a subsection of the animation. Defaults to 0 when the animation starts to play ## if it is not assigned another value. 'animationStartTime' will always be within ## the range [ `0`, `animationDuration` ] animationStartTime: set: | this.animationStartTime = value ? Math.min( Math.max( 0, value ), this.animationDuration ) : value; value: null ## The time that the animation should stop at. Used with animationStartTime to play ## a subsection of the animation. Defaults to 'animationDuration' when the animation ## starts to play if it is not assigned another value. 'animationStopTime' will always ## be within the range [ `0`, `animationDuration` ] animationStopTime: set: | this.animationStopTime = value ? Math.min( Math.max( 0, value ), this.animationDuration ) : value; value: null ## The frame that the animation should start at. Setting this value automatically updates the "animationStartTime" ## to the "animationStartFrame" / "fps" animationStartFrame: set: | this.animationStartTime = value / this.animationFPS; get: | return Math.floor(this.animationStartTime * this.animationFPS); ## The frame that the animation should stop at. Setting this value automatically updates the "animationStopTime" ## to the "animationStopFrame" / "fps" animationStopFrame: set: | this.animationStopTime = value / this.animationFPS; get: | return Math.floor(this.animationStopTime * this.animationFPS); ## The frames per second of the animation loaded from the source model. animationFPS: 30 ## The number of frames of the animation loaded from the source model. animationFrames: set: | this.animationDuration = value / this.animationFPS; get: | return Math.ceil(this.animationFPS * this.animationDuration); ## The current frame the animation is on. Equivalent to animationTime. animationFrame: set: | if(this.animationFPS) { this.animationTime = value / this.animationFPS; } get: | if(this.animationFPS) { return Math.floor(this.animationTime * this.animationFPS); } ## The animation update rate in ticks per second of simulation time. animationTPS: 60 methods: # Play the animation from the start. animationPlay: parameters: - startTime - stopTime body: | if(!isNaN(stopTime)) { this.animationStopTime = stopTime; } if(!isNaN(startTime)) { this.animationStartTime = startTime; } this.animationPlaying = true; # Pause the animation at the current position. animationPause: | this.animationPlaying = false; # Resume the animation from the current position. animationResume: | this.animationPlaying = true; # Stop the animation and reset the position to the start. animationStop: | this.animationPlaying = false; this.animationTime = 0; ## Update the animation. If `animationTime` has reached the end, stop or loop depending on the ## `animationLoop` setting. Schedule the next tick if the animation did not stop. ## ## `animationTick` is for internal use. Do not call this method directly. animationTick: | if ( this.animationPlaying ) { // Read the time to recognize the current time and update. // TODO: move side effects out of the getter!!! (says Kevin) this.animationTime; // Loop or stop after reaching the end. if ( this.time === this.animationStopSIM ) { if ( this.animationLoop ) { this.animationLooped(); this.animationTime = this.animationRate >= 0 ? this.animationStartTime : this.animationStopTime; } else { this.animationPlaying = false; } } // Schedule the next tick if still playing. if ( this.animationPlaying ) { if ( this.animationStopSIM - this.time > 1 / this.animationTPS ) { this.in( 1 / this.animationTPS ).animationTick(); // next interval } else { // TODO: When animationStopSIM is 0 (usually when a model does not actually have an // animation on it), we schedule a method call for a time in the past (at time 0). // That immediately calls animationTick again, but this.time does not equal // animationStopSIM as we would expect. So, it doesn't stop the animation and we get // caught in an infinite loop. // Ideally we should catch the case where animationStopSIM is 0 before this point. // But for now, we catch it here. if ( this.animationStopSIM > 0 ) { this.at( this.animationStopSIM ).animationTick(); // exactly at end } else { this.animationPlaying = false; } } } else { this.logger.debug( "canceling animationTick" ); } } //@ sourceURL=animation.animationTick.vwf ## `animation.vwf` calls `animationUpdate` each time `animationTime` changes. Nodes that ## implement `animation.vwf` should provide an `animationUpdate` method to calculate the animation ## state appropriate for the node. ## ## Since this event is sent within the `animationTime` getter function, references to ## `this.animationTime` will return `undefined` due to reentrancy protections. Use the provided ## `time` parameter instead. ## ## @param {Number} time ## The animation's `animationTime`. ## @param {Number} duration ## The animation's `animationDuration`. animationUpdate: parameters: - time - duration events: # Sent when the animation starts playing from the beginning. animationStarted: # Sent when the animation reaches the end and `animationLoop` is not set. animationStopped: # Sent when the animation reaches the end and `animationLoop` is set. animationLooped: ## Sent each time `animationTime` changes. ## ## Since this event is sent within the `animationTime` getter function, references to ## `this.animationTime` will return `undefined` due to reentrancy protections. Use the provided ## `time` parameter instead. ## ## @param {Number} time ## The animation's `animationTime`. animationTimeChanged: parameters: - time scripts: - | this.initialize = function() { // Locate child nodes that extend or implement "http://vwf.example.com/animation/position.vwf" // to identify themselves as animation key positions. var positions = this.find( "./element(*,'http://vwf.example.com/animation/position.vwf')" ); // Fill in missing `animationTime` properties, distributing evenly between the left and right // positions that define `animationTime`. // 1: [ - ] => [ 0 ] // 1: [ 0, - ] => [ 0, 1 ] // 1: [ -, 1 ] => [ 0, 1 ] // 1: [ 0, -, - ] => [ 0, 1/2, 1 ] // 1: [ -, -, 1 ] => [ 0, 1/2, 1 ] // 1: [ 0, - , -, 1 ] => [ 0, 1/3 , 2/3, 1 ] var leftTime, leftIndex; var rightTime, rightIndex = -Infinity; if ( positions.length > 0 ) { positions.sort(function(a, b) { return a.sequence - b.sequence; }); if ( positions[0].animationTime === null ) { positions[0].animationTime = 0; } if ( positions[positions.length-1].animationTime === null ) { positions[positions.length-1].animationTime = this.animationDuration; } positions.forEach( function( position, index ) { if ( position.animationTime !== null ) { leftTime = position.animationTime; leftIndex = index; } else { if ( index > rightIndex ) { for ( rightIndex = index + 1; rightIndex < positions.length; rightIndex++ ) { if ( ( rightTime = /* assignment! */ positions[rightIndex].animationTime ) !== null ) { break; } } } position.animationTime = leftTime + ( rightTime - leftTime ) * ( index - leftIndex ) / ( rightIndex - leftIndex ); } }, this ); } } //@ sourceURL=http://vwf.example.com/animation.vwf/scripts~initialize