Manius
Manius

Reputation: 3644

JavaFX - Prevent blurring on scaled Canvas for a pixelated zoom

Is there any way to stop this blurring when scaling a Canvas? I presume it's related to GPU interpolation. But what I need is a pixel-perfect "pixelated" zoom here. Just use the color of the nearest "real" neighboring pixel.

I've seen the solutions here but of the two suggested that work (#1 / #4), #4 is definitely CPU scaling and #1 I guess is too.

This scaling needs to be FAST - I'd like to be able to support up to maybe 20-25 layers (probably Canvases in a StackPane but I'm open to other ideas as long as they don't melt the CPU). I'm having doubts this can be done without GPU support which JFX offers, but maybe not with a flexible enough API. Strategies like #4 in the linked answer which rely on CPU rescaling probably aren't going to work.

If you zoom into the highest zoom level with this code the blurring is obvious.

Do we need an update to the JFX API to support this or something? This should be possible to do.

import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Paint;
import javafx.stage.Stage;

public class ScaleTest extends Application {
    
    private static final int width = 1200;
    private static final int height = 800;
    private static final int topMargin = 32;
    private StackPane stackPane;
    private IntegerProperty zoomLevel = new SimpleIntegerProperty(100);
    
    @Override
    public void start(Stage stage) {
        stage.setWidth(width);
        stage.setMinHeight(height);
        
        stackPane = new StackPane();
        stackPane.setLayoutY(topMargin);
        Canvas canvas = new Canvas(width, height - topMargin);
        
        Label label = new Label();
        label.setLayoutY(2);
        label.setLayoutX(2);
        label.setStyle("-fx-text-fill: #FFFFFF");
        label.textProperty().bind(zoomLevel.asString());
        Button zoomInBtn = new Button("Zoom In");
        zoomInBtn.setLayoutY(2);
        zoomInBtn.setLayoutX(50);
        zoomInBtn.onActionProperty().set((e) -> {
            if (zoomLevel.get() < 3200) {
                zoomLevel.set(zoomLevel.get() * 2);
                stackPane.setScaleX(zoomLevel.get() / 100.0);
                stackPane.setScaleY(zoomLevel.get() / 100.0);
            }
        });
        Button zoomOutBtn = new Button("Zoom Out");
        zoomOutBtn.setLayoutY(2);
        zoomOutBtn.setLayoutX(140);
        zoomOutBtn.onActionProperty().set((e) -> {
            if (zoomLevel.get() > 25) {
                zoomLevel.set(zoomLevel.get() / 2);
                stackPane.setScaleX(zoomLevel.get() / 100.0);
                stackPane.setScaleY(zoomLevel.get() / 100.0);
            }
        });
        
        Pane mainPane = new Pane(stackPane, label, zoomInBtn, zoomOutBtn);
        mainPane.setStyle("-fx-background-color: #000000");
        Scene scene = new Scene(mainPane);
        stage.setScene(scene);
        
        drawGrid(canvas, 0, 0, width, height - topMargin, 16);
        stackPane.getChildren().add(canvas);
        
        stage.show();
    }
    
    private void drawGrid(Canvas canvas, int xPos, int yPos, int width, int height, int gridSize) {
        boolean darkX = true;
        String darkCol = "#111111";
        String lightCol = "#222266";
        
        for (int x = xPos; x < canvas.getWidth(); x += gridSize) {
            boolean dark = darkX;
            darkX = !darkX;
            if (x > width) {
                break;
            }
            
            for (int y = yPos; y < canvas.getHeight(); y += gridSize) {
                if (y > height) {
                    break;
                }
                
                dark = !dark;
                String color;
                if (dark) {
                    color = darkCol;
                } else {
                    color = lightCol;
                }
                canvas.getGraphicsContext2D().setFill(Paint.valueOf(color));
                canvas.getGraphicsContext2D().fillRect(x, y, gridSize, gridSize);
            }
        }
    }
}

blurry image

Upvotes: 5

Views: 793

Answers (2)

trashgod
trashgod

Reputation: 205865

Your example adjusts the node's scale properties to resample a fixed-size image, which inevitably results in such artifact. Only a vector representation can be scaled with arbitrary precision. You may need to decide how your application will support vectors and/or bitmaps. For example, if your application were really about scaling rectangles, you would invoke fillRect() with scaled coordinates, rather than scaling a picture of a smaller rectangle.

You've cited a good summary or resizing, so I'll focus on vector opportunities:

  • Concrete subclasses of Shape are, in effect, vector representations of geometric elements that can be rendered at any scale; resize the example shown here or here to see the effect; scroll to zoom and click to drag the circle here, noting that its border remains sharp at all scales.

  • An SVGPath is a Shape that can be scaled as shown here.

  • Internally, a Font stores the vector representation of individual glyphs. When instantiated, the glyph is rasterized at a certain point size.

  • If a suitable vector representation is known, drawing can be tied to the size of the Canvas as shown here.

Upvotes: 3

swpalmer
swpalmer

Reputation: 4380

This seems to do the trick:


package com.analogideas.scratch;
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Paint;
import javafx.stage.Stage;


public class ImagePixelator extends Application {

    private static final int width = 1200;
    private static final int height = 800;
    private static final int topMargin = 32;
    private IntegerProperty zoomLevel = new SimpleIntegerProperty(1);

    public static void main(String[] args) {
        launch(args);
    }
    
    @Override
    public void start(Stage stage) {
        stage.setWidth(width);
        stage.setMinHeight(height);

        Canvas canvasOut = new Canvas(width, height - topMargin);
        Canvas canvas = new Canvas(width, height - topMargin);
        drawGrid(canvas, 0, 0, width, height - topMargin, 16);
        WritableImage img = canvas.snapshot(null, null);
        drawImage(canvasOut, img, 0, 0, 1);
        StackPane pane = new StackPane(canvasOut);

        Label label = new Label();
        label.setLayoutY(2);
        label.setLayoutX(2);
        label.setStyle("-fx-text-fill: #FFFFFF");
        label.textProperty().bind(zoomLevel.asString());
        Button zoomInBtn = new Button("Zoom In");
        zoomInBtn.setLayoutY(2);
        zoomInBtn.setLayoutX(50);
        zoomInBtn.onActionProperty().set((e) -> {
            if (zoomLevel.get() < 3200) {
                zoomLevel.set(zoomLevel.get() * 2);
                int z = zoomLevel.get();
                drawImage(canvasOut, img, 0, 0, z);
            }
        });
        Button zoomOutBtn = new Button("Zoom Out");
        zoomOutBtn.setLayoutY(2);
        zoomOutBtn.setLayoutX(140);
        zoomOutBtn.onActionProperty().set((e) -> {
            if (zoomLevel.get() > 1) {
                zoomLevel.set(zoomLevel.get() /2);
                int z = zoomLevel.get();
                drawImage(canvasOut, img, 0, 0, z);
            }
        });

        Pane mainPane = new Pane(pane, label, zoomInBtn, zoomOutBtn);
        mainPane.setStyle("-fx-background-color: #000000");
        Scene scene = new Scene(mainPane);
        stage.setScene(scene);
        stage.show();
    }

    private void drawImage(Canvas canvas, Image image, int x, int y, float zoom) {
        GraphicsContext g = canvas.getGraphicsContext2D();
        g.setImageSmoothing(false);
        int w = (int) (image.getWidth() * zoom);
        int h = (int) (image.getHeight() * zoom);
        g.drawImage(image, x, y, w, h);

    }

    private void drawGrid(Canvas canvas, int xPos, int yPos, int width, int height, int gridSize) {
        Paint darkPaint = Paint.valueOf("#111111");
        Paint lightPaint = Paint.valueOf("#222266");
        GraphicsContext g = canvas.getGraphicsContext2D();
        g.setImageSmoothing(false);
        int xLimit = width / gridSize;
        int yLimit = height / gridSize;
        for (int x = 0; x <= xLimit; x++) {
            for (int y = 0; y <= yLimit; y++) {
                boolean dark = (((x ^ y) & 1) == 0);
                g.setFill(dark ? darkPaint : lightPaint);
                g.fillRect(xPos + x * gridSize, yPos + y * gridSize, gridSize, gridSize);
            }
        }
    }
}

Upvotes: 1

Related Questions