javajavafxjavafx-css

JavaFX css grid inconsistent


The following code produces and infinitely pannable grid and also allows for zooming in and out. The grid is being drawn using css.

My issue is that the css grid doesn't work well when zoomed in very closely or zoomed out a lot. It also is inconsistent and will sometimes display only vertical or only horizontal lines. I'm not sure how to reliably reproduce that, it just kinda happens sometimes.

The perfect solution would be something similar to the grid in blender, where new lines come in to replace the smaller lines that are no longer visible.

Part that creates and sets css:

private void reSetStyle(Pane pane) {
        Scene scene = pane.getScene();

        double startPointX = pane.getTranslateX()+(scene.getWidth()/2)+(0.5*scale);
        double endPointX = pane.getTranslateX()+(scene.getWidth()/2)+(10.5*scale);
        double zeroX = pane.getTranslateX()+(scene.getWidth()/2)*scale;

        startPointX = Math.round(startPointX * 1000d)/1000d;
        endPointX = Math.round(endPointX * 1000d)/1000d;
        zeroX = Math.round(zeroX * 1000d)/1000d;

        double startPointY = pane.getTranslateY()+(scene.getHeight()/2)+(0.5*scale);
        double endPointY = pane.getTranslateY()+(scene.getHeight()/2)+(10.5*scale);
        double zeroY = pane.getTranslateY()+(scene.getHeight()/2)*scale;

        startPointY = Math.round(startPointY * 1000d)/1000d;
        endPointY = Math.round(endPointY * 1000d)/1000d;
        zeroY = Math.round(zeroY * 1000d)/1000d;

        pane.getParent().setStyle("-fx-background-color: #393939," +
                "linear-gradient(from "+startPointX+"px "+zeroX+"px to "+endPointX+"px "+zeroX+"px, repeat, #2f2f2f 5%, transparent 6%)," +
                "linear-gradient(from "+zeroY+"px "+startPointY+"px to "+zeroY+"px "+endPointY+"px, repeat, #2f2f2f 5%, transparent 6%);");
}

Full Code:

public class Main extends Application {

    private double scale = 1d, minimumScale = 0.2, maximumScale = 2.5;

    @Override
    public void start(Stage primaryStage) {
        Pane pane = new Pane();
        StackPane stackPane = new StackPane();

        pane.translateXProperty().addListener((observableValue, number, t1) -> reSetStyle(pane));
        pane.translateYProperty().addListener((observableValue, number, t1) -> reSetStyle(pane));
        new Pannable(pane, stackPane);


        stackPane.getChildren().add(pane);
        stackPane.addEventFilter(ScrollEvent.ANY, scrollEvent -> {
            scrollEvent.consume();
            zoom(scrollEvent, pane);
        });

        primaryStage.setTitle("broken css");
        primaryStage.setScene(new Scene(new BorderPane(stackPane), 640, 480));
        primaryStage.show();

        reSetStyle(pane);
    }

    private void zoom(ScrollEvent scrollEvent, Pane pane) {
        if (scrollEvent.getDeltaY() == 0) return;

        double SCALE_DELTA = 0.1;
        double scaleFactor = (scrollEvent.getDeltaY() > 0) ? SCALE_DELTA : -1 * SCALE_DELTA;

        double nonZoomedXOffset = pane.getTranslateX() / scale;
        double nonZoomedYOffset = pane.getTranslateY() / scale;

        //Rounding is needed because java will cause floating point errors otherwise
        scale = clamp(minimumScale, Math.round((pane.getScaleX() + scaleFactor) * 1000d)/1000d, maximumScale);

        pane.setScaleX(scale);
        pane.setScaleY(scale);

        pane.setTranslateX(nonZoomedXOffset * scale);
        pane.setTranslateY(nonZoomedYOffset * scale);

        reSetStyle(pane);
    }

    private void reSetStyle(Pane pane) {
        Scene scene = pane.getScene();

        double startPointX = pane.getTranslateX()+(scene.getWidth()/2)+(0.5*scale);
        double endPointX = pane.getTranslateX()+(scene.getWidth()/2)+(10.5*scale);
        double zeroX = pane.getTranslateX()+(scene.getWidth()/2)*scale;

        startPointX = Math.round(startPointX * 1000d)/1000d;
        endPointX = Math.round(endPointX * 1000d)/1000d;
        zeroX = Math.round(zeroX * 1000d)/1000d;

        double startPointY = pane.getTranslateY()+(scene.getHeight()/2)+(0.5*scale);
        double endPointY = pane.getTranslateY()+(scene.getHeight()/2)+(10.5*scale);
        double zeroY = pane.getTranslateY()+(scene.getHeight()/2)*scale;

        startPointY = Math.round(startPointY * 1000d)/1000d;
        endPointY = Math.round(endPointY * 1000d)/1000d;
        zeroY = Math.round(zeroY * 1000d)/1000d;

        pane.getParent().setStyle("-fx-background-color: #393939," +
                "linear-gradient(from "+startPointX+"px "+zeroX+"px to "+endPointX+"px "+zeroX+"px, repeat, #2f2f2f 5%, transparent 6%)," +
                "linear-gradient(from "+zeroY+"px "+startPointY+"px to "+zeroY+"px "+endPointY+"px, repeat, #2f2f2f 5%, transparent 6%);");
    }

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

    private static class Pannable implements EventHandler<MouseEvent> {
        private double lastMouseX = 0, lastMouseY = 0;

        private final Node eventNode;
        private final Node dragNode;

        public Pannable(final Node dragNode, final Node eventNode) {
            this.eventNode = eventNode;
            this.dragNode = dragNode;
            this.eventNode.addEventHandler(MouseEvent.ANY, this);
        }

        @Override
        public final void handle(final MouseEvent event) {
            if(!event.isPrimaryButtonDown() && !event.isMiddleButtonDown()) return;

            if (MouseEvent.MOUSE_PRESSED == event.getEventType()) {
                if (this.eventNode.contains(event.getX(), event.getY())) {
                    this.lastMouseX = event.getSceneX();
                    this.lastMouseY = event.getSceneY();
                    event.consume();
                }
            } else if (MouseEvent.MOUSE_DRAGGED == event.getEventType()) {
                ((Node) event.getSource()).setCursor(Cursor.MOVE);

                final double deltaX = (event.getSceneX() - this.lastMouseX);
                final double deltaY = (event.getSceneY() - this.lastMouseY);

                final double initialTranslateX = dragNode.getTranslateX();
                final double initialTranslateY = dragNode.getTranslateY();
                dragNode.setTranslateX(initialTranslateX+deltaX);
                dragNode.setTranslateY(initialTranslateY+deltaY);

                this.lastMouseX = event.getSceneX();
                this.lastMouseY = event.getSceneY();
            }
        }
    }
}

Solution

  • As far as I am aware of, the approach you followed is pretty simple and straightforward which simplifies the logic when compared with other approaches.

    Having said that, the reason for the undesired behavior is due to relying on % values for color stops, which will be constantly changing based on the base value. I have not done a deep investigation regarding this, but I am quite convinced that this could be the reason :).

    So the alternate to % is to rely on "px". But there is catch here... there is bug in JavaFX CSSParser.java class, where, applying the linear-gradient with "px" based color stops through CSS does not work correctly.

    To over come this issue, set the background using "setBackground" API instead of setting it through "setStyle". What I mean is, instead of setting like

    String grad = "linear-gradient(from "+xStart+"px 0px to " + xEnd + "px 0px , repeat, transparent " + (gridSize - 1) + "px, #ffffff50 1px)";
    pane.getParent().setStyle("-fx-background-color:"+grad);
    

    set the background using below approach:

    String grad = "linear-gradient(from "+xStart+"px 0px to " + xEnd + "px 0px , repeat, transparent " + (gridSize - 1) + "px, #ffffff50 1px)";
    Background background = new Background(new BackgroundFill(LinearGradient.valueOf(grad ), CornerRadii.EMPTY, Insets.EMPTY));
    ((StackPane) pane.getParent()).setBackground(background);
    

    And also there is another issue in using "%" color stops. The width of the grid lines will not be consistent. I mean you cannot maintain same px(say 1px) on different scales. You can notice that in your demo.

    So considering all these, I only changed the code of your resetStyle method as below (a fine tuning may be needed) and the output is shown in the attached gif.

    private void reSetStyle(Pane pane) {
            Scene scene = pane.getScene();
            double defaultBlockSize = 50;//px Default size of the grid at 100% scale
            double gridSize = Math.ceil(defaultBlockSize * scale);
            double xStart = Math.ceil(pane.getTranslateX()+scene.getWidth()/2);
            double xEnd = xStart+ gridSize;
            double yStart = Math.ceil(pane.getTranslateY()+scene.getHeight()/2);
            double yEnd = yStart+gridSize;
            double linePx = 2; // the line thickness in px
            String vLines = "linear-gradient(from "+xStart+"px 0px to " + xEnd + "px 0px , repeat, transparent " + (gridSize - linePx) + "px, #ffffff50 1px)";
            String hLines = "linear-gradient(from 0px "+yStart+"px to 0px " + yEnd + "px , repeat, transparent " + (gridSize - linePx) + "px , #ffffff50 1px )";
            Background background = new Background(new BackgroundFill(Paint.valueOf("#393939"), CornerRadii.EMPTY, Insets.EMPTY),
                    new BackgroundFill(LinearGradient.valueOf(vLines), CornerRadii.EMPTY, Insets.EMPTY),
                    new BackgroundFill(LinearGradient.valueOf(hLines), CornerRadii.EMPTY, Insets.EMPTY));
            ((StackPane) pane.getParent()).setBackground(background);
        }
    

    enter image description here

    I hope you are already well aware of JavaFX gradients. But just in case you can refer to this blog JavaFX Gradients if you need a bit more details. This is very old blog which I wrote back in 2012. I assume nothing is changed much ;)

    [UPDATE] : I just found some glitches in the code. Will again update the answer once I fix thim.

    [UPDATE 2] : Looks like the gradient is having issues when trying to render some floating point values. When rounded those values, it looks like the issue is fixed. Also updated the code to set the zoom point at center. Can you give a try. I updated my code and gif.

    [UPDATE 3] : Explanation about line thickness

    The 'gridSize" is the amount we are setting for the linear gradient to render. The gradient repeats after this amount of size. So given two colors, <color1 stop1>, <color2 stop2> The <color1> is rendered till <stop1> amount(px) and then from there <color2> start rendering till <stop2>. In this case the stop2 value does'nt matter(it can be 1px or 2px or even 100px). So pretty much the stop1 value will decide the line thickness (a.k.a the second color rendering). The below image can give you some quick explanation. enter image description here