kopia lustrzana https://github.com/jameshball/osci-render
commit
d0f0c19e64
61
README.md
61
README.md
|
@ -6,7 +6,7 @@ Program for drawing objects, text, and images on an oscilloscope using audio out
|
|||
|
||||
This allows for 3D rendering of `.obj` files, `.svg` images, and `.txt` files.
|
||||
|
||||
Lots of this code was built as part of a 24hr hackathon: IC Hack 20. The original repository can be found here: https://github.com/wdhg/ICHack20 It won 'Best Newcomers Prize' at the event.
|
||||
Some of this was built as part of a 24hr hackathon: IC Hack 20. The original repository can be found here: https://github.com/wdhg/ICHack20 It won 'Best Newcomers Prize' at the event.
|
||||
|
||||
### Video Demonstration
|
||||
|
||||
|
@ -18,9 +18,9 @@ Lots of this code was built as part of a 24hr hackathon: IC Hack 20. The origina
|
|||
- Render `.svg` files
|
||||
- Render text
|
||||
- Rotation of objects
|
||||
- Scaling image
|
||||
- Translating image
|
||||
- GUI for controlling renderer
|
||||
- Scaling images
|
||||
- Translating images
|
||||
- Applying image effects
|
||||
|
||||
## Proposed Features
|
||||
|
||||
|
@ -34,7 +34,7 @@ Lots of this code was built as part of a 24hr hackathon: IC Hack 20. The origina
|
|||
|
||||
Using osci-render is very easy; run the program and choose the file you would like to render, and it will output as audio to visualise on your oscilloscope.
|
||||
|
||||
By default, the program loads the `cube.obj` example. If this is working, you're good to go and should be able to load your own objects, files, or images!
|
||||
By default, the program loads the example rotating cube. If this is working, you're good to go and should be able to load your own objects, files, or images!
|
||||
|
||||
Control the output using the sliders and text boxes provided. Currently the following can be controlled:
|
||||
|
||||
|
@ -48,9 +48,58 @@ There are some additional controls for `.obj` files:
|
|||
- Focal length of camera
|
||||
- Position of camera
|
||||
|
||||
## Screenshots
|
||||
|
||||
<img width="250px" height="396px" src="gui.png">
|
||||
|
||||
## Running
|
||||
|
||||
Head over to [Releases](https://github.com/jameshball/osci-render/releases) and download the latest `.exe` or `.jar`.
|
||||
|
||||
`.exe` is highly recommended if you're on Windows as it is much simpler to get up and running.
|
||||
|
||||
### Running using .exe
|
||||
|
||||
- Download the latest `osci-render-VERSION.exe` from [Releases](https://github.com/jameshball/osci-render/releases)
|
||||
- Open the `.exe` skipping any Windows security warnings (at your own risk!)
|
||||
- It should open briefly and then close without any user input
|
||||
- Check your start menu for `osci-render` or open `osci-render.exe` at `C:\Program Files\osci-render`
|
||||
- Start rendering!
|
||||
|
||||
Updating to later versions is as simple as running the latest `osci-render-VERSION.exe` again.
|
||||
|
||||
To uninstall, use Windows control panel, as you would expect.
|
||||
|
||||
### Running using .jar
|
||||
|
||||
- Download the latest `osci-render-VERSION.jar` from [Releases](https://github.com/jameshball/osci-render/releases)
|
||||
- Download and install [Java 15 or later](https://www.oracle.com/java/technologies/javase-jdk16-downloads.html)
|
||||
- Donwload and unpack [JavaFX 16 or later](https://gluonhq.com/products/javafx/)
|
||||
- Make sure you scroll down to `Latest Release`
|
||||
- Download the SDK for your platform
|
||||
- Unpack the `.zip` at your root directory (e.g. `C:\javafx-sdk-16` for me on Windows)
|
||||
- The `lib` subfolder should be located at `/javafx-sdk-16/lib`
|
||||
- Run the following command from your terminal to run the `.jar`, substituting the correct paths
|
||||
- `java --enable-preview --module-path /javafx-sdk-16/lib --add-modules=javafx.controls,javafx.fxml -jar "path/to/osci-render-VERSION.jar"`
|
||||
- Start rendering!
|
||||
|
||||
## Building
|
||||
|
||||
All dependencies are specified in the `pom.xml` file. Cloning this repo and using IntelliJ with Maven should make building a painless process.
|
||||
I am using Maven for dependency management and to package the program. Doing the following will setup the project. I highly recommend using IntelliJ.
|
||||
|
||||
- Download and install [Java 15 or later](https://www.oracle.com/java/technologies/javase-jdk16-downloads.html)
|
||||
- Run `git clone git@github.com:jameshball/osci-render.git`
|
||||
- `cd` into the `osci-render` directory
|
||||
- Run `mvn package`
|
||||
- Open the project in IntelliJ
|
||||
- Add `target/modules` as a library
|
||||
- Right click the folder in IntelliJ and `Add as Library...`
|
||||
- Select `osci-render` in the `Add to module` dropdown
|
||||
- Navigate to `File -> Project Structure`
|
||||
- Remove all external Maven libraries other than the `modules` folder just added
|
||||
- You're good to go!
|
||||
|
||||
You should now be able to run `sh.ball.gui.Gui` and start the program 😊
|
||||
|
||||
## Contact
|
||||
|
||||
|
|
Plik binarny nie jest wyświetlany.
Po Szerokość: | Wysokość: | Rozmiar: 28 KiB |
2
pom.xml
2
pom.xml
|
@ -6,7 +6,7 @@
|
|||
|
||||
<groupId>sh.ball</groupId>
|
||||
<artifactId>osci-render</artifactId>
|
||||
<version>1.1.1</version>
|
||||
<version>1.1.2</version>
|
||||
|
||||
<name>osci-render</name>
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package sh.ball.audio;
|
||||
|
||||
import sh.ball.audio.effect.Effect;
|
||||
import xt.audio.*;
|
||||
import xt.audio.Enums.XtSample;
|
||||
import xt.audio.Enums.XtSetup;
|
||||
import xt.audio.Enums.XtSystem;
|
||||
|
@ -11,12 +12,6 @@ import xt.audio.Structs.XtDeviceStreamParams;
|
|||
import xt.audio.Structs.XtFormat;
|
||||
import xt.audio.Structs.XtMix;
|
||||
import xt.audio.Structs.XtStreamParams;
|
||||
import xt.audio.XtAudio;
|
||||
import xt.audio.XtDevice;
|
||||
import xt.audio.XtPlatform;
|
||||
import xt.audio.XtSafeBuffer;
|
||||
import xt.audio.XtService;
|
||||
import xt.audio.XtStream;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
@ -30,38 +25,24 @@ import java.util.List;
|
|||
|
||||
public class AudioPlayer implements Renderer<List<Shape>> {
|
||||
|
||||
private static final int SAMPLE_RATE = 192000;
|
||||
private static final int BUFFER_SIZE = 20;
|
||||
|
||||
private final XtMix MIX = new XtMix(SAMPLE_RATE, XtSample.FLOAT32);
|
||||
private final XtChannels CHANNELS = new XtChannels(0, 0, 2, 0);
|
||||
private final XtFormat FORMAT = new XtFormat(MIX, CHANNELS);
|
||||
private final XtFormat format;
|
||||
private final BlockingQueue<List<Shape>> frameQueue = new ArrayBlockingQueue<>(BUFFER_SIZE);
|
||||
private final Map<Object, Effect> effects = new HashMap<>();
|
||||
|
||||
private Map<Object, Effect> effects = new HashMap<>();
|
||||
private List<Shape> frame;
|
||||
private int currentShape = 0;
|
||||
private int audioFramesDrawn = 0;
|
||||
|
||||
private double translateSpeed = 0;
|
||||
private Vector2 translateVector = new Vector2();
|
||||
private final Phase translatePhase = new Phase();
|
||||
private double rotateSpeed = 0;
|
||||
private final Phase rotatePhase = new Phase();
|
||||
private double scale = 1;
|
||||
private double weight = Shape.DEFAULT_WEIGHT;
|
||||
|
||||
private volatile boolean stopped;
|
||||
|
||||
public AudioPlayer() {
|
||||
}
|
||||
|
||||
public AudioPlayer(double rotateSpeed, double translateSpeed, Vector2 translateVector, double scale, double weight) {
|
||||
setRotationSpeed(rotateSpeed);
|
||||
setTranslationSpeed(translateSpeed);
|
||||
setTranslation(translateVector);
|
||||
setScale(scale);
|
||||
setQuality(weight);
|
||||
public AudioPlayer(int sampleRate) {
|
||||
XtMix mix = new XtMix(sampleRate, XtSample.FLOAT32);
|
||||
XtChannels channels = new XtChannels(0, 0, 2, 0);
|
||||
this.format = new XtFormat(mix, channels);
|
||||
}
|
||||
|
||||
private int render(XtStream stream, XtBuffer buffer, Object user) throws InterruptedException {
|
||||
|
@ -73,16 +54,13 @@ public class AudioPlayer implements Renderer<List<Shape>> {
|
|||
Shape shape = getCurrentShape();
|
||||
|
||||
shape = shape.setWeight(weight);
|
||||
shape = scale(shape);
|
||||
shape = rotate(shape, FORMAT.mix.rate);
|
||||
shape = translate(shape, FORMAT.mix.rate);
|
||||
|
||||
double totalAudioFrames = shape.getWeight() * shape.getLength();
|
||||
double drawingProgress = totalAudioFrames == 0 ? 1 : audioFramesDrawn / totalAudioFrames;
|
||||
Vector2 nextVector = applyEffects(f, shape.nextVector(drawingProgress));
|
||||
|
||||
output[f * FORMAT.channels.outputs] = (float) nextVector.getX();
|
||||
output[f * FORMAT.channels.outputs + 1] = (float) nextVector.getY();
|
||||
output[f * format.channels.outputs] = (float) nextVector.getX();
|
||||
output[f * format.channels.outputs + 1] = (float) nextVector.getY();
|
||||
|
||||
audioFramesDrawn++;
|
||||
|
||||
|
@ -107,60 +85,6 @@ public class AudioPlayer implements Renderer<List<Shape>> {
|
|||
return vector;
|
||||
}
|
||||
|
||||
private Shape rotate(Shape shape, double sampleRate) {
|
||||
if (rotateSpeed != 0) {
|
||||
shape = shape.rotate(
|
||||
nextTheta(sampleRate, rotateSpeed, translatePhase)
|
||||
);
|
||||
}
|
||||
|
||||
return shape;
|
||||
}
|
||||
|
||||
private Shape translate(Shape shape, double sampleRate) {
|
||||
if (translateSpeed != 0 && !translateVector.equals(new Vector2())) {
|
||||
return shape.translate(translateVector.scale(
|
||||
Math.sin(nextTheta(sampleRate, translateSpeed, rotatePhase))
|
||||
));
|
||||
}
|
||||
|
||||
return shape;
|
||||
}
|
||||
|
||||
private double nextTheta(double sampleRate, double frequency, Phase phase) {
|
||||
phase.value += frequency / sampleRate;
|
||||
|
||||
if (phase.value >= 1.0) {
|
||||
phase.value = -1.0;
|
||||
}
|
||||
|
||||
return phase.value * Math.PI;
|
||||
}
|
||||
|
||||
private Shape scale(Shape shape) {
|
||||
if (scale != 1) {
|
||||
return shape.scale(scale);
|
||||
}
|
||||
|
||||
return shape;
|
||||
}
|
||||
|
||||
public void setRotationSpeed(double speed) {
|
||||
this.rotateSpeed = speed;
|
||||
}
|
||||
|
||||
public void setTranslation(Vector2 translation) {
|
||||
this.translateVector = translation;
|
||||
}
|
||||
|
||||
public void setTranslationSpeed(double speed) {
|
||||
translateSpeed = speed;
|
||||
}
|
||||
|
||||
public void setScale(double scale) {
|
||||
this.scale = scale;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setQuality(double quality) {
|
||||
this.weight = quality;
|
||||
|
@ -191,11 +115,11 @@ public class AudioPlayer implements Renderer<List<Shape>> {
|
|||
if (defaultOutput == null) return;
|
||||
|
||||
try (XtDevice device = service.openDevice(defaultOutput)) {
|
||||
if (device.supportsFormat(FORMAT)) {
|
||||
if (device.supportsFormat(format)) {
|
||||
|
||||
XtBufferSize size = device.getBufferSize(FORMAT);
|
||||
XtBufferSize size = device.getBufferSize(format);
|
||||
XtStreamParams streamParams = new XtStreamParams(true, this::render, null, null);
|
||||
XtDeviceStreamParams deviceParams = new XtDeviceStreamParams(streamParams, FORMAT, size.current);
|
||||
XtDeviceStreamParams deviceParams = new XtDeviceStreamParams(streamParams, format, size.current);
|
||||
try (XtStream stream = device.openStream(deviceParams, null);
|
||||
XtSafeBuffer safe = XtSafeBuffer.register(stream, true)) {
|
||||
stream.start();
|
||||
|
@ -239,9 +163,4 @@ public class AudioPlayer implements Renderer<List<Shape>> {
|
|||
effects.remove(identifier);
|
||||
}
|
||||
|
||||
private static final class Phase {
|
||||
|
||||
private double value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,5 +2,8 @@ package sh.ball.audio.effect;
|
|||
|
||||
public enum EffectType {
|
||||
VECTOR_CANCELLING,
|
||||
BIT_CRUSH
|
||||
BIT_CRUSH,
|
||||
SCALE,
|
||||
ROTATE,
|
||||
TRANSLATE
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
package sh.ball.audio.effect;
|
||||
|
||||
public abstract class PhaseEffect implements Effect {
|
||||
|
||||
protected final int sampleRate;
|
||||
|
||||
protected double speed;
|
||||
private double phase;
|
||||
|
||||
protected PhaseEffect(int sampleRate, double speed) {
|
||||
this.sampleRate = sampleRate;
|
||||
this.speed = speed;
|
||||
}
|
||||
|
||||
protected double nextTheta() {
|
||||
phase += speed / sampleRate;
|
||||
|
||||
if (phase >= 1.0) {
|
||||
phase = -1.0;
|
||||
}
|
||||
|
||||
return phase * Math.PI;
|
||||
}
|
||||
|
||||
public void setSpeed(double speed) {
|
||||
this.speed = speed;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package sh.ball.audio.effect;
|
||||
|
||||
import sh.ball.shapes.Vector2;
|
||||
|
||||
public class RotateEffect extends PhaseEffect {
|
||||
|
||||
public RotateEffect(int sampleRate, double speed) {
|
||||
super(sampleRate, speed);
|
||||
}
|
||||
|
||||
public RotateEffect(int sampleRate) {
|
||||
this(sampleRate, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vector2 apply(int count, Vector2 vector) {
|
||||
if (speed != 0) {
|
||||
return vector.rotate(nextTheta());
|
||||
}
|
||||
|
||||
return vector;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package sh.ball.audio.effect;
|
||||
|
||||
import sh.ball.shapes.Vector2;
|
||||
|
||||
public class ScaleEffect implements Effect {
|
||||
|
||||
private double scale;
|
||||
|
||||
public ScaleEffect(double scale) {
|
||||
this.scale = scale;
|
||||
}
|
||||
|
||||
public ScaleEffect() {
|
||||
this(1);
|
||||
}
|
||||
|
||||
public void setScale(double scale) {
|
||||
this.scale = scale;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vector2 apply(int count, Vector2 vector) {
|
||||
return vector.scale(scale);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package sh.ball.audio.effect;
|
||||
|
||||
import sh.ball.shapes.Vector2;
|
||||
|
||||
public class TranslateEffect extends PhaseEffect {
|
||||
|
||||
private Vector2 translation;
|
||||
|
||||
public TranslateEffect(int sampleRate, double speed, Vector2 translation) {
|
||||
super(sampleRate, speed);
|
||||
this.translation = translation;
|
||||
}
|
||||
|
||||
public TranslateEffect(int sampleRate) {
|
||||
this(sampleRate, 0, new Vector2());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vector2 apply(int count, Vector2 vector) {
|
||||
if (speed != 0 && !translation.equals(new Vector2())) {
|
||||
return vector.translate(translation.scale(Math.sin(nextTheta())));
|
||||
}
|
||||
|
||||
return vector;
|
||||
}
|
||||
|
||||
public void setTranslation(Vector2 translation) {
|
||||
this.translation = translation;
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
package sh.ball.gui;
|
||||
|
||||
import javafx.scene.control.*;
|
||||
import sh.ball.audio.AudioPlayer;
|
||||
import sh.ball.audio.effect.Effect;
|
||||
import sh.ball.audio.Renderer;
|
||||
import sh.ball.audio.effect.*;
|
||||
import sh.ball.audio.FrameProducer;
|
||||
|
||||
import java.io.File;
|
||||
|
@ -24,8 +24,6 @@ import javafx.stage.Stage;
|
|||
import javax.xml.parsers.ParserConfigurationException;
|
||||
|
||||
import org.xml.sax.SAXException;
|
||||
import sh.ball.audio.effect.EffectType;
|
||||
import sh.ball.audio.effect.EventFactory;
|
||||
import sh.ball.engine.Vector3;
|
||||
import sh.ball.parser.obj.ObjFrameSettings;
|
||||
import sh.ball.parser.obj.ObjParser;
|
||||
|
@ -35,17 +33,18 @@ import sh.ball.shapes.Vector2;
|
|||
|
||||
public class Controller implements Initializable {
|
||||
|
||||
private static final int SAMPLE_RATE = 192000;
|
||||
private static final InputStream DEFAULT_OBJ = Controller.class.getResourceAsStream("/models/cube.obj");
|
||||
|
||||
private final FileChooser fileChooser = new FileChooser();
|
||||
// TODO: Reduce coupling on AudioPlayer
|
||||
private final AudioPlayer renderer = new AudioPlayer();
|
||||
private final Renderer<List<Shape>> renderer;
|
||||
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
|
||||
private FrameProducer<List<Shape>> producer = new FrameProducer<>(
|
||||
renderer,
|
||||
new ObjParser(DEFAULT_OBJ).parse()
|
||||
);
|
||||
private final RotateEffect rotateEffect = new RotateEffect(SAMPLE_RATE);
|
||||
private final TranslateEffect translateEffect = new TranslateEffect(SAMPLE_RATE);
|
||||
private final ScaleEffect scaleEffect = new ScaleEffect();
|
||||
|
||||
private FrameProducer<List<Shape>> producer;
|
||||
|
||||
private Stage stage;
|
||||
|
||||
|
@ -94,7 +93,12 @@ public class Controller implements Initializable {
|
|||
@FXML
|
||||
private Slider bitCrushSlider;
|
||||
|
||||
public Controller() throws IOException {
|
||||
public Controller(Renderer<List<Shape>> renderer) throws IOException {
|
||||
this.renderer = renderer;
|
||||
this.producer = new FrameProducer<>(
|
||||
renderer,
|
||||
new ObjParser(DEFAULT_OBJ).parse()
|
||||
);
|
||||
}
|
||||
|
||||
private Map<Slider, SliderUpdater<Double>> initializeSliderMap() {
|
||||
|
@ -102,11 +106,11 @@ public class Controller implements Initializable {
|
|||
weightSlider,
|
||||
new SliderUpdater<>(weightLabel::setText, renderer::setQuality),
|
||||
rotateSpeedSlider,
|
||||
new SliderUpdater<>(rotateSpeedLabel::setText, renderer::setRotationSpeed),
|
||||
new SliderUpdater<>(rotateSpeedLabel::setText, rotateEffect::setSpeed),
|
||||
translationSpeedSlider,
|
||||
new SliderUpdater<>(translationSpeedLabel::setText, renderer::setTranslationSpeed),
|
||||
new SliderUpdater<>(translationSpeedLabel::setText, translateEffect::setSpeed),
|
||||
scaleSlider,
|
||||
new SliderUpdater<>(scaleLabel::setText, renderer::setScale),
|
||||
new SliderUpdater<>(scaleLabel::setText, scaleEffect::setScale),
|
||||
focalLengthSlider,
|
||||
new SliderUpdater<>(focalLengthLabel::setText, this::setFocalLength)
|
||||
);
|
||||
|
@ -135,7 +139,7 @@ public class Controller implements Initializable {
|
|||
}
|
||||
|
||||
InvalidationListener translationUpdate = observable ->
|
||||
renderer.setTranslation(new Vector2(
|
||||
translateEffect.setTranslation(new Vector2(
|
||||
tryParse(translationXTextField.getText()),
|
||||
tryParse(translationYTextField.getText())
|
||||
));
|
||||
|
@ -150,7 +154,6 @@ public class Controller implements Initializable {
|
|||
tryParse(cameraZTextField.getText())
|
||||
)));
|
||||
|
||||
|
||||
cameraXTextField.textProperty().addListener(cameraPosUpdate);
|
||||
cameraYTextField.textProperty().addListener(cameraPosUpdate);
|
||||
cameraZTextField.textProperty().addListener(cameraPosUpdate);
|
||||
|
@ -175,6 +178,10 @@ public class Controller implements Initializable {
|
|||
}
|
||||
});
|
||||
|
||||
renderer.addEffect(EffectType.SCALE, scaleEffect);
|
||||
renderer.addEffect(EffectType.ROTATE, rotateEffect);
|
||||
renderer.addEffect(EffectType.TRANSLATE, translateEffect);
|
||||
|
||||
executor.submit(producer);
|
||||
new Thread(renderer).start();
|
||||
}
|
||||
|
|
|
@ -7,17 +7,20 @@ import javafx.scene.Parent;
|
|||
import javafx.scene.Scene;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.stage.Stage;
|
||||
import sh.ball.audio.AudioPlayer;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class Gui extends Application {
|
||||
|
||||
private static final int SAMPLE_RATE = 192000;
|
||||
|
||||
@Override
|
||||
public void start(Stage stage) throws Exception {
|
||||
FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/osci-render.fxml"));
|
||||
Controller controller = new Controller(new AudioPlayer(SAMPLE_RATE));
|
||||
loader.setController(controller);
|
||||
Parent root = loader.load();
|
||||
Controller controller = loader.getController();
|
||||
controller.setStage(stage);
|
||||
|
||||
stage.getIcons().add(new Image(Objects.requireNonNull(getClass().getResourceAsStream("/icons/icon.png"))));
|
||||
stage.setTitle("osci-render");
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<?import javafx.scene.layout.GridPane?>
|
||||
<?import javafx.scene.layout.RowConstraints?>
|
||||
|
||||
<GridPane alignment="center" hgap="10" prefWidth="400.0" vgap="10" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sh.ball.gui.Controller">
|
||||
<GridPane alignment="center" hgap="10" prefWidth="400.0" vgap="10" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<columnConstraints>
|
||||
<ColumnConstraints />
|
||||
</columnConstraints>
|
||||
|
@ -26,9 +26,9 @@
|
|||
<Slider fx:id="rotateSpeedSlider" layoutX="116.0" layoutY="79.0" max="10.0" prefHeight="14.0" prefWidth="198.0" />
|
||||
<Label layoutX="37.0" layoutY="77.0" text="Rotate speed" />
|
||||
<Label fx:id="rotateSpeedLabel" layoutX="316.0" layoutY="77.0" maxWidth="40.0" text="0" />
|
||||
<Slider fx:id="translationSpeedSlider" layoutX="116.0" layoutY="105.0" max="10.0" prefHeight="14.0" prefWidth="198.0" value="1.0" />
|
||||
<Slider fx:id="translationSpeedSlider" layoutX="116.0" layoutY="105.0" max="10.0" prefHeight="14.0" prefWidth="198.0" />
|
||||
<Label layoutX="14.0" layoutY="103.0" text="Translation speed" />
|
||||
<Label fx:id="translationSpeedLabel" layoutX="316.0" layoutY="103.0" maxWidth="40.0" text="1" />
|
||||
<Label fx:id="translationSpeedLabel" layoutX="316.0" layoutY="103.0" maxWidth="40.0" text="0" />
|
||||
<Slider fx:id="scaleSlider" layoutX="116.0" layoutY="130.0" max="10.0" prefHeight="14.0" prefWidth="198.0" value="1.0" />
|
||||
<Label layoutX="80.0" layoutY="128.0" text="Scale" />
|
||||
<Label fx:id="scaleLabel" layoutX="316.0" layoutY="128.0" maxWidth="40.0" text="1" />
|
||||
|
|
Ładowanie…
Reference in New Issue