JavaFX Transparent Cursor using WritableImage
Asked Answered
S

2

1

Edit-Answer:

You can check Fabian's answer and also this library (https://github.com/goxr3plus/JFXCustomCursor)

Actual Question

I want to create a cursor which is fading out in JavaFX so for that i am using a WritableImage and i am continuously reading pixels from the original Image and writing them to a new WritableImage.Then i set a custom cursor to the Scene using ImageCursor(writableImage),below is the full code(give it a try).

The problem is that a get black pixels where transparent pixels are expected.

Note that all the below classes have to be in package sample.

Code(Main):

package sample;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;

public class Main extends Application {

    FadingCursor fade = new FadingCursor();

    @Override
    public void start(Stage primaryStage) throws Exception {

        primaryStage.setWidth(300);
        primaryStage.setHeight(300);

        Scene scene = new Scene(new FlowPane());

        primaryStage.setScene(scene);

        fade.startFade(scene,100);

        primaryStage.show();
    }

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

}

Code(FadingCursor)(Edited):

package sample;

import java.util.concurrent.CountDownLatch;

import javafx.application.Platform;
import javafx.scene.ImageCursor;
import javafx.scene.Scene;
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;

public class FadingCursor {

    private int counter;
    private Image cursorImage;

    /**
     * Change the image of the Cursor
     * 
     * @param image
     */
    public void setCursorImage(Image image) {
        this.cursorImage = image;
    }

    /**
     * Start fading the Cursor
     * 
     * @param scene
     */
    public void startFade(Scene scene, int millisecondsDelay) {

        // Create a Thread
        new Thread(() -> {

            // Keep the original image stored here
            Image image = new Image(getClass().getResourceAsStream("fire.png"), 64, 64, true, true);
            PixelReader pixelReader = image.getPixelReader();

            // Let's go
            counter = 10;
            for (; counter >= 0; counter--) {
                CountDownLatch count = new CountDownLatch(1);
                Platform.runLater(() -> {

                    // Create the fading image
                    WritableImage writable = new WritableImage(64, 64);
                    PixelWriter pixelWriter = writable.getPixelWriter();

                    // Fade out the image
                    for (int readY = 0; readY < image.getHeight(); readY++) {
                        for (int readX = 0; readX < image.getWidth(); readX++) {
                            Color color = pixelReader.getColor(readX, readY);

                            // Now write a brighter color to the PixelWriter.

                            // -------------------------Here some way happens
                            // the problem------------------
                            color = new Color(color.getRed(), color.getGreen(), color.getBlue(), (counter / 10.00) * color.getOpacity());
                            pixelWriter.setColor(readX, readY, color);
                        }
                    }

                    System.out.println("With counter:"+counter+" opacity is:" + writable.getPixelReader().getColor(32, 32).getOpacity());


                    scene.setCursor(new ImageCursor(writable));
                    count.countDown();
                });
                try {
                    // Wait JavaFX Thread to change the cursor
                    count.await();
                    // Sleep some time
                    Thread.sleep(millisecondsDelay);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }

        }).start();

    }

}

The image(needs to be downloaded)(Right Click ->Save Image as...):

enter image description here

Stability answered 25/9, 2016 at 23:1 Comment(2)
So your problem is the white space around the image shows up as black pixels (instead of transparent)when displayed correct?Mensural
@adam Ren Exactly :)Stability
P
2

You set the opacity of every pixel to a value only depending on the loop variable here:

color = new Color(color.getRed(), color.getGreen(), color.getBlue(), counter / 10.00);

For transparent pixels (opacity = 0) you actually increase the opacity making the values stored in the other channels (in this case 0 / black) visible. You need to make sure transparent pixels remain transparent, which usually is done like this:

color = new Color(color.getRed(), color.getGreen(), color.getBlue(), (counter / 10.00) * color.getOpacity());

Alternatively you could use deriveColor:

color = color.deriveColor(0, 1, 1, counter / 10d);

Edit

For some reason ImageCursor doesn't seem to like a completely transparent image. You can check that this works, if at least one pixel is not completely transparent by adding

pixelWriter.setColor(0, 0, new Color(0, 0, 0, 0.01));

After the for loops writing the image.

To fix this you could simply use Cursor.NONE instead of an ImageCursor with a fully transparent image:

for (; counter >= 1; counter--) {
    ...
}
Platform.runLater(() -> scene.setCursor(Cursor.NONE));

 

Alternative without the need to recreate the image/cursor

You could simulate the cursor yourself by moving a image Across the root of the Scene. This won't make the image show up beyond the bounds of the Scene, but you can apply animations to the ImageView for fading instead of modifying the opacity of each pixel manually...

public class CursorSimulator {

    private final FadeTransition fade;

    public CursorSimulator(Image image, Scene scene, ObservableList<Node> rootChildrenWriteable, double hotspotX, double hotspotY) {
        ImageView imageView = new ImageView(image);
        imageView.setManaged(false);
        imageView.setMouseTransparent(true);

        fade = new FadeTransition(Duration.seconds(2), imageView);
        fade.setFromValue(0);
        fade.setToValue(1);

        // keep image on top
        rootChildrenWriteable.addListener((Observable o) -> {
            if (imageView.getParent() != null
                    && rootChildrenWriteable.get(rootChildrenWriteable.size() - 1) != imageView) {
                // move image to top, after changes are done...
                Platform.runLater(() -> imageView.toFront());
            }
        });
        scene.addEventFilter(MouseEvent.MOUSE_ENTERED, evt -> {
            rootChildrenWriteable.add(imageView);
        });
        scene.addEventFilter(MouseEvent.MOUSE_EXITED, evt -> {
            rootChildrenWriteable.remove(imageView);
        });
        scene.addEventFilter(MouseEvent.MOUSE_MOVED, evt -> {
            imageView.setLayoutX(evt.getX() - hotspotX);
            imageView.setLayoutY(evt.getY() - hotspotY);
        });
        scene.setCursor(Cursor.NONE);
    }

    public void fadeOut() {
        fade.setRate(-1);
        if (fade.getStatus() != Animation.Status.RUNNING) {
            fade.playFrom(fade.getTotalDuration());
        }
    }

    public void fadeIn() {
        fade.setRate(1);
        if (fade.getStatus() != Animation.Status.RUNNING) {
            fade.playFromStart();
        }
    }

}
@Override
public void start(Stage primaryStage) {
    Button btn = new Button("Say 'Hello World'");
    btn.setOnAction((ActionEvent event) -> {
        System.out.println("Hello World!");
    });

    StackPane root = new StackPane();
    root.getChildren().add(btn);

    Scene scene = new Scene(root, 500, 500);
    
    Image image = new Image("https://i.sstatic.net/OHj1R.png");
    CursorSimulator simulator = new CursorSimulator(image, scene, root.getChildren(), 32, 50);
    scene.setOnMouseClicked(new EventHandler<MouseEvent>() {

        private boolean fadeOut = true;
        
        @Override
        public void handle(MouseEvent event) {
            if (fadeOut) {
                simulator.fadeOut();
            } else {
                simulator.fadeIn();
            }
            fadeOut = !fadeOut;
        }
    });

    primaryStage.setScene(scene);
    primaryStage.show();
}
Pother answered 26/9, 2016 at 11:19 Comment(6)
Thanks for the effort.Fabian i need to know why it shows black pixels instead of transparent ones.Stability
@GoXR3Plus Sorry about that. I edited the question and added info about the issue to the start of the question.Pother
Fabian i have edited the question using what you have suggested.It works until opacity is 0.1 and then at opacity 0 the whole rectangle becomes blackStability
I think there is a bug actually i also use Color color = new Color(0.5,0.5,0.5,(counter /10d) * color1.getOpacity()) or Color color = Color.rgb(170,170,0,(counter / 10d) * color1.getOpacity()) and again black on the end :)Stability
@GoXR3Plus There seems to be an issue with fully transparent images... Edited the answer...Pother
Thanks!Also thanks for the second solution with that i created a class to created library to create custom cursors and published it to GitHub.I will accept your answer and add one more answer also.Stability
S
1

The reason of this question was to create a kind of cursor that can be modified.For example here i wanted to make it had a fade effect.For future users who want to create custom cursors i have created a library on github and i will show some code here: https://github.com/goxr3plus/JFXCustomCursor

Code:

import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;

/**
 * This class allows you to set as a Cursor in a JavaFX Scene,whatever you want
 * ,even a video!. <br>
 * <br>
 * <b>What you have to do is create a basic layout,for example:</b><br>
 * #-->A BorderPane which contains a MediaView,<br>
 * #-->A StackPane which contains an animated ImageView,<br>
 * #-->A Pane which contains an animated Rectangle or something more complex
 * etc..)<br>
 * 
 * <br>
 * <br>
 * The options are unlimited!
 * 
 * @author GOXR3PLUS
 * @param <T>
 * @Version 1.0
 */
public class JFXCustomCursor {

    private SimpleIntegerProperty hotSpotX = new SimpleIntegerProperty();
    private SimpleIntegerProperty hotSpotY = new SimpleIntegerProperty();

    private Scene scene;
    private Pane sceneRoot;
    private Pane content;
    private EventHandler<MouseEvent> eventHandler1;
    private EventHandler<MouseEvent> eventHandler2;
    private EventHandler<MouseEvent> eventHandler3;

    /**
     * Constructor
     * 
     * @param scene
     *            The Scene of your Stage
     * @param sceneRoot
     *            The Root of your Stage Scene
     * @param content
     *            The content of the JFXCustomCursor class
     * @param hotspotX
     *            Represents the location of the cursor inside the content on X
     *            axis
     * @param hotspotY
     *            Represents the location of the cursor inside the content on Y
     *            axis
     */
    public JFXCustomCursor(Scene scene, Pane sceneRoot, Pane content, int hotspotX, int hotspotY) {

        // Go
        setRoot(scene, sceneRoot, content, hotspotX, hotspotY);

    }

    /**
     * This method changes the content of the JFXCustomCursor
     * 
     * @param scene
     *            The Scene of your Stage
     * @param sceneRoot
     *            The Root of your Stage Scene
     * @param content
     *            The content of the JFXCustomCursor class
     * @param hotspotX
     *            Represents the location of the cursor inside the content on X
     *            axis
     * @param hotspotY
     *            Represents the location of the cursor inside the content on Y
     *            axis
     */
    public void setRoot(Scene scene, Pane sceneRoot, Pane content, int hotSpotX, int hotSpotY) {

        // Keep them in case of unRegister-reRegister
        unRegister(); // has to be called before the below happens
        this.scene = scene;
        this.sceneRoot = sceneRoot;
        this.content = content;

        // hot spots
        this.hotSpotX.set(hotSpotX);
        this.hotSpotX.set(hotSpotY);

        // cursor container
        content.setManaged(false);
        content.setMouseTransparent(true);

        // Keep the Content on the top of Scene
        ObservableList<Node> observable = sceneRoot.getChildren();
        observable.addListener((Observable osb) -> {
            if (content.getParent() != null && observable.get(observable.size() - 1) != content) {
                // move the cursor on the top
                Platform.runLater(content::toFront);
            }
        });

        if (!observable.contains(content))
            observable.add(content);

        // Add the event handlers
        eventHandler1 = evt -> {
            if (!sceneRoot.getChildren().contains(content))
                observable.add(content);
        };
        eventHandler2 = evt -> observable.remove(content);

        eventHandler3 = evt -> {
            content.setLayoutX(evt.getX() - hotSpotX);
            content.setLayoutY(evt.getY() - hotSpotY);
        };

        scene.addEventFilter(MouseEvent.MOUSE_ENTERED, eventHandler1);
        scene.addEventFilter(MouseEvent.MOUSE_EXITED, eventHandler2);
        scene.addEventFilter(MouseEvent.MOUSE_MOVED, eventHandler3);

    }

    /**
     * Unregisters the CustomCursor from the Scene completely
     */
    public void unRegister() {
        if (scene != null) {
            sceneRoot.getChildren().remove(content);
            scene.removeEventFilter(MouseEvent.MOUSE_ENTERED, eventHandler1);
            scene.removeEventFilter(MouseEvent.MOUSE_EXITED, eventHandler2);
            scene.removeEventFilter(MouseEvent.MOUSE_MOVED, eventHandler3);
        }
    }

    /**
     * Re register the CustomCursor to the Scene,<b>this method is
     * experimental(use with caution!)</b>
     */
    public void reRegister() {
        if (scene != null)
            setRoot(scene, sceneRoot, content, hotSpotX.get(), hotSpotY.get());
    }

    public SimpleIntegerProperty hotSpotXProperty() {
        return hotSpotX;
    }

    public SimpleIntegerProperty hotSpotYProperty() {
        return hotSpotY;
    }

}
Stability answered 27/9, 2016 at 8:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.