GSprite.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.extras;
import com.dkt.graphics.elements.GRectangle;
import com.dkt.graphics.elements.GraphicE;
import com.dkt.graphics.exceptions.InvalidArgumentException;
import java.awt.Graphics2D;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Objects;
/**
* Creates a simple sprite, if you need help creating the pixmaps, you should
* probably try out PixmapCreator
*
* @author Federico Vera {@literal<[email protected]>}
*/
public class GSprite extends GraphicE implements Iterable<GPixMap> {
private final ArrayList<GPixMap> frames = new ArrayList<>(10);
private GPixMap current;
private GRectangle bounds;
private boolean cyclic = true;
private int cursor = 0;
private int skips = 1;
private int skipCount = 0;
private int pixelSize = 1;
private boolean drawGrid;
private boolean visible = true;
public GSprite(GSprite e) {
super(e);
}
/**
* Creates an empty sprite
*/
public GSprite() {
}
/**
* Append a new {@link GPixMap} to this {@code GSprite}, this method
* will skip all {@code GPixMap} that are already in the {@code GSprite}
*
* @param map {@code GPixMap} to add
* @throws IllegalArgumentException if {@code map} is {@code null}
* @throws InvalidArgumentException if the {@code map} doesn't have
* the same size.
*/
public void append(GPixMap map) throws IllegalArgumentException,
InvalidArgumentException {
if (map == null) {
throw new IllegalArgumentException("The pixmap can't be null");
}
for (final GPixMap frame : frames) {
if (map.getXSize() != frame.getXSize() |
map.getYSize() != frame.getYSize()) {
final String msg = "The pixmaps must have the same size";
throw new InvalidArgumentException(msg);
}
}
map.setPixelSize(pixelSize);
map.setDrawLines(drawGrid);
map.setVisible(visible);
if (frames.isEmpty()) {
current = map;
bounds = map.getBounds();
}
frames.add(map.clone());
}
/**
* Removes a given {@code map} from the sprite. This method is most
* likely to fail, since {@link GPixMap#equals(java.lang.Object)} relies on
* the pixel size and the grid, and those attributes are mostly certainly
* changed by the constructor.
*
* @param map Map to remove
* @return {@code true} if the element was found and removed and
* {@code false} otherwise
* @throws IllegalArgumentException if {@code map} is {@code null}
*/
public boolean remove(GPixMap map) throws IllegalArgumentException {
if (map == null) {
throw new IllegalArgumentException("The pixmap can't be null");
}
final boolean res = frames.remove(map);
if (res && frames.isEmpty()) {
current = null;
bounds = null;
}
if (Objects.equals(current, map)) {
current = frames.get(0);
bounds = current.getBounds();
}
return res;
}
/**
* Retrieves the current cursor position
*
* @return cursor position
*/
public int getCursor() {
return cursor;
}
/**
* Sets the first {@link GPixMap} as the current one.
*/
public void first() {
if (!frames.isEmpty()) {
current = frames.get(0);
bounds = current.getBounds();
skipCount = 0;
}
}
/**
* Passes to the previous {@link GPixMap} in the current sprite.<br>
* This method depends on the number of skips set, that is: if skips is 3,
* then this method must be called 3 times in order to make an actual
* change, apart from it, it also depends on {@link GSprite#isCyclic()}.
*
* @return {@code true} the pixmap change and {@code false} otherwise.
* @see GSprite#setSkips(int)
* @see GSprite#skips()
* @see GSprite#isCyclic()
*/
public boolean prev() {
if (!isCyclic() && cursor - 1 < 0) {
return false;
}
if (++skipCount % skips == 0) {
if (--cursor < 0 && isCyclic()) {
cursor = frames.size() - 1;
}
current = frames.get(cursor);
bounds = current.getBounds();
skipCount = 0;
return true;
}
return false;
}
/**
* Passes to the next {@link GPixMap} in the current sprite.<br>
* This method depends on the number of skips set, that is: if skips is 3,
* then this method must be called 3 times in order to make an actual
* change, apart from it, it also depends on {@link GSprite#isCyclic()}.
*
* @return {@code true} the pixmap change and {@code false} otherwise.
* @see GSprite#setSkips(int)
* @see GSprite#skips()
* @see GSprite#isCyclic()
*/
public boolean next() {
if (!isCyclic() && cursor + 1 >= frames.size()) {
return false;
}
if (++skipCount % skips == 0) {
if (++cursor >= frames.size() && isCyclic()) {
cursor = 0;
}
current = frames.get(cursor);
bounds = current.getBounds();
skipCount = 0;
return true;
}
return false;
}
/**
* Sets the last {@link GPixMap} as the current one.
*/
public void last() {
if (!frames.isEmpty()) {
current = frames.get(frames.size() - 1);
bounds = current.getBounds();
skipCount = 0;
}
}
/**
* Tells if the {@code GSprite} should behave as a circular list.<br>
* The default value is {@code true}.
*
* @return {@code true} if the {@code GSprite} is circular, and
* {@code false} otherwise.
* @see GSprite#setCyclic(boolean)
*/
public boolean isCyclic() {
return cyclic;
}
/**
* Tells the {@code GSprite} to act as if the list of elements is circular
* (cyclic), that means that when the last {@link GPixMap} is reached,
* then it will continue with the first one, and viceversa.<br>
* The default value is {@code true}.
*
* @param cyclic {@code true} if the {@code GSprite} should act as circular,
* and {@code false} otherwise.
* @see GSprite#isCyclic()
*/
public void setCyclic(boolean cyclic) {
this.cyclic = cyclic;
}
/**
* Sets the number of skips, this means how many times should the
* {@link GSprite#next()} and {@link GSprite#prev()} should be called
* in order to actually work<br>
* The default value is 1
*
* @param s new number of skips
* @throws InvalidArgumentException if {@code s} is less than 1
*/
public void setSkips(int s) throws InvalidArgumentException {
if (s < 1) {
final String msg = "The number of skips must be bigger than zero";
throw new InvalidArgumentException(msg);
}
skips = s;
}
/**
* Retrieves the number of skips that are necessary in order for the
* {@link GSprite#next()} and {@link GSprite#prev()} methods to make an
* actual change.
*
* @return number of skips
*/
public int skips() {
return skips;
}
/**
* Sets a new pixel size for all of the {@link GPixMap} of this {@code
* GSprite}
*
* @param px new pixel size
* @throws InvalidArgumentException if the {@code size <= 0}
*/
public void setPixelSize(int px) throws InvalidArgumentException {
if (px < 1) {
final String msg = "The pixel size must be bigger than 1";
throw new InvalidArgumentException(msg);
}
pixelSize = px;
for (final GPixMap frame : frames) {
frame.setPixelSize(pixelSize);
}
if (current != null) {
bounds = current.getBounds();
}
}
/**
* Retrieves the pixel size of the {@code GSprite}
*
* @return pixel size
*/
public int getPixelSize() {
return pixelSize;
}
/**
* Tells the {@code GSprite} to draw the inner/outer lines
*
* @param grid {@code true} to draw the lines and {@code false} otherwise
*/
public void setDrawGrid(boolean grid) {
drawGrid = grid;
for (final GPixMap map : frames) {
map.setDrawLines(drawGrid);
}
}
/**
* Tells if the {@code GSprite} will draw the inner lines or not
*
* @return {@code true} if the {@code GSprite} is drawing the inner lines
* and {@code false} otherwise
*/
public boolean drawGrid() {
return drawGrid;
}
/**
* Retrieves the current bounds of this {@code GSprite}
*
* @return bounds of this GSprite
*/
public GRectangle getBounds() {
return bounds;
}
/**
* Tells the {@code GSprite} if it should draw itself
*
* @param visible {@code true} if the {@code GSprite} should be drawn and
* {@code false} otherwise
*/
public void setVisible (boolean visible) {
this.visible = visible;
for (final GPixMap frame : frames) {
frame.setVisible(visible);
}
}
/**
* Tells if the {@code GSprite} is visible
*
* @return {@code true} if the {@code GSprite} is visible and {@code false}
* otherwise
*/
public boolean isVisible() {
return visible;
}
@Override
public void draw(Graphics2D g) {
if (current != null) {
current.draw(g);
}
}
@Override
public GSprite clone() {
return new GSprite(this);
}
@Override
public void traslate(int x, int y) {
if (bounds != null) {
bounds.traslate(x, y);
}
for (final GPixMap frame : frames) {
frame.traslate(x, y);
}
}
@Override
public Iterator<GPixMap> iterator() {
return frames.iterator();
}
}