Actuate: Update tweens on 'ENTER_FRAME' instead of time passed?

Hi,

The problem i have, is that Actuate seems to update its tweening progress by the time past since the tween has started, where I would need it to update by frame (so that a tween which takes 1s at 60fps will have 60 steps of updating, regardless how much time has past in between the frames).

Is there some way to achieve that?

SIDE NOTE:
If you want some context, I wrote a screencasting solution to export visual material (gif, flv, png) of an app that is running inside Flash player. Unfortunatly, as you can imagine, that can cause some overhead and the framerate may drop, causing jumpy animations in the recorded material.

Thanks for helping me out

Perhaps you could make a custom actuator that does not use the standard frame timing, and set the Actuate.defaultActuator to your custom class?

You might also try dabbling around, and seeing what kind of changes you come up with, the timing is done in the SimpleActuator class

Alright, Iā€™ll give it a try.
Thank you!

Let me know how it goes :slight_smile:

Hello again,

Iā€™ve mentioned that simply pause and resume of all Actuators fixed my problem with actual gameplay recording, because buffering frames did not cause as much overhead that the framerate would suffer (and luckily the back buffer read back on my graphics card didā€™nt either) - only when rendering the frame buffer to file I had to pause ongoing tweens.

But now that I needed to capture some animated stuff, created with Flash IDE, using software rendering with lots of filters, there was a real need for an Actuator that updates tweens frame by frame.

Itā€™s a modified copy of SimpleActuator (AS3) and in case that someone would want it, I post it here (hope thatā€™s ok). The modified sections of the code are marked by

ā€˜// FrameByFrame mod.ā€™

@singmajesty: Iā€™m not shure how to handle the author tag in this case - please let me know if it should be done differently.

package com.eclecticdesignstudio.motion.actuators {
	
	
	import com.eclecticdesignstudio.motion.Actuate;
	import flash.display.DisplayObject;
	import flash.display.Shape;
	import flash.events.Event;
	import flash.utils.Dictionary;
	import flash.utils.getTimer;
	
	use namespace MotionInternal;
	
	
	/**
	 * @author Joshua Granick (Luke's Mod)
	 * @version 1.2
	 */
	public class FrameByFrameActuator extends GenericActuator {
		
		
		MotionInternal var timeOffset:Number;
		
		protected static var actuators:Array = new Array ();
		protected static var actuatorsLength:uint = 0;
		protected static var shape:Shape;
		
		protected var active:Boolean = true;
		protected var cacheVisible:Boolean;
		protected var detailsLength:uint;
		protected var initialized:Boolean;
		protected var paused:Boolean;
		protected var pauseTime:Number;
		protected var propertyDetails:Array = new Array ();
		protected var sendChange:Boolean = false;
		protected var setVisible:Boolean;
		protected var startTime:Number = getTimer () / 1000;
		protected var toggleVisible:Boolean;
		
		// Luke's Mod.
		protected var numFrames:uint;
		protected var currentFrame:uint;
		protected var frameTime:Number;
		public static var frameRate:uint = 0;
		
		
		public function FrameByFrameActuator (target:Object, duration:Number, properties:Object) {
			
			super (target, duration, properties);
			
			// Luke's Mod.
			if (frameRate == 0)
				throw new Error ("Set FrameByFrameActuator.frameRate first.");
			else 
			{
				frameTime = 1 / frameRate;
				numFrames = duration / frameTime;
				currentFrame = 0;
			}
			
			if (!shape) {
				
				shape = new Shape ();
				shape.addEventListener (Event.ENTER_FRAME, shape_onEnterFrame);
			}
		}
		
		// Luke's Mod.
		public static function pauseAll():void
		{
			for (var i:int = 0; i < actuators.length; i++) 
			{
				FrameByFrameActuator (actuators[i]).pause ();
			}
		}
		public static function resumeAll():void
		{
			for (var i:int = 0; i < actuators.length; i++) 
			{
				FrameByFrameActuator (actuators[i]).resume ();
			}
		}
		
		/**
		 * @inheritDoc
		 */
		public override function autoVisible (value:Boolean = true):GenericActuator {
			
			MotionInternal::autoVisible = value;
			
			if (!value) {
				
				toggleVisible = false;
				
				if (setVisible) {
					
					target.visible = cacheVisible;
					
				}
				
			}
			
			return this;
			
		}
		
		
		/**
		 * @inheritDoc
		 */
		public override function delay (duration:Number):GenericActuator {
			
			// Original code.
			/*
			MotionInternal::delay = duration;
			timeOffset = startTime + duration;
			*/
			
			// Luke's Mod.
			pause ();
			
			new TimerByPass (duration, resume);
			
			return this;
		}
		
		
		protected function initialize ():void {
			
			var details:PropertyDetails;
			var start:Number;
			
			for (var propertyName:String in properties) {
				
				start = Number (target[propertyName]);
				details = new PropertyDetails (target, propertyName, start, Number (properties[propertyName] - start));
				propertyDetails.push (details);
				
			}
			
			detailsLength = propertyDetails.length;
			initialized = true;
			
		}
		
		
		MotionInternal override function move ():void {
			
			toggleVisible = ("alpha" in properties && target is DisplayObject);
			
			if (toggleVisible && !target.visible && properties.alpha != 0) {
				
				setVisible = true;
				cacheVisible = target.visible;
				target.visible = true;
				
			}
			
			timeOffset = startTime;
			actuators.push (this);
			++actuatorsLength;
			
		}
		
		
		/**
		 * @inheritDoc
		 */
		public override function onUpdate (handler:Function, ... parameters:Array):GenericActuator {
			
			MotionInternal::onUpdate = handler;
			MotionInternal::onUpdateParams = parameters;
			sendChange = true;
			
			return this;
			
		}
		
		
		MotionInternal override function pause ():void {
			
			paused = true;
			pauseTime = getTimer ();
			
		}
		
		
		MotionInternal override function resume ():void {
			
			if (paused) {
				
				paused = false;
				timeOffset += (getTimer () - pauseTime) / 1000;
				
			}
			
		}
		
		
		MotionInternal override function stop (properties:Object, complete:Boolean, sendEvent:Boolean):void {
			
			if (active) {
				
				for (var propertyName:String in properties) {
					
					if (propertyName in this.properties) {
						
						active = false;
						
						if (complete) {
							
							apply ();
							
						}
						
						this.complete (sendEvent);
						return;
						
					}
					
				}
				
				if (!properties) {
					
					active = false;
					
					if (complete) {
						
						apply ();
						
					}
					
					this.complete (sendEvent);
					return;
					
				}
				
			}
			
		}
		
		
		MotionInternal function update (currentTime:Number):void {
			
			if (!paused) {
				
				var details:PropertyDetails;
				var easing:Number;
				var i:uint;
				
				// Luke's Mod.
				currentFrame ++;
				var tweenPosition:Number = currentFrame * frameTime / duration;
				
				if (tweenPosition > 1) {
					
					tweenPosition = 1;
				}
				
				if (!initialized) {
					
					initialize ();
					
				}
				
				if (!MotionInternal::special) {
					
					easing = MotionInternal::ease.calculate (tweenPosition);
					
					for (i = 0; i < detailsLength; ++i) {
						
						details = propertyDetails[i];
						details.target[details.propertyName] = details.start + (details.change * easing);
						
					}
					
				} else {
					
					if (!MotionInternal::reverse) {
						
						easing = MotionInternal::ease.calculate (tweenPosition);
						
					} else {
						
						easing = MotionInternal::ease.calculate (1 - tweenPosition);
						
					}
					
					var endValue:Number;
					
					for (i = 0; i < detailsLength; ++i) {
						
						details = propertyDetails[i];
						
						if (MotionInternal::smartRotation && (details.propertyName == "rotation" || details.propertyName == "rotationX" || details.propertyName == "rotationY" || details.propertyName == "rotationZ")) {
							
							var rotation:Number = details.change % 360;
							
							if (rotation > 180) {
								
								rotation -= 360;
								
							} else if (rotation < -180) {
								
								rotation += 360;
								
							}
							
							endValue = details.start + rotation * easing;
							
						} else {
							
							endValue = details.start + (details.change * easing);
							
						}
						
						if (!MotionInternal::snapping) {
							
							details.target[details.propertyName] = endValue;
							
						} else {
							
							details.target[details.propertyName] = Math.round (endValue);
							
						}
						
					}
					
				}
				
				if (tweenPosition === 1) {
					
					if (MotionInternal::repeat === 0) {
						
						active = false;
						
						if (toggleVisible && target.alpha === 0) {
							
							target.visible = false;
							
						}
						
						complete (true);
						return;
						
					} else {
						
						if (MotionInternal::reflect) {
							
							MotionInternal::reverse = !MotionInternal::reverse;
							
						}
						
						startTime = currentTime;
						timeOffset = startTime + MotionInternal::delay;
						
						if (MotionInternal::repeat > 0) {
							
							MotionInternal::repeat --;
							
						}
						
					}
					
				}
				
				if (sendChange) {
					
					change ();
					
				}
				
			}
			
		}
		
		
		// Event Handlers
		
		
		
		protected static function shape_onEnterFrame (event:Event):void {
			
			var currentTime:Number = getTimer () / 1000;
			
			var actuator:FrameByFrameActuator;
			
			for (var i:uint = 0; i < actuatorsLength; i++) {
				
				actuator = actuators[i];
				
				if (actuator.active) {
					
					if (currentTime > actuator.timeOffset) {
						
						actuator.MotionInternal::update (currentTime);
						
					}
					
				} else {
					
					actuators.splice (i, 1);
					--actuatorsLength;
					i --;
					
				}
				
			}
			
		}
		
		
	}
	
}

import com.eclecticdesignstudio.motion.Actuate;
import com.eclecticdesignstudio.motion.actuators.FrameByFrameActuator;

class TimerByPass
	{
		function TimerByPass(duration:Number, func:Function):void
		{
			Actuate.timer (duration, FrameByFrameActuator)
				.onComplete (func);
		}
	}

Have fun.

1 Like

I know this is old but Iā€™m making some video from an animation too, and the frame sizes are large and the Linux/cpp target chokes when saving out the pngs and everything goes wonky as the animation using Actuate is time based.

The class above once Iā€™ve dropped it in motion.actuators, and sorted out the syntax probs on the package declaration then borks at ā€œuse namespaceā€.

@LukeT can you shed some light on how to use this class please?

Hi tjrhodes,

Theese issues are probably because youā€™re writing Haxe Code and the class above is written in Actionscript. If you tell me what version of Actuate you are using, Iā€™ll have a look into the code (in the evening CETime).

Have fun

I would prefer to have a nicer API in the future, but there are some manual APIs you can use to control the time elapsed in Actuate, you can see an example of this in our unit tests for Actuate:

https://github.com/openfl/actuate/blob/master/tests/test/TweenTest.hx#L202-L213

Overriding SimpleActuator.getTime and manually calling SimpleActuator.stage_onEnterFrame can work for advancing Actuateā€™s animations, if you use <haxedef name="actuate-manual-time" /> in the project

Link is broken :frowning:
Any solutions, when time is frame dependant?

Itā€™s still there:
https://github.com/jgranick/actuate/blob/master/test/test/TweenTest.hx#L202-L216

If I build with flag:

I get runtime error:
Uncaught TypeError: motion_actuators_SimpleActuator.getTime is not a function

Ok, Something like this:

SimpleActuator.getTime = () -> totalTime;
....    
    var totalTime:Float = 0.0;
    private function enterFrame(event:Event):Void
    {
        totalTime += 1 / 60;
    }