Skip to main content
Engineering LibreTexts

11.4: Creating and Drawing Your Own Morphs

  • Page ID
    36393
  • \( \newcommand{\vecs}[1]{\overset { \scriptstyle \rightharpoonup} {\mathbf{#1}} } \) \( \newcommand{\vecd}[1]{\overset{-\!-\!\rightharpoonup}{\vphantom{a}\smash {#1}}} \)\(\newcommand{\id}{\mathrm{id}}\) \( \newcommand{\Span}{\mathrm{span}}\) \( \newcommand{\kernel}{\mathrm{null}\,}\) \( \newcommand{\range}{\mathrm{range}\,}\) \( \newcommand{\RealPart}{\mathrm{Re}}\) \( \newcommand{\ImaginaryPart}{\mathrm{Im}}\) \( \newcommand{\Argument}{\mathrm{Arg}}\) \( \newcommand{\norm}[1]{\| #1 \|}\) \( \newcommand{\inner}[2]{\langle #1, #2 \rangle}\) \( \newcommand{\Span}{\mathrm{span}}\) \(\newcommand{\id}{\mathrm{id}}\) \( \newcommand{\Span}{\mathrm{span}}\) \( \newcommand{\kernel}{\mathrm{null}\,}\) \( \newcommand{\range}{\mathrm{range}\,}\) \( \newcommand{\RealPart}{\mathrm{Re}}\) \( \newcommand{\ImaginaryPart}{\mathrm{Im}}\) \( \newcommand{\Argument}{\mathrm{Arg}}\) \( \newcommand{\norm}[1]{\| #1 \|}\) \( \newcommand{\inner}[2]{\langle #1, #2 \rangle}\) \( \newcommand{\Span}{\mathrm{span}}\)

    While it is possible to make many interesting and useful graphical representations by composing morphs, sometimes you will need to create something completely different. To do this you define a subclass of Morph and override the drawOn: method to change its appearance.

    The morphic framework sends the message drawOn: to a morph when it needs to redisplay the morph on the screen. The parameter to drawOn: is a kind of Canvas; the expected behaviour is that the morph will draw itself on that canvas, inside its bounds. Let’s use this knowledge to create a cross-shaped morph.

    \(\bigstar\) Using the class browser, define a new class CrossMorph inheriting from Morph:

    Code \(\PageIndex{1}\) (Squeak): Defining CrossMorph

    Morph subclass: #CrossMorph
        instanceVariableNames: ''
        classVariableNames: ''
        poolDictionaries: ''
        category: 'SBE--Morphic'
    

    We can define the drawOn: method like this:

    Code \(\PageIndex{2}\) (Squeak): Drawing a CrossMorph

    drawOn: aCanvas
        | crossHeight crossWidth horizontalBar verticalBar |
        crossHeight := self height / 3.0 .
        crossWidth := self width / 3.0 .
        horizontalBar := self bounds insetBy: 0 @ crossHeight.
        verticalBar := self bounds insetBy: crossWidth @ 0.
        aCanvas fillRectangle: horizontalBar color: self color.
        aCanvas fillRectangle: verticalBar color: self color
    

    Sending the bounds message to a morph answers its bounding box, which is an instance of Rectangle. Rectangles understand many messages that create other rectangles of related geometry; here we use the insetBy: message with a point as its argument to create first a rectangle with reduced height, and then another rectangle with reduced width.

    A CrossMorph with its halo.
    Figure \(\PageIndex{1}\): A CrossMorph with its halo; you can resize it as you wish.

    \(\bigstar\) To test your new morph, execute CrossMorph new openInWorld.

    The result should look something like Figure \(\PageIndex{1}\):. However, you will notice that the sensitive zone — where you can click to grab the morph — is still the whole bounding box. Let’s fix this.

    When the Morphic framework needs to find out which Morphs lie under the cursor, it sends the message containsPoint: to all the morphs whose bounding boxes lie under the mouse pointer. So, to limit the sensitive zone of the morph to the cross shape, we need to override the containsPoint: method.

    \(\bigstar\) Define the following method in class CrossMorph:

    Code \(\PageIndex{3}\) (Squeak): Shaping the Sensitive Zone of the CrossMorph

    containsPoint: aPoint
        | crossHeight crossWidth horizontalBar verticalBar |
        crossHeight := self height / 3.0.
        crossWidth := self width / 3.0.
        horizontalBar := self bounds insetBy: 0 @ crossHeight.
        verticalBar := self bounds insetBy: crossWidth @ 0.
        ↑ (horizontalBar containsPoint: aPoint)
            or: [verticalBar containsPoint: aPoint]
    

    This method uses the same logic as drawOn:, so we can be confident that the points for which containsPoint: answers true are the same ones that will be colored in by drawOn. Notice how we leverage the containsPoint: method in class Rectangle to do the hard work.

    There are two problems with the code in Code \(\PageIndex{2}\) and \(\PageIndex{3}\). The most obvious is that we have duplicated code. This is a cardinal error: if we find that we need to change the way that horizonatalBar or verticalBar are calculated, we are quite likely to forget to change one of the two occurrences.

    The solution is to factor out these calculations into two new methods, which we put in the private protocol:

    Code \(\PageIndex{4}\) (Squeak): horizontalBar

    horizontalBar
        | crossHeight |
        crossHeight := self height / 3.0.
        ↑ self bounds insetBy: 0 @ crossHeight
    

    Code \(\PageIndex{5}\) (Squeak): verticalBar

    verticalBar
        | crossWidth |
        crossWidth := self width / 3.0.
        ↑ self bounds insetBy: crossWidth @ 0 
    

    We can then define both drawOn: and containsPoint: using these methods:

    Code \(\PageIndex{6}\) (Squeak): Refactored CrossMorph»drawOn

    drawOn: aCanvas
        aCanvas fillRectangle: self horizontalBar color: self color.
        aCanvas fillRectangle: self verticalBar color: self color 
    

    Code \(\PageIndex{7}\) (Squeak): Refactored CrossMorph»containsPoint

    containsPoint: aPoint
        ↑ (self horizontalBar containsPoint: aPoint)
            or: [self verticalBar containsPoint: aPoint]
    

    This code is much simpler to understand, largely because we have given meaningful names to the private methods. In fact, it is so simple that you may have noticed the second problem: the area in the center of the cross, which is under both the horizontal and the vertical bars, is drawn twice. This doesn’t matter when we fill the cross with an opaque colour, but the bug becomes apparent immediately if we draw a semi-transparent cross, as shown in Figure \(\PageIndex{2}\).

    \(\bigstar\) Execute the following code in a workspace, line by line:

    m := CrossMorph new bounds: (0@0 corner: 300@300).
    m openInWorld.
    m color: (Color blue alpha: 0.3).
    

    The fix is to divide the vertical bar into three pieces, and to fill only the top and bottom. Once again we find a method in class Rectangle that does the hard work for us: r1 areasOutside: r2 answers an array of rectangles comprising the parts of r1 outside r2. Here is the revised code:

    The center of the cross is filled twice with the colour.
    Figure \(\PageIndex{2}\): The center of the cross is filled twice with the colour.
    The cross-shaped morph, showing a row of unfilled pixels.
    Figure \(\PageIndex{3}\): The cross-shaped morph, showing a row of unfilled pixels.

    Code \(\PageIndex{8}\) (Squeak): The Revised drawOn: Method, Which Fills the Center of the Cross Once

    drawOn: aCanvas
        | topAndBottom |
        aCanvas fillRectangle: self horizontalBar color: self color.
        topAndBottom := self verticalBar areasOutside: self horizontalBar.
        topAndBottom do: [ :each | aCanvas fillRectangle: each color: self color]
    

    This code seems to work, but if you try it on some crosses and resize them, you may notice that at some sizes, a one-pixel wide line separates the bottom of the cross from the remainder, as shown in Figure \(\PageIndex{3}\). This is due to rounding: when the size of the rectangle to be filled is not an integer, fillRectangle: color: seems to round inconsistently, leaving one row of pixels unfilled. We can work around this by rounding explicitly when we calculate the sizes of the bars.

    Code \(\PageIndex{9}\) (Squeak): CrossMorph»horizontalBar with Explicit Rounding

    horizontalBar
        | crossHeight |
        crossHeight := (self height / 3.0) rounded.
        ↑ self bounds insetBy: 0 @ crossHeight
    

    Code \(\PageIndex{10}\) (Squeak): CrossMorph»verticalBar with Explicit Rounding

    verticalBar
        | crossWidth |
        crossWidth := (self width / 3.0) rounded.
        ↑ self bounds insetBy: crossWidth @ 0

    This page titled 11.4: Creating and Drawing Your Own Morphs is shared under a CC BY-SA 3.0 license and was authored, remixed, and/or curated by Andrew P. Black, Stéphane Ducasse, Oscar Nierstrasz, Damien Pollet via source content that was edited to the style and standards of the LibreTexts platform; a detailed edit history is available upon request.