Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Explore Animation Observables #34

Closed
thomasnield opened this issue Jun 23, 2016 · 5 comments
Closed

Explore Animation Observables #34

thomasnield opened this issue Jun 23, 2016 · 5 comments

Comments

@thomasnield
Copy link
Collaborator

thomasnield commented Jun 23, 2016

I'll need to do some exploring and see if RxJavaFX can support animation a little better.

http://www.java2s.com/Tutorials/Java/JavaFX/1010__JavaFX_Timeline_Animation.htm

@thomasnield
Copy link
Collaborator Author

thomasnield commented Jun 24, 2016

Here is some messing around I did with RxKotlinFX and TornadoFX. This creates a Rectangle and rotates it incrementally faster every second. I think there are some cool things we can do here, but some thoughts:

  1. It might be worthwhile creating an FX-equivalent to Observable.interval() that emits on the FX thread possibly

  2. There has got to be a way to streamline the creation of KeyValue, KeyFrame and Timeline in the Subscriber.

class MyView: View() {

    override val root = VBox()

    init {
        with(root) {

            val rectangle = Rectangle(50.0, 50.0, 50.0, 50.0).apply {
                padding = Insets(20.0)
            }

            this += rectangle

            JavaFxObservable.interval(Duration.millis(10000))
                .map { it * 1.0 }
                .scan { x, y -> x + y  }
                .subscribe {
                    val timeline = Timeline()
                    val keyValue = KeyValue(rectangle.rotateProperty(), it)
                    val keyFrame = KeyFrame(Duration.millis(500.0), keyValue)
                    timeline.cycleCount = 1
                    timeline.keyFrames += keyFrame
                    timeline.play()
                }

        }
    }
}

animation

@thomasnield
Copy link
Collaborator Author

I just added a JavaFxObservable.interval() factory to emit an incremental Long at a specified JavaFX Duration. This is pretty much identical to Observable.interval() but it occurs on the JavaFX thread through a Timeline.

 JavaFxObservable.interval(Duration.millis(1000))
     .subscribe(i -> System.out.println(i));

@thomasnield
Copy link
Collaborator Author

thomasnield commented Jun 24, 2016

Haha, oh my gosh this is fun. Using some creative Observable composition, as soon as the rotation hits 90 degrees I can have it "snap back" to 0 degrees and be done!

Observable.concat(JavaFxObservable.interval(Duration.millis(10000))
                .map { it * 1.0 }
                .scan { x, y -> x + y  }
                .takeWhile { it < 90.0 }, Observable.just(0.0))
                .subscribeAnimation(rectangle.rotateProperty().asObject(), Duration.seconds(1.0), Interpolator.EASE_OUT)

animation

I can even compose it to do this routine three times.

  Observable.range(0, 2)
            .concatMap {
                Observable.concat(JavaFxObservable.interval(Duration.millis(10000))
                        .map { it * 1.0 }
                        .scan { x, y -> x + y }
                        .takeWhile { it < 90.0 }, Observable.just(0.0))
            }.subscribeAnimation(rectangle.rotateProperty().asObject(), Duration.seconds(1.0), Interpolator.EASE_OUT)

@thomasnield thomasnield changed the title Explore Timeline Observables Explore Animation Observables Jun 24, 2016
@thomasnield
Copy link
Collaborator Author

thomasnield commented Jun 26, 2016

Alright, here is a complete working concept written in Java. I don't know if the interval() factory even needs to execute on the JavaFX thread via JavaFxObservable.interval(). It seems to work using Observable.interval() although not sure if it is jeopardziing thread safety. Regardless, the former emulates UI input events effectively that can drive animation.

In this example, it will keep rotating the square at increasing speed until it hits 90 degrees rotation, then it "snaps back" to 0. Then it rotates -180 degrees in the opposite direction and snaps back again. What's cool here though is the switchMap() can be leveraged to cancel the entire sequence and transition it back to 0. The Button alternates between starting and resetting the operation.

1

import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
import rx.Observable;
import rx.observables.JavaFxObservable;

import java.util.concurrent.atomic.AtomicBoolean;

public final class JavaFxApp extends Application {

    private final Rectangle rectangle = new Rectangle(60,60,60,60);
    private final Button startStopButton = new Button("Start/Stop");
    private final AtomicBoolean isWorking = new AtomicBoolean(false);

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

        BorderPane root = new BorderPane();

        root.setCenter(rectangle);
        root.setBottom(startStopButton);

        JavaFxObservable.fromActionEvents(startStopButton)
                .map(ae -> isWorking.getAndSet(!isWorking.get()))
                .switchMap(b -> {
                    if (b) {
                        return Observable.just(0);
                    } else {
                        return Observable.concat(
                                JavaFxObservable.interval(Duration.millis(200))
                                        .map(i -> i * 2)
                                        .takeWhile(i -> rectangle.getRotate() <= 90),

                                Observable.just(0),  //reset

                                JavaFxObservable.interval(Duration.millis(200))
                                        .map(i -> i * -3)
                                        .takeWhile(i -> rectangle.getRotate() >= -180),

                                Observable.just(0) //reset
                        ).doOnCompleted(() -> isWorking.set(false));
                    }
                }).subscribe(i -> {
                    Timeline timeline = new Timeline();

                    KeyValue keyValue = new KeyValue(rectangle.rotateProperty(), i, Interpolator.EASE_OUT);
                    KeyFrame keyFrame = new KeyFrame(Duration.seconds(1), keyValue);
                    timeline.getKeyFrames().add(keyFrame);

                    timeline.play();
                });

        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

}

I don't think any additional code other than the JavaFxObservable.interval() is necessary to accomplish this. For RxKotlinFX, I may follow the same pattern but leverage some extensions I submitted in TornadoFX.

@thomasnield
Copy link
Collaborator Author

Okay, I think I'm ready to close this issue and do a release later. Another application, throttling the sequence with Button clicks...

2

import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
import rx.Observable;
import rx.observables.JavaFxObservable;

public final class JavaFxApp extends Application {

    private final Rectangle rectangle = new Rectangle(60,60,60,60);
    private final Button nextButton = new Button("Next");
    private final Label rotationLabel = new Label("0");

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

        BorderPane root = new BorderPane();

        root.setCenter(rectangle);
        root.setLeft(nextButton);
        root.setBottom(rotationLabel);

        Observable<ActionEvent> buttonClicks = JavaFxObservable.fromActionEvents(nextButton);

        Observable<Integer> sequence = Observable.concat(
                Observable.range(1,1000)
                        .map(i -> i * 10)
                        .zipWith(buttonClicks, (i,ae) -> i)
                        .takeWhile(i -> rectangle.getRotate() <= 90),

                Observable.just(0).zipWith(buttonClicks, (i,ae) -> i),  //reset

                Observable.range(1,1000)
                        .map(i -> i * -20)
                        .zipWith(buttonClicks, (i,ae) -> i)
                        .takeWhile(i -> rectangle.getRotate() >= -180),

                Observable.just(0).zipWith(buttonClicks, (i,ae) -> i) //reset
        );

        sequence.subscribe(i -> {
                    Timeline timeline = new Timeline();

                    KeyValue keyValue = new KeyValue(rectangle.rotateProperty(), i, Interpolator.EASE_OUT);
                    KeyFrame keyFrame = new KeyFrame(Duration.seconds(1), keyValue);
                    timeline.getKeyFrames().add(keyFrame);

                    timeline.play();
                });

        JavaFxObservable.fromObservableValue(rectangle.rotateProperty())
                .map(Object::toString)
                .subscribe(rotationLabel::setText);

        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant