2

I made a little JavaFX app generating longshadows. At this point I struggle with the rendering (see picture).

  1. The missing line on the rectangle's corner seems hard to fix. Changing the loop, which applies the manipulation, will mess up other shapes' shadow (e.g. circle).
  2. The glitch at 'a' is related to the Bresenham algorithm, I guess.(?)

Additional info:

Changing the image resolution makes no difference: Gitches keep showing.

Question:

How to get it fixed? Does the SDK provide something helpful? Do I have to rewrite the code?

Screenshot

Code

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;

import javax.imageio.ImageIO;

import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelWriter;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.stage.Stage;


public class Main extends Application {

    private PrintWriter writer;

    private String colorObjFilter = "0x009688ff";

    private static final String IMG_PATH = "img/ls-test-1k.png";

    private static final int LONGSHADOW_LENGTH = 100;

    private static final String
            ANSI_GREEN = "\u001B[32m",
            ANSI_RESET = "\u001B[0m";


    @Override
    public void start(Stage stage) throws Exception {
        writer = new PrintWriter("out.txt", "UTF-8");

        InputStream is = new FileInputStream(new File(IMG_PATH));
        ImageView imageView = new ImageView(new Image(is));
        Image image = imageView.getImage();

        StackPane root = new StackPane();
        Scene scene = new Scene(root, image.getWidth(), image.getHeight(), Paint.valueOf
                ("#EEEEEE"));
        scene.addEventFilter(KeyEvent.KEY_PRESSED, evt -> {
            if (evt.getCode().equals(KeyCode.ESCAPE)) {
                stage.close();
            }
        });

        final Canvas canvas = new Canvas(image.getWidth(), image.getHeight());

        canvas.setOnMouseClicked((MouseEvent e) -> {
            Color color = image.getPixelReader().getColor((int) e.getX(), (int) e.getY());
            System.out.println(ANSI_GREEN + " -> " + color.toString() + ANSI_RESET);
            colorObjFilter = color.toString();

            try {
                processImage(root, canvas, image);
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        });

        root.getChildren().addAll(imageView, canvas);
        stage.setScene(scene);
        stage.show();
    }


    private void processImage(StackPane root, Canvas canvas, Image image) throws IOException {
        long delta = System.currentTimeMillis();

        int width = (int) image.getWidth();
        int height = (int) image.getHeight();

        GraphicsContext gc = canvas.getGraphicsContext2D();
        System.out.println("width: " + width + "\theight: " + height);

        BufferedImage bufferedImage = ImageIO.read(new File(IMG_PATH));

        // keep threshold small to get clean paths to draw
        edgeDetection(gc, image, 0.00000001d);

        writer.close();
        Label label = new Label();
        root.setAlignment(Pos.BOTTOM_LEFT);
        root.setOnMouseMoved(event -> label.setText(event.getX() + "|" + event.getY()
                + "|" + bufferedImage.getRGB((int) event.getX(), (int) event.getY())));
        root.getChildren().addAll(label);
        System.out.println("took: " + (System.currentTimeMillis() - delta) + " ms");

    }


    public void edgeDetection(GraphicsContext gc, Image image, double threshold) {
        Color topPxl, lowerPxl;
        double topIntensity, lowerIntensity;
        PixelWriter pw = gc.getPixelWriter();

        for (int y = 0; y < image.getHeight() - 1; y++) {
            for (int x = 1; x < image.getWidth(); x++) {

                topPxl = image.getPixelReader().getColor(x, y);
                lowerPxl = image.getPixelReader().getColor(x - 1, y + 1);

                topIntensity = (topPxl.getRed() + topPxl.getGreen() + topPxl.getBlue()) / 3;
                lowerIntensity = (lowerPxl.getRed() + lowerPxl.getGreen() + lowerPxl.getBlue()) / 3;

                if (Math.abs(topIntensity - lowerIntensity) > threshold) {
                    int y2 = y;
                    for (int x2 = x; x2 < x + LONGSHADOW_LENGTH; x2++) {
                        y2++;
                        try {
                            Color color = image.getPixelReader().getColor(x2, y2);
                            // colorObjFilter protects the purple letter being manipulated
                            if (!color.toString().toLowerCase()
                                    .contains(colorObjFilter.toLowerCase())) {
                                pw.setColor(x2, y2, Color.color(.7f, .7f, .7f, .9f));
                            }
                        } catch (Exception e) {
                            System.out.println("Error: " + e.getMessage());
                        }
                    }
                }
            }
        }
    }


    public static void main(String[] args) {
        launch(args);
    }
}
Martin Pfeffer
  • 12,471
  • 9
  • 59
  • 68

1 Answers1

2

I have no idea why your original program has some rendering artifacts.

Here is an alternate solution, which is extremely brute force, as it just generates a shadow image that it renders over and over again at different offsets to end up with a long shadow. A couple of methods of generating the shadowImage are demonstrated, one is a ColorAdjust effect on the original image, the other is generation of a shadow image using a PixelWriter.

Your original solution of using a PixelWriter for everything with an appropriate algorithm for shadow generation is more elegant (if you can get it to work ;-).

shadow

import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Label;
import javafx.scene.effect.ColorAdjust;
import javafx.scene.image.Image;
import javafx.scene.image.PixelReader;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

public class ShadowSpray extends Application {
    private static final double W = 400;
    private static final double H = 400;
    private static final int SHADOW_LENGTH = 100;

    private static final double IMG_X = 20;
    private static final double IMG_Y = 20;

    private static final int FONT_SIZE = 200;

    private static final double SHADOW_SLOPE_FACTOR = 1.5;

    Color SHADOW_COLOR = Color.GRAY.brighter();

    @Override
    public void start(Stage stage) {
        Image image = getImage();

        Canvas canvas = new Canvas(W, H);
        GraphicsContext gc = canvas.getGraphicsContext2D();
//        drawWithShadowUsingStencil(gc, image, IMG_X, IMG_Y, SHADOW_LENGTH, SHADOW_COLOR);
        drawWithShadowUsingColorAdjust(gc, image, IMG_X, IMG_Y, SHADOW_LENGTH);

        stage.setScene(new Scene(new Group(canvas)));
        stage.show();
    }

    private void drawWithShadowUsingColorAdjust(GraphicsContext gc, Image image, double x, double y, int shadowLength) {
        // here the color adjust for the shadow is based upon the intensity of the input image color
        // which is a weird way to calculate a shadow color, but does come out nicely
        // because it appropriately handles antialiased input images.
        ColorAdjust monochrome = new ColorAdjust();
        monochrome.setBrightness(+0.5);
        monochrome.setSaturation(-1.0);

        gc.setEffect(monochrome);

        for (int offset = shadowLength; offset > 0; --offset) {
            gc.drawImage(image, x + offset, y + offset / SHADOW_SLOPE_FACTOR);
        }

        gc.setEffect(null);

        gc.drawImage(image, x, y);
    }

    private void drawWithShadowUsingStencil(GraphicsContext gc, Image image, double x, double y, int shadowLength, Color shadowColor) {
        Image shadow = createShadowImage(image, shadowColor);

        for (int offset = shadowLength; offset > 0; --offset) {
            gc.drawImage(shadow, x + offset, y + offset / SHADOW_SLOPE_FACTOR);
        }

        gc.drawImage(image, x, y);
    }


    private Image createShadowImage(Image image, Color shadowColor) {
        WritableImage shadow = new WritableImage(image.getPixelReader(), (int) image.getWidth(), (int) image.getHeight());
        PixelReader reader = shadow.getPixelReader();
        PixelWriter writer = shadow.getPixelWriter();
        for (int ix = 0; ix < image.getWidth(); ix++) {
            for (int iy = 0; iy < image.getHeight(); iy++) {
                int argb = reader.getArgb(ix, iy);
                int a = (argb >> 24) & 0xFF;
                int r = (argb >> 16) & 0xFF;
                int g = (argb >>  8) & 0xFF;
                int b =  argb        & 0xFF;

                // because we use a binary choice, we lose anti-alising info in the shadow so it looks a bit jaggy.
                Color fill = (r > 0 || g > 0 || b > 0) ? shadowColor : Color.TRANSPARENT;

                writer.setColor(ix, iy, fill);
            }
        }
        return shadow;
    }

    private Image getImage() {
        Label label = new Label("a");
        label.setStyle("-fx-text-fill: forestgreen; -fx-background-color: transparent; -fx-font-size: " + FONT_SIZE + "px;");
        Scene scene = new Scene(label, Color.TRANSPARENT);
        SnapshotParameters snapshotParameters = new SnapshotParameters();
        snapshotParameters.setFill(Color.TRANSPARENT);
        return label.snapshot(snapshotParameters, null);
    }

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

Inbuilt, JavaFX has a DropShadow effect, which is almost what you want, especially when you set the spread to 1 and the radius to 0, however, it just generates a single offset shadow image rather than a long shadow effect.


With some alternate text and a shorter "long shadow":

Cat

jewelsea
  • 150,031
  • 14
  • 366
  • 406
  • Thanks jewelsea, I will check my code with the monochromed effect and let you know. Great answer. – Martin Pfeffer Mar 20 '17 at 23:50
  • After taking a closer look into your code, I am even more impressed by your skill. Helped a lot and gave me a new understanding about using the canvas in JavaFX. – Martin Pfeffer Mar 21 '17 at 15:42