From 41550b683f79d2e157c165c2b7b56e085d2d0715 Mon Sep 17 00:00:00 2001 From: Victor Date: Fri, 25 Aug 2017 10:40:01 +0300 Subject: [PATCH] Add property animations in timeline --- .../main/java/com/annimon/hotarufx/Main.java | 41 ++++++++++++---- .../annimon/hotarufx/visual/Composition.java | 16 +++---- .../hotarufx/visual/PropertyTimeline.java | 28 +++++++++++ .../com/annimon/hotarufx/visual/TimeLine.java | 29 ++++++----- .../hotarufx/visual/objects/CircleNode.java | 48 ++++++++++++++++--- .../hotarufx/visual/objects/ObjectNode.java | 11 +++-- .../visual/objects/PropertyConsumers.java | 21 ++++++++ .../hotarufx/visual/visitors/NodeVisitor.java | 8 ++++ .../visual/visitors/RenderVisitor.java | 19 ++++++++ .../annimon/hotarufx/visual/TimeLineTest.java | 3 +- 10 files changed, 182 insertions(+), 42 deletions(-) create mode 100644 app/src/main/java/com/annimon/hotarufx/visual/PropertyTimeline.java create mode 100644 app/src/main/java/com/annimon/hotarufx/visual/objects/PropertyConsumers.java create mode 100644 app/src/main/java/com/annimon/hotarufx/visual/visitors/NodeVisitor.java create mode 100644 app/src/main/java/com/annimon/hotarufx/visual/visitors/RenderVisitor.java diff --git a/app/src/main/java/com/annimon/hotarufx/Main.java b/app/src/main/java/com/annimon/hotarufx/Main.java index 54f7342..e96ebbc 100644 --- a/app/src/main/java/com/annimon/hotarufx/Main.java +++ b/app/src/main/java/com/annimon/hotarufx/Main.java @@ -3,12 +3,11 @@ package com.annimon.hotarufx; import com.annimon.hotarufx.visual.Composition; import com.annimon.hotarufx.visual.KeyFrame; import com.annimon.hotarufx.visual.objects.CircleNode; +import com.annimon.hotarufx.visual.visitors.RenderVisitor; import javafx.application.Application; import javafx.scene.paint.Color; import javafx.scene.paint.Paint; import javafx.stage.Stage; -import lombok.Data; -import lombok.AllArgsConstructor; import lombok.val; public class Main extends Application { @@ -16,21 +15,45 @@ public class Main extends Application { @Override public void start(Stage primaryStage) { val composition = new Composition(1280, 720, 30); - val scene = composition.newScene(KeyFrame.of(0)); + val scene = composition.getScene(); val colors = new Paint[] {Color.GREEN, Color.RED}; val halfWidth = scene.getVirtualWidth() / 2; val halfHeight = scene.getVirtualHeight() / 2; + val renderVisitor = new RenderVisitor(composition.getTimeline()); for (int y = -1; y <= 1; y++) { for (int x = -1; x <= 1; x++) { - val circle = new CircleNode(); - circle.getCircle().setFill(colors[Math.abs(x * y)]); - circle.getCircle().setCenterX(x * halfWidth); - circle.getCircle().setCenterY(y * halfHeight); - circle.getCircle().setRadius(50); - circle.render(scene); + val node = new CircleNode(); + node.circle.setFill(colors[Math.abs(x * y)]); + node.circle.setCenterX(x * halfWidth); + node.circle.setCenterY(y * halfHeight); + node.circle.setRadius(50); + node.radiusProperty() + .add(KeyFrame.of(30), 70) + .add(KeyFrame.of(90), 20) + .add(KeyFrame.of(300), 70); + if (x == 0 && y == 0) { + node.centerXProperty() + .add(KeyFrame.of(60), 0) + .add(KeyFrame.of(90), -400) + .add(KeyFrame.of(150), 400) + .add(KeyFrame.of(180), 0); + node.centerYProperty() + .add(KeyFrame.of(180), 0) + .add(KeyFrame.of(210), -400) + .add(KeyFrame.of(270), 400) + .add(KeyFrame.of(300), 0); + node.radiusProperty() + .add(KeyFrame.of(320), 180); + } + node.accept(renderVisitor, scene); } } + + primaryStage.setTitle("HotaruFX"); + primaryStage.setScene(composition.produceAnimationScene()); + composition.getTimeline().getFxTimeline().play(); + primaryStage.show(); } public static void main(String[] args) { diff --git a/app/src/main/java/com/annimon/hotarufx/visual/Composition.java b/app/src/main/java/com/annimon/hotarufx/visual/Composition.java index a13ea0c..ebaf5c3 100644 --- a/app/src/main/java/com/annimon/hotarufx/visual/Composition.java +++ b/app/src/main/java/com/annimon/hotarufx/visual/Composition.java @@ -16,33 +16,31 @@ public class Composition { private final double factor; @Getter - private final double frameRate; + private final TimeLine timeline; @Getter - private final TimeLine timeLine; + private final VirtualScene scene; public Composition(int sceneWidth, int sceneHeight, double frameRate) { this.sceneWidth = sceneWidth; this.sceneHeight = sceneHeight; - this.frameRate = frameRate; virtualHeight = 1080; factor = virtualHeight / (double) sceneHeight; virtualWidth = (int) (sceneWidth * factor); - timeLine = new TimeLine(); + timeline = new TimeLine(frameRate); + scene = newScene(); } - public VirtualScene newScene(KeyFrame keyFrame) { + private VirtualScene newScene() { val group = new Group(); group.setScaleX(1d / factor); group.setScaleY(1d / factor); group.setTranslateX(sceneWidth / 2); group.setTranslateY(sceneHeight / 2); - val scene = new VirtualScene(group, virtualWidth, virtualHeight); - timeLine.add(keyFrame, scene); - return scene; + return new VirtualScene(group, virtualWidth, virtualHeight); } - public Scene produceAnimationScene(VirtualScene scene) { + public Scene produceAnimationScene() { return new Scene(scene.getGroup(), sceneWidth, sceneHeight, Color.WHITE); } } diff --git a/app/src/main/java/com/annimon/hotarufx/visual/PropertyTimeline.java b/app/src/main/java/com/annimon/hotarufx/visual/PropertyTimeline.java new file mode 100644 index 0000000..d19015e --- /dev/null +++ b/app/src/main/java/com/annimon/hotarufx/visual/PropertyTimeline.java @@ -0,0 +1,28 @@ +package com.annimon.hotarufx.visual; + +import com.annimon.hotarufx.exceptions.KeyFrameDuplicationException; +import java.util.Map; +import java.util.TreeMap; +import javafx.beans.value.WritableValue; +import lombok.Getter; +import lombok.val; + +@Getter +public class PropertyTimeline { + + private final WritableValue property; + private final Map keyFrames; + + public PropertyTimeline(WritableValue property) { + this.property = property; + keyFrames = new TreeMap<>(); + } + + public PropertyTimeline add(KeyFrame keyFrame, T value) { + val previous = keyFrames.put(keyFrame, value); + if (previous != null) { + throw new KeyFrameDuplicationException(keyFrame); + } + return this; + } +} diff --git a/app/src/main/java/com/annimon/hotarufx/visual/TimeLine.java b/app/src/main/java/com/annimon/hotarufx/visual/TimeLine.java index faf1e15..2be0cb2 100644 --- a/app/src/main/java/com/annimon/hotarufx/visual/TimeLine.java +++ b/app/src/main/java/com/annimon/hotarufx/visual/TimeLine.java @@ -1,24 +1,29 @@ package com.annimon.hotarufx.visual; -import com.annimon.hotarufx.exceptions.KeyFrameDuplicationException; -import java.util.Map; -import java.util.TreeMap; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.util.Duration; import lombok.Getter; -import lombok.val; public class TimeLine { @Getter - private final Map keyFrames; + private final double frameRate; - public TimeLine() { - keyFrames = new TreeMap<>(); + @Getter + private final Timeline fxTimeline; + + public TimeLine(double frameRate) { + this.frameRate = frameRate; + fxTimeline = new Timeline(frameRate); } - public void add(KeyFrame keyFrame, VirtualScene scene) { - val previous = keyFrames.put(keyFrame, scene); - if (previous != null) { - throw new KeyFrameDuplicationException(keyFrame); - } + public void addKeyFrame(KeyFrame keyFrame, KeyValue fxKeyValue) { + fxTimeline.getKeyFrames().add(new javafx.animation.KeyFrame( + duration(keyFrame), fxKeyValue)); + } + + private Duration duration(KeyFrame keyFrame) { + return Duration.millis(1000d * keyFrame.getFrame() / frameRate); } } diff --git a/app/src/main/java/com/annimon/hotarufx/visual/objects/CircleNode.java b/app/src/main/java/com/annimon/hotarufx/visual/objects/CircleNode.java index 091e1e9..8e9ad38 100644 --- a/app/src/main/java/com/annimon/hotarufx/visual/objects/CircleNode.java +++ b/app/src/main/java/com/annimon/hotarufx/visual/objects/CircleNode.java @@ -1,20 +1,54 @@ package com.annimon.hotarufx.visual.objects; -import com.annimon.hotarufx.visual.VirtualScene; +import com.annimon.hotarufx.visual.PropertyTimeline; +import com.annimon.hotarufx.visual.TimeLine; +import com.annimon.hotarufx.visual.visitors.NodeVisitor; +import java.util.Optional; import javafx.scene.shape.Circle; -import lombok.Getter; -public class CircleNode implements ObjectNode { +public class CircleNode extends ObjectNode { - @Getter - private final Circle circle; + public final Circle circle; + + private Optional> centerX, centerY, radius; public CircleNode() { circle = new Circle(); + centerX = Optional.empty(); + centerY = Optional.empty(); + radius = Optional.empty(); + } + + public PropertyTimeline centerXProperty() { + if (!centerX.isPresent()) { + centerX = Optional.of(new PropertyTimeline<>(circle.centerXProperty())); + } + return centerX.get(); + } + + public PropertyTimeline centerYProperty() { + if (!centerY.isPresent()) { + centerY = Optional.of(new PropertyTimeline<>(circle.centerYProperty())); + } + return centerY.get(); + } + + public PropertyTimeline radiusProperty() { + if (!radius.isPresent()) { + radius = Optional.of(new PropertyTimeline<>(circle.radiusProperty())); + } + return radius.get(); + } + + public void buildTimeline(TimeLine timeline) { + super.buildTimeline(timeline); + centerX.ifPresent(PropertyConsumers.numberConsumer(timeline)); + centerY.ifPresent(PropertyConsumers.numberConsumer(timeline)); + radius.ifPresent(PropertyConsumers.numberConsumer(timeline)); } @Override - public void render(VirtualScene scene) { - scene.add(circle); + public R accept(NodeVisitor visitor, T input) { + return visitor.visit(this, input); } } diff --git a/app/src/main/java/com/annimon/hotarufx/visual/objects/ObjectNode.java b/app/src/main/java/com/annimon/hotarufx/visual/objects/ObjectNode.java index c82bbfd..44edc08 100644 --- a/app/src/main/java/com/annimon/hotarufx/visual/objects/ObjectNode.java +++ b/app/src/main/java/com/annimon/hotarufx/visual/objects/ObjectNode.java @@ -1,8 +1,13 @@ package com.annimon.hotarufx.visual.objects; -import com.annimon.hotarufx.visual.VirtualScene; +import com.annimon.hotarufx.visual.TimeLine; +import com.annimon.hotarufx.visual.visitors.NodeVisitor; -public interface ObjectNode { +public abstract class ObjectNode { - void render(VirtualScene scene); + public abstract R accept(NodeVisitor visitor, T input); + + public void buildTimeline(TimeLine timeline) { + + } } diff --git a/app/src/main/java/com/annimon/hotarufx/visual/objects/PropertyConsumers.java b/app/src/main/java/com/annimon/hotarufx/visual/objects/PropertyConsumers.java new file mode 100644 index 0000000..28ca402 --- /dev/null +++ b/app/src/main/java/com/annimon/hotarufx/visual/objects/PropertyConsumers.java @@ -0,0 +1,21 @@ +package com.annimon.hotarufx.visual.objects; + +import com.annimon.hotarufx.visual.PropertyTimeline; +import com.annimon.hotarufx.visual.TimeLine; +import java.util.function.Consumer; +import javafx.animation.KeyValue; + +public class PropertyConsumers { + + public static Consumer> numberConsumer(TimeLine timeline) { + return genericConsumer(timeline); + } + + public static Consumer> genericConsumer(TimeLine timeline) { + return t -> { + t.getKeyFrames().forEach((keyFrame, value) -> { + timeline.addKeyFrame(keyFrame, new KeyValue(t.getProperty(), value)); + }); + }; + } +} diff --git a/app/src/main/java/com/annimon/hotarufx/visual/visitors/NodeVisitor.java b/app/src/main/java/com/annimon/hotarufx/visual/visitors/NodeVisitor.java new file mode 100644 index 0000000..d576cbf --- /dev/null +++ b/app/src/main/java/com/annimon/hotarufx/visual/visitors/NodeVisitor.java @@ -0,0 +1,8 @@ +package com.annimon.hotarufx.visual.visitors; + +import com.annimon.hotarufx.visual.objects.CircleNode; + +public interface NodeVisitor { + + R visit(CircleNode node, T input); +} diff --git a/app/src/main/java/com/annimon/hotarufx/visual/visitors/RenderVisitor.java b/app/src/main/java/com/annimon/hotarufx/visual/visitors/RenderVisitor.java new file mode 100644 index 0000000..2cc9852 --- /dev/null +++ b/app/src/main/java/com/annimon/hotarufx/visual/visitors/RenderVisitor.java @@ -0,0 +1,19 @@ +package com.annimon.hotarufx.visual.visitors; + +import com.annimon.hotarufx.visual.TimeLine; +import com.annimon.hotarufx.visual.VirtualScene; +import com.annimon.hotarufx.visual.objects.CircleNode; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class RenderVisitor implements NodeVisitor { + + private final TimeLine timeline; + + @Override + public Void visit(CircleNode node, VirtualScene scene) { + node.buildTimeline(timeline); + scene.add(node.circle); + return null; + } +} diff --git a/app/src/test/java/com/annimon/hotarufx/visual/TimeLineTest.java b/app/src/test/java/com/annimon/hotarufx/visual/TimeLineTest.java index 104fa8d..44672e6 100644 --- a/app/src/test/java/com/annimon/hotarufx/visual/TimeLineTest.java +++ b/app/src/test/java/com/annimon/hotarufx/visual/TimeLineTest.java @@ -4,13 +4,12 @@ import lombok.val; import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; -import static org.junit.jupiter.api.Assertions.*; class TimeLineTest { @Test void add() { - val timeline = new TimeLine(); + val timeline = new PropertyTimeline(null); timeline.add(KeyFrame.of(20), null); timeline.add(KeyFrame.of(10), null); timeline.add(KeyFrame.of(0), null);