Issue 58 * October 10 2008

Predicting the future with Mystic Mark – Part Three
Good scheduling makes things run smoothly

by Mark Waddingham

Introduction

Last time around, we left our gradient moving at a constant (uniform) speed in a circle around a central point. However, if you recall, the original stack had a nice fade-in/speed-up and fade-out/slow-down effect at the beginning and end of the animation.

Although the effect is quite nice, the code accompanying it definitely was not! Having only decided to add those effects at the last minute, the resulting implementation of the easing-in and easing-out effects shows its rather after-thought laden and rushed status.

Therefore, in this part, rather than waste time attempting to explain the rather odd and unclear state-based mechanism I used in the original, we will attempt to correct this travesty of coding by introducing much more structured and cleaner approach - a timeline - and marrying it with some simple ideas of linear interpolation .

As with previous parts, there is an accompanying stack which is simplified relative to the original to aid understanding. The stack corresponding to this part can be fetched by executing the following from the Message Box :

go stack url " http://www.runrev.com/newsletter/october/issue58/timeline_ball.rev "

Everything has its time

The problem we have is simple - we want to smoothly change the values of two variables over time (speed and opacity). In order to achieve this the first thing we have to do is know what values we want these variables to be at certain pre-ordained times!

This is precisely what we will take to be a timeline. It is simply the list of values we wish the speed and opacity variables to take at fixed times. For example, the original crystal ball had this timeline:

time (in ms)

0

1500

4500

6000

speed

0

1

1

0

opacity

0

100

100

0

Note that speed is actually the factor by which the individual glow's speed is multiplied rather than an absolute measure. [ Also note that the length of time the middle portion runs for is actually computed randomly in the original, we just fix it at 3000ms for clarity of exposition ].

So, for example, at 500ms past the animation started, the glows travel at 1/3 rd of their final speed and are only 33% opaque.

Now, this is actually already an ideal representation for the timeline - a list of columns with the attributes time, speed and opacity. Indeed, we can use Revolution 3.0's new nested arrays for the purpose: the points in the timeline will be numerically indexed keys in an array, and then each key will have as its value another array with the keys time, speed and opacity .

With this structure, the above table is simply constructed in code with:

put 0 into sSwirlTimeline[1][“time”]
put 0 into sSwirlTimeline[1][“speed”]
put 0 into sSwirlTimeline[1][“opacity”]

put 1500 into sSwirlTimeline[2][“time”]
put 1 into sSwirlTimeline[2][“speed”]
put 100 into sSwirlTimeline[2][“opacity”]

put 4500 into sSwirlTimeline[3][“time”]
put 1 into sSwirlTimeline[3][“speed”]
put 100 into sSwirlTimeline[3][“opacity”]

put 6000 into sSwirlTimeline[4][“time”]
put 0 into sSwirlTimeline[4][“speed”]
put 0 into sSwirlTimeline[4][“opacity”]

[ It is perhaps worth noting here that another perfectly reasonable representation of the timeline would be a string of lines consisting of 3 items. The reason I chose arrays here was mainly for readibility - the keys are self documenting, whereas when using items in strings, you always have to remember which item is which. (Of course, it also demonstrates another cool feature of Rev 3!) ]

I interpolate therefore I am

Okay, so now that we have our representation of our data structure we need to do something with it!

As we mentioned in the previous section, we want to smoothly change the speed and opacity of our glow as time moves by. We will achieve this by linearly interpolating each variables value between any two points in the timeline. Here interpolation means choosing a value for each point in time based on certain fixed values in such a way the value never 'jumps' in a discontinuous manner, and linearly means that the amount the value will change in any given time is constant.

Instead of viewing our timeline as a series of points, we can instead view it as a series of phases. In the above case we have:

            Phase 1 - from time 0ms to time 1500ms

                        speed goes from 0 up to 1

                        opacity goes from 0 up to 100

            Phase 2 - from time 1500ms to time 4500ms

                        speed remains constant at 1

                        opacity remains constant at 100

            Phase 3 - from time 4500ms to time 6000ms

                        speed goes from 1 to 0

                        opacity goes from 100 to 0

Indeed, with this point of view the interpolation we have the following simple computation to do the interpolation:

function interpolate pTime, pPhaseStartTime, pPhaseEndTime, \
   pStartValue, pEndValue
   local tPhaseDuration
   put pPhaseEndTime – pPhaseStartTime into tPhaseDuration
   
   local tPhaseOffset
   put pTime – pPhaseStartTime into tPhaseOffset
   
   local tPhaseRatio
   put tPhaseOffset / tPhaseDuration into tPhaseRatio
   
   return (1.0 – tPhaseRatio) * pStartValue + tPhaseRatio * \
   pEndValue
end interpolate

Here, we pTime should be within pPhaseStartTime and pPhaseEndTime .

To put this together with our timeline structure, all we need to do is find the phase in which a given time fits and then use the values of the variables at the start and the end of the phase together with interpolate :

command computeSwirl pTime, @rSpeed, @rOpacity, @rFinished 
          local tIndex 
          put 1 into tIndex 
          
          –- Loop until we find the first point immediately \
   after the requested time
          repeat while sSwirlTimeline[tIndex] is an array and \ 
                           sSwirlTimeline[tIndex][“time”] < \
      pTime
                    add 1 to tIndex 
          end repeat 
   
          –- If pTime is beyond the end of the timeline, then \
      the index will be empty
             –- (not an array) so take the final values as the \
      result and set the finished
             –- flag. 
             if sSwirlTimeline[tIndex] is not an array then 
                       put sSwirlTimeline[tIndex – 1][“speed”] \
         into rSpeed
                       put sSwirlTimeline[tIndex – 1][“opacity”] \
         into rOpacity
                       put true into rFinished 
                       exit computeSwirl 
             else 
                       put false into rFinished 
             end if 
      
             put interpolate(pTime, \ 
                    sSwirlTimeline[tIndex – 1][“time”], \
      sSwirlTimeline[tIndex][“time”], \
                    sSwirlTimeline[tIndex – 1][“speed”], \
      sSwirlTimeline[tIndex][“speed”]) \
                           into rSpeed 
      
      
             put interpolate(pTime, \ 
                    sSwirlTimeline[tIndex – 1][“time”], \
      sSwirlTimeline[tIndex][“time”], \
                    sSwirlTimeline[tIndex – 1][“opacity”], \
      sSwirlTimeline[tIndex][“opacity”]) \
                           into rOpacity 
   end computeSwirl  

You will find this implemented at the end of the card script of the first card "Timelined Ball - First Attempt" of the sample stack - to see the effect click the Run button.

The best laid plans of mice...

[ And, yes, I do mean "The best laid plans of mice "... ]

If any of you happily went off to try out sample stack at this point, you may have been a bit disappointed - things aren't quite right!

Indeed, although the fading-in and fading-out works fine there is something deeply wrong with the speed - first of all it doesn't smoothly change and secondly the gradient starts moving backwards at the end!

The problem here is a misunderstanding relating the speed variable and how it relates to the speed parameter of the circularPath function discussed last time. What we are currently doing is (at every point in time) changing the speed of the gradient for the entire time it has been moving at every point in time - this is obviously not correct as speed changes are cumulative: If one travels at 10mph for 1 hour, and then 20mph for 1 hour you end up travelling 30miles not 40 which is what our code would currently have us believe.

Okay, so we have identified a problem the question is - how do we fix it (preferably without touching the parametric stuff that already works quite nicely!)?

Its all in the name

As it turns out, our circularPath function parameters are perhaps a little inappropriately named. What we call the time parameter could equally be called distance - i.e. it returns the point on the path that will be reached after it has travelled a given distance (for some suitable units).

[ Note that this observation only really works in the case where the parametric path has a uniformity property - i.e. the length of the path in distance is proportional to its length in time. This is the case with a circle, where the constant factor is pi. ]

With this little bit of renaming, it becomes more obvious how to fix our issue - we need to be passing the distance travelled after taking into account changes in speed to the parametric function rather than the time and interpolated speed!

So what does this mean exactly? Well, essentially a little bit of maths together with a   more complicated computeSwirl function together. You can find a correct implementation on the second card "Timelined Ball - Correct" of the sample stack.

Basically, the correct version of the computeSwirl function iterates through each phase until it reaches the one containing the requested time accumualting the distance as it goes using the high-school formula:

            d = ½ at² + st

Here d is distance, a is the acceleration, s is the initial speed and t is the time.

Finally

Well that wraps up my short series on the RunRev Crystal Ball . On the final card "RunRev Crystal Ball" of the sample stack you will find the original ball reimplemented using the techniques discussed since - while the functionality is identical, hopefully the script makes for easier reading!

Again, have fun predicting!

 

Main Menu What's New