Adobe SVG Viewer and animation modification (Part III)
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 the second part, an asynchronous pause and unpause function were presented. This third and last part ties the pieces together by presenting a solution which makes it relatively easy to control the ASV without the crashes that can happen frequently when there are active animations.
ASVPauseAnimationToExecute(fnop, fncomplete)
The pause/execute/unpause function ASVPauseAnimationToExecute(fnop, fncomplete) is conceptually very simple. It calls ASVPauseAnimation(), passing in a callback. When animations have paused, the callback is called and in turn calls the operations function fnop as many times as necessary until the operations are complete. It then calls ASVUnPauseAnimation() and passes in an unpause callback. Once the unpause is complete, the unpause callback calls the completion callback fncomplete that was passed in to the ASVPauseAnimationToExecute() function.
A few complications exist in error handling, including providing timeout protection against unexpected errors in the process, but nothing very complex.
Let's start by looking at some global control variables we need.
/* Adobe SVG Viewer pause/op/unpause control variables */ var ASVPauseOpBusy = false; var ASVPauseOpPaused = false; /* did we need to pause animation? */ var ASVPauseOpCallback = null; var ASVPauseOpFinalCallback = null; var ASVPauseOpResult = null; var ASVOpSeqNum = 0; var ASVPauseOpTimer = null; var ASVAbandonPauseOpTimer = null; var ASVOpCurrentTime = 0.0;
false if the function is busy.
// Function ASVPauseAnimationToExecute(fnop, fncomplete) // Arguments fnop is a function: fnop(seqnum (int)) // fncomplete is a function: fncomplete(status (int), seqnum (int)) function ASVPauseAnimationToExecute(fnop, fncomplete) { if (ASVPauseOpBusy) return false; if (!fnop) return false; /* initialise global values */ ASVPauseOpBusy = false; ASVPauseOpPaused = false; ASVPauseOpCallback = null; setASVOpSeqNum("ASVPauseAnimationToExecute",ASVOperationOK); ASVOpSeqNum = 0; ASVPauseOpTimer = null; ASVAbandonPauseOpTimer = null; ASVOpCurrentTime = 0.0; ASVPauseOpCallback = fnop; if (fncomplete) ASVPauseOpFinalCallback = fncomplete; else ASVPauseOpFinalCallback = null; ASVPauseOpBusy = true; ASVPauseOpResult = ASVOperationOK; ASVPauseOpPaused = !svg._svg.animationsPaused(); var pstatus = ASVPauseAnimation(_PauseOpPauseCallback); if (pstatus < -10) { log("ASVPauseAnimation returned " + pstatus); return false; } else { if (pstatus < 0) log("ASVPauseAnimation pausing ("+pstatus+")"); return true; } }
The operation callback fnop is mandatory. The completion callback fncompletion is optional, but is the only way of telling whether the process was ultimately successful. The completion function is passed a status. Possible values are:
/* * Status constants passed in to fncomplete when * ASVPauseAnimationToExecute completes */ var ASVOperationOK = 0; var ASVPauseFailedError = 1; var ASVOperationFailedError = 2; var ASVUnPauseFailedError = 4;
Pause callback
The pause callback _PauseOpPauseCallback() referred to needs to handle the calling of the operation callback fnop() and unpausing animations with ASVUnPauseAnimation(). The interaction with the operation callback must also be protected with a timeout so that it is more difficult to end up in an endless loop. This pause callback is shown below. Observant readers will notice that it has two arguments, while the pause callback from ASVPauseAnimation() is only called with one argument. This extra argument allows us to pass in extra information when we call this function from a timer and to identify when we are called by the pause function. When we are called from the pause function, the second argument is 'undefined'.
function _PauseOpPauseCallback(success, seqnum) { if (success) { if (seqnum == undefined) { /* * This happens when we are called from * ASVPauseAnimation. * Set up a timer to kill the operation * in case it takes too long * TODO the duration should be configurable */ setASVOpSeqNum("_PauseOpPauseCallback:1",0); ASVAbandonPauseOpTimer = setTimeout(function() { ASVPauseOpCallback = null; _PauseOpPauseCallback(true, -1); }, 5000); } if (ASVPauseOpCallback) { /* still processing operation while paused */ setASVOpSeqNum("_PauseOpPauseCallback:2", ASVPauseOpCallback(getASVOpSeqNum("_PauseOpPauseCallback:1"))); if (getASVOpSeqNum("_PauseOpPauseCallback:2") <= 0) { clearTimeout(ASVAbandonPauseOpTimer); ASVAbandonPauseOpTimer = null; ASVPauseOpTimer = null; if (ASVPauseOpPaused) { ASVUnPauseAnimation(_PauseOpUnPauseCallback); } else { /* no need to unpause - call callback directly */ _PauseOpUnPauseCallback(true); } } else { ASVPauseOpTimer = setTimeout(function() { _PauseOpPauseCallback(true, getASVOpSeqNum("_PauseOpPauseCallback:4")); }, 5); } } else { /* operation has timed out so unpause */ log("ASVPauseAnimationToExecute: operation timed out"); clearTimeout(ASVPauseOpTimer); ASVPauseOpTimer = null; ASVAbandonPauseOpTimer = null; ASVPauseOpResult |= ASVOperationFailedError; if (ASVPauseOpPaused) { ASVUnPauseAnimation(_PauseOpUnPauseCallback); } else { /* no need to unpause - call callback directly */ _PauseOpUnPauseCallback(true); } } } else { log("ASVPauseAnimationToExecute: pause failed"); if (ASVPauseOpFinalCallback) { ASVPauseOpFinalCallback(ASVPauseFailedError,null); ASVPauseOpFinalCallback = null; } setASVOpSeqNum("_PauseOpPauseCallback:3",0); ASVPauseOpBusy = false; ASVPauseOpPaused = false; ASVPauseOpCallback = null; } }
It is worth noting that since ASVPauseAnimationToExecute() uses the ASVPauseAnimation() and ASVUnPauseAnimation() functions, the ASVPause/ASVUnPause events will also be triggered during the process. Furthermore the same timeout periods apply, so ASVPauseFailed/ASVUnPauseFailed events can also be generated.
UnPause callback
The unpause callback _PauseOpUnPauseCallback() referred to in _PauseOpPauseCallback() needs to handle the calling of the user defined completion function and then reset global variables. ASVPauseOpResult is a global variable that is used to communicate any errors from other functions and this value is passed in to the completion function to indicate success or failure using the status values defined earlier.
function _PauseOpUnPauseCallback(success) { if (!success) log("ASVPauseAnimationToExecute: unpause failed"); if (ASVPauseOpFinalCallback) { var result = ASVPauseOpResult; if (!success) result |= ASVUnPauseFailedError; if (getASVOpSeqNum("_PauseOpUnPauseCallback:1") < 0) result |= ASVOperationFailedError; ASVPauseOpFinalCallback(result, getASVOpSeqNum("_PauseUnOpPauseCallback:2")); ASVPauseOpFinalCallback = null; } ASVPauseOpBusy = false; ASVPauseOpPaused = false; ASVPauseOpCallback = null; ASVPauseOpResult = ASVOperationOK; setASVOpSeqNum("_PauseOpUnPauseCallback",0); ASVPauseOpTimer = null; ASVAbandonPauseOpTimer = null; ASVOpCurrentTime = 0.0; }
Utility functions
A couple of utility functions are used in the above functions to help with debugging and they are included below:
/* get/set ASVOpSeqNum which is a global variable */ var ASVOpSeqNumBusy = false; function getASVOpSeqNum(msg) { var val = -1; if (ASVOpSeqNumBusy) { log("getASVOpSeqNum is busy ("+msg+")"); } else { ASVOpSeqNumBusy = true; val = ASVOpSeqNum; ASVOpSeqNumBusy = false; } return (val); } function setASVOpSeqNum(msg,val) { var retval = false; if (ASVOpSeqNumBusy) { log("setASVOpSeqNum("+val+") is busy ("+msg+")"); } else { try { ASVOpSeqNumBusy = true; if (val !== ASVOpSeqNum) ASVOpSeqNum = val; retval = true; } catch(er) { } ASVOpSeqNumBusy = false; } return retval; }
User defined operation callback
The user-defined callback is called repeatedly once animations have been paused. A timeout period (5 seconds) is enforced so that bugs or unexpected errors will not compeltely lock up the pause/unpause feature.
- 'Prototype'
function fnop(seqnum)
- Input argument
seqnum(integer) the sequence number in the user operation.
fnop is a user-defined callback function taking a single argument - a sequence number - which is called repeatedly once animations have been paused. Interaction through this callback is as follows:
- the callback function is initially called with a sequence number of
- when the callback function returns,
ASVPauseAnimationToExecutesaves the value in a global variableASVOpSeqNumand arranges to call the function again in 5 milliseconds with the value of the global at that time - to indicate that the operation is completed, the user callback function must return 0
function fnop(seqnum)
if seqnum = 0 // starting process
set animation durations
seqnum = 1
if seqnum = 1
if animations durations have been updated
set current time
seqnum = 2
if seqnum = 2
if time has been set
seqnum = 0 // process has finished
return seqnum
In this way, if operations occur immediately, the process will be completed synchronously. If delays occur, there can be one or more subsequent calls to the callback. This method works well for this situation of setting animation durations. However, not all operations can safely occur immediately (for example, using this framework to provide mousewheel zoom support) and in such cases the construction of the callback would be more like:
function fnop(seqnum)
if seqnum = 0 // starting process
kick off operation 1
seqnum = 1
else if seqnum = 1
if operation 1 has finished
kick off operation 2
seqnum = 2
else if seqnum = 2
if operation 2 has finished
seqnum = 0 // process has finished
else
error - invalid seqnum
return seqnum
User defined completion callback
- 'Prototype'
function fncomplete(result, seqnum)
- Input arguments
result(integer) will be a combination of one or more of the following:var ASVOperationOK = 0; var ASVPauseFailedError = 1; var ASVOperationFailedError = 2; var ASVUnPauseFailedError = 4;
seqnum(integer) will be the sequence number when the user operations finished. If the process has been successful, this will be 0. If result has theASVOperationFailedErrorbit set,seqnumwill probably have a non-zero value indicating where the operations failed (or timed out).
This function is called immediately before animations are unpaused (if they were paused by ASVPauseAnimationToExecute()) and is provided with the result of the operation in an integer. If the process has completed successfully, the value will be zero (ASVOperationOK). A non-zero value will be made up of one or more of the bit values listed above. The callback can do whatever is necessary to report completion, clean up an incomplete process or report errors. Any return value from the callback will be ignored.
Conclusion
This method has also been applied to the mousewheel zoom problem for which a solution is described in blog entries Adobe SVG Viewer and mousewheel zoom Part 1 and Part 2. This may be described in a later blog article, since it requires considerably less code and complexity than the solution presented in those two blog entries. If such an article would be helpful to you, feel free to ask (NB: it requires the 5 millisecond delays between the operations as shown above in the second example of an operation callback.)
I hope these discussions can help someone to get more reliable behaviour from the ASV 3.0.3 when animations are involved. I have not yet tested the solution with ASV 6 and my previous experience has been that ASV 6 behaves slightly differently from ASV 3 with respect to pausing/unpausing of animations. Maybe this could be the subject of another blog entry....
