您即将进行一次大规模的深潜。动画,好的动画,你没有注意到的动画,真的很难实现,而且是一个非常复杂的主题。
你已经朝着正确的方向开始了。您需要某种“ticker”来告诉您动画何时应该更新,但在那之后它就会崩溃。
您要做的第一件事是摆脱“线性渐进”的概念。也就是说,在每个“滴答”上,对象都会移动一个特定的增量值。这不会产生良好的动画效果,并且当您想要更改动画的速度或持续时间时会很快崩溃。
更好的解决方案是从“基于持续时间的进度”开始。基本上,动画将在固定的时间段内运行,并且在动画的每个滴答声中,您都会根据已过去的时间量和剩余时间量计算对象的新“状态”。
这有利于“规范化”时间线。即动画发生在0-1之间。由此,计算对象应该沿着该时间线的位置变得非常容易。想让它更快或更慢吗?更改时长,剩下的交给你!
首先,弄清楚如何将一个点从一个点移动到另一个点,如果你能移动一个,你就可以移动一千个。
持续时间基础动画引擎...
密切关注:
-
Utilities 类
-
DurationAnimationEngine
引擎由 Swing Timer 支持,因此在 Swing 中使用是安全的。它的全部目的是在指定的时间段内(尽可能快地运行)并产生随着进展量发生的“滴答声”(记住,标准化时间)
以下是动画的基本实现。很多“工作”发生在mouseClicked 事件中,因为它启动了引擎。当引擎滴答作响时,点会更新。每个点都包裹在AnimatableDot 中,它有一个“from”和“to”点,然后它根据归一化时间计算它的新位置,然后执行一次绘画通道
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Point2D;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
public class DurationTest {
public static void main(String[] args) {
new DurationTest();
}
public DurationTest() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
JFrame frame = new JFrame();
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class Utilities {
public static Point2D pointOnCircle(double degress, double radius) {
double rads = Math.toRadians(degress - 90); // 0 becomes the top
double xPosy = Math.round((Math.cos(rads) * radius));
double yPosy = Math.round((Math.sin(rads) * radius));
return new Point2D.Double(radius + xPosy, radius + yPosy);
}
public static Point2D pointOnCircle(double xOffset, double yOffset, double degress, double radius) {
Point2D poc = pointOnCircle(degress, radius);
return new Point2D.Double(xOffset + poc.getX(), yOffset + poc.getY());
}
}
public class TestPane extends JPanel {
private List<AnimatedDot> dots = new ArrayList<>(128);
private Duration duration = Duration.ofSeconds(5);
private DurationAnimationEngine engine;
private List<Color> colors = Arrays.asList(new Color[]{
Color.RED,
Color.BLUE,
Color.CYAN,
Color.GRAY,
Color.GREEN,
Color.LIGHT_GRAY,
Color.MAGENTA,
Color.PINK,
Color.WHITE,
Color.YELLOW
});
public TestPane() {
Random rnd = new Random();
setBackground(Color.BLACK);
for (int index = 0; index < 100; index++) {
double fromAngle = 360.0 * rnd.nextDouble();
double toAngle = fromAngle + 180.0;
Collections.shuffle(colors);
Color color = colors.get(0);
dots.add(new AnimatedDot(
Utilities.pointOnCircle(fromAngle, 150),
Utilities.pointOnCircle(toAngle, 150),
color, 2));
}
addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (engine != null) {
engine.stop();
engine = null;
// Reset poisitions
for (AnimatedDot dot : dots) {
dot.move(0);
}
repaint();
return;
}
engine = new DurationAnimationEngine(duration, new DurationAnimationEngine.Tickable() {
@Override
public void animationDidTick(double progress) {
for (AnimatedDot dot : dots) {
dot.move(progress);
}
repaint();
}
});
engine.start();
}
});
}
@Override
public Dimension getPreferredSize() {
return new Dimension(400, 400);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON
);
int xOffset = (getWidth() - 300) / 2;
int yOffset = (getWidth() - 300) / 2;
g2d.translate(xOffset, yOffset);
g2d.setColor(Color.DARK_GRAY);
g2d.drawOval(0, 0, 300, 300);
for (AnimatedDot dot : dots) {
dot.paint(g2d);
}
g2d.dispose();
}
}
public class DurationAnimationEngine {
public interface Tickable {
public void animationDidTick(double progress);
}
private Duration duration;
private Instant timeStarted;
private Timer timer;
private Tickable tickable;
public DurationAnimationEngine(Duration duration, Tickable tickable) {
this.duration = duration;
this.tickable = tickable;
}
public void start() {
// You could create the timer lazierly and restarted it as needed
if (timer != null) {
return;
}
timer = new Timer(5, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (timeStarted == null) {
timeStarted = Instant.now();
}
Duration runtime = Duration.between(timeStarted, Instant.now());
double progress = Math.min(1.0, runtime.toMillis() / (double) duration.toMillis());
tickable.animationDidTick(progress);
if (progress >= 1.0) {
stop();
}
}
});
timer.start();
}
public void stop() {
if (timer == null) {
return;
}
timer.stop();
timer = null;
}
}
public class AnimatedDot {
private Dot dot;
private Point2D from;
private Point2D to;
public AnimatedDot(Point2D from, Point2D to, Color color, int radius) {
dot = new Dot(from.getX(), from.getY(), color, radius);
this.from = from;
this.to = to;
}
public void paint(Graphics2D g) {
dot.paint(g);
}
public void move(double progress) {
Point2D pointAt = pointAt(progress);
dot.setLocation(pointAt);
}
public Point2D getFrom() {
return from;
}
public Point2D getTo() {
return to;
}
protected double getFromX() {
return getFrom().getX();
}
protected double getFromY() {
return getFrom().getY();
}
public Double getXDistance() {
return getTo().getX() - getFrom().getX();
}
public Double getYDistance() {
return getTo().getY() - getFrom().getY();
}
protected Point2D pointAt(double progress) {
double xDistance = getXDistance();
double yDistance = getYDistance();
double xValue = Math.round(xDistance * progress);
double yValue = Math.round(yDistance * progress);
xValue += getFromX();
yValue += getFromY();
return new Point2D.Double(xValue, yValue);
}
}
public class Dot {
private Color color;
private double y;
private double x;
private int radius;
private Ellipse2D dot;
public Dot(double x, double y, Color color, int radius) {
this.x = x;
this.y = y;
this.color = color;
this.radius = radius;
dot = new Ellipse2D.Double(0, 0, radius * 2, radius * 2);
}
public void setLocation(Point2D point) {
setLocation(point.getX(), point.getY());
}
public void setLocation(double x, double y) {
this.x = x;
this.y = y;
}
public void paint(Graphics2D g) {
Graphics2D g2d = (Graphics2D) g.create();
g2d.setColor(color);
g2d.translate(x - radius, y - radius);
g2d.fill(dot);
g2d.dispose();
}
}
}
好的,所以当我们运行它时,我们得到...
? ...嗯,我想说那是意料之中的,但是一旦我看到它,就很明显出了什么问题。
所有点在相同的时间范围内以相同的速度移动!
那么,答案是什么。嗯,其实有几个……
- 我们可以更改每个点的持续时间,以便它们具有单独的持续时间。这会使运动“随机化”,但我不确定它会产生完全相同的效果,因为它们会以不同的速度运动
- 我们可以随机化点的开始时间,使它们在不同的时间开始,让它们都具有相同的持续时间(甚至是随机的持续时间)
- 我们只能移动一小部分点,但这意味着您可能最终要等待当前子集完成后才能开始下一个子集
- 2 和 3 的组合
个性化、随机化的持续时间...
好的,为了简单(和我的理智),我实际上将从 1 开始。每个点都有自己的随机持续时间。这意味着每个点将以不同的速度移动。
密切关注LinearAnimationEngine 和AnimatedDot#move 方法。
这应该看起来很眼熟,基本上和之前的动画逻辑一样,只是为了点本身而隔离
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Point2D;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
public class RandomIndividualDuration {
public static void main(String[] args) {
new RandomIndividualDuration();
}
public RandomIndividualDuration() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
JFrame frame = new JFrame();
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class Utilities {
public static Point2D pointOnCircle(double degress, double radius) {
double rads = Math.toRadians(degress - 90); // 0 becomes the top
double xPosy = Math.round((Math.cos(rads) * radius));
double yPosy = Math.round((Math.sin(rads) * radius));
return new Point2D.Double(radius + xPosy, radius + yPosy);
}
public static Point2D pointOnCircle(double xOffset, double yOffset, double degress, double radius) {
Point2D poc = pointOnCircle(degress, radius);
return new Point2D.Double(xOffset + poc.getX(), yOffset + poc.getY());
}
}
public class DurationRange {
private Duration from;
private Duration to;
public DurationRange(Duration from, Duration to) {
this.from = from;
this.to = to;
}
public Duration getFrom() {
return from;
}
public Duration getTo() {
return to;
}
public Duration getDistance() {
return Duration.ofNanos(getTo().toNanos() - getFrom().toNanos());
}
public Duration valueAt(double progress) {
Duration distance = getDistance();
long value = (long) Math.round((double) distance.toNanos() * progress);
value += getFrom().getNano();
return Duration.ofNanos(value);
}
}
public class TestPane extends JPanel {
private List<AnimatedDot> dots = new ArrayList<>(128);
private Duration duration = Duration.ofSeconds(5);
private LinearAnimationEngine engine;
private List<Color> colors = Arrays.asList(new Color[]{
Color.RED,
Color.BLUE,
Color.CYAN,
Color.GRAY,
Color.GREEN,
Color.LIGHT_GRAY,
Color.MAGENTA,
Color.PINK,
Color.WHITE,
Color.YELLOW
});
public TestPane() {
Random rnd = new Random();
setBackground(Color.BLACK);
DurationRange range = new DurationRange(Duration.ofSeconds(1), Duration.ofSeconds(5));
for (int index = 0; index < 100; index++) {
double fromAngle = 360.0 * rnd.nextDouble();
double toAngle = fromAngle + 180.0;
Collections.shuffle(colors);
Color color = colors.get(0);
Duration duration = range.valueAt(rnd.nextDouble());
dots.add(new AnimatedDot(
Utilities.pointOnCircle(fromAngle, 150),
Utilities.pointOnCircle(toAngle, 150),
color, 2, duration));
}
addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (engine != null) {
engine.stop();
engine = null;
reset();
return;
}
System.out.println("Go");
List<AnimatedDot> avaliableDots = new ArrayList<>(120);
avaliableDots.addAll(dots);
engine = new LinearAnimationEngine(new LinearAnimationEngine.Tickable() {
@Override
public void animationDidTick() {
List<AnimatedDot> completed = new ArrayList<>(128);
// Reset poisitions
for (AnimatedDot dot : avaliableDots) {
if (!dot.move()) {
completed.add(dot);
}
}
avaliableDots.removeAll(completed);
repaint();
if (avaliableDots.isEmpty()) {
engine.stop();
engine = null;
reset();
}
}
});
engine.start();
}
});
}
protected void reset() {
for (AnimatedDot dot : dots) {
dot.reset();
}
}
@Override
public Dimension getPreferredSize() {
return new Dimension(400, 400);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON
);
int xOffset = (getWidth() - 300) / 2;
int yOffset = (getWidth() - 300) / 2;
g2d.translate(xOffset, yOffset);
g2d.setColor(Color.DARK_GRAY);
g2d.drawOval(0, 0, 300, 300);
for (AnimatedDot dot : dots) {
dot.paint(g2d);
}
g2d.dispose();
}
}
public class LinearAnimationEngine {
public interface Tickable {
public void animationDidTick();
}
private Tickable tickable;
private Timer timer;
public LinearAnimationEngine(Tickable tickable) {
this.tickable = tickable;
}
public void start() {
// You could create the timer lazierly and restarted it as needed
if (timer != null) {
return;
}
timer = new Timer(5, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
tickable.animationDidTick();
}
});
timer.start();
}
public void stop() {
if (timer == null) {
return;
}
timer.stop();
timer = null;
}
}
public class AnimatedDot {
private Dot dot;
private Point2D from;
private Point2D to;
private Duration duration;
private Instant timeStarted;
public AnimatedDot(Point2D from, Point2D to, Color color, int radius, Duration duration) {
dot = new Dot(from.getX(), from.getY(), color, radius);
this.from = from;
this.to = to;
this.duration = duration;
}
public void paint(Graphics2D g) {
dot.paint(g);
}
public void reset() {
Point2D futureFrom = to;
to = from;
from = futureFrom;
timeStarted = null;
}
public boolean move() {
if (timeStarted == null) {
timeStarted = Instant.now();
}
Duration runtime = Duration.between(timeStarted, Instant.now());
double progress = Math.min(1.0, runtime.toMillis() / (double) duration.toMillis());
Point2D pointAt = pointAt(progress);
dot.setLocation(pointAt);
return progress < 1.0;
}
public Point2D getFrom() {
return from;
}
public Point2D getTo() {
return to;
}
protected double getFromX() {
return getFrom().getX();
}
protected double getFromY() {
return getFrom().getY();
}
public Double getXDistance() {
return getTo().getX() - getFrom().getX();
}
public Double getYDistance() {
return getTo().getY() - getFrom().getY();
}
protected Point2D pointAt(double progress) {
double xDistance = getXDistance();
double yDistance = getYDistance();
double xValue = Math.round(xDistance * progress);
double yValue = Math.round(yDistance * progress);
xValue += getFromX();
yValue += getFromY();
return new Point2D.Double(xValue, yValue);
}
}
public class Dot {
private Color color;
private double y;
private double x;
private int radius;
private Ellipse2D dot;
public Dot(double x, double y, Color color, int radius) {
this.x = x;
this.y = y;
this.color = color;
this.radius = radius;
dot = new Ellipse2D.Double(0, 0, radius * 2, radius * 2);
}
public void setLocation(Point2D point) {
setLocation(point.getX(), point.getY());
}
public void setLocation(double x, double y) {
this.x = x;
this.y = y;
}
public void paint(Graphics2D g) {
Graphics2D g2d = (Graphics2D) g.create();
g2d.setColor(color);
g2d.translate(x - radius, y - radius);
g2d.fill(dot);
g2d.dispose();
}
}
}
现在,当我们运行它时,我们得到...
嗯,至少它现在更加“随机”了,这就是我认为第 2 点和第 3 点可能更好的组合。
但他们没有反弹?!
啊,好吧,实际上,再次单击第二个示例!点将从当前位置(原点到点)移动并返回到原点。太棒了,从概念上讲,它是可行的。
但是当我点击它时,它们并不是来自一张漂亮的图片!
? ... 所以上面的例子演示了如何在指定的时间段内将一个对象从 A 点动画到 B 点,形成图片只是改变目标目的地(假设你知道它是从什么开始的)。根据我的观察,在移动到最终图片位置之前,首先允许移动点移动到其当前的“结束”位置,因为试图计算弯曲路径会让我?。
缺少什么...
是的,有些东西不见了,你可能看不到,但它对我来说真的很突出。
每个点开始缓慢,然后加速,然后减速到位。这被称为“地役权”(或者,在这种情况下,是缓入/缓出),它并不是世界上最容易实现的东西。有兴趣的可以看看How can I implement easing functions with a thread
现在,您的问题的实际答案是什么?除非你完全疯了(我也是),否则不要试图自己做这种事情,除非你有一个非常具体的理由这样做。相反,请使用其他现成的引擎之一,例如:
上面使用的大部分概念都来自我的动画游乐场源Super Simple Swing Animation Framework。这是我经常玩耍和实验的地方