I made a class capable of moving Polygons around on a Polygon in a mostly smooth manner. Disclaimer: this was part of a school project (thus the use of Polygon rather than Image), but this code is entirely outside of the scope of the assignment, and the assignment has already been graded.
The project was a Bejeweled clone, so the Animator is capable of the two animations needed for that clone: it can take two ColoredPolygons (a simple wrapper around a Color and a Polygon) and swap their position, or a List of ColoredPolygons and translate them all some dx/dy. A built in assumption in this animation is that only one animation will be active at a time, and that the area covered by the animation only contains the Polygons that were passed in.
package cad97.bejeweled.animation;
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
*/
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 {
final Color color;
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 finished.
* (E.g. the frame before a fully finished 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 finished.
* (E.g. the frame before a fully finished 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
*/
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.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();
}
}
I've never done actual animation before, so this is likely a hacky solution that goes against best practices. This is within a Swing context, so any suggestions using the Swing library and not just AWT are welcome.
Here is a minimal harness for this code on Gist. This is not part of the review, merely a way to enable ease of testing. In the creation of this harness, I noticed a bug on line 102 of this class, the fix to which is commented in the Gist.