Java page peel effect

For years I have been impressed by the nice page peel effect as seen in numerous online flash applications used to display product folders or other types of documents. Never have I found a good explanation on how this effect is achieved, let alone detailed instructions on how to reproduce this myself. Sparked by Chet and Romains brilliant book “Filthy Rich Clients” I ventured out to create a filthy rich feature myself. For me this was the obvious choice.

The effect

After careful analysis of several flash implementations of the page peel effect I decided that a convincing look could be achieved in a few fairly simple steps. Below is a plain example of what the effect looks like. The front page is white and the underlying page is a solid grey. The backside of the front page is cyan.


Plain view of the peel effect

The effect is created using two polygons and three gradients.
One polygon represents the backside of the front page, the second is the part that is exposed of the underlying page.
One linear gradient is used to create a shadow effect on the front page. The second gradient is to higlight the peeled section of the page. And last gradient draws a shadow over the underlying page. The key is to draw everything in the right place in the right order.

Gatherering key elements

In order to draw the peel effect we need to define the two polygons that make up part of the effect.
Below is a schematic representation of the peel effect when in use. The user is currently dragging the front page with the mouse pointer at location M. The two polygons are identified T1 and T2.
T1 being the backside of the front page and T2 being the exposed part of underlying page.

Schematic representation of the peel effect

When we are peeling only two things are known at the time. The first is the location of the corner we are peeling from, the origin, point O. The second is the current mouse location, where the tip of the page we're peeling is located,  point M. The front page is folding over the line through points A and B, which is halfway and orthogonal to the line between points O and M. To determine points A and B we can use the following procedure.

Mx is the distance between O and M on the x-axis, My is the distance between O and M on the y-axis.

The function for the line through points O and M is:

y = My/Mx * x + b

Where b can be solved by entering known point M into the equation:

b = -My/Mx * M.x + M.y

The the line through A and B is orthogonal to the line through O and M. So the function for that line is:

y = 1 / (My/Mx) * x + b_orth

b_orth can be solved by using known point D, which is exactly half way between O and M.
With the function for the line through A and B we can find points A and B by solving the formula using the known y position for point A, which is O.y and the known x position for point B, which is O.x.

With points O, M, A and B know we can now contruct the two po lygons T1 and T1.
T1 consists of points M, A and B and T2 consists of O, A and B.

This is what this looks like in Java code:

double mx = O.x - M.x;
double my = O.y - M.y;
double rc = my / mx;

// Find b as y = rc * x + b
double b = -rc * M.x + M.y;

double Dx = M.x + (O.x - M.x) / 2.0;
double Dy = M.y + (O.y - M.y) / 2.0;

// Line through A and B → y = 1/rc * x + b
double b_orth = (1.0 / rc) * Dx + Dy;

int Ax = (int) ((O.y - b_orth) / (-1.0 / rc));
int By = (int) ((-1.0 / rc) * O.x + b_orth);

Point A = new Point(Ax, O.y);
Point B = new Point(O.x, By);

Now the polygons T1 and T2 are constructed using the calculated points

Polygon T1 = new Polygon(new int[] { M.x, A.x, B.x }, new int[] { M.y, A.y, B.y }, 3);
Polygon T2 = new Polygon(new int[] { B.x, A.x, O.x }, new int[] { B.y, A.y, O.y }, 3);

Drawing the effect.

With polygons T1 and T2 known we can now put it together by drawing each element in the following order

  1. Draw the current page
  2. Draw the shade gradient over the current page.
  3. Calculate the rotation for the image on the backside
  4. Draw the image on the backside into polygon T1
  5. Draw the highlight gradient into polygon T1
  6. Draw the image of the next page into polygon T2
  7. Draw the shade gradient over the next page
  8. Draw a masking line over the fold

The next section provides the code examples for each of these steps.

1. Draw the current page

g2.drawImage(currentPage, 0, 0, bnds.width, bnds.height, null);

2. Draw the shade gradient over the current page

The gradient flows from point M to point D

if (((int) Dx != M.x) && ((int) Dy != M.y)) {
LinearGradientPaint gpBounds = new LinearGradientPaint((float) M.x, (float) M.y
, (float) Dx, (float) Dy
,  new float[] { 0.0f, 1.0f }
, new Color[] { new Color(1.0f, 1.0f, 1.0f, 0.0f)
, new Color(0.0f, 0.0f, 0.0f, 0.5f) });
g2.setPaint(gpBounds);
g2.fill(bnds);    // bnds is the current component getBounds() Rectangle
}

3. Calculate the rotation for the image on the backside

The backside image is translated to point M and rotated so that it reflects the direction we are peeling. eg . If we peel upwards, the backside image must be drawn upside down.

double delta = Mx / My; // tan(theta) = delta
double theta = Math.atan(delta); // radians, the rotate transform uses degrees

// Find out translation
int tx = 0;
int ty = 0;
Point rotator = new Point(0, 0);
switch (dragDir) {
case 0: // top-left
tx = -bnds.width + M.x;
ty = M.y;
rotator.x = bnds.width;
break;
case 1: // top-right
tx = M.x;
ty = M.y;
break;
case 2: // bottom-right
tx = M.x;
ty = M.y - bnds.height;
rotator.x = 0;
rotator.y = bnds.height;
break;
case 3: // bottom-left
tx = -bnds.width + M.x;
ty = M.y - bnds.height;
rotator.x = bnds.width;
rotator.y = bnds.height;
break;
}

4. Draw the image on the backside into polygon T1

Set the clip to polygon T1, translate to the right location to start drawing the image then rotate to the right angle and draw the image. After that reverse the transformations to set the graphics object to its initial state.

Shape clip = g2.getClip();
g2.setClip(T1);
g2.translate(tx, ty);
g2.rotate(-2 * theta + Math.PI, rotator.x, rotator.y);
g2.drawImage(backSide, 0, 0, backSide.getWidth(), backSide.getHeight(), null);
g2.rotate(2 * theta + Math.PI, rotator.x, rotator.y);
g2.translate(-tx, -ty);
g2.setClip(clip);

5. Draw the highlight gradient into polygon T1

The gradient flows from point M to point D (fig. 2)

if (((int) Dx != M.x) && ((int) Dy != M.y)) {
LinearGradientPaint gpPeel = new LinearGradientPaint((float) M.x, (float) M.y
, (float) Dx, (float) Dy
, new float[] { 0.0f, 0.7f, 0.9f, 1.0f }
, new Color[] { new Color(0.0f, 0.0f, 0.0f, 0.1f)
, new Color(0.0f, 0.0f, 0.0f, 0.1f)
, new Color(0.0f, 0.0f, 0.0f, 0.2f)
, new Color(1.0f, 1.0f, 1.0f, 0.5f) });
g2.setPaint(gpPeel);
g2.fill(T1);
}

6. Draw the image of the next page into polygon T2

Set the clip to T2, then draw the nextPage image in the top left corner.

g2.setClip(T2);
g2.drawImage(nextPage, 0, 0, nextPage.getWidth(), nextPage.getHeight(), null);

7. Draw the shadow over the next page

if (((int) dpx != peelLocation.x) && ((int) dpy != peelLocation.y)) {
LinearGradientPaint gpPeel = new LinearGradientPaint((float) dpx, (float) dpy
, locFrom.x, locFrom.y, new float[] { 0.0f, 0.25f}
, new Color[] { new Color(0.0f, 0.0f, 0.0f, 0.6f)
, new Color(0.0f, 0.0f, 0.0f, 0.0f)});
g2.setPaint(gpPeel);
g2.fill(q);
}

8. Draw a masking line over the fold

To hide artefacts at the line where the two polygons join we draw a dark grey antialiased line right over it.

g2.setClip(clip);
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(new Color(0.2f, 0.2f, 0.2f, 1f));
g2.drawLine(p1.x, p1.y, p2.x, p2.y);               
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);

Click the webstart launch button below to see a demo of the page peel effect.

Launch

This demo utilizes JXLayer for event handling and the trident animation library for animation. I strongly advise to check those libraries out.

The sourcecode is in the following zip archive:

pagepeel-src.zip

For questions feel free to contact me at aphilip(at)dexels.com

 

 

Dexels BV, Grasweg 67, 1031 HX, Amsterdam Noord, t. +31 (0)20 490 4977, e. info@dexels.com