package trident.swing; import java.awt.*; import java.awt.geom.Ellipse2D; import java.awt.geom.GeneralPath; import java.awt.image.*; import java.util.*; import java.util.List; import javax.swing.*; import org.pushingpixels.trident.Timeline; import org.pushingpixels.trident.Timeline.RepeatBehavior; import org.pushingpixels.trident.Timeline.TimelineState; import org.pushingpixels.trident.callback.TimelineCallbackAdapter; import org.pushingpixels.trident.ease.Spline; import org.pushingpixels.trident.swing.SwingRepaintTimeline; public class Particles { /** * This class is based on Romain Guy's work from * http://www.jroller.com/gfx/entry/new_blendings_modes_for_java2d available * under BSD license. */ private static final class AddContext implements CompositeContext { private Add add; public AddContext(Add add) { this.add = add; } @Override public void dispose() { } public void compose(Raster src, Raster dstIn, WritableRaster dstOut) { if (src.getSampleModel().getDataType() != DataBuffer.TYPE_INT || dstIn.getSampleModel().getDataType() != DataBuffer.TYPE_INT || dstOut.getSampleModel().getDataType() != DataBuffer.TYPE_INT) { throw new IllegalStateException( "Source and destination must store pixels as INT."); } int width = Math.min(src.getWidth(), dstIn.getWidth()); int height = Math.min(src.getHeight(), dstIn.getHeight()); float alpha = this.add.alpha; int[] srcPixel = new int[4]; int[] dstPixel = new int[4]; int[] srcPixels = new int[width]; int[] dstPixels = new int[width]; for (int y = 0; y < height; y++) { src.getDataElements(0, y, width, 1, srcPixels); dstIn.getDataElements(0, y, width, 1, dstPixels); for (int x = 0; x < width; x++) { int pixel = srcPixels[x]; srcPixel[0] = (pixel >>> 24) & 0xFF; srcPixel[1] = (pixel >>> 16) & 0xFF; srcPixel[2] = (pixel >>> 8) & 0xFF; srcPixel[3] = (pixel) & 0xFF; pixel = dstPixels[x]; dstPixel[0] = (pixel >>> 24) & 0xFF; dstPixel[1] = (pixel >>> 16) & 0xFF; dstPixel[2] = (pixel >>> 8) & 0xFF; dstPixel[3] = (pixel) & 0xFF; int[] result = new int[] { Math.min(255, srcPixel[0] + dstPixel[0]), Math.min(255, srcPixel[1] * srcPixel[0] / 255 + dstPixel[1] * dstPixel[0] / 255), Math.min(255, srcPixel[2] * srcPixel[0] / 255 + dstPixel[2] * dstPixel[0] / 255), Math.min(255, srcPixel[3] * srcPixel[0] / 255 + dstPixel[3] * dstPixel[0] / 255) }; dstPixels[x] = ((int) (dstPixel[0] + (result[0] - dstPixel[0]) * alpha) & 0xFF) << 24 | ((int) (dstPixel[1] + (result[1] - dstPixel[1]) * alpha) & 0xFF) << 16 | ((int) (dstPixel[2] + (result[2] - dstPixel[2]) * alpha) & 0xFF) << 8 | (int) (dstPixel[3] + (result[3] - dstPixel[3]) * alpha) & 0xFF; } dstOut.setDataElements(0, y, width, 1, dstPixels); } } } public static class Add implements Composite { private float alpha; public Add(float alpha) { this.alpha = alpha; } @Override public CompositeContext createContext(ColorModel srcColorModel, ColorModel dstColorModel, RenderingHints hints) { return new AddContext(this); } } public static class Particle { float x; float y; float opacity; float angle; int size; Color color; public Particle(float x, float y, int size, Color color, float angle) { this.x = x; this.y = y; this.size = size; this.opacity = 1.0f; this.angle = angle; this.color = color; } public void setX(float x) { this.x = x; } public void setY(float y) { this.y = y; } public void setOpacity(float opacity) { this.opacity = opacity; } public void setAngle(float angle) { this.angle = angle; } public void paint(Graphics g) { Graphics2D g2d = (Graphics2D) g.create(); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setComposite(new Add(this.opacity)); g2d.translate(this.x, this.y); g2d.setPaint(new RadialGradientPaint(0, 0, this.size / 2.0f, new float[] { 0.0f, 0.6f, 1.0f }, new Color[] { this.color, this.color, new Color(this.color.getRed(), this.color .getGreen(), this.color.getBlue(), 96) })); g2d.fill(new Ellipse2D.Float(-this.size / 2.0f, -this.size / 2.0f, this.size, this.size)); float d1 = (float) (Math.cos(this.angle) * this.size / 3); float d2 = (float) (Math.sin(this.angle) * this.size / 3); GeneralPath rh = new GeneralPath(); rh.moveTo(d1, d2); rh.lineTo(d2, -d1); rh.lineTo(-d1, -d2); rh.lineTo(-d2, d1); rh.closePath(); g2d.setPaint(new RadialGradientPaint(0, 0, this.size / 3.0f, new float[] { 0.0f, 1.0f }, new Color[] { new Color(255, 255, 255, 96), new Color(255, 255, 255, 32) })); g2d.fill(rh); Color c1 = new Color(255, 255, 255, 16); Color c2 = new Color(255, 255, 255, 32); g2d.setPaint(new LinearGradientPaint(d1, d2, -d1, -d2, new float[] { 0.0f, 0.5f, 1.0f }, new Color[] { c1, c2, c1 })); g2d.drawLine((int) d1, (int) d2, -(int) d1, -(int) d2); g2d.setPaint(new LinearGradientPaint(-d2, d1, d2, -d1, new float[] { 0.0f, 0.5f, 1.0f }, new Color[] { c1, c2, c1 })); g2d.drawLine(-(int) d2, (int) d1, (int) d2, -(int) d1); g2d.dispose(); } } public static class ParticlesPanel extends JPanel { private List<Particle> particles; boolean wasMouseInside; int lastX, lastY; public ParticlesPanel() { this.particles = new ArrayList<Particle>(); Timeline mouseTracker = new Timeline(); mouseTracker.addCallback(new TimelineCallbackAdapter() { @Override public void onTimelinePulse(float durationFraction, float timelinePosition) { Point mouseLoc = MouseInfo.getPointerInfo().getLocation(); SwingUtilities.convertPointFromScreen(mouseLoc, ParticlesPanel.this); boolean isInside = (mouseLoc.x >= 0) && (mouseLoc.y >= 0) && (mouseLoc.x < getWidth()) && (mouseLoc.y < getHeight()); if (isInside) { int x = mouseLoc.x; int y = mouseLoc.y; if (wasMouseInside) { int dist = (int) Math.sqrt((lastX - x) * (lastX - x) + (lastY - y) * (lastY - y)); makeParticles(lastX, lastY, x, y, 1 + dist / 16); } lastX = x; lastY = y; wasMouseInside = true; } else { wasMouseInside = false; } } }); mouseTracker.playLoop(RepeatBehavior.LOOP); Timeline repaint = new SwingRepaintTimeline(this); repaint.playLoop(RepeatBehavior.LOOP); } private synchronized void makeParticles(int x1, int y1, int x2, int y2, int numParticles) { Random randomizer = new Random(); Color[] cs = new Color[] { Color.red, Color.blue, Color.green, Color.magenta, Color.cyan, Color.yellow }; for (int i = 0; i < numParticles; i++) { int size = 4 + randomizer.nextInt(44); int duration = (200 - size) * 3; int moveDistance = 4 + randomizer.nextInt(76 - size); double moveDirection = 2 * Math.PI * Math.random(); int startX = x1 + i * (x2 - x1) / numParticles; int startY = y1 + i * (y2 - y1) / numParticles; int goalX = startX + (int) (moveDistance * Math.cos(moveDirection)); int goalY = startY + (int) (moveDistance * Math.sin(moveDirection)); float startAngle = (float) (2 * Math.PI * Math.random()); float endAngle = (float) (startAngle + 4 * Math.PI * Math.random() - 2 * Math.PI); final Particle particle = new Particle(startX, startY, size, cs[randomizer.nextInt(cs.length)], startAngle); this.particles.add(particle); Timeline timeline = new Timeline(particle); timeline.addPropertyToInterpolate("x", startX, goalX); timeline.addPropertyToInterpolate("y", startY, goalY); timeline.addPropertyToInterpolate("opacity", 1.0f, 0.0f); timeline .addPropertyToInterpolate("angle", startAngle, endAngle); timeline.addCallback(new TimelineCallbackAdapter() { @Override public void onTimelineStateChanged(TimelineState oldState, TimelineState newState, float durationFraction, float timelinePosition) { if (newState == TimelineState.DONE) { synchronized (ParticlesPanel.this) { particles.remove(particle); } } } }); timeline.setDuration(duration); timeline.setEase(new Spline(1.0f)); timeline.play(); } } BufferedImage offscreen; @Override protected void paintComponent(Graphics g) { g.setColor(Color.black); int w = getWidth(); int h = getHeight(); g.fillRect(0, 0, w, h); if ((offscreen == null) || (offscreen.getWidth() != w) || (offscreen.getHeight() != h)) { offscreen = GraphicsEnvironment.getLocalGraphicsEnvironment() .getDefaultScreenDevice().getDefaultConfiguration() .createCompatibleImage(w, h, BufferedImage.TRANSLUCENT); } Graphics2D g2d = offscreen.createGraphics(); g2d.setColor(new Color(0, 0, 0, 0)); g2d.setComposite(AlphaComposite.Src); g2d.fillRect(0, 0, w, h); synchronized (this) { for (Particle p : this.particles) p.paint(offscreen.getGraphics()); } g.drawImage(offscreen, 0, 0, null); } } public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { JFrame fr = new JFrame("Particles"); fr.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); ParticlesPanel panel = new ParticlesPanel(); panel.setPreferredSize(new Dimension(400, 300)); fr.add(panel); fr.pack(); fr.setLocationRelativeTo(null); fr.setVisible(true); } }); } }