Friday, March 24, 2017

Friday Fun XLIV - The Matrix

Aloha,

Last weekend I was thinking about a dot matrix display and remembered that this was always something I would like to do in JavaFX. Well after spending nearly 2 weeks in Singapore working on infrastructure documentation etc. I really needed some coding and because of the jetlag I had enough time during the early mornings to create a little control.
To give you an idea what I'm talking about here is a little screenshot of the control...



So as you can see this is not a really fancy control but more something simple and there is a reason for that. I remember that I've once started to create some kind of matrix control for JavaFX but that was in the early JavaFX 2 days and we had no Canvas node which was the reason why I've used nodes for each dot in the old version. But with this amount of dots the whole thing became really slow and because of that I simply never tried again.
But when I thought about it over the weekend the Canvas node came to my mind again and so I've started a new approach with the dot matrix control.
So this version runs completely in one Canvas node and is based on an integer array to make it fast. The main idea behind the dot matrix display was to create some kind of animated stock ticker.
This idea lead to 3 requirements

  1. the matrix size has to be configurable
  2. it must be fast enough for animation
  3. it must be possible to set colors for each dot
  4. it should be possible to show text

So let's take a short look at each requirement.

1. To make the matrix size configurable it is in principle only needed to define the number of dots per column and row you would like to see in the dot matrix. But there is one drawback here, if the control should be resizable you will also need to define the preferred size of the control to be able to resize it with the right aspect ratio.
Therefor you will find a constructor that not only takes the number of columns and rows but also the preferredWidth and preferredHeight. In addition you could also add a color for the dot on and dot off state. 
So to create a DotMatrix with a size of 128x16 dots and a dot on color of red and a dot off color of dark gray you have to write the following code...


DotMatrix m = new DotMatrix(128, 16, 264, 33, Color.RED, Color.DARKGRAY);

or if you prefer using the DotMatrixBuilder it will look like follows...


DotMatrix m = DotMatrixBuilder.create()
                              .prefSize(264, 33)
                              .colsAndRows(128, 16)
                              .dotOnColor(Color.RED)
                              .dotOffColor(Color.DARKGRAY)
                              .build();

2. To make the dot matrix fast enough for animation I've tried to avoid as much overhead as possible. With overhead I mean things like 

  • many nodes on the scene graph
  • crazy graphics (e.g. each dot has light reflection etc.)
  • complex drawing method

To avoid as many nodes on the scene graph as possible the best solution is to make use of the Canvas node. This represents only one node on the scenegraph, can contain complex graphics and the redraw is more or less under your control (immediate mode rendering).
And in addition the Canvas node is really fast :)
A picture explains more than 1000 words...so here is a screenshot of a former matrix that I did in Enzo...



I think you now understand what I mean with crazy graphics...I mean it looks nice but for a bigger display this is simply overkill. For this control I used nodes for each dot...or better each LED.
Because when animating the control I have to call the drawing method a lot and therefor this drawing method has to be as short and simple as possible. So let's take a look at the drawing method in the DotMatrix control...


private void drawMatrix() {
    ctx.clearRect(0, 0, width, height);
    for (int y = 0; y < rows; y++) {
        for (int x = 0; x < cols; x++) {
            ctx.setFill(convertToColor(matrix[x][y]));
            ctx.fillOval(x * dotSize + spacer, y * dotSize + spacer, 
                         dotSizeMinusDoubleSpacer, dotSizeMinusDoubleSpacer);
        }
    }
}

That's all, I clear the canvas, then simply iterate over the rows and columns, set the fill for the current dot and draw a circle at the current position defined by x and y.
Keeping things simple makes the drawing fast enough to do some animation.

3. To make it possible to set the color for each dot you need a 2d data structure which defines the position and stores the color. You can think of all possible combinations of Lists, Maps and custom Objects to realize that but if you would like to keep it simple you could simply stick with a simple 2-dimensional integer array. This will contain the x and y coordinates of each dot and you could use a 1 to enable the dot or a 0 to disable the dot.
This works and is simple and fast BUT how to enable custom colors for each dot???
For this it would be great to represent a color as an integer value but if you take a look at the javafx.scene.paint.Color class you will find all sorts of methods but unfortunately no method that returns the color as an integer value. Lucky me I remembered that the java.awt.Color class had such a method and so it was easy to take a look at the source code of OpenJDK (it's so nice to be able to have the source code of Java available).
To convert a color into an integer you simply can make use of some bit-shifting and the code to convert a color into an integer looks as follows...


public int convertToInt(final float R, final float G, final float B, final float A) {
    int red   = Math.round(255 * R);
    int green = Math.round(255 * G);
    int blue  = Math.round(255 * B);
    int alpha = Math.round(255 * A);
    return (alpha << 24) | (red << 16) | (green << 8) | blue;
}

Now with this in place it was easy to store the color value as integer in the 2-dimensional array of the matrix. Oh and of course it would also be nice to have a method to convert an integer back to a Color, so here it is...


public static Color convertToColor(final int COLOR_VALUE) {
    return Color.rgb((COLOR_VALUE & RED_MASK) >> 16, 
                     (COLOR_VALUE & GREEN_MASK) >> 8, 
                     (COLOR_VALUE & BLUE_MASK), 
                     ALPHA_FACTOR * ((COLOR_VALUE & ALPHA_MASK) >>> 24));
}

In JavaFX the opacity is defined by a double compared to an int in the AWT color which is the reason for the ALPHA_FACTOR multiplication here.
So now we can define for each dot in the matrix the location and the color by simply using a 2-dimensional integer array which is good for the performance.
Means most of the work in the drawing method is done by converting an integer back to a Color. But because all of the operations used for the conversion are mainly bit-shifting and object creation this is not a problem.

4. To be able to show text on the DotMatrix I needed to create some mapping of characters to dots. So my idea was to keep the DotMatrix close to a real hardware DotMatrix where you can define the xy position of the dot and a color. Means in the DotMatrix control switching on a dot is done by calling the method


public void setPixel(final int X, final int Y, final int COLOR_VALUE) {
    if (X >= cols || X < 0) return;
    if (Y >= rows || Y < 0) return;
    matrix[X][Y] = COLOR_VALUE;
}

As you can see I don't call the drawMatrix() method automatically which means I leave that up to you. So first one can set all the pixels needed and then you can call the drawMatrix() method. With this approach you can avoid a redraw after set a pixel.
This is a big advantage when doing some kind of dot matrix ticker where text is moving from right to left. In this case you can first set a vertical line of pixels before you really draw them.
But if you would like to automatically redraw after a pixel was set you will also find a method called setPixelWithRedraw() which does exactly this.
Now back to the character mapping, here I've decided to create an 8x8 font for the ASCII characters between 32 - 126. And if you only need numbers I've also created an 8x8 font that only contains the numbers from 0 - 9.
Again the goal was to keep it as simple as possible which was the reason to define each character by an integer array. So for example the digit 0 looks like this...


{
    0b00111000,
    0b01000100,
    0b01000100,
    0b01000100,
    0b01000100,
    0b01000100,
    0b01000100,
    0b00111000
}

As you can see I used the binary literals which is nice to represent a matrix of dots.
Now the only thing I had to do was to read out each bit of each row and do some math related to the dot position.
These calculations can be found in the setCharAt() and setDigitAt() methods in the DotMatrix control. 
A demo of how to use the DotMatrix control for a running text display can be found in the Demo class in the source code.

I will definitely add this control also to TilesFX because it could be handy as a stock ticker on a dashboard.
So that's it for today, I hope this control will be useful for someone except me :)

UPDATE:
I've removed the plain digits and added another MatrixFont which is 8x11 pixels. So now you can choose between 

  • MatrixFont8x8
  • MatrixFont8x11

In addition I've also added the possibility to set the dot shape to either DotShape.ROUND which is default or to DotShape.SQUARE.

Here is a little video which shows the control in action...


Please find the source code as usual at github.

More examples can be found here

Enjoy the upcoming weekend and keep coding... 

2 comments:

  1. Hi Gerrit,

    Looks really nice.

    I'm curious how many nodes were you using before to make it slow, with the retained graphics approach?

    Thanks,

    ReplyDelete
    Replies
    1. Well that really depends on hiw many effects, paths and overlapping nodes you use. There is no absolute number.
      Cheers,
      Gerrit

      Delete