Skip to content

Instantly share code, notes, and snippets.

@CAD97
Last active December 9, 2016 15:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save CAD97/9574b1a26a55d908694d019b0d06f986 to your computer and use it in GitHub Desktop.
Save CAD97/9574b1a26a55d908694d019b0d06f986 to your computer and use it in GitHub Desktop.
Animation Test
import javax.swing.*;
import java.awt.*;
import java.awt.geom.Area;
import java.util.List;
/**
* Animate Polygons around on a Graphics object.
*
* @author Christopher Durham
* @date 11/25/2016
*/
@SuppressWarnings("WeakerAccess")
public final class Animator {
public static final Animator INSTANCE = new Animator();
private final static int ANIMATION_FRAMES = 30;
private final static int ANIMATION_FPS = 60;
private final static int ANIMATION_INTERVAL = 1000 / ANIMATION_FPS;
/**
* Convenience class bundling a {@link Color} and a {@link Polygon}
*/
public static class ColoredPolygon {
public final Color color;
public final Polygon polygon;
public ColoredPolygon(Color color, Polygon polygon) {
this.color = color;
this.polygon = polygon;
}
}
/**
* Animate the swap of two Polygons on a Graphics object.
* <p>
* The parameter Polygons are mutated during the execution of this method.
* <p>
* A redraw of the animated area in the {@code after} callback is suggested,
* as the final frame of this animation is not guaranteed to be fully swapped.
* (E.g. the frame before a fully swapped position.)
*
* @param bgColor the background Color used to erase frames
* @param first the first ColoredPolygon being switched
* @param second the second ColoredPolygon being switched
* @param g the Graphics object to draw on
* @param after callback run on UI thread after animation is finished
*/
public void translateSwap(final Color bgColor, final ColoredPolygon first, final ColoredPolygon second,
final Graphics g, final Runnable after) {
new SwingWorker<Void, Void>() {
@Override
protected Void doInBackground() throws Exception {
final Rectangle b1 = first.polygon.getBounds();
final Rectangle b2 = second.polygon.getBounds();
final int deltaX = (b1.x - b2.x) / ANIMATION_FRAMES;
final int deltaY = (b1.y - b2.y) / ANIMATION_FRAMES;
final Rectangle animationBounds = b1.union(b2);
for (int i = 0; i < ANIMATION_FRAMES; i++) {
first.polygon.translate(-deltaX, -deltaY);
second.polygon.translate(deltaX, deltaY);
SwingUtilities.invokeAndWait(() -> {
g.setColor(bgColor);
g.fillRect(animationBounds.x, animationBounds.y, animationBounds.width, animationBounds.height);
g.setColor(first.color);
g.fillPolygon(first.polygon);
g.setColor(second.color);
g.fillPolygon(second.polygon);
});
Thread.sleep(ANIMATION_INTERVAL);
}
SwingUtilities.invokeLater(after);
return null;
}
}.execute();
}
/**
* Translate a group of Polygons a direction on a Graphics object.
* <p>
* The passed Polygons are mutated during the execution of this method.
* <p>
* A redraw of the animated area in the {@code after} callback is suggested,
* as the final frame of this animation is not guaranteed to be fully swapped.
* (E.g. the frame before a fully swapped position.)
*
* @param bgColor the background Color used to erase frames
* @param polygons a list of ColoredPolygons to translate
* @param dx the delta x to translate the polygons
* @param dy the delta y to translate the polygons
* @param g the Graphics object to draw on
* @param after callback run on UI thread after animation is finished
*/
@SuppressWarnings("SameParameterValue")
public void batchTranslate(final Color bgColor, final List<ColoredPolygon> polygons,
final int dx, final int dy, final Graphics2D g, final Runnable after) {
new SwingWorker<Void, Void>() {
@Override
protected Void doInBackground() throws Exception {
final Area animationBounds = polygons.stream().sequential()
.map(it -> it.polygon).map(Polygon::getBounds).peek(it -> {
it.grow(dx / 2, dy / 2);
//it.grow(Math.abs(dx) / 2, Math.abs(dy) / 2);
it.translate(dx / 2, dy / 2);
}).map(Area::new).reduce((lhs, rhs) -> {
if (lhs == null) return rhs;
rhs.add(lhs);
return rhs;
})
.orElseThrow(AssertionError::new);
final int deltaX = dx / ANIMATION_FRAMES;
final int deltaY = dy / ANIMATION_FRAMES;
for (int i = 0; i < ANIMATION_FRAMES; i++) {
polygons.forEach(it -> it.polygon.translate(deltaX, deltaY));
SwingUtilities.invokeAndWait(() -> {
g.setColor(bgColor);
g.fill(animationBounds);
polygons.forEach(it -> {
g.setColor(it.color);
g.fill(it.polygon);
});
});
Thread.sleep(ANIMATION_INTERVAL);
}
SwingUtilities.invokeLater(after);
return null;
}
}.execute();
}
}
import java.awt.*;
import java.util.Arrays;
import static java.util.Arrays.stream;
public class Jewel {
/**
* The size of this Jewel.
*/
public static final int SIZE = 100;
/**
* The margin size around this Jewel.
*/
public static final double MARGIN = 0.05;
/**
* The type of this Jewel.
*/
public final JewelType type;
/**
* The Polygon that is actually drawn.
* <p>
* Set in {@link #recalculateDrawnPolygon()}.
*/
private Polygon drawnShape;
private boolean dirty;
private Point point;
/**
* Construct a Jewel object.
*
* @param point the location of this Jewel
* @param type the type of Jewel this is
*/
public Jewel(Point point, JewelType type) {
this.type = type;
this.setPosition(point);
}
/**
* Set the location of this Jewel on the screen.
* <p>
* No matter how many times this is called in one frame,
* the drawn polygon is recalculated once at draw time.
*
* @param point the new location
*/
public void setPosition(Point point) {
this.point = point;
this.dirty = true;
}
/**
* Recalculate the {@link #drawnShape} for this Jewel.
* <p>
* Drawn coordinate =
* <ul>
* <li>The coordinate of this Jewel multiplied by a Jewel's SIZE (calculates the low border of the AABB) plus
* <li>The MARGIN factor times the SIZE of a Jewel (the amount of space before this Jewel) plus
* <li>The coordinate of the source point scaled to the correct size:
* <ul>
* <li>Divide by 1000 to get to a scalar [0,1]
* <li>Multiply by a Jewel's SIZE to get on the Jewel's scale
* <li>Multiply by (1 - MARGIN * 2) to scale to the space that avoids margins on both sides.
* </ul>
* </ul>
*
* <!-- Render this JavaDoc as HTML for a pretty list version of the algorithm -->
*/
private void recalculateDrawnPolygon() {
// Note that compile-time constant math is optimized out.
this.drawnShape = new Polygon(
stream(this.type.shape.xpoints).map(point ->
(int) (((this.point.x + MARGIN) * SIZE) + (point * (SIZE / 1000d * (1 - MARGIN * 2))))
).toArray(),
stream(this.type.shape.ypoints).map(point ->
(int) (((this.point.y + MARGIN) * SIZE) + (point * (SIZE / 1000d * (1 - MARGIN * 2))))
).toArray(),
this.type.shape.npoints
);
this.dirty = false;
}
/**
* Draw this Jewel within a given {@link Graphics} context.
*
* @param g the Graphics to draw on
*/
public void draw(Graphics g) {
if (this.dirty) {
this.recalculateDrawnPolygon();
}
g.setColor(this.type.color);
g.fillPolygon(this.drawnShape);
}
/**
* Get a copy of the current drawing polygon.
*
* @return a polygon that, when drawn, appears like this Jewel
*/
public Polygon getPolygon() {
if (this.dirty)
this.recalculateDrawnPolygon();
return new Polygon(this.drawnShape.xpoints, this.drawnShape.ypoints, this.drawnShape.npoints);
}
/**
* Get the point at which this Jewel is located on its Board.
*
* @return the point at which this Jewel is located on its Board
*/
public Point getPoint() {
return this.point;
}
}
import java.awt.*;
import java.util.Random;
/**
* The shapes that a {@link Jewel} can be.
* <p>
* Shapes fill the AABB (0,0),(1000,1000)
*/
public enum JewelType {
DIAMOND(new Polygon(
new int[]{1000, 500, 0, 500},
new int[]{500, 0, 500, 1000},
4
), Color.BLUE),
OCTAGON(new Polygon(
new int[]{0, 250, 750, 1000, 1000, 750, 250, 0},
new int[]{250, 0, 0, 250, 750, 1000, 1000, 750},
8
), Color.ORANGE);
final Polygon shape;
public final Color color;
JewelType(Polygon shape, Color color) {
this.shape = shape;
this.color = color;
}
}
import javax.swing.*;
import java.awt.*;
import java.util.Arrays;
import java.util.Scanner;
public class Main extends JFrame {
private static AnimationPanel panel;
private Main() {
super("Animation");
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
panel = new AnimationPanel();
this.add(panel);
pack();
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
JFrame jFrame = new Main();
jFrame.setVisible(true);
});
try (Scanner scanner = new Scanner(System.in)) {
while (true) {
System.out.print(">");
String input = scanner.nextLine().trim();
if (panel != null) {
switch (input) {
case "swap":
panel.swap();
break;
case "down":
panel.down();
break;
case "up":
panel.up();
break;
case "right":
panel.right();
break;
case "left":
panel.left();
break;
default:
System.out.println("Invalid command.");
System.out.println(" - swap");
System.out.println(" - down");
System.out.println(" - up");
System.out.println(" - right");
System.out.println(" - left");
}
}
}
}
}
class AnimationPanel extends JPanel {
Jewel jewel1;
Jewel jewel2;
AnimationPanel() {
setPreferredSize(new Dimension(600, 600));
jewel1 = new Jewel(new Point(2, 1), JewelType.DIAMOND);
jewel2 = new Jewel(new Point(3, 1), JewelType.OCTAGON);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
jewel1.draw(g);
jewel2.draw(g);
}
private void up() {
Point point1 = new Point(jewel1.getPoint());
Point point2 = new Point(jewel2.getPoint());
point1.translate(0, -1);
point2.translate(0, -1);
Animator.INSTANCE.batchTranslate(
getBackground(),
Arrays.asList(
new Animator.ColoredPolygon(jewel1.type.color, jewel1.getPolygon()),
new Animator.ColoredPolygon(jewel2.type.color, jewel2.getPolygon())
),
0, -Jewel.SIZE, (Graphics2D) this.getGraphics(), () -> {
jewel1.setPosition(point1);
jewel2.setPosition(point2);
repaint();
}
);
}
private void down() {
Point point1 = new Point(jewel1.getPoint());
Point point2 = new Point(jewel2.getPoint());
point1.translate(0, 1);
point2.translate(0, 1);
Animator.INSTANCE.batchTranslate(
getBackground(),
Arrays.asList(
new Animator.ColoredPolygon(jewel1.type.color, jewel1.getPolygon()),
new Animator.ColoredPolygon(jewel2.type.color, jewel2.getPolygon())
),
0, Jewel.SIZE, (Graphics2D) this.getGraphics(), () -> {
jewel1.setPosition(point1);
jewel2.setPosition(point2);
repaint();
}
);
}
private void right() {
Point point1 = new Point(jewel1.getPoint());
Point point2 = new Point(jewel2.getPoint());
point1.translate(1, 0);
point2.translate(1, 0);
Animator.INSTANCE.batchTranslate(
getBackground(),
Arrays.asList(
new Animator.ColoredPolygon(jewel1.type.color, jewel1.getPolygon()),
new Animator.ColoredPolygon(jewel2.type.color, jewel2.getPolygon())
),
Jewel.SIZE, 0, (Graphics2D) this.getGraphics(), () -> {
jewel1.setPosition(point1);
jewel2.setPosition(point2);
repaint();
}
);
}
private void left() {
Point point1 = new Point(jewel1.getPoint());
Point point2 = new Point(jewel2.getPoint());
point1.translate(-1, 0);
point2.translate(-1, 0);
Animator.INSTANCE.batchTranslate(
getBackground(),
Arrays.asList(
new Animator.ColoredPolygon(jewel1.type.color, jewel1.getPolygon()),
new Animator.ColoredPolygon(jewel2.type.color, jewel2.getPolygon())
),
-Jewel.SIZE, 0, (Graphics2D) this.getGraphics(), () -> {
jewel1.setPosition(point1);
jewel2.setPosition(point2);
repaint();
}
);
}
private void swap() {
Point point1 = jewel2.getPoint();
Point point2 = jewel1.getPoint();
Animator.INSTANCE.translateSwap(
this.getBackground(),
new Animator.ColoredPolygon(jewel1.type.color, jewel1.getPolygon()),
new Animator.ColoredPolygon(jewel2.type.color, jewel2.getPolygon()),
this.getGraphics(), () -> {
jewel1.setPosition(point1);
jewel2.setPosition(point2);
repaint();
}
);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment