Newsletter sign-up
View all newsletters

Sign up for our Enterprise Java Newsletter

Enterprise Java
JavaWorld Daily Brew

Reducing JavaFX's memory footprint via a CustomNode alternative


 

Much has been written about JavaFX's performance from the perspective of execution speed. Because less has been written about performance in terms of memory footprint, I was interested to discover developer Markus Kohler's JavaFX memory overhead blog post.

Kohler's post focuses on the amount of memory occupied by JavaFX variables of various types (such as Booleans), which looks to be considerable. I suspect that Sun will manage to significantly reduce this memory usage as JavaFX matures. However, I wonder if Sun will address another area where memory usage could become considerable: custom nodes.

JavaFX's CustomNode class inherits more than 30 attributes (or variables, if you prefer) from its Node superclass. Combine this count with the number of attributes you might add to your own CustomNode subclass, take Kohler's findings into account, and you can see that a single instance of your subclass will probably occupy at least a few hundred bytes.

Imagine a scenario where you create a scene that renders hundreds of custom node instances. For example, perhaps the scene is a complex schematic diagram of an electronic circuit that consists of many instances of various custom node classes representing resistors, diodes, capacitors, and other electronics components. Collectively, these instances could occupy a considerable amount of memory.

I regard CustomNode as JavaFX's analogue of a heavyweight component. For complex scenes that are comprised of many symbols (such as resistors) and other items (such as lines representing connecting wires), I believe that something lightweight is needed, and present a lightweight alternative to CustomNode in this post. Ironically, this alternative depends on a single heavyweight Node instance.

The SGCanvas and Canvas classes

My CustomNode alternative combines an SGCanvas Java class with a Canvas JavaFX class. These classes implement a canvas node onto which a string of drawing instructions (specified via a Canvas attribute, and implemented via SGCanvas's paint() method) renders content whenever the node is rendered. Listing 1 presents SGCanvas's source code.

Listing 1: SGCanvas.java (version 1)

/*
* SGCanvas.java
*/

package canvasdemo1;

import java.awt.Color;
import java.awt.Graphics2D;

import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;

import java.util.StringTokenizer;

import com.sun.scenario.scenegraph.SGLeaf;

public class SGCanvas extends SGLeaf
{
    private Color fill;

    private int height, width;

    private String content;

    @Override
    public final Rectangle2D getBounds (AffineTransform transform)
    {
        System.out.println ("getBounds");

        float x = 0;
        float y = 0;
        float w = width;
        float h = height;

        if (transform != null && !transform.isIdentity ())
        {
            if (transform.getShearX () == 0 && transform.getShearY () == 0)
            {
                // No rotations...

                if (transform.getScaleX () == 1 && transform.getScaleY () == 1)
                {
                    // just a translation...

                    x += transform.getTranslateX ();
                    y += transform.getTranslateY ();
                }
                else
                {
                    float coords [] = { x, y, x+w, y+h };
                    transform.transform (coords, 0, coords, 0, 2);
                    x = Math.min (coords [0], coords [2]);
                    y = Math.min (coords [1], coords [3]);
                    w = Math.max (coords [0], coords [2])-x;
                    h = Math.max (coords [1], coords [3])-y;
                }
            }
            else
            {
                float coords [] = { x, y, x+w, y, x, y+h, x+w, y+h };
                transform.transform (coords, 0, coords, 0, 4);
                x = w = coords [0];
                y = h = coords [1];
                for (int i = 2; i < coords.length; i += 2)
                {
                    if (x > coords [i]) x = coords [i];
                    if (w < coords [i]) w = coords [i];
                    if (y > coords [i+1]) y = coords [i+1];
                    if (h < coords [i+1]) h = coords [i+1];
                }
                w -= x;
                h -= y;
            }
        }

        return new Rectangle2D.Float (0, 0, width, height);
    }

    @Override
    public void paint (Graphics2D g)
    {
        g.setColor (fill);
        g.fillRect (0, 0, width, height);

        int ox = 0, oy = 0;

        StringTokenizer st = new StringTokenizer (content);
        while (st.hasMoreTokens ())
        {
            String token = st.nextToken ();

            if (token.equals ("C"))
            {
                int red = Integer.parseInt (st.nextToken ());
                int grn = Integer.parseInt (st.nextToken ());
                int blu = Integer.parseInt (st.nextToken ());

                g.setColor (new Color (red, grn, blu));
            }
            else
            if (token.equals ("L"))
            {
                int x = Integer.parseInt (st.nextToken ());
                int y = Integer.parseInt (st.nextToken ());
                g.drawLine (ox, oy, x, y);
                ox = x;
                oy = y;
            }
            else
            if (token.equals ("M"))
            {
                int x = Integer.parseInt (st.nextToken ());
                int y = Integer.parseInt (st.nextToken ());
                ox = x;
                oy = y;
            }
        }
    }

    public final void setContent (String content)
    {
        System.out.println ("setContent");
        this.content = content;
        repaint (false);
    }
   
    public final void setFill (Color fill)
    {
        System.out.println ("setFill");
        this.fill = fill;
        repaint (false);
    }

    public final void setHeight (int height)
    {
        System.out.println ("setHeight");
        this.height = height;
        repaint (true);
    }

    public final void setWidth (int width)
    {
        System.out.println ("setWidth");
        this.width = width;
        repaint (true);
    }
}

SGCanvas subclasses Project Scene Graph's (PSG's) com.sun.scenario.scenegraph.SGLeaf class in order to tell the JavaFX runtime that it knows how to render its appearance. If you're unfamiliar with PSG (also known as Scenario), check out my InformIT article, Creating Java User Interfaces with Project Scene Graph.

SGCanvas next declares four private write-only properties that specify the canvas's background color, height and width, and content (a string of rendering instructions); and overrides the public final Rectangle2D getBounds(AffineTransform transform) method to return bounds information when needed by the JavaFX runtime. (I copied much of this method's code from PSG's SGImage class.)

Moving on, SGCanvas overrides the public void paint(Graphics2D g) method to render its content whenever the JavaFX runtime requires this node to be rendered. After filling the background with the color specified by the fill property, paint() uses a tokenizer to extract each instruction from the content property, and then performs the instruction's graphics operation:

  • C red grn blu: Specify the current drawing color. Capital letter C is followed by three integer values that specify the color's red, green, and blue components. Each value must range from 0 through 255.
  • L x y: Draw a line from the current position to the new position. Capital letter L is followed by two integer values that specify the X coordinate followed by the Y coordinate. Each value must be greater than or equal to 0.
  • M x y: Set the current position from where a line is drawn. Capital letter M is followed by two integer values that specify the X coordinate followed by the Y coordinate. Each value must be greater than or equal to 0.

For simplicity, I chose to parse the content string via java.util.StringTokenizer. Because a new StringTokenizer must be created each time the paint() method is invoked (canvas content might have changed), and because at least one space character must appear between each token (usually to prevent a runtime exception), I present a better alternative in the next version of this class.

Finally, SGCanvas provides four methods for setting its four properties. Each method outputs a message to standard output (letting you know when it's called). It then updates the property and invokes an inherited repaint() method to repaint the canvas: false is passed if only the visual state has changed; true is passed if node bounds have also been changed and must be recalculated.

Listing 2 presents Canvas's source code.

Listing 2: Canvas.fx (version 1)

/*
* Canvas.fx
*/

package canvasdemo1;

import javafx.scene.Node;

import javafx.scene.paint.Color;

import com.sun.scenario.scenegraph.SGNode;

public class Canvas extends Node
{
   override function impl_createSGNode (): SGNode
   {
       new SGCanvas ()
   }

   function getSGCanvas (): SGCanvas
   {
       impl_getSGNode () as SGCanvas
   }

   public var content: String on replace
   {
       getSGCanvas ().setContent (content)
   }

   public var fill: Color = Color.WHITE on replace
   {
       getSGCanvas ().setFill (new java.awt.Color (fill.red, fill.green,
                                                   fill.blue))
   }

   public var height = 0 on replace
   {
       getSGCanvas ().setHeight (height)
   }

   public var width = 0 on replace
   {
       getSGCanvas ().setWidth (width)
   }
}

Canvas relies on Node's undocumented impl_createSGNode() function to instantiate SGCanvas. It also relies on the undocumented impl_getSGNode() function to return this reference whenever it needs to invoke an SGCanvas method, which takes place exclusively in the context of a replace trigger.

Undocumented danger!

Although both versions of SGCanvas and Canvas work properly under JavaFX 1.1 (and probably under 1.1.1 as well), it's possible that these classes might not work under a future JavaFX release because they rely on JavaFX runtime features (which aren't officially documented).

I've chosen what I think are reasonable defaults for Canvas's four attributes: no content to render, a white fill color, and zero dimensions (how big should the node default to when you don't know where it will be placed?). You will need to assign meaningful values to content, height, and width, and possibly to fill, as Listing 3 demonstrates.

Listing 3: Main.fx (version 1)

/*
* Main.fx
*/

package canvasdemo1;

import javafx.scene.Scene;

import javafx.scene.paint.Color;

import javafx.stage.Stage;

Stage
{
    title: "Canvas Demo #1"
    width: 300
    height: 300

    var sceneRef: Scene
    scene: sceneRef = Scene
    {
        var canvasRef: Canvas
        content:
        [
            Canvas
            {
                content: "C 255 0 0 M 10 10 L 10 90 L 90 90 L 90 10 L 10 10 "
                         "C 0 255 0 L 90 90 C 0 0 255 M 90 10 L 10 90"
                fill: Color.BLACK
                width: 100
                height: 100
            }

            canvasRef = Canvas
            {
                content: "C 255 0 0 M 10 10 L 10 90 L 90 90 L 90 10 L 10 10 "
                         "C 0 255 0 L 90 90 C 0 0 255 M 90 10 L 10 90"
                fill: Color.BLACK
                width: 100
                height: 100
                translateX: bind (sceneRef.width-
                                  canvasRef.boundsInParent.width)/2.0
                translateY: bind (sceneRef.height-
                                  canvasRef.boundsInParent.height)/2.0
                rotate: 30
                scaleX: 1.5
                scaleY: 1.5
            }
        ]
    }
}

Listing 3 reveals two occurrences of Canvas being integrated into a scene. You can scale, rotate, and translate the canvas. (You could even assign a key or mouse listener to this node.) The most important aspect of the canvas node, however, is the compactness achieved by specifying its content via a string of instructions. Figure 1 shows the resulting untransformed and transformed canvases with their content.

Figure 1: Untransformed and transformed versions of the canvas reveal a need for antialiasing. (Click to enlarge.)

You need to add Scenario.jar (which contains SGLeaf, SGNode, and other PSG classes) to the CanvasDemo1 script's classpath before you can build and run this script -- you also need to perform this task before you can build and run the next section's CanvasDemo2 script. Assuming that you're using NetBeans IDE 6.5, complete the following steps to accomplish this task:

  • Activate the Project Properties dialog box via the File menu, or by right-clicking the project name and selecting Properties from the popup menu.
  • Select this dialog box's Libraries category and click the resulting pane's Add JAR/Folder button.
  • Use the resulting file chooser to navigate to Scenario.jar. On my platform, this file locates in the C:\Program Files\NetBeans 6.5\javafx2\javafx-sdk\lib\desktop\ directory.

Improving the canvas node

The canvas node needs many improvements. For example, its lines are not antialiased. Also, this node suffers from the string tokenizer issues mentioned previously. And then there is the problem of being restricted to a very limited language in which to specify rendering instructions. This section addresses these deficiencies by revealing an improved canvas node, starting with Listing 4's updated SGCanvas class.

Listing 4: SGCanvas.java (version 2)

/*
* SGCanvas.java
*/

package canvasdemo2;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;

import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;

import java.util.Stack;

import com.sun.javafx.runtime.sequence.Sequence;

import com.sun.scenario.scenegraph.SGLeaf;

public class SGCanvas extends SGLeaf
{
    private boolean smooth;

    private Color fill;

    private int height, width;

    private Sequence<? extends String> content;

    private Tokenizer tok = new Tokenizer ("");

    private Stack<AffineTransform> stack = new Stack<AffineTransform> ();

    private int ox, oy;

    @Override
    public final Rectangle2D getBounds (AffineTransform transform)
    {
        System.out.println ("getBounds");

        float x = 0;
        float y = 0;
        float w = width;
        float h = height;

        if (transform != null && !transform.isIdentity ())
        {
            if (transform.getShearX () == 0 && transform.getShearY () == 0)
            {
                // No rotations...

                if (transform.getScaleX () == 1 && transform.getScaleY () == 1)
                {
                    // just a translation...

                    x += transform.getTranslateX ();
                    y += transform.getTranslateY ();
                }
                else
                {
                    float coords [] = { x, y, x+w, y+h };
                    transform.transform (coords, 0, coords, 0, 2);
                    x = Math.min (coords [0], coords [2]);
                    y = Math.min (coords [1], coords [3]);
                    w = Math.max (coords [0], coords [2])-x;
                    h = Math.max (coords [1], coords [3])-y;
                }
            }
            else
            {
                float coords [] = { x, y, x+w, y, x, y+h, x+w, y+h };
                transform.transform (coords, 0, coords, 0, 4);
                x = w = coords [0];
                y = h = coords [1];
                for (int i = 2; i < coords.length; i += 2)
                {
                    if (x > coords [i]) x = coords [i];
                    if (w < coords [i]) w = coords [i];
                    if (y > coords [i+1]) y = coords [i+1];
                    if (h < coords [i+1]) h = coords [i+1];
                }
                w -= x;
                h -= y;
            }
        }

        return new Rectangle2D.Float (0, 0, width, height);
    }

    @Override
    public void paint (Graphics2D g)
    {
        if (smooth)
            g.setRenderingHint (RenderingHints.KEY_ANTIALIASING,
                                RenderingHints.VALUE_ANTIALIAS_ON);

        g.setColor (fill);
        g.fillRect (0, 0, width, height);

        ox = oy = 0;
        parseContent (content, 0, g);
    }

    private void parseContent (Sequence<? extends String> content, int index,
                               Graphics2D g)
    {
        tok.reset (content.get (index));

        while (tok.type != Tokenizer.TOK_EOS)
        {
           if (tok.type != Tokenizer.TOK_LETTER)
           {
               System.out.println ("instruction letter expected");
               return;
           }

           if (tok.letter == 'C')
           {
               tok.nextToken ();
               if (tok.type != Tokenizer.TOK_NUMBER)
               {
                   System.out.println ("number expected");
                   return;
               }
               if (tok.number > 255)
               {
                   System.out.println ("red component > 255: "+tok.number);
                   return;
               }
               int red = tok.number;

               tok.nextToken ();
               if (tok.type != Tokenizer.TOK_NUMBER)
               {
                   System.out.println ("number expected");
                   return;
               }
               if (tok.number > 255)
               {
                   System.out.println ("green component > 255: "+tok.number);
                   return;
               }
               int grn = tok.number;

               tok.nextToken ();
               if (tok.type != Tokenizer.TOK_NUMBER)
               {
                   System.out.println ("number expected");
                   return;
               }
               if (tok.number > 255)
               {
                   System.out.println ("blue component > 255: "+tok.number);
                   return;
               }
               int blu = tok.number;

               g.setColor (new Color (red, grn, blu));

               tok.nextToken ();
               continue;
           }

           if (tok.letter == 'L')
           {
               tok.nextToken ();
               if (tok.type != Tokenizer.TOK_NUMBER)
               {
                   System.out.println ("number expected");
                   return;
               }
               int x = tok.number;

               tok.nextToken ();
               if (tok.type != Tokenizer.TOK_NUMBER)
               {
                   System.out.println ("number expected");
                   return;
               }
               int y = tok.number;

               g.drawLine (ox, oy, x, y);
               ox = x;
               oy = y;

               tok.nextToken ();
               continue;
           }

           if (tok.letter == 'M')
           {
               tok.nextToken ();
               if (tok.type != Tokenizer.TOK_NUMBER)
               {
                   System.out.println ("number expected");
                   return;
               }
               int x = tok.number;

               tok.nextToken ();
               if (tok.type != Tokenizer.TOK_NUMBER)
               {
                   System.out.println ("number expected");
                   return;
               }
               int y = tok.number;

               ox = x;
               oy = y;

               tok.nextToken ();
               continue;
           }

           if (tok.letter == 'P')
           {
               g.setTransform (stack.pop ());

               tok.nextToken ();
               continue;
           }

           if (tok.letter == 'R')
           {
               tok.nextToken ();
               if (tok.type != Tokenizer.TOK_NUMBER)
               {
                   System.out.println ("number expected");
                   return;
               }
               int degrees = tok.number;

               g.rotate (degrees);

               tok.nextToken ();
               continue;
           }

           if (tok.letter == 'S')
           {
               tok.nextToken ();
               if (tok.type != Tokenizer.TOK_NUMBER)
               {
                   System.out.println ("number expected");
                   return;
               }
               int sx = tok.number;

               tok.nextToken ();
               if (tok.type != Tokenizer.TOK_NUMBER)
               {
                   System.out.println ("number expected");
                   return;
               }
               int sy = tok.number;

               g.scale (sx, sy);

               tok.nextToken ();
               continue;
           }

           if (tok.letter == 'T')
           {
               tok.nextToken ();
               if (tok.type != Tokenizer.TOK_NUMBER)
               {
                   System.out.println ("number expected");
                   return;
               }
               int tx = tok.number;

               tok.nextToken ();
               if (tok.type != Tokenizer.TOK_NUMBER)
               {
                   System.out.println ("number expected");
                   return;
               }
               int ty = tok.number;

               g.translate (tx, ty);

               tok.nextToken ();
               continue;
           }

           if (tok.letter == 'X')
           {
               stack.push (g.getTransform ());

               tok.nextToken ();
               continue;
           }

           if (tok.letter == 's')
           {
               tok.nextToken ();
               if (tok.type != Tokenizer.TOK_NUMBER)
               {
                   System.out.println ("number expected");
                   return;
               }
               index = tok.number;
               if (index < 1 || index >= content.size ())
               {
                   System.out.println ("index out of range");
                   return;
               }

               Color c = g.getColor ();
               tok.push ();
               parseContent (content, index, g);
               tok.pop ();
               g.setColor (c);

               tok.nextToken ();
               continue;
           }

           System.out.println ("invalid instruction letter");
           return;
        }
    }

    public final void setContent (Sequence<? extends String> content)
    {
        System.out.println ("setContent: "+content.size ());
        this.content = content;
        repaint (false);
    }

    public final void setFill (Color fill)
    {
        System.out.println ("setFill");
        this.fill = fill;
        repaint (false);
    }

    public final void setHeight (int height)
    {
        System.out.println ("setHeight");
        this.height = height;
        repaint (true);
    }

    public final void setSmooth (boolean smooth)
    {
        System.out.println ("setSmooth");
        this.smooth = smooth;
        repaint (false);
    }

    public final void setWidth (int width)
    {
        System.out.println ("setWidth");
        this.width = width;
        repaint (true);
    }

    class Tokenizer
    {
        final static int TOK_LETTER = 0;
        final static int TOK_NUMBER = 1;
        final static int TOK_CHAR = 2;
        final static int TOK_EOS = 3;

        int type;
        char letter;
        int number;

        private String s;
        private int len, pos;

        private Stack<State> stack = new Stack<State> ();

        Tokenizer (String s)
        {
            reset (s);
        }

        void nextToken ()
        {
            while (pos < len && s.charAt (pos) == ' ')
                pos++;

            if (pos == len)
            {
                type = TOK_EOS;
                return;
            }

            if (s.charAt (pos) >= 'A' && s.charAt (pos) <= 'Z' ||
                s.charAt (pos) >= 'a' && s.charAt (pos) <= 'z')
            {
                letter = s.charAt (pos++);
                type = TOK_LETTER;
                return;
            }

            if (s.charAt (pos) == '-' || s.charAt (pos) >= '0' &&
                s.charAt (pos) <= '9')
            {
                int sign = 1;

                if (s.charAt (pos) == '-')
                {
                    sign = -1;
                    if (++pos == len || s.charAt (pos) < '0' ||
                        s.charAt (pos) > '9')
                    {
                        letter = '-';
                        type = TOK_CHAR;
                        return;
                    }
                }

                number = 0;
                do
                {
                   number *= 10;
                   number += (s.charAt (pos++)-'0');
                }
                while (pos < len && s.charAt (pos) >= '0' &&
                       s.charAt (pos) <= '9');
                number *= sign;
                type = TOK_NUMBER;
                return;
            }

            letter = s.charAt (pos++);
            type = TOK_CHAR;
        }

        void pop ()
        {
            State state = stack.pop ();
            s = state.s;
            len = state.len;
            pos = state.pos;
            type = state.type;
            number = state.number;
            letter = state.letter;
        }

        void push ()
        {
            State state = new State ();
            state.s = s;
            state.len = len;
            state.pos = pos;
            state.type = type;
            state.number = number;
            state.letter = letter;
            stack.push (state);
        }

        void reset (String s)
        {
            this.s = s;
            len = s.length ();
            pos = 0;
            nextToken ();
        }
    }

    class State
    {
        String s;
        int len, pos, type, number;
        char letter;
    }
}

This version of SGCanvas supports antialiased lines by providing a write-only Boolean property named smooth, and a setter method named setSmooth(). Furthermore, the paint() method executes g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) whenever smooth is set to true.

I've introduced a new Tokenizer inner class as an alternative to StringTokenizer. This class is instantiated only once, permits zero or more space characters between letters and numbers, and offers push/pop capabilities that are needed to support an expanded language where symbols (such as resistors, diodes, and other electronic component symbols) can be defined and accessed multiple times.

This language expansion is also supported by modifying the content property so that it can store a sequence of Strings. The first string contains instructions for rendering a drawing, and additional strings contain instructions for rendering various symbols. This modification takes advantage of JavaFX's com.sun.javafx.runtime.sequence.Sequence class.

The following instructions have been added to the language:

  • P: Pop the most recently pushed transformation matrix from the stack and make it current. Capital letter P is specified by itself.
  • R deg: Append a rotation to the current transformation matrix. Capital letter R is followed by an integer value that specifies the number of degrees to rotate.
  • S sx sy: Append a scaling transformation to the current transformation matrix. Capital letter S is followed by two integer values that specify the amount of scaling horizontally followed by the amount of scaling vertically. For example, S 1 1 means no scaling, and S 2 2 means multiply every coordinate by 2 to double the size of the drawing.
  • T tx ty: Append a translation to the current transformation matrix. Capital letter T is followed by two integer values that specify the amount of translation horizontally followed by the amount of translation vertically.
  • X: Push the current transformation matrix onto the stack. Capital letter X is specified by itself.
  • s index: Execute a symbol's instructions. Lowercase letter s is followed by a one-based integer value that indexes into content. This index value must range from 1 through one less than the number of entries in the content sequence.

Listing 5 presents the updated Canvas class's source code. Apart from a new smooth attribute that provides access to SGCanvas's smooth property, this Canvas version is identical to the original version.

Listing 5: Canvas.fx (version 2)

/*
* Canvas.fx
*/

package canvasdemo2;

import javafx.scene.Node;

import javafx.scene.paint.Color;

import com.sun.scenario.scenegraph.SGNode;

public class Canvas extends Node
{
   override function impl_createSGNode (): SGNode
   {
       new SGCanvas ()
   }

   function getSGCanvas (): SGCanvas
   {
       impl_getSGNode () as SGCanvas
   }

   public var content: String[] on replace
   {
       getSGCanvas ().setContent (content)
   }

   public var fill: Color = Color.WHITE on replace
   {
       getSGCanvas ().setFill (new java.awt.Color (fill.red, fill.green,
                                                   fill.blue))
   }

   public var height = 0 on replace
   {
       getSGCanvas ().setHeight (height)
   }

   public var smooth: Boolean on replace
   {
       getSGCanvas ().setSmooth (smooth)
   }

   public var width = 0 on replace
   {
       getSGCanvas ().setWidth (width)
   }
}

I've created a new script that demonstrates the updated canvas node. Listing 6 presents its source code.

Listing 6: Main.fx (version 2)

/*
* Main.fx
*/

package canvasdemo2;

import javafx.scene.Scene;

import javafx.scene.paint.Color;

import javafx.stage.Stage;

Stage
{
    title: "Canvas Demo #2"
    width: 300
    height: 300

    var sceneRef: Scene
    scene: sceneRef = Scene
    {
        var canvasRef: Canvas
        content:
        [
            Canvas
            {
                content: "C255 0 0M10 10L10 90L90 90L90 10L10 10"
                         "C0 255 0L90 90C0 0 255M90 10L10 90"
                fill: Color.BLACK
                width: 100
                height: 100
                smooth: true
            }

            canvasRef = Canvas
            {
                content:
                [
                    "C255 255 255 S 2 2 T -14 -5 M 19 20 L 25 20 X T 25 18 s1 P"
                    "M 37 20 L 42 20 X T 42 18 s1 P M 54 20 L 59 20"
                    "L 59 35 L 42 35 X T 35 32 s2 P M 35 35 L 19 35 L 19 20",

                    "M 0 2 L 2 0 L 4 2 L 6 0 L 8 2 L 10 0 L 12 2",

                    "M0 0L0 6M2 2L2 5M4 0L4 6M6 2 L6 4 M-2 -4 L2 -4 M0 -6 L0 -2"
                ]
                fill: Color.BLACK
                width: 100
                height: 100
                smooth: true
                translateX: bind (sceneRef.width-
                                  canvasRef.boundsInParent.width)/2.0
                translateY: bind (sceneRef.height-
                                  canvasRef.boundsInParent.height)/2.0
                rotate: 30
                scaleX: 1.5
                scaleY: 1.5
            }
        ]
    }
}

Listing 6 is similar to Listing 3 except for a new smooth attribute in both Canvas literals, and a new content sequence in the second literal. This sequence contains three entries, with the first entry specifying instructions for an overall drawing, and the second and third entries each specifying instructions for rendering a symbol.

Each of the second and third entries define a symbol in its own coordinate system, with (0, 0) as the upper-left corner. The first entry uses the translation instruction to position two instances of the first symbol and one instance of the second symbol into the drawing. Ultimately, as shown in Figure 2, a simplified electronic circuit schematic diagram of two resistors connected in series to a battery is revealed.

Figure 2 Untransformed and transformed versions of the improved canvas reveal antialiased versions of the previous script's content and an electronic circuit schematic diagram. (Click to enlarge.)

Conclusion

Although the canvas node alternative to CustomNode is helpful for reducing JavaFX memory overhead, it could be improved. For example, this node would benefit from dynamically updating part of a drawing whenever mouse activity occurs over that part. Also, its language would benefit from floating-point values instead of integers (especially for scaling). What other improvements would your recommend?

Download a source file: csj40709-src.zip

Like this blog? Subscribe to the CSJ Explorer RSS feed

Sheer Elegance!

This is something useful for graphic intensive JavaFX applications, a reusable rubberstamp enabled node that is created only once. It has a state that is easily reset via a simple instruction string before being re-rendered, very cool!

It would be great to see support for rendering font characters added to this node, maybe even support for rendering SVG strings. Event handlers and a built-in mechanism for tracking and hitting specific rendered regions would also be great.

Thanks Jeff, for the innovation and making my day,

Thom

RE: Sheer Elegance!

Hi Thom,

Thanks for your kind words. The canvas node's language could be expanded to deal with SVG and other features, and perhaps this will happen.

All the best.

Jeff Friesen

Thanks!

Thanks!

This is using private APIs that will break

Please be aware that this technique is using private APIs that *will* break in the next release of JavaFX. The JSGPanel and other com.sun.* APIs are implementation details that can change at any time in the name of performance and memory footprint reduction. You should only use the javafx.* APIs.

Is there a reason why you couldn't do what you wanted with the existing APIs? It seems like the Path related APIs would let you do what you want.

RE: This is using private APIs that will break

Hi Josh,

I mentioned the possibility (which appears to be definite, based on your response) that the private APIs will break in my post's "Undocumented danger!" note.

Path and associated node-less classes like LineTo and MoveTo seem like the better way to go. However, one needs to instantiate these classes each time one wants to move to a drawing position, draw a line from this position to a new position, draw an arc, and so on. Depending on the complexity of the drawing, it seems like a lot of objects could be created, increasing the script's runtime memory footprint. In contrast, it seems to me that it's more compact to specify L 10 10 (to draw a line to position 10, 10) as part of a String rather than instantiate a LineTo object, but perhaps not as readable.

Another concern with Path involves symbols. For example, consider the post's electronics circuit example. The example introduces two (crudely drawn) symbols for a resistor and a battery. It then renders two occurrences of the resistor symbol and one occurrence of the battery symbol, applying translations to position these symbols appropriately.

Although one could define the resistor symbol via one Path and the battery symbol via a second Path, would it not be more verbose (from an object-creation perspective) to create the same electronic circuit via Path? Also, would it be as easy to specify the circuit using Paths?

My canvas node is an experiment in reducing memory overhead while achieving complex drawings that require symbols. It's far from perfect (the language could be expanded and the implementation improved). Although the current dependence on the JavaFX runtime will break for future JavaFX releases, perhaps it will be possible to release better versions that work under JavaFX 1.5, 2.0, and so on when they're released. However, that will depend on whether or not the canvas node has any merit for JavaFX developers.

All the best.

Jeff Friesen

Hi jmarinacci, I totally

Hi jmarinacci, I totally agree with you that this technique is using private APIs and I think it seems like the Path related APIs would let you do what you want too.
Thanks,
Alan

The existing CustomNode is

The existing CustomNode is good for many tasks however, for graphics involving many details it can be a real memory hog. It is also not suited for some types of immediate rendering tasks.

Its really useful Post. I

Its really useful Post. I will go through it. Thank you for the nice Post…

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

Post new comment

  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <p> <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd> <br /> <br> <strike>
  • Lines and paragraphs break automatically.
  • Use <!--pagebreak--> to create page breaks.
  • You may post code using <code>...</code> (generic) or <?php ... ?> (highlighted PHP) tags.

More information about formatting options

CAPTCHA
Just checking to see if you're an actual person rather than a spammer. Sorry for the inconvenience.