Last active
June 3, 2018 10:42
-
-
Save RustyKnight/1df628b8e7c4b2b93bc7c4d2a0b81e40 to your computer and use it in GitHub Desktop.
Simple example of a central "animator" and "animatable" properties, with easement
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.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; | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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