Start with the idea that animation is the illusion of change over time. So you need to be able to move from one state to another based on the amount of available time and the current time used.
So, what we really need is some way to change the size of the shape dynamically based on the amount of time that has been played.
So, the first thing would be to take your drawing code and create a Shape
out of it. This provides as with a base line.
public class HeartShape extends Path2D.Double {
public HeartShape() {
moveTo(0.0, -150.0);
curveTo(-200.0, -25.0, -200.0, 225.0, 0, 100.0);
moveTo(0.0, -150.0);
curveTo(200.0, -25.0, 205.0, 235.0, 0, 100.0);
}
}
This will become our "origin", it will make more sense later
Next we need to setup the playback state, this includes the "start time" or "anchor time" and the desired playback length.
Once we have that, we can use a Timer
to calculate the amount of time played and generate a "progress" value from it.
As a side note, time based animations like this are more flexible and generally look bette then linear based progressions (IMHO)
We can then use the progression value as a bases for re-sizing the shape.
To do that, we take advantage of the Shape
API...
Shape shape = heartShape.createTransformedShape(AffineTransform.getScaleInstance(scale, scale));
This all might look something like...
class MyJPanel extends JPanel {
private static final long serialVersionUID = 1L;
private HeartShape heartShape = new HeartShape();
private Instant anchorPoint;
private Duration playDuration = Duration.ofSeconds(5);
private double scale = 1;
public MyJPanel() {
super();
setPreferredSize(new Dimension(800, 600));
setBackground(new Color(200, 200, 255));
Timer timer = new Timer(5, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (anchorPoint == null) {
anchorPoint = Instant.now();
}
Duration playTime = Duration.between(anchorPoint, Instant.now());
double progress = (double)playTime.toMillis() / playDuration.toMillis();
if (progress >= 1) {
anchorPoint = null;
progress = 1;
}
scale = progress;
repaint();
}
});
timer.start();
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
AffineTransform gat = new AffineTransform();
gat.scale(1.0, -1.0);
gat.translate(getWidth() / 2.0, -getHeight() / 2.0);
g2d.transform(gat);
Shape shape = heartShape.createTransformedShape(AffineTransform.getScaleInstance(scale, scale));
g2d.setPaint(Color.PINK);
g2d.fill(shape);
g2d.setStroke(new BasicStroke(5.0f));
g2d.setPaint(Color.BLACK);
g2d.draw(shape);
g2d.dispose();
}
}
Okay, but that simple moves from 0-1, we need it to "bounce" (from a start point to end point and back again).
At this point, we've entered the territory of "key frames", but I'm going to try and keep it simple, but if you're interested, you can look at:
But we warned, it will like make your head - it does mine
First, we need to define a "range" of valid values. This will represent the range within which the shape will be scaled...
private double lowerRange = 0.75;
private double upperRange = 1.25;
Here, I've just chosen some arbitrary values around 1.0
, you can play with these and see what you like.
Next, in the Timer
, we need to change how the scale
is calculated to something more like...
scale = ((upperRange - lowerRange) * progress) + lowerRange;
repaint();
Ok, cool we now have the shape growing from 0.75
to 1.25
, but it's still only in a linear direction.
Ok, this is where key frames and a time line would be really helpful, however, since all we really want is something like "auto reverse" (except within the applied duration), we can do something more like...
if (progress > 0.5) {
progress = 1.0 - progress;
}
scale = ((upperRange - lowerRange) * progress) + lowerRange;
repaint();
And, presto, we have a pulsating heart , sweet
Ok, but wait, that's a bit slow. Well, then, just modify the playDuration
, maybe try private Duration playDuration = Duration.ofSeconds(1);
instead!
See how easy that was, you only need to change one variable and you've completely changed how the animation plays!! Told you time based animations rocked
Final runnable example...
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D;
import java.time.Duration;
import java.time.Instant;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
public class HelloCurves {
public HelloCurves() {
JFrame jf = new JFrame("HelloCurves");
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel jp = new MyJPanel();
jf.add(jp);
jf.pack();
jf.setResizable(false);
jf.setLocationRelativeTo(null);
jf.setVisible(true);
}
public static void main(String[] args) {
EventQueue.invokeLater(HelloCurves::new);
}
class MyJPanel extends JPanel {
private static final long serialVersionUID = 1L;
private HeartShape heartShape = new HeartShape();
private Instant anchorPoint;
private Duration playDuration = Duration.ofSeconds(1);
private double scale = 1;
private double lowerRange = 0.75;
private double upperRange = 1.25;
public MyJPanel() {
super();
setPreferredSize(new Dimension(800, 600));
setBackground(new Color(200, 200, 255));
Timer timer = new Timer(5, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (anchorPoint == null) {
anchorPoint = Instant.now();
}
Duration playTime = Duration.between(anchorPoint, Instant.now());
double progress = (double) playTime.toMillis() / playDuration.toMillis();
if (progress >= 1) {
anchorPoint = null;
progress = 1;
}
if (progress > 0.5) {
progress = 1.0 - progress;
}
scale = ((upperRange - lowerRange) * progress) + lowerRange;
repaint();
}
});
timer.start();
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
AffineTransform gat = new AffineTransform();
gat.scale(1.0, -1.0);
gat.translate(getWidth() / 2.0, -getHeight() / 2.0);
g2d.transform(gat);
Shape shape = heartShape.createTransformedShape(AffineTransform.getScaleInstance(scale, scale));
g2d.setPaint(Color.PINK);
g2d.fill(shape);
g2d.setStroke(new BasicStroke(5.0f));
g2d.setPaint(Color.BLACK);
g2d.draw(shape);
g2d.dispose();
}
}
public class HeartShape extends Path2D.Double {
public HeartShape() {
moveTo(0.0, -150.0);
curveTo(-200.0, -25.0, -200.0, 225.0, 0, 100.0);
moveTo(0.0, -150.0);
curveTo(200.0, -25.0, 205.0, 235.0, 0, 100.0);
}
}
}
Be sure you can explain this back to your lecture, because if you presented me with this code, I'd be asking a lot of questions about how it works and why you choose this path