/**
* Copyright 2007 Pieter-Jan Savat
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package be.savat.components;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Font;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Transparency;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.geom.GeneralPath;
import java.awt.image.BufferedImage;
import java.awt.image.ImageObserver;
import javax.swing.AbstractAction;
import javax.swing.ImageIcon;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.Timer;
import javax.swing.event.MouseInputListener;
/**
* The BookPanel uses the page turn effect to navigate between pages. The images used
* as pages are set by specifying their location, the basename and the extension.
* Each image should have the same basename followed by its index, ranging 1 to x.
* If a 0 index image exists at the location then it is used (even though it is not
* counted in the nrOfPages for the setPages() method).
*
* Navigating through the book can be done by clicking in the bottomright or
* bottomleft corner, by using the 4 arrow keys or by dragging and dropping
* a page.
* To turn pages programmatically either call nextPage() or previousPage(). Jumping
* to a page can be done by calling setLeftPageIndex().
*
* By default soft clipping is off and the borders at the edges of the papers are visible.
*
* TODO fade drop shadow on percentage turned (see passedThreshold for calculations)
*
* @author Pieter-Jan Savat
*/
public class JBookPanel extends JPanel
implements MouseInputListener, ActionListener {
protected static final double PI_DIV_2 = Math.PI / 2.0;
protected enum AutomatedAction {AUTO_TURN, AUTO_DROP_GO_BACK, AUTO_DROP_BUT_TURN}
protected int rotationX;
protected double nextPageAngle;
protected double backPageAngle;
protected Timer timer;
protected AutomatedAction action;
protected Point autoPoint;
protected Point tmpPoint;
protected int leftPageIndex;
protected Image currentLeftImage;
protected Image currentRightImage;
protected Image nextLeftImage;
protected Image nextRightImage;
protected Image previousLeftImage;
protected Image previousRightImage;
protected String pageLocation;
protected String pageName;
protected String pageExtension;
protected int nrOfPages;
protected boolean leftPageTurn;
protected int refreshSpeed;
// used to store the bounds of the book
protected Rectangle bookBounds;
protected int pageWidth;
protected int shadowWidth;
protected int cornerRegionSize;
protected boolean softClipping;
protected boolean borderLinesVisible;
// vars for optimization and reuse
protected Color shadowDarkColor;
protected Color shadowNeutralColor;
protected GeneralPath clipPath;
/**
* Constructor
*/
public JBookPanel() {
super();
this.addMouseMotionListener(this);
this.addMouseListener(this);
this.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), "nextPage");
this.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), "nextPage");
this.getActionMap().put("nextPage", new AbstractAction() {
public void actionPerformed(final ActionEvent e) {
leftPageTurn = false;
nextPage();
}});
this.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), "previousPage");
this.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), "previousPage");
this.getActionMap().put("previousPage", new AbstractAction() {
public void actionPerformed(final ActionEvent e) {
previousPage();
}});
refreshSpeed = 25;
shadowWidth = 100;
cornerRegionSize = 20;
bookBounds = new Rectangle();
softClipping = false;
borderLinesVisible = true;
clipPath = new GeneralPath();
shadowDarkColor = new Color(0,0,0,130);
shadowNeutralColor = new Color(255,255,255,0);
this.setBackground(Color.LIGHT_GRAY);
timer = new Timer(refreshSpeed, this);
timer.stop();
leftPageIndex = 1;
setPages(null, null, null,
13, 210, 342);
setMargins(70, 80);
}
///////////////////
// PAINT METHODS //
///////////////////
@Override
public void paintComponent(final Graphics g) {
final Graphics2D g2 = (Graphics2D) g;
setGraphicsHints(g2);
// background
g.setColor(this.getBackground());
g.fillRect(0, 0, this.getWidth(), this.getHeight());
// page 1
paintPage(g2, currentLeftImage, bookBounds.x, bookBounds.y, pageWidth, bookBounds.height, this, false);
// page 2
paintPage(g2, currentRightImage, bookBounds.x + pageWidth, bookBounds.y, pageWidth, bookBounds.height, this, true);
if (leftPageTurn) {
if (softClipping) {
paintLeftPageSoftClipped(g2);
} else {
paintLeftPage(g2);
}
} else {
if (softClipping) {
paintRightPageSoftClipped(g2);
} else {
paintRightPage(g2);
}
}
}
protected void paintLeftPage(final Graphics2D g2) {
// translate to rotation point
g2.translate(bookBounds.width + bookBounds.x - rotationX + bookBounds.x,
bookBounds.y + bookBounds.height);
// rotate over back of page A angle
g2.rotate(-backPageAngle);
// translate the amount already done
final int done = bookBounds.width + bookBounds.x - rotationX;
g2.translate(done - pageWidth, 0);
// page 3 (= back of page 1)
clipPath.reset();
clipPath.moveTo(pageWidth, 0);
clipPath.lineTo(pageWidth-done, 0);
clipPath.lineTo(pageWidth,
-1 * (int)(Math.tan(PI_DIV_2 - nextPageAngle) * done));
clipPath.closePath();
final Shape s = g2.getClip();
g2.clip(clipPath);
paintPage(g2, previousRightImage, 0, 0 - bookBounds.height, pageWidth, bookBounds.height, this, false);
g2.setClip(s);
// translate back
g2.translate(pageWidth - done, 0);
// rotate back
g2.rotate(backPageAngle);
// translate back
g2.translate(rotationX - bookBounds.width - bookBounds.x - bookBounds.x,
-bookBounds.y - bookBounds.height);
// page 4
clipPath.reset();
clipPath.moveTo(bookBounds.x,
bookBounds.height + bookBounds.y);
clipPath.lineTo(bookBounds.x + done,
bookBounds.height + bookBounds.y);
clipPath.lineTo(bookBounds.x,
bookBounds.height + bookBounds.y - (int)(Math.tan(PI_DIV_2 - nextPageAngle) * done));
clipPath.closePath();
g2.clip(clipPath);
paintPage(g2, previousLeftImage, bookBounds.x, bookBounds.y,
pageWidth, bookBounds.height, this, true);
// add drop shadow
final GradientPaint grad = new GradientPaint(bookBounds.x + done,
bookBounds.height + bookBounds.y,
shadowDarkColor,
bookBounds.x + done - shadowWidth,
bookBounds.height + bookBounds.y + (int)(Math.cos(PI_DIV_2 - nextPageAngle) * shadowWidth),
shadowNeutralColor,
false);
g2.setPaint(grad);
g2.fillRect(bookBounds.x, bookBounds.y,
pageWidth, bookBounds.height);
}
protected void paintLeftPageSoftClipped(final Graphics2D g2) {
// calculate amount done (traveled)
final int done = bookBounds