While JavaFX offers a rich set of pre-built UI controls and shape nodes for building interfaces, sometimes you need raw, high-performance rendering power for complex visualizations, games, or custom data displays. This is where the JavaFX Canvas API shines—providing a direct, immediate-mode drawing surface that delivers exceptional performance for dynamic graphics.
Understanding the Canvas Paradigm
The Canvas API represents a fundamental shift from JavaFX's retained-mode scene graph:
- Retained Mode (Scene Graph): You create
Shapeobjects (Circle, Rectangle, etc.) and add them to the scene graph. JavaFX manages these objects, handling their rendering, mouse events, and transformations automatically. - Immediate Mode (Canvas): You draw directly onto a pixel buffer using a graphics context. Once drawn, the shapes are "forgotten"—they become just pixels. You must manually redraw everything when changes occur.
This makes Canvas ideal for:
- Dynamic, frequently changing visuals (games, simulations)
- Complex, data-driven visualizations (charts, graphs, heat maps)
- Custom-drawn UI components with complex appearance
- Image processing and manipulation
- Pixel-level operations
Core Components: Canvas and GraphicsContext
The Canvas API centers around two key classes:
// Create a Canvas with specific dimensions Canvas canvas = new Canvas(800, 600); // Get the GraphicsContext - your drawing toolkit GraphicsContext gc = canvas.getGraphicsContext2D();
The GraphicsContext provides all the drawing operations you need, from basic shapes to complex transformations and effects.
Basic Drawing Operations
1. Shapes and Paths
public class BasicShapesExample extends Application { @Override public void start(Stage stage) { Canvas canvas = new Canvas(400, 300); GraphicsContext gc = canvas.getGraphicsContext2D(); // Set fill and stroke colors gc.setFill(Color.LIGHTBLUE); gc.setStroke(Color.DARKBLUE); gc.setLineWidth(2); // Draw a filled rectangle gc.fillRect(50, 50, 100, 80); // Draw a stroked rectangle (outline) gc.strokeRect(50, 50, 100, 80); // Draw a circle (using arc) gc.setFill(Color.RED); gc.fillOval(200, 50, 80, 80); // Draw a line gc.setStroke(Color.GREEN); gc.setLineWidth(3); gc.strokeLine(50, 200, 350, 200); // Draw a complex path gc.setStroke(Color.PURPLE); gc.beginPath(); gc.moveTo(100, 250); gc.lineTo(150, 220); gc.lineTo(200, 250); gc.lineTo(250, 220); gc.lineTo(300, 250); gc.stroke(); Group root = new Group(canvas); stage.setScene(new Scene(root)); stage.setTitle("Basic Canvas Drawing"); stage.show(); } } 2. Text Rendering
private void drawText(GraphicsContext gc) { gc.setFill(Color.BLACK); gc.setFont(Font.font("Arial", FontWeight.BOLD, 24)); // Basic text gc.fillText("Hello Canvas!", 50, 50); // Stroked text gc.setStroke(Color.BLUE); gc.setLineWidth(1); gc.strokeText("Outlined Text", 50, 80); // Text with background gc.setFill(Color.YELLOW); gc.fillRect(45, 95, 160, 30); gc.setFill(Color.RED); gc.fillText("Text with Background", 50, 115); } 3. Images
private void drawImages(GraphicsContext gc) { // Load an image Image image = new Image("https://example.com/image.png"); // Draw the entire image gc.drawImage(image, 50, 50); // Draw a portion of the image (source rectangle -> destination rectangle) gc.drawImage(image, 10, 10, 50, 50, // source: x, y, width, height 150, 50, 100, 100 // destination: x, y, width, height ); } Advanced Canvas Techniques
1. Transformations and Compositing
private void demonstrateTransformations(GraphicsContext gc) { // Save the current state gc.save(); // Translate (move) the coordinate system gc.translate(200, 100); // Rotate the coordinate system (45 degrees) gc.rotate(45); // Scale the coordinate system gc.scale(1.5, 1.5); // Draw a rectangle - it will be transformed gc.setFill(Color.ORANGE); gc.fillRect(-25, -25, 50, 50); // Restore the original state gc.restore(); // Demonstrate blending gc.setGlobalAlpha(0.5); // 50% opacity gc.setFill(Color.BLUE); gc.fillRect(220, 80, 60, 60); gc.setGlobalAlpha(1.0); // Reset opacity // Different blend modes gc.setGlobalBlendMode(BlendMode.MULTIPLY); gc.setFill(Color.RED); gc.fillRect(240, 100, 60, 60); gc.setGlobalBlendMode(BlendMode.SRC_OVER); // Reset to default } 2. Gradients and Patterns
private void demonstrateFills(GraphicsContext gc) { // Linear gradient LinearGradient linearGrad = new LinearGradient( 0, 0, 1, 0, true, CycleMethod.REFLECT, new Stop(0.0, Color.RED), new Stop(0.5, Color.YELLOW), new Stop(1.0, Color.GREEN) ); gc.setFill(linearGrad); gc.fillRect(50, 50, 200, 100); // Radial gradient RadialGradient radialGrad = new RadialGradient( 0, 0, 100, 100, 50, false, CycleMethod.REFLECT, new Stop(0.0, Color.WHITE), new Stop(1.0, Color.BLUE) ); gc.setFill(radialGrad); gc.fillOval(300, 50, 100, 100); // Image pattern Image patternImage = new Image("pattern.png"); ImagePattern pattern = new ImagePattern(patternImage, 0, 0, 20, 20, false); gc.setFill(pattern); gc.fillRect(50, 200, 200, 100); } Animation and Interactivity
1. Game Loop with AnimationTimer
The AnimationTimer is perfect for creating smooth animations and games:
public class ParticleSystem extends Application { private Canvas canvas; private GraphicsContext gc; private List<Particle> particles = new ArrayList<>(); private class Particle { double x, y, vx, vy, life; Color color; } @Override public void start(Stage stage) { canvas = new Canvas(800, 600); gc = canvas.getGraphicsContext2D(); // Create initial particles for (int i = 0; i < 100; i++) { particles.add(createParticle()); } // Handle mouse interaction canvas.setOnMouseMoved(e -> { // Add particles at mouse position for (int i = 0; i < 5; i++) { Particle p = createParticle(); p.x = e.getX(); p.y = e.getY(); particles.add(p); } }); // Animation loop new AnimationTimer() { @Override public void handle(long now) { updateParticles(); render(); } }.start(); stage.setScene(new Scene(new Group(canvas))); stage.show(); } private Particle createParticle() { Particle p = new Particle(); p.x = 400; p.y = 300; p.vx = Math.random() * 4 - 2; p.vy = Math.random() * 4 - 2; p.life = 1.0; p.color = Color.rgb( (int)(Math.random() * 255), (int)(Math.random() * 255), (int)(Math.random() * 255) ); return p; } private void updateParticles() { Iterator<Particle> it = particles.iterator(); while (it.hasNext()) { Particle p = it.next(); p.x += p.vx; p.y += p.vy; p.vy += 0.1; // gravity p.life -= 0.01; if (p.life <= 0 || p.x < 0 || p.x > 800 || p.y > 600) { it.remove(); } } // Add new particles occasionally if (Math.random() < 0.1) { particles.add(createParticle()); } } private void render() { // Clear with semi-transparent black for trail effect gc.setFill(Color.rgb(0, 0, 0, 0.1)); gc.fillRect(0, 0, canvas.getWidth(), canvas.getHeight()); // Draw all particles for (Particle p : particles) { gc.setFill(p.color.deriveColor(0, 1, 1, p.life)); gc.fillOval(p.x, p.y, 4 * p.life, 4 * p.life); } // Draw FPS counter gc.setFill(Color.WHITE); gc.fillText("Particles: " + particles.size(), 10, 20); } } 2. Interactive Data Visualization
public class InteractiveChart extends Application { private Canvas canvas; private GraphicsContext gc; private double[] data = new double[100]; private double scale = 1.0; private double offset = 0.0; @Override public void start(Stage stage) { canvas = new Canvas(800, 400); gc = canvas.getGraphicsContext2D(); // Generate sample data for (int i = 0; i < data.length; i++) { data[i] = Math.sin(i * 0.1) * 50 + Math.random() * 20; } // Zoom with scroll canvas.setOnScroll(e -> { scale *= (e.getDeltaY() > 0) ? 1.1 : 0.9; scale = Math.max(0.1, Math.min(10.0, scale)); renderChart(); }); // Pan with drag final double[] lastX = new double[1]; canvas.setOnMousePressed(e -> lastX[0] = e.getX()); canvas.setOnMouseDragged(e -> { offset += (e.getX() - lastX[0]) * 0.1; lastX[0] = e.getX(); renderChart(); }); renderChart(); stage.setScene(new Scene(new Group(canvas))); stage.setTitle("Interactive Data Chart"); stage.show(); } private void renderChart() { gc.clearRect(0, 0, canvas.getWidth(), canvas.getHeight()); // Draw grid gc.setStroke(Color.LIGHTGRAY); gc.setLineWidth(0.5); for (int x = 0; x < canvas.getWidth(); x += 50) { gc.strokeLine(x, 0, x, canvas.getHeight()); } for (int y = 0; y < canvas.getHeight(); y += 50) { gc.strokeLine(0, y, canvas.getWidth(), y); } // Draw data line gc.setStroke(Color.BLUE); gc.setLineWidth(2); gc.beginPath(); double centerY = canvas.getHeight() / 2; for (int i = 0; i < data.length; i++) { double x = i * 8 * scale + offset; double y = centerY - data[i] * scale; if (i == 0) { gc.moveTo(x, y); } else { gc.lineTo(x, y); } } gc.stroke(); // Draw data points gc.setFill(Color.RED); for (int i = 0; i < data.length; i++) { double x = i * 8 * scale + offset; double y = centerY - data[i] * scale; gc.fillOval(x - 2, y - 2, 4, 4); } // Draw info gc.setFill(Color.BLACK); gc.fillText(String.format("Scale: %.2f, Offset: %.2f", scale, offset), 10, 20); } } Performance Optimization Tips
1. Minimize State Changes
// ❌ Inefficient - frequent state changes for (Shape shape : shapes) { gc.setFill(shape.getColor()); gc.setStroke(shape.getBorderColor()); drawShape(shape); } // ✅ Efficient - batch by state Map<Color, List<Shape>> shapesByColor = shapes.stream() .collect(Collectors.groupingBy(Shape::getColor)); for (Map.Entry<Color, List<Shape>> entry : shapesByColor.entrySet()) { gc.setFill(entry.getKey()); for (Shape shape : entry.getValue()) { drawShape(shape); } } 2. Use Pixel Buffer for Heavy Manipulation
public class PixelBufferExample { private WritableImage buffer; private PixelWriter pixelWriter; private PixelReader pixelReader; public PixelBufferExample(Canvas canvas) { buffer = new WritableImage((int)canvas.getWidth(), (int)canvas.getHeight()); pixelWriter = buffer.getPixelWriter(); pixelReader = buffer.getPixelReader(); } public void processPixels() { // Direct pixel manipulation for (int y = 0; y < buffer.getHeight(); y++) { for (int x = 0; x < buffer.getWidth(); x++) { Color color = pixelReader.getColor(x, y); Color newColor = processPixel(color, x, y); pixelWriter.setColor(x, y, newColor); } } // Draw the buffer to canvas in one operation GraphicsContext gc = canvas.getGraphicsContext2D(); gc.drawImage(buffer, 0, 0); } private Color processPixel(Color original, int x, int y) { // Apply some pixel transformation return Color.color( original.getRed() * 0.8, original.getGreen() * 1.2, original.getBlue() * 0.9 ); } } When to Use Canvas vs. Scene Graph
| Use Case | Recommended Approach |
|---|---|
| Static UI with complex styling | Scene Graph with CSS |
| Many simple, interactive shapes | Scene Graph |
| Complex, data-driven visualization | Canvas |
| Game with many moving objects | Canvas |
| Image processing/manipulation | Canvas |
| Custom-drawn charts/graphs | Canvas |
| Pixel-level effects | Canvas |
Conclusion
The JavaFX Canvas API provides a powerful, high-performance alternative to the scene graph for demanding graphics applications. By giving you direct control over pixel rendering with a comprehensive set of drawing operations, it enables:
- Exceptional performance for dynamic content
- Fine-grained control over every aspect of rendering
- Complex visualizations and custom graphics
- Smooth animations and interactive experiences
While it requires more manual management than the scene graph, the performance benefits for graphics-intensive applications make it well worth the effort. Whether you're building games, data visualizations, or custom UI components, the Canvas API gives you the tools to create stunning, high-performance graphics in JavaFX.