Article
Difficulty Rating
8
Using the Send Command

.

.

.

.

 

Using the "Send" Command to Schedule Future Events and Animation

You wouldn't guess from the name, but the send command it is actually one of MetaCard's most important features. This article details using it to schedule events and animations.

This is a basic send command:

send "mouseUp" to me in 5 seconds
                                    

That statement causes the message "mouseUp" to be delivered to the same object that sent it, 5 seconds from when the command was executed. The MetaCard engine is not occupied during that period, and is therefore free to do other things. After 5 seconds have elapsed, that message will be delivered to the object.

The advantages of this approach are many. You can cause messages to be sent at any time in future. There is no measurable performance decrease in the interim between sending and receiving the message. MetaCard remains free to do other tasks during that period. This completely eliminates the need to use messages such as "idle", a common, complicated and processor intensive kludge used in many other tools.

Scheduling a Repeat Loop

Setting something to occur regularly is easy. The following script updates a field on screen to show the time, once every ten seconds.

on mouseUp
  --this handler starts the timer
  setClock
end mouseUp

 

on setClock
  put the time into fld 1
  send "setClock" to me in 10 seconds
end setClock
                           

This example would repeat forever, putting the time into field 1 every 10 seconds.

We can easily modify this example to be more useful. We'll include a facility to cancel the message, and also make the time update much more frequently, and include seconds.

local lTimerID

 

on mouseUp
  --this handler starts the timer
  setClock
end mouseUp

 

on setClock
  put the long time into fld 1 --long time also has seconds
  send "setClock" to me in 600 milliSeconds --600 milliseconds or 0.6 seconds
  --will be enough to keep the seconds on the clock current
  put the result into lTimerID
  --the above line stores the "ID" of the message just sent
end setClock
                           

The result contains a unique ID for the message you have sent. You can stop that message from being delivered at any time by passing that ID to the cancel command:

cancel lTimerID
                                    

The pendingMessages function contains a list of all the messages scheduled to be delivered in the future. Messages are added to it every time an event is scheduled, and removed when the message is delivered (or cancelled with the cancel command).

Here is a line from the pendingMessages:

12,913382037.933333,setClock,button id 1003 of card id 1002 
of stack "Stack 913381993"
                                    

The first item is the ID of the message. You can cancel the message using this ID just in the same way you would cancel it using the result function to retrieve and store the id.

cancel (item 1 of the pendingMessages)
                                    

The second item contains the time the message will be delivered at. The third item contains the name of the message being delivered, and the fourth item contains the path for the object that the message will be delivered to.

The pendingMessages function can be used to check if a message has already been scheduled. In the above example of updating a clock, a problem may arise if the user presses the original button used to trigger the script for a second time. Two sets of the same message will then start being sent, and the clock will be updated twice as frequently as originally specified. Repeated clicking of the button will eventually cause messages to be sent almost constantly, locking up MetaCard. Inserting the following line into the top of the mouseUp message prevents this problem:

if "setClock" is in the pendingMessages then exit mouseUp
                                    

This will prevent the mouseUp handler from executing if the cycle has already been started. Of course, advanced users reading this may have noticed it is a few milliSeconds more efficient to set up a variable the first time the handler is executed, and check that instead of reading a function such as the pendingMessages. Checking the pendingMessages is simpler and more English like, so this kind of optimisation will only matter if the statement is being performed repeatedly in an extremely time sensitive script.

Be sure to cancel any pending messages when they are not going to be required. It is not atypical to want to cancel all messages when a card or stack is closed. The best technique for doing this is to cancel each message using its ID, from a list of variables you created and updated whenever you sent a message. This results in accurate cancellation and won't affect any messages still running in other stacks.

However, there is also a brute-force way of cancelling all pending messages:

on closeCard
  repeat for each line l in the pendingMessages
    cancel (item 1 of l)
  end repeat
end closeCard
                           

That repeat loop is useful to remember. Run it in the message box or keep it handy in a button when you're debugging any complex send based script. You may want to use it in an emergency to clear the pendingMessages when you get into something you find you can't get out of.

Doing animation with "send"

An obvious use for the send command is to do animation. Carefully done, animation can be virtually asynchronous, meaning that MetaCard can respond to events while the animation is taking place. For a smooth animation, it is important that the individual handlers executed by the send command are as short as possible. Where multiple things need to be done, it is best split into multiple handlers.

The reason for this is simple: only one script can be executing at a time. Send is no exception. Like any other command, when a send command is running, other scripts cannot run. In fact, a send command can only be delivered if there is no other handler running. Otherwise, it will wait until the end of whatever handler is running to be delivered. Keeping all the handlers that execute short, and splitting longer scripts into multiple handlers delivered by send, allows you to maintain user interaction whilst updating the screen.

Why is MetaCard not truly multi-threaded? A multi-threaded engine would be capable of running multiple scripts at once. MetaCard does not support this, because of the complexities of creating and debugging a multi-threaded application. The send command is provided as an alternative, and if used well, can achieve excellent results without the hassle of keeping track of multi-threaded script or code. Note that the GIF animation commands and the move command are exceptions to this: they will both continue to run when other scripts are running (with a few exceptions such as using the file access commands to read in huge files).

It is a good idea to use the move command in conjunction with animated GIFs to do the bulk of any animation wherever possible. You may want to start and stop GIF animations, start and stop move commands, manually alter the frame being shown in a GIF animation, alter button icons in a moving button, start and stop sounds, show and hide objects, scroll fields, or use various other techniques or a combination of all of these to produce a presentation. All of these can be controlled, while still allowing user interaction, with a series of carefully scheduled send commands.

In this example, we will cause a field to scroll from start to finish. You can use this effect on a locked field without a scrollbar to animate a typical "credits" screen with rolling credits. You could also set the hScroll property of the field instead of the vScroll property to scroll horizontally, a technique used in the MetaCard demo stack. Finally, this script example can be applied to other animation related activities that must be scheduled to occur smoothly and reliably.

We'll start with a basic script, just like in the previous example.

local lCurrentFieldScroll, lCurrentEndValue

 

on mouseUp
  put 0 into lcurrentFieldScroll --start with the field scrolled to 0
  put 500 into lCurrentEndValue --the vScroll of the field when fully scrolled down
  updateField
end mouseUp

 

on updateField
  add 10 to lCurrentFieldScroll
  if lCurrentFieldScroll > lCurrentEndValue then exit updateField
  set the vScroll of fld "example" to lCurrentFieldScroll
  send "updateField" to me in 100 milliseconds
end updateField
                           

This example will scroll a field, incrementing every 100 milliseconds. For basic animation, this is often enough. The script will take the same order of magnitude of time to run regardless of the speed of machine it is run on, and varying the delay between messages can be used to alter the speed of the animation.

However, if you need to guarantee that the script will take a certain amount of time to run, you need to do a little more work. Other messages could get delivered when that field is scrolling, or the machine might be busy and therefore slow down slightly. If you are playing back audio or other parts of a presentation to be exactly in sync, you cannot allow this field to be left behind or gradually become out of sync if anything eles on the computer causes a glitch or slow down.

Here is a new script that will ensure that the field scrolling is complete inside ten seconds. If there is a delay or interruption causing messages to be delivered late, the scrolling will jump to the correct position when processor time is returned, rather than increase the overall time taken to complete the effect.

local lCurrentFieldScroll, lCurrentEndValue, lTotalTime, lStartTime

 

on mouseUp
  put 10000 into lTotalTime -- the total time allowed in milliseconds
  put 0 into lcurrentFieldScroll --start with the field scrolled to 0
  put 500 into lCurrentEndValue  --the vScroll of the field when fully
                                 --scrolled down
  put the milliseconds into lStartTime --measure the animation from 
                                 --this start time
  updateField
end mouseUp

 

on updateField
  put the milliSeconds - lStartTime into tCurrentTime
  if tCurrentTime > lTotalTime then --we've reached the end
    set the vScroll of fld "example" to tEndValue --ensure that the 
                                                  --field is at the end
    exit updateField --don't send this message again
  end if
  put tCurrentTime / lTotalTime * lCurrentEndValue into lCurrentFieldScroll
  --thats the position the scroll bar should be based on the time elapsed
  set the vScroll of fld "example" to round(lCurrentFieldScroll) 
  -- rounding is required or the script will not work
  send "updateField" to me in 50 milliseconds
end updateField
                           

You can use the above script for just about any time related activity that needs to be performed on time. Just alter the variables in the first handler, such as the total length, the total number of frames (in this case the total scroll value of the field). You can also change how frequently the message is delivered. This message was delivered every 50 milliseconds, changing that value down the way will result in a smoother animation, and changing the value up the way will leave more processor time free. But altering that interval will not alter the total length of time time to complete the animation.

Enjoy experimenting and using the send command! And look out for a timeline animation tool that automates writing the scripts in our forthcoming Editor for MetaCard.

Did you find this article useful? Have any ideas for future topics? Email Us!