Worksheet 2

The Morphic Graphics System



Based on Tutorial: Fun with the Morphic Graphics System by John Maloney
Version 1.0, 17 April 2001, by Andrew P. Black,
black@cse.ogi.edu

Introduction

In this worksheet you will build a Morphic object by writing code (as opposed to direct manipulation). We're going to start with an empty class that is a subclass of Morph. Morph is the generic graphical class in the Morphic object system.

Open a Browser (from the world menu>>open submenu, or drag one from the tools flap) select the category for your stuff that you created previously. (If you don't have such a category, create one now — see the box.) Make sure that none of the other browser panes have stuff selected. The bottom pane of the browser will be showing the template for creating a new class. Make it look like this:

 
Morph subclass: #TestMorph
    instanceVariableNames: ''
    classVariableNames: ''
    poolDictionaries: ''
    category: 'ECOOP-MyStuff'

First select Object (double-click it) and type the word Morph with a capital M. The new class will be a subclass of Morph. Select NameOfSubClass" and type in "TestMorph". The category name will already be filled in, and will probably be different from mine!. Type Command-s to accept (save), or select accept (s) from the yellow button menu.

Now open a workspace (or switch to an existing workspace) and create an instance of your new class. Type

TestMorph new openInWorld.

Hit Command-d or choose "do It" from the yellow button menu. Since nothing was selected, the "doIt" applies to the whole of the line that the cursor is on.

Your new Morph (graphical object), a blue square, is in the upper left corner of the display. You can pick this object up and move it around. Just grab it with the mouse. Notice how, when you picked up this Morphic object, it threw a dark shadow behind it. That helps you to see that you've actually lifted it up above everything else. If there were overlapping objects, grabbing this object would automatically pull it to the front, and the shadow would show that it's in front of everything else.

You'll notice the odd capitalization of 'openInWorld'. Smalltalkers place a high value on readability, but in Smalltalk a message selector must be a single string without any spaces in it. The convention is to use capital letters at the beginning of each English word except for the first, and to stitching them all together without spaces. Other languages often use underbars or dashes between the words. Squeak does not. You will probably grow to like this convention, but you should adopt it even if you don't!

The Morphic Halo

Now click the blue mouse button on your Morph, and you will see a surrounding array of colored circles, which we call a "Morphic halo". Some of these circles are buttons, and some are handles; each of them is a quick way to send a command to the Morph. If you linger over any of the dots with the mouse, a help balloon will pop up telling you what the dot does.

The black handle's balloon says: "Pick up". In this case, if you drag the black handle, the effect is the same as if you dragged the blue rectangle itself. That may seem like a useless function, but the reason that it is there is that sometimes Morphs will have an interactive behavior, like a button. The pick up handle gives you a way of directly manipulating something that has behavior associated with mouse clicks.

The basic idea behind the halo is that there is no need to resort to "modes" for changing the size of an object, etc. That is, Morphic does not have a separate mode for editing an object as opposed to using it. In Morphic, you can get a halo on anything regardless of how active it is. The halo is a safe way of dealing with a Morph while it is running. For example, there is no need to turn off the function of a button in order to change its appearance.

Let's look at what the halo can do. The green duplicate handle is a button that makes a copy, and the pink handle with the x deletes the Morph (moves it to the trash). The yellow handle is used to resize the object—try dragging it. Resist the temptation to do too many other things to it yet; in particular, don't use the blue rotate handle.

Adding Custom Behaviour

Now let's start making this our own kind of object, by writing some methods to customize it.

The first thing you will do is make your object do something when you click on it with the mouse. This involves writing two methods. Click on the Browser. Click on "no messages" in the third pane. Type (or paste) the following text into the bottom pane, and Accept. (Beware: cutting text from some web browsers includes some invisible garbage characters; if the Squeak compiler doesn't like this text, that may be the problem).

handlesMouseDown: evt
    ^ true

If this is the first time that you have accepted a method in this image, you will be asked to type your initials, which will be recorded along with the methods that you change.

The fact that this method, handlesMouseDown:, answers true tells Squeak that this object wants to receive a message when a mouse button is pressed over it. (Pressing a mouse button generates a mouseDown event; guess what you get when a mouse button is released!)

So now we better define a method corresponding to that message. Go ahead and type (or paste) the following text right over the other method.

mouseDown: evt
    self position: self position + (10 @ 0).

This will move the object ten pixels to the right when you click on it. How's that? Well, the infix @ notation is used to build a Point from two numbers. For example, 2@3 is a Point, with x coordinate 2 and y coordinate 3. The message position, when sent to a Morph, answers a Point that is the Morph's current position. And + is defined on Points to do elementwise addition. Note that position: is a different method; this one takes a Point as argument and changes the position of the receiver.

Try clicking on the object; see what happens.

To summarize: at this point we have two methods. One of them returns true to say that we want to handle mouseDown: events. The second one responds to the mouseDown event by changing the object's position.

Now lets give this Morph some behavior for redrawing itself. Type or paste this method into the bottom pane of the browser and accept it. Rather than worrying about indentation, if you wish you can choose pretty print from the yellow-button menu more... menu.

drawOn: aCanvas
    | colors |
    colors := Color wheel: 10.
    colors withIndexDo: [:c :i |
        aCanvas fillOval: (self bounds insetBy: self width // 25 * i + 1)
                color: c].

Now when you click on the object it looks totally different. When the screen needs to be redrawn, the drawOn: message is sent by the world to all of the objects in it. So, by defining this method for our Morph, we are defining its appearance. Let's go back and look at what this method is doing.

Understanding the drawOn: method

The first line, drawOn: aCanvas, is a heading that gives the name of the method drawOn: and a name for the formal parameter, aCanvas. After the heading, the second line of the method, | colors |, declares a local variable colors. Actually, there is no need to type this line; if you omit it, the compiler will offer to add it for you. Remember, Smalltalk has no type declarations — any variable can name any kind of object. Smalltalk programmers often choose identifiers for variables based on the types of objects that they will name.

The third line of the method is the first line of executable code: it assigns to colors the result of the expression Color wheel: 10. To see what that does, select wheel: and type Command-m for immMMMMplementers. You will see that there are two implementations of wheel: select the one in Color class.

One thing we often do in Squeak is we put a little comment at the head of a method. The first line says what wheel: does. It returns a collection of thisMany colors, where thisMany is the argument. The colors are evenly spaced around the color wheel.

When it's easy to give an example of the method in action, Smalltalkers do that too. The second line here is an expression, which you can actually execute. Select the quoted text (by double-clicking just inside the quotes) and doIt (command-d). Across the top of the screen Squeak just sprays out the color wheel—a collection of colors spaced around the spectrum. (But not with total saturation nor full brightness, so they won't be too garish.)

Notice that you can scribble directly on the screen in Squeak—you don't have to be in some sort of window to draw. To clean up such scribbles, use the restore display item in the world menu (command-r).

This was a digression to figure out what wheel: meant. We now know that it's a collection of Colors. Close the Implementors of wheel: window by clicking in the "x" on the left side of the title bar.

The next part of the drawOn: method steps through that set of colors.

This is a fragment of the method drawOn:, which you have already typed:
    colors withIndexDo: [:c :i |
        aCanvas fillOval: (self bounds insetBy: self width // 25 * i + 1)
                color: c].

The withIndexDo: method evaluates the block between [ and ] ten times. It supplies as arguments to the block both an element of colors, c, and an index, i. The index is needed to keep track of where we are in the collection of colors; i will be 1 the first time around, 2 the second, and so on, all the way through to 10.

Inside the block, we send aCanvas the message, fillOval:color: with two arguments: the mess in parens and c, our color. What about that mess in parens? It is a Rectangle inset by (((self width) // 25) * i + 1) pixels inside the bounding Rectangle returned by (self bounds). As the i increases, the inset also increases, returning ever smaller nested Rectangles, in each of which we inscribe an oval and fill it with the color c.

When we look at our object, we see up to ten bands of color. Because the drawOn: method is using the result of the message width being sent to the Morph itself, this code "knows" how big the Morph is. Bring up the halo, and drag on the yellow dot. As you resize the Morph, it just keeps redrawing itself at the new size.

Animating the Morph

The next thing to do is make clicking on our Morph invoke a more exciting kind of motion—some animation. This means that we need to create a list of points that will be the path over which the object moves. When you click on the object, we want it to move to each point in the path in turn. So, we will need some state in our object, so that it can remember its path.

Morph subclass: #TestMorph
    instanceVariableNames: 'path'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'ECOOP-MyStuff'

Go back and click in the Browser, and then on the "instance" button (at the bottom of the second pane). That will bring up the TestMorph class definition. Then click in the place for instance variable names, between the single quotes, and type path. Accept it.

Initializing an Instance Variable

Since you added an instance variable, you need to make sure that it's initialized to something. Let's define an initialization message for TestMorph. Click on the message category whose name has changed to "as yet unclassified", and then type (or paste) the following text in the bottom pane of the browser.

initialize
    super initialize.
    path := OrderedCollection new.

Why is the line super initialize here? Even though the definition of class TestMorph is very simple, path is not the only instance variable that it has. In the second pane of the Browser, bring up the yellow button menu and choose inst var refs. You see a list of all the instance variables in TestMorph. Path is at the bottom, but above that is a list of other instance variables, which TestMorph inherits from Morph. There is no need right now to select any of them, but if you do, Squeak will show you all the methods that use that instance variable.

So, since Morph has a bunch of instance variables, there is a good bet that it will have an initialize method that sets some or all of them to good values. (Take a look at it if you wish; its fine to open several more browsers). When we define our own initialize method, we override the one in our superclass, class Morph. So unless we do something special, the initialize method in class Morph will no longer execute. This would be bad!

The "something special" is the super initialize. This executes the initialize method of our superclass, class Morph. When you send the message initialize to the receiver super, the message is really sent to self, but Squeak makes sure that the inherited version of initialize is invoked instead of the one that is executing.

After taking care of that small but vital detail, the guts of our initialize method is the assignment path := OrderedCollection new. This creates a new, empty, Collection and puts it into path. So, when our TestMorph receives the initialize message, it first does all the initialization that Morph would do, and then it does it's own initialization. The result is that all those instance variables inherited from class Morph will be initialized as well as the new one that you added.

We are actually using a convention here, since a new object is not automatically sent the initialize message. But if you look at the new method for class Morph, you will see that it always sends initialize to each new Morph instance. This is how many, many objects are coded.

Now, our original TestMorph, here on the screen, was not initialized with our new code: we made it before we defined our version of initialize. We can fix this in two ways: with an inspector, or by deleting the Morph and making a new one.

Using an Inspector

The simplest thing is just to delete the TestMorph. But before we do that, lets see what an inspector can do for us. Inspectors are really useful tools for looking at and changing individual objects, so it is worth getting to know what they can do.

Bring up the Halo on our TestMorph, click on the debug handle and select inspect morph from the debug menu. An inspector will attach itself to the mouse cursor. Put the inspector down in a convenient place. The left pane contains a list of all of the instance variables in our TestMorph, including path and those inherited from Morph. Click on path, and you will see (in the right pane) that its value is nil, that is, path has not been initialized.

You can fix this right now by selecting nil and replacing it with OrderedCollection new. Accept; what you typed will be evaluated, and the result, a new empty collection, will be assigned to path. The text will change to an OrderedCollection(), which is the way that empty collections print themselves.

By the way, the bottom pane of the inspector is a little workspace. You can type a Smalltalk expression there and do it or print it. The useful thing about this workspace is that, here, the pseudo-variable self names the object that you are inspecting. So you can type self color and print it and see the result of sending the color message to this very TestMorph.

Meanwhile, back to the Animation...

OK, so we are done with the inspector, and with our original TestMorph. Dismiss them both (using the x in the menu bar of the inspector, and the pink button with the x in the TestMorph's halo). Now get a new TestMorph (properly initialized this time) by executing:

TestMorph new openInWorld.

We're going to make our object do something different when you click the mouse on it. We've got the Collection in path. Type into the bottom pane of the browser and accept this method:

startAnimation
    path reset.
    0 to: 9 do: [:i | path add: self position + (0@(10 * i))].
    path := path, path reverse.
    self startStepping.

We are resetting our OrderedCollection in line 2, just to make sure that it is empty. In the next line, we're doing a block ten times. See if you can figure out what it is doing! Remember, 0@30 is a Point, with x coordinate 0 and y coordinate 30.

In line 3, we change the path to be (path, path reverse). I'm sure that you can guess what sending reverse to an OrderedCollection does! The comma (,) is just another message; it concatenates two collections.

What we've done is taken our list of ten points which are going from zero to ninety, and added on a list that goes from ninety back to zero. So now we've got twenty Points that start out and end up at the original position, and go to a bunch of places in between.

The last line of this method enrolls our morph in an engine that keeps sending it the message step. This is the "tick" of the animation engine. So now we must make our morph understand the step message.

step
    path size > 0 ifTrue: [self position: path removeFirst].

This one is pretty easy to understand. It says that as long as there is something in that list of points, that is, as long as the size of path is greater than zero, then move myself. path removeFirst does two things: it removes the first point from path, making path a collection with one less element, and it answers the Point that it just removed. So, the argument to self position: is the first point that was in the path. Sending myself the position: message changes my position.

Graphically, the effect is that our TestMorph it will jump to the next point; you will see the change on the next frame of the animation.

Starting the Animation

All that we need do now is actually kick our TestMorph off off by sending it the message startAnimation. One way to do this is to use a temporary variable in the Workspace. Type these lines in the Workspace, select them, and doIt.

t := TestMorph new openInWorld.
t startAnimation

You could also bring up an inspector on your TestMorph, and accept self startAnimation in the bottom pane.

A graphical alternative to all this typing is to make our TestMorph send itself the startAnimation message when the mouse button goes down.

mouseDown: evt
    self startAnimation.

There are two ways you can define this. You can paste it over any method showing in the bottom pane of the Browser. Or you can click on mouseDown: in the right pane of the Browser, and modify the existing mouseDown: method. Either way, the same thing happens when you accept.

When you've done that, go over and click on your morph, and you should see something happen. It's animating, which is great, but it's going very, very slowly. Don't be too disappointed; Squeak is faster than this! What is actually happening is that, by default, step is sent to an object once every second. But the object can decide how often it wants to be told to step by implementing the method stepTime.

stepTime
    ^ 50

This answers the time in milliseconds between step messages that the object is requesting. Now, obviously, you can ask for zero time between step messages, but you won't get it. Basically, stepTime is an assertion of how often you would like to be stepped.

Now if we click on our object we get a much faster animation. Change it to 10 milliseconds, and try to get a hundred frames a second. Let's see if that works. If you are really interested in knowing how fast your animation is running, see if you can find out how to record the times at which your Morph receives the step message.

More Realistic Motion

Now, this tutorial was originally written by John Maloney, who works for Disney, "The Animation Company". Our animation starts and stops instantly, but everyone at Disney knows that "slow in" and "slow out" are needed to make it look really good. Here is a modification that uses the square of i to start the motion off slowly.

startAnimation
    path reset.
    0 to: 29 do: [:i | path add: self position + (0@(i squared / 5.0))].
    path := path, path reversed.
    self startStepping.

Only one line in the startAnimation method needs to be changed. This code puts thirty points in the path, and then appends the reverse version to make sixty points. The other change is that instead of saying times ten, we say "i squared divided by five". At the end, the expression will be 841 (292) divided by 5.

This gives us an animation that starts down slowly and speeds up, and then slows down again as it comes back up. In fact, it looks a bit like like a ball bouncing.

More Morphs

Now would be a good time to play with some pre-made Morphic objects on your own. Let's get a parts bin full of objects. Click on the "Supplies" tab at the bottom of the screen. (If there is no tab at the bottom of the screen, use the World menu and choose "authoring tools...". Choose "standard parts bin".) In either case you have a bunch of objects that you can drag onto the screen. After you have dragged one out, blue-click to get a halo, and hold down on the red circle to get the object's content menu. Here are some of the Morphs to play with:

In this worksheet, you have been introduced to morphic programming. You probably noticed some of the other parts of the Morphic Halo that we did not talk about, like the eye and the rectangle buttons. These give access to an entirely different way of creating applications: not by programming, but by direct manipulation. Feel free to experiment! Worksheet 1b gives an example of building a complete application by direct manipulation — if you haven't had a chance to try it out yet, do so! There are also other tutorials available on the web that will introduce you to this style of application building.

A Final Example

Use a file list browser to find the file Hungary.gif. From the yellow-button menu, choose open image in a window. The resulting map is a full-fledged Morph (an instance of SketchMorph).

You should already have filed-in wkSheet.cs—if you have not, file it in now. In a workspace, type and execute

Hungary new openInWorld.

The result will be an instance of HungaryMorph! Superimpose it on the map. Use the Supplies tab to drag in other Morphs that help you to complete Hungary. For example, you can drag in a Text for Editing, change the text to read Budapest, and place it in the appropriate position on the map. Then use the red handle in the morphic halo to bring up a menu on the TextMorph, and embed ... it into Hungary. Now you will be able to move Hungary, and Budapest will move with it.