Skip to content

Instantly share code, notes, and snippets.

@RustyKnight
Last active June 3, 2018 10:42
Show Gist options
  • Save RustyKnight/1df628b8e7c4b2b93bc7c4d2a0b81e40 to your computer and use it in GitHub Desktop.
Save RustyKnight/1df628b8e7c4b2b93bc7c4d2a0b81e40 to your computer and use it in GitHub Desktop.
Simple example of a central "animator" and "animatable" properties, with easement
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class Animation {
public static void main(String[] args) {
new Test();
}
public Test() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
public TestPane() {
setLayout(null);
Slider slider1 = new Slider();
slider1.setBackground(Color.BLUE);
slider1.setLocation(0, 44);
add(slider1);
Slider slider2 = new Slider();
slider2.setBackground(Color.MAGENTA);
slider2.setLocation(0, 88);
add(slider2);
}
@Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
}
public class Slider extends JPanel {
private Animatable<Integer> ap;
private IntRange maxRange = new IntRange(44, 150);
private Duration duration = Duration.ofSeconds(1);
public Slider() {
setSize(44, 44);
addMouseListener(new MouseAdapter() {
@Override
public void mouseEntered(MouseEvent e) {
animateTo(150);
}
@Override
public void mouseExited(MouseEvent e) {
animateTo(44);
}
public void animateTo(int to) {
if (ap != null) {
Animator.INSTANCE.remove(ap);
}
IntRange animationRange = new IntRange(getWidth(), to);
ap = new IntAnimatable(animationRange, maxRange, duration, new AnimatableListener<Integer>() {
@Override
public void stateChanged(Animatable<Integer> animator) {
setSize(animator.getValue(), 44);
repaint();
}
});
Animator.INSTANCE.add(ap);
}
});
}
}
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) {
Iterator<Animatable> it = properies.iterator();
while (it.hasNext()) {
Animatable ap = it.next();
if (ap.tick()) {
it.remove();
}
}
if (properies.isEmpty()) {
timer.stop();
}
}
});
}
public void add(Animatable ap) {
properies.add(ap);
timer.start();
}
public void remove(Animatable ap) {
properies.remove(ap);
if (properies.isEmpty()) {
timer.stop();
}
}
}
public interface Animatable<T> {
public Range<T> getRange();
public T getValue();
public boolean tick();
public void setDuration(Duration duration);
public Duration getDuration();
public Easement getEasement();
}
public interface AnimatableListener<T> {
public void stateChanged(Animatable<T> animator);
}
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);
}
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> listener;
private Easement easement;
public AbstractAnimatable(Range<T> range, AnimatableListener<T> listener) {
this.range = range;
this.value = range.getFrom();
this.listener = listener;
}
public AbstractAnimatable(Range<T> range, Easement easement, AnimatableListener<T> listener) {
this(range, listener);
this.easement = easement;
}
public void setEasement(Easement easement) {
this.easement = easement;
}
@Override
public Easement getEasement() {
return easement;
}
public void setDuration(Duration duration) {
this.duration = duration;
}
public Duration getDuration() {
return duration;
}
public Range<T> getRange() {
return range;
}
@Override
public T getValue() {
return value;
}
@Override
public boolean tick() {
if (startTime == null) {
startTime = LocalDateTime.now();
}
Duration duration = getDuration();
Duration runningTime = Duration.between(startTime, LocalDateTime.now());
Duration timeRemaining = duration.minus(runningTime);
boolean done = false;
if (timeRemaining.isNegative()) {
done = true;
}
double progress = (runningTime.toMillis() / (double) duration.toMillis());
Easement easement = getEasement();
if (!done && easement != null) {
progress = easement.interpolate(progress);
}
value = getRange().valueAt(progress);
listener.stateChanged(this);
return progress >= 1.0;
}
}
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;
}
}
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);
}
}
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;
}
}
}
}
@RustyKnight
Copy link
Author

RustyKnight commented Jun 2, 2018

Based on this Q and A https://stackoverflow.com/questions/50651974/sliding-effect-menu-with-jcomponents and the SplineInterpolator from https://gist.github.com/RustyKnight/4da7747831e172dbb6f77d8310ee0023, this is a simple idea of how to implement a central "animator" (main-loop) and generate "animation" through "animatable" objects which calculate progression of their animation (based on a range of values) over time

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment