Last active
December 9, 2016 15:41
-
-
Save CAD97/9574b1a26a55d908694d019b0d06f986 to your computer and use it in GitHub Desktop.
Animation Test
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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