mirror of
https://github.com/aNNiMON/HotaruFX.git
synced 2024-09-19 14:14:21 +03:00
Add property animations in timeline
This commit is contained in:
parent
d9001bc106
commit
41550b683f
@ -3,12 +3,11 @@ package com.annimon.hotarufx;
|
|||||||
import com.annimon.hotarufx.visual.Composition;
|
import com.annimon.hotarufx.visual.Composition;
|
||||||
import com.annimon.hotarufx.visual.KeyFrame;
|
import com.annimon.hotarufx.visual.KeyFrame;
|
||||||
import com.annimon.hotarufx.visual.objects.CircleNode;
|
import com.annimon.hotarufx.visual.objects.CircleNode;
|
||||||
|
import com.annimon.hotarufx.visual.visitors.RenderVisitor;
|
||||||
import javafx.application.Application;
|
import javafx.application.Application;
|
||||||
import javafx.scene.paint.Color;
|
import javafx.scene.paint.Color;
|
||||||
import javafx.scene.paint.Paint;
|
import javafx.scene.paint.Paint;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import lombok.Data;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.val;
|
import lombok.val;
|
||||||
|
|
||||||
public class Main extends Application {
|
public class Main extends Application {
|
||||||
@ -16,21 +15,45 @@ public class Main extends Application {
|
|||||||
@Override
|
@Override
|
||||||
public void start(Stage primaryStage) {
|
public void start(Stage primaryStage) {
|
||||||
val composition = new Composition(1280, 720, 30);
|
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 colors = new Paint[] {Color.GREEN, Color.RED};
|
||||||
val halfWidth = scene.getVirtualWidth() / 2;
|
val halfWidth = scene.getVirtualWidth() / 2;
|
||||||
val halfHeight = scene.getVirtualHeight() / 2;
|
val halfHeight = scene.getVirtualHeight() / 2;
|
||||||
|
val renderVisitor = new RenderVisitor(composition.getTimeline());
|
||||||
for (int y = -1; y <= 1; y++) {
|
for (int y = -1; y <= 1; y++) {
|
||||||
for (int x = -1; x <= 1; x++) {
|
for (int x = -1; x <= 1; x++) {
|
||||||
val circle = new CircleNode();
|
val node = new CircleNode();
|
||||||
circle.getCircle().setFill(colors[Math.abs(x * y)]);
|
node.circle.setFill(colors[Math.abs(x * y)]);
|
||||||
circle.getCircle().setCenterX(x * halfWidth);
|
node.circle.setCenterX(x * halfWidth);
|
||||||
circle.getCircle().setCenterY(y * halfHeight);
|
node.circle.setCenterY(y * halfHeight);
|
||||||
circle.getCircle().setRadius(50);
|
node.circle.setRadius(50);
|
||||||
circle.render(scene);
|
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) {
|
public static void main(String[] args) {
|
||||||
|
@ -16,33 +16,31 @@ public class Composition {
|
|||||||
private final double factor;
|
private final double factor;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
private final double frameRate;
|
private final TimeLine timeline;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
private final TimeLine timeLine;
|
private final VirtualScene scene;
|
||||||
|
|
||||||
public Composition(int sceneWidth, int sceneHeight, double frameRate) {
|
public Composition(int sceneWidth, int sceneHeight, double frameRate) {
|
||||||
this.sceneWidth = sceneWidth;
|
this.sceneWidth = sceneWidth;
|
||||||
this.sceneHeight = sceneHeight;
|
this.sceneHeight = sceneHeight;
|
||||||
this.frameRate = frameRate;
|
|
||||||
virtualHeight = 1080;
|
virtualHeight = 1080;
|
||||||
factor = virtualHeight / (double) sceneHeight;
|
factor = virtualHeight / (double) sceneHeight;
|
||||||
virtualWidth = (int) (sceneWidth * factor);
|
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();
|
val group = new Group();
|
||||||
group.setScaleX(1d / factor);
|
group.setScaleX(1d / factor);
|
||||||
group.setScaleY(1d / factor);
|
group.setScaleY(1d / factor);
|
||||||
group.setTranslateX(sceneWidth / 2);
|
group.setTranslateX(sceneWidth / 2);
|
||||||
group.setTranslateY(sceneHeight / 2);
|
group.setTranslateY(sceneHeight / 2);
|
||||||
val scene = new VirtualScene(group, virtualWidth, virtualHeight);
|
return new VirtualScene(group, virtualWidth, virtualHeight);
|
||||||
timeLine.add(keyFrame, scene);
|
|
||||||
return scene;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Scene produceAnimationScene(VirtualScene scene) {
|
public Scene produceAnimationScene() {
|
||||||
return new Scene(scene.getGroup(), sceneWidth, sceneHeight, Color.WHITE);
|
return new Scene(scene.getGroup(), sceneWidth, sceneHeight, Color.WHITE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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<T> {
|
||||||
|
|
||||||
|
private final WritableValue<T> property;
|
||||||
|
private final Map<KeyFrame, T> keyFrames;
|
||||||
|
|
||||||
|
public PropertyTimeline(WritableValue<T> property) {
|
||||||
|
this.property = property;
|
||||||
|
keyFrames = new TreeMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public PropertyTimeline<T> add(KeyFrame keyFrame, T value) {
|
||||||
|
val previous = keyFrames.put(keyFrame, value);
|
||||||
|
if (previous != null) {
|
||||||
|
throw new KeyFrameDuplicationException(keyFrame);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
@ -1,24 +1,29 @@
|
|||||||
package com.annimon.hotarufx.visual;
|
package com.annimon.hotarufx.visual;
|
||||||
|
|
||||||
import com.annimon.hotarufx.exceptions.KeyFrameDuplicationException;
|
import javafx.animation.KeyValue;
|
||||||
import java.util.Map;
|
import javafx.animation.Timeline;
|
||||||
import java.util.TreeMap;
|
import javafx.util.Duration;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.val;
|
|
||||||
|
|
||||||
public class TimeLine {
|
public class TimeLine {
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
private final Map<KeyFrame, VirtualScene> keyFrames;
|
private final double frameRate;
|
||||||
|
|
||||||
public TimeLine() {
|
@Getter
|
||||||
keyFrames = new TreeMap<>();
|
private final Timeline fxTimeline;
|
||||||
|
|
||||||
|
public TimeLine(double frameRate) {
|
||||||
|
this.frameRate = frameRate;
|
||||||
|
fxTimeline = new Timeline(frameRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void add(KeyFrame keyFrame, VirtualScene scene) {
|
public void addKeyFrame(KeyFrame keyFrame, KeyValue fxKeyValue) {
|
||||||
val previous = keyFrames.put(keyFrame, scene);
|
fxTimeline.getKeyFrames().add(new javafx.animation.KeyFrame(
|
||||||
if (previous != null) {
|
duration(keyFrame), fxKeyValue));
|
||||||
throw new KeyFrameDuplicationException(keyFrame);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Duration duration(KeyFrame keyFrame) {
|
||||||
|
return Duration.millis(1000d * keyFrame.getFrame() / frameRate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,54 @@
|
|||||||
package com.annimon.hotarufx.visual.objects;
|
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 javafx.scene.shape.Circle;
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
public class CircleNode implements ObjectNode {
|
public class CircleNode extends ObjectNode {
|
||||||
|
|
||||||
@Getter
|
public final Circle circle;
|
||||||
private final Circle circle;
|
|
||||||
|
private Optional<PropertyTimeline<Number>> centerX, centerY, radius;
|
||||||
|
|
||||||
public CircleNode() {
|
public CircleNode() {
|
||||||
circle = new Circle();
|
circle = new Circle();
|
||||||
|
centerX = Optional.empty();
|
||||||
|
centerY = Optional.empty();
|
||||||
|
radius = Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public PropertyTimeline<Number> centerXProperty() {
|
||||||
|
if (!centerX.isPresent()) {
|
||||||
|
centerX = Optional.of(new PropertyTimeline<>(circle.centerXProperty()));
|
||||||
|
}
|
||||||
|
return centerX.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public PropertyTimeline<Number> centerYProperty() {
|
||||||
|
if (!centerY.isPresent()) {
|
||||||
|
centerY = Optional.of(new PropertyTimeline<>(circle.centerYProperty()));
|
||||||
|
}
|
||||||
|
return centerY.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public PropertyTimeline<Number> 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
|
@Override
|
||||||
public void render(VirtualScene scene) {
|
public <R, T> R accept(NodeVisitor<R, T> visitor, T input) {
|
||||||
scene.add(circle);
|
return visitor.visit(this, input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
package com.annimon.hotarufx.visual.objects;
|
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, T> R accept(NodeVisitor<R, T> visitor, T input);
|
||||||
|
|
||||||
|
public void buildTimeline(TimeLine timeline) {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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<PropertyTimeline<Number>> numberConsumer(TimeLine timeline) {
|
||||||
|
return genericConsumer(timeline);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> Consumer<PropertyTimeline<T>> genericConsumer(TimeLine timeline) {
|
||||||
|
return t -> {
|
||||||
|
t.getKeyFrames().forEach((keyFrame, value) -> {
|
||||||
|
timeline.addKeyFrame(keyFrame, new KeyValue(t.getProperty(), value));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package com.annimon.hotarufx.visual.visitors;
|
||||||
|
|
||||||
|
import com.annimon.hotarufx.visual.objects.CircleNode;
|
||||||
|
|
||||||
|
public interface NodeVisitor<R, T> {
|
||||||
|
|
||||||
|
R visit(CircleNode node, T input);
|
||||||
|
}
|
@ -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<Void, VirtualScene> {
|
||||||
|
|
||||||
|
private final TimeLine timeline;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Void visit(CircleNode node, VirtualScene scene) {
|
||||||
|
node.buildTimeline(timeline);
|
||||||
|
scene.add(node.circle);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -4,13 +4,12 @@ import lombok.val;
|
|||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.hamcrest.Matchers.contains;
|
import static org.hamcrest.Matchers.contains;
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
|
|
||||||
class TimeLineTest {
|
class TimeLineTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void add() {
|
void add() {
|
||||||
val timeline = new TimeLine();
|
val timeline = new PropertyTimeline<String>(null);
|
||||||
timeline.add(KeyFrame.of(20), null);
|
timeline.add(KeyFrame.of(20), null);
|
||||||
timeline.add(KeyFrame.of(10), null);
|
timeline.add(KeyFrame.of(10), null);
|
||||||
timeline.add(KeyFrame.of(0), null);
|
timeline.add(KeyFrame.of(0), null);
|
||||||
|
Loading…
Reference in New Issue
Block a user