1
0
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:
Victor 2017-09-09 13:01:00 +03:00
parent 1f5ded0c40
commit b1408c5891
5 changed files with 164 additions and 12 deletions

View File

@ -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;

View File

@ -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));

View File

@ -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);
}
}

View File

@ -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;
}
} }

View File

@ -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"/>