Adobe SVG Viewer and animation modification (Part II)

In the first part of this article, the difficulties of enhancing WaterSums to use toolbar buttons to control simulation playback speed with the Adobe SVG Viewer (ASV) were discussed. In this part, we see the implementation of some functions to help with the necessary pausing of animations before various ASV operations can be performed successfully. Searches of the web did not find any useful hints in this direction, but extensive experimentation and testing came up with a method that worked without crashing.

Update: This blog entry has been updated to include extra checks to make the pause function safer for more complex animations.

In the code which follows, some of the global variables and other functions are detailed in an earlier blog article here and its second part here. The log() function is simply a function to log errors or debug messages.

Pause function

First we look at the pause method. The only complicated requirement is the need to make sure we don't try to pause animations when the clock time has just passed an animation restart time or some other 'significant' event in the animation (eg. changing the href in a text reference (<tref> element). The ASV is a little fragile at these times and attempting to pause the animations can cause an unexpected exception in JavaScript, an access violation, or the sudden total disappearance of Internet Explorer. After several such events, it is sometimes necessary to reboot! At these times, the Adobe SVG Viewer seems to be busy with the changes necessary to perform the animation. A small hint of these problems is found on w3.org here.

Some extra global variables needed by the function are also included:

/* Adobe SVG Viewer pause control variables */
var ASVPauseCheckTimer = null;
var ASVAbandonPauseTimer = null;
var ASVPauseCallback = null;
 
function ASVPauseAnimation(fnpause) {
	var first = false;
	if (ASVPauseState < 0) {
		/* our pause functions are busy */
		if (ASVPauseState == ASVAnimationWaitingToPause &&
				fnpause === -1) {
			ASVPauseState = ASVAnimationExecuting;
			fnpause = ASVPauseCallback;
		} else {
			log("ASVPauseAnimation: pause mechanism is busy");
			return (ASVPauseState-10); /* already busy */
		}
	} else {
		first = true;
	}
	if (ASVPauseState == ASVAnimationPaused) {
		/* we think animations are already paused, but check anyway */
		if (!svg._svg.animationsPaused()) {
			log("ASVPauseAnimation: state (paused)"+
					" and ASV (not paused) don't agree - pausing");
			ASVPauseState = ASVAnimationExecuting;
			return (ASVPauseAnimation());
		}
	} else if (ASVPauseState == ASVAnimationExecuting) {
		/* we think animations are running, but check anyway */
		if (svg._svg.animationsPaused())
			log("ASVPauseAnimation: state (not paused)"+
					" and ASV (paused) don't agree");
		if (fnpause) ASVPauseCallback = fnpause;
		if (first) {
			/*
			 * how safe it is to pause depends to some extent
			 * on what has just been done, so we always wait
			 * before pausing just in case - this makes it safer
			 */
			ASVPauseState = ASVAnimationWaitingToPause;
			ASVPauseState = ASVAnimationWaitingToPause;
			ASVPauseCallback = fnpause;
			setTimeout(function() {
				ASVPauseAnimation(-1);	/* magic value */
			}, 5);
			return (ASVPauseState);
		}
		if (clockproganim) {
			var t = svg._svg.getCurrentTime();
			var dur = clockproganim.getAttributeNS(null,'dur');
			var t0 = t/dur;					/* number of cycles */
			var t1 = t0 - Math.floor(t0);		/* fraction of this cycle */
			var t2 = t1 * dur;				/* time in this cycle */
			/* check if we are too close to a 'frame' switch */
			var stepsnow = Math.floor((t2*simclockSpeedMultiplier)+
					 (simclockNSteps*0.001));
			if ((t2-0.001-(stepsnow/simclockSpeedMultiplier)) < 0.003) {
				ASVPauseState = ASVAnimationWaitingToPause;
				ASVPauseCallback = fnpause;
				setTimeout(function() {
					ASVPauseAnimation(-1);	/* magic value */
				}, 3);
				return (ASVPauseState);
			}
		}
		try {
			svg._svg.pauseAnimations();
		} catch(er) {
			log("pauseAnimation failed: "+er.message);
			ASVPauseState = ASVAnimationWaitingToPause;
			ASVPauseCallback = fnpause;
			setTimeout(function() {
				ASVPauseAnimation(-1);	/* magic value */
			}, 50);
			return (ASVPauseState);
		}
		/* most of the time, animations pause 'immediately', so check if they have */
		if (svg._svg.animationsPaused()) {
			ASVPauseState = ASVAnimationPaused;
			if (ASVPauseCallback) {
				try {
					ASVPauseCallback(true);
				}
				catch(er){
					log("ASVPauseAnimation callback error: "+er.message);
				}
				ASVPauseCallback = null;
			}
			$('#SVGdiv').trigger('ASVPaused');
		} else {
			/* animations have not paused yet, so set a timeout and a timer to check */
			ASVPauseState = ASVAnimationPausePending;
			ASVAbandonPauseTimer =
				setTimeout(_ASVAbandonPauseAttempt,5000);
			ASVPauseCheckTimer = setTimeout(_ASVPauseCheck,5);
		}
	} else {
		log("ASVPauseAnimation: invalid state value: "+ASVPauseState);
	}
	return ASVPauseState;
}
This pause function refers to two callback functions called from timers, one to abandon the attempt to pause the animations and the other to check whether the animations have paused. The callbacks are shown below.
/* internal function called by timer to time out pause attempt */
function _ASVAbandonPauseAttempt() {
	var success = false;
	clearTimeout(ASVPauseCheckTimer);
	ASVPauseCheckTimer = null;
	ASVAbandonPauseTimer = null;
	if (!svg._svg.animationsPaused()) {
		log("_ASVAbandonPauseAttempt: timed out waiting for animation to pause");
		ASVPauseState = ASVAnimationExecuting;
	} else {
		ASVPauseState = ASVAnimationPaused;
		success = true;
	}
	if (ASVPauseCallback) {
		try {
			ASVPauseCallback(success);
		}
		catch(er){
			log("ASVPauseAnimation callback error: "+er.message);
		}
		ASVPauseCallback = null;
	}
	try {
		if (success) {
			$('#SVGdiv').trigger('ASVPaused');
		} else {
			$('#SVGdiv').trigger('ASVPauseFailed');
		}
	}
	catch(er){
		log("_ASVAbandonPauseAttempt event error: "+er.message);
	}
}
 
/* internal function called by timer to check if pause has occurred */
function _ASVPauseCheck() {
	ASVPauseCheckTimer = null;
	if (svg._svg.animationsPaused()) {
		ASVPauseState = ASVAnimationPaused;
		clearTimeout(ASVAbandonPauseTimer);
		ASVAbandonPauseTimer = null;
		if (ASVPauseCallback) {
			try {
				ASVPauseCallback(true);
			}
			catch(er){
				log("ASVPauseAnimation Callback error: "+er.message);
			}
			ASVPauseCallback = null;
		}
		try {
			$('#SVGdiv').trigger('ASVPaused');
		}
		catch(er){
			log("ASVPaused event error: "+er.message);
		}
	} else  {
		log("_ASVPauseCheck still waiting for animations to pause");
		ASVPauseCheckTimer = setTimeout(_ASVPauseCheck,5);
	}
}
Both callbacks trigger events associated with the SVG div with ID SVGdiv as used by previous blog entries. Any code wishing to receive these success/fail events would need to use the jQuery bind function. For example:
function MyPauseHandler(event) {
	log("ASVPaused event received");
}
 
$('#SVGdiv').bind('ASVPaused',MyPauseHandler);
or using an anonymous function:
$('#SVGdiv').bind('ASVPaused',function(event) {
	log("ASVPausedEvent received");
});

UnPause function

Next we look at the unpause method and some extra global variables it needs:

/* Adobe SVG Viewer unpause control variables */
var ASVUnPauseCheckTimer = null;
var ASVAbandonUnPauseTimer = null;
var ASVUnPauseCallback = null;
 
function ASVUnPauseAnimation(fnunpause) {
	if (ASVPauseState < 0) {
		log("ASVUnPauseAnimation: in pending state "+ASVPauseState);
		return (ASVPauseState-10); /* already busy */
	}
	if (ASVPauseState == ASVAnimationExecuting) {
		/* we think animation is already running, but make sure we check */
		if (svg._svg.animationsPaused()) {
			log("ASVUnPauseAnimation: state (not paused)"+
					" and ASV (paused) don't agree - unpausing");
			ASVPauseState = ASVAnimationPaused;
			return (ASVUnPauseAnimation());
		}
	} else if (ASVPauseState == ASVAnimationPaused) {
		/* we think animation is paused, but make sure we check */
		if (!svg._svg.animationsPaused())
			log("ASVUnPauseAnimation: state (paused)"+
					" and ASV (not paused) don't agree");
		if (fnunpause) ASVUnPauseCallback = fnunpause;
		/* unpause the animations */
		svg._svg.unpauseAnimations();
		/*
		 * testing shows that having a delay here works best
		 * but the length of the delay seems to be different
		 * depending on whether the unpause happened immediately.
		 * If it did, use 25ms else 5ms.
		 */
		ASVPauseState = ASVAnimationUnPausePending;
		ASVAbandonUnPauseTimer =
			setTimeout(_ASVAbandonUnPauseAttempt,5000);
		if (svg._svg.animationsPaused()) {
			ASVUnPauseCheckTimer = setTimeout(_ASVUnPauseCheck,5);
		} else {
			ASVUnPauseCheckTimer = setTimeout(_ASVUnPauseCheck,25);
		}
	} else {
		log("ASVUnPauseAnimation: invalid state value: "+ASVPauseState);
	}
	return ASVPauseState;
}
The unpause function refers to two callback functions called from timers, one to abandon the attempt to unpause the animations and the other to check whether the animations have unpaused. These are shown below.
/* internal function called by timer to time out unpause attempt */
function _ASVAbandonUnPauseAttempt() {
	var success = false;
	clearTimeout(ASVUnPauseCheckTimer);
	ASVUnPauseCheckTimer = null;
	ASVAbandonUnPauseTimer = null;
	if (svg._svg.animationsPaused()) {
		log("_ASVAbandonUnPauseAttempt: timed out waiting for animation to pause");
		ASVPauseState = ASVAnimationPaused;
	} else {
		ASVPauseState = ASVAnimationExecuting;
		success = true;
	}
	if (ASVUnPauseCallback) {
		try {
			ASVUnPauseCallback(success);
		}
		catch(er){
			log("ASVUnPauseAnimation callback error: "+er.message);
		}
		ASVUnPauseCallback = null;
	}
	try {
		if (success) {
			$('#SVGdiv').trigger('ASVUnPaused');
		} else {
			$('#SVGdiv').trigger('ASVUnPauseFailed');
		}
	}
	catch(er){
		log("ASVUnPauseFailed event error: "+er.message);
	}
}
 
/* internal function called by timer to check if unpause has occurred
 * Updated 6 November 2009 to fix basic bugs
 */
function _ASVUnPauseCheck() {
	ASVUnPauseCheckTimer = null;
	if (!svg._svg.animationsPaused()) {
		ASVPauseState = ASVAnimationExecuting;
		clearTimeout(ASVAbandonUnPauseTimer);
		ASVAbandonUnPauseTimer = null;
		if (ASVUnPauseCallback) {
			try {
				ASVUnPauseCallback(true);
			}
			catch(er){
				log("ASVUnPauseAnimation Callback error: "+er.message);
			}
			ASVUnPauseCallback = null;
		}
		try {
			$('#SVGdiv').trigger('ASVUnPaused');
		}
		catch(er){
			log("ASVUnPaused event error: "+er.message);
		}
	} else  {
		ASVUnPauseCheckTimer = setTimeout(_ASVUnPauseCheck,5);
	}
}

Once again, both callbacks send events associated with the SVG div with ID SVGdiv as used by previous blog entries. Any code wishing to receive these success/fail events would need to use the jQuery bind function to bind a listener function.

In the third part of this article, the Pause and UnPause functions will be combined (DV) to produce a function that allows the caller to wrap a pause/unpause envelope around an operation or a series of operations. Wait for Adobe SVG Viewer and animation modification (Part III).