If you’ve ever tried drawing anything complicated with shapes on a Pane, you know first hand the lag you can get if you overload a scene. The Canvas object brings a potential answer to that, allowing you to draw without the same overhead. It achieves this by trading to some extent the convenience of having modifiable shapes for the performance you get by dropping those references.

The JavaFX canvas is a node that facilitates drawing commands that are lower level than are otherwise available in JavaFX. It allows users to pass drawing commands for line, text, shape and image objects directly to a rendering buffer. This data is processed and flattened directly into pixel data, enhancing application performance.

That processing and flattening means they can’t be transformed or referenced as we would a node in a Pane. Or rather, they can – but you need to maintain that knowledge yourself and re-draw the correct objects once they’ve changed (after all, this is what JavaFX is doing in the background with your Shape nodes).

The benefit the Canvas brings it that as a developer, you get to decide which references are worth keeping, and which parts of the Canvas need to be redrawn at any one time.

What will you get from this tutorial?

Because the Canvas is so different to normal node-centric UI control, I’ll take you through the basics of how to create the canvas, resize it, draw on it, and style the things you draw.

Issuing draw calls to a canvas instead of building a scene graph has its benefits, but it also comes with challenges. Let’s jump in.

Creating and using a Canvas

A Canvas can be created with no parameters, but it will by default have zero width and height, which is probably not what you want. Alternatively, it can be created at a set size by passing the width and height as constructor parameters.

Canvas canvas = new Canvas();
Canvas sizedCanvas = new Canvas(600, 400);

The canvas object itself is responsible for only three things:

  • The size of the canvas (height and width)
  • Holding a reference to the graphics context (for drawing)
  • Some high-level performance monitoring

It’s the GraphicsContext object that does the heavy lifting. Check out the drop-down if you want a bit more information on what the graphics context is and does.

Resizing a Canvas

Resizing a canvas is pretty easy. The canvas width and height can be set absolutely, or by using property binding using the heightProperty and widthProperty attributes of the canvas.

Manually Resizing

Manually resizing the canvas is as easy as setting the width and height attributes by invoking setWidth() and setHeight() respectively.

canvas.setWidth(250);
canvas.setHeight(150);

Warning: I don’t think the design intention behind the Canvas was for repeated resizing. That means when you resize the canvas, you won’t resize (e.g. scale) anything you’ve already created.

Remember the canvas is transparent. It’s blue here so you can see what’s going on, but resizing might not be obvious depending on your scene.

This is also true if you resize the canvas by property binding.

Automated resizing through binding

If you have a compelling use case for dynamically resizing the canvas, it can be done with property binding. In this case, I’d suggest creating a specialised Region object that manages the Canvas’s width and height as it is itself resized.

The benefit of using a Region and not a Pane to wrap the canvas object is that you get the benefits of managing the size of a canvas, but you send an obvious message to classes using this object that it’s not to be used for laying out other nodes such as controls or charts.

public class ResizeableCanvas extends Region {
    private Canvas canvas;

    ResizeableCanvas(double width, double height) {
        //set the width and height of this and the canvas as the same
        setWidth(width);
        setHeight(height);
        canvas = new Canvas(width, height);

        //add the canvas as a child
        getChildren().add(canvas);

        //bind the canvas width and height to the region
        canvas.widthProperty().bind(this.widthProperty());
        canvas.heightProperty().bind(this.heightProperty());
    }
    
    public GraphicsContext getGraphicsContext2D() {
        return canvas.getGraphicsContext2D();
    }
}

Because the canvas is responsible for so few attributes (size and graphics context accessors), you need only include a single accessor method for the canvas’s graphics context. The region itself has methods for width and height, so it will behave exactly like a canvas, except it will be resizable.

You could also bind listeners to the width and height to refresh the canvas according to its size. This is really useful if you want the canvas to have a background color that stretches all the way across the canvas.

Setting the background color of a canvas

The canvas is by default a transparent block of pixels that you can draw on. There are some obvious use cases for creating a background – be it an image background, or a block color. I’ll do a block color here, but skip to here for drawing images.

Setting a background color on a canvas isn’t what it’s designed for, but that doesn’t mean it’s impossible (or even difficult). There are two ways to make this happen:

  • Issue draw commands manually
  • Wrap the canvas in a Region and set the background of the wrapper instead

They’re both useful for different circumstances. If you’re running a game loop, you might want to draw the background every frame, because will frequently change. In cases like that, I’d always go for manual drawing.

If you’re drawing a static object like a graph, you may just want to have the background set once – and styleable (regions are styleable, while canvases aren’t).

Issuing draw commands manually

The simplest way to set the background color on a canvas is to draw a rectangle of the same size as the canvas specifying the color you want.

The command setFill() is used to define the fill color, and fillRect() is used to draw an opaque rectangle of the defined size. There’s more information on drawing operations below, but that’s all we need for now.

In this case, drawing a rectangle that starts at coordinates (0, 0) and extends the width and height of the canvas will completely overwrite any existing content and replace it.

Here, we’ll set the fill as yellow (the default is black), so our rectangle will overwrite the canvas with yellow pixels.

Canvas canvas = new Canvas(400, 200);
GraphicsContext context = canvas.getGraphicsContext2D();

context.setFill(Color.YELLOW);
context.fillRect(
        0, 
        0, 
        canvas.getWidth(), 
        canvas.getHeight());

By linking the size of the rectangle directly with the width and height of the canvas, you can ensure it’s completely covered.

Set the background of a Canvas wrapper

In the section above on resizing a canvas, we wrapped the canvas in a Region object, which helped with the sizing behaviours. This can also be useful in setting a background, because while the canvas may be made of transparent pixels, the region doesn’t have to be.

To set the background of a region, we set a Background object, for which we define a BackgroundFill.

ResizeableCanvas canvas = new ResizeableCanvas(200, 200);
canvas.setBackground(
        new Background(
                new BackgroundFill(
                        Color.YELLOW,
                        CornerRadii.EMPTY,
                        Insets.EMPTY
                )
        )
);

The Background class is a little clunky if you’re only interested in color, so I’d suggest a utility method setBackgroundColor(Color color). Let it do the heavy lifting with the Background and BackgroundFill objects, so the user class just has to worry about setting the color.

public void setBackgroundColor(Color color){
    this.setBackground(
            new Background(
                    new BackgroundFill(
                            color,
                            CornerRadii.EMPTY,
                            Insets.EMPTY
                    )
            )
    );
}

Alternatively, we can set this same property using CSS in Java code, or we could add a style class to the region, and define the style in a separate file.

canvas.setStyle("-fx-background-color: red"); //set manually

canvas.getStylesheets().add("ourStylesheet.css");
canvas.getStyleClass().add("this-style"); //add style class (set color in CSS file)

Just a reminder that these will only work if you’re wrapping the canvas in a region. Cavasses extend Node directly and don’t have a background property that you can style either manually or by CSS.

Clearing a canvas

The process for clearing a canvas is exactly the same as for setting the background, except here we want to set pixels back to transparent rather than setting them as a single color.

The canvas object includes a function clearRect(), which will clear the specified portion of a canvas. Again, by starting the rectangle at coordinates (0, 0) and linking the rectangle size with the size of the canvas, we can clear the canvas in a single draw call.

Canvas canvas = new Canvas(400, 200);
GraphicsContext context = canvas.getGraphicsContext2D();
context.clearRect(
        0,
        0,
        canvas.getWidth(),
        canvas.getHeight());

How to draw on a canvas

The Canvas object itself isn’t responsible for drawing – it’s just responsible for maintaining its own bounds. Instead, every style instruction and draw call we make will involve invoking methods on a GraphicsContext object that the canvas maintains a reference to.

You can get to it by invoking getGraphicsContext2D() on the canvas, which returns the graphics context specific to that canvas.

GraphicsContext context = canvas.getGraphicsContext2D();

One of the most fundamental aspects of canvas drawing is that styling and drawing are separate things. You cannot set the style of a line or rectangle as you would with a Shape in a Pane.

I’ve seen so many tutorials that present drawing lines as a mixed-bag of styling and drawing. If you take one thing from this page take this: When you set a style on a canvas, it applies to every object drawn from that point. For that reason, I’ll take you through how to draw shapes first and then how to control the fill colour and line properties separately.

Styles: Invoking a method like setLineWidth​(double lw) or setFill(Paint color) alters the drawing instructions that the rendering layer of JavaFX (Prism) will use to interpret your drawing instructions.

Drawing: Then, every time you draw a line or fill a shape, the rendering layer processes the drawing with the style instruction present at the time.

The graphics context itself is a brilliantly powerful tool for drawing complex vector graphics. If you’re interested in how the graphics context works, there’s a full description later on, but in this section, I’ll focus on drawing and styling.

Drawing lines, shapes, images and text

Drawing commands are separated into fill and stroke operations. There are 6 methods to create a filled shape, and 8 methods to draw lines and shape borders, known as strokes.

  • strokeRect()
  • strokeRoundRect()
  • strokeOval()
  • strokeArc()
  • strokePolygon()
  • stroke()
  • strokeLine()
  • strokePolyline()
  • fillRect()
  • fillRoundRect()
  • fillOval()
  • fillArc()
  • fillPolygon()
  • fill()

In addition to that, the graphics context also supports drawing images, and text.

  • strokeText()
  • fillText()
  • drawImage()

I’ll go through each type of shape first, then lines, and finally paths, covering both fill and stroke operations at the same time. Then, I’ll cover drawing images and text.

Shapes

The graphics context supports drawing rectangles (with and without rounded corners), ovals and arcs.

In each of these cases, the transforms that are already present in the graphics context (rotation, translation and scale) will be applied by default to every shape draw call. If you want to change these, check out the section below on styling the canvas.

Rectangles:

Options: fillRect() and strokeRect()
Parameters: x origin, y origin, width and height (all double)
Output: A filled or empty rectangle, with the top left corner positioned at the defined origin and of the specified width and height.

fillRect(double x, double y, double w, double h);
strokeRect(double x, double y, double w, double h);
Rectangles with rounded corners

Options: fillRoundRect() and strokeRoundRect()
Parameters: x origin, y origin, width, height, arc width and arc height (all double)
Output: A filled or empty rectangle, with the top left corner positioned at the defined origin and of the specified width and height. The corner rounding is defined by the arc width and arc height parameters.

fillRoundRect(double x, double y, double w, double h, double arcWidth, double arcHeight);
strokeRoundRect(double x, double y, double w, double h, double arcWidth, double arcHeight);
Canvas fillRoundRect() and strokeRoundRect() commands
Ovals

Options: fillOval() and strokeOval()
Parameters: x origin, y origin, width and height (all double)
Output: A filled or empty oval of the specified width and height. Unlike when creating a circle node, the origin of the oval is positioned at the top left corner of its bounding rectangle.

fillOval(double x, double y, double w, double h);
strokeOval(double x, double y, double w, double h);

Note: the pen position doesn’t move to the center of the oval, as it would when you create a circle shape on a Pane.

Arcs

Options: fillArc() and strokeArc()
Parameters: x origin, y origin, width, height, startAngle and arcExtent (all double), and closure, defined as an ArcType enum.
Output: A filled or empty arc of the specified width and height. The amount of an arc drawn is defined by the startAngle and arcExtend. The drawing rules for filling and stroking and defined by the ArcType enum closure.

fillRect(double x, double y, double w, double h, double startAngle, double arcExtent, ArcType closure);
strokeRect(double x, double y, double w, double h, double startAngle, double arcExtent, ArcType closure);
Canvas fillArc() and strokeArc() commands

In addition to these basic shapes, the graphics context supports drawing and filling complex polygonal shapes by defining the points as arrays of values manually.

Polygons

Options: fillPolygon() and strokePolygon()
Parameters: x-points and y-points as double arrays, specifying the number of points a s a third integer parameter, points.
Output: A filled or empty polygon drawn between the specified points.

fillPolygon(double[] xPoints, double[] yPoints, int nPoints);
strokePolygon(double[] xPoints, double[] yPoints, int nPoints);
Additional array values beyond nPoints are ignored. Arrays shorter than nPoints will throw exceptions.

Lines

The graphics context supports drawing individual straight lines by invoking strokeLine(), or multiple continuous straight lines by invoking strokePolyLine(). More complex lines can be drawn by using the graphics context’s path drawing functions, which I’ll cover separately below.

Straight Line

Options: strokeLine()
Parameters: x and y coordinates of line start, and line end (all double).
Output: A single straight line

strokeLine(double x1, double y1, double x2, double y2);
Multiple connected straight lines

Options: strokePolyLine()
Parameters: x-points and y-points as double arrays, specifying the number of points a s a third integer parameter, points.
Output: A series of straight lines, starting at the first x-y coordinates specified, and progressing through each set of coordinates until the number of points specified in the nPoints argument is reached.

strokePolyline(double[] xPoints, double[] yPoints, int nPoints);
Canvas fillPolyline() and strokePolyLine() commands
Complex Lines – Paths

A Path is a series of linear and non-linear line-segments that can be continuous, or interspersed with gaps. Each Path is initiated by invoking beginPath() on the graphics context of the canvas, and terminated by calling closePath().

The JavaFX Canvas supports 8 path drawing types: moves, lines, quadratic curves, bezier curves, arcs (two types), rectangles, and SVG paths.

A few warnings and house-keeping before we dive in:

Note on drawing: nothing will be drawn until you invoke gc.stroke() or gc.fill(), which will stroke or fill the path respectively. This also has the effect of closing the path.

Note on transforms: unlike when drawing a shape or simple line, invoking any of the below methods to define a path doesn’t result in drawing. JavaFX applies the transforms present at the time when you invoke the method to the path element you’re adding.

Once you’re finished with your path, you can draw it using the fill() and stroke() methods, which will produce filled and empty paths respectively. No transforms are applied during drawing, so if you’re going to use transforms, you should define them as you create the path.

moveTo()

Parameters: x and y coordinates (double)
Path element added: Move from the current coordinates to the given x and y coordinates without drawing a line.

moveTo(double x, double y)
Canvas MoveTo command
No line’s drawn here. This is useful for moving around the canvas with the ‘pen off the paper’
lineTo()

Parameters: x and y coordinates (double).
Path element added: Move from the current coordinates to the given x and y coordinates, drawing a straight line between the points.

lineTo(double x, double y)
Canvas LineTo command
quadraticCurveTo()

Parameters: the x and y coordinates of the control point, and the x and y coordintes of the destination
Path element added: Move from the current coordinates to the given x and y coordinates, drawing a quadratic Bezier curve defined by the coordinates of the control point provided.

quadraticCurveTo(double xc, double yc, double x1, double y1)
Canvas quadraticCurveTo command

bezierCurveTo()

This provides more control than the quadraticCurveTo() command, but will produce identical results if the two control points provided are the same.

Parameters: two sets of control points as doubles, and the destinatoin coordinates as doubles.
Path element added: Move from the current coordinates to the given x and y coordinates, drawing a cubic Bezier curve defined by the coordinates of the two control points provided.

bezierCurveTo(double xc1, double yc1, double xc2, double yc2, double x1, double y1)
Canvas bezierCurveTo command

arc()

Hint: arc() is pretty good for drawing arcs and circles, as opposed to arcTo(), which is better suited for drawing rounded corners.

Parameters: The coordinates of the center of the arc (x and y), the x and y radius of the arc, the start angle, and the length of the arc.
Path element added: Draw a line from the current pen position to the start of the arc (this is not the same as centre). Then, draw an arc counterclockwise starting at the start angle (see below – it’s not the top of the circle!) for the length specified.

arc(double centerX, double centerY, double radiusX, double radiusY, double startAngle, double length)
Canvas arc() command
Only the black line is drawn. If the arc() command extends an existing path (instead of starting it), a line is also drawn from the previous pen position to the start of the arc

arcTo()

Hint: arcTo() is well suited for drawing rounded corners. If you just want arcs and circles, I’d recommend using the arc() command.

Parameters: two control points (x1, y1 and x2, y2) and a radius
Path element added: Draw a line from the current position to the first control point (x1, y1), then draw an arc of the defined radius (see diagram… trust me)

arcTo(double x1, double y1, double x2, double y2, double radius)
Canvas arcTo command

Definitely worth remembering that the line to the arc also gets drawn. This makes the arcTo() method particularly good for drawing rounded corners of a particular radius in complex shapes.

Example of rounded corners achieved with arcTo() and lineTo() commands on a canvas

appendSVGPath()

Parameters: The SVG path element, defined as a string.
Path element added: The SVG path element itself defined a series of move and stroke operations that are translated into path elements and added to the current path.

appendSVGPath(String svgpath)

The appendSVGPath() method is useful for complex drawing operations you don’t want to define manually each time. I got this SVG from flaticon.com (as always), and it was made by Nikita Golubev.


rect()

Parameters: the x and y coordinates of the upper left of the rectangle, and the width and height
Path element added: Move to the x and y coordinates defined, and create a rectangle of the defined width and height. The position of the ‘pen’ for the next path element will be the origin of the rectangle.

rect(double x, double y, double w, double h)

Images

Images are a relatively simple part of the canvas API (at least compared with paths!). Drawing images can be accomplished by invoking the drawImage() methods, which can be parameterised in three ways.

Simple image drawing

Calling drawImage() on a graphics context with an image, x, and y value will faithfully draw the entire image positioning the upper-left corner of the image at the given (x, y) coordinates.

drawImage​(Image img, double x, double y);
Drawing images with widths and heights

Calling drawImage() with the additional w and h width and height arguments will still render the image wiht the upper-left corner at the (x, y) coordiante, but it will also scale the image to the given w x h dimensions.

drawImage​(Image img, double x, double y, double w, double h);

The graphics context will apply scaling filters to improve the quality of the image if image smoothing has been enabled (setImageSmoothing(true)) but it will not preserve any aspect ratio. When drawing on a canvas, the aspect ratio is your responsibility!

Sub-image rendering with Canvas

Finally, it’s possible to render sampled areas of a image by specifying the coordinates of the source rectangle in addition to the coordinates usually specified.

drawImage​(Image img, double sx, double sy, double sw, double sh, double dx, double dy, double dw, double dh);

In this case, the arguments dx, dy, dw and dh specify the position and size of the rectangle on the canvas, just like before. However, the additional parameters sx, sy, sw and sh specify the location and size of the sampled rectangle from the provided image to be rendered.

This is really useful if you want to implement frame-based animations like sprite animation.

Here’s a really simple example implemented just by moving the origin of the source square each frame. You could obviously add to this by changing the destination rectangle to mimic movement properly.

Text

As with images, the text API is a satisfying break from the complexity of path rendering. As with other shapes, the text outline (stroke) and fill are done separately. Specifying fill and stroke separately gives you flexibility, at expense of potential duplication.

Parameters: the text as a String, the x and y coordinates of the upper left corner of the text bounds, and, optionally, the maximum width of the text.

fillText​(String text, double x, double y);
fillText​(String text, double x, double y, double maxWidth);
strokeText​(String text, double x, double y);
strokeText​(String text, double x, double y, double maxWidth);
Apart from a wildly opportunistic plug for my own website, this is a good demonstration of the ability to resize text by adding a maximum width. This can be done to either fill or stroke calls.

Styling lines and shapes

Separate to directing draw commands, you can also style what you’re drawing. You can do this by setting the colour of fills and strokes, as well as style of strokes themselves.

Color

Specifying a color to the graphics context is actually fairly simple (I mean, if you made it through the ‘path’ section everything seems simple). Fill colors are specified by invoking setFill() and line colors are specified by invoking setStroke() on the graphics context with the correct Paint object (like a Color) as a parameter.

setFill(Paint p);
setStroke(Paint p);

Just remember that the color you set applies to every drawing operation from that point forwards, not just the next one. That’s caught me out a few times..

Line Styles

Line styles are a little more complicated. You can customise the width, the cap (the end of the line) and the way two lines are joined.

If you need to, you can specify a dash pattern, with both dash lengths and the offset.

Line Width

Parameters: the width of the line as a double

setLineWidth(double lw);
These are all lines, not rectangles, just of different stroke widths. This can be useful for reducing the complexity of your draw calls.
Line Cap

Parameters: the cap that should be rendered at the ends of the lines subsequently drawn as a StrokeLineCap enum.
Options: the StrokeLineCap enum facilitates butt, round and square line caps.

setLineCap(StrokeLineCap cap)
Definitely worth remembering here that with Round and Square line caps, the line will extend slightly beyond the actual coordinates set.
Line Join

Parameters: the join that should be rendered at the join of any two lines subsequently drawn as a StrokeLineJoin enum.
Options: the StrokeLineJoin enum facilitates round, miter and bevel line caps.

setLineJoin(StrokeLineJoin join);

A note on miter limit: if your line join is set to StrokeLineJoin.MITER, you can also specify a miter limit value, which will convert any miter join into a bevel join under certain circumstances. You can do this by invoking setMiterLimit().

setMiterLimit(double ml);
The join distance is how far the mitre would extend (assuming it isn’t truncated).

Rotating shapes on a canvas

Graphics context maintains a state, which describes a set of clip, transform and drawing-specific properties. Modifying the rotate part of the transform can be achieved by invoking rotate(), just as with other transformations like scale and translation.

rotate​(double degrees);

The clip, transform and drawing properties are applied individually to every draw call (such as strokeLine()), so once this state is modified, it will be applied in future operations.

Warning: This is applied to ALL future draw calls, not just the next one!

Obviously, most of the time you’ll want to rotate one shape, and then carry on placing objects that aren’t rotated. There are two ways to rotate an object and then put the rotation back to normal.

1. Rotate it back!

The simplest way is to rotate the canvas, apply the fill or stroke, and rotate it back again. This is usually the best option if you’re only applying one or a small number of transformations.

double angle = 45d;
context.rotate(angle);
context.fillText(10, 15, "Eden Coding");
context.rotate(-angle);
2 Save and restore

If you’re applying multiple transforms, the canvas also has the option to save and restore states. It actually holds them in a stack (first-in-first-out), so you can hold multiple states at once. Then, you can retrieve them by popping the last state off the stack.

context.save();
context.rotate(angle);
context.fillText(10, 15, "Eden Coding");
context.restore();

The full canvas state includes fill and stroke parameters, as well as clip regions, effects, text properties and transforms.

Conclusions

The canvas object, and the graphics context it maintains, are fantastically flexible and powerful drawing tools. Using simple instructions, the canvas allows the user to dynamically render images, text, shapes and complex paths onto a node.

These objects are flattened into pixel data, meaning little performance overhead for maintaining bounds and transforms.

The canvas itself is by default transparent. However, the background can be set, or cleared, by issuing simple rectangle drawing commands. This overwrites existing pixel data completely, essentially resetting the canvas.

Finally, the shapes and lines of a canvas can be extensively customised. These style instructions are maintained in a State object by the graphics context, meaning multiple objects can be drawn without having to re-issue styling calls or arguments each time.