Search code examples
javacanvasjavafxgraphics2d

JavaFX Canvas: fill closed path composed of multiple geometries


I want draw and fill a path made up of 2 arcs and 2 lines with a specific color. I need to use the Canvas from JavaFX, because there is more i need to draw. The problem is, that the path which I created is not even drawn nor filled. What I want is this

enter image description here

but my code produces this

enter image description here

As you may notice this arc is thinner on the left and on the right side than it is in the middle. Just using a simple arc with a ceratin stroke width is unfortuneatly not an option for me.

This is my code where the commented part produces the second image. I already tried to use the draw path facilities and to fill it, but it is not working

import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;
import javafx.scene.shape.ArcType;
import javafx.stage.Stage;

public final class CanvasTest extends Application {

    private static final int WIDTH = 400;
    private static final int HEIGHT = 300;
    private static final int RADIUS = 250;

    private static final int BRICK_HEIGHT = 15;
    private static final int RADIUSHALF = RADIUS / 2;
    private static GraphicsContext gc;

    @Override
    public void start(Stage stage) {
        final Group root = new Group();
        final Scene scene = new Scene(root, WIDTH, HEIGHT);
        final Canvas can = new Canvas(WIDTH, HEIGHT);
        gc = can.getGraphicsContext2D();

        gc.setFill(Color.BLACK);
        gc.fillRect(0, 0, WIDTH, HEIGHT);

        drawArc(WIDTH / 2, HEIGHT / 2);

        root.getChildren().add(can);
        stage.setScene(scene);
        stage.show();
    }

    private void drawArc(final int posX, final int posY) {
        gc.setStroke(Color.WHITE);
        gc.setLineWidth(1);
        gc.setFill(Color.WHITE);

        final double newRadius = RADIUSHALF - BRICK_HEIGHT;
        final double yOffsetLowerArc = Math.cos(Math.toRadians(45)) * newRadius;
        final double xOffsetLowerArc = Math.sin(Math.toRadians(45)) * newRadius;
        final double newAngleLowerArc = Math.toDegrees(Math.atan2(xOffsetLowerArc, yOffsetLowerArc + BRICK_HEIGHT));
        final double xOffsetUpperArc = Math.cos(Math.toRadians(45)) * RADIUSHALF;
        final double yOffsetUpperArc = Math.sin(Math.toRadians(45)) * RADIUSHALF;
        final double yOffsetNewLowerArc = Math.cos(Math.toRadians(newAngleLowerArc)) * RADIUSHALF;
        final double xOffsetNewLowerArc = Math.sin(Math.toRadians(newAngleLowerArc)) * RADIUSHALF;

        // this code produces the un-filled custom arc
        // gc.strokeArc(posX - RADIUSHALF, posY - RADIUSHALF, RADIUS, RADIUS, 45, 90, ArcType.OPEN);
        // gc.strokeArc(posX - RADIUSHALF, posY - RADIUSHALF + BRICK_HEIGHT, RADIUS, RADIUS, 90 - newAngleLowerArc, 2 * newAngleLowerArc, ArcType.OPEN);
        // gc.strokeLine(posX - xOffsetNewLowerArc, posY + BRICK_HEIGHT - yOffsetNewLowerArc, posX - xOffsetUpperArc, posY - yOffsetUpperArc);
        // gc.strokeLine(posX + xOffsetNewLowerArc, posY + BRICK_HEIGHT - yOffsetNewLowerArc, posX + xOffsetUpperArc, posY - yOffsetUpperArc);

        gc.beginPath();
        gc.arc(posX - RADIUSHALF, posY - RADIUSHALF, RADIUS, RADIUS, 45, 90/*, ArcType.OPEN*/);
        gc.arc(posX - RADIUSHALF, posY - RADIUSHALF + BRICK_HEIGHT, RADIUS, RADIUS, 90 - newAngleLowerArc, 2 * newAngleLowerArc/*, ArcType.OPEN*/);
        gc.moveTo(posX + xOffsetNewLowerArc, posY + BRICK_HEIGHT - yOffsetNewLowerArc);
        gc.lineTo(posX + xOffsetUpperArc, posY - yOffsetUpperArc);
        gc.moveTo(posX - xOffsetNewLowerArc, posY + BRICK_HEIGHT - yOffsetNewLowerArc);
        gc.lineTo(posX - xOffsetUpperArc, posY - yOffsetUpperArc);
        gc.closePath();
        gc.fill();
    }

    public static void main(String[] args) {
        Application.launch(args);
    }
}

Maybe I also misunderstood the concept of paths in JavaFX, but for me my approch is sound :-) never the less it is not working. (I also read this "Working with Canvas")


Solution

  • There are a few issues with your code. The parameters of GraphicsContext.arc are not the same as GraphicsContext.strokeArc.

    strokeArc(double x,
              double y,
              double w,
              double h,
              double startAngle,
              double arcExtent,
              ArcType closure)
    arc(double centerX,
        double centerY,
        double radiusX,
        double radiusY,
        double startAngle,
        double length)
    

    The difference is: strokeArc draws the arc that is part of the oval in the rectangle positioned at point (x, y) with given width and height. arc however constructs a path that is part of the oval centered at (centerX, centerY) with radii radiusX and radiusY. To get the appropriate parameters for arc you can use the following formulae:

    centerX = (x+w)/2
    centerY = (y+h)/2
    radiusX = w/2
    radiusY = h/2
    

    Also you don't need to call moveTo to constuct the path. 2 arcs are sufficient to construct the path. Also make sure to use arcs that go in different directions (one clockwise one counter-clockwise) to connect the closest ends of the arcs:

    gc.beginPath();
    gc.arc(posX, posY, RADIUSHALF, RADIUSHALF, 45, 90);
    
    // next arc in opposite direction
    gc.arc(posX, posY + BRICK_HEIGHT, RADIUSHALF, RADIUSHALF, 90 + newAngleLowerArc, -2 * newAngleLowerArc);
    gc.closePath();
    
    gc.fill();