Friday, May 25, 2012

Dead bitmaps could be beautiful... Part I

In the SteelSeries Swing library i use this conical gradient to visualize the frames of the gauges like this


When I started porting the gauges to JavaFX I stumbled upon the described problem and that's the reason why you won't find these frame designs in the current version of the gauges in the JFXtras project.

The much i like the scenegraph of JavaFX it has it's drawbacks too. E.g. if you would like to draw a cone like on the following image as a background image


The problem is that in a scenegraph every node is "alive" where in an image every pixel is more or less "dead". The advantage of death bitmaps is that you have to draw them once and after that you simply use the whole image where in a scenegraph every "living" node will be accessible as long as the scenegraph is alive. That means all the Line nodes that would be needed to create such a cone are always on the scenegraph which would slow it down.
So it absolutely makes sense to have an image node on which you could draw something once and after that simply use this image node to visualize something like this cone. With this solution you would end up with only one node instead of thousands of nodes for the same effect.
With JavaFX 2.2 there will be the Canvas node which represents exactly this image node I was talking about. The name canvas might remind you on the HTML5 canvas everybody is talking about and you are absolutely right. The Canvas node 2D API in JavaFX will be very similar to the 2D api that is used in HTML5 canvas. This means if you are familiar with the HTML5 canvas node it should be easy to work with the JavaFX Canvas node.
Well to be honest in my dreams the image node would not been using the Canvas api but the JavaFX api to be more consistent but...ok now we have to live with the Canvas api which is fine too.

So what I would like to do is to create a Shape object and apply a conical gradient to it...hmm...there are NO custom paints in JavaFX right now...
To be able to use the conical gradient in an easy way i created a little helper method named createConicalGradient() which takes the following arguments
  • a Shape object
  • an array of Stop objects
  • the rotation offset
and here it is...

private Canvas createConicalGradient(final Shape SHAPE, final Stop[] STOPS, final double ROTATION_OFFSET) {
    final Canvas CANVAS = new Canvas(SHAPE.getLayoutBounds().getWidth(), SHAPE.getLayoutBounds().getHeight());
    createConicalGradient(CANVAS, SHAPE, STOPS, ROTATION_OFFSET);
    return CANVAS;
}

private void createConicalGradient(final Canvas CANVAS, final Shape SHAPE, final Stop[] STOPS, final double ROTATION_OFFSET) {
    // adjust size of canvas to size of shape if needed
    if (CANVAS.getLayoutBounds().getWidth() < SHAPE.getLayoutBounds().getWidth()) {
        CANVAS.setWidth(SHAPE.getLayoutBounds().getWidth());
    }
    if (CANVAS.getLayoutBounds().getHeight() < SHAPE.getLayoutBounds().getHeight()) {
        CANVAS.setHeight(SHAPE.getLayoutBounds().getHeight());
    }
    // create clip shape
    final Shape CLIP = SHAPE;
    CLIP.setTranslateX(-SHAPE.getLayoutBounds().getMinX());
    CLIP.setTranslateY(-SHAPE.getLayoutBounds().getMinY());
    // adjust position of canvas in relation to shape
    CANVAS.setLayoutX(SHAPE.getLayoutBounds().getMinX());
    CANVAS.setLayoutY(SHAPE.getLayoutBounds().getMinY());
    CANVAS.setClip(CLIP);
    // create the gradient with the given stops
    final GraphicsContext CTX         = CANVAS.getGraphicsContext2D();
    final Bounds BOUNDS               = SHAPE.getLayoutBounds();
    final Point2D CENTER              = new Point2D(BOUNDS.getWidth() / 2, BOUNDS.getHeight() / 2);
    final double RADIUS               = Math.sqrt(BOUNDS.getWidth() * BOUNDS.getWidth() + BOUNDS.getHeight() * BOUNDS.getHeight()) / 2;
    final double ANGLE_STEP           = 0.1;
    final GradientLookup COLOR_LOOKUP = new GradientLookup(STOPS);
    CTX.translate(CENTER.getX(), CENTER.getY());
    CTX.rotate(-90 + ROTATION_OFFSET);
    CTX.translate(-CENTER.getX(), -CENTER.getY());
    for (int i = 0, size = STOPS.length - 1; i < size; i++) {
        for (double angle = STOPS[i].getOffset() * 360; Double.compare(angle,STOPS[i + 1].getOffset() * 360) <= 0; angle += 0.1) {
            CTX.beginPath();
            CTX.moveTo(CENTER.getX() - RADIUS, CENTER.getY() - RADIUS);
            CTX.setFill(COLOR_LOOKUP.getColorAt(angle / 360));
            if (RADIUS > 0) {
                CTX.fillArc(CENTER.getX() - RADIUS, CENTER.getY() - RADIUS, 2 * RADIUS, 2 * RADIUS, angle, ANGLE_STEP, ArcType.ROUND);
            } else {
                CTX.moveTo(CENTER.getX() - RADIUS, CENTER.getY() - RADIUS);
            }
            CTX.fill();
        }
    }
}

This method either uses an existing Canvas object or creates a new one, then it uses the given Shape object as the clip and draws the conical gradient by using the given Stop objects. To make sure that the conical gradient will fill the whole area I took the diagonal of the given shape to calculate the radius for the gradient. In this example i use a class named GradientLookup that is not part of JavaFX but could be found in the JFXtras project. This class makes it possible to interpolate colors in a given array of Stop objects. To create a radial frame that looks like the one in the first picture you just have to write the following code:

Pane pane = new Pane();

Stop[] stops = {
    new Stop(0.0, Color.rgb(254, 254, 254)),
    new Stop(0.125, Color.BLACK),
    new Stop(0.35, Color.rgb(153, 153, 153)),
    new Stop(0.5, Color.BLACK),
    new Stop(0.65, Color.rgb(153, 153, 153)),
    new Stop(0.875, Color.BLACK),
    new Stop(1.0, Color.rgb(254, 254, 254))
};
        
Shape shape = Shape.subtract(new Circle(200, 200, 200), new Circle(200, 200, 168));

Canvas canvas = createConicalGradient(shape, stops, 0);

pane.getChildren().addAll(canvas);

Scene scene = new Scene(pane, 800, 800, Color.rgb(68, 68, 68));

The result of this code looks like this




It's close to the original but not the same quality. This is because of the technique I use to create the gradient. In principle I create a lot of arc objects and vary the fill color of these arcs. So you could imagine that the stepsize that is used to calculate the angles for these Arc objects is very important for the quality of the resulting image. Another example of the conical gradient could be a stainless steel effect like this...






That's it for Part I, so stay tuned and keep coding...

1 comment: