javaanimationjavafxbeziercubic-bezier

JavaFX animation to restore a Path original state


I'm a beginner in JavaFX, and I'm working on a project where I'm using four Bezier curves to draw a circle. My goal is to eventually achieve a "wobbly" effect by allowing the curves to be deformed when the user drags them. To accomplish this, I want to add a feature that restores the original state of the circle when the user releases the mouse click (EDIT) performing an animation that makes the curve move to its initial path, which I stored in the "initialPath" list.

While I have chosen to use JavaFX for this project, I am aware that it may not be the best tool for the job... I'm just trying to explore JavaFX's capabilities and would appreciate any guidance or tips on how to implement this functionality. Thank you for your help.

I tried working with Timeline and Keyframes' interpolation, I can't seem to find any other question on this exact topic and I'm finding it hard to grasp. This is the state of my code at the moment:

    import javafx.application.Application;
    import javafx.scene.Scene;
    import javafx.scene.input.MouseEvent;
    import javafx.scene.layout.Pane;
    import javafx.scene.paint.Color;
    import javafx.scene.shape.CubicCurveTo;
    import javafx.scene.shape.MoveTo;
    import javafx.scene.shape.Path;
    import javafx.scene.shape.PathElement;
    import javafx.stage.Stage;

    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.List;

    public class BezierCurves extends Application {

    private double dragStartX;
    private double dragStartY;
    private List<Path> initialPaths = new ArrayList<>();
    private Pane pane;


    public Path drawCurve(double[] moveTo, double[] coordinates, int color) {
        Path path = new Path();
        switch (color) {
            case 0 -> path.setStroke(Color.RED);
            case 1 -> path.setStroke(Color.BLUE);
            case 2 -> path.setStroke(Color.GREEN);
            case 3 -> path.setStroke(Color.ORANGE);
            default -> path.setStroke(Color.BLACK);
        }
        path.setStrokeWidth(2);
        path.setFill(null);
        MoveTo moveTo1 = new MoveTo(moveTo[0], moveTo[1]);
        CubicCurveTo curveTo1 = new CubicCurveTo(coordinates[0],
                coordinates[1],
                coordinates[2],
                coordinates[3],
                coordinates[4],
                coordinates[5]);
        path.getElements().addAll(moveTo1, curveTo1);
        return path;
    }

    @Override
    public void start(Stage primaryStage) {
        pane = new Pane();
        Scene scene = new Scene(pane, 400, 400);
        double centerX = 200, centerY = 200, radius = 100, kappa = 0.5522848;

        Path path1 = drawCurve(new double[]{centerX, centerY - radius},
                new double[]{centerX + radius * kappa,
                        centerY - radius,
                        centerX + radius,
                        centerY - radius * kappa,
                        centerX + radius, centerY}, 0);
        path1.setOnMousePressed(this::handleMousePressed);
        path1.setOnMouseDragged(this::handleMouseDragged);
        path1.setOnMouseReleased(this::handleMouseReleased);

        Path path2 = drawCurve(new double[]{centerX + radius, centerY},
                new double[]{centerX + radius,
                        centerY + radius * kappa,
                        centerX + radius * kappa,
                        centerY + radius, centerX,
                        centerY + radius}, 1);
        path2.setOnMousePressed(this::handleMousePressed);
        path2.setOnMouseDragged(this::handleMouseDragged);
        path2.setOnMouseReleased(this::handleMouseReleased);

        Path path3 = drawCurve(new double[]{centerX, centerY + radius},
                new double[]{centerX - radius * kappa,
                        centerY + radius,
                        centerX - radius,
                        centerY + radius * kappa,
                        centerX - radius, centerY}, 2);
        path3.setOnMousePressed(this::handleMousePressed);
        path3.setOnMouseDragged(this::handleMouseDragged);
        path3.setOnMouseReleased(this::handleMouseReleased);

        Path path4 = drawCurve(new double[]{centerX - radius, centerY},
                new double[]{centerX - radius,
                        centerY - radius * kappa,
                        centerX - radius * kappa,
                        centerY - radius,
                        centerX,
                        centerY - radius}, 3);
        path4.setOnMousePressed(this::handleMousePressed);
        path4.setOnMouseDragged(this::handleMouseDragged);
        path4.setOnMouseReleased(this::handleMouseReleased);

        initialPaths.addAll(Arrays.asList(path1, path2, path3, path4));
        pane.getChildren().addAll(path1, path2, path3, path4);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private void handleMousePressed(MouseEvent event) {
        dragStartX = event.getX();
        dragStartY = event.getY();
    }

    private void handleMouseDragged(MouseEvent event) {
        double offsetX = event.getX() - dragStartX;
        double offsetY = event.getY() - dragStartY;
       
        Path path = (Path) event.getSource();

        for (PathElement element : path.getElements()) {
            if (element instanceof CubicCurveTo curve) {
                curve.setControlX1(curve.getControlX1() + offsetX * 0.02);
                curve.setControlY1(curve.getControlY1() + offsetY * 0.02);
                curve.setControlX2(curve.getControlX2() + offsetX * 0.02);
                curve.setControlY2(curve.getControlY2() + offsetY * 0.02);
            }
        }
    }

    private void handleMouseReleased(MouseEvent event) {
            // TODO
        }

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

    }

Solution

  • For this requirement, all you need to do is to store the initial values of controlX1,Y1,X2,Y2 and reset the curves control properties to the initial values using a Timeline.

    Below is the output, if you add the below changes to your code:

    enter image description here

    private double x1, y1, x2, y2;
    
    private void handleMousePressed(MouseEvent event) {
        dragStartX = event.getX();
        dragStartY = event.getY();
        Path path = (Path) event.getSource();
        for (PathElement element : path.getElements()) {
            if (element instanceof CubicCurveTo curve) {
                x1 = curve.getControlX1();
                y1 = curve.getControlY1();
                x2 = curve.getControlX2();
                y2 = curve.getControlY2();
            }
        }
    }
    
    private void handleMouseReleased(MouseEvent event) {
        Path path = (Path) event.getSource();
        for (PathElement element : path.getElements()) {
            if (element instanceof CubicCurveTo curve) {
                // Create key values to reset the properties to the initial state
                KeyValue kx1 = new KeyValue(curve.controlX1Property(), x1);
                KeyValue ky1 = new KeyValue(curve.controlY1Property(), y1);
                KeyValue kx2 = new KeyValue(curve.controlX2Property(), x2);
                KeyValue ky2 = new KeyValue(curve.controlY2Property(), y2);
                // Change the speed as per your needs
                Duration speed = Duration.millis(300);
                Timeline tl = new Timeline(new KeyFrame(speed, kx1, ky1, kx2, ky2));
                tl.play();
            }
        }
    }