JavaFX Fundamentals & the Scene Graph

Basic Shapes & the Canvas

18 min Lesson 6 of 12

Basic Shapes & the Canvas

JavaFX ships with two distinct drawing systems. The first is the shape node system: declarative objects like Rectangle, Circle, and Line that live as first-class nodes in the scene graph, respond to CSS, receive mouse events, and can be animated with the same transition API you use for any other node. The second is the Canvas: an immediate-mode pixel surface that works like the HTML5 canvas — you obtain a GraphicsContext and issue drawing commands imperatively. Both approaches have their place, and understanding which to reach for is a key skill.

Shape Nodes: Declarative Vector Graphics

All shape classes extend javafx.scene.shape.Shape, which in turn extends Node. That means every shape automatically participates in layout, hit-testing, and animations. The most commonly used shapes are:

  • Rectangle — defined by x, y, width, height, plus optional arcWidth/arcHeight for rounded corners.
  • Circle — defined by centre coordinates centerX, centerY and radius.
  • Ellipse — like Circle but with independent radiusX and radiusY.
  • Line — connects two points: (startX, startY) to (endX, endY).
  • Polygon / Polyline — arbitrary sequences of x/y coordinate pairs.
  • Path — the most powerful shape: a sequence of PathElement objects (MoveTo, LineTo, CubicCurveTo, ArcTo, etc.) that can describe any outline.

Here is a small scene that places three shapes in a Pane and applies fills and strokes:

import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.layout.Pane; import javafx.scene.paint.Color; import javafx.scene.shape.Circle; import javafx.scene.shape.Line; import javafx.scene.shape.Rectangle; import javafx.stage.Stage; public class ShapeDemo extends Application { @Override public void start(Stage stage) { // Rounded rectangle Rectangle rect = new Rectangle(40, 40, 180, 100); rect.setArcWidth(20); rect.setArcHeight(20); rect.setFill(Color.STEELBLUE); rect.setStroke(Color.DARKBLUE); rect.setStrokeWidth(2); // Circle with no fill (ring) Circle ring = new Circle(320, 90, 50); ring.setFill(Color.TRANSPARENT); ring.setStroke(Color.CORAL); ring.setStrokeWidth(4); // Diagonal line Line diagonal = new Line(30, 200, 420, 200); diagonal.setStroke(Color.DIMGRAY); diagonal.setStrokeWidth(2); diagonal.getStrokeDashArray().addAll(12.0, 6.0); // dashed Pane pane = new Pane(rect, ring, diagonal); stage.setScene(new Scene(pane, 460, 260)); stage.setTitle("Shape Nodes"); stage.show(); } public static void main(String[] args) { launch(args); } }
Fill vs Stroke: setFill() paints the inside of a shape; setStroke() paints its outline. Both accept any Paint — a Color, a LinearGradient, or a RadialGradient. Setting fill to Color.TRANSPARENT is the correct way to create an outline-only shape.

Working with LinearGradient

JavaFX paints are first-class objects. A LinearGradient sweeps between colour stops along a direction vector. The constructor arguments are: start-X, start-Y, end-X, end-Y (all in the range 0–1 when proportional=true), cycleMethod, proportional flag, and a list of Stop objects.

import javafx.scene.paint.CycleMethod; import javafx.scene.paint.LinearGradient; import javafx.scene.paint.Stop; import javafx.scene.shape.Rectangle; Rectangle gradRect = new Rectangle(50, 50, 200, 100); LinearGradient gradient = new LinearGradient( 0, 0, 1, 0, // left to right true, // proportional CycleMethod.NO_CYCLE, new Stop(0.0, Color.DEEPSKYBLUE), new Stop(1.0, Color.MEDIUMPURPLE) ); gradRect.setFill(gradient);

Stroke Properties

The Shape class exposes fine-grained stroke control that mirrors the SVG/canvas model:

  • setStrokeLineCap(StrokeLineCap.ROUND) — rounded ends on open paths and lines.
  • setStrokeLineJoin(StrokeLineJoin.MITER) — how corners are joined.
  • setStrokeDashArray(Double...) — alternating dash and gap lengths.
  • setStrokeDashOffset(double) — phase shift into the dash pattern, useful for animated "marching ants".

The Canvas API: Immediate-Mode Drawing

Canvas is a Node with a fixed pixel buffer. You do not add child shapes to it; instead you issue drawing commands on its GraphicsContext. Changes are immediate and permanent — there is no retained shape hierarchy to query or animate. That makes Canvas the right choice for dense visualisations (charts, game sprites, heatmaps) where managing thousands of individual scene-graph nodes would be too expensive.

import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import javafx.scene.text.Font; import javafx.stage.Stage; public class CanvasDemo extends Application { @Override public void start(Stage stage) { Canvas canvas = new Canvas(480, 300); GraphicsContext gc = canvas.getGraphicsContext2D(); // --- background --- gc.setFill(Color.web("#1e1e2e")); gc.fillRect(0, 0, 480, 300); // --- filled circle --- gc.setFill(Color.CORAL); gc.fillOval(40, 60, 100, 100); // x, y, w, h (bounding box) // --- stroked rectangle --- gc.setStroke(Color.LIMEGREEN); gc.setLineWidth(3); gc.strokeRect(200, 60, 120, 80); // --- dashed line --- gc.setLineDashes(10, 5); gc.setStroke(Color.GOLD); gc.strokeLine(30, 220, 450, 220); // --- text --- gc.setFill(Color.WHITE); gc.setFont(Font.font("Monospaced", 16)); gc.fillText("Canvas Demo", 180, 280); // --- bezier curve --- gc.setStroke(Color.HOTPINK); gc.setLineWidth(2); gc.setLineDashes(); // reset dashes gc.beginPath(); gc.moveTo(30, 160); gc.bezierCurveTo(120, 100, 320, 240, 450, 150); gc.stroke(); stage.setScene(new Scene(new StackPane(canvas))); stage.setTitle("Canvas Demo"); stage.show(); } public static void main(String[] args) { launch(args); } }
State machine mindset: The GraphicsContext maintains a current drawing state (fill paint, stroke paint, line width, font, transform, clip). Call setters before each draw call that needs them. Use gc.save() / gc.restore() to push and pop the entire state stack, which is the cleanest way to apply a temporary transform or clip without polluting subsequent draw calls.

Clearing and Redrawing the Canvas

Because the canvas is a pixel buffer, updating it means repainting. The standard pattern is to clear the dirty region and repaint everything:

// Inside an AnimationTimer or event handler: gc.clearRect(0, 0, canvas.getWidth(), canvas.getHeight()); drawEverything(gc); // your custom method
Canvas is not retained: Unlike shape nodes, pixels drawn on a Canvas are not associated with objects. You cannot "select" a previously drawn circle and change its colour — you must repaint. If you need interactive, selectable graphics, prefer shape nodes. If you need to draw thousands of items per frame, prefer Canvas.

Saving and Restoring GraphicsContext State

gc.save(); // push state gc.translate(200, 150); // temporary transform: move origin gc.rotate(45); // rotate 45 degrees gc.setFill(Color.ORANGE); gc.fillRect(-30, -30, 60, 60); // centred on new origin gc.restore(); // pop state — transform gone, fill reset

Choosing Between Shape Nodes and Canvas

  • Use shape nodes when you need click/hover events on individual shapes, CSS styling, or smooth property animations via the transitions API.
  • Use Canvas when you are drawing hundreds of items per frame, implementing a custom chart or a game loop, or need precise pixel-level control.
  • Mix both freely: a Canvas is just a Node — you can place it in the same Pane as shape nodes, layering a pixel-drawn background behind interactive vector controls.

Summary

JavaFX gives you two powerful drawing tools. Shape nodes — Rectangle, Circle, Path, and their siblings — live in the scene graph, respond to CSS and events, and are the right default for interactive UI elements. The Canvas with its GraphicsContext is an immediate-mode surface that excels for dense, high-frequency rendering. Master both, understand the state-machine model of GraphicsContext, and use save()/restore() to keep complex drawing routines clean and composable.