java - 如何将计时器融入到心跳动画中?

标签 java animation timer

我一直在拼命想弄清楚如何为我的类(class)作业创建动画。目标是使用 Path2D.curveTo 创建一颗心,然后模拟心跳动画。

    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();
            }
        }
    }

所以这个作业的最后部分真正困扰着我,我以前从未创建过动画,所以我不知道要使用什么确切的包。我知道应该有一个计时器。如何使用定时器来实现不断出现和消失第一颗心的功能?而第二个则相反?

最佳答案

首先认为动画是随时间变化的幻觉。因此,您需要能够根据可用时间量和当前使用的时间从一种状态转移到另一种状态。

因此,我们真正需要的是某种方法来根据已播放的时间量动态更改形状的大小。

因此,第一件事是获取绘图代码并从中创建一个Shape。这提供了一个基线。

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);
    }

}

这将成为我们的“原点”,稍后会更有意义😉

接下来我们需要设置播放状态,其中包括“开始时间”或“ anchor 时间”以及所需的播放长度。

一旦我们有了这个,我们就可以使用Timer来计算播放的时间并从中生成“进度”值。

顺便说一句,像这样的基于时间的动画更灵活,通常看起来比基于线性的进度更好(恕我直言)

然后我们可以使用级数值作为调整形状大小的基础。

为此,我们利用 Shape API...

Shape shape = heartShape.createTransformedShape(AffineTransform.getScaleInstance(scale, scale));

这一切可能看起来像......

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();
    }
}

好吧,但是从 0-1 的简单移动,我们需要它“弹跳”(从起点到终点,然后再返回)。

至此,我们已经进入了“关键帧”的领域,但我会尽量保持简单,但如果您有兴趣,可以看一下:

但我们警告过,它会让你头疼 🤯 - 它也会让我头疼 😝

首先,我们需要定义有效值的“范围”。这将表示形状缩放的范围...

private double lowerRange = 0.75;
private double upperRange = 1.25;

在这里,我刚刚选择了 1.0 附近的一些任意值,您可以使用这些值,看看您喜欢什么。

接下来,在Timer中,我们需要将scale的计算方式更改为更像...

scale = ((upperRange - lowerRange) * progress) + lowerRange;
repaint();

好吧,很酷,我们现在形状从 0.75 增长到 1.25,但它仍然只是在线性方向上。

好吧,这就是关键帧和时间线真正有用的地方,但是,由于我们真正想要的是“自动反转”之类的东西(除了在应用的持续时间内),我们可以做更多类似的事情...

if (progress > 0.5) {
    progress = 1.0 - progress;
}

scale = ((upperRange - lowerRange) * progress) + lowerRange;
repaint();

很快,我们就有了一颗跳动的心💓,甜蜜

好的,但是等等,这有点慢。那么,只需修改 playDuration,也许可以尝试 private Duration playDuration = Duration.ofSeconds(1);!

看看这有多简单,您只需要更改一个变量,就完全改变了动画的播放方式!告诉你基于时间的动画震撼了 😉

最终可运行示例...

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);
        }

    }
}

确保你可以在你的讲座中解释这一点,因为如果你向我提供这段代码,我会问很多关于它是如何工作的以及你为什么选择这条路径的问题😉

关于java - 如何将计时器融入到心跳动画中?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60048242/

相关文章:

c# - DispatcherTimer 在更新 UI 时每分钟变慢

java - 如何将 csv 字符串转换为 Spark-ML 兼容的 Dataset<Row> 格式?

java - 实现线程安全共享计数器的功能方法

python - 有没有一种使用 Pygame 制作和保存动画的简单方法?

android - 如何在谷歌地图上显示图标闪烁

javascript - 一页上有多个倒计时

Delphi:简单的 hh:mm:ss 计时器

java - 如何仅捕获 ArrayList 中的重复元素?

java - 如何检查Java中另一个类中的方法何时运行(方法调用监听器)?

ios - Xcode - 使用 PNG 序列制作动画