Something for the weekend
Updating source code and licensing
Well I'm back, and I'm very jet-lagged! As part of staying awake I've decided to release the source code under the Apache 2.0 license meaning you can all get it into which-ever projects you like pretty easily. Look in the downloads section for the updated Netbeans project including all of the latest content. You'll also need to download and link against the Cascade.jar or delete the references to the Demo file if you get linking errors, it's up to you!

I've also tweaked the carousel example following a suggestion from Romain Guy (his site has moved btw, follow updated link) that the jittering some people experienced is due to single pixel increments in size, so I've made it only size to even pixels and we'll see if that helps (Webstart updated).

Other bits of house keeping have also been done to help people find their way around the site, so I hope it's a little bit clearer now.

Finally, thanks so much for all of the messages (and comments) with encouragement and ideas. Every single one has been valuable and greatly appreciated.
|
Carousel Menu (a la Apple TV)
While everyone was wrapped up in Steve's reality distortion field during his recent keynote, I was ogling the Apple TV interface. What a fantastic product (I'm getting one of those, just what I need). Anyhow, one of the interface features that I noticed was that they had re-used a carousel to produce a funky menu. Well, I have a carousel component how hard can it be to re-use it to create.... a carousel menu?

CarouselMenu

Starting off, let's list out the tasks ahead of us:

1. We need to do a small amount of re-factoring on the carousel layout to allow us to sub-class it and over-ride a couple of pieces of functionality.
2. We need a new component that will contain a carousel, and a menu (I'm going to use a JList, it does the job!)
3. The menu needs a custom cell renderer to make the selected cell look pretty

The re-factoring was pretty minimal, but I did add a couple of methods, just before I start that, I need to mention one of the existing methods which needed to be over-ridden:

public void setFrontMostComponent(Component component)
Previously this would spin the specified component to the FRONT of the carousel. This time, I wanted it to be on the right of the carousel, so the new method (in the rather imaginatively named OffsetCarouselLayout class). This function just determines the target angle differently.

protected boolean shouldHide(Component comp, double angle)
One of the problems with the over-riding of the above function is that you can have component in front of the currently focused one. This new function is used to determine if a component should be hidden (based on where it is on the carousel). It lets me stop things appearing in front of the current component.

The next two are pretty obvious, they let me manipulate the center of the carousel (in the component) and the radius of the carousel (I didn't want it quite as squashed).

That's it, not bad, gotta-love OO programming!

So, let's summarize what we have; a single component with a JCarousel component (with a new layout manager), and a JList in it, pretty straight forward....

JCarouselMenu Structure

As you can imagine, Java makes most of this pretty easy. But there are a couple more steps that you'll find in the source code.

It turns out, JLists don't like wrapping the selected object when the user is using the keys to cycle through (it gets stuck at the top or the bottom). So you'll see in the code of the Carousel Menu that it has a keyboard listener to move the cursor to the top or the bottom when it hits one of the ends. Also we need to update which component is at the "front" of the carousel based on which item is selected, and a simple implementation of the list selection listener achieves that.

And of course, we need a nice border around the selected item. I've cheated a little and created a simple border called "ImageBorder" which takes a bounded image (like the one shown below) and a set of insets, and slices and dices it to make it resize-able.

menu_highlight

I've added the code for this below, and of course, once I'm back from my trip I'll update my project file so that you can all download the source. And rather wonderfully, that's it. Once again, Java can really deliver on a modern UI and I think you'll agree it makes my blog examples a little more usable!

webstart-small2

|
Java Dock Component
As promised I've done some work on an OS X a-like Dock component for Swing. I've implemented a couple of different uses for it, a Glass Pane version that displays the dock over a window, but also kept it in a separate component (which could be useful for a funky tab-like-pane). After spending several evenings tweaking it, I threw it all away for a much simpler solution using GridBagLayout, for more details, read on.

Picture 1


The final implementation essentially uses a GridBagLayout to control the dock, with spacers "pushing" the icons in the dock into the center. The mouse is tracked, and when it moves over one of the Components (you can just add any Java component into there) its preferred size is changed (therefore increasing the size). Of course, like everything it's not QUITE that simple. The diagram below shows the basic strategy.


dock panel

I'll update the source code for the Blog project when I get back home after my trip, but I'd like to focus on a couple of the interesting problems that needed solving.

Scaling The Icons Based on Distance From Pointer
The DockPanel sets the preferred size of the icons to control their "zooming" in the dock. It's implemented in a method called. This is implemented in the getComponentPreferedSize method. First it checks to see if the mouse is actually hovering over the given icon, if it is, it returns the enlarged size, otherwise it looks for the distance from the mouse pointer to the center of the icon, and linearly scales. The method is protected so it could be over-ridden by a subsequent implementation. I point this out because I'm pretty sure Apple are using a Sigma curve or the like scale theirs as the distance increases. I'll leave it to you to try it yourself!

Watching for Change
Another over-used class of mine that I've added to the Blog project is a MouseTracker which does a couple of things, including tracking a mouse inside a container, filtering out a lot of the events, and just passing interesting ones across (when I publish the source you'll see). One of the MouseTrackerListener call-backs is mouseMoved, and it's here that I update the parameters on the GridBagLayout to make any changes. There's one other place that the same thing is done. As usual we don't want everything flicking from small to giant etc so I have a timer that updates a tweened animation between the target size for the icons in the dock, and the size they currently are. Consequently actionPerformed also updates the layout.

Easy Animation
I've been pretty lazy here. If you look at the updateLayout method (which updates all the grid bag constraints) you'll see that the result of getComponentPreferredSize is passed into the tweenValue method. This is where the animation is done. The tweenValue takes a current size, and a target size and calculates a value between the two sizes. If the two aren't already the same it lets the animation timer know that the size being passed on isn't the final size, and then returns an intermediate size. Finally, it does a little bit of "clipping" to make sure that the icons can always fit in the space available.

Docking to the Different Sides
The code looks pretty lengthy, but a lot is taken up by dealing with the various layout wrinkles that come along with aligning the dock to different sides, so as you read through, just pick a compass point and worry about that.

Now speaking of the source code here it is, you can launch the web-start demo (it creates an AWT has been updated with a dock tab too to keep you going until all the source is available.

Note: I have fixed the problem that stopped the web-start working (it uses an AWTEventListener which requires ALL permissions, so the jar needed to be signed).

webstart-small2

Dock Panel Source Code

/*
* DockPanel.java
*
* Created on January 9, 2007, 6:22 AM
*
* Re-implementation of the dock to use a grid-bag-laid-out panel instead
* of the very complicated code I was developing.
*
*/

package com.blogofbug.swing.components;

import com.blogofbug.swing.delegates.MouseTracker;
import com.blogofbug.swing.delegates.MouseTrackerListener;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.Hashtable;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import java.awt.Insets;

/**
*
* @author nigel
*/
public class DockPanel extends JPanel implements MouseTrackerListener, ActionListener{
/**
* Enumeration object to contain the side of the screen/window the dock is against
*
*/
public enum Side {NORTH, SOUTH, EAST, WEST};

//The enlarged size of the icons
private int enlargedSize = 96;

//The normal size of the icons
private int normalSize = 32;

//Insets around each dock icon
private int insets = 2;

//The side the dock is docked to
private Side dockedTo = Side.SOUTH;

//Autohide?
private boolean autoHide = false;

//Auto-hiding!
private boolean hiding = false;

//Layout and standard constraints
private GridBagLayout layout = new GridBagLayout();
private GridBagConstraints filler = new GridBagConstraints();
private GridBagConstraints title = new GridBagConstraints();
private GridBagConstraints icon = new GridBagConstraints();

//Spacer Panels
protected JSpacer spacer; ;
protected JSpacer firstSpacer; ;
protected JSpacer lastSpacer;

//Hold index of components and their titles
private Hashtable iconLabels = new Hashtable();

//Track mouse movements (will be over the entire component)
private MouseTracker mouseTracker;

//Animation timer
private Timer timer = new Timer(0,this);
private boolean animating = false;

//Mouse location (last known)
private Point lastKnownMouse = new Point(0,0);

/** Creates a new instance of DockPanel */
public DockPanel(int normalSize, int enlargedSize, Side dockedTo) {
//Set up the basics
this.dockedTo = dockedTo;
this.enlargedSize = enlargedSize;
this.normalSize = normalSize;

//Prepare the timer
timer.setCoalesce(true);
timer.setDelay(20);
timer.setRepeats(true);
timer.start();

//make me transparent
setBackground(null);
setOpaque(false);

//Set up the mouse tracker
mouseTracker = new MouseTracker(this,this);

//Set up the spacers
spacer = new JSpacer();
firstSpacer = new JSpacer();
lastSpacer = new JSpacer();

//Initial setup
setLayout(layout);
initializeLayout();
add(spacer,filler);
add(firstSpacer,icon);
add(lastSpacer,icon);

//Perform initial layout
updateLayout(null);
}

public boolean isAutoHiding(){
return autoHide;
}

public void setAutoHiding(boolean autoHide){
this.autoHide = autoHide;
mouseMoved(lastKnownMouse);
}

public DockPanel(int normalSize, int enlargedSize){
this(normalSize,enlargedSize,Side.SOUTH);
}

public void setNormalSize(int normalSize){
this.normalSize = normalSize;
updateLayout(null);
layout.layoutContainer(this);
}

public void setEnlargedSize(int enlargedSize){
this.enlargedSize = enlargedSize;
updateLayout(null);
layout.layoutContainer(this);
}

/**
* Sets up the basic layout and then calls updateLayout to finalize the
* individual dock elements
*/
private void initializeLayout(){
//Standard settings for the filler panel
filler.fill = GridBagConstraints.BOTH;
filler.weightx = 1.0;
filler.weighty = 1.0;

//Standard settings for the title components
title.anchor = GridBagConstraints.CENTER;

//Standard settings for the icon components
icon.anchor = GridBagConstraints.CENTER;
icon.weightx = 0.0;
icon.weighty = 0.0;
icon.insets = new Insets(insets/2,insets/2,insets/2,insets/2);

//Do various bits of specific constraints layout
switch (dockedTo){
case SOUTH:
filler.gridx = 0;
filler.gridy = 0;
filler.gridheight =1;
filler.gridwidth = GridBagConstraints.REMAINDER;
title.gridx = 0;
title.gridy = 1;
icon.gridx = 0;
icon.gridy = 2;
icon.anchor = GridBagConstraints.SOUTH;
break;
case NORTH:
filler.gridx = 0;
filler.gridy = 2;
filler.gridheight =1;
filler.gridwidth = GridBagConstraints.REMAINDER;
title.gridx = 0;
title.gridy = 1;
icon.gridx = 0;
icon.gridy = 0;
icon.anchor = GridBagConstraints.NORTH;
break;
case WEST:
filler.gridx = 2;
filler.gridy = 0;
filler.gridheight =GridBagConstraints.REMAINDER;
filler.gridwidth = 1;
title.gridx = 1;
title.gridy = 0;
icon.gridx = 0;
icon.gridy = 0;
icon.anchor = GridBagConstraints.WEST;
break;
case EAST:
filler.gridx = 0;
filler.gridy = 0;
filler.gridheight =GridBagConstraints.REMAINDER;
filler.gridwidth = 1;
title.gridx = 1;
title.gridy = 0;
icon.gridx = 2;
icon.gridy = 0;
icon.anchor = GridBagConstraints.EAST;
break;
}
}

/**
* Changes the side of the panel the dock is attached to
*/
public void setDockedTo(DockPanel.Side dockedTo) {
this.dockedTo = dockedTo;
initializeLayout();
layout.setConstraints(spacer,filler);
updateLayout(null);
layout.layoutContainer(this);
}

/**
* Determines the appropriate gridbag weight for the "book-end" spacers
* at either end of the dock
*
* @return The weight
*/
private double spacerWeightX(){
switch (dockedTo){
case NORTH:
case SOUTH:
return 1.0;
case EAST:
case WEST:
return 0.0;
}
return 0.0;
}

/**
* Determines the appropriate gridbag weight for the "book-end" spacers
* at either end of the dock
*
* @return The weight
*/
private double spacerWeightY(){
return 1.0-spacerWeightX();
}

/**
* Determines the appropriate size for the component based on the current "mouse over"
* component, the location in the dock area relative to the overall dock panel, and a given component
* This is protected so it can be over-ridden to apply a better alogrithm .
*
* @param p The location of the mouse relative to the overall dock
* @param compoment The component to be sized
* @param highlightedComponent The current component with the mouse over it
* @return A Dimension object with the recommened size of the component
*/
protected Dimension getComponentPreferedSize(Point p, Component component, Component highlightedComponent){
//If we are hiding it should be small
if (hiding){
return new Dimension(insets*3,insets*3);
}

//determine it's distance based on half the size of the biggest component
double compSize = component.getSize().height;
double distanceFromCenter = 0;
double delta = (double) (enlargedSize-normalSize);

if ((highlightedComponent!=null)){
if (highlightedComponent==component){
return new Dimension(enlargedSize,enlargedSize);
}
switch (dockedTo){
case NORTH:
case SOUTH:
distanceFromCenter = Math.abs((double) ((component.getLocation().x + compSize/2 )- p.x));
break;
case EAST:
case WEST:
distanceFromCenter = Math.abs((double) ((component.getLocation().y + compSize/2 )- p.y));
break;
}
double cdy = 1.0 - distanceFromCenter/(double) (enlargedSize*2);
double tSize = Math.max((double) normalSize + cdy * delta,(double) normalSize);
return new Dimension((int)tSize,(int) tSize);
} else {
return new Dimension(normalSize,normalSize);
}
}

/**
* Essentially removes the spacer that forces the dock to the bottom of the
* panel, neat if you would like to use the panel as something like a
* special tab top.
*
* @param fillSpace true if the panel should fill up any empty space above the dock,
* false if it should not
*/
public void fillSpace(boolean fillSpace){
spacer.setVisible(fillSpace);
}

/**
* Determines a value between the current and target including easing. Can
* be over-ridden if desired to have different sizing behavior. Implementers
* should note that if the size is not clipped to ensure that the icons will
* all fit in the dock, gridbag does Bad Things (tm).
*
* @param current The current value
* @param target The target value
* @return A value equal to or between target and current
*/
protected double tweenValue(double current, double target){
//Just make sure we do nothing if they are the same
if (current==target){
return target;
}

//Determine the difference
double delta = (target-current)/4.0;

//Tween in the right direction, but always by at least one pixel
if (delta>0){
delta = Math.max(1.0,delta);
} else if (delta<0){
delta = Math.min(-1.0,delta);
}

switch (dockedTo){
case SOUTH:
case NORTH:
return Math.min(current+delta,( getWidth()-insets*iconLabels.size()*2)/iconLabels.size()-10);
default:
return Math.min(current+delta,(getHeight()-insets*iconLabels.size()*2)/iconLabels.size());
}
}

/**
* Determines if a component is in the dock
*
*/
public boolean dockContains(Component component){
if (iconLabels.get(component)!=null){
return true;
}
return false;
}

/**
* Takes a current size, and determines a new size setting up an animation
* if needed.
*
* @param currentSize The current size of the component
* @param newSize The new size of the component
* @return The in-between size
*/
private Dimension tweenSize(Dimension currentSize, Dimension newSize){
if ((newSize.width != currentSize.width) || (newSize.height != currentSize.height)){
animating = true;
newSize.width = (int) tweenValue(currentSize.width,newSize.width);
newSize.height = (int) tweenValue(currentSize.height,newSize.height);
}
return newSize;
}

/**
* Determines the new settings for the gridbag layout, and then applies them
* to all components
*
*/
private void updateLayout(Point p){
Component highlightedComponent = null;
animating = false;
if (p!=null){
highlightedComponent = getComponentAt(p);
//Make sure it's at least in the dock zone
switch (dockedTo){
case NORTH:
if (p.y>spacer.getY()){
highlightedComponent=null;
}
break;
case SOUTH:
if (p.y
highlightedComponent=null;
}
break;
case WEST:
if (p.x>spacer.getX()){
highlightedComponent=null;
}
break;
case EAST:
if (p.x
highlightedComponent=null;
}
break;
}
if ((highlightedComponent==firstSpacer) || (highlightedComponent==lastSpacer)){
highlightedComponent = null;
}
}
//Variables used for incrementing grid-x's and grid-y's
int dx=0,dy=0;
switch (dockedTo){
case NORTH:
case SOUTH:
dx=1;
break;
case EAST:
case WEST:
dy=1;
break;
}

//Set everything up as it should be
initializeLayout();

//Add first spacer and move to next position
icon.weightx = spacerWeightX();
icon.weighty = spacerWeightY();
icon.fill = GridBagConstraints.BOTH;
layout.setConstraints(firstSpacer,icon);
icon.fill = GridBagConstraints.NONE;
icon.gridx += dx;
icon.gridy += dy;
title.gridx += dx;
title.gridy += dy;

//Iterate through the components updating everything
Component[] components = getComponents();
for (Component component : components){
//Make sure it's not a filler or a title
if (!((component == spacer) || (component instanceof DockLabel) || (component == firstSpacer) || (component == lastSpacer))){
//Set the weight
icon.weightx = 0.0;
icon.weighty = 0.0;
//Icon first
layout.setConstraints(component, icon);

//Set the prefered size of the component
component.setPreferredSize(tweenSize(component.getSize(),getComponentPreferedSize(p,component,highlightedComponent)));

//Set the visibility of its label
if (highlightedComponent==component){
iconLabels.get(component).setVisible(true);
} else {
iconLabels.get(component).setVisible(false);
}

//Title next
layout.setConstraints(iconLabels.get(component),title);

//Move to next position
icon.gridx += dx;
icon.gridy += dy;
title.gridx += dx;
title.gridy += dy;
}
}

//Add the last spacer
icon.weightx = spacerWeightX();
icon.weighty = spacerWeightY();
icon.fill = GridBagConstraints.BOTH;
layout.setConstraints(lastSpacer,icon);
}

/**
* Adds a new item to the dock
*
*/
public void addDockElement(Component component, String label){
if (iconLabels.get(component)!=null){
return;
}

DockLabel newLabel = new DockLabel(label);

//We would like it to do something funcky as it adds them and make them grow
component.setPreferredSize(new Dimension(0,0));
iconLabels.put(component,newLabel);
add(component);
add(newLabel);
updateLayout(mouseTracker.getPosition());
}

/**
* Not interested
*/
public void mouseCrossThreshold(boolean mouseEntered) {
}

/**
* When the mouse moves, update the layout. Should be optimized when the
* highlighted component is the spacer to not do anything
*/
public void mouseMoved(Point position) {
if (autoHide){
Component mouseOver = getComponentAt(position);
if ((mouseOver == firstSpacer) || (mouseOver == lastSpacer) || (spacer==mouseOver)){
hiding = true;
} else {
hiding = false;
}
} else {
hiding = false;
}
lastKnownMouse = position;
updateLayout(position);
layout.layoutContainer(this);
}

public void paint(Graphics graphics) {
graphics.setColor(new Color(200,200,220,127));
Rectangle dockSize = null;
int oldNormalSize=normalSize;
if (hiding){
normalSize = Math.min(normalSize, ((dockedTo==Side.NORTH) || (dockedTo==Side.SOUTH)) ? firstSpacer.getHeight(): firstSpacer.getWidth());
}
//Draw the main dock background
switch (dockedTo){
case NORTH:
dockSize = new Rectangle(firstSpacer.getX()+firstSpacer.getWidth(),0,getWidth()-(lastSpacer.getWidth()+firstSpacer.getWidth()),normalSize);
break;
case SOUTH:
dockSize = new Rectangle(firstSpacer.getX()+firstSpacer.getWidth(),getHeight()-normalSize,getWidth()-(lastSpacer.getWidth()+firstSpacer.getWidth()),normalSize);
break;
case WEST:
dockSize = new Rectangle(0,firstSpacer.getHeight(),normalSize,getHeight()-(lastSpacer.getHeight()+firstSpacer.getHeight()));
break;
case EAST:
dockSize = new Rectangle(getWidth()-normalSize,firstSpacer.getHeight(),normalSize,getHeight()-(lastSpacer.getHeight()+firstSpacer.getHeight()));
break;
}
if (hiding){
normalSize = oldNormalSize;
}
dockSize.x-=insets;
dockSize.y-=insets;
dockSize.width+=insets*2;
dockSize.height+=insets*2;
graphics.fillRect(dockSize.x,dockSize.y,dockSize.width,dockSize.height);

//Draw the outline
graphics.setColor(new Color(255,255,255,127));
switch (dockedTo){
case SOUTH:
graphics.drawRect(dockSize.x,dockSize.y,dockSize.width-1,dockSize.height);
break;
}
super.paint(graphics);
}

public void actionPerformed(ActionEvent actionEvent) {
if (animating){
updateLayout(lastKnownMouse);
layout.layoutContainer(this);
}
}

//For convenience
public class JSpacer extends JComponent {
public JSpacer(){
setBackground(null);
setOpaque(false);
}



}

//Temporary until I do something else
public class DockLabel extends StrokedLabel {
public DockLabel(String title){
super(title);
}

};
}

|
Recreating the Apple Dock in Java
I had hoped to keep up the momentum with a few more of my "rough hacks" being tweaked enough to be published, but the start of the business year has as always deflected me. To top it all I'm off on a couple of weeks of business trips so I'm unlikely to be in a position to publish. However, a couple of international flights can be great for giving you time to code in the flight. To pique your interest while I'm off, I thought you might like to see the next component I've been working up, the Dock. It's actually pretty close but I haven't done things like allowing it to be attached to the various compass points.

Anyhow, there is something coming along, and here's a little teaser....


Picture 2

|
Java Self-Leveling Fluid Simulation - Help Needed!
I blogged in the new year about a little hack-up of an old algorithm. Well I've been fiddling with it in the evenings and I've managed to optimize the self-leveling code so that it barely impacts performance. Great, I hear you say, that's wonderful now post a Web-Start demo. I'm going to, but first I would like you to read a bit, and then help!

Picture 1

Sleep patterns have been knocked all over the place with the return to work and the bustle of the new year. Oh, and there's a matter of the Ashes which is on during the middle of the night giving me an excuse to get out of bed rather than trying to go to sleep. So I decided to use the early hours to optimize.

A few notes first:

1. This has been optimized for dual-or-more-core processors (more later)
2. I'm not claiming it's the fastest or best or anything. This is intellectual mastication (ahem) and hopefully a hint of "oh shiny!"

About 1. As I've said, once upon a time I used to write games, back in the days when 20Mhz was pretty exciting and SVGA was bleeding edge. I've built up some opinions about how I would go about making use of dual cores, and this demo implements some of them. The simulation runs in one thread, and is considered more important than drawing. The second thread does the painting (javax.swing.Timer anyone) and run's every 20th of a second (here in the UK on the Amiga, 50Hz was the frame-rate we were aiming for, so that's what I was going for).

I have a MacBook Pro and an almost-exactly-the-same-spec Dell 620 to test on so they are both Dual-Core, I'd love to read your comments on performance on single core (in fact whatever machine you have) machines. But that brings me to something else I really need your help with.

I'm running on Java SE 6 on both the Mac and Windows. Windows has always been a bit faster (since I've been writing this), but when I split up the main thread, Windows suddenly started to claim it was A LOT faster. As in it's claiming 0ms on the Dell for the simulation, and often 0ms for the re-draw. Hum. Can't see why the instrumentation would suddenly change, but hot-damn it does seem to be running without slow down (on the dell at least) barely chewing any (or either) of the cores. So again, any comments on your experiences would be great.

The web-startable app below needs Java 5, but please try on Java 6. Please post your results... and don't forget to click on "Tap On" when you are ready to start the water!

webstart-small2

|
Re-writing history implementing old game in Java
I have a confession, I was an Amiga fan-boy. More than that, I used to develop games for the platform. One game we were never able to find a publisher for was called Cascade. It used a really simple algorithm to simulate water a particle at a time, and by manipulating the environment a player would attempt to collect enough water before the time ran out. Well, with a really tightly engineered loop the Amiga version on a 1200 used to manage about 400 drops + the rest of the game engine every 50th of a second. I recently wondered how Java would handle the algorithm and whether a few of the limitations could be worked around. I have to admit, it's looking good!

Picture 2


As you can see, 400 drops is no longer a problem inside the "20ms" limit required to meet the Amiga's performance, in-fact you can crank it up to over 100,000 on my MBP before it starts to sweat. No great surprise there, the processor is massively faster. What has surprised me is how easy it is to implement in Java, how well Java2D works. In addition, the Java implementation is doing a whole lot more than the original version. I'm not sure if I'm going to post the full source or not, but here are some of the problems that have been solved with the old algorithm.

Vertical Columns
The old routine didn't have any idea of the inertia a particle of water had as it flowed over the scene, so when the water could go down it would, creating very artificial looking columns of falling water.

Picture 6

Easy to address, give the water some inertia, immediately much nicer diffusing water falls.

Picture 7

Self-Leveling
This was a big one, the levels of the game had to be very carefully designed to ensure that no part of a level would highlight the fact that the water would not self-level (pressure equalizing the water level). It was a real pain, and subtly caused other minor problems. Have a look at the U-bends in the screen shot below. The water should be pushing down equalizing the pressure in the u-bend.

Picture 10

Well in the spirit of the original code (that is that it looks as much like water as possible, without ever really simulating what water does), I've managed to solve this problem too, although it is currently impacting performance (3-4 times slower in regions where water self-levels) but there are lots of ways that can be improved it just the raw solution at the moment, plugged into the inner loop. As you can see, it's working perfectly...

Picture 12

So what's the point of the teasing post? Well I'm not sure what I'm going to do with this code yet, it's partly an exercise for walking down memory lane for me, in which case I'll just post the code so people can do a better job than me! However, it's also an interesting look at writing dynamic graphics algorithms in Java, in which case I'm going to publish my findings so that they can be exploited by others. Anyhow, let me know if you find it interesting, I'll make it web-startable soon so you can all play with the routine (it's kind of hypnotic).

Update: The web-start demo has been implemented, and I would really like your help with getting some results from a range of machines: Click Here
|