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!
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 objecttry dragging it. Resist the temptation to do too many other things to it yet; in particular, don't use the blue rotate handle.
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.
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 wheela 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 Squeakyou 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.
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.
The next thing to do is make clicking on our Morph invoke a more exciting kind of motionsome 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.
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.
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.
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.
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.
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.
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.
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.csif 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.