0

I've been desperately trying to figure out how to create an animation for my course homework. The object was to create a heart using Path2D.curveTo, and then simulate a heartbeat animation as well.

    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.geom.AffineTransform;
    import java.awt.geom.Path2D;
    import javax.swing.Timer;

    import javax.swing.JFrame;
    import javax.swing.JPanel;
    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;
            public MyJPanel() {
                super();
                setPreferredSize(new Dimension(800, 600));
                setBackground(new Color(200, 200, 255));
            }
            @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);
                Path2D p2d = new Path2D.Double();

//Larger First Heart
    //            p2d.moveTo(0.0, -250.0);
    //            p2d.curveTo(-350.0, -125.0,-350.0, 375.0, 0, 175.0);
    //            p2d.curveTo(350.0, 375.0,350.0, -125.0, 0, -250.0);

//Smaller Second Heart
                p2d.moveTo(0.0, -150.0);
                p2d.curveTo(-200.0, -25.0,-200.0, 225.0, 0, 100.0);
                p2d.moveTo(0.0, -150.0);
                p2d.curveTo(200.0, -25.0,205.0, 235.0, 0, 100.0);

                g2d.setPaint(Color.PINK);
                g2d.fill(p2d);
                g2d.setStroke(new BasicStroke(5.0f));
                g2d.setPaint(Color.BLACK);
                g2d.draw(p2d);
                g2d.dispose();
            }
        }
    }

So the final part of this assignment is what really stumps me, I've never created an animation before, so I don't know what exact packages to use. I know there should be a timer involved. How can I use a timer to have a function of constantly have the first heart appearing and disappearing; while the second doing the vice versa?

1 Answers1

0

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

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366