Skip to main content
Engineering LibreTexts

13.4: Creating and Drawing Your Own Morphs

  • Page ID
    39650
    \( \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}}\)\(\newcommand{\AA}{\unicode[.8,0]{x212B}}\)

    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.

    Using the browser, define a new class CrossMorph inheriting from Morph:

    Morph subclass: #CrossMorph
        instanceVariableNames: ''
        classVariableNames: ''
        package: 'PBE-Morphic'
    

    We can define the drawOn: method like this:

    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.

    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.

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

    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.

    Define the following method in class CrossMorph:

    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 the two methods above.

    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 horizontalBar 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:

    CrossMorph >> horizontalBar
        | crossHeight |
        crossHeight := self height / 3.0.
        ^ self bounds insetBy: 0 @ crossHeight
    
    CrossMorph >> verticalBar
        | crossWidth |
        crossWidth := self width / 3.0.
        ^ self bounds insetBy: crossWidth @ 0
    

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

    CrossMorph >> drawOn: aCanvas
        aCanvas fillRectangle: self horizontalBar color: self color.
        aCanvas fillRectangle: self verticalBar color: self color
    
    CrossMorph >> 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}\).

    The cross-shaped morph with filled center.
    Figure \(\PageIndex{2}\): The center of the cross is filled twice with the color.

    Execute the following code in a playground, line by line:

    CrossMorph new openInWorld;
        bounds: (0@0 corner: 200@200);
        color: (Color blue alpha: 0.4)
    

    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:

    CrossMorph >> 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 ]
    
    The cross-shaped morph with a row of unfilled pixels.
    Figure \(\PageIndex{3}\): The cross-shaped morph, showing a row of unfilled pixels.

    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.

    CrossMorph >> horizontalBar
        | crossHeight |
        crossHeight := (self height / 3.0) rounded.
        ^ self bounds insetBy: 0 @ crossHeight
    
    CrossMorph >> verticalBar
        | crossWidth |
        crossWidth := (self width / 3.0) rounded.
        ^ self bounds insetBy: crossWidth @ 0
    

    This page titled 13.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 via source content that was edited to the style and standards of the LibreTexts platform; a detailed edit history is available upon request.