/*
* Copyright (C) 2010 Markus Echterhoff <tam@edu.uni-klu.ac.at>
*
* This file is part of EvoPaint.
*
* EvoPaint is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EvoPaint. If not, see <http://www.gnu.org/licenses/>.
*/
package evopaint.gui.util;
import evopaint.util.ExceptionHandler;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JComponent;
/**
* The <code>WrappingScalableCanvas</code> class is used to create a parallax
* surface which can be moved into two directions and be scaled indefinetly.
* <p>
* Keep in mind two spaces which need transformation from one to another. The
* user space in which the user performs clicks and the image is scaled into
* and the image space in which operations are performed on the image.<br>
* As a rule of thumb remember to transform any points from mouse clicks
* performed on the canvas to image space before processing them.
* </p>
* <p>
* Implementing <code>IOverlayable</code>, this canvas supports alpha overlays,
* which it will also wrap and scale to user space accordingly.
* </p>
*
* @author Markus Echterhoff <tam@edu.uni-klu.ac.at>
*/
public class WrappingScalableCanvas extends JComponent implements IOverlayable {
private BufferedImage image;
private Graphics2D imageG2;
private int imageWidth;
private int imageHeight;
private int integerScale;
private double scale;
private Point translation;
private AffineTransform scaleTransform;
private AffineTransform translationTransform;
private AffineTransform transform;
private List<IOverlay> overlays;
/**
* Creates a new WrappingScalableCanvas using the image supplied.
* @param image
*/
public WrappingScalableCanvas(BufferedImage image) {
this.image = image;
this.imageWidth = image.getWidth();
this.imageHeight = image.getHeight();
this.imageG2 = (Graphics2D)image.getGraphics();
this.integerScale = 10;
this.scale = 1;
this.translation = new Point(0, 0);
this.translationTransform = new AffineTransform();
this.overlays = new ArrayList<IOverlay>();
updateScale();
updateComponentSize();
}
/**
* Magnifies the display of the image
*/
public void scaleUp() {
integerScale++;
scale = integerScale / 10d;
updateScale();
}
/**
* Shrinks the display of the image
*/
public void scaleDown() {
if (integerScale <= 1) {
return;
}
integerScale--;
updateScale();
}
/**
* resets the display of the image to its original size
*/
public void scaleReset() {
integerScale = 10;
updateScale();
}
private void updateScale() {
scale = integerScale / 10d;
scaleTransform = AffineTransform.getScaleInstance(scale, scale);
transform = new AffineTransform(scaleTransform);
transform.concatenate(translationTransform);
updateComponentSize();
revalidate();
}
private void updateComponentSize() {
setPreferredSize(new Dimension((int)Math.ceil(imageWidth * scale),
(int)Math.ceil(imageHeight * scale)));
}
/**
* Translates the display of the underlying image using user space
* coordinates.
* @param origin the origin eg. of a user space drag operation
* @param destination the destination eg. of a user space drag operation
*/
public void translateInUserSpace(Point origin, Point destination) {
// transform points to image space before translation so they scale
// correctly
origin = transformToImageSpace(new Point(origin));
destination = transformToImageSpace(new Point(destination));
translation.x += destination.x - origin.x;
if (translation.x < (-1) * imageWidth) {
translation.x += imageWidth;
} else if (translation.x > imageWidth) {
translation.x -= imageWidth;
}
translation.y += destination.y - origin.y;
if (translation.y < (-1) * imageHeight) {
translation.y += imageHeight;
} else if (translation.y > imageHeight) {
translation.y -= imageHeight;
}
translationTransform = AffineTransform.getTranslateInstance(translation.x, translation.y);
transform = new AffineTransform(scaleTransform);
transform.concatenate(translationTransform);
}
/**
* Transforms a <code>Point</code> from user space to image space. It will
* translate the <code>Point</code> back to its origin in user space and
* rescale it to the original image space scale.
* @param point The <code>Point</code> used to create the
* @return A new Point in image space corresponding to the passed
* <code>Point</code>
*/
public Point transformToImageSpace(Point point) {
AffineTransform invertedTransform = new AffineTransform(transform);
try {
invertedTransform.invert();
} catch (NoninvertibleTransformException ex) {
ExceptionHandler.handle(ex, false);
}
Point2D.Float floatPoint = (Point2D.Float)invertedTransform.transform(point, null);
Point ret = new Point((int)floatPoint.x, (int)floatPoint.y);
// the transformed point may have coordinates wich lie out of our
// image, so we have to wrap them
if (ret.x < 0) {
ret.x += imageWidth;
}
else if (ret.x > imageWidth) {
ret.x -= imageWidth;
}
if (ret.y < 0) {
ret.y += imageHeight;
}
else if (ret.y > imageHeight) {
ret.y -= imageHeight;
}
return ret;
}
/**
* Scales an arbitrary <code>Shape</code> from image space to user space
* @param shape The <code>Shape</code> you wish to scale
* @return A new <code>Shape</code> scaled to user space
* @see Shape
*/
public Shape scaleToUserSpace(Shape shape) {
return scaleTransform.createTransformedShape(shape);
}
/**
* Scales an arbitrary <code>Shape</code> from user space to image space
* @param shape The <code>Shape</code> you wish to scale
* @return A new <code>Shape</code> scaled to image space
* @see Shape
*/
public Shape scaleToImageSpace(Shape shape) {
try {
return scaleTransform.createInverse().createTransformedShape(shape);
} catch (NoninvertibleTransformException ex) {
ExceptionHandler.handle(ex, false);
}
assert (false);
return null;
}
/**
* Transforms a <code>Point</code> from image space to user space the
* resulting <code>Point</code> will be translated and scaled to match
* user space coordinates.
* @param point The <code>Point</code> you wish to transform
* @return a new <code>Point</code> in user space corresponding to the
* argument point
*/
public Point transformToUserSpace(Point point) {
return (Point)transform.transform(point, null);
}
/**
* Transforms an arbitrary <code>Shape</code> from image space to user space, translates
* and scales.
* @param shape The <code>Shape</code> you wish to transform
* @return A new <code>Shape</code> corresponding the the argument
* @see Shape
*/
public Shape transformToUserSpace(Shape shape) {
return transform.createTransformedShape(shape);
}
/**
* Adds an overlay to the canvas
* @param overlay The Overlay you wish to subscribe
* @see IOverlayable
* @see IOverlay
*/
public void subscribe(IOverlay overlay) {
overlays.add(overlay);
}
/**
* Removes an overlay from the subscribed overlays
* @param overlay The Overlay you wish to unsubscribe
* @see IOverlayable
* @see IOverlay
*/
public void unsubscribe(IOverlay overlay) {
overlays.remove(overlay);
}
/**
* Paints the outline of a <code>Shape</code> onto this canvas using the
* underlying image graphics context. The shape will be wrapped around the
* edges of the canvas. This method is designed to be used by overlays.
* @param shape The <code>Shape</code> to draw, bounds expected in image
* space coordinates
* @see IOverlay
*/
public void draw(Shape shape) {
imageG2.draw(shape);
Rectangle bounds = shape.getBounds();
// wrapping horizontal
// west
if (bounds.x < 0) {
Rectangle wrappedRest = new Rectangle(bounds.x + imageWidth, bounds.y, bounds.width + bounds.x, bounds.height);
imageG2.draw(wrappedRest);
// corner NW
if (bounds.y < 0) {
wrappedRest = new Rectangle(bounds.x + imageWidth, bounds.y + imageHeight, bounds.width + bounds.x, bounds.height + bounds.y);
imageG2.draw(wrappedRest);
}
// corner SW
else if (bounds.y + bounds.height > imageHeight) {
wrappedRest = new Rectangle(bounds.x + imageWidth, 0, bounds.width + bounds.x, bounds.y + bounds.height - imageHeight);
imageG2.draw(wrappedRest);
}
}
// east
else if (bounds.x + bounds.width > imageWidth) {
Rectangle wrappedRest = new Rectangle(0, bounds.y, bounds.x + bounds.width - imageWidth, bounds.height);
imageG2.draw(wrappedRest);
// corner NE
if (bounds.y < 0) {
wrappedRest = new Rectangle(0, bounds.y + imageHeight, bounds.x + bounds.width - imageWidth, bounds.height + bounds.y);
imageG2.draw(wrappedRest);
}
// corner SE
else if (bounds.y + bounds.height > imageHeight) {
wrappedRest = new Rectangle(0, 0, bounds.x + bounds.width - imageWidth, bounds.y + bounds.height - imageHeight);
imageG2.draw(wrappedRest);
}
}
// wrapping vertical (corners already painted)
// north
if (bounds.y < 0) {
Rectangle wrappedRest = new Rectangle(bounds.x, bounds.y + imageHeight, bounds.width, bounds.height + bounds.y);
imageG2.draw(wrappedRest);
}
// south
else if (bounds.y + bounds.height > imageHeight) {
Rectangle wrappedRest = new Rectangle(bounds.x, 0, bounds.width, bounds.y + bounds.height - imageHeight);
imageG2.draw(wrappedRest);
}
}
/**
* Paints a filled <code>Shape</code> onto this canvas using the underlying
* image graphics context. The shape will be wrapped around the edges of
* the canvas. This method is designed to be used by overlays.
* @param shape The <code>Shape</code> to draw, bounds expected in image
* space coordinates
* @see IOverlay
*/
public void fill(Shape shape) {
imageG2.fill(shape);
Rectangle bounds = shape.getBounds();
// wrapping horizontal
// west
if (bounds.x < 0) {
Rectangle wrappedRest = new Rectangle(bounds.x + imageWidth, bounds.y, bounds.width + bounds.x, bounds.height);
imageG2.fill(wrappedRest);
// corner NW
if (bounds.y < 0) {
wrappedRest = new Rectangle(bounds.x + imageWidth, bounds.y + imageHeight, bounds.width + bounds.x, bounds.height + bounds.y);
imageG2.fill(wrappedRest);
}
// corner SW
else if (bounds.y + bounds.height > imageHeight) {
wrappedRest = new Rectangle(bounds.x + imageWidth, 0, bounds.width + bounds.x, bounds.y + bounds.height - imageHeight);
imageG2.fill(wrappedRest);
}
}
// east
else if (bounds.x + bounds.width > imageWidth) {
Rectangle wrappedRest = new Rectangle(0, bounds.y, bounds.x + bounds.width - imageWidth, bounds.height);
imageG2.fill(wrappedRest);
// corner NE
if (bounds.y < 0) {
wrappedRest = new Rectangle(0, bounds.y + imageHeight, bounds.x + bounds.width - imageWidth, bounds.height + bounds.y);
imageG2.fill(wrappedRest);
}
// corner SE
else if (bounds.y + bounds.height > imageHeight) {
wrappedRest = new Rectangle(0, 0, bounds.x + bounds.width - imageWidth, bounds.y + bounds.height - imageHeight);
imageG2.fill(wrappedRest);
}
}
// wrapping vertical (corners already painted)
// north
if (bounds.y < 0) {
Rectangle wrappedRest = new Rectangle(bounds.x, bounds.y + imageHeight, bounds.width, bounds.height + bounds.y);
imageG2.fill(wrappedRest);
}
// south
else if (bounds.y + bounds.height > imageHeight) {
Rectangle wrappedRest = new Rectangle(bounds.x, 0, bounds.width, bounds.y + bounds.height - imageHeight);
imageG2.fill(wrappedRest);
}
}
/**
* Paints the canvas
* @param g The graphics context to paint on
*/
@Override
public void paintComponent(Graphics g) {
for (IOverlay overlay : overlays) {
overlay.paint(imageG2);
}
Graphics2D g2 = (Graphics2D)g;
g2.clip(scaleToUserSpace(new Rectangle(imageWidth, imageHeight)));
// paint NW
transform.translate((-1) * imageWidth, (-1) * imageHeight);
g2.drawRenderedImage(image, transform);
// paint N
transform.translate(imageWidth, 0);
g2.drawRenderedImage(image, transform);
// paint NE
transform.translate(imageWidth, 0);
g2.drawRenderedImage(image, transform);
// paint E
transform.translate(0, imageHeight);
g2.drawRenderedImage(image, transform);
// paint SE
transform.translate(0, imageHeight);
g2.drawRenderedImage(image, transform);
// paint S
transform.translate((-1) * imageWidth, 0);
g2.drawRenderedImage(image, transform);
// paint SW
transform.translate((-1) * imageWidth, 0);
g2.drawRenderedImage(image, transform);
// paint W
transform.translate(0, (-1) * imageHeight);
g2.drawRenderedImage(image, transform);
// back to normal
transform.translate(imageWidth, 0);
g2.drawRenderedImage(image, transform);
}
public BufferedImage scaleAndTranslate(BufferedImage image) {
BufferedImage ret = new BufferedImage((int)(imageWidth * scale), (int)(imageHeight * scale), BufferedImage.TYPE_INT_RGB);
Graphics2D g2 = ret.createGraphics();
g2.clip(scaleToUserSpace(new Rectangle(imageWidth, imageHeight)));
// paint NW
transform.translate((-1) * imageWidth, (-1) * imageHeight);
g2.drawRenderedImage(image, transform);
// paint N
transform.translate(imageWidth, 0);
g2.drawRenderedImage(image, transform);
// paint NE
transform.translate(imageWidth, 0);
g2.drawRenderedImage(image, transform);
// paint E
transform.translate(0, imageHeight);
g2.drawRenderedImage(image, transform);
// paint SE
transform.translate(0, imageHeight);
g2.drawRenderedImage(image, transform);
// paint S
transform.translate((-1) * imageWidth, 0);
g2.drawRenderedImage(image, transform);
// paint SW
transform.translate((-1) * imageWidth, 0);
g2.drawRenderedImage(image, transform);
// paint W
transform.translate(0, (-1) * imageHeight);
g2.drawRenderedImage(image, transform);
// back to normal
transform.translate(imageWidth, 0);
g2.drawRenderedImage(image, transform);
return ret;
}
public BufferedImage translate(BufferedImage image) {
BufferedImage ret = new BufferedImage((int)(imageWidth), (int)(imageHeight), BufferedImage.TYPE_INT_RGB);
Graphics2D g2 = ret.createGraphics();
g2.clip(new Rectangle(imageWidth, imageHeight));
// paint NW
translationTransform.translate((-1) * imageWidth, (-1) * imageHeight);
g2.drawRenderedImage(image, translationTransform);
// paint N
translationTransform.translate(imageWidth, 0);
g2.drawRenderedImage(image, translationTransform);
// paint NE
translationTransform.translate(imageWidth, 0);
g2.drawRenderedImage(image, translationTransform);
// paint E
translationTransform.translate(0, imageHeight);
g2.drawRenderedImage(image, translationTransform);
// paint SE
translationTransform.translate(0, imageHeight);
g2.drawRenderedImage(image, translationTransform);
// paint S
translationTransform.translate((-1) * imageWidth, 0);
g2.drawRenderedImage(image, translationTransform);
// paint SW
translationTransform.translate((-1) * imageWidth, 0);
g2.drawRenderedImage(image, translationTransform);
// paint W
translationTransform.translate(0, (-1) * imageHeight);
g2.drawRenderedImage(image, translationTransform);
// back to normal
translationTransform.translate(imageWidth, 0);
g2.drawRenderedImage(image, translationTransform);
return ret;
}
}