← Example apps

Readable source

SketchApp.java

student-template/src/main/java/zero/SketchApp.java

This is a read-only view. Edit the copy in your local project, then Run App.

package zero;

import java.util.EnumSet;
import javafx.animation.AnimationTimer;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.Image;
import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.stage.Stage;

/**
 * Canvas app with explicit update and draw callbacks. Read or change this source when contributing to Zero.
 * settings configures the window; setup runs once after canvas creation,
 * before the window is shown.
 * Each frame calls update(seconds), then draw, on the JavaFX application thread.
 * Keep callbacks short: slow work blocks drawing and normal JavaFX controls.
 */
public abstract class SketchApp extends SimpleApp {
    private Canvas canvas;
    private BorderPane layout;
    private GraphicsContext graphics;
    private AnimationTimer timer;
    private final EnumSet<KeyCode> keys = EnumSet.noneOf(KeyCode.class);
    private final EnumSet<MouseButton> buttons = EnumSet.noneOf(MouseButton.class);
    private double mouseX;
    private double mouseY;

    public void update(double seconds) { }
    public void draw() { }

    @Override protected final javafx.scene.Node createContent() {
        canvas = new Canvas(initialWidth(), initialHeight());
        canvas.setFocusTraversable(true);
        graphics = canvas.getGraphicsContext2D();
        layout = new BorderPane(canvas);
        canvas.addEventHandler(javafx.scene.input.KeyEvent.KEY_PRESSED, event -> {
            if (canvas.isFocused()) keys.add(event.getCode());
        });
        canvas.focusedProperty().addListener((property, wasFocused, focused) -> {
            if (!focused) clearInput();
        });
        mouseX = width() / 2;
        mouseY = height() / 2;
        return layout;
    }

    @Override protected final Scene createScene(BorderPane content) { return new Scene(content); }

    @Override protected final void ready(Stage stage) {
        Scene scene = stage.getScene();
        // Release an owned input even when dragging outside the canvas.
        scene.addEventFilter(javafx.scene.input.KeyEvent.KEY_RELEASED,
                event -> keys.remove(event.getCode()));
        scene.addEventFilter(javafx.scene.input.MouseEvent.ANY, event -> {
            boolean onCanvas = event.getTarget() == canvas;
            if (onCanvas || !buttons.isEmpty()) {
                var point = canvas.sceneToLocal(event.getSceneX(), event.getSceneY());
                mouseX = point.getX();
                mouseY = point.getY();
            }
            if (event.getEventType() == javafx.scene.input.MouseEvent.MOUSE_PRESSED && onCanvas) {
                canvas.requestFocus();
                buttons.add(event.getButton());
            }
            if (event.getEventType() == javafx.scene.input.MouseEvent.MOUSE_RELEASED) {
                buttons.remove(event.getButton());
            }
        });
        stage.focusedProperty().addListener((property, wasFocused, focused) -> {
            if (!focused) clearInput();
        });
        canvas.requestFocus();
        timer = new AnimationTimer() {
            private long previous;
            @Override public void handle(long now) {
                double seconds = previous == 0 ? 0 : Math.min(0.1, (now - previous) / 1_000_000_000.0);
                previous = now;
                try {
                    update(seconds);
                    draw();
                } catch (RuntimeException | Error failure) {
                    SketchApp.this.stop(); // Stop the timer and clear held input after a failed frame.
                    throw failure;
                }
            }
        };
        timer.start();
    }

    private void clearInput() { keys.clear(); buttons.clear(); }

    @Override protected final void shutdown() {
        if (timer != null) timer.stop();
        clearInput();
    }

    public final double width() { return canvas.getWidth(); }
    public final double height() { return canvas.getHeight(); }
    public final double mouseX() { return mouseX; }
    public final double mouseY() { return mouseY; }
    public final boolean keyDown(KeyCode key) { return keys.contains(key); }
    public final boolean mouseDown(MouseButton button) { return buttons.contains(button); }

    /** Standard JavaFX escape hatches: call after the canvas is created. */
    public final Canvas canvas() { return canvas; }
    public final GraphicsContext graphics() { return graphics; }
    public final BorderPane layout() { return layout; }

    /** Clears the full canvas, retaining the current drawing styles. */
    public final void background(Color color) {
        graphics.save();
        graphics.setTransform(1, 0, 0, 1, 0, 0);
        graphics.clearRect(0, 0, width(), height());
        graphics.setFill(color);
        graphics.fillRect(0, 0, width(), height());
        graphics.restore();
    }
    public final void fill(Color color) { graphics.setFill(color); }
    public final void stroke(Color color) { graphics.setStroke(color); }
    public final void strokeWidth(double pixels) { graphics.setLineWidth(pixels); }
    public final void textSize(double pixels) { graphics.setFont(Font.font(pixels)); }
    public final void rect(double x, double y, double width, double height) { graphics.fillRect(x, y, width, height); }
    public final void outlineRect(double x, double y, double width, double height) { graphics.strokeRect(x, y, width, height); }
    public final void circle(double centreX, double centreY, double diameter) {
        graphics.fillOval(centreX - diameter / 2, centreY - diameter / 2, diameter, diameter);
    }
    public final void line(double x1, double y1, double x2, double y2) { graphics.strokeLine(x1, y1, x2, y2); }
    /** Text y is the baseline, following JavaFX conventions. */
    public final void text(String value, double x, double y) { graphics.fillText(value, x, y); }
    public final void image(Image image, double x, double y) { graphics.drawImage(image, x, y); }
    public final void image(Image image, double x, double y, double width, double height) {
        graphics.drawImage(image, x, y, width, height);
    }
    /** Loads a bundled resource once (for example /player.png in src/main/resources). */
    public final Image loadImage(String resourcePath) {
        var resource = getClass().getResource(resourcePath);
        if (resource == null) throw new IllegalArgumentException("Image resource not found: " + resourcePath);
        Image image = new Image(resource.toExternalForm());
        if (image.isError()) throw new IllegalArgumentException("Cannot load image: " + resourcePath, image.getException());
        return image;
    }
}