mirror of
https://github.com/aNNiMON/HotaruFX.git
synced 2024-09-19 14:14:21 +03:00
Add ability to render animation
This commit is contained in:
parent
1f5ded0c40
commit
b1408c5891
@ -10,17 +10,19 @@ import com.annimon.hotarufx.parser.visitors.InterpreterVisitor;
|
|||||||
import com.annimon.hotarufx.visual.Composition;
|
import com.annimon.hotarufx.visual.Composition;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import javafx.scene.Scene;
|
||||||
import javafx.stage.Modality;
|
import javafx.stage.Modality;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import lombok.val;
|
import lombok.val;
|
||||||
|
|
||||||
public class Renderer {
|
public class RenderPreparer {
|
||||||
|
|
||||||
public static Renderer init() {
|
public static RenderPreparer init() {
|
||||||
return new Renderer();
|
return new RenderPreparer();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Renderer() {
|
private RenderPreparer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public WithInput input(String input) {
|
public WithInput input(String input) {
|
||||||
@ -58,24 +60,30 @@ public class Renderer {
|
|||||||
|
|
||||||
public Evaluated evaluateWithBundles(List<Class<? extends Bundle>> bundles) {
|
public Evaluated evaluateWithBundles(List<Class<? extends Bundle>> bundles) {
|
||||||
BundleLoader.load(context, bundles);
|
BundleLoader.load(context, bundles);
|
||||||
return evaluate();
|
evaluate();
|
||||||
|
return new Evaluated(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Evaluated evaluate() {
|
public EvaluatedForRender evaluateForRender() {
|
||||||
|
BundleLoader.load(context, BundleLoader.runtimeBundles());
|
||||||
|
evaluate();
|
||||||
|
return new EvaluatedForRender(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void evaluate() {
|
||||||
val parser = new HotaruParser(HotaruLexer.tokenize(source.input));
|
val parser = new HotaruParser(HotaruLexer.tokenize(source.input));
|
||||||
val program = parser.parse();
|
val program = parser.parse();
|
||||||
if (parser.getParseErrors().hasErrors()) {
|
if (parser.getParseErrors().hasErrors()) {
|
||||||
throw new RendererException(parser.getParseErrors().toString());
|
throw new RendererException(parser.getParseErrors().toString());
|
||||||
}
|
}
|
||||||
program.accept(new InterpreterVisitor(), context);
|
program.accept(new InterpreterVisitor(), context);
|
||||||
return new Evaluated(this);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public class Evaluated {
|
public class Evaluated {
|
||||||
|
|
||||||
private final WithContext source;
|
protected final WithContext source;
|
||||||
|
|
||||||
private Evaluated(WithContext source) {
|
private Evaluated(WithContext source) {
|
||||||
this.source = source;
|
this.source = source;
|
||||||
@ -87,10 +95,14 @@ public class Renderer {
|
|||||||
stage.initOwner(primaryStage);
|
stage.initOwner(primaryStage);
|
||||||
stage.initModality(Modality.WINDOW_MODAL);
|
stage.initModality(Modality.WINDOW_MODAL);
|
||||||
val composition = source.context.composition();
|
val composition = source.context.composition();
|
||||||
stage.setScene(composition.produceAnimationScene());
|
stage.setScene(sceneProvider().apply(composition));
|
||||||
return new WithStage(composition, stage);
|
return new WithStage(composition, stage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected Function<Composition, Scene> sceneProvider() {
|
||||||
|
return Composition::producePreviewScene;
|
||||||
|
}
|
||||||
|
|
||||||
private void checkCompositionExists() {
|
private void checkCompositionExists() {
|
||||||
if (source.context.composition() == null) {
|
if (source.context.composition() == null) {
|
||||||
throw new RendererException("There is no composition.\n" +
|
throw new RendererException("There is no composition.\n" +
|
||||||
@ -99,6 +111,18 @@ public class Renderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class EvaluatedForRender extends Evaluated {
|
||||||
|
|
||||||
|
private EvaluatedForRender(WithContext source) {
|
||||||
|
super(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Function<Composition, Scene> sceneProvider() {
|
||||||
|
return Composition::produceRendererScene;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class WithStage {
|
public class WithStage {
|
||||||
|
|
||||||
private final Composition composition;
|
private final Composition composition;
|
@ -8,7 +8,7 @@ import com.annimon.hotarufx.io.DocumentManager;
|
|||||||
import com.annimon.hotarufx.io.FileManager;
|
import com.annimon.hotarufx.io.FileManager;
|
||||||
import com.annimon.hotarufx.io.IOStream;
|
import com.annimon.hotarufx.io.IOStream;
|
||||||
import com.annimon.hotarufx.lib.Context;
|
import com.annimon.hotarufx.lib.Context;
|
||||||
import com.annimon.hotarufx.ui.Renderer;
|
import com.annimon.hotarufx.ui.RenderPreparer;
|
||||||
import com.annimon.hotarufx.ui.SyntaxHighlighter;
|
import com.annimon.hotarufx.ui.SyntaxHighlighter;
|
||||||
import com.annimon.hotarufx.ui.control.LibraryItem;
|
import com.annimon.hotarufx.ui.control.LibraryItem;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -30,6 +30,7 @@ import javafx.scene.control.CheckMenuItem;
|
|||||||
import javafx.scene.control.TextArea;
|
import javafx.scene.control.TextArea;
|
||||||
import javafx.scene.control.TitledPane;
|
import javafx.scene.control.TitledPane;
|
||||||
import javafx.scene.layout.Pane;
|
import javafx.scene.layout.Pane;
|
||||||
|
import javafx.stage.DirectoryChooser;
|
||||||
import javafx.stage.Modality;
|
import javafx.stage.Modality;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
@ -37,6 +38,7 @@ import lombok.val;
|
|||||||
import org.fxmisc.richtext.CodeArea;
|
import org.fxmisc.richtext.CodeArea;
|
||||||
import org.fxmisc.richtext.LineNumberFactory;
|
import org.fxmisc.richtext.LineNumberFactory;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
public class EditorController implements Initializable, DocumentListener {
|
public class EditorController implements Initializable, DocumentListener {
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
@ -128,7 +130,7 @@ public class EditorController implements Initializable, DocumentListener {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
Renderer.init()
|
RenderPreparer.init()
|
||||||
.input(input)
|
.input(input)
|
||||||
.context(new Context())
|
.context(new Context())
|
||||||
.evaluateWithRuntimeBundle()
|
.evaluateWithRuntimeBundle()
|
||||||
@ -157,6 +159,54 @@ public class EditorController implements Initializable, DocumentListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void handleMenuRender(ActionEvent event) {
|
||||||
|
log.setText("");
|
||||||
|
val input = editor.getText();
|
||||||
|
if (input.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
RenderPreparer.init()
|
||||||
|
.input(input)
|
||||||
|
.context(new Context())
|
||||||
|
.evaluateForRender()
|
||||||
|
.prepareStage(primaryStage)
|
||||||
|
.peek((stage, composition) -> {
|
||||||
|
val chooser = new DirectoryChooser();
|
||||||
|
chooser.setTitle("Choose directory for rendering frames");
|
||||||
|
val directory = chooser.showDialog(primaryStage);
|
||||||
|
if (directory == null || !directory.exists() || !directory.isDirectory()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
val fxTimeline = composition.getTimeline().getFxTimeline();
|
||||||
|
stage.setOnShown(e -> {
|
||||||
|
fxTimeline.playFromStart();
|
||||||
|
fxTimeline.pause();
|
||||||
|
val task = new RenderTask(directory, composition, stage.getScene());
|
||||||
|
task.messageProperty().addListener(ev -> {
|
||||||
|
stage.setTitle(task.getMessage());
|
||||||
|
});
|
||||||
|
task.setOnFailed(ev -> {
|
||||||
|
logError(Exceptions.stackTraceToString(ev.getSource().getException()));
|
||||||
|
logPane.setExpanded(true);
|
||||||
|
stage.close();
|
||||||
|
});
|
||||||
|
task.setOnSucceeded(ev -> stage.close());
|
||||||
|
new Thread(task).start();
|
||||||
|
});
|
||||||
|
stage.show();
|
||||||
|
});
|
||||||
|
} catch (RendererException re) {
|
||||||
|
logError(re.getMessage());
|
||||||
|
logPane.setExpanded(true);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
logError(Exceptions.stackTraceToString(e));
|
||||||
|
logPane.setExpanded(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize(URL location, ResourceBundle resources) {
|
public void initialize(URL location, ResourceBundle resources) {
|
||||||
editor.setParagraphGraphicFactory(LineNumberFactory.get(editor));
|
editor.setParagraphGraphicFactory(LineNumberFactory.get(editor));
|
||||||
|
@ -0,0 +1,71 @@
|
|||||||
|
package com.annimon.hotarufx.ui.controller;
|
||||||
|
|
||||||
|
import com.annimon.hotarufx.visual.Composition;
|
||||||
|
import com.annimon.hotarufx.visual.TimeLine;
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.concurrent.Task;
|
||||||
|
import javafx.embed.swing.SwingFXUtils;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.image.WritableImage;
|
||||||
|
import javafx.util.Duration;
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import lombok.val;
|
||||||
|
|
||||||
|
public class RenderTask extends Task<Boolean> {
|
||||||
|
|
||||||
|
private final File directory;
|
||||||
|
private final Composition composition;
|
||||||
|
private final Scene scene;
|
||||||
|
private final TimeLine timeLine;
|
||||||
|
private final double frameRate;
|
||||||
|
|
||||||
|
public RenderTask(File directory, Composition composition, Scene scene) {
|
||||||
|
this.directory = directory;
|
||||||
|
this.composition = composition;
|
||||||
|
this.scene = scene;
|
||||||
|
this.timeLine = composition.getTimeline();
|
||||||
|
frameRate = timeLine.getFrameRate();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Boolean call() throws Exception {
|
||||||
|
val fxTimeline = timeLine.getFxTimeline();
|
||||||
|
|
||||||
|
val totalFrames = toFrame(fxTimeline.getTotalDuration());
|
||||||
|
int frame = 0;
|
||||||
|
while (frame < totalFrames) {
|
||||||
|
updateProgress(frame, totalFrames);
|
||||||
|
updateMessage(String.format("%d / %d", frame + 1, totalFrames));
|
||||||
|
fxTimeline.jumpTo(toDuration(frame));
|
||||||
|
|
||||||
|
val image = newImage();
|
||||||
|
|
||||||
|
val latch = new CountDownLatch(1);
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
scene.snapshot(image);
|
||||||
|
latch.countDown();
|
||||||
|
});
|
||||||
|
latch.await();
|
||||||
|
|
||||||
|
val file = new File(directory, String.format("frame_%05d.png", frame + 1));
|
||||||
|
ImageIO.write(SwingFXUtils.fromFXImage(image, null), "png", file);
|
||||||
|
frame++;
|
||||||
|
}
|
||||||
|
return Boolean.TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private WritableImage newImage() {
|
||||||
|
return new WritableImage(composition.getSceneWidth(), composition.getSceneHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
private int toFrame(Duration d) {
|
||||||
|
return (int) (d.toMillis() * frameRate / 1000d);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Duration toDuration(int frame) {
|
||||||
|
return Duration.millis(frame * 1000d / frameRate);
|
||||||
|
}
|
||||||
|
}
|
@ -62,7 +62,7 @@ public class Composition {
|
|||||||
return new VirtualScene(group, virtualWidth, virtualHeight);
|
return new VirtualScene(group, virtualWidth, virtualHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Scene produceAnimationScene() {
|
public Scene producePreviewScene() {
|
||||||
val fxScene = new Scene(scene.getGroup(), sceneWidth, sceneHeight, background);
|
val fxScene = new Scene(scene.getGroup(), sceneWidth, sceneHeight, background);
|
||||||
fxScene.setOnKeyPressed(e -> {
|
fxScene.setOnKeyPressed(e -> {
|
||||||
switch (e.getCode()) {
|
switch (e.getCode()) {
|
||||||
@ -89,4 +89,10 @@ public class Composition {
|
|||||||
});
|
});
|
||||||
return fxScene;
|
return fxScene;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Scene produceRendererScene() {
|
||||||
|
val fxScene = new Scene(scene.getGroup(), sceneWidth, sceneHeight, background);
|
||||||
|
|
||||||
|
return fxScene;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,7 @@
|
|||||||
</Menu>
|
</Menu>
|
||||||
<Menu mnemonicParsing="false" text="Composition">
|
<Menu mnemonicParsing="false" text="Composition">
|
||||||
<MenuItem onAction="#handleMenuPlay" text="Preview" accelerator="F5"/>
|
<MenuItem onAction="#handleMenuPlay" text="Preview" accelerator="F5"/>
|
||||||
|
<MenuItem onAction="#handleMenuRender" text="Render"/>
|
||||||
</Menu>
|
</Menu>
|
||||||
<Menu mnemonicParsing="false" text="Help">
|
<Menu mnemonicParsing="false" text="Help">
|
||||||
<MenuItem onAction="#handleMenuAbout" text="About"/>
|
<MenuItem onAction="#handleMenuAbout" text="About"/>
|
||||||
|
Loading…
Reference in New Issue
Block a user