Coup de pinceau dans JavaFX


J'essaie d'écrire une application de peinture dans JavaFX. Je veux un pinceau ressemblant à un vrai pinceau, mais je ne sais pas comment démarrer l'algorithme. Le code ci-dessous montre mon coup de pinceau actuel, bien que ce soit un coup utile, ce n'est pas vraiment un pinceau:

import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.shape.StrokeLineJoin;
import javafx.stage.Stage;

import static javafx.scene.input.MouseEvent.*;


public class BrushTester extends Application {

    private static final Color color = Color.CHOCOLATE;
    private static final double START_OPACITY = 0.3;
    private static final double OPACITY_MODIFIER = 0.002;

    private double currentOpacity = START_OPACITY;
    private double strokeWidth = 15;

    public static void main(String[] args) {
        Application.launch(BrushTester.class);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        Canvas canvas = new Canvas(600d, 600d);
        GraphicsContext gc = canvas.getGraphicsContext2D();

        canvas.addEventHandler(MOUSE_DRAGGED, e -> BrushTester.this.handleMouseDragged(gc, e));
        canvas.addEventHandler(MOUSE_PRESSED, e -> handleMousePressed(gc, e));
        canvas.addEventHandler(MOUSE_RELEASED, e -> handleMouseReleased(gc, e));

        Group root = new Group();
        root.getChildren().add(canvas);
        primaryStage.setScene(new Scene(root, Color.DARKGRAY));
        primaryStage.show();
    }

    private void configureGraphicsContext(GraphicsContext gc) {
        gc.setStroke(new Color(color.getRed(), color.getGreen(), color.getBlue(), currentOpacity));
        gc.setLineCap(StrokeLineCap.ROUND);
        gc.setLineJoin(StrokeLineJoin.ROUND);
        gc.setLineWidth(strokeWidth);
    }

    public void handleMousePressed(GraphicsContext gc, MouseEvent e) {
        configureGraphicsContext(gc);
        gc.beginPath();
        gc.moveTo(e.getX(), e.getY());
        gc.stroke();
    }

    public void handleMouseReleased(GraphicsContext gc, MouseEvent e) {
        currentOpacity = START_OPACITY;
        gc.closePath();
    }

    public void handleMouseDragged(GraphicsContext gc, MouseEvent e) {
        currentOpacity = Math.max(0, currentOpacity - OPACITY_MODIFIER);
        configureGraphicsContext(gc);
        gc.lineTo(e.getX(), e.getY());
        gc.stroke();
    }
}

Quelqu'un avec quelques conseils sur la façon de se rapprocher de la vraie chose?

Author: BartCr, 2015-08-10

1 answers

Tout dépend de ce que vous essayez de réaliser. Personnellement, j'utiliserais

  • un animateur
  • un pinceau personnalisable (c'est-à-dire une Image) au lieu d'un trait, de sorte que vous pouvez spécifier la taille et la dureté
  • un algorithme de dessin au trait (comme Bresenham) pour connecter l'emplacement précédent de la souris avec l'emplacement actuel pour obtenir une ligne complète entre les points

Un exemple rapide avec un algorithme de dessin simple:

import java.util.Random;

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.Image;
import javafx.scene.image.WritableImage;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.RadialGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;

public class Main extends Application {

    private static double SCENE_WIDTH = 1280;
    private static double SCENE_HEIGHT = 720;

    static Random random = new Random();

    Canvas canvas;
    GraphicsContext graphicsContext;

    AnimationTimer loop;

    Point2D mouseLocation = new Point2D( 0, 0);
    boolean mousePressed = false;
    Point2D prevMouseLocation = new Point2D( 0, 0);

    Scene scene;

    Image brush = createBrush( 30.0, Color.CHOCOLATE);
    double brushWidthHalf = brush.getWidth() / 2.0;
    double brushHeightHalf = brush.getHeight() / 2.0;



    @Override
    public void start(Stage primaryStage) {

        BorderPane root = new BorderPane();

        canvas = new Canvas( SCENE_WIDTH, SCENE_HEIGHT);

        graphicsContext = canvas.getGraphicsContext2D();

        Pane layerPane = new Pane();

        layerPane.getChildren().addAll(canvas);

        root.setCenter(layerPane);

        scene = new Scene(root, SCENE_WIDTH, SCENE_HEIGHT);

        primaryStage.setScene(scene);
        primaryStage.show();

        addListeners();

        startAnimation();


    }

    private void startAnimation() {

        loop = new AnimationTimer() {

            @Override
            public void handle(long now) {

                if( mousePressed) {

                    // try this
                    // graphicsContext.drawImage( brush, mouseLocation.getX() - brushWidthHalf, mouseLocation.getY() - brushHeightHalf);

                    // then this
                    bresenhamLine( prevMouseLocation.getX(), prevMouseLocation.getY(), mouseLocation.getX(), mouseLocation.getY());

                }

                prevMouseLocation = new Point2D( mouseLocation.getX(), mouseLocation.getY());

            }
        };

        loop.start();

    }

    // https://de.wikipedia.org/wiki/Bresenham-Algorithmus
    private void bresenhamLine(double x0, double y0, double x1, double y1)
    {
      double dx =  Math.abs(x1-x0), sx = x0<x1 ? 1. : -1.;
      double dy = -Math.abs(y1-y0), sy = y0<y1 ? 1. : -1.;
      double err = dx+dy, e2; /* error value e_xy */

      while( true){
        graphicsContext.drawImage( brush, x0 - brushWidthHalf, y0 - brushHeightHalf);
        if (x0==x1 && y0==y1) break;
        e2 = 2.*err;
        if (e2 > dy) { err += dy; x0 += sx; } /* e_xy+e_x > 0 */
        if (e2 < dx) { err += dx; y0 += sy; } /* e_xy+e_y < 0 */
      }
    }


    private void addListeners() {

        scene.addEventFilter(MouseEvent.ANY, e -> {

            mouseLocation = new Point2D(e.getX(), e.getY());

            mousePressed = e.isPrimaryButtonDown();

        });


    }


    public static Image createImage(Node node) {

        WritableImage wi;

        SnapshotParameters parameters = new SnapshotParameters();
        parameters.setFill(Color.TRANSPARENT);

        int imageWidth = (int) node.getBoundsInLocal().getWidth();
        int imageHeight = (int) node.getBoundsInLocal().getHeight();

        wi = new WritableImage(imageWidth, imageHeight);
        node.snapshot(parameters, wi);

        return wi;

    }


    public static Image createBrush( double radius, Color color) {

        // create gradient image with given color
        Circle brush = new Circle(radius);

        RadialGradient gradient1 = new RadialGradient(0, 0, 0, 0, radius, false, CycleMethod.NO_CYCLE, new Stop(0, color.deriveColor(1, 1, 1, 0.3)), new Stop(1, color.deriveColor(1, 1, 1, 0)));

        brush.setFill(gradient1);

        // create image
        return createImage(brush);

    }


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

Bien sûr, vous pouvez étendre cela avec par exemple

  • plusieurs couches
  • Les modes de fusion de JavaFX au niveau calque et graphicscontext
  • pour simuler la force, j'utiliserais un délai de peinture (par exemple 200 ms) et un tampon pour les emplacements de la souris et laisserais l'opacité dépendre du fait que la souris soit toujours enfoncée ou non
  • lisser les lignes en utilisant les courbes de bézier
  • ...

Exemple avec des variations de pinceau lorsque vous commencez à peindre:

import java.util.Random;

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.Image;
import javafx.scene.image.WritableImage;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.RadialGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;

public class Main extends Application {

    private static double SCENE_WIDTH = 1280;
    private static double SCENE_HEIGHT = 720;

    static Random random = new Random();

    Canvas canvas;
    GraphicsContext graphicsContext;

    AnimationTimer loop;

    Point2D mouseLocation = new Point2D( 0, 0);
    boolean mousePressed = false;
    Point2D prevMouseLocation = new Point2D( 0, 0);

    Scene scene;

    double brushMaxSize = 30;
    Image brush = createBrush( brushMaxSize, Color.CHOCOLATE);
    double brushWidthHalf = brush.getWidth() / 2.0;
    double brushHeightHalf = brush.getHeight() / 2.0;

    double pressure = 0;
    double pressureDelay = 0.04;

    private Image[] brushVariations = new Image[256];

    @Override
    public void start(Stage primaryStage) {

        BorderPane root = new BorderPane();

        canvas = new Canvas( SCENE_WIDTH, SCENE_HEIGHT);

        for( int i=0; i < brushVariations.length; i++) {

            double size = (brushMaxSize - 1) / (double) brushVariations.length  * (double) i + 1;

            brushVariations[i] = createBrush( size, Color.CHOCOLATE);
        }

        graphicsContext = canvas.getGraphicsContext2D();

        Pane layerPane = new Pane();

        layerPane.getChildren().addAll(canvas);

        root.setCenter(layerPane);

        scene = new Scene(root, SCENE_WIDTH, SCENE_HEIGHT);

        primaryStage.setScene(scene);
        primaryStage.show();

        addListeners();

        startAnimation();


    }

    private void startAnimation() {

        loop = new AnimationTimer() {

            @Override
            public void handle(long now) {

                if( mousePressed) {

                    // try this
                    // graphicsContext.drawImage( brush, mouseLocation.getX() - brushWidthHalf, mouseLocation.getY() - brushHeightHalf);

                    // then this
                    bresenhamLine( prevMouseLocation.getX(), prevMouseLocation.getY(), mouseLocation.getX(), mouseLocation.getY());

                    pressure += pressureDelay;
                    if( pressure > 1) {
                        pressure = 1;
                    }

                } else {

                    pressure = 0;

                }

                prevMouseLocation = new Point2D( mouseLocation.getX(), mouseLocation.getY());

            }
        };

        loop.start();

    }

    // https://de.wikipedia.org/wiki/Bresenham-Algorithmus
    private void bresenhamLine(double x0, double y0, double x1, double y1)
    {
      double dx =  Math.abs(x1-x0), sx = x0<x1 ? 1. : -1.;
      double dy = -Math.abs(y1-y0), sy = y0<y1 ? 1. : -1.;
      double err = dx+dy, e2; /* error value e_xy */

      while( true){

        int variation = (int) (pressure * (brushVariations.length - 1));
        Image brushVariation = brushVariations[ variation ];

        graphicsContext.setGlobalAlpha(pressure);
        graphicsContext.drawImage( brushVariation, x0 - brushWidthHalf, y0 - brushHeightHalf);

        if (x0==x1 && y0==y1) break;
        e2 = 2.*err;
        if (e2 > dy) { err += dy; x0 += sx; } /* e_xy+e_x > 0 */
        if (e2 < dx) { err += dx; y0 += sy; } /* e_xy+e_y < 0 */
      }
    }


    private void addListeners() {

        scene.addEventFilter(MouseEvent.ANY, e -> {

            mouseLocation = new Point2D(e.getX(), e.getY());

            mousePressed = e.isPrimaryButtonDown();

        });


    }


    public static Image createImage(Node node) {

        WritableImage wi;

        SnapshotParameters parameters = new SnapshotParameters();
        parameters.setFill(Color.TRANSPARENT);

        int imageWidth = (int) node.getBoundsInLocal().getWidth();
        int imageHeight = (int) node.getBoundsInLocal().getHeight();

        wi = new WritableImage(imageWidth, imageHeight);
        node.snapshot(parameters, wi);

        return wi;

    }


    public static Image createBrush( double radius, Color color) {

        // create gradient image with given color
        Circle brush = new Circle(radius);

        RadialGradient gradient1 = new RadialGradient(0, 0, 0, 0, radius, false, CycleMethod.NO_CYCLE, new Stop(0, color.deriveColor(1, 1, 1, 0.3)), new Stop(1, color.deriveColor(1, 1, 1, 0)));

        brush.setFill(gradient1);

        // create image
        return createImage(brush);

    }


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

Exemple avec variation pour limiter le pinceau longueur

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.ColorPicker;
import javafx.scene.image.Image;
import javafx.scene.image.WritableImage;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.RadialGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;

public class Main extends Application {

    private static double SCENE_WIDTH = 1280;
    private static double SCENE_HEIGHT = 720;

    Canvas canvas;
    GraphicsContext graphicsContext;

    AnimationTimer loop;

    Point2D mouseLocation = new Point2D(0, 0);
    boolean mousePressed = false;
    Point2D prevMouseLocation = new Point2D(0, 0);

    Scene scene;

    double brushMaxSize = 30;

    double pressure = 0;
    double pressureDelay = 0.04;
    double pressureDirection = 1;

    double strokeTimeMax = 1;
    double strokeTime = 0;
    double strokeTimeDelay = 0.07;

    private Image[] brushVariations = new Image[256];

    ColorPicker colorPicker = new ColorPicker();

    @Override
    public void start(Stage primaryStage) {

        BorderPane root = new BorderPane();

        canvas = new Canvas(SCENE_WIDTH, SCENE_HEIGHT);

        graphicsContext = canvas.getGraphicsContext2D();
        graphicsContext.setFill(Color.WHITE);
        graphicsContext.fillRect(0, 0, SCENE_WIDTH, SCENE_HEIGHT);

        Pane layerPane = new Pane();

        layerPane.getChildren().addAll(canvas);

        colorPicker.setValue(Color.CHOCOLATE);
        colorPicker.setOnAction(e -> {
            createBrushVariations();
        });

        root.setCenter(layerPane);
        root.setTop(colorPicker);

        scene = new Scene(root, SCENE_WIDTH, SCENE_HEIGHT, Color.WHITE);

        primaryStage.setScene(scene);
        primaryStage.show();

        createBrushVariations();

        addListeners();

        startAnimation();

    }

    private void createBrushVariations() {

        for (int i = 0; i < brushVariations.length; i++) {

            double size = (brushMaxSize - 1) / (double) brushVariations.length * (double) i + 1;

            brushVariations[i] = createBrush(size, colorPicker.getValue());
        }

    }

    private void startAnimation() {

        loop = new AnimationTimer() {

            @Override
            public void handle(long now) {

                if (mousePressed) {

                    // try this
                    // graphicsContext.drawImage( brush, mouseLocation.getX() -
                    // brushWidthHalf, mouseLocation.getY() - brushHeightHalf);

                    // then this
                    bresenhamLine(prevMouseLocation.getX(), prevMouseLocation.getY(), mouseLocation.getX(), mouseLocation.getY());

                    // increasing or decreasing
                    strokeTime += strokeTimeDelay * pressureDirection;

                    // invert direction
                    if (strokeTime > strokeTimeMax) {
                        pressureDirection = -1;
                    }

                    // while still
                    if (strokeTime > 0) {

                        pressure += pressureDelay * pressureDirection;

                        // clamp value of pressure to be [0,1]
                        if (pressure > 1) {
                            pressure = 1;
                        } else if (pressure < 0) {
                            pressure = 0;
                        }

                    } else {

                        pressure = 0;

                    }

                } else {

                    pressure = 0;
                    pressureDirection = 1;
                    strokeTime = 0;

                }

                prevMouseLocation = new Point2D(mouseLocation.getX(), mouseLocation.getY());

            }
        };

        loop.start();

    }

    // https://de.wikipedia.org/wiki/Bresenham-Algorithmus
    private void bresenhamLine(double x0, double y0, double x1, double y1) {
        double dx = Math.abs(x1 - x0), sx = x0 < x1 ? 1. : -1.;
        double dy = -Math.abs(y1 - y0), sy = y0 < y1 ? 1. : -1.;
        double err = dx + dy, e2; /* error value e_xy */

        while (true) {

            int variation = (int) (pressure * (brushVariations.length - 1));
            Image brushVariation = brushVariations[variation];

            graphicsContext.setGlobalAlpha(pressure);
            graphicsContext.drawImage(brushVariation, x0 - brushVariation.getWidth() / 2.0, y0 - brushVariation.getHeight() / 2.0);

            if (x0 == x1 && y0 == y1)
                break;
            e2 = 2. * err;
            if (e2 > dy) {
                err += dy;
                x0 += sx;
            } /* e_xy+e_x > 0 */
            if (e2 < dx) {
                err += dx;
                y0 += sy;
            } /* e_xy+e_y < 0 */
        }
    }

    private void addListeners() {

        canvas.addEventFilter(MouseEvent.ANY, e -> {

            mouseLocation = new Point2D(e.getX(), e.getY());

            mousePressed = e.isPrimaryButtonDown();

        });

    }

    public static Image createImage(Node node) {

        WritableImage wi;

        SnapshotParameters parameters = new SnapshotParameters();
        parameters.setFill(Color.TRANSPARENT);

        int imageWidth = (int) node.getBoundsInLocal().getWidth();
        int imageHeight = (int) node.getBoundsInLocal().getHeight();

        wi = new WritableImage(imageWidth, imageHeight);
        node.snapshot(parameters, wi);

        return wi;

    }

    public static Image createBrush(double radius, Color color) {

        // create gradient image with given color
        Circle brush = new Circle(radius);

        RadialGradient gradient1 = new RadialGradient(0, 0, 0, 0, radius, false, CycleMethod.NO_CYCLE, new Stop(0, color.deriveColor(1, 1, 1, 0.3)), new Stop(1, color.deriveColor(1, 1, 1, 0)));

        brush.setFill(gradient1);

        // create image
        return createImage(brush);

    }

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

Voici à quoi cela ressemble:

entrez la description de l'image ici

Ou en utilisant différentes couleurs, j'ai ajouté un sélecteur de couleurs dans le dernier exemple:

entrez la description de l'image ici

 7
Author: Roland, 2015-08-12 06:24:25