HOMEWORK 4: Dynamic Visual Effects

This is an INDIVIDUAL assignment.

Objective

In this assignment we'll explore some techniques for creating dynamic visual effects in user interfaces. Here, we'll learn how to create a visual page turn effect to make the Courier application have more of the look and feel of a real paper notebook.

The learning goals for this assignment are:

Description

In this homework, we'll add a visual page turning effect to the Courier application. After the end of the previous homework, you should have two mechanisms for navigating between pages: the next/previous page buttons, and the gestures you added in Homework 3. Currently, however, these controls just trigger an "instantaneous" page turn, in which the previous page vanishes and the new page appears. To make the Courier application have more of the look-and-feel of a real paper notebook, we'll be adding a visual effect that makes it look like a page of paper is turning, as well as a new user input mechanism for turning pages. The effect should look much like what you see in the Courier video at around 1:24 and elsewhere (and should be familiar if you use tools like iBooks or the Kindle app).

The requirements for this assignment are as follows:

Here's an image from a book reader application of what the page turn effect should look like in it's "basic" mode. Note that effectively what's happening here is that a rectangular region is being drawn over the current page, sweeping from right to left, revealing the next page beneath:

Implementing the Page Turn Dynamics

As an implementation strategy for this assignment, I'd suggest starting by creating the basic page turning animation, and connect it to your buttons and gestures, before starting on the more interactive version that allows the user to dynamically drag a page.

Take a look at the image above. You'll see that the image is showing something we haven't seen before in a Swing application: both the current page component and the underlying page component are displayed, as well as the semi-rectangular area between them representing the turning page. How can you display two components plus some other stuff at the same time? How do you occlude the current page component with the rectangular turn area and let the page underneath "show through?"

In order to do this, a key goal of this assignment is to explore a common mechanism called "off screen rendering" that's used by Swing and other toolkits.

Off-screen Rendering

Let's look at how off-screen rendering works, before moving on to how we'll use it with animation to accomplish the page turning effect.

The basic idea with off-screen rendering is that we'll "capture" the image of both our current page, and the underlying page, in an off-screen buffer. We do this by causing the page components to draw themselves to the buffer, rather than to the screen. Then we can combine these buffers along with other graphics during our animation.

How do we get a component to draw itself onto an offscreen buffer? Well, first we create the buffer itself. Next, Swing lets us get a Graphics object for drawing into that buffer. Then, we just call the component's normal paint() code, passing that graphics object to it. Essentially, we're just causing the component's normal painting code to be called, but this time the component is rendering onto the graphics object we've passed to it (which causes the drawing to go to the buffered image) rather than to the screen.

Here's a snippet of code that will do this for you. You can pass in any component as the source, and this code will create a BufferedImage of the same size as the source, render the source component into it, and return it. (FYI, there are less verbose ways of creating an image and rendering a component into it, but this is the safest and most portable):

import java.awt.image.BufferedImage;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsEnvironment;

public BufferedImage makeOffscreenImage (JComponent source) {
    // Create our BufferedImage and get a Graphics object for it
    GraphicsConfiguration gfxConfig = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
    BufferedImage offscreenImage = gfxConfig.createCompatibleImage(source.getWidth(), source.getHeight();
    Graphics2D offscreenGraphics = (Graphics2D) offscreenImage.getGraphics();
    
    // Tell the component to paint itself onto the image
    source.paint(offscreenGraphics);
    
    // return the image
    return offscreenImage;
}
Essentially what this code is doing is: 1) We pass it a JComponent called source, which is the component we want to render to an off-screen buffer. 2) We get the "graphics configuration" for the current screen, which is basically just an object that contains a bunch of information about resolution, color capabilities, etc., of the current screen. 3) Then, we use this to create a BufferedImage, called offscreenImage. This is just an image buffer with the same width and height of the source component. 4) We get the graphics object for the image, by calling getGraphics. 5) Then we just tell the source component to paint itself onto this graphics object, using its normal paint() method (which calls paintComponent(), paintChildren, and paintBorders()). This causes the source component's normal painting code to be called but this time, the component is rendering onto the graphics object we've passed to it (which causes the drawing to go to the buffered image) rather than to the screen. Finally, 6) we return this buffered image.

Using code like the snippet above, you can capture an image of both your current page, and the underlying page, just before you start the page turn animation. This will make drawing the animation easier.

Next, let's see how we'll use this in our animation.

Animation and the Repaint Cycle

There are several things you need to do in order to create the animation, which will use the off-screen rendering trick described above.

The first change you'll need to make is that the listener code for your next and previous buttons, and the gesture recognizer, will no longer "instantly" change the page. Instead, they'll kick off the animation process for page turning. So, you'll need to update the code in these listeners, and this part of your recognizer.

Your new code should first set a flag that you'll use to keep track of the fact that you're in the midst of "page turning mode," and then set up a Swing timer that will fire periodically, say once every tenth of a second, for five or so iterations (giving a total animation time of 0.5 seconds).

What will this timer do? Before the first iteration, it should capture an image of the current and underlying pages; these images will be used throughout the animation cycle. It should also set up some variables that you create that will indicate the current position of the turned page for that iteration; you'll use these to keep track of how much of the current and underlying page images to display, and where the turning page rectangular graphic currently is.

Then, in each successive iteration of the animation, the code will update the variables that indicate the current position in the animation, and call repaint() to trigger the drawing of the current iteration in the animation.

Finally, after the last iteration of the animation, the page change is now complete. The timer can unset the flag that indicates you're in the midst of a page change animation. Also, once the animation is completed, you can also remove the current page from your layout and add the next or previous one; be sure to call revalidate() and repaint() so that the application correctly updates.

A key to creating the page turn animation is to realize that--just like with all drawing in Swing--the actual drawing of the animation has to happen in the paintComponent() method of your components. In your paintComponent code, you can look at your "page turning mode" flag to determine if you should draw your component normally, or instead draw the current iteration of your animation if you're in the middle of a page turn.

If you're in the middle of a page turn, your paintComponent() method should draw a portion of the current component (taken from the offscreen image you stored previously), the semi-rectangular region that represents the turning page, and a portion of the underlying component (also taken from the offscreen image you stored previously).

To draw a portion of a BufferedImage to the screen, you can do something like the following:

public void paintComponent(Graphics g) {
	// ...
	
	BufferedImage portion = offscreenImage.getSubimage(x, y, width, height);
	g.drawImage(portion, 0, 0, this);
}
getSubImage() returns a rectangular sub-region of the buffered image that you call it on. Then, you can use the drawImage() method on the Graphics object that gets passed to your paint component to render this image onto the screen. (The first two parameters to drawImage() are the coordinates at which the image will be drawn within your component; the last parameter is typically the current JComponent.)

You can use code like this to "mix" the selected regions of both your current and underlying page images, as well as the page turning rectangle; the specific coordinates at which these get mixed will be determined by the variables that indicate the current position in the animation cycle, set via successive iterations in your timer.

In essence, the strategy here is that the current page component is responsible for drawing the entire animation cycle up until it is finished, at which point it is replaced by the underlying component.

Interactive Page Switching

Once you've got the animated page switching working via the next/previous buttons and gestures, it should be pretty straightforward to add the interactive page dragging behavior. Basically, the way this feature should work from the user experience perspective is that the user right-clicks and drags the mouse near the edge of the page to start page turning. But rather than complete the page turn with an animation, the page turn stays in progress until the user releases the mouse; as the user drags, the amount of exposed area changes dynamically. In other words, once you've "grabbed" a page, you can move back and forth, changing the position of the semi-rectangular turned page area, as well as the relative proportions of the current and exposed page.

The user completes the page turning by releasing the right mouse button. If the user releases before dragging at least half of the page width, the page "snaps back" into place and the turn is aborted. If the user releases after dragging at least half the page width, however, the page turn completes. In both cases, the snap back and the completion should be animated from the point where the user releases the mouse button.

Almost all of the code you've written for the first part of the assignment should be tweakable to do dynamic page turning. Basically, when you're in "dynamic page turning mode" (detected by a right click and drag within some small number of pixels from either the left or right edge), you update the variables that control the position of the rectangular area and the displayed regions of the current and underlying pages from the current mouse position, rather than via iteration through the animation cycle. Your paint code stays the same, combining the two off-screen regions and the page turn rectangle depending on these variables. Then, when the mouse is released, you start up a Swing timer that iterates through an animation cycle starting from the current position.

Extra Credit

As usual, there are a lot of ways you might make this assignment much fancier than described:

Deliverable

See here for instructions on how to submit your homework. These instructions will be the same for each assignment.