
Graphing Calculator, Part 2
Beware! This document needs cleaning up. Formatting may be messy or broken. Text may be inaccurate or in need of editing.
You can look at this preview, but be aware that the content will change before the instructor assigns this to the class.
This document is the result of an automated conversion. You can view the original version but again beware: that version too is not necessarily what will eventually be assigned.
Learning goals
The goal of this activity is to learn about how programs allow users to interact with them by handling input events.
You will continue working with the same graphing calculator as in the previous activity, add a user interface to let the user control the view and animation.
**(This is the same GitHub Classroom assignment and link as part 1. Continue working with the same partner(s). No need to pull a new activity, please work from the same repository)**
Here is the** Kilt-Graphics Documentation**; you’ll need it!
Get set up
You need the graphing calculator showing some animated functions. If you didn’t get that far in the previous activity, or if your functions are messy, or if you just want to see something cool, copy the following code into GraphingCalculator
’s main method (and run it to make sure it works):
for (int n = 1; n < 12; n++) {
double base = n * 0.1 + 1.5;
calc.show((x, t) -> {
double result = 0;
for (int i = 1; i < 20; i++) {
result += Math.sin(x * Math.pow(base, i) - t * i * 3)
/ Math.pow(base, i);
}
return result;
});
}
Buttons
Let’s add two buttons to the graphing calculator that will make it zoom in and out. In GraphingCalculator
’s constructor (not the main method, the constructor!), create two Button
objects with the titles “Zoom In” and “Zoom Out.” Add them to the canvas and position them in a corner, one next to the other.
⚠️WARNING ⚠️ Make sure you import the correct class,
edu.macalester.graphics.ui.Button
, and not some other class named “Button.” There are several Buttons floating around in Java’s universe, and the activity won’t work if you import the wrong one.
You can double check which classes you’ve imported by looking at the
import
statements at the top of the file, or by command-clicking (macOS) or ctrl-clicking (Windows) on the wordButton
in the code.
Run your code. You should see your two buttons floating above the animated graph — and see them respond when you click them! But they don’t do anything yet. You need to say what should happen when the button gets clicked.
Event handlers
Add the following code:
zoomIn.onClick(() -> setScale(getScale() * 1.5));
Run your code again, and you should be able to enlarge the plot by clicking the Zoom In button.
(Note that we have to use the setter method instead of setting the scale
instance variable directly, so that the plot will update.)
Let’s take a moment to analyze that code. You create a zero-argument lambda that calls setScale()
. That lambda does not run immediately. Instead, you pass the lambda to the onClick
method of the zoomIn
button. The button holds on to the lambda, and runs it whenever the button is clicked.
This is a classic example of what a lambda is good for. It lets you say what should happen, but lets other code decide when it happens. Note that the lambda depends on local context: it calls methods of this GraphingCalculator
object, which the lambda can do because it captures the implicit this
variable. The lambda thus serves to tie an event from the outside world — in this case, a button click — back to this code’s local world.
Add code to make the zoomOut
button work too. Test it and make sure it works.
(Should you add the button event handlers before or after the call to canvas.animate()
? Does it make any difference? Nope! Why? Discuss with your partner.)
Mouse events
Temporarily comment out the whole call to canvas.animate()
. We will enable animation again later, but for now we want it off.
Add the following code to the constructor:
canvas.onDrag(event ->
setAnimationParameter(
event.getPosition().getX() / width));
This tells the canvas what should happen when the user drags the mouse over the canvas. (“Drag” means moving the mouse while the button is held down.) The lambda you pass to onDrag
gets called repeatedly as the mouse moves. In this case, we take the x position of the mouse and use it to change the animation parameter.
Note that this lambda takes one parameter. It is a MouseMotionEvent
. (You can look at that class’s API in your IDE, or in the kilt-graphics javadoc.) Event handlers often receive event objects that describe the event that caused the handler to be called.
Run the code and try it! You should be able to manually animate the plot by dragging the mouse back and forth.
Dealing with coordinates
Note that we are using the absolute x position, which means that the animation parameter jumps suddenly to wherever you start dragging. Try dragging the plot all the way to the right, releasing the mouse button, moving the mouse all the way to the left, and then dragging again. Note the sudden jump when you start dragging again.
It would be nice if instead our event handler started the animation parameter from its current value, then added the relative motion of the mouse to it. The event
object has a getDelta()
method that can help you. It returns the difference between the mouse’s current position and its position at the previous mouse event. You can use it like this:
- Use
getDelta()
instead ofgetPosition()
in the event handler, so you are computing how much the animation parameter should change instead of computing its new value. - Now add that change to the animation parameter’s current value.
Hint: event.getDelta().getX()/width + getAnimationParameter()
Once you’ve done this, when you do the drag-lift-move-drag sequence described at the beginning of this section, the plot should move smoothly.
Mouse button events
Try uncommenting the animation code again. Can it animate and drag at the same time? Let’s find out! Run the program and see.
Note how janky the motion is when you drag in the opposite direction of the animation. What is happening is that Java is rapidly alternating calls to the lambda passed to animate()
and calls to lambda passed to onDrag(). If the two lambdas move the animation parameter in opposite directions, you see it twitching back and forth rapidly:
animate increase animationParameter
animate increase animationParameter
animate increase animationParameter
animate increase animationParameter
mouseDrag decrease animationParameter ← dragging starts here
animate increase animationParameter
mouseDrag decrease animationParameter
animate increase animationParameter
animate increase animationParameter
mouseDrag decrease animationParameter
animate increase animationParameter
To fix this, we’d like to stop the animation when the user starts dragging, and resume it when they finish.
First, let’s make a boolean flag that can start and stop animation. Declare a new boolean instance variable named animating
, and change the animation lambda so that it only does something when animating
is true. (Hint: to put an if statement in the lambda, you will need to convert it to a multi-statement lambda.)
Test your change. Try initializing animating
to false and run the program, and you should see no animation. Now try initializing it to true, and you should see animation.
That working? There are two event handling methods in CanvasWindow
called onMouseDown()
and onMouseUp()
that can notify you when the mouse button goes down and up, respectively. Add a mouse down handler that sets animating
to false, and a mouse up handler that sets it to true. Now the sequence of events looks something like this:
animate animating is true, so change animationParameter
animate animating is true, so change animationParameter
mouseDown set animating to false
animate animating is false, so do nothing
mouseDrag change animationParameter using mouse event’s delta x
animate animating is false, so do nothing
mouseDrag change animationParameter using mouse event’s delta x
animate animating is false, so do nothing
animate animating is false, so do nothing
mouseDrag change animationParameter using mouse event’s delta x
animate animating is false, so do nothing
mouseUp set animating to true
animate animating is true, so change animationParameter
animate animating is true, so change animationParameter
Run your program and try it! The animation should freeze when you press the mouse button, allowing you to drag smoothly, then resume when you lift the mouse button.
Super amazing bonus challenge tasks
Implement iPhone-style “coasting”
Instead of having the animation always move in the same direction at the same speed, we could use the mouse movement to make the plot continue coasting in the speed and direction the user was dragging. Here is a sketch of how to do it:
- Instead of making the animation increment by a constant as the current code does, create an instance variable named
animationSpeed
, initialize it to 0.01, and use that in the animation lambda. Test that; animation should look the same. - In the
onDrag
handler, in addition to callingsetAnimationParameter()
, take the same quantity that you just added to the animation parameter, take the average of that quantity with the currentanimationSpeed
, and make that the new value ofanimationSpeed
. Test that; you should now be able to make the animation coast in either direction. - Did you end up with a big repeated subexpression in two places? If so, try extracting it to a local variable inside the lambda. Test again after to make sure it still works.
(The actual iPhone algorithm is more sophisticated than this, but this simple one feels reasonably nice.)
Implement panning
It would be nice to be able to move the view around, wouldn’t it?
Alter your onDrag
handler so that it does two different things:
- If the shift key is pressed, add the mouse event’s delta to the
origin
instance variable. (Note that they are bothPoint
s andPoint
has anadd()
method, so you do not need to add the x and y components separately.) - If the shift key is not pressed, do what you are currently doing.
Hint: use the getModifiers()
method of event
to see what modifier keys are pressed. This will take some API exploring! See if you can figure it out. Here are some hints:
You can use the constant ModifierKey.SHIFT
to refer to the shift key.
getModifiers()
returns a Set
. Find the Javadoc for that class. How do you test whether a particular value is in a Set
?
Make zoom preserve the visible center
The current zoom code zooms the graph centered on the origin. If you have panned far away from the origin using the shift key, pressing the zoom button now makes the graph jump quite a bit! Try altering the zoom code to preserve the current center of the screen, whatever it is.
This is tricky! Here are some hints:
Use the methods of Point
instead of doing math directly with x and y coordinates.
You want to move origin
closer to or farther from the current center of the canvas by the same factor you multiplied the scale by.
You can use canvas.getCenter()
to get the current center of the canvas.
Subtract the canvas center from origin
, then scale that, then add the center back.