defano / jmonet

An easy-to-use toolkit for incorporating MacPaint / Microsoft Paint-like tools into a Java Swing or JavaFX application.
https://defano.github.io/jmonet/javadoc/
MIT License
31 stars 6 forks source link

Eraser erases clears all pixels, including those of uploaded image #47

Open ernOho opened 2 years ago

ernOho commented 2 years ago

Hi, I've noticed the standard settings of the eraser clears everything, including the pixels of the uploaded image(see example of image with red airbrush and grey cleared lines). Are there options to disable the eraser to clear the pixels of the image (and just painted lines)? Or is this still a ToDo?

image

Thank you and kind regards

defano commented 2 years ago

Hi @ernOho ,

When you say "uploaded image" do you mean that you've imported an image onto your canvas (i.e., following these instructions)?

If so, the behavior you're witnessing is expected. When you import an image it becomes part of the canvas and the pixels of that image are treated no differently that pixels you paint yourself. So if you erase it, it gets erased. :)

It sounds like what you're trying to achieve is to have a background image that you want to paint on top of, but that does not get modified by the paint tools. You can achieve this by drawing your background image onto a Swing/JFX component, then placing a JMonet Canvas component on top of it (make sure the canvas color is transparent).

Does that help?

ernOho commented 2 years ago

Hey @defano, The way you described it is exactly what I am trying to achieve. Sadly, I have been struggling to implement it like this.

On a normal FX Canvas it works, however, I'm struggling to implement it with the JMonetCanvas. Basically, I load the Image into the ImageView node, create a JMonetCanvas and stack them on top of each other with the StackPane (see attached image). The problem comes in once I want to start drawing on the Canvas. I've tried multiple options that the Canvas offers (see the light blue highlights on the image), but it doesn't want to go transparent. It turns light gray, or the eraser still erases light gray or completely messes with the magnifier and eraser tool.

image

Before starting to draw image

After drawing and erasing (image gets covered by canvas) image. I have also run different combinations of these methods, with no success.

Am I missing anything or implementing these wrongly? Keeping the canvas transparent is the issue I'm having.

Many thanks for your feedback so far. Am currently also working on issue #46.

ernOho commented 2 years ago

Hi @defano, After researching, it seems that the canvas is a heavyweight component whose color can't be put transparent. I have tried a couple different approaches to try to solve this issue of mine, but sadly without success.

Many thanks and kind regards

defano commented 2 years ago

💩

I suspect you're right. Sorry about that. If I get some time I'll look into what it might take to provide a JFX-native PaintCanvas object.

ernOho commented 2 years ago

Hi, I've tried one last approach which worked for me :). Instead of using the Basic Eraser Tool, I use the Selection Tool as an eraser.

//resize image
resizedImg = scale(myImage, 500, 500);
myCanvas = new JMonetCanvas(new Dimension(resizedImg.getWidth(),resizedImg.getHeight()));
myCanvas.commit(new ImageLayerSet(resizedImg));

JFXPaintCanvasNode myCanvasNode = new JFXPaintCanvasNode(myCanvas);

SelectionTool eraser = (SelectionTool) PaintToolBuilder.create(PaintToolType.SELECTION)
        .makeActiveOnCanvas(myCanvas)
        .build();  

scrollPane.setContent(myCanvasNode);

Then I add a listener to my JFXPaintCanvasNode, which basically enables me to take the selected section of the canvas, create a clipped image from the original image and use this clipped image to replace the selected image in the canvas (not sure why, but I had to use .setDirty() and .redrawSelection(true) to make it work better)

//handle eraser
myCanvasNode.setOnMouseReleased(new EventHandler(){
    @Override
    public void handle(Event event) {
        if(eraser != null){
            try{
                eraser.eraseSelectedPixelsFromCanvas();
                //create clipped snippet with location and dimensions from selected shape
                BufferedImage snippet = createClippedImg(eraser.getSelectionFrame());
                //set the snippet to the frame
                eraser.setSelectedImage(snippet);
                eraser.setDirty();
                eraser.redrawSelection(true);
             }
             catch(Exception e){
                //eraser.getSelectionFrame() null if eraser tool just pressed without being dragged
            }
        }
    }
});

With my createClippedImg() Method defined as follows:

public BufferedImage createClippedImg()(Shape selectionFrame) {
        //return a clipped image of the original image to given shape and at given location
        return resizedImg.getSubimage((int) selectionFrame.getBounds().getX(), (int) selectionFrame.getBounds().getY(), (int) selectionFrame.getBounds().width, (int) selectionFrame.getBounds().height);
    }

As I don't use the Selection tool classs anywhere else, I did edit it to fit my needs. So after the text has been 'erased', I don't want to be able to move the selected box. I commented out the relevant code from the handling method in the SelectionTool class.

 /**
     * {@inheritDoc}
     */
    @Override
    public void mouseDragged(MouseEvent e, Point canvasLoc) {

        // User is moving an existing selection --> don't allow user to move selection!
        if (hasSelection() && isMovingSelection) {
            //setDirty();
            //translateSelection(canvasLoc.x - lastPoint.x, canvasLoc.y - lastPoint.y);
            //redrawSelection(true);
            lastPoint = canvasLoc;
        }

        // User is defining a new selection rectangle
        else {
            Rectangle canvasBounds = new Rectangle(new Point(), getCanvas().getCanvasSize());
            getDelegate().addPointToSelectionFrame(initialPoint, MathUtils.constrainToBounds(canvasLoc, canvasBounds), e.isShiftDown());

            getScratch().clear();
            drawSelectionFrame();
            getCanvas().repaint();
        }
    }

I would imagine writing a similar SelectionTool class for the eraser could solve this issue? I haven't tried this for the normal eraser (not sure how well it would work).

Thank you for your speedy responses. Kind regards

defano commented 2 years ago

Don't kill me... :)

After reading through your code, it occurred to me that there might be an easier way to achieve what you're looking to do. You can simply setCanvasBackground() to a TexturePaint that contains the image you want to paint atop of. Then, when you erase paint from the canvas (irrespective of the tool you use) you'll reveal the canvas background image.

Something like:

try {
  BufferedImage bgImg = ImageIO.read(new File("MyBackground.png"));
  TexturePaint bg = new TexturePaint(bgImg, new Rectangle(0, 0, bgImg.getWidth(), bgImg.getHeight()));
  myCanvasNode.getCanvas().setCanvasBackground(bg);
} catch (IOException e) {
  e.printStackTrace();
}

Will that work?

ernOho commented 2 years ago

Haha don't worry. I appreciate the help!

That works very nicely. Being able to save my image with the painted graphics and then reloading it and still being able to 'erase' them makes my thrown together version a bit more suitable for my use-case.

But I think your solution actually solves this issue. Funny how easy it was xD.