如何使 Java 中的多边形对象脉动(就像 Atari 游戏中的圣杯 "Adventure")
How can I make a polygon object in Java pulsate (like the chalice in the Atari game "Adventure")
这就是我的 paintComponent 中的内容(省略了大多数其他内容,只是与名为 chalice 的 Item 对象有关的内容,带有多边形字段,if 语句的显式参数对于这个问题并不重要)
目前,它显示为纯白色,因为我将颜色设置为全 255,但我想让它逐渐平滑地过渡到不同的颜色,而不是频闪,更像是脉动,但我真的不知道那叫什么。我正在考虑用循环遍历该数组中的数字的数组替换 Color 的显式参数,并以某种方式 link 到 TimerListener,但我是图形新手,所以我不确定这是否是最好的方法开始吧。
public void paintComponent(Graphics g) {
Graphics2D sprite = (Graphics2D) g;
if (chalice.getHolding() == true || roomID == chalice.getRoomDroppedIn()) {
sprite.setColor(new Color(255, 255, 255));
sprite.fill(chalice.getPoly());
}
}
一些基本概念...
- 脉动效果需要在两个方向上移动,需要淡入淡出
- 为了知道应该应用多少“效果”,需要知道两件事。首先,整体效果循环需要多长时间(从完全不透明到完全透明再返回)以及动画循环的距离。
这不是一件容易管理的事情,有很多“有状态”的信息需要管理和维护,并且通常与其他效果或实体分开进行。
在我看来,最简单的解决方案是设计某种“时间线”,它沿着时间线管理关键点(关键帧),计算每个点之间的距离及其代表的值。
先退一步。我们知道:
- 0% 我们希望完全不透明
- 50% 我们希望完全透明
- 100% 我们希望完全不透明
以上考虑到我们要“自动反转”动画。
使用百分比的原因是它允许我们定义任何给定持续时间的时间线,时间线将处理其余部分。在可能的情况下,始终使用像这样的标准化值,它使整个事情变得简单得多。
TimeLine
以下是一个非常简单的“时间线”概念。它有一个 Duration
,时间线播放的时间,关键帧,它提供时间线持续时间内的关键值,以及计算时间线生命周期内特定点的特定值的方法。
此实现还提供“自动”重播功能。也就是说,如果时间线播放“结束”,它被指定 Duration
,而不是停止,它会自动重置并考虑“结束”的时间量作为下一个循环的一部分(整洁)
public class TimeLine {
private Map<Float, KeyFrame> mapEvents;
private Duration duration;
private LocalDateTime startedAt;
public TimeLine(Duration duration) {
mapEvents = new TreeMap<>();
this.duration = duration;
}
public void start() {
startedAt = LocalDateTime.now();
}
public boolean isRunning() {
return startedAt != null;
}
public float getValue() {
if (startedAt == null) {
return getValueAt(0.0f);
}
Duration runningTime = Duration.between(startedAt, LocalDateTime.now());
if (runningTime.compareTo(duration) > 0) {
runningTime = runningTime.minus(duration);
startedAt = LocalDateTime.now().minus(runningTime);
}
long total = duration.toMillis();
long remaining = duration.minus(runningTime).toMillis();
float progress = remaining / (float) total;
return getValueAt(progress);
}
public void add(float progress, float value) {
mapEvents.put(progress, new KeyFrame(progress, value));
}
public float getValueAt(float progress) {
if (progress < 0) {
progress = 0;
} else if (progress > 1) {
progress = 1;
}
KeyFrame[] keyFrames = getKeyFramesBetween(progress);
float max = keyFrames[1].progress - keyFrames[0].progress;
float value = progress - keyFrames[0].progress;
float weight = value / max;
float blend = blend(keyFrames[0].getValue(), keyFrames[1].getValue(), 1f - weight);
return blend;
}
public KeyFrame[] getKeyFramesBetween(float progress) {
KeyFrame[] frames = new KeyFrame[2];
int startAt = 0;
Float[] keyFrames = mapEvents.keySet().toArray(new Float[mapEvents.size()]);
while (startAt < keyFrames.length && keyFrames[startAt] <= progress) {
startAt++;
}
if (startAt >= keyFrames.length) {
startAt = keyFrames.length - 1;
}
frames[0] = mapEvents.get(keyFrames[startAt - 1]);
frames[1] = mapEvents.get(keyFrames[startAt]);
return frames;
}
protected float blend(float start, float end, float ratio) {
float ir = (float) 1.0 - ratio;
return (float) (start * ratio + end * ir);
}
public class KeyFrame {
private float progress;
private float value;
public KeyFrame(float progress, float value) {
this.progress = progress;
this.value = value;
}
public float getProgress() {
return progress;
}
public float getValue() {
return value;
}
@Override
public String toString() {
return "KeyFrame progress = " + getProgress() + "; value = " + getValue();
}
}
}
设置时间线非常简单...
timeLine = new TimeLine(Duration.ofSeconds(5));
timeLine.add(0.0f, 1.0f);
timeLine.add(0.5f, 0.0f);
timeLine.add(1.0f, 1.0f);
我们给出指定的 Duration
并设置关键帧值。之后我们只需要“启动”它并根据播放时间从 TimeLine
中获取当前的 value
。
对于看似简单的问题,这似乎需要大量工作,但请记住,这是动态的且可重用的。
它是动态的,因为您可以提供任何您想要的 Duration
,从而改变速度,它将“正常工作”并且可重复使用,因为您可以为多个实体生成多个实例并且它将独立管理。
示例...
以下示例简单地使用 Swing Timer
作为动画的“主循环”。在每个循环中,它向 TimeLine
询问“当前”值,它只是作为“脉动”效果的 alpha
值。
TimeLine
class 本身已经足够解耦,以至于你“如何”建立你的“主循环”并不重要,你只需启动它 运行 并且尽可能从中提取“当前”值...
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.TreeMap;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class Test {
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 {
private TimeLine timeLine;
private float alpha = 0;
public TestPane() {
timeLine = new TimeLine(Duration.ofSeconds(5));
timeLine.add(0.0f, 1.0f);
timeLine.add(0.5f, 0.0f);
timeLine.add(1.0f, 1.0f);
Timer timer = new Timer(5, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (!timeLine.isRunning()) {
timeLine.start();
}
alpha = timeLine.getValue();
repaint();
}
});
timer.start();
}
@Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setComposite(AlphaComposite.SrcOver.derive(alpha));
g2d.setColor(Color.RED);
g2d.fill(new Rectangle(45, 45, 110, 110));
g2d.dispose();
g2d = (Graphics2D) g.create();
g2d.setColor(getBackground());
g2d.fill(new Rectangle(50, 50, 100, 100));
g2d.setColor(Color.BLACK);
g2d.draw(new Rectangle(50, 50, 100, 100));
g2d.dispose();
}
}
}
我会将 TimeLine
作为指定实体效果的一部分。这会将 TimeLine
绑定到特定实体,这意味着许多实体都可以拥有自己的 TimeLine
来计算不同值和效果的验证
“有更简单的解决方案吗?”
这是一道主观题。 “可能”有一种“更简单”的方法可以完成同样的工作,但不像这种方法那样可扩展或可重复使用。
动画是一门复杂的学科,试图使其在复杂的解决方案中发挥作用,运行多种不同的效果和实体只会使问题复杂化
我考虑过让 TimeLine
通用的想法,这样它就可以根据期望的结果生成不同值的真实性,使其成为一个更加灵活和可重复使用的解决方案.
颜色混合....
我不知道这是否是一项要求,但如果您想要混合一系列颜色,TimeLine
也可以帮助您(您不需要持续时间,所以很多)。您可以设置一系列颜色(充当关键帧)并根据动画的进度计算要使用的颜色。
混合颜色有点麻烦,我花了很多时间试图找到一个适合我的像样的算法,在Color fading algorithm? and Java: Smooth Color Transition
中有演示
这就是我的 paintComponent 中的内容(省略了大多数其他内容,只是与名为 chalice 的 Item 对象有关的内容,带有多边形字段,if 语句的显式参数对于这个问题并不重要) 目前,它显示为纯白色,因为我将颜色设置为全 255,但我想让它逐渐平滑地过渡到不同的颜色,而不是频闪,更像是脉动,但我真的不知道那叫什么。我正在考虑用循环遍历该数组中的数字的数组替换 Color 的显式参数,并以某种方式 link 到 TimerListener,但我是图形新手,所以我不确定这是否是最好的方法开始吧。
public void paintComponent(Graphics g) {
Graphics2D sprite = (Graphics2D) g;
if (chalice.getHolding() == true || roomID == chalice.getRoomDroppedIn()) {
sprite.setColor(new Color(255, 255, 255));
sprite.fill(chalice.getPoly());
}
}
一些基本概念...
- 脉动效果需要在两个方向上移动,需要淡入淡出
- 为了知道应该应用多少“效果”,需要知道两件事。首先,整体效果循环需要多长时间(从完全不透明到完全透明再返回)以及动画循环的距离。
这不是一件容易管理的事情,有很多“有状态”的信息需要管理和维护,并且通常与其他效果或实体分开进行。
在我看来,最简单的解决方案是设计某种“时间线”,它沿着时间线管理关键点(关键帧),计算每个点之间的距离及其代表的值。
先退一步。我们知道:
- 0% 我们希望完全不透明
- 50% 我们希望完全透明
- 100% 我们希望完全不透明
以上考虑到我们要“自动反转”动画。
使用百分比的原因是它允许我们定义任何给定持续时间的时间线,时间线将处理其余部分。在可能的情况下,始终使用像这样的标准化值,它使整个事情变得简单得多。
TimeLine
以下是一个非常简单的“时间线”概念。它有一个 Duration
,时间线播放的时间,关键帧,它提供时间线持续时间内的关键值,以及计算时间线生命周期内特定点的特定值的方法。
此实现还提供“自动”重播功能。也就是说,如果时间线播放“结束”,它被指定 Duration
,而不是停止,它会自动重置并考虑“结束”的时间量作为下一个循环的一部分(整洁)
public class TimeLine {
private Map<Float, KeyFrame> mapEvents;
private Duration duration;
private LocalDateTime startedAt;
public TimeLine(Duration duration) {
mapEvents = new TreeMap<>();
this.duration = duration;
}
public void start() {
startedAt = LocalDateTime.now();
}
public boolean isRunning() {
return startedAt != null;
}
public float getValue() {
if (startedAt == null) {
return getValueAt(0.0f);
}
Duration runningTime = Duration.between(startedAt, LocalDateTime.now());
if (runningTime.compareTo(duration) > 0) {
runningTime = runningTime.minus(duration);
startedAt = LocalDateTime.now().minus(runningTime);
}
long total = duration.toMillis();
long remaining = duration.minus(runningTime).toMillis();
float progress = remaining / (float) total;
return getValueAt(progress);
}
public void add(float progress, float value) {
mapEvents.put(progress, new KeyFrame(progress, value));
}
public float getValueAt(float progress) {
if (progress < 0) {
progress = 0;
} else if (progress > 1) {
progress = 1;
}
KeyFrame[] keyFrames = getKeyFramesBetween(progress);
float max = keyFrames[1].progress - keyFrames[0].progress;
float value = progress - keyFrames[0].progress;
float weight = value / max;
float blend = blend(keyFrames[0].getValue(), keyFrames[1].getValue(), 1f - weight);
return blend;
}
public KeyFrame[] getKeyFramesBetween(float progress) {
KeyFrame[] frames = new KeyFrame[2];
int startAt = 0;
Float[] keyFrames = mapEvents.keySet().toArray(new Float[mapEvents.size()]);
while (startAt < keyFrames.length && keyFrames[startAt] <= progress) {
startAt++;
}
if (startAt >= keyFrames.length) {
startAt = keyFrames.length - 1;
}
frames[0] = mapEvents.get(keyFrames[startAt - 1]);
frames[1] = mapEvents.get(keyFrames[startAt]);
return frames;
}
protected float blend(float start, float end, float ratio) {
float ir = (float) 1.0 - ratio;
return (float) (start * ratio + end * ir);
}
public class KeyFrame {
private float progress;
private float value;
public KeyFrame(float progress, float value) {
this.progress = progress;
this.value = value;
}
public float getProgress() {
return progress;
}
public float getValue() {
return value;
}
@Override
public String toString() {
return "KeyFrame progress = " + getProgress() + "; value = " + getValue();
}
}
}
设置时间线非常简单...
timeLine = new TimeLine(Duration.ofSeconds(5));
timeLine.add(0.0f, 1.0f);
timeLine.add(0.5f, 0.0f);
timeLine.add(1.0f, 1.0f);
我们给出指定的 Duration
并设置关键帧值。之后我们只需要“启动”它并根据播放时间从 TimeLine
中获取当前的 value
。
对于看似简单的问题,这似乎需要大量工作,但请记住,这是动态的且可重用的。
它是动态的,因为您可以提供任何您想要的 Duration
,从而改变速度,它将“正常工作”并且可重复使用,因为您可以为多个实体生成多个实例并且它将独立管理。
示例...
以下示例简单地使用 Swing Timer
作为动画的“主循环”。在每个循环中,它向 TimeLine
询问“当前”值,它只是作为“脉动”效果的 alpha
值。
TimeLine
class 本身已经足够解耦,以至于你“如何”建立你的“主循环”并不重要,你只需启动它 运行 并且尽可能从中提取“当前”值...
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.TreeMap;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class Test {
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 {
private TimeLine timeLine;
private float alpha = 0;
public TestPane() {
timeLine = new TimeLine(Duration.ofSeconds(5));
timeLine.add(0.0f, 1.0f);
timeLine.add(0.5f, 0.0f);
timeLine.add(1.0f, 1.0f);
Timer timer = new Timer(5, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (!timeLine.isRunning()) {
timeLine.start();
}
alpha = timeLine.getValue();
repaint();
}
});
timer.start();
}
@Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setComposite(AlphaComposite.SrcOver.derive(alpha));
g2d.setColor(Color.RED);
g2d.fill(new Rectangle(45, 45, 110, 110));
g2d.dispose();
g2d = (Graphics2D) g.create();
g2d.setColor(getBackground());
g2d.fill(new Rectangle(50, 50, 100, 100));
g2d.setColor(Color.BLACK);
g2d.draw(new Rectangle(50, 50, 100, 100));
g2d.dispose();
}
}
}
我会将 TimeLine
作为指定实体效果的一部分。这会将 TimeLine
绑定到特定实体,这意味着许多实体都可以拥有自己的 TimeLine
来计算不同值和效果的验证
“有更简单的解决方案吗?”
这是一道主观题。 “可能”有一种“更简单”的方法可以完成同样的工作,但不像这种方法那样可扩展或可重复使用。
动画是一门复杂的学科,试图使其在复杂的解决方案中发挥作用,运行多种不同的效果和实体只会使问题复杂化
我考虑过让 TimeLine
通用的想法,这样它就可以根据期望的结果生成不同值的真实性,使其成为一个更加灵活和可重复使用的解决方案.
颜色混合....
我不知道这是否是一项要求,但如果您想要混合一系列颜色,TimeLine
也可以帮助您(您不需要持续时间,所以很多)。您可以设置一系列颜色(充当关键帧)并根据动画的进度计算要使用的颜色。
混合颜色有点麻烦,我花了很多时间试图找到一个适合我的像样的算法,在Color fading algorithm? and Java: Smooth Color Transition
中有演示