Canvas.java
/*
* ..::jDrawingLib::..
*
* Copyright (C) Federico Vera 2012 - 2023 <[email protected]>
*
* This program 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 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.dkt.graphics.canvas;
import com.dkt.graphics.elements.GString;
import com.dkt.graphics.elements.Graphic;
import com.dkt.graphics.elements.GraphicE;
import com.dkt.graphics.exceptions.InvalidArgumentException;
import com.dkt.graphics.utils.TPS;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsEnvironment;
import java.awt.Paint;
import java.awt.RenderingHints;
import java.awt.Transparency;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.text.DecimalFormat;
import java.util.LinkedList;
import javax.swing.JPanel;
import javax.swing.Timer;
/**
* This class represents a basic canvas, it has some features that may seem
* useless... they are...<br><br>
*
* Within the canvas there are 2 types of {@link GraphicE}, fixed and mobile.
* fixed elements are only redrawn when the canvas is resized or when
* specifically told to.
*
* @author Federico Vera {@literal<[email protected]>}
*/
public class Canvas extends JPanel implements ActionListener {
private final LinkedList<GraphicE> elements = new LinkedList<>();
private final LinkedList<GraphicE> fixed = new LinkedList<>();
private boolean centerBounds, fullArea, invert;
private int xSize = 100;
private int ySize = 100;
private int xO;
private int yO;
private Paint drawableAreaPaint = Color.WHITE;
private Paint drawableBorderPaint = Color.BLACK;
private boolean useAntiAliasing = true;
private boolean autoRepaint;
private int repaintDelay = 50;
private final Timer repaintTimer = new Timer(500, this);
private boolean showFPS;
private final GString fps = new GString(10, 20, "");
private final TPS tps = new TPS();
private final DecimalFormat formatter = new DecimalFormat("#.00");
private final AffineTransform emptyTran = new AffineTransform();
private static GraphicsConfiguration GFX_CFG;
private AffineTransform transform = new AffineTransform();
private boolean centerOrigin;
private transient BufferedImage background;
public Canvas(){
//Init timer config
repaintTimer.setRepeats(true);
repaintTimer.setCoalesce(true);
repaintTimer.setDelay(repaintDelay);
setBackground(Color.LIGHT_GRAY);
setIgnoreRepaint(true);
addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
if (fullArea) {
final Dimension dim = getSize();
xSize = dim.width;
ySize = dim.height;
setCenterOrigin(centerOrigin);
} else {
redraw(true);
}
}
});
}
/**
* Sets the canvas size.<br>
* This value doesn't affect the panel's size, just the drawable area, this
* means that the canvas area will be of the given size.
*
* <pre>
* +------+----+
* | | |
* |canvas| |
* | | |
* +------+ |
* | panel |
* +-----------+</pre>
*
* @param xSize horizontal size
* @param ySize vertical size
* @throws InvalidArgumentException if either size is less than 1
*/
public void setDrawableSize(int xSize, int ySize) {
if (xSize <= 0 | ySize <= 0){
String msg = "The size can't be less than 1";
throw new InvalidArgumentException(msg);
}
this.xSize = xSize;
this.ySize = ySize;
if (centerOrigin){
setCenterOrigin(true);
}
redraw(true);
}
/**
* Tells the canvas to center (or not) the drawable area in the panel.
*
* <pre>
* +--------------+
* | |
* | +--------+ |
* | | | |
* | | canvas | |
* | | | |
* | +--------+ |
* | panel |
* +--------------+</pre>
* @param centerBounds {@code true} to center the bounds an {@code false}
* otherwise
*/
public void setCenterBounds(boolean centerBounds){
this.centerBounds = centerBounds;
redraw(true);
}
/**
* Tells if the canvas is being centered.<br>
* <i>Note:</i> the default value is {@code false}
*
* @return {@code true} if the canvas is being centered and {@code false}
* otherwise
*/
public boolean centerBounds() {
return centerBounds;
}
/**
* Tells if the drawable area will be automatically set to the canvas size.
* <br><i>Note:</i> the default value is {@code false}
* @return {@code true} if the drawable area is the canvas size, and {@code
* false} otherwise.
*/
public boolean useFullArea() {
return fullArea;
}
/**
* Tells the canvas to set the drawable area size to the size of the canvas.
*
* @param fullArea {@code true} for the canvas and drawable area to have the
* same size, and {@code false} to set it manually.
*/
public void setUseFullArea(boolean fullArea) {
this.fullArea = fullArea;
if (fullArea) {
final Dimension dim = getSize();
xSize = dim.width;
ySize = dim.height;
}
setCenterOrigin(centerOrigin);
}
/**
* Retrieves the horizontal size of the drawable area
*
* @return horizontal size in px
*/
public int getXSize() {
return xSize;
}
/**
* Retrieves the vertical size of the drawable area
*
* @return vertical size in px
*/
public int getYSize() {
return ySize;
}
/**
* Sends the selected {@link GraphicE} to the bottom of the canvas, so other
* elements will be painted on top
*
* @param element {@link GraphicE} to be send to the bottom
*/
public void sendToBottom(GraphicE element){
synchronized (elements){
if (elements.contains(element)){
elements.remove (element);
elements.addFirst(element);
redraw(false);
}
}
}
/**
* Sends the selected {@link GraphicE} to the top of the canvas, so it will
* be painted on top of all the other elements.<br>
* <i>Note:</i> if a new element is added it will land on top of this one.
*
* @param element {@link GraphicE} to be send to the front
*/
public void sendToFront(GraphicE element){
synchronized (elements){
if (elements.contains(element)){
elements.remove(element);
elements.addLast(element);
redraw(false);
}
}
}
/**
* Adds a {@link GraphicE} to the canvas.<br>
* This method doesn't check if the element is already contained on the
* canvas, this mean, that you can add elements twice (With no particular
* gain).
*
* @param element element that will be added
* @see Canvas#contains(com.dkt.graphics.elements.GraphicE)
* @see Graphic#add(com.dkt.graphics.elements.GraphicE)
*/
public void add(GraphicE element){
if (element == null) {
return;
}
synchronized (elements){
elements.add(element);
redraw(false);
}
}
/**
* Tells if a {@link GraphicE} is already being painted on the canvas.
*
* @param element element to test
* @return {@code true} if the element is contained and {@code false}
* otherwise
*/
public boolean contains(GraphicE element){
if (element == null) {
return false;
}
synchronized (elements){
return elements.contains(element);
}
}
/**
* Removes a given {@link GraphicE} from the canvas
*
* @param element element to remove
* @return {@code true} if the element was contained and {@code false}
* otherwise
*/
public boolean remove(GraphicE element){
if (element == null) {
return false;
}
synchronized (elements){
boolean stat = elements.remove(element);
redraw(false);
return stat;
}
}
@Override
public void removeAll(){
super.removeAll();
synchronized (elements){
elements.clear();
}
synchronized (fixed){
fixed.clear();
}
}
/**
* Adds a new fixed {@link GraphicE} to the canvas
*
* @param element element that you want to add
*/
public void addFixed(GraphicE element){
if (element == null) {
return;
}
synchronized (fixed){
fixed.add(element);
redraw(true);
}
}
/**
* Removes a given fixed {@link GraphicE} from the canvas
*
* @param element element to remove
* @return {@code true} if the element was contained and {@code false}
* otherwise
*/
public boolean removeFixed(GraphicE element){
if (element == null) {
return true;
}
synchronized (fixed){
boolean stat = fixed.remove(element);
redraw(stat);
return stat;
}
}
@Override
public void setBackground(Color bg) {
if (bg == null){
throw new IllegalArgumentException("The background color can't be null");
}
super.setBackground(bg);
//Since this method is called by the super class constructor so the
//class might not be completly initialized when this method is called
//the first time
if (fixed != null & elements != null){
redraw(true);
}
}
/**
* Retrieves the {@link Paint} used as background on the drawable area of
* the canvas
*
* @return drawable area background paint
*/
public Paint getDrawableAreaPaint() {
return drawableAreaPaint;
}
/**
* Sets the background {@link Paint} of the drawable area of the canvas
*
* @param paint new paint
* @throws IllegalArgumentException if the color is {@code null}
*/
public void setDrawableAreaPaint(Paint paint) {
if (paint == null){
throw new IllegalArgumentException("The paint can't be null");
}
drawableAreaPaint = paint;
redraw(true);
}
/**
* Retrieves the {@link Paint} of the border of the drawable area
*
* @return drawable area border paint
*/
public Paint getDrawableBorderPaint() {
return drawableBorderPaint;
}
/**
* Sets the border {@link Paint} of the drawable area of the canvas
*
* @param paint new paint
* @throws IllegalArgumentException if the paint is {@code null}
*/
public void setDrawableBorderPaint(Paint paint) {
if (paint == null){
throw new IllegalArgumentException("The paint can't be null");
}
drawableBorderPaint = paint;
redraw(true);
}
/**
* Tells the canvas to invert the Y axis.<br>
* This method comes in handy when plotting functions or working with
* physics since it gives a more <i>natural</i> way of drawing. But should
* be ignored most of the time, since it might break other transforms.
* <br><br>
* <i><b>WARNING: </b></i> this method does its magic using an
* {@link AffineTransform}, so this basically means that it draws in the
* conventional way and then invert the image, so text and images will
* appear inverted.
*
* @param invert {@code true} if you wish to invert the Y axis and
* {@code false} otherwise
*/
public void setInvertYAxis(boolean invert){
this.invert = invert;
redraw(true);
}
/**
* Tells if the canvas is inverting the Y axis
*
* @return {@code true} if the canvas is inverting the Y axis and
* {@code false} otherwise
*/
public boolean invertYAxis() {
return invert;
}
/**
* Moves the origin of coordinates to the given position, this method is
* very useful when working with mathematical functions since it gives a
* more natural way of drawing things.
*
* @param x new X coordinate of the origin
* @param y new Y coordinate of the origin
*/
public void moveOrigin(int x, int y){
xO = x;
yO = y;
redraw(true);
}
/**
* This method redraws the background and then repaints the canvas
*/
public void repaintWBackground(){
redraw(true);
repaint();
}
private void calcTransform() {
transform = AffineTransform.getTranslateInstance(xO, yO);
if (invert){
transform.concatenate(AffineTransform.getScaleInstance(1, -1));
}
}
/**
* Tells the canvas to center the origin of coordinates within the drawable
* area.
*
* @param center {@code true} if you want to center the origin and
* {@code false} otherwise
*/
public void setCenterOrigin(boolean center) {
centerOrigin = center;
if (center){
moveOrigin(getXSize() / 2, getYSize() / 2);
} else {
moveOrigin(0, 0);
}
}
/**
* Tells if the canvas is rendering using antialiasing.<br>
* <i>Note:</i> the default value is {@code true}
*
* @return {@code true} if antialiasing is on and {@code false} otherwise
*/
public boolean useAntiAliasing() {
return useAntiAliasing;
}
/**
* Turns anti-aliasing <tt>on</tt> or <tt>off</tt>
*
* @param useAntiAliasing {@code true} to turn antialiasing on, and {@code
* false} to turn it off.
*/
public void setUseAntiAliasing(boolean useAntiAliasing) {
this.useAntiAliasing = useAntiAliasing;
redraw(true);
}
/**
* Tells the canvas to repaint itself automatically
*
* @param repaint {@code true} if the canvas should repaint itself and
* {@code false} otherwise
* @see Canvas#setRepaintDelay(int)
*/
public void setAutoRepaint(boolean repaint){
autoRepaint = repaint;
if (repaint){
repaintTimer.start();
} else {
repaintTimer.stop();
}
}
/**
* Tells if the canvas is repainting itself automatically
*
* @return repaint {@code true} if the canvas is repainting itself and
* {@code false} otherwise
* @see Canvas#setRepaintDelay(int)
*/
public boolean autoRepaint(){
return autoRepaint;
}
/**
* The time in ms of the repaint interval.<br>
* This will not guaranty that each is repaint is done every n ms, it will
* only call the repaint method every n ms.
*
* @param delay time in ms
* @throws InvalidArgumentException if the delay is less than 1
* @see Canvas#setAutoRepaint(boolean)
*/
public void setRepaintDelay(int delay){
if (delay < 1){
throw new InvalidArgumentException("Delay MUST be a positive real");
}
repaintDelay = delay;
repaintTimer.setDelay(repaintDelay);
}
/**
* Retrieves the time in ms for the canvas to repaint itself
*
* @return time in ms
* @see Canvas#setAutoRepaint(boolean)
*/
public long repaintDelay(){
return repaintDelay;
}
/**
* Tells if the canvas is centering automatically the origin of coordinates.
* <pre>
* not centered centered
* +------------+ +------------+
* |*(0,0) | | |
* | | | |
* | | | *(0,0) |
* | | | |
* | canvas | | canvas |
* +------------+ +------------+</pre>
*
* @return {@code true} if the origin is centered and {@code false}
* otherwise
*/
public boolean centerOrigin(){
return centerOrigin;
}
/**
* This method tells the canvas to print the current FPS value on the screen
* (it will be painted in the upper left corner above all other elements).
* <br><i>Note:</i> if {@code showFPS} is set to {@code true} it will most
* likely have a small impact on performance (usually we draw hundreds of
* thousands of {@link GraphicE}, but in small applications with very high
* FPS the impact is quite noticeable).
*
* @param show {@code true} if you want to show the FPS and {@code false}
* otherwise
*/
public void setShowFPS(boolean show){
if (show){
tps.reset();
}
this.showFPS = show;
}
private void redraw(boolean with_background){
calcTransform();
if (with_background){
createBackground();
if (getParent() != null){
getParent().repaint();
}
}
}
private void createBackground() {
//Release resourses
if (background != null){
background.flush();
}
//When a resize event happened is possible that the screen configuration
//has changed
GFX_CFG = GraphicsEnvironment
.getLocalGraphicsEnvironment()
.getDefaultScreenDevice()
.getDefaultConfiguration();
//New background image
background = GFX_CFG.createCompatibleImage(
getWidth () + 2,
getHeight() + 2
);
Graphics2D g2d = background.createGraphics();
g2d.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
useAntiAliasing ? RenderingHints.VALUE_ANTIALIAS_ON
: RenderingHints.VALUE_ANTIALIAS_OFF
);
//Paint background color
g2d.setBackground(getBackground());
g2d.clearRect(0, 0, getWidth(), getHeight());
//Calculate affine transform for the background
final AffineTransform btransform;
if (centerBounds){
final int txoff = xO + (getWidth () - xSize) / 2;
final int tyoff = yO + (getHeight() - ySize) / 2;
btransform = AffineTransform.getTranslateInstance(txoff, tyoff);
} else {
btransform = AffineTransform.getTranslateInstance(xO, yO);
}
if (invert){
btransform.concatenate(AffineTransform.getScaleInstance(1, -1));
}
//Calculate background offsets
final int xOff;
final int yOff;
if (centerBounds){
xOff = (getWidth () - xSize) / 2;
yOff = (getHeight() - ySize) / 2;
} else {
xOff = 0;
yOff = 0;
}
//Draw 'drawing' area
g2d.setPaint(drawableAreaPaint);
g2d.fillRect(xOff, yOff, xSize, ySize);
g2d.setPaint(drawableBorderPaint);
//The extra px is for the border (looks nicer)
g2d.drawRect(xOff - 1, yOff - 1, xSize + 1, ySize + 1);
g2d.clipRect(xOff , yOff , xSize , ySize );
g2d.setTransform(btransform);
//Draw all fixed elements
synchronized (fixed){
for (GraphicE element : fixed){
element.draw(g2d);
}
}
g2d.dispose();
}
/**
* Paints the canvas drawable area on the given graphics
*
* @param g2d Where to paint
* @param back {@code true} if you want to paint the background and {@code
* false} otherwise ({@code false} is needed when painting with transparent
* components.
* @throws IllegalArgumentException if the g2d is {@code null}
*/
public void paintDrawableArea(Graphics2D g2d, boolean back) {
if (g2d == null){
throw new IllegalArgumentException("Graphics can't be null");
}
//Paint the background
if (back) {
g2d.setPaint(drawableAreaPaint);
g2d.fillRect(0, 0, xSize, ySize);
}
//Set the coordinate transform
g2d.setTransform(transform);
//Set anti-aliasing
g2d.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
useAntiAliasing ? RenderingHints.VALUE_ANTIALIAS_ON
: RenderingHints.VALUE_ANTIALIAS_OFF
);
//Draw all fixed components
synchronized (fixed){
for (GraphicE element : fixed){
element.draw(g2d);
}
}
//Draw all volatile components
synchronized (elements){
for (GraphicE element : elements){
element.draw(g2d);
}
}
}
@Override
public void paintComponent (Graphics g){
super.paintComponent(g);
final BufferedImage content = GFX_CFG.createCompatibleImage(
xSize,
ySize,
Transparency.TRANSLUCENT
);
//Try to accelerate the image
content.setAccelerationPriority(1);
Graphics2D g2d = content.createGraphics();
g2d.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
useAntiAliasing ? RenderingHints.VALUE_ANTIALIAS_ON
: RenderingHints.VALUE_ANTIALIAS_OFF
);
//Don't allow drawing outside the drawable area
g2d.clipRect(0, 0, xSize, ySize);
//Draw all elements
g2d.setTransform(transform);
synchronized (elements){
for (GraphicE element : elements){
element.draw(g2d);
}
}
//Paint the FPS number on the screen
if (showFPS){
tps.action();
g2d.setTransform(emptyTran);
fps.setString(formatter.format(tps.ctps()));
fps.draw(g2d);
}
//Dispose of new image graphics
g2d.dispose();
//Image bounds
final int xOff;
final int yOff;
if (centerBounds){
xOff = (getWidth () - xSize) / 2;
yOff = (getHeight() - ySize) / 2;
} else {
xOff = 0;
yOff = 0;
}
//Draw back
g.drawImage(background, 0, 0, null);
//Draw front
g.drawImage(content, xOff, yOff, null);
}
@Override
public void actionPerformed(ActionEvent ae) {
repaint();
}
/**
* Tells if a given point is contained in the drawing area.
*
* @param x X coordinate of the point
* @param y Y coordinate of the point
* @return {@code true} if the point is contained in the drawing area, and
* {@code false} otherwise
*/
public boolean inDrawingArea(int x, int y) {
final int xOff;
final int yOff;
if (centerBounds){
xOff = (getWidth () - xSize) / 2;
yOff = (getHeight() - ySize) / 2;
} else {
xOff = 0;
yOff = 0;
}
return xOff <= x & x <= xOff + xSize &
yOff <= y & y <= yOff + ySize;
}
/**
* Retrieves the horizontal offset of the drawing area in the canvas.
*
* @return horizontal offset in px
*/
public int getXOff() {
return centerBounds ? (getWidth () - xSize) / 2 : 0;
}
/**
* Retrieves the vertical offset of the drawing area in the canvas.
*
* @return vertical offset in px
*/
public int getYOff() {
return centerBounds ? (getHeight() - ySize) / 2 : 0;
}
}