Friday, July 13, 2012

Pushing pixels with PixelWriter

Rembember my blog post about the JavaFX Canvas node? I was so happy to be able to draw some pixel graphics to a node and use it as an ImagePattern to fill a shape...especially the conical gradient was fun to create.
BUT as you might have recognized the colors have not been as brilliant as in the Java Swing version. That's the image I'm talking about...




The reason for the subdued colors is the method that I used to create this gradient. In principle I simply rotate a line around the center of the image and vary the color of the line during the rotation. Because the lines have a defined width and the variation of the rotation angle happens in steps it could come to strange effects on large areas...




Now you could argue to decrease the angle stepsize but this would lead to a kind of smear effect in the inner part of the gradient where all the lines will overlay.


Long story short...this is not the right approach !!!


So what could be the solution then ??? Exactly, use the raster of pixels and calculate the color for each pixel dependend on the angle and the distance to the rotation center. To give you an idea I've tried to create an image that hopefully helps to explain it...




Each of those squares should represent one pixel and instead of drawing a simple line from the rotation center of the gradient to the outer part we now have to calculate the color for each pixel. Because we have no overlays and no problems on the outer parts of the gradient the colors will look much better as you could see on the next image...




On the left you see the line based gradient and on the right you see the raster based gradient. The colors are much more brilliant on the raster approach than on the line approach. If you take a close look to the left image you will also see some artifacts at 90°, 180° and 270°.


To get this raster based gradient I used the PixelWriter class that will be available in JavaFX 2.2. The following code shows you how to use it in the simplest way...

public Image randomPixels(int width, int height) {
    WritableImage img = new WritableImage(width, height);
    PixelWriter   pw  = img.getPixelWriter();
    Random        rnd = new Random();
    for (int y = 0 ; y < height ; y++) {
        for (int x = 0 ; x < width ; x++) {
            // Do the pixel manipulation
            pw.setColor(x, y, Color.rgb(rnd.nextInt(255), 
                                        rnd.nextInt(255), 
                                        rnd.nextInt(255)));
        }
    }
    return img;
}

If you call this method you will get something like this...




This means with a little math you could implement a lot of fun image filters/generators. The method that creates the conical gradient looks like this...



public Image getImage(int width, int height, List<Stop> stops) {
    WritableImage raster = new WritableImage(width, height);
    PixelWriter pixelWriter = raster.getPixelWriter();
    Color color = Color.TRANSPARENT;
    Point2D center = new Point2D(width / 2, height / 2); 
    for (int y = 0 ; y < height ; y++) {
        for (int x = 0 ; x < width ; x++) {
            double dx = x - center.getX();
            double dy = y - center.getY();
            double distance = Math.sqrt((dx * dx) + (dy * dy));
            double angle = Math.abs(Math.toDegrees(Math.acos(dx / distance)));
            if (dx >= 0 && dy <= 0) {
                angle = 90.0 - angle;
            } else if (dx >= 0 && dy >= 0) {
                angle += 90.0;
            } else if (dx <= 0 && dy >= 0) {
                angle += 90.0;
            } else if (dx <= 0 && dy <= 0) {
                angle = 450.0 - angle;
            }
            for (int i = 0; i < (stops.size() - 1); i++) {
                double offset = stops.get(i).getOffset();
                double nextOffset = stops.get(i + 1).getOffset();
                if (angle >= (offset * 360) && angle < (nextOffset * 360)) {
                    double fraction = 
                        (angle - offset * 360) / ((nextOffset - offset) * 360);
                    color = 
                        interpolateColor(stops.get(i).getColor(), 
                                         stops.get(i + 1).getColor(), 
                                         fraction);
                }
            }
            pixelWriter.setColor(x, y, color);
        }
    }
    return raster;
}

With this feature it was easy to implement some more of the missing features of the gauges in the JFXtras-labs project. Here is a little screenshot of a gauge using this kind of gradient for the frame and the background...




Well that's all for today, I hope you enjoy coding in JavaFX as much as I do...and if not...I don't care :)


So keep coding...

9 comments:

  1. Great, I have adapted this to the JavaScript library. I added circular plotting, and inner limit (to plot rings) and some anti-aliasing to the edges. Much better than the rotated line version :)

    ReplyDelete
    Replies
    1. Nice, that was also on my list, so thx for adding it before me... :)
      Cheers,
      Gerrit

      Delete
  2. Unfortunately pixel poking the HTML5 canvas is really slow in JavaScript. I'm optimising the loops as best I can at the expense of some readability. It should be just about acceptable from what I have so far.

    ReplyDelete
    Replies
    1. Hi Mark,
      I suggest to check it in and wait for the users response...because the painting usually happens only once or twice the time should be hopefully acceptable.
      Cheers,
      Gerrit

      Delete
    2. Done. The new routines plotted the patterns rotated 180 degrees from the original, I've just applied a simple top/bottom flip to fix this, cheaper than a rotation.

      Delete
  3. Good evening, this is exactly what I have been searching the net for, for 'bout a week now (nearly!) Say, you would not happen to have a complete example available, would you? I tried to re-create this, but keep getting an artifact (Red) at 90 degrees. Thanks again!

    ReplyDelete
    Replies
    1. Hi Chellsie,
      You could find the implementation of the "ConicalGradient" in the sources of the JFXtras project (http://jfxtras.org). The source is hosted on github and the code you are looking for should be in the utils package.
      Cheers,
      Gerrit

      Delete
  4. OMG I love this, I'm still new to javafx so the part where you write the image to a canvas or some other layout is the part that I'm not seeing do you have a full github implementation of the random pixel generator code?

    ReplyDelete
    Replies
    1. Hi there,
      Unfortunately not of this example but you will find the code also in my Medusa library on github, just search for the Helper.java class which contains the random pixel generator (https://github.com/HanSolo/Medusa)

      Delete