Difference between revisions of "W1521 Moving Along"

From Coder Merlin
Line 49: Line 49:
         canvas.render(strokeStyle, fillStyle, lineWidth, ellipse)
         canvas.render(strokeStyle, fillStyle, lineWidth, ellipse)
     }
     }
</syntaxhighlight>
At this point, we've created a {{SwiftClass|Ball}} class but we haven't '''instantiated''' an instance of it.  In order for it to participate in our {{SwiftClass|Scene}} we'll need to add it to a {{SwiftClass|Layer}}.  Because we'll later be interacting with the ball, let's add it to our {{SwiftClass|InteractionLayer}}.  Edit the code in {{Pathname|InteractionLayer.swift}} as follows:
<syntaxhighlight lang="swift" highlight="7,8">
class InteractionLayer : Layer {
    init() {
        // Using a meaningful name can be helpful for debugging
        super.init(name:"Interaction")
        // We insert our RenderableEntities in the constructor
        insert(entity: Ball(), at: .front)
    }
}
</syntaxhighlight>
</syntaxhighlight>



Revision as of 22:42, 10 January 2021

Within these castle walls be forged Mavens of Computer Science ...
— Merlin, The Coder
Animated Horse

Prerequisites[edit]

Research[edit]

Background[edit]

An event is an asynchronous occurrence to which our software may react through the implementation of an event handler. Events are often triggered by either the user (e.g. moving the mouse or pressing a key) or by the system (e.g. timers, screen refresh). Handling user-generated events in this manner enables our system to be more responsive, because our program has the opportunity to react to the event as it occurs. An alternative method involved polling, in which we periodically check to see if the state of a system has changed. For example, we can periodically check to see if a key has been pressed on the keyboard. Generally, polling leads to a less than ideal experience for the user. We may, for example, miss a keypress because we were busy doing something else, or we may respond much more slowly and not meet the user's expectations. In this lab, we'll be using the update event handler to refresh the objects drawn on the canvas and the onClick event to respond to a mouse click. The update event handler is invoked by the system periodically to refresh the canvas, while the onClick event is generated as a result of the user clicking the mouse.

Using the tools that we've learned to date, we're able to produce many types of still images. In this lab, we'll focus on defining objects which persist throughout our Igis session, enabling us to modify the properties of these objects to create simple animations.

Prepare[edit]

Create a new Scenes shell project within your Experiences directory:

ty-cam@codermerlin:~$  cd ~/Experiences

ty-cam@codermerlin:~/Experiences$  git clone https://github.com/TheCoderMerlin/ScenesShellBasic W1521


Enter the Sources/ScenesShell directory of the new project:

ty-cam@codermerlin:~/Experiences$  cd W1521/Sources/ScenesShell/


Start button green arrow
Run the program.

ty-cam@codermerlin:~/Experiences/W1521/Sources/ScenesShell$  run


Ensure that you are logged on to the wiki. Then, click on the Tools menu followed by right-clicking on IGIS and selecting the menu item Open in New Window or Open in New Tab.

You'll know you're successful if you see the title bar change to "Coder Merlin: IGIS". (The browser window will be blank because we haven't added any graphics yet.)

Hint.pngHelpful Hint
It's useful to bookmark this page in your browser.

Experiment[edit]

First Steps[edit]

Let's set start by creating a ball. Create a new file Ball.swift in emacs. Begin by defining the following properties on a new Ball class:

class Ball: RenderableEntity {
    let ellipse = Ellipse(center:Point(x:0, y:0), radiusX:30, radiusY:30, fillMode:.fillAndStroke)
    let strokeStyle = StrokeStyle(color:Color(.orange))
    let fillStyle = FillStyle(color:Color(.red))
    let lineWidth = LineWidth(width:5) 
}

This defines a new class with properties defining the visual aspects required to render a ball.

We'll need to initialize the ball in our init constructor. Create the constructor as follows:

    init() {
        // Using a meaningful name can be helpful for debugging
        super.init(name:"Ball")
    }

The setup method is invoked once, when our canvas is first setup. We'll use this opportunity to set the initial position of the ellipse. Create a new method, setup, as follows:

    override func setup(canvasSize: Size, canvas: Canvas) {
        // Position the ellipse at the center of the canvas
        ellipse = canvasSize.center
    }


Finally, we'll need to render the ball when requested. We'll do that in the render method. The render method is invoked frequently for as long as our application is running. Add a render method as follows:

    override func render(canvas:Canvas) {
        canvas.render(strokeStyle, fillStyle, lineWidth, ellipse)
    }


At this point, we've created a Ball class but we haven't instantiated an instance of it. In order for it to participate in our Scene we'll need to add it to a Layer. Because we'll later be interacting with the ball, let's add it to our InteractionLayer. Edit the code in InteractionLayer.swift as follows:

class InteractionLayer : Layer {

    init() {
        // Using a meaningful name can be helpful for debugging
        super.init(name:"Interaction")

        // We insert our RenderableEntities in the constructor
        insert(entity: Ball(), at: .front)
     }
 }
Start button green arrow
Run the program and view in a browser before continuing. Be sure that you understand the role of each method.

Mouse Click Events[edit]

Hint.pngHelpful Hint
You might want to review W1513 Patterns of Patterns to refresh your memory with regard to event handling.

Declare Conformance to Protocol[edit]

class Background : RenderableEntity, EntityMouseClickHandler {
   ...
}

Notify Dispatcher[edit]

    override func setup(canvasSize:Size, canvas:Canvas) {
        // ... other code is here ...
        dispatcher.registerEntityMouseClickHandler(handler:self)
    }

    override func teardown() {
        dispatcher.unregisterEntityMouseClickHandler(handler:self)
    }

Implement Event Handler[edit]

Let's add another method, onEntityMouseClick, which is invoked when the mouse button is clicked. The method provides the location on the canvas at the position that the mouse was clicked. We can use the location to move the ball from its original, center position to the location that the mouse was clicked.

    func onEntityMouseClick(globalLocation: Point) {
        ball.center = globalLocation
    }

Note the manner in which we took advantage of objects by assigning an instance of Point directly to the ball's center, which is also a property of type Point.

Define the BoundingRect[edit]

    override func boundingRect() -> Rect {
        if let rect = rect {
            return rect
        } else {
            return Rect()
        }
    }


Start button green arrow
Run the program and view in a browser before continuing.
Did the ball move to the new location? If not, what happened? Why?

Clear the Canvas[edit]

Let's add another flag to track whether or not we need to draw the ball. We only need to draw it if it's moved. If it hasn't moved, then there's no reason to redraw it. Add the following property (below "var coordinateSet = false"):

    var needToDraw = true

We initialize this property to true because at this point, we haven't yet drawn the ball. Each time we do draw the ball, we'll set this property to false. We won't change the flag to true unless something has changed. Modify the update method so that we check the flag before painting the ball and subsequently clear the flag:

        if needToDraw {
                canvas.paint(ball)
                needToDraw = false
        }

We'll also need to modify the onClick method to set this flag to true, indicating that we'll need to draw the ball (because we've changed the center).

    override func onClick(location:Point) {
        ball.center = location
        needToDraw = true
    }


Finally, we'll need to clear the canvas. We do this by painting a rectangle and specifying the .clear fill mode. Let's define a helper function for this purpose:

    func clearScreen(canvas:Canvas, canvasSize:Size) {
        let rect = Rect(topLeft:Point(x:0, y:0), size:canvasSize)
        let rectangle = Rectangle(rect:rect, fillMode:.clear)
        canvas.paint(rectangle)
    }

Of course, we'll need to invoke the function. The best place to do this is immediately before we paint the ball. Add the following line immediately before the ball is painted:

                clearScreen(canvas:canvas, canvasSize:canvasSize)
Start button green arrow

Run the project.

View the results in the browser. Did the ball move to the new location? If not, what happened? Why?

Tracking the Mouse[edit]

Clicking the mouse can move the ball to that location, but we can also have the ball track the mouse. We'll simply use a different method, named onMouseMove. This method is invoked whenever the mouse is moved on the canvas. Change the name of the onClick method to onMouseMove. No other changes are necessary.

Start button green arrow

Run the project.

View the results in the browser. Does the ball track the mouse? How is this behavior different from the situation when we were using onClick?

Exercises[edit]

  1. Change the background of the animation from white to a sky of blue and grass of green

Key Concepts[edit]

  • An event is an asynchronous occurrence to which our software may react through the implementation of an event handler
    • Events may be triggered by the system
      • Timers
      • Screen refresh
    • Events may be triggered by a user
      • Mouse movement
      • Mouse clicks
      • Keypresses
  • Flags are Boolean values that indicate the state of a binary situation
    • clearing a flag means setting it to false
    • setting a flag means setting it to true