Last active
August 19, 2018 01:26
-
-
Save RustyKnight/6eabf86d11e963d265ba07a2a78e0616 to your computer and use it in GitHub Desktop.
A simple Java/Swing based time based animation framework
This file contains hidden or 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.time.Duration; | |
import java.time.LocalDateTime; | |
public abstract class AbstractAnimatable<T> implements Animatable<T> { | |
private Range<T> range; | |
private LocalDateTime startTime; | |
private Duration duration = Duration.ofSeconds(5); | |
private T value; | |
private AnimatableListener<T> animatableListener; | |
private AnimatableLifeCycleListener<T> lifeCycleListener; | |
private Easement easement; | |
private double rawOffset; | |
public AbstractAnimatable(Range<T> range, Duration duration, AnimatableListener<T> listener) { | |
this.range = range; | |
this.value = range.getFrom(); | |
this.animatableListener = listener; | |
} | |
public AbstractAnimatable(Range<T> range, Duration duration, AnimatableListener<T> listener, AnimatableLifeCycleListener<T> lifeCycleListener) { | |
this(range, duration, listener); | |
this.lifeCycleListener = lifeCycleListener; | |
} | |
public AbstractAnimatable(Range<T> range, Duration duration, Easement easement, AnimatableListener<T> listener) { | |
this(range, duration, listener); | |
this.easement = easement; | |
} | |
public AbstractAnimatable(Range<T> range, Duration duration, Easement easement, AnimatableListener<T> listener, AnimatableLifeCycleListener<T> lifeCycleListener) { | |
this(range, duration, easement, listener); | |
this.lifeCycleListener = lifeCycleListener; | |
} | |
public void setEasement(Easement easement) { | |
this.easement = easement; | |
} | |
@Override | |
public Easement getEasement() { | |
return easement; | |
} | |
public Duration getDuration() { | |
return duration; | |
} | |
public Range<T> getRange() { | |
return range; | |
} | |
public void setRange(Range<T> range) { | |
this.range = range; | |
} | |
@Override | |
public T getValue() { | |
return value; | |
} | |
protected void setDuration(Duration duration) { | |
this.duration = duration; | |
} | |
public double getCurrentProgress(double rawProgress) { | |
Easement easement = getEasement(); | |
double progress = Math.min(1.0, Math.max(0.0, getRawProgress())); | |
if (easement != null) { | |
progress = easement.interpolate(progress); | |
} | |
return Math.min(1.0, Math.max(0.0, progress)); | |
} | |
public double getRawProgress() { | |
if (startTime == null) { | |
return 0.0; | |
} | |
Duration duration = getDuration(); | |
Duration runningTime = Duration.between(startTime, LocalDateTime.now()); | |
double progress = rawOffset + (runningTime.toMillis() / (double) duration.toMillis()); | |
return Math.min(1.0, Math.max(0.0, progress)); | |
} | |
@Override | |
public void tick() { | |
if (startTime == null) { | |
startTime = LocalDateTime.now(); | |
fireAnimationStarted(); | |
} | |
double rawProgress = getRawProgress(); | |
double progress = getCurrentProgress(rawProgress); | |
if (rawProgress >= 1.0) { | |
progress = 1.0; | |
} | |
value = getRange().valueAt(progress); | |
fireAnimationChanged(); | |
if (rawProgress >= 1.0) { | |
fireAnimationCompleted(); | |
} | |
} | |
@Override | |
public void start() { | |
if (startTime != null) { | |
// Restart? | |
return; | |
} | |
Animator.INSTANCE.add(this); | |
} | |
@Override | |
public void stop() { | |
stopWithNotitifcation(true); | |
} | |
@Override | |
public void pause() { | |
rawOffset += getRawProgress(); | |
stopWithNotitifcation(false); | |
double remainingProgress = 1.0 - rawOffset; | |
Duration remainingTime = getDuration().minusMillis((long) remainingProgress); | |
setDuration(remainingTime); | |
lifeCycleListener.animationStopped(this); | |
} | |
protected void fireAnimationChanged() { | |
if (animatableListener == null) { | |
return; | |
} | |
animatableListener.animationChanged(this); | |
} | |
protected void fireAnimationCompleted() { | |
stopWithNotitifcation(false); | |
if (lifeCycleListener == null) { | |
return; | |
} | |
lifeCycleListener.animationCompleted(this); | |
} | |
protected void fireAnimationStarted() { | |
if (lifeCycleListener == null) { | |
return; | |
} | |
lifeCycleListener.animationStarted(this); | |
} | |
protected void fireAnimationPaused() { | |
if (lifeCycleListener == null) { | |
return; | |
} | |
lifeCycleListener.animationPaused(this); | |
} | |
protected void stopWithNotitifcation(boolean notify) { | |
Animator.INSTANCE.remove(this); | |
startTime = null; | |
if (notify) { | |
if (lifeCycleListener == null) { | |
return; | |
} | |
lifeCycleListener.animationStopped(this); | |
} | |
} | |
} |
This file contains hidden or 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.time.Duration; | |
public interface Animatable<T> { | |
public Range<T> getRange(); | |
public T getValue(); | |
public void tick(); | |
public Duration getDuration(); | |
public Easement getEasement(); | |
// Wondering if these should be part of a secondary interface | |
// Provide a "self managed" unit of work | |
public void start(); | |
public void stop(); | |
public void pause(); | |
} |
This file contains hidden or 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.event.ActionEvent; | |
import java.awt.event.ActionListener; | |
import java.util.ArrayList; | |
import java.util.Iterator; | |
import java.util.List; | |
import javax.swing.Timer; | |
public enum Animator { | |
INSTANCE; | |
private Timer timer; | |
private List<Animatable> properies; | |
private Animator() { | |
properies = new ArrayList<>(5); | |
timer = new Timer(5, new ActionListener() { | |
@Override | |
public void actionPerformed(ActionEvent e) { | |
List<Animatable> copy = new ArrayList<>(properies); | |
Iterator<Animatable> it = copy.iterator(); | |
while (it.hasNext()) { | |
Animatable ap = it.next(); | |
ap.tick(); | |
} | |
if (properies.isEmpty()) { | |
timer.stop(); | |
} | |
} | |
}); | |
} | |
public void add(Animatable ap) { | |
properies.add(ap); | |
timer.start(); | |
} | |
protected void removeAll(List<Animatable> completed) { | |
properies.removeAll(completed); | |
} | |
public void remove(Animatable ap) { | |
properies.remove(ap); | |
if (properies.isEmpty()) { | |
timer.stop(); | |
} | |
} | |
} |
This file contains hidden or 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
public class ColorAnimatable extends AbstractAnimatable<Color> { | |
public ColorAnimatable(ColorRange animationRange, Duration duration, AnimatableListener<Color> listener) { | |
super(animationRange, duration, listener); | |
} | |
} |
This file contains hidden or 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
public class ColorRange extends Range<Color> { | |
public ColorRange(Color from, Color to) { | |
super(from, to); | |
} | |
@Override | |
public Color valueAt(double progress) { | |
return blend(getTo(), getFrom(), progress); | |
} | |
protected Color blend(Color color1, Color color2, double ratio) { | |
float r = (float) ratio; | |
float ir = (float) 1.0 - r; | |
float red = color1.getRed() * r + color2.getRed() * ir; | |
float green = color1.getGreen() * r + color2.getGreen() * ir; | |
float blue = color1.getBlue() * r + color2.getBlue() * ir; | |
float alpha = color1.getAlpha() * r + color2.getAlpha() * ir; | |
red = Math.min(255f, Math.max(0f, red)); | |
green = Math.min(255f, Math.max(0f, green)); | |
blue = Math.min(255f, Math.max(0f, blue)); | |
alpha = Math.min(255f, Math.max(0f, alpha)); | |
Color color = null; | |
try { | |
color = new Color((int) red, (int) green, (int) blue, (int) alpha); | |
} catch (IllegalArgumentException exp) { | |
exp.printStackTrace(); | |
} | |
return color; | |
} | |
} |
This file contains hidden or 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.geom.Point2D; | |
import java.util.ArrayList; | |
import java.util.List; | |
public enum Easement { | |
SLOWINSLOWOUT(1d, 0d, 0d, 1d), FASTINSLOWOUT(0d, 0d, 1d, 1d), SLOWINFASTOUT(0d, 1d, 0d, 0d), SLOWIN(1d, 0d, 1d, 1d), SLOWOUT(0d, 0d, 0d, 1d); | |
private final double[] points; | |
private final List<PointUnit> normalisedCurve; | |
private Easement(double x1, double y1, double x2, double y2) { | |
points = new double[]{x1, y1, x2, y2}; | |
final List<Double> baseLengths = new ArrayList<>(); | |
double prevX = 0; | |
double prevY = 0; | |
double cumulativeLength = 0; | |
for (double t = 0; t <= 1; t += 0.01) { | |
Point2D xy = getXY(t); | |
double length = cumulativeLength + Math.sqrt((xy.getX() - prevX) * (xy.getX() - prevX) + (xy.getY() - prevY) * (xy.getY() - prevY)); | |
baseLengths.add(length); | |
cumulativeLength = length; | |
prevX = xy.getX(); | |
prevY = xy.getY(); | |
} | |
normalisedCurve = new ArrayList<>(baseLengths.size()); | |
int index = 0; | |
for (double t = 0; t <= 1; t += 0.01) { | |
double length = baseLengths.get(index++); | |
double normalLength = length / cumulativeLength; | |
normalisedCurve.add(new PointUnit(t, normalLength)); | |
} | |
} | |
public double interpolate(double fraction) { | |
int low = 1; | |
int high = normalisedCurve.size() - 1; | |
int mid = 0; | |
while (low <= high) { | |
mid = (low + high) / 2; | |
if (fraction > normalisedCurve.get(mid).getPoint()) { | |
low = mid + 1; | |
} else if (mid > 0 && fraction < normalisedCurve.get(mid - 1).getPoint()) { | |
high = mid - 1; | |
} else { | |
break; | |
} | |
} | |
/* | |
* The answer lies between the "mid" item and its predecessor. | |
*/ | |
final PointUnit prevItem = normalisedCurve.get(mid - 1); | |
final double prevFraction = prevItem.getPoint(); | |
final double prevT = prevItem.getDistance(); | |
final PointUnit item = normalisedCurve.get(mid); | |
final double proportion = (fraction - prevFraction) / (item.getPoint() - prevFraction); | |
final double interpolatedT = prevT + (proportion * (item.getDistance() - prevT)); | |
return getY(interpolatedT); | |
} | |
protected Point2D getXY(double t) { | |
final double invT = 1 - t; | |
final double b1 = 3 * t * invT * invT; | |
final double b2 = 3 * t * t * invT; | |
final double b3 = t * t * t; | |
final Point2D xy = new Point2D.Double((b1 * points[0]) + (b2 * points[2]) + b3, (b1 * points[1]) + (b2 * points[3]) + b3); | |
return xy; | |
} | |
protected double getY(double t) { | |
final double invT = 1 - t; | |
final double b1 = 3 * t * invT * invT; | |
final double b2 = 3 * t * t * invT; | |
final double b3 = t * t * t; | |
return (b1 * points[2]) + (b2 * points[3]) + b3; | |
} | |
protected class PointUnit { | |
private final double distance; | |
private final double point; | |
public PointUnit(double distance, double point) { | |
this.distance = distance; | |
this.point = point; | |
} | |
public double getDistance() { | |
return distance; | |
} | |
public double getPoint() { | |
return point; | |
} | |
} | |
} |
This file contains hidden or 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
public class IntAnimatable extends AbstractAnimatable<Integer> { | |
public IntAnimatable(IntRange animationRange, IntRange maxRange, Duration duration, AnimatableListener<Integer> listener) { | |
super(animationRange, Easement.SLOWINSLOWOUT, listener); | |
int maxDistance = maxRange.getDistance(); | |
int aniDistance = animationRange.getDistance(); | |
double progress = Math.min(100, Math.max(0, Math.abs(aniDistance / (double) maxDistance))); | |
Duration remainingDuration = Duration.ofMillis((long) (duration.toMillis() * progress)); | |
setDuration(remainingDuration); | |
} | |
} |
This file contains hidden or 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
public class IntRange extends Range<Integer> { | |
public IntRange(Integer from, Integer to) { | |
super(from, to); | |
} | |
public Integer getDistance() { | |
return getTo() - getFrom(); | |
} | |
@Override | |
public Integer valueAt(double progress) { | |
int distance = getDistance(); | |
int value = (int) Math.round((double) distance * progress); | |
value += getFrom(); | |
int from = getFrom(); | |
int to = getTo(); | |
if (from < to) { | |
value = Math.max(from, Math.min(to, value)); | |
} else { | |
value = Math.max(to, Math.min(from, value)); | |
} | |
return value; | |
} | |
} |
This file contains hidden or 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
public abstract class Range<T> { | |
private T from; | |
private T to; | |
public Range(T from, T to) { | |
this.from = from; | |
this.to = to; | |
} | |
public T getFrom() { | |
return from; | |
} | |
public T getTo() { | |
return to; | |
} | |
@Override | |
public String toString() { | |
return "From " + getFrom() + " to " + getTo(); | |
} | |
public abstract T valueAt(double progress); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This represents a simple, centralised animation engine for Java/Swing. It has a central "loop" which provides "ticks" to something which is "animatable".
The
Animatable
s are time based animations (something which is animated over a period of time) which generates a "progression" of how far they are through their own animation cycle. This information is used in conjunction with aRange
which is capable of calculating a value between two points based on the normalised progression, this value is then used to actually update the UI in some specific manner.Most of the animation I need to do is change value "A" to value "B" over a period of time "n". This API concept provides the backbone for that functionality.
While only demonstrating a
int
andColor
range, the concept can be applied to anything which can be blended from a "start" value through to a "end" value over a normalised period (0-1
)The API is provides some basic easement to make the animation more "natural"
The reason behind this implementation is to:
I tend to work on low end performing machines, so a straight "linear, incremental" based solution just isn't going to work (and wouldn't work on a high end machine either). A time based solution provides better scalability across different platforms
Feature's I'd like to investigate:
Animatable
can be reversed at any point during it's operation without having to re-create a newAnimatable
(manually) based on the current state