2

I've started to take interest with making animations(slideshows, backgrounds etc) in Java. I know that JavaFX is much better for doing this, but I'm just to stubborn to bother switching over.

Here is what I got so far.

import java.awt.Color;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Random;

import javax.swing.JFrame;
import javax.swing.JPanel;

public class BlurredLightCells extends JPanel {

    private static final long serialVersionUID = 4610174943257637060L;

    private Random random = new Random();

    private ArrayList<LightCell> lightcells;

    private float[] blurData = new float[500];

    public static void main(String[] args) {
        JFrame frame = new JFrame("Swing animated bubbles");
        frame.setSize(1000, 750);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        frame.add(new BlurredLightCells(60));

        frame.setVisible(true);
    }

    public BlurredLightCells(int amtOfBCells) {
        setSize(1000, 750);
        /**
         * Below we initiate all the cells that are going to be drawn on screen
         */
        Arrays.fill(blurData, 1f / 20f);
        lightcells = new ArrayList<LightCell>(amtOfBCells);
        for (int i = 0; i < amtOfBCells; i++) {

            /**
             * Below we generate all the values for each cell(SHOULD be random for each one)
             */
            int baseSpeed = random(0, 3);
            int xSpeed = (int) Math.floor((Math.random() * (baseSpeed - -baseSpeed + baseSpeed)) + -baseSpeed);
            int ySpeed = (int) Math.round((Math.random() * baseSpeed) + 0.5);
            int radius = random(25, 100);
            int x = (int) Math.floor(Math.random() * getWidth());
            int y = (int) Math.floor(Math.random() * getHeight());
            int blurrAmount = (int) (Math.floor(Math.random() * 10) + 5);
            int alpha = (int) ((Math.random() * 15) + 3);

            /**
             * Now we draw a image, and apply transparency and a slight blur to it
             */
            Kernel kernel = new Kernel(blurrAmount, blurrAmount, blurData);
            BufferedImageOp op = new ConvolveOp(kernel);
            BufferedImage circle = new BufferedImage(150, 150, BufferedImage.TYPE_INT_ARGB);
            Graphics2D circlegfx = circle.createGraphics();
            circlegfx.setColor(new Color(255, 255, 255, alpha));
            circlegfx.fillOval(20, 20, radius, radius);
            circle = op.filter(circle, null);
            LightCell bubble = new LightCell(x, y, xSpeed, ySpeed, radius, getDirection(random.nextInt(3)), circle);
            lightcells.add(bubble);
        }
    }

    public int random(int min, int max) {
        final int n = Math.abs(max - min);
        return Math.min(min, max) + (n == 0 ? 0 : random.nextInt(n));
    }

    @Override
    public void paint(Graphics g) {
        int w = getWidth();
        int h = getHeight();
        final Graphics2D g2 = (Graphics2D) g;
        GradientPaint gp = new GradientPaint(-w, -h, Color.LIGHT_GRAY, w, h, Color.DARK_GRAY);
        g2.setPaint(gp);
        g2.fillRect(0, 0, w, h);
        long start = System.currentTimeMillis();
        for (int i = 0; i < lightcells.size(); i++) {
            LightCell cell = lightcells.get(i);
            cell.process(g2);
        }
        System.out.println("Took " + (System.currentTimeMillis() - start) + " milliseconds to draw ALL cells.");
        repaint();
    }

    public String getDirection(int i) {
        switch (i) {
            case 0:
                return "right";
            case 1:
                return "left";
            case 2:
                return "up";
            case 3:
                return "down";
        }
        return "";
    }

    private class LightCell {

        private int x, y, xSpeed, ySpeed, radius;
        private String direction;
        private BufferedImage image;

        public LightCell(int x, int y, int xSpeed, int ySpeed, int radius, String direction, BufferedImage image) {
            this.x = x;
            this.y = y;
            this.xSpeed = xSpeed;
            this.ySpeed = ySpeed;
            this.radius = radius;
            this.direction = direction;
            this.image = image;
        }

        public void process(Graphics g) {
            switch (direction) {
                case "right":
                    moveRight();
                    break;
                case "left":
                    moveLeft();
                    break;
                case "up":
                    moveUp();
                    break;
                case "down":
                    moveDown();
                    break;
            }
            g.drawImage(image, x, y, null);
        }

        private void moveUp() {
            x += xSpeed;
            y -= ySpeed;
            if (y + (radius / 2) < 0) {
                y = getHeight() + (radius / 2);
                x = (int) Math.floor(Math.random() * getWidth());
            }

            if ((x + radius / 2) < 0 || (x - radius / 2) > getWidth()) {
                y = radius + (radius / 2);
                x = (int) Math.floor(Math.random() * getWidth());
            }
        }

        private void moveDown() {
            x += xSpeed;
            y += ySpeed;
            if (y - (radius / 2) > getHeight()) {
                y = 0 - (radius / 2);
                x = (int) Math.floor(Math.random() * getWidth());
            }

            if ((x + radius / 2) < 0 || (x - radius / 2) > getWidth()) {
                y = getHeight() + (radius / 2);
                x = (int) Math.floor(Math.random() * getWidth());
            }
        }

        private void moveRight() {
            x += ySpeed;
            y += xSpeed;
            if (y - (radius / 2) > getHeight() || y + (radius / 2) < 0) {
                x = 0 - (radius / 2);
                y = (int) Math.floor(Math.random() * getHeight());
            }

            if ((x - radius / 2) > getWidth()) {
                x = 0 - (radius / 2);
                y = (int) Math.floor(Math.random() * getWidth());
            }
        }

        private void moveLeft() {
            x -= ySpeed;
            y -= xSpeed;
            if (y - (radius / 2) > getHeight() || y + (radius / 2) < 0) {
                x = getWidth() + (radius / 2);
                y = (int) Math.floor(Math.random() * getHeight());
            }

            if ((x + radius / 2) < 0) {
                x = getWidth() + (radius / 2);
                y = (int) Math.floor(Math.random() * getWidth());
            }
        }
    }
}

If you run that, you will see the cells move at a very high speed, and if you look through the code, you see I call repaint() in the paint method in which I override. I know thats not good to do. But my question is, is their any other way in which I could draw each cell outside of the repaint() loop I have right now, because that causes other components to flash/flicker when I use this in a JFrame with other components.

FYI: Aventually I'd like to achieve something similar to this: Click Here

Thanks!

Jonathan Beaudoin
  • 2,158
  • 4
  • 27
  • 63

1 Answers1

6

The issue of flicker is to do with the fact that top level containers are not double buffered. Instead of extending from JFrame (or other top level containers), you should consider using something more like JPanel and override it's paintComponent.

nb- Had it in my head that the OP was extending from JFrame...

Two issues could be causing the flickering. The first is overriding paint, the second is not calling super.paint(g) and the time between the updates. A better solution would be to override paintComponent and make sure you are calling super.paintComponent. Also using something like a javax.swing.Timer to schedule updates and regular intervals would also help...

Only call repaint when you want to encourage the RepaintManager to update you component. Don't call repaint from within any paintXxx method, this will cause a never ending loop of paint requests to schedule onto the event queue, eventually consuiming your CPU

I would avoid doing anything in your paintXxx methods that might take time to perform, this will slow down the rendering process. Instead, I would use a javax.swing.Timer for simple updates or for more complicated processing, a Thread which could be used to update the model before it is rendered to the screen.

This is an example of some simple optimisation process I did to take animation of 500 objects to 4500 with only a slight degration in the overall performance.

Updated

I changed you code slight and it works fine...

enter image description here

I changed your paint method to paintComponent and added super.paintComponent(g)

@Override
protected void paintComponent(Graphics g) {
    super.paintComponent(g);
    int w = getWidth();
    int h = getHeight();
    final Graphics2D g2 = (Graphics2D) g;
    GradientPaint gp = new GradientPaint(-w, -h, Color.LIGHT_GRAY, w, h, Color.DARK_GRAY);
    g2.setPaint(gp);
    g2.fillRect(0, 0, w, h);
    for (int i = 0; i < lightcells.size(); i++) {
        LightCell cell = lightcells.get(i);
        cell.process(g2);
    }
}

And at the end of your constructor I added...

Timer timer = new Timer(40, new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        repaint();
    }
});
timer.start();

To update the UI on a regular bases...

Community
  • 1
  • 1
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • As far as I know, there's nothing wrong in repeteadly calling `repaint`. According to Java documentation, repaint requests are coalesced. So, if you call repaint 10 times, but none of these 10 requests has been dispatched yet, all the 10 requests will be coalesced into a single one (the first or the last 9 repaints will be ignored). – Giulio Franco Sep 16 '13 at 00:30
  • More precisely, the javadoc of JComponent#paintImmediately states: "In most cases it's more efficient to call repaint, which defers the actual painting and can collapse redundant requests into a single paint call." – Giulio Franco Sep 16 '13 at 00:33
  • Thanks, very informative answer! ended up getting it working perfectly. I also used a JLayeredPane to stop any remaining flickering. Thanks again! – Jonathan Beaudoin Sep 16 '13 at 00:35
  • 1
    @GiulioFranco `repaint` requests that the `RepaintManager` schedule a paint event on the Event Queue, this is then processed by the Event Dispatching Thread. Calling `repaint` (or any of its equivalent methods) from within a `paintXxx`) will result in new paint events being scheduled onto the queue, placing a load on the EDT to repeatedly process these new events. If done fast enought, it's possible for the EDT to consume the CPu, effecting performance of the application. – MadProgrammer Sep 16 '13 at 00:39
  • @GiulioFranco As you can see in my updated code, I call `repaint` every 40 milliseconds, this gives the EDT some breaking room, reducing the load on the CPU... – MadProgrammer Sep 16 '13 at 00:39
  • @GiulioFranco Further, I did an experiment, when I was able to schedule a repaint every 2 milliseconds and had the EDT process a paint event for 99% of them with the CPU running upwards of 90%... – MadProgrammer Sep 16 '13 at 00:40
  • @MadProgrammer that makes sense, but 40ms means limiting the framerate of the animation. Well, it's not really stupid, since an animation is supposed to be an eyecandy, and should not drain the CPU. I just think it's not flexible, and called repaint after the update loop (in game style). – Giulio Franco Sep 16 '13 at 00:43
  • @MadProgrammer yeah, I got caught in existential thoughts about fairness, and forgot to complete my comment... – Giulio Franco Sep 16 '13 at 00:47
  • @GiulioFranco Calling update at the end of the update loop isn't a bad idea, but generally, even game loops try and ensure that the updates occur at regularly timed intervals, adding in a `sleep` where required, based on the time between updates... – MadProgrammer Sep 16 '13 at 01:16
  • JComponent#paintImmediately invoked on EDT caused exceptions from RepaintManager – mKorbel Sep 16 '13 at 11:08