How can I make a polygon object in Java pulsate (like the chalice in the Atari game “Adventure”)

无人久伴 提交于 2020-06-26 14:13:51

问题


This is what I have in my paintComponent (most other things omitted, just the stuff that pertains to an Item object called chalice with a polygon field, the explicit parameters of the if statement are not important for this question) Currently, it shows up as solid white because I set the color to all 255, but I want to make it gradually transition to different colors smoothly, not strobing, more like pulsating but I don't really know what that is called. I was thinking about replacing the explicit parameters of the Color with arrays that cycle through numbers in that array and somehow link that to a TimerListener, but I am new to graphics so I am not sure if that is the best way to go about this.

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

回答1:


Some basic concepts...

  • A pulsating effect needs to move in two direction, it needs to fade in AND out
  • In order to know how much "effect" should be applied, two things need to be known. First, how long the overall effect takes to cycle (from fully opaque to fully transparent and back again) and how far through the cycle the animation is.

This is not a simple thing to manage, there's a lot of "stateful" information which needs to be managed and maintained, and normally, done so separately from other effects or entities.

To my mind, the simplest solution is to devise some kind of "time line" which manages key points (key frames) along the time line, calculates the distance between each point and the value that it represents.

Take a step back for a second. We know that at:

  • 0% we want to be fully opaque
  • 50% we want to be fully transparent
  • 100% we want to be fully opaque

The above takes into consideration that we want to "auto reverse" the animation.

The reason for working with percentages, is that it allows us to define a timeline of any given duration and the timeline will take care of the rest. Where ever possible, always work with normalised values like this, it makes the whole thing a lot simpler.

TimeLine

The following is a pretty simple concept of a "timeline". It has a Duration, the time over which the timeline is played, key frames, which provide key values over the duration of the timeline and the means to calculate a specific value at a specific point over the life of the timeline.

This implementation also provides "auto" replay-ability. That is, if the timeline is played "over" it's specified Duration, rather then stopping, it will automatically reset and take into consideration the amount of time "over" as part of it's next cycle (neat)

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

    }

}

Setting up the timeline is pretty simple...

timeLine = new TimeLine(Duration.ofSeconds(5));
timeLine.add(0.0f, 1.0f);
timeLine.add(0.5f, 0.0f);
timeLine.add(1.0f, 1.0f);

We give a specified Duration and setup the key frame values. After that we just need to "start" it and get the current value from the TimeLine based on how long it's been playing.

This might seem like a lot of work for what seems like a simple problem, but remember, this is both dynamic and re-usable.

It's dynamic in that you can provide any Duration you want, thus changing the speed, at it will "just work", and re-usable, as you can generate multiple instances for multiple entities and it will be managed independently.

Example...

The following example simply makes use of Swing Timer to act as the "main-loop" for the animation. On each cycle, it asks the TimeLine for the "current" value, which simply acts as the alpha value for the "pulsating" effect.

The TimeLine class itself is decoupled enough that it won't matter "how" you've establish your "main-loop", you simply start it running and pull the "current" value from it when ever you can...

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

    }
}

I would tie up the TimeLine as part of the effect for the specified entity. This would tie the TimeLine to a specific entity, meaning that many entities could all have their own TimeLines calculating a verify of different values and effects

"Is there a simpler solution?"

This is a subjective question. There "might" be a "simpler" approach which will do the same job, but which won't be as scalable or re-usable as this kind of approach.

Animation is a complex subject, trying to make it work in a complex solution, running multiple different effects and entities just compounds the problem

I've toyed with the idea of making the TimeLine generic so it could be used to generate a verity of different values based on the desired result, making it a much more flexible and re-usable solution.

Color blending ....

I don't know if this is a requirement, but if you have a series of colors you want to blend between, a TimeLine would also help you here (you don't need the duration so much). You could set up a series of colors (acting as key frames) and calculate which color to use based on the progression of the animation.

Blending colors is somewhat troublesome, I've spent a lot of time trying to find a decent algorithm which works for me, which is demonstrated at Color fading algorithm? and Java: Smooth Color Transition



来源:https://stackoverflow.com/questions/50539835/how-can-i-make-a-polygon-object-in-java-pulsate-like-the-chalice-in-the-atari-g

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!