import java.awt.Canvas; import java.awt.Color; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.Graphics2D; import java.awt.Point; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.image.BufferStrategy; import java.util.Arrays; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadLocalRandom; import javax.swing.JFrame; public final class ParticleFountainTask { public static void main(String[] args) { EventQueue.invokeLater( () -> { JFrame.setDefaultLookAndFeelDecorated(true); JFrame frame = new JFrame("Particle Fountain"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setResizable(false); ParticleFountain particleFountain = new ParticleFountain(3_000, 1_000, 750); frame.add(particleFountain); frame.pack(); frame.setLocationRelativeTo(null); frame.setVisible(true); particleFountain.start(); } ); } private static final class ParticleFountain extends Canvas { public ParticleFountain(int aParticleCount, int aWidth, int aHeight) { particleCount = aParticleCount; width = aWidth; height = aHeight; saturation = 0.6; spread = 1.5; range = 1.5; reciprocate = false; setPreferredSize( new Dimension(width, height) ); addKeyListener( new InputHandler() ); executorService = Executors.newSingleThreadExecutor(); } public void start() { requestFocus(); createBufferStrategy(2); executorService.execute( new DrawingCycle() ); } private final class DrawingCycle implements Runnable { public DrawingCycle() { positions = new double[2 * particleCount]; velocities = new double[2 * particleCount]; lifetimes = new double[particleCount]; points = new Point[particleCount]; Arrays.fill(points, new Point(0, 0) ); random = ThreadLocalRandom.current(); } @Override public void run() { bufferStrategy = getBufferStrategy(); while ( true ) { update(0.005); draw(); } } private void update(double animationSpeed) { int xIndex = 0; int yIndex = 1; pointIndex = 0; for ( int index = 0; index < particleCount; index++ ) { boolean showParticle = false; if ( lifetimes[index] <= 0.0 ) { if ( random.nextDouble() < animationSpeed ) { lifetimes[index] = 2.5; positions[xIndex] = width / 20; positions[yIndex] = height / 10; velocities[xIndex] = 10 * ( spread * random.nextDouble() - spread / 2 + additionalXSpeed() ); velocities[yIndex] = ( random.nextDouble() - 2.9 ) * height / 20.5; showParticle = true; } } else { if ( positions[yIndex] > height / 10 && velocities[yIndex] > 0 ) { velocities[yIndex] *= -0.3; // bounce particle } velocities[yIndex] += animationSpeed * height / 10; positions[xIndex] += velocities[xIndex] * animationSpeed; positions[yIndex] += velocities[yIndex] * animationSpeed; lifetimes[index] -= animationSpeed; showParticle = true; } if ( showParticle ) { points[pointIndex] = new Point((int) ( positions[xIndex] * 10 ), (int) ( positions[yIndex] * 10 )); pointIndex += 1; } xIndex += 2; yIndex = xIndex + 1; } } private void draw() { Graphics2D graphics2D = (Graphics2D) bufferStrategy.getDrawGraphics(); graphics2D.setColor(Color.BLACK); graphics2D.fillRect(0, 0, getWidth(), getHeight()); for ( int i = 0; i < pointIndex; i++ ) { graphics2D.setColor(Color.getHSBColor(random.nextFloat(), (float) saturation, 1.0F)); graphics2D.fillOval(points[i].x, points[i].y, 5, 5); } graphics2D.dispose(); bufferStrategy.show(); } private double additionalXSpeed() { return ( reciprocate ) ? range * Math.sin(System.currentTimeMillis() / 1_000) : 0.0; } private double[] positions; private double[] velocities; private double[] lifetimes; private int pointIndex; private Point[] points; private BufferStrategy bufferStrategy; private ThreadLocalRandom random; } // End DrawingCycle class private final class InputHandler extends KeyAdapter { @Override public void keyPressed(KeyEvent aKeyEvent) { final int keyCode = aKeyEvent.getKeyCode(); switch ( keyCode ) { case KeyEvent.VK_UP -> saturation = Math.min(saturation + 0.1, 1.0); case KeyEvent.VK_DOWN -> saturation = Math.max(saturation - 0.1, 0.0); case KeyEvent.VK_PAGE_UP -> spread = Math.min(spread + 0.1, 5.0); case KeyEvent.VK_PAGE_DOWN -> spread = Math.max(spread - 0.1, 0.5); case KeyEvent.VK_RIGHT -> range = Math.min(range + 0.1, 2.0); case KeyEvent.VK_LEFT -> range = Math.max(range + 0.1, 0.1); case KeyEvent.VK_SPACE -> reciprocate = ! reciprocate; case KeyEvent.VK_Q -> Runtime.getRuntime().exit(0); default -> { /* Take no action */ } } } } // End InputHandler class private int particleCount; private int width; private int height; private double saturation; private double spread; private double range; private boolean reciprocate; private ExecutorService executorService; } // End ParticleFountain class } // End ParticleFountainTask class