Make line endings consistent and refresh GUI

pull/35/head
James Ball 2021-05-26 20:44:18 +01:00
rodzic 0c3a3b7319
commit d65261ca3c
47 zmienionych plików z 3434 dodań i 3426 usunięć

Wyświetl plik

@ -56,7 +56,7 @@ Additional effects can be applied to the image such as:
## Screenshots
<img width="250px" height="396px" src="gui.png">
<img width="524px" height="330px" src="gui.png">
## Running

BIN
gui.png

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 28 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 58 KiB

574
pom.xml
Wyświetl plik

@ -1,288 +1,288 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>sh.ball</groupId>
<artifactId>osci-render</artifactId>
<version>1.6.1</version>
<name>osci-render</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>15</maven.compiler.release>
<javafx.version>16</javafx.version>
<project.modulePath>${project.build.directory}\modules</project.modulePath>
<appModule>oscirender</appModule>
<appMainClass>sh.ball.gui.Gui</appMainClass>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>2.4.1</version>
<executions>
<execution>
<id>auto-clean</id>
<phase>initialize</phase>
<goals>
<goal>clean</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<id>default-jar</id>
<phase>none</phase>
<configuration>
<finalName>unwanted</finalName>
<classifier>unwanted</classifier>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>sh.ball.gui.Gui</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<appendAssemblyId>false</appendAssemblyId>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.1.2</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>generate-sources</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/modules</outputDirectory>
<overWriteReleases>true</overWriteReleases>
<overWriteSnapshots>true</overWriteSnapshots>
<overWriteIfNewer>true</overWriteIfNewer>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${maven.compiler.release}</source>
<target>${maven.compiler.release}</target>
<compilerArgs>
<compilerArg>--enable-preview</compilerArg>
<compilerArg>--module-path</compilerArg>
<compilerArg>${project.modulePath}</compilerArg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.panteleyev</groupId>
<artifactId>jpackage-maven-plugin</artifactId>
<version>1.4.0</version>
<configuration>
<name>osci-render</name>
<appVersion>${project.version}</appVersion>
<vendor>james.ball.sh</vendor>
<module>${appModule}/${appMainClass}</module>
<modulePath>${project.build.directory}/modules;${project.build.directory}/classes</modulePath>
<destination>${project.build.directory}</destination>
<javaOptions>
<option>--enable-preview</option>
<option>-Dfile.encoding=UTF-8</option>
</javaOptions>
</configuration>
<executions>
<execution>
<id>win</id>
<configuration>
<icon>src/main/resources/icons/icon.ico</icon>
<winMenu>true</winMenu>
<winMenuGroup>osci-render</winMenuGroup>
</configuration>
</execution>
<execution>
<id>debian</id>
<configuration>
<type>deb</type>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.moditect</groupId>
<artifactId>moditect-maven-plugin</artifactId>
<version>1.0.0.Beta2</version>
<executions>
<execution>
<id>add-module-infos</id>
<phase>generate-resources</phase>
<goals>
<goal>add-module-info</goal>
</goals>
<configuration>
<overwriteExistingFiles>true</overwriteExistingFiles>
<outputDirectory>${project.build.directory}/modules</outputDirectory>
<modules>
<module>
<artifact>
<groupId>org.unbescape</groupId>
<artifactId>unbescape</artifactId>
<version>1.1.6.RELEASE</version>
</artifact>
<moduleInfo>
<name>org.unbescape.html</name>
</moduleInfo>
</module>
<module>
<artifact>
<groupId>com.github.mokiat</groupId>
<artifactId>java-data-front</artifactId>
<version>2.0.0</version>
</artifact>
<moduleInfo>
<name>java.data.front</name>
</moduleInfo>
</module>
<module>
<artifact>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</artifact>
<moduleInfo>
<name>javafx.controls.exposed</name>
</moduleInfo>
</module>
<module>
<artifact>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</artifact>
<moduleInfo>
<name>javafx.fxml.exposed</name>
</moduleInfo>
</module>
<module>
<artifact>
<groupId>org.openjfx</groupId>
<artifactId>javafx-graphics</artifactId>
<version>${javafx.version}</version>
</artifact>
<moduleInfo>
<name>javafx.graphics.exposed</name>
</moduleInfo>
</module>
<module>
<artifact>
<groupId>org.openjfx</groupId>
<artifactId>javafx-base</artifactId>
<version>${javafx.version}</version>
</artifact>
<moduleInfo>
<name>javafx.base.exposed</name>
</moduleInfo>
</module>
<module>
<artifact>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>5.6.0</version>
</artifact>
<moduleInfo>
<name>com.sun.jna</name>
</moduleInfo>
</module>
<module>
<artifact>
<groupId>org.jheaps</groupId>
<artifactId>jheaps</artifactId>
<version>0.13</version>
</artifact>
<moduleInfo>
<name>org.jheaps</name>
</moduleInfo>
</module>
</modules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>jitpack.io</id>
<name>jitpack</name>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<!-- https://sjoerdvankreel.github.io/xt-audio/ -->
<dependency>
<groupId>com.github.sjoerdvankreel</groupId>
<artifactId>xt.audio</artifactId>
<version>1.9</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.unbescape/unbescape -->
<dependency>
<groupId>org.unbescape</groupId>
<artifactId>unbescape</artifactId>
<version>1.1.6.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.jgrapht/jgrapht-core -->
<dependency>
<groupId>org.jgrapht</groupId>
<artifactId>jgrapht-core</artifactId>
<version>1.5.0</version>
</dependency>
<dependency>
<groupId>com.github.mokiat</groupId>
<artifactId>java-data-front</artifactId>
<version>2.0.0</version>
</dependency>
</dependencies>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>sh.ball</groupId>
<artifactId>osci-render</artifactId>
<version>1.6.1</version>
<name>osci-render</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>15</maven.compiler.release>
<javafx.version>16</javafx.version>
<project.modulePath>${project.build.directory}\modules</project.modulePath>
<appModule>oscirender</appModule>
<appMainClass>sh.ball.gui.Gui</appMainClass>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>2.4.1</version>
<executions>
<execution>
<id>auto-clean</id>
<phase>initialize</phase>
<goals>
<goal>clean</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<id>default-jar</id>
<phase>none</phase>
<configuration>
<finalName>unwanted</finalName>
<classifier>unwanted</classifier>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>sh.ball.gui.Gui</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<appendAssemblyId>false</appendAssemblyId>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.1.2</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>generate-sources</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/modules</outputDirectory>
<overWriteReleases>true</overWriteReleases>
<overWriteSnapshots>true</overWriteSnapshots>
<overWriteIfNewer>true</overWriteIfNewer>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${maven.compiler.release}</source>
<target>${maven.compiler.release}</target>
<compilerArgs>
<compilerArg>--enable-preview</compilerArg>
<compilerArg>--module-path</compilerArg>
<compilerArg>${project.modulePath}</compilerArg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.panteleyev</groupId>
<artifactId>jpackage-maven-plugin</artifactId>
<version>1.4.0</version>
<configuration>
<name>osci-render</name>
<appVersion>${project.version}</appVersion>
<vendor>james.ball.sh</vendor>
<module>${appModule}/${appMainClass}</module>
<modulePath>${project.build.directory}/modules;${project.build.directory}/classes</modulePath>
<destination>${project.build.directory}</destination>
<javaOptions>
<option>--enable-preview</option>
<option>-Dfile.encoding=UTF-8</option>
</javaOptions>
</configuration>
<executions>
<execution>
<id>win</id>
<configuration>
<icon>src/main/resources/icons/icon.ico</icon>
<winMenu>true</winMenu>
<winMenuGroup>osci-render</winMenuGroup>
</configuration>
</execution>
<execution>
<id>debian</id>
<configuration>
<type>deb</type>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.moditect</groupId>
<artifactId>moditect-maven-plugin</artifactId>
<version>1.0.0.Beta2</version>
<executions>
<execution>
<id>add-module-infos</id>
<phase>generate-resources</phase>
<goals>
<goal>add-module-info</goal>
</goals>
<configuration>
<overwriteExistingFiles>true</overwriteExistingFiles>
<outputDirectory>${project.build.directory}/modules</outputDirectory>
<modules>
<module>
<artifact>
<groupId>org.unbescape</groupId>
<artifactId>unbescape</artifactId>
<version>1.1.6.RELEASE</version>
</artifact>
<moduleInfo>
<name>org.unbescape.html</name>
</moduleInfo>
</module>
<module>
<artifact>
<groupId>com.github.mokiat</groupId>
<artifactId>java-data-front</artifactId>
<version>2.0.0</version>
</artifact>
<moduleInfo>
<name>java.data.front</name>
</moduleInfo>
</module>
<module>
<artifact>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</artifact>
<moduleInfo>
<name>javafx.controls.exposed</name>
</moduleInfo>
</module>
<module>
<artifact>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</artifact>
<moduleInfo>
<name>javafx.fxml.exposed</name>
</moduleInfo>
</module>
<module>
<artifact>
<groupId>org.openjfx</groupId>
<artifactId>javafx-graphics</artifactId>
<version>${javafx.version}</version>
</artifact>
<moduleInfo>
<name>javafx.graphics.exposed</name>
</moduleInfo>
</module>
<module>
<artifact>
<groupId>org.openjfx</groupId>
<artifactId>javafx-base</artifactId>
<version>${javafx.version}</version>
</artifact>
<moduleInfo>
<name>javafx.base.exposed</name>
</moduleInfo>
</module>
<module>
<artifact>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>5.6.0</version>
</artifact>
<moduleInfo>
<name>com.sun.jna</name>
</moduleInfo>
</module>
<module>
<artifact>
<groupId>org.jheaps</groupId>
<artifactId>jheaps</artifactId>
<version>0.13</version>
</artifact>
<moduleInfo>
<name>org.jheaps</name>
</moduleInfo>
</module>
</modules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>jitpack.io</id>
<name>jitpack</name>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<!-- https://sjoerdvankreel.github.io/xt-audio/ -->
<dependency>
<groupId>com.github.sjoerdvankreel</groupId>
<artifactId>xt.audio</artifactId>
<version>1.9</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.unbescape/unbescape -->
<dependency>
<groupId>org.unbescape</groupId>
<artifactId>unbescape</artifactId>
<version>1.1.6.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.jgrapht/jgrapht-core -->
<dependency>
<groupId>org.jgrapht</groupId>
<artifactId>jgrapht-core</artifactId>
<version>1.5.0</version>
</dependency>
<dependency>
<groupId>com.github.mokiat</groupId>
<artifactId>java-data-front</artifactId>
<version>2.0.0</version>
</dependency>
</dependencies>
</project>

Wyświetl plik

@ -1,3 +1,3 @@
Manifest-Version: 1.0
Main-Class: sh.ball.gui.Gui
Manifest-Version: 1.0
Main-Class: sh.ball.gui.Gui

Wyświetl plik

@ -1,17 +1,17 @@
module oscirender {
requires javafx.controls;
requires javafx.controls.exposed;
requires javafx.graphics;
requires javafx.graphics.exposed;
requires javafx.fxml;
requires javafx.fxml.exposed;
requires javafx.base.exposed;
requires xt.audio;
requires java.xml;
requires java.data.front;
requires org.jgrapht.core;
requires org.unbescape.html;
requires java.desktop;
opens sh.ball.gui;
module oscirender {
requires javafx.controls;
requires javafx.controls.exposed;
requires javafx.graphics;
requires javafx.graphics.exposed;
requires javafx.fxml;
requires javafx.fxml.exposed;
requires javafx.base.exposed;
requires xt.audio;
requires java.xml;
requires java.data.front;
requires org.jgrapht.core;
requires org.unbescape.html;
requires java.desktop;
opens sh.ball.gui;
}

Wyświetl plik

@ -1,7 +1,7 @@
package sh.ball.audio;
import sh.ball.shapes.Vector2;
public interface Effect {
Vector2 apply(int count, Vector2 vector);
}
package sh.ball.audio;
import sh.ball.shapes.Vector2;
public interface Effect {
Vector2 apply(int count, Vector2 vector);
}

Wyświetl plik

@ -1,30 +1,30 @@
package sh.ball.audio;
public class FrameProducer<S, T> implements Runnable {
private final Renderer<S, T> renderer;
private final FrameSet<S> frames;
private boolean running;
public FrameProducer(Renderer<S, T> renderer, FrameSet<S> frames) {
this.renderer = renderer;
this.frames = frames;
}
@Override
public void run() {
running = true;
while (running) {
renderer.addFrame(frames.next());
}
}
public void stop() {
running = false;
}
public void setFrameSettings(Object settings) {
frames.setFrameSettings(settings);
}
}
package sh.ball.audio;
public class FrameProducer<S, T> implements Runnable {
private final Renderer<S, T> renderer;
private final FrameSet<S> frames;
private boolean running;
public FrameProducer(Renderer<S, T> renderer, FrameSet<S> frames) {
this.renderer = renderer;
this.frames = frames;
}
@Override
public void run() {
running = true;
while (running) {
renderer.addFrame(frames.next());
}
}
public void stop() {
running = false;
}
public void setFrameSettings(Object settings) {
frames.setFrameSettings(settings);
}
}

Wyświetl plik

@ -1,12 +1,12 @@
package sh.ball.audio;
import sh.ball.parser.obj.Listener;
public interface FrameSet<T> {
T next();
void setFrameSettings(Object settings);
void addListener(Listener listener);
}
package sh.ball.audio;
import sh.ball.parser.obj.Listener;
public interface FrameSet<T> {
T next();
void setFrameSettings(Object settings);
void addListener(Listener listener);
}

Wyświetl plik

@ -1,105 +1,105 @@
package sh.ball.audio;
import sh.ball.audio.fft.FFT;
import java.util.ArrayList;
import java.util.List;
public class FrequencyAnalyser<S, T> implements Runnable {
private static final float NORMALIZATION_FACTOR_2_BYTES = Short.MAX_VALUE + 1.0f;
private final Renderer<S, T> renderer;
private final List<FrequencyListener> listeners = new ArrayList<>();
private final int frameSize;
private final int sampleRate;
public FrequencyAnalyser(Renderer<S, T> renderer, int frameSize, int sampleRate) {
this.renderer = renderer;
this.frameSize = frameSize;
this.sampleRate = sampleRate;
}
public void addListener(FrequencyListener listener) {
listeners.add(listener);
}
private void notifyListeners(double leftFrequency, double rightFrequency) {
for (FrequencyListener listener : listeners) {
listener.updateFrequency(leftFrequency, rightFrequency);
}
}
// Adapted from https://stackoverflow.com/questions/53997426/java-how-to-get-current-frequency-of-audio-input
@Override
public void run() {
byte[] buf = new byte[2 << 18]; // <--- increase this for higher frequency resolution
while (true) {
try {
renderer.read(buf);
} catch (InterruptedException e) {
e.printStackTrace();
}
double[] leftSamples = decode(buf, true);
double[] rightSamples = decode(buf, false);
FFT leftFft = new FFT(leftSamples, null, false, true);
FFT rightFft = new FFT(rightSamples, null, false, true);
double[] leftMags = leftFft.getMagnitudeSpectrum();
double[] rightMags = rightFft.getMagnitudeSpectrum();
double[] bins = leftFft.getBinLabels(sampleRate);
int maxLeftIndex = 0;
double maxLeft = Double.NEGATIVE_INFINITY;
int maxRightIndex = 0;
double maxRight = Double.NEGATIVE_INFINITY;
for (int i = 1; i < leftMags.length; i++) {
if (bins[i] < 20 || bins[i] > 20000) {
continue;
}
if (leftMags[i] > maxLeft) {
maxLeftIndex = i;
maxLeft = leftMags[i];
}
if (rightMags[i] > maxRight) {
maxRightIndex = i;
maxRight = rightMags[i];
}
}
notifyListeners(bins[maxLeftIndex], bins[maxRightIndex]);
}
}
private double[] decode(final byte[] buf, boolean decodeLeft) {
final double[] fbuf = new double[(buf.length / 2) / frameSize];
int byteNum = 0;
int i = 0;
for (int pos = 0; pos < buf.length; pos += frameSize) {
int sample = byteToIntLittleEndian(buf, pos, frameSize);
// normalize to [0,1] (not strictly necessary, but makes things easier)
double normalSample = sample / NORMALIZATION_FACTOR_2_BYTES;
if (decodeLeft) {
if (byteNum < 2) {
fbuf[i++] = normalSample;
}
} else if (byteNum >= 2) {
fbuf[i++] = normalSample;
}
byteNum++;
byteNum %= 4;
}
return fbuf;
}
private static int byteToIntLittleEndian(final byte[] buf, final int offset, final int bytesPerSample) {
int sample = 0;
for (int byteIndex = 0; byteIndex < bytesPerSample; byteIndex++) {
final int aByte = buf[offset + byteIndex] & 0xff;
sample += aByte << 8 * (byteIndex);
}
return sample;
}
}
package sh.ball.audio;
import sh.ball.audio.fft.FFT;
import java.util.ArrayList;
import java.util.List;
public class FrequencyAnalyser<S, T> implements Runnable {
private static final float NORMALIZATION_FACTOR_2_BYTES = Short.MAX_VALUE + 1.0f;
private final Renderer<S, T> renderer;
private final List<FrequencyListener> listeners = new ArrayList<>();
private final int frameSize;
private final int sampleRate;
public FrequencyAnalyser(Renderer<S, T> renderer, int frameSize, int sampleRate) {
this.renderer = renderer;
this.frameSize = frameSize;
this.sampleRate = sampleRate;
}
public void addListener(FrequencyListener listener) {
listeners.add(listener);
}
private void notifyListeners(double leftFrequency, double rightFrequency) {
for (FrequencyListener listener : listeners) {
listener.updateFrequency(leftFrequency, rightFrequency);
}
}
// Adapted from https://stackoverflow.com/questions/53997426/java-how-to-get-current-frequency-of-audio-input
@Override
public void run() {
byte[] buf = new byte[2 << 18]; // <--- increase this for higher frequency resolution
while (true) {
try {
renderer.read(buf);
} catch (InterruptedException e) {
e.printStackTrace();
}
double[] leftSamples = decode(buf, true);
double[] rightSamples = decode(buf, false);
FFT leftFft = new FFT(leftSamples, null, false, true);
FFT rightFft = new FFT(rightSamples, null, false, true);
double[] leftMags = leftFft.getMagnitudeSpectrum();
double[] rightMags = rightFft.getMagnitudeSpectrum();
double[] bins = leftFft.getBinLabels(sampleRate);
int maxLeftIndex = 0;
double maxLeft = Double.NEGATIVE_INFINITY;
int maxRightIndex = 0;
double maxRight = Double.NEGATIVE_INFINITY;
for (int i = 1; i < leftMags.length; i++) {
if (bins[i] < 20 || bins[i] > 20000) {
continue;
}
if (leftMags[i] > maxLeft) {
maxLeftIndex = i;
maxLeft = leftMags[i];
}
if (rightMags[i] > maxRight) {
maxRightIndex = i;
maxRight = rightMags[i];
}
}
notifyListeners(bins[maxLeftIndex], bins[maxRightIndex]);
}
}
private double[] decode(final byte[] buf, boolean decodeLeft) {
final double[] fbuf = new double[(buf.length / 2) / frameSize];
int byteNum = 0;
int i = 0;
for (int pos = 0; pos < buf.length; pos += frameSize) {
int sample = byteToIntLittleEndian(buf, pos, frameSize);
// normalize to [0,1] (not strictly necessary, but makes things easier)
double normalSample = sample / NORMALIZATION_FACTOR_2_BYTES;
if (decodeLeft) {
if (byteNum < 2) {
fbuf[i++] = normalSample;
}
} else if (byteNum >= 2) {
fbuf[i++] = normalSample;
}
byteNum++;
byteNum %= 4;
}
return fbuf;
}
private static int byteToIntLittleEndian(final byte[] buf, final int offset, final int bytesPerSample) {
int sample = 0;
for (int byteIndex = 0; byteIndex < bytesPerSample; byteIndex++) {
final int aByte = buf[offset + byteIndex] & 0xff;
sample += aByte << 8 * (byteIndex);
}
return sample;
}
}

Wyświetl plik

@ -1,6 +1,6 @@
package sh.ball.audio;
public interface FrequencyListener {
void updateFrequency(double leftFrequency, double rightFrequency);
}
package sh.ball.audio;
public interface FrequencyListener {
void updateFrequency(double leftFrequency, double rightFrequency);
}

Wyświetl plik

@ -1,22 +1,22 @@
package sh.ball.audio;
import sh.ball.audio.effect.Effect;
public interface Renderer<S, T> extends Runnable {
void stop();
void setQuality(double quality);
void addFrame(S frame);
void addEffect(Object identifier, Effect effect);
void removeEffect(Object identifier);
void read(byte[] buffer) throws InterruptedException;
void startRecord();
T stopRecord();
}
package sh.ball.audio;
import sh.ball.audio.effect.Effect;
public interface Renderer<S, T> extends Runnable {
void stop();
void setQuality(double quality);
void addFrame(S frame);
void addEffect(Object identifier, Effect effect);
void removeEffect(Object identifier);
void read(byte[] buffer) throws InterruptedException;
void startRecord();
T stopRecord();
}

Wyświetl plik

@ -1,7 +1,7 @@
package sh.ball.audio.effect;
import sh.ball.shapes.Vector2;
public interface Effect {
Vector2 apply(int count, Vector2 vector);
}
package sh.ball.audio.effect;
import sh.ball.shapes.Vector2;
public interface Effect {
Vector2 apply(int count, Vector2 vector);
}

Wyświetl plik

@ -1,46 +1,46 @@
package sh.ball.audio.effect;
import sh.ball.shapes.Vector2;
public class EffectFactory {
public static Effect vectorCancelling(int frequency) {
return (count, v) -> count % frequency == 0 ? v.scale(-1) : v;
}
public static Effect bitCrush(double value) {
return (count, v) -> {
double x = v.getX();
double y = v.getY();
return new Vector2(round(x, value), round(y, value));
};
}
private static double round(double value, double places) {
if (places < 0) throw new IllegalArgumentException();
long factor = (long) Math.pow(10, places);
value = value * factor;
long tmp = Math.round(value);
return (double) tmp / factor;
}
public static Effect horizontalDistort(double value) {
return (count, v) -> {
if (count % 2 == 0) {
return v.translate(new Vector2(value, 0));
} else {
return v.translate(new Vector2(-value, 0));
}
};
}
public static Effect verticalDistort(double value) {
return (count, v) -> {
if (count % 2 == 0) {
return v.translate(new Vector2(0, value));
} else {
return v.translate(new Vector2(0, -value));
}
};
}
}
package sh.ball.audio.effect;
import sh.ball.shapes.Vector2;
public class EffectFactory {
public static Effect vectorCancelling(int frequency) {
return (count, v) -> count % frequency == 0 ? v.scale(-1) : v;
}
public static Effect bitCrush(double value) {
return (count, v) -> {
double x = v.getX();
double y = v.getY();
return new Vector2(round(x, value), round(y, value));
};
}
private static double round(double value, double places) {
if (places < 0) throw new IllegalArgumentException();
long factor = (long) Math.pow(10, places);
value = value * factor;
long tmp = Math.round(value);
return (double) tmp / factor;
}
public static Effect horizontalDistort(double value) {
return (count, v) -> {
if (count % 2 == 0) {
return v.translate(new Vector2(value, 0));
} else {
return v.translate(new Vector2(-value, 0));
}
};
}
public static Effect verticalDistort(double value) {
return (count, v) -> {
if (count % 2 == 0) {
return v.translate(new Vector2(0, value));
} else {
return v.translate(new Vector2(0, -value));
}
};
}
}

Wyświetl plik

@ -1,12 +1,12 @@
package sh.ball.audio.effect;
public enum EffectType {
VECTOR_CANCELLING,
BIT_CRUSH,
SCALE,
ROTATE,
TRANSLATE,
VERTICAL_DISTORT,
HORIZONTAL_DISTORT,
WOBBLE
}
package sh.ball.audio.effect;
public enum EffectType {
VECTOR_CANCELLING,
BIT_CRUSH,
SCALE,
ROTATE,
TRANSLATE,
VERTICAL_DISTORT,
HORIZONTAL_DISTORT,
WOBBLE
}

Wyświetl plik

@ -1,30 +1,30 @@
package sh.ball.audio.effect;
public abstract class PhaseEffect implements Effect {
private static final double LARGE_VAL = 2 << 20;
protected final int sampleRate;
protected double speed;
private double phase = -LARGE_VAL;
protected PhaseEffect(int sampleRate, double speed) {
this.sampleRate = sampleRate;
this.speed = speed;
}
protected double nextTheta() {
phase += speed / sampleRate;
if (phase >= LARGE_VAL) {
phase = -LARGE_VAL;
}
return phase * Math.PI;
}
public void setSpeed(double speed) {
this.speed = speed;
}
}
package sh.ball.audio.effect;
public abstract class PhaseEffect implements Effect {
private static final double LARGE_VAL = 2 << 20;
protected final int sampleRate;
protected double speed;
private double phase = -LARGE_VAL;
protected PhaseEffect(int sampleRate, double speed) {
this.sampleRate = sampleRate;
this.speed = speed;
}
protected double nextTheta() {
phase += speed / sampleRate;
if (phase >= LARGE_VAL) {
phase = -LARGE_VAL;
}
return phase * Math.PI;
}
public void setSpeed(double speed) {
this.speed = speed;
}
}

Wyświetl plik

@ -1,23 +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;
}
}
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;
}
}

Wyświetl plik

@ -1,25 +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);
}
}
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);
}
}

Wyświetl plik

@ -1,30 +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;
}
}
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;
}
}

Wyświetl plik

@ -1,44 +1,44 @@
package sh.ball.audio.effect;
import sh.ball.audio.FrequencyListener;
import sh.ball.shapes.Vector2;
public class WobbleEffect extends PhaseEffect implements FrequencyListener {
private static final double DEFAULT_VOLUME = 0.2;
private double frequency;
private double lastFrequency;
private double volume;
public WobbleEffect(int sampleRate, double volume) {
super(sampleRate, 2);
this.volume = Math.max(Math.min(volume, 1), 0);
}
public WobbleEffect(int sampleRate) {
this(sampleRate, DEFAULT_VOLUME);
}
public void update() {
frequency = lastFrequency;
}
public void setVolume(double volume) {
this.volume = volume;
}
@Override
public void updateFrequency(double leftFrequency, double rightFrequency) {
lastFrequency = leftFrequency;
}
@Override
public Vector2 apply(int count, Vector2 vector) {
double theta = nextTheta();
double x = vector.getX() + volume * Math.sin(frequency * theta);
double y = vector.getY() + volume * Math.sin(frequency * theta);
return new Vector2(x, y);
}
}
package sh.ball.audio.effect;
import sh.ball.audio.FrequencyListener;
import sh.ball.shapes.Vector2;
public class WobbleEffect extends PhaseEffect implements FrequencyListener {
private static final double DEFAULT_VOLUME = 0.2;
private double frequency;
private double lastFrequency;
private double volume;
public WobbleEffect(int sampleRate, double volume) {
super(sampleRate, 2);
this.volume = Math.max(Math.min(volume, 1), 0);
}
public WobbleEffect(int sampleRate) {
this(sampleRate, DEFAULT_VOLUME);
}
public void update() {
frequency = lastFrequency;
}
public void setVolume(double volume) {
this.volume = volume;
}
@Override
public void updateFrequency(double leftFrequency, double rightFrequency) {
lastFrequency = leftFrequency;
}
@Override
public Vector2 apply(int count, Vector2 vector) {
double theta = nextTheta();
double x = vector.getX() + volume * Math.sin(frequency * theta);
double y = vector.getY() + volume * Math.sin(frequency * theta);
return new Vector2(x, y);
}
}

Wyświetl plik

@ -1,47 +1,47 @@
package sh.ball.engine;
import java.util.Objects;
public class Line3D {
private final Vector3 start;
private final Vector3 end;
public Line3D(Vector3 start, Vector3 end) {
this.start = start;
this.end = end;
}
public Vector3 getStart() {
return start;
}
public Vector3 getEnd() {
return end;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Line3D line3D = (Line3D) o;
return Objects.equals(start, line3D.start) && Objects.equals(end, line3D.end);
}
@Override
public int hashCode() {
return Objects.hash(start, end);
}
@Override
public String toString() {
return "Line3D{" +
"start=" + start +
", end=" + end +
'}';
}
}
package sh.ball.engine;
import java.util.Objects;
public class Line3D {
private final Vector3 start;
private final Vector3 end;
public Line3D(Vector3 start, Vector3 end) {
this.start = start;
this.end = end;
}
public Vector3 getStart() {
return start;
}
public Vector3 getEnd() {
return end;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Line3D line3D = (Line3D) o;
return Objects.equals(start, line3D.start) && Objects.equals(end, line3D.end);
}
@Override
public int hashCode() {
return Objects.hash(start, end);
}
@Override
public String toString() {
return "Line3D{" +
"start=" + start +
", end=" + end +
'}';
}
}

Wyświetl plik

@ -1,381 +1,390 @@
package sh.ball.gui;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.scene.control.*;
import javafx.util.Duration;
import sh.ball.audio.*;
import sh.ball.audio.effect.*;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
import javafx.beans.InvalidationListener;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException;
import sh.ball.audio.effect.Effect;
import sh.ball.audio.effect.EffectType;
import sh.ball.engine.Vector3;
import sh.ball.parser.obj.Listener;
import sh.ball.parser.obj.ObjSettingsFactory;
import sh.ball.parser.obj.ObjParser;
import sh.ball.parser.ParserFactory;
import sh.ball.shapes.Shape;
import sh.ball.shapes.Vector2;
public class Controller implements Initializable, FrequencyListener, Listener {
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();
private final Renderer<List<Shape>, AudioInputStream> renderer;
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final RotateEffect rotateEffect = new RotateEffect(SAMPLE_RATE);
private final TranslateEffect translateEffect = new TranslateEffect(SAMPLE_RATE);
private final WobbleEffect wobbleEffect = new WobbleEffect(SAMPLE_RATE);
private final ScaleEffect scaleEffect = new ScaleEffect();
private FrameProducer<List<Shape>, AudioInputStream> producer;
private boolean recording = false;
private Stage stage;
@FXML
private Label frequencyLabel;
@FXML
private Button chooseFileButton;
@FXML
private Label fileLabel;
@FXML
private Button recordButton;
@FXML
private Label recordLabel;
@FXML
private TextField translationXTextField;
@FXML
private TextField translationYTextField;
@FXML
private Slider weightSlider;
@FXML
private Slider rotateSpeedSlider;
@FXML
private Slider translationSpeedSlider;
@FXML
private Slider scaleSlider;
@FXML
private TitledPane objTitledPane;
@FXML
private Slider focalLengthSlider;
@FXML
private TextField cameraXTextField;
@FXML
private TextField cameraYTextField;
@FXML
private TextField cameraZTextField;
@FXML
private Slider objectRotateSpeedSlider;
@FXML
private CheckBox rotateCheckBox;
@FXML
private CheckBox vectorCancellingCheckBox;
@FXML
private Slider vectorCancellingSlider;
@FXML
private CheckBox bitCrushCheckBox;
@FXML
private Slider bitCrushSlider;
@FXML
private CheckBox verticalDistortCheckBox;
@FXML
private Slider verticalDistortSlider;
@FXML
private CheckBox horizontalDistortCheckBox;
@FXML
private Slider horizontalDistortSlider;
@FXML
private CheckBox wobbleCheckBox;
@FXML
private Slider wobbleSlider;
public Controller(Renderer<List<Shape>, AudioInputStream> renderer) throws IOException {
this.renderer = renderer;
FrameSet<List<Shape>> frames = new ObjParser(DEFAULT_OBJ).parse();
frames.addListener(this);
this.producer = new FrameProducer<>(renderer, frames);
}
private Map<Slider, Consumer<Double>> initializeSliderMap() {
return Map.of(
weightSlider,
renderer::setQuality,
rotateSpeedSlider,
rotateEffect::setSpeed,
translationSpeedSlider,
translateEffect::setSpeed,
scaleSlider,
scaleEffect::setScale,
focalLengthSlider,
d -> updateFocalLength(),
objectRotateSpeedSlider,
d -> updateObjectRotateSpeed()
);
}
private Map<EffectType, Slider> effectTypes;
private void initializeEffectTypes() {
effectTypes = Map.of(
EffectType.VECTOR_CANCELLING,
vectorCancellingSlider,
EffectType.BIT_CRUSH,
bitCrushSlider,
EffectType.VERTICAL_DISTORT,
verticalDistortSlider,
EffectType.HORIZONTAL_DISTORT,
horizontalDistortSlider,
EffectType.WOBBLE,
wobbleSlider
);
}
@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
Map<Slider, Consumer<Double>> sliders = initializeSliderMap();
initializeEffectTypes();
for (Slider slider : sliders.keySet()) {
slider.valueProperty().addListener((source, oldValue, newValue) ->
sliders.get(slider).accept(slider.getValue())
);
}
translationXTextField.textProperty().addListener(e -> updateTranslation());
translationYTextField.textProperty().addListener(e -> updateTranslation());
cameraXTextField.focusedProperty().addListener(e -> updateCameraPos());
cameraYTextField.focusedProperty().addListener(e -> updateCameraPos());
cameraZTextField.focusedProperty().addListener(e -> updateCameraPos());
InvalidationListener vectorCancellingListener = e ->
updateEffect(EffectType.VECTOR_CANCELLING, vectorCancellingCheckBox.isSelected(),
EffectFactory.vectorCancelling((int) vectorCancellingSlider.getValue()));
InvalidationListener bitCrushListener = e ->
updateEffect(EffectType.BIT_CRUSH, bitCrushCheckBox.isSelected(),
EffectFactory.bitCrush(bitCrushSlider.getValue()));
InvalidationListener verticalDistortListener = e ->
updateEffect(EffectType.VERTICAL_DISTORT, verticalDistortCheckBox.isSelected(),
EffectFactory.verticalDistort(verticalDistortSlider.getValue()));
InvalidationListener horizontalDistortListener = e ->
updateEffect(EffectType.HORIZONTAL_DISTORT, horizontalDistortCheckBox.isSelected(),
EffectFactory.horizontalDistort(horizontalDistortSlider.getValue()));
InvalidationListener wobbleListener = e -> {
wobbleEffect.setVolume(wobbleSlider.getValue());
updateEffect(EffectType.WOBBLE, wobbleCheckBox.isSelected(), wobbleEffect);
};
vectorCancellingSlider.valueProperty().addListener(vectorCancellingListener);
vectorCancellingCheckBox.selectedProperty().addListener(vectorCancellingListener);
bitCrushSlider.valueProperty().addListener(bitCrushListener);
bitCrushCheckBox.selectedProperty().addListener(bitCrushListener);
verticalDistortSlider.valueProperty().addListener(verticalDistortListener);
verticalDistortCheckBox.selectedProperty().addListener(verticalDistortListener);
horizontalDistortSlider.valueProperty().addListener(horizontalDistortListener);
horizontalDistortCheckBox.selectedProperty().addListener(horizontalDistortListener);
wobbleSlider.valueProperty().addListener(wobbleListener);
wobbleCheckBox.selectedProperty().addListener(wobbleListener);
wobbleCheckBox.selectedProperty().addListener(e -> wobbleEffect.update());
fileChooser.setInitialFileName("out.wav");
fileChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("All Files", "*.*"),
new FileChooser.ExtensionFilter("WAV Files", "*.wav"),
new FileChooser.ExtensionFilter("Wavefront OBJ Files", "*.obj"),
new FileChooser.ExtensionFilter("SVG Files", "*.svg"),
new FileChooser.ExtensionFilter("Text Files", "*.txt")
);
chooseFileButton.setOnAction(e -> {
File file = fileChooser.showOpenDialog(stage);
if (file != null) {
chooseFile(file);
}
});
recordButton.setOnAction(event -> toggleRecord());
updateObjectRotateSpeed();
renderer.addEffect(EffectType.SCALE, scaleEffect);
renderer.addEffect(EffectType.ROTATE, rotateEffect);
renderer.addEffect(EffectType.TRANSLATE, translateEffect);
executor.submit(producer);
new Thread(renderer).start();
FrequencyAnalyser<List<Shape>, AudioInputStream> analyser = new FrequencyAnalyser<>(renderer, 2, SAMPLE_RATE);
analyser.addListener(this);
analyser.addListener(wobbleEffect);
new Thread(analyser).start();
}
private void toggleRecord() {
recording = !recording;
if (recording) {
recordLabel.setText("Recording...");
recordButton.setText("Stop Recording");
renderer.startRecord();
} else {
recordButton.setText("Record");
AudioInputStream input = renderer.stopRecord();
try {
File file = fileChooser.showSaveDialog(stage);
SimpleDateFormat formatter= new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
Date date = new Date(System.currentTimeMillis());
if (file == null) {
file = new File("out-" + formatter.format(date) + ".wav");
}
AudioSystem.write(input, AudioFileFormat.Type.WAVE, file);
input.close();
recordLabel.setText("Saved to " + file.getAbsolutePath());
} catch (IOException e) {
recordLabel.setText("Error saving file");
e.printStackTrace();
}
}
}
private void updateFocalLength() {
double focalLength = focalLengthSlider.getValue();
producer.setFrameSettings(ObjSettingsFactory.focalLength(focalLength));
}
private void updateObjectRotateSpeed() {
double rotateSpeed = objectRotateSpeedSlider.getValue();
producer.setFrameSettings(
ObjSettingsFactory.rotateSpeed((Math.exp(3 * rotateSpeed) - 1) / 50)
);
}
private void updateTranslation() {
translateEffect.setTranslation(new Vector2(
tryParse(translationXTextField.getText()),
tryParse(translationYTextField.getText())
));
}
private void updateCameraPos() {
producer.setFrameSettings(ObjSettingsFactory.cameraPosition(new Vector3(
tryParse(cameraXTextField.getText()),
tryParse(cameraYTextField.getText()),
tryParse(cameraZTextField.getText())
)));
}
private double tryParse(String value) {
try {
return Double.parseDouble(value);
} catch (NumberFormatException e) {
return 0;
}
}
private void updateEffect(EffectType type, boolean checked, Effect effect) {
if (checked) {
renderer.addEffect(type, effect);
effectTypes.get(type).setDisable(false);
} else {
renderer.removeEffect(type);
effectTypes.get(type).setDisable(true);
}
}
private void chooseFile(File file) {
try {
producer.stop();
String path = file.getAbsolutePath();
FrameSet<List<Shape>> frames = ParserFactory.getParser(path).parse();
frames.addListener(this);
producer = new FrameProducer<>(renderer, frames);
updateObjectRotateSpeed();
updateFocalLength();
executor.submit(producer);
KeyFrame kf1 = new KeyFrame(Duration.seconds(0), e -> wobbleEffect.setVolume(0));
KeyFrame kf2 = new KeyFrame(Duration.seconds(1), e -> {
wobbleEffect.update();
wobbleEffect.setVolume(wobbleSlider.getValue());
});
Timeline timeline = new Timeline(kf1, kf2);
Platform.runLater(timeline::play);
if (file.exists() && !file.isDirectory()) {
fileLabel.setText(path);
objTitledPane.setDisable(!ObjParser.isObjFile(path));
} else {
objTitledPane.setDisable(true);
}
} catch (IOException | ParserConfigurationException | SAXException ioException) {
ioException.printStackTrace();
}
}
public void setStage(Stage stage) {
this.stage = stage;
}
protected boolean mouseRotate() {
return rotateCheckBox.isSelected();
}
protected void disableMouseRotate() {
rotateCheckBox.setSelected(false);
}
protected void setObjRotate(Vector3 vector) {
producer.setFrameSettings(ObjSettingsFactory.rotation(vector));
}
@Override
public void updateFrequency(double leftFrequency, double rightFrequency) {
Platform.runLater(() ->
frequencyLabel.setText(String.format("L Frequency: %d Hz\nR Frequency: %d Hz", Math.round(leftFrequency), Math.round(rightFrequency)))
);
}
@Override
public void update(Object pos) {
if (pos instanceof Vector3 vector) {
Platform.runLater(() -> {
cameraXTextField.setText(String.valueOf(vector.getX()));
cameraYTextField.setText(String.valueOf(vector.getY()));
cameraZTextField.setText(String.valueOf(vector.getZ()));
});
}
}
}
package sh.ball.gui;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.scene.control.*;
import javafx.util.Duration;
import sh.ball.audio.*;
import sh.ball.audio.effect.*;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
import javafx.beans.InvalidationListener;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException;
import sh.ball.audio.effect.Effect;
import sh.ball.audio.effect.EffectType;
import sh.ball.engine.Vector3;
import sh.ball.parser.obj.Listener;
import sh.ball.parser.obj.ObjSettingsFactory;
import sh.ball.parser.obj.ObjParser;
import sh.ball.parser.ParserFactory;
import sh.ball.shapes.Shape;
import sh.ball.shapes.Vector2;
public class Controller implements Initializable, FrequencyListener, Listener {
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();
private final Renderer<List<Shape>, AudioInputStream> renderer;
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final RotateEffect rotateEffect = new RotateEffect(SAMPLE_RATE);
private final TranslateEffect translateEffect = new TranslateEffect(SAMPLE_RATE);
private final WobbleEffect wobbleEffect = new WobbleEffect(SAMPLE_RATE);
private final ScaleEffect scaleEffect = new ScaleEffect();
private FrameProducer<List<Shape>, AudioInputStream> producer;
private boolean recording = false;
private Stage stage;
@FXML
private Label frequencyLabel;
@FXML
private Button chooseFileButton;
@FXML
private Label fileLabel;
@FXML
private Button recordButton;
@FXML
private Label recordLabel;
@FXML
private TextField translationXTextField;
@FXML
private TextField translationYTextField;
@FXML
private Slider weightSlider;
@FXML
private Slider rotateSpeedSlider;
@FXML
private Slider translationSpeedSlider;
@FXML
private Slider scaleSlider;
@FXML
private TitledPane objTitledPane;
@FXML
private Slider focalLengthSlider;
@FXML
private TextField cameraXTextField;
@FXML
private TextField cameraYTextField;
@FXML
private TextField cameraZTextField;
@FXML
private Slider objectRotateSpeedSlider;
@FXML
private CheckBox rotateCheckBox;
@FXML
private CheckBox vectorCancellingCheckBox;
@FXML
private Slider vectorCancellingSlider;
@FXML
private CheckBox bitCrushCheckBox;
@FXML
private Slider bitCrushSlider;
@FXML
private CheckBox verticalDistortCheckBox;
@FXML
private Slider verticalDistortSlider;
@FXML
private CheckBox horizontalDistortCheckBox;
@FXML
private Slider horizontalDistortSlider;
@FXML
private CheckBox wobbleCheckBox;
@FXML
private Slider wobbleSlider;
public Controller(Renderer<List<Shape>, AudioInputStream> renderer) throws IOException {
this.renderer = renderer;
FrameSet<List<Shape>> frames = new ObjParser(DEFAULT_OBJ).parse();
frames.addListener(this);
this.producer = new FrameProducer<>(renderer, frames);
}
private Map<Slider, Consumer<Double>> initializeSliderMap() {
return Map.of(
weightSlider,
renderer::setQuality,
rotateSpeedSlider,
rotateEffect::setSpeed,
translationSpeedSlider,
translateEffect::setSpeed,
scaleSlider,
scaleEffect::setScale,
focalLengthSlider,
d -> updateFocalLength(),
objectRotateSpeedSlider,
d -> updateObjectRotateSpeed()
);
}
private Map<EffectType, Slider> effectTypes;
private void initializeEffectTypes() {
effectTypes = Map.of(
EffectType.VECTOR_CANCELLING,
vectorCancellingSlider,
EffectType.BIT_CRUSH,
bitCrushSlider,
EffectType.VERTICAL_DISTORT,
verticalDistortSlider,
EffectType.HORIZONTAL_DISTORT,
horizontalDistortSlider,
EffectType.WOBBLE,
wobbleSlider
);
}
@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
Map<Slider, Consumer<Double>> sliders = initializeSliderMap();
initializeEffectTypes();
for (Slider slider : sliders.keySet()) {
slider.valueProperty().addListener((source, oldValue, newValue) ->
sliders.get(slider).accept(slider.getValue())
);
}
translationXTextField.textProperty().addListener(e -> updateTranslation());
translationYTextField.textProperty().addListener(e -> updateTranslation());
cameraXTextField.focusedProperty().addListener(e -> updateCameraPos());
cameraYTextField.focusedProperty().addListener(e -> updateCameraPos());
cameraZTextField.focusedProperty().addListener(e -> updateCameraPos());
InvalidationListener vectorCancellingListener = e ->
updateEffect(EffectType.VECTOR_CANCELLING, vectorCancellingCheckBox.isSelected(),
EffectFactory.vectorCancelling((int) vectorCancellingSlider.getValue()));
InvalidationListener bitCrushListener = e ->
updateEffect(EffectType.BIT_CRUSH, bitCrushCheckBox.isSelected(),
EffectFactory.bitCrush(bitCrushSlider.getValue()));
InvalidationListener verticalDistortListener = e ->
updateEffect(EffectType.VERTICAL_DISTORT, verticalDistortCheckBox.isSelected(),
EffectFactory.verticalDistort(verticalDistortSlider.getValue()));
InvalidationListener horizontalDistortListener = e ->
updateEffect(EffectType.HORIZONTAL_DISTORT, horizontalDistortCheckBox.isSelected(),
EffectFactory.horizontalDistort(horizontalDistortSlider.getValue()));
InvalidationListener wobbleListener = e -> {
wobbleEffect.setVolume(wobbleSlider.getValue());
updateEffect(EffectType.WOBBLE, wobbleCheckBox.isSelected(), wobbleEffect);
};
vectorCancellingSlider.valueProperty().addListener(vectorCancellingListener);
vectorCancellingCheckBox.selectedProperty().addListener(vectorCancellingListener);
bitCrushSlider.valueProperty().addListener(bitCrushListener);
bitCrushCheckBox.selectedProperty().addListener(bitCrushListener);
verticalDistortSlider.valueProperty().addListener(verticalDistortListener);
verticalDistortCheckBox.selectedProperty().addListener(verticalDistortListener);
horizontalDistortSlider.valueProperty().addListener(horizontalDistortListener);
horizontalDistortCheckBox.selectedProperty().addListener(horizontalDistortListener);
wobbleSlider.valueProperty().addListener(wobbleListener);
wobbleCheckBox.selectedProperty().addListener(wobbleListener);
wobbleCheckBox.selectedProperty().addListener(e -> wobbleEffect.update());
fileChooser.setInitialFileName("out.wav");
fileChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("All Files", "*.*"),
new FileChooser.ExtensionFilter("WAV Files", "*.wav"),
new FileChooser.ExtensionFilter("Wavefront OBJ Files", "*.obj"),
new FileChooser.ExtensionFilter("SVG Files", "*.svg"),
new FileChooser.ExtensionFilter("Text Files", "*.txt")
);
chooseFileButton.setOnAction(e -> {
File file = fileChooser.showOpenDialog(stage);
if (file != null) {
chooseFile(file);
}
});
recordButton.setOnAction(event -> toggleRecord());
updateObjectRotateSpeed();
renderer.addEffect(EffectType.SCALE, scaleEffect);
renderer.addEffect(EffectType.ROTATE, rotateEffect);
renderer.addEffect(EffectType.TRANSLATE, translateEffect);
executor.submit(producer);
new Thread(renderer).start();
FrequencyAnalyser<List<Shape>, AudioInputStream> analyser = new FrequencyAnalyser<>(renderer, 2, SAMPLE_RATE);
analyser.addListener(this);
analyser.addListener(wobbleEffect);
new Thread(analyser).start();
}
private void toggleRecord() {
recording = !recording;
if (recording) {
recordLabel.setText("Recording...");
recordButton.setText("Stop Recording");
renderer.startRecord();
} else {
recordButton.setText("Record");
AudioInputStream input = renderer.stopRecord();
try {
File file = fileChooser.showSaveDialog(stage);
SimpleDateFormat formatter= new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
Date date = new Date(System.currentTimeMillis());
if (file == null) {
file = new File("out-" + formatter.format(date) + ".wav");
}
AudioSystem.write(input, AudioFileFormat.Type.WAVE, file);
input.close();
recordLabel.setText("Saved to " + file.getAbsolutePath());
} catch (IOException e) {
recordLabel.setText("Error saving file");
e.printStackTrace();
}
}
}
private void updateFocalLength() {
double focalLength = focalLengthSlider.getValue();
producer.setFrameSettings(ObjSettingsFactory.focalLength(focalLength));
}
private void updateObjectRotateSpeed() {
double rotateSpeed = objectRotateSpeedSlider.getValue();
producer.setFrameSettings(
ObjSettingsFactory.rotateSpeed((Math.exp(3 * rotateSpeed) - 1) / 50)
);
}
private void updateTranslation() {
translateEffect.setTranslation(new Vector2(
tryParse(translationXTextField.getText()),
tryParse(translationYTextField.getText())
));
}
private void updateCameraPos() {
producer.setFrameSettings(ObjSettingsFactory.cameraPosition(new Vector3(
tryParse(cameraXTextField.getText()),
tryParse(cameraYTextField.getText()),
tryParse(cameraZTextField.getText())
)));
}
private double tryParse(String value) {
try {
return Double.parseDouble(value);
} catch (NumberFormatException e) {
return 0;
}
}
private void updateEffect(EffectType type, boolean checked, Effect effect) {
if (checked) {
renderer.addEffect(type, effect);
effectTypes.get(type).setDisable(false);
} else {
renderer.removeEffect(type);
effectTypes.get(type).setDisable(true);
}
}
private void chooseFile(File file) {
try {
producer.stop();
String path = file.getAbsolutePath();
FrameSet<List<Shape>> frames = ParserFactory.getParser(path).parse();
frames.addListener(this);
producer = new FrameProducer<>(renderer, frames);
updateObjectRotateSpeed();
updateFocalLength();
executor.submit(producer);
KeyFrame kf1 = new KeyFrame(Duration.seconds(0), e -> wobbleEffect.setVolume(0));
KeyFrame kf2 = new KeyFrame(Duration.seconds(1), e -> {
wobbleEffect.update();
wobbleEffect.setVolume(wobbleSlider.getValue());
});
Timeline timeline = new Timeline(kf1, kf2);
Platform.runLater(timeline::play);
if (file.exists() && !file.isDirectory()) {
fileLabel.setText(path);
objTitledPane.setDisable(!ObjParser.isObjFile(path));
} else {
objTitledPane.setDisable(true);
}
} catch (IOException | ParserConfigurationException | SAXException ioException) {
ioException.printStackTrace();
}
}
public void setStage(Stage stage) {
this.stage = stage;
}
protected boolean mouseRotate() {
return rotateCheckBox.isSelected();
}
protected void disableMouseRotate() {
rotateCheckBox.setSelected(false);
}
protected void setObjRotate(Vector3 vector) {
producer.setFrameSettings(ObjSettingsFactory.rotation(vector));
}
@Override
public void updateFrequency(double leftFrequency, double rightFrequency) {
Platform.runLater(() ->
frequencyLabel.setText(String.format("L/R Frequency:\n%d Hz / %d Hz", Math.round(leftFrequency), Math.round(rightFrequency)))
);
}
@Override
public void update(Object pos) {
if (pos instanceof Vector3 vector) {
Platform.runLater(() -> {
cameraXTextField.setText(String.valueOf(round(vector.getX(), 3)));
cameraYTextField.setText(String.valueOf(round(vector.getY(), 3)));
cameraZTextField.setText(String.valueOf(round(vector.getZ(), 3)));
});
}
}
private static double round(double value, double places) {
if (places < 0) throw new IllegalArgumentException();
long factor = (long) Math.pow(10, places);
value = value * factor;
long tmp = Math.round(value);
return (double) tmp / factor;
}
}

Wyświetl plik

@ -1,5 +1,5 @@
package sh.ball.gui;
enum EffectType {
VECTOR_CANCELLING
}
package sh.ball.gui;
enum EffectType {
VECTOR_CANCELLING
}

Wyświetl plik

@ -1,68 +1,66 @@
package sh.ball.gui;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import sh.ball.audio.AudioPlayer;
import sh.ball.engine.Vector3;
import java.util.Objects;
public class Gui extends Application {
private static final int SAMPLE_RATE = 192000;
@Override
public void start(Stage stage) throws Exception {
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
System.setProperty("prism.lcdtext", "false");
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();
stage.getIcons().add(new Image(Objects.requireNonNull(getClass().getResourceAsStream("/icons/icon.png"))));
stage.setTitle("osci-render");
Scene scene = new Scene(root);
scene.getStylesheets().add(getClass().getResource("/css/main.css").toExternalForm());
scene.addEventFilter(MouseEvent.MOUSE_MOVED, event -> {
if (controller.mouseRotate()) {
controller.setObjRotate(new Vector3(
3 * Math.PI * (event.getSceneY() / scene.getHeight()),
3 * Math.PI * (event.getSceneX() / scene.getWidth()),
0
));
}
});
scene.addEventHandler(KeyEvent.KEY_PRESSED, t -> {
if (t.getCode() == KeyCode.ESCAPE) {
controller.disableMouseRotate();
}
});
stage.setScene(scene);
stage.setResizable(false);
controller.setStage(stage);
stage.show();
stage.setOnCloseRequest(t -> {
Platform.exit();
System.exit(0);
});
}
public static void main(String[] args) {
launch(args);
}
}
package sh.ball.gui;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;
import sh.ball.audio.AudioPlayer;
import sh.ball.engine.Vector3;
import java.util.Objects;
public class Gui extends Application {
private static final int SAMPLE_RATE = 192000;
@Override
public void start(Stage stage) throws Exception {
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
System.setProperty("prism.lcdtext", "false");
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();
stage.getIcons().add(new Image(Objects.requireNonNull(getClass().getResourceAsStream("/icons/icon.png"))));
stage.setTitle("osci-render");
Scene scene = new Scene(root);
scene.getStylesheets().add(getClass().getResource("/css/main.css").toExternalForm());
scene.addEventFilter(MouseEvent.MOUSE_MOVED, event -> {
if (controller.mouseRotate()) {
controller.setObjRotate(new Vector3(
3 * Math.PI * (event.getSceneY() / scene.getHeight()),
3 * Math.PI * (event.getSceneX() / scene.getWidth()),
0
));
}
});
scene.addEventHandler(KeyEvent.KEY_PRESSED, t -> {
if (t.getCode() == KeyCode.ESCAPE) {
controller.disableMouseRotate();
}
});
stage.setScene(scene);
stage.setResizable(false);
controller.setStage(stage);
stage.show();
stage.setOnCloseRequest(t -> {
Platform.exit();
System.exit(0);
});
}
public static void main(String[] args) {
launch(args);
}
}

Wyświetl plik

@ -1,7 +1,7 @@
package sh.ball.gui;
@FunctionalInterface
public interface Settable<T> {
void set(T value);
}
package sh.ball.gui;
@FunctionalInterface
public interface Settable<T> {
void set(T value);
}

Wyświetl plik

@ -1,25 +1,25 @@
package sh.ball.parser;
import java.io.IOException;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException;
public abstract class FileParser<T> {
public abstract String getFileExtension();
protected void checkFileExtension(String path) throws IllegalArgumentException {
if (!hasCorrectFileExtension(path)) {
throw new IllegalArgumentException(
"File to parse is not a ." + getFileExtension() + " file.");
}
}
public boolean hasCorrectFileExtension(String path) {
return path.matches(".*\\." + getFileExtension());
}
public abstract T parse()
throws ParserConfigurationException, IOException, SAXException, IllegalArgumentException;
}
package sh.ball.parser;
import java.io.IOException;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException;
public abstract class FileParser<T> {
public abstract String getFileExtension();
protected void checkFileExtension(String path) throws IllegalArgumentException {
if (!hasCorrectFileExtension(path)) {
throw new IllegalArgumentException(
"File to parse is not a ." + getFileExtension() + " file.");
}
}
public boolean hasCorrectFileExtension(String path) {
return path.matches(".*\\." + getFileExtension());
}
public abstract T parse()
throws ParserConfigurationException, IOException, SAXException, IllegalArgumentException;
}

Wyświetl plik

@ -1,28 +1,28 @@
package sh.ball.parser;
import org.xml.sax.SAXException;
import sh.ball.audio.FrameSet;
import sh.ball.parser.obj.ObjParser;
import sh.ball.parser.svg.SvgParser;
import sh.ball.parser.txt.TextParser;
import sh.ball.shapes.Shape;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.IOException;
import java.util.List;
public class ParserFactory {
public static FileParser<FrameSet<List<Shape>>> getParser(String filePath) throws IOException, ParserConfigurationException, SAXException {
if (ObjParser.isObjFile(filePath)) {
return new ObjParser(filePath);
} else if (SvgParser.isSvgFile(filePath)) {
return new SvgParser(filePath);
} else if (TextParser.isTxtFile(filePath)) {
return new TextParser(filePath);
}
throw new IOException("No known parser that can parse " + new File(filePath).getName());
}
}
package sh.ball.parser;
import org.xml.sax.SAXException;
import sh.ball.audio.FrameSet;
import sh.ball.parser.obj.ObjParser;
import sh.ball.parser.svg.SvgParser;
import sh.ball.parser.txt.TextParser;
import sh.ball.shapes.Shape;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.IOException;
import java.util.List;
public class ParserFactory {
public static FileParser<FrameSet<List<Shape>>> getParser(String filePath) throws IOException, ParserConfigurationException, SAXException {
if (ObjParser.isObjFile(filePath)) {
return new ObjParser(filePath);
} else if (SvgParser.isSvgFile(filePath)) {
return new SvgParser(filePath);
} else if (TextParser.isTxtFile(filePath)) {
return new TextParser(filePath);
}
throw new IOException("No known parser that can parse " + new File(filePath).getName());
}
}

Wyświetl plik

@ -1,80 +1,80 @@
package sh.ball.parser;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.RandomAccess;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
public final class XmlUtil {
private XmlUtil() {
}
public static List<Node> asList(NodeList n) {
return n.getLength() == 0 ?
Collections.emptyList() : new NodeListWrapper(n);
}
static final class NodeListWrapper extends AbstractList<Node>
implements RandomAccess {
private final NodeList list;
NodeListWrapper(NodeList l) {
list = l;
}
public Node get(int index) {
return list.item(index);
}
public int size() {
return list.getLength();
}
}
public static String getNodeValue(Node node, String namedItem) {
Node attribute = node.getAttributes().getNamedItem(namedItem);
return attribute == null ? null : attribute.getNodeValue();
}
public static Document getXMLDocument(InputStream input)
throws IOException, SAXException, ParserConfigurationException {
// Opens XML reader for svg file.
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// Remove validation which massively slows down file parsing
factory.setNamespaceAware(false);
factory.setValidating(false);
factory.setFeature("http://xml.org/sax/features/namespaces", false);
factory.setFeature("http://xml.org/sax/features/validation", false);
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false);
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
DocumentBuilder builder = factory.newDocumentBuilder();
return builder.parse(input);
}
public static List<Node> getAttributesOnTags(Document xml, String tagName, String attribute) {
List<Node> attributes = new ArrayList<>();
for (Node elem : asList(xml.getElementsByTagName(tagName))) {
attributes.add(elem.getAttributes().getNamedItem(attribute));
}
return attributes;
}
}
package sh.ball.parser;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.RandomAccess;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
public final class XmlUtil {
private XmlUtil() {
}
public static List<Node> asList(NodeList n) {
return n.getLength() == 0 ?
Collections.emptyList() : new NodeListWrapper(n);
}
static final class NodeListWrapper extends AbstractList<Node>
implements RandomAccess {
private final NodeList list;
NodeListWrapper(NodeList l) {
list = l;
}
public Node get(int index) {
return list.item(index);
}
public int size() {
return list.getLength();
}
}
public static String getNodeValue(Node node, String namedItem) {
Node attribute = node.getAttributes().getNamedItem(namedItem);
return attribute == null ? null : attribute.getNodeValue();
}
public static Document getXMLDocument(InputStream input)
throws IOException, SAXException, ParserConfigurationException {
// Opens XML reader for svg file.
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// Remove validation which massively slows down file parsing
factory.setNamespaceAware(false);
factory.setValidating(false);
factory.setFeature("http://xml.org/sax/features/namespaces", false);
factory.setFeature("http://xml.org/sax/features/validation", false);
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false);
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
DocumentBuilder builder = factory.newDocumentBuilder();
return builder.parse(input);
}
public static List<Node> getAttributesOnTags(Document xml, String tagName, String attribute) {
List<Node> attributes = new ArrayList<>();
for (Node elem : asList(xml.getElementsByTagName(tagName))) {
attributes.add(elem.getAttributes().getNamedItem(attribute));
}
return attributes;
}
}

Wyświetl plik

@ -1,6 +1,6 @@
package sh.ball.parser.obj;
public interface Listener {
void update(Object obj);
}
package sh.ball.parser.obj;
public interface Listener {
void update(Object obj);
}

Wyświetl plik

@ -1,72 +1,72 @@
package sh.ball.parser.obj;
import sh.ball.audio.FrameSet;
import sh.ball.engine.Camera;
import sh.ball.engine.Vector3;
import sh.ball.engine.WorldObject;
import sh.ball.shapes.Shape;
import java.util.ArrayList;
import java.util.List;
public class ObjFrameSet implements FrameSet<List<Shape>> {
private final WorldObject object;
private final Camera camera;
private final List<Listener> listeners = new ArrayList<>();
private Vector3 rotation = new Vector3();
private Double rotateSpeed = 0.0;
public ObjFrameSet(WorldObject object, Camera camera) {
this.object = object;
this.camera = camera;
}
@Override
public void addListener(Listener listener) {
listeners.add(listener);
notifyListener(listener);
}
private void notifyListener(Listener listener) {
listener.update(camera.getPos());
}
private void notifyListeners() {
listeners.forEach(this::notifyListener);
}
@Override
public List<Shape> next() {
if (rotateSpeed == 0) {
object.setRotation(rotation);
} else {
object.rotate(rotation.scale(rotateSpeed));
}
return camera.draw(object);
}
// TODO: Refactor!
@Override
public void setFrameSettings(Object settings) {
if (settings instanceof ObjFrameSettings obj) {
if (obj.focalLength != null && camera.getFocalLength() != obj.focalLength) {
camera.setFocalLength(obj.focalLength);
}
if (obj.cameraPos != null && camera.getPos() != obj.cameraPos) {
camera.setPos(obj.cameraPos);
notifyListeners();
}
if (obj.rotation != null) {
this.rotation = obj.rotation;
}
if (obj.rotateSpeed != null) {
this.rotateSpeed = obj.rotateSpeed;
}
if (obj.resetRotation) {
object.resetRotation();
}
}
}
}
package sh.ball.parser.obj;
import sh.ball.audio.FrameSet;
import sh.ball.engine.Camera;
import sh.ball.engine.Vector3;
import sh.ball.engine.WorldObject;
import sh.ball.shapes.Shape;
import java.util.ArrayList;
import java.util.List;
public class ObjFrameSet implements FrameSet<List<Shape>> {
private final WorldObject object;
private final Camera camera;
private final List<Listener> listeners = new ArrayList<>();
private Vector3 rotation = new Vector3();
private Double rotateSpeed = 0.0;
public ObjFrameSet(WorldObject object, Camera camera) {
this.object = object;
this.camera = camera;
}
@Override
public void addListener(Listener listener) {
listeners.add(listener);
notifyListener(listener);
}
private void notifyListener(Listener listener) {
listener.update(camera.getPos());
}
private void notifyListeners() {
listeners.forEach(this::notifyListener);
}
@Override
public List<Shape> next() {
if (rotateSpeed == 0) {
object.setRotation(rotation);
} else {
object.rotate(rotation.scale(rotateSpeed));
}
return camera.draw(object);
}
// TODO: Refactor!
@Override
public void setFrameSettings(Object settings) {
if (settings instanceof ObjFrameSettings obj) {
if (obj.focalLength != null && camera.getFocalLength() != obj.focalLength) {
camera.setFocalLength(obj.focalLength);
}
if (obj.cameraPos != null && camera.getPos() != obj.cameraPos) {
camera.setPos(obj.cameraPos);
notifyListeners();
}
if (obj.rotation != null) {
this.rotation = obj.rotation;
}
if (obj.rotateSpeed != null) {
this.rotateSpeed = obj.rotateSpeed;
}
if (obj.resetRotation) {
object.resetRotation();
}
}
}
}

Wyświetl plik

@ -1,29 +1,29 @@
package sh.ball.parser.obj;
import sh.ball.engine.Vector3;
public class ObjFrameSettings {
protected Double focalLength;
protected Vector3 cameraPos;
protected Vector3 rotation;
protected Double rotateSpeed;
protected boolean resetRotation = false;
protected ObjFrameSettings(double focalLength) {
this.focalLength = focalLength;
}
protected ObjFrameSettings(Vector3 cameraPos) {
this.cameraPos = cameraPos;
}
protected ObjFrameSettings(Vector3 rotation, Double rotateSpeed) {
this.rotation = rotation;
this.rotateSpeed = rotateSpeed;
}
protected ObjFrameSettings(boolean resetRotation) {
this.resetRotation = resetRotation;
}
}
package sh.ball.parser.obj;
import sh.ball.engine.Vector3;
public class ObjFrameSettings {
protected Double focalLength;
protected Vector3 cameraPos;
protected Vector3 rotation;
protected Double rotateSpeed;
protected boolean resetRotation = false;
protected ObjFrameSettings(double focalLength) {
this.focalLength = focalLength;
}
protected ObjFrameSettings(Vector3 cameraPos) {
this.cameraPos = cameraPos;
}
protected ObjFrameSettings(Vector3 rotation, Double rotateSpeed) {
this.rotation = rotation;
this.rotateSpeed = rotateSpeed;
}
protected ObjFrameSettings(boolean resetRotation) {
this.resetRotation = resetRotation;
}
}

Wyświetl plik

@ -1,70 +1,70 @@
package sh.ball.parser.obj;
import sh.ball.audio.FrameSet;
import sh.ball.engine.Camera;
import sh.ball.engine.Vector3;
import sh.ball.engine.WorldObject;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import sh.ball.parser.FileParser;
import sh.ball.shapes.Shape;
public class ObjParser extends FileParser<FrameSet<List<Shape>>> {
private final boolean isDefaultPosition;
private final InputStream input;
private final Camera camera;
private WorldObject object;
public ObjParser(InputStream input, float cameraX, float cameraY, float cameraZ,
float focalLength, boolean isDefaultPosition) {
this.input = input;
this.isDefaultPosition = isDefaultPosition;
Vector3 cameraPos = new Vector3(cameraX, cameraY, cameraZ);
this.camera = new Camera(focalLength, cameraPos);
}
public ObjParser(InputStream input, float focalLength) {
this(input, 0, 0, 0, focalLength, true);
}
public ObjParser(InputStream input) {
this(input, (float) Camera.DEFAULT_FOCAL_LENGTH);
}
public ObjParser(String path) throws FileNotFoundException {
this(new FileInputStream(path), (float) Camera.DEFAULT_FOCAL_LENGTH);
}
@Override
public String getFileExtension() {
return "obj";
}
@Override
public FrameSet<List<Shape>> parse() throws IllegalArgumentException, IOException {
object = new WorldObject(input);
camera.findZPos(object);
return new ObjFrameSet(object, camera);
}
// If camera position arguments haven't been specified, automatically work out the position of
// the camera based on the size of the object in the camera's view.
public void setFocalLength(double focalLength) {
camera.setFocalLength(focalLength);
if (isDefaultPosition) {
camera.findZPos(object);
}
}
public static boolean isObjFile(String path) {
return path.matches(".*\\.obj");
}
}
package sh.ball.parser.obj;
import sh.ball.audio.FrameSet;
import sh.ball.engine.Camera;
import sh.ball.engine.Vector3;
import sh.ball.engine.WorldObject;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import sh.ball.parser.FileParser;
import sh.ball.shapes.Shape;
public class ObjParser extends FileParser<FrameSet<List<Shape>>> {
private final boolean isDefaultPosition;
private final InputStream input;
private final Camera camera;
private WorldObject object;
public ObjParser(InputStream input, float cameraX, float cameraY, float cameraZ,
float focalLength, boolean isDefaultPosition) {
this.input = input;
this.isDefaultPosition = isDefaultPosition;
Vector3 cameraPos = new Vector3(cameraX, cameraY, cameraZ);
this.camera = new Camera(focalLength, cameraPos);
}
public ObjParser(InputStream input, float focalLength) {
this(input, 0, 0, 0, focalLength, true);
}
public ObjParser(InputStream input) {
this(input, (float) Camera.DEFAULT_FOCAL_LENGTH);
}
public ObjParser(String path) throws FileNotFoundException {
this(new FileInputStream(path), (float) Camera.DEFAULT_FOCAL_LENGTH);
}
@Override
public String getFileExtension() {
return "obj";
}
@Override
public FrameSet<List<Shape>> parse() throws IllegalArgumentException, IOException {
object = new WorldObject(input);
camera.findZPos(object);
return new ObjFrameSet(object, camera);
}
// If camera position arguments haven't been specified, automatically work out the position of
// the camera based on the size of the object in the camera's view.
public void setFocalLength(double focalLength) {
camera.setFocalLength(focalLength);
if (isDefaultPosition) {
camera.findZPos(object);
}
}
public static boolean isObjFile(String path) {
return path.matches(".*\\.obj");
}
}

Wyświetl plik

@ -1,26 +1,26 @@
package sh.ball.parser.obj;
import sh.ball.engine.Vector3;
public class ObjSettingsFactory {
public static ObjFrameSettings focalLength(double focalLength) {
return new ObjFrameSettings(focalLength);
}
public static ObjFrameSettings cameraPosition(Vector3 cameraPos) {
return new ObjFrameSettings(cameraPos);
}
public static ObjFrameSettings rotation(Vector3 rotation) {
return new ObjFrameSettings(rotation, null);
}
public static ObjFrameSettings rotateSpeed(double rotateSpeed) {
return new ObjFrameSettings(null, rotateSpeed);
}
public static ObjFrameSettings resetRotation() {
return new ObjFrameSettings(true);
}
}
package sh.ball.parser.obj;
import sh.ball.engine.Vector3;
public class ObjSettingsFactory {
public static ObjFrameSettings focalLength(double focalLength) {
return new ObjFrameSettings(focalLength);
}
public static ObjFrameSettings cameraPosition(Vector3 cameraPos) {
return new ObjFrameSettings(cameraPos);
}
public static ObjFrameSettings rotation(Vector3 rotation) {
return new ObjFrameSettings(rotation, null);
}
public static ObjFrameSettings rotateSpeed(double rotateSpeed) {
return new ObjFrameSettings(null, rotateSpeed);
}
public static ObjFrameSettings resetRotation() {
return new ObjFrameSettings(true);
}
}

Wyświetl plik

@ -1,28 +1,28 @@
package sh.ball.parser.svg;
import java.util.List;
import sh.ball.shapes.Line;
import sh.ball.shapes.Shape;
class ClosePath {
// Parses close path commands (Z and z commands)
private static List<Shape> parseClosePath(SvgState state, List<Float> args) {
if (!state.currPoint.equals(state.initialPoint)) {
Line line = new Line(state.currPoint, state.initialPoint);
state.currPoint = state.initialPoint;
return List.of(line);
} else {
return List.of();
}
}
static List<Shape> absolute(SvgState state, List<Float> args) {
return parseClosePath(state, args);
}
static List<Shape> relative(SvgState state, List<Float> args) {
return parseClosePath(state, args);
}
}
package sh.ball.parser.svg;
import java.util.List;
import sh.ball.shapes.Line;
import sh.ball.shapes.Shape;
class ClosePath {
// Parses close path commands (Z and z commands)
private static List<Shape> parseClosePath(SvgState state, List<Float> args) {
if (!state.currPoint.equals(state.initialPoint)) {
Line line = new Line(state.currPoint, state.initialPoint);
state.currPoint = state.initialPoint;
return List.of(line);
} else {
return List.of();
}
}
static List<Shape> absolute(SvgState state, List<Float> args) {
return parseClosePath(state, args);
}
static List<Shape> relative(SvgState state, List<Float> args) {
return parseClosePath(state, args);
}
}

Wyświetl plik

@ -1,105 +1,105 @@
package sh.ball.parser.svg;
import java.util.ArrayList;
import java.util.List;
import sh.ball.shapes.CubicBezierCurve;
import sh.ball.shapes.QuadraticBezierCurve;
import sh.ball.shapes.Shape;
import sh.ball.shapes.Vector2;
class CurveTo {
// Parses curveto commands (C, c, S, s, Q, q, T, and t commands)
// isCubic should be true for parsing C, c, S, and s commands
// isCubic should be false for parsing Q, q, T, and t commands
// isSmooth should be true for parsing S, s, T, and t commands
// isSmooth should be false for parsing C, c, Q, and q commands
private static List<Shape> parseCurveTo(SvgState state, List<Float> args, boolean isAbsolute,
boolean isCubic, boolean isSmooth) {
int expectedArgs = isCubic ? 4 : 2;
if (!isSmooth) {
expectedArgs += 2;
}
if (args.size() % expectedArgs != 0 || args.size() < expectedArgs) {
throw new IllegalArgumentException("SVG curveto command has incorrect number of arguments.");
}
List<Shape> curves = new ArrayList<>();
for (int i = 0; i < args.size(); i += expectedArgs) {
Vector2 controlPoint1;
Vector2 controlPoint2 = new Vector2();
if (isSmooth) {
if (isCubic) {
controlPoint1 = state.prevCubicControlPoint == null ? state.currPoint
: state.prevCubicControlPoint.reflectRelativeToVector(state.currPoint);
} else {
controlPoint1 = state.prevQuadraticControlPoint == null ? state.currPoint
: state.prevQuadraticControlPoint.reflectRelativeToVector(state.currPoint);
}
} else {
controlPoint1 = new Vector2(args.get(i), args.get(i + 1));
}
if (isCubic) {
controlPoint2 = new Vector2(args.get(i + 2), args.get(i + 3));
}
Vector2 newPoint = new Vector2(args.get(i + expectedArgs - 2),
args.get(i + expectedArgs - 1));
if (!isAbsolute) {
controlPoint1 = state.currPoint.translate(controlPoint1);
controlPoint2 = state.currPoint.translate(controlPoint2);
newPoint = state.currPoint.translate(newPoint);
}
if (isCubic) {
curves.add(new CubicBezierCurve(state.currPoint, controlPoint1, controlPoint2, newPoint));
state.currPoint = newPoint;
state.prevCubicControlPoint = controlPoint2;
} else {
curves.add(new QuadraticBezierCurve(state.currPoint, controlPoint1, newPoint));
state.currPoint = newPoint;
state.prevQuadraticControlPoint = controlPoint1;
}
}
return curves;
}
static List<Shape> absolute(SvgState state, List<Float> args) {
return parseCurveTo(state, args, true, true, false);
}
static List<Shape> relative(SvgState state, List<Float> args) {
return parseCurveTo(state, args, false, true, false);
}
static List<Shape> smoothAbsolute(SvgState state, List<Float> args) {
return parseCurveTo(state, args, true, true, true);
}
static List<Shape> smoothRelative(SvgState state, List<Float> args) {
return parseCurveTo(state, args, false, true, true);
}
static List<Shape> quarticAbsolute(SvgState state, List<Float> args) {
return parseCurveTo(state, args, true, false, false);
}
static List<Shape> quarticRelative(SvgState state, List<Float> args) {
return parseCurveTo(state, args, false, false, false);
}
static List<Shape> quarticSmoothAbsolute(SvgState state, List<Float> args) {
return parseCurveTo(state, args, true, false, true);
}
static List<Shape> quarticSmoothRelative(SvgState state, List<Float> args) {
return parseCurveTo(state, args, false, false, true);
}
}
package sh.ball.parser.svg;
import java.util.ArrayList;
import java.util.List;
import sh.ball.shapes.CubicBezierCurve;
import sh.ball.shapes.QuadraticBezierCurve;
import sh.ball.shapes.Shape;
import sh.ball.shapes.Vector2;
class CurveTo {
// Parses curveto commands (C, c, S, s, Q, q, T, and t commands)
// isCubic should be true for parsing C, c, S, and s commands
// isCubic should be false for parsing Q, q, T, and t commands
// isSmooth should be true for parsing S, s, T, and t commands
// isSmooth should be false for parsing C, c, Q, and q commands
private static List<Shape> parseCurveTo(SvgState state, List<Float> args, boolean isAbsolute,
boolean isCubic, boolean isSmooth) {
int expectedArgs = isCubic ? 4 : 2;
if (!isSmooth) {
expectedArgs += 2;
}
if (args.size() % expectedArgs != 0 || args.size() < expectedArgs) {
throw new IllegalArgumentException("SVG curveto command has incorrect number of arguments.");
}
List<Shape> curves = new ArrayList<>();
for (int i = 0; i < args.size(); i += expectedArgs) {
Vector2 controlPoint1;
Vector2 controlPoint2 = new Vector2();
if (isSmooth) {
if (isCubic) {
controlPoint1 = state.prevCubicControlPoint == null ? state.currPoint
: state.prevCubicControlPoint.reflectRelativeToVector(state.currPoint);
} else {
controlPoint1 = state.prevQuadraticControlPoint == null ? state.currPoint
: state.prevQuadraticControlPoint.reflectRelativeToVector(state.currPoint);
}
} else {
controlPoint1 = new Vector2(args.get(i), args.get(i + 1));
}
if (isCubic) {
controlPoint2 = new Vector2(args.get(i + 2), args.get(i + 3));
}
Vector2 newPoint = new Vector2(args.get(i + expectedArgs - 2),
args.get(i + expectedArgs - 1));
if (!isAbsolute) {
controlPoint1 = state.currPoint.translate(controlPoint1);
controlPoint2 = state.currPoint.translate(controlPoint2);
newPoint = state.currPoint.translate(newPoint);
}
if (isCubic) {
curves.add(new CubicBezierCurve(state.currPoint, controlPoint1, controlPoint2, newPoint));
state.currPoint = newPoint;
state.prevCubicControlPoint = controlPoint2;
} else {
curves.add(new QuadraticBezierCurve(state.currPoint, controlPoint1, newPoint));
state.currPoint = newPoint;
state.prevQuadraticControlPoint = controlPoint1;
}
}
return curves;
}
static List<Shape> absolute(SvgState state, List<Float> args) {
return parseCurveTo(state, args, true, true, false);
}
static List<Shape> relative(SvgState state, List<Float> args) {
return parseCurveTo(state, args, false, true, false);
}
static List<Shape> smoothAbsolute(SvgState state, List<Float> args) {
return parseCurveTo(state, args, true, true, true);
}
static List<Shape> smoothRelative(SvgState state, List<Float> args) {
return parseCurveTo(state, args, false, true, true);
}
static List<Shape> quarticAbsolute(SvgState state, List<Float> args) {
return parseCurveTo(state, args, true, false, false);
}
static List<Shape> quarticRelative(SvgState state, List<Float> args) {
return parseCurveTo(state, args, false, false, false);
}
static List<Shape> quarticSmoothAbsolute(SvgState state, List<Float> args) {
return parseCurveTo(state, args, true, false, true);
}
static List<Shape> quarticSmoothRelative(SvgState state, List<Float> args) {
return parseCurveTo(state, args, false, false, true);
}
}

Wyświetl plik

@ -1,146 +1,146 @@
package sh.ball.parser.svg;
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.util.ArrayList;
import java.util.List;
import sh.ball.shapes.Line;
import sh.ball.shapes.Shape;
import sh.ball.shapes.Vector2;
class EllipticalArcTo {
private static final int EXPECTED_ARGS = 7;
private static List<Shape> parseEllipticalArc(SvgState state, List<Float> args, boolean isAbsolute) {
if (args.size() % EXPECTED_ARGS != 0 || args.size() < EXPECTED_ARGS) {
throw new IllegalArgumentException(
"SVG elliptical arc command has incorrect number of arguments.");
}
List<Shape> arcs = new ArrayList<>();
for (int i = 0; i < args.size(); i += EXPECTED_ARGS) {
Vector2 newPoint = new Vector2(args.get(i + 5), args.get(i + 6));
newPoint = isAbsolute ? newPoint : state.currPoint.translate(newPoint);
arcs.addAll(createArc(
state.currPoint,
args.get(i),
args.get(i + 1),
args.get(i + 2),
args.get(i + 3) == 1,
args.get(i + 4) == 1,
newPoint
));
state.currPoint = newPoint;
}
return arcs;
}
// The following algorithm is completely based on https://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
private static List<Shape> createArc(Vector2 start, double rx, double ry, float theta, boolean largeArcFlag, boolean sweepFlag, Vector2 end) {
double x2 = end.getX();
double y2 = end.getY();
// Ensure radii are valid
if (rx == 0 || ry == 0) {
return List.of(new Line(start, end));
}
double x1 = start.getX();
double y1 = start.getY();
// Compute the half distance between the current and the final point
double dx2 = (x1 - x2) / 2.0;
double dy2 = (y1 - y2) / 2.0;
// Convert theta from degrees to radians
theta = (float) Math.toRadians(theta % 360);
//
// Step 1 : Compute (x1', y1')
//
double x1prime = Math.cos(theta) * dx2 + Math.sin(theta) * dy2;
double y1prime = -Math.sin(theta) * dx2 + Math.cos(theta) * dy2;
// Ensure radii are large enough
rx = Math.abs(rx);
ry = Math.abs(ry);
double Prx = rx * rx;
double Pry = ry * ry;
double Px1prime = x1prime * x1prime;
double Py1prime = y1prime * y1prime;
double d = Px1prime / Prx + Py1prime / Pry;
if (d > 1) {
rx = Math.abs(Math.sqrt(d) * rx);
ry = Math.abs(Math.sqrt(d) * ry);
Prx = rx * rx;
Pry = ry * ry;
}
//
// Step 2 : Compute (cx', cy')
//
double sign = (largeArcFlag == sweepFlag) ? -1.0 : 1.0;
// Forcing the inner term to be positive. It should be >= 0 but can sometimes be negative due
// to double precision.
double coef = sign *
Math.sqrt(Math.abs((Prx * Pry) - (Prx * Py1prime) - (Pry * Px1prime)) / ((Prx * Py1prime) + (Pry * Px1prime)));
double cxprime = coef * ((rx * y1prime) / ry);
double cyprime = coef * -((ry * x1prime) / rx);
//
// Step 3 : Compute (cx, cy) from (cx', cy')
//
double sx2 = (x1 + x2) / 2.0;
double sy2 = (y1 + y2) / 2.0;
double cx = sx2 + Math.cos(theta) * cxprime - Math.sin(theta) * cyprime;
double cy = sy2 + Math.sin(theta) * cxprime + Math.cos(theta) * cyprime;
//
// Step 4 : Compute the angleStart (theta1) and the angleExtent (dtheta)
//
double ux = (x1prime - cxprime) / rx;
double uy = (y1prime - cyprime) / ry;
double vx = (-x1prime - cxprime) / rx;
double vy = (-y1prime - cyprime) / ry;
double p, n;
// Compute the angle start
n = Math.sqrt((ux * ux) + (uy * uy));
p = ux; // (1 * ux) + (0 * uy)
sign = (uy < 0) ? -1.0 : 1.0;
double angleStart = Math.toDegrees(sign * Math.acos(p / n));
// Compute the angle extent
n = Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy));
p = ux * vx + uy * vy;
sign = (ux * vy - uy * vx < 0) ? -1.0 : 1.0;
double angleExtent = Math.toDegrees(sign * Math.acos(p / n));
if (!sweepFlag && angleExtent > 0) {
angleExtent -= 360;
} else if (sweepFlag && angleExtent < 0) {
angleExtent += 360;
}
angleExtent %= 360;
angleStart %= 360;
Arc2D.Float arc = new Arc2D.Float();
arc.x = (float) (cx - rx);
arc.y = (float) (cy - ry);
arc.width = (float) (rx * 2.0);
arc.height = (float) (ry * 2.0);
arc.start = (float) -angleStart;
arc.extent = (float) -angleExtent;
AffineTransform transform = AffineTransform.getRotateInstance(theta, arc.getX() + arc.getWidth()/2, arc.getY() + arc.getHeight()/2);
return Shape.convert(transform.createTransformedShape(arc));
}
static List<Shape> absolute(SvgState state, List<Float> args) {
return parseEllipticalArc(state, args, true);
}
static List<Shape> relative(SvgState state, List<Float> args) {
return parseEllipticalArc(state, args, false);
}
}
package sh.ball.parser.svg;
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.util.ArrayList;
import java.util.List;
import sh.ball.shapes.Line;
import sh.ball.shapes.Shape;
import sh.ball.shapes.Vector2;
class EllipticalArcTo {
private static final int EXPECTED_ARGS = 7;
private static List<Shape> parseEllipticalArc(SvgState state, List<Float> args, boolean isAbsolute) {
if (args.size() % EXPECTED_ARGS != 0 || args.size() < EXPECTED_ARGS) {
throw new IllegalArgumentException(
"SVG elliptical arc command has incorrect number of arguments.");
}
List<Shape> arcs = new ArrayList<>();
for (int i = 0; i < args.size(); i += EXPECTED_ARGS) {
Vector2 newPoint = new Vector2(args.get(i + 5), args.get(i + 6));
newPoint = isAbsolute ? newPoint : state.currPoint.translate(newPoint);
arcs.addAll(createArc(
state.currPoint,
args.get(i),
args.get(i + 1),
args.get(i + 2),
args.get(i + 3) == 1,
args.get(i + 4) == 1,
newPoint
));
state.currPoint = newPoint;
}
return arcs;
}
// The following algorithm is completely based on https://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
private static List<Shape> createArc(Vector2 start, double rx, double ry, float theta, boolean largeArcFlag, boolean sweepFlag, Vector2 end) {
double x2 = end.getX();
double y2 = end.getY();
// Ensure radii are valid
if (rx == 0 || ry == 0) {
return List.of(new Line(start, end));
}
double x1 = start.getX();
double y1 = start.getY();
// Compute the half distance between the current and the final point
double dx2 = (x1 - x2) / 2.0;
double dy2 = (y1 - y2) / 2.0;
// Convert theta from degrees to radians
theta = (float) Math.toRadians(theta % 360);
//
// Step 1 : Compute (x1', y1')
//
double x1prime = Math.cos(theta) * dx2 + Math.sin(theta) * dy2;
double y1prime = -Math.sin(theta) * dx2 + Math.cos(theta) * dy2;
// Ensure radii are large enough
rx = Math.abs(rx);
ry = Math.abs(ry);
double Prx = rx * rx;
double Pry = ry * ry;
double Px1prime = x1prime * x1prime;
double Py1prime = y1prime * y1prime;
double d = Px1prime / Prx + Py1prime / Pry;
if (d > 1) {
rx = Math.abs(Math.sqrt(d) * rx);
ry = Math.abs(Math.sqrt(d) * ry);
Prx = rx * rx;
Pry = ry * ry;
}
//
// Step 2 : Compute (cx', cy')
//
double sign = (largeArcFlag == sweepFlag) ? -1.0 : 1.0;
// Forcing the inner term to be positive. It should be >= 0 but can sometimes be negative due
// to double precision.
double coef = sign *
Math.sqrt(Math.abs((Prx * Pry) - (Prx * Py1prime) - (Pry * Px1prime)) / ((Prx * Py1prime) + (Pry * Px1prime)));
double cxprime = coef * ((rx * y1prime) / ry);
double cyprime = coef * -((ry * x1prime) / rx);
//
// Step 3 : Compute (cx, cy) from (cx', cy')
//
double sx2 = (x1 + x2) / 2.0;
double sy2 = (y1 + y2) / 2.0;
double cx = sx2 + Math.cos(theta) * cxprime - Math.sin(theta) * cyprime;
double cy = sy2 + Math.sin(theta) * cxprime + Math.cos(theta) * cyprime;
//
// Step 4 : Compute the angleStart (theta1) and the angleExtent (dtheta)
//
double ux = (x1prime - cxprime) / rx;
double uy = (y1prime - cyprime) / ry;
double vx = (-x1prime - cxprime) / rx;
double vy = (-y1prime - cyprime) / ry;
double p, n;
// Compute the angle start
n = Math.sqrt((ux * ux) + (uy * uy));
p = ux; // (1 * ux) + (0 * uy)
sign = (uy < 0) ? -1.0 : 1.0;
double angleStart = Math.toDegrees(sign * Math.acos(p / n));
// Compute the angle extent
n = Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy));
p = ux * vx + uy * vy;
sign = (ux * vy - uy * vx < 0) ? -1.0 : 1.0;
double angleExtent = Math.toDegrees(sign * Math.acos(p / n));
if (!sweepFlag && angleExtent > 0) {
angleExtent -= 360;
} else if (sweepFlag && angleExtent < 0) {
angleExtent += 360;
}
angleExtent %= 360;
angleStart %= 360;
Arc2D.Float arc = new Arc2D.Float();
arc.x = (float) (cx - rx);
arc.y = (float) (cy - ry);
arc.width = (float) (rx * 2.0);
arc.height = (float) (ry * 2.0);
arc.start = (float) -angleStart;
arc.extent = (float) -angleExtent;
AffineTransform transform = AffineTransform.getRotateInstance(theta, arc.getX() + arc.getWidth()/2, arc.getY() + arc.getHeight()/2);
return Shape.convert(transform.createTransformedShape(arc));
}
static List<Shape> absolute(SvgState state, List<Float> args) {
return parseEllipticalArc(state, args, true);
}
static List<Shape> relative(SvgState state, List<Float> args) {
return parseEllipticalArc(state, args, false);
}
}

Wyświetl plik

@ -1,75 +1,75 @@
package sh.ball.parser.svg;
import java.util.ArrayList;
import java.util.List;
import sh.ball.shapes.Line;
import sh.ball.shapes.Shape;
import sh.ball.shapes.Vector2;
class LineTo {
// Parses lineto commands (L, l, H, h, V, and v commands)
// isHorizontal and isVertical should be true for parsing L and l commands
// Only isHorizontal should be true for parsing H and h commands
// Only isVertical should be true for parsing V and v commands
private static List<Shape> parseLineTo(SvgState state, List<Float> args, boolean isAbsolute,
boolean isHorizontal, boolean isVertical) {
int expectedArgs = isHorizontal && isVertical ? 2 : 1;
if (args.size() % expectedArgs != 0 || args.size() < expectedArgs) {
throw new IllegalArgumentException("SVG lineto command has incorrect number of arguments.");
}
List<Shape> lines = new ArrayList<>();
for (int i = 0; i < args.size(); i += expectedArgs) {
Vector2 newPoint;
if (expectedArgs == 1) {
newPoint = new Vector2(args.get(i), args.get(i));
} else {
newPoint = new Vector2(args.get(i), args.get(i + 1));
}
if (isHorizontal && !isVertical) {
newPoint = isAbsolute ? newPoint.setY(state.currPoint.getY()) : newPoint.setY(0);
} else if (isVertical && !isHorizontal) {
newPoint = isAbsolute ? newPoint.setX(state.currPoint.getX()) : newPoint.setX(0);
}
if (!isAbsolute) {
newPoint = state.currPoint.translate(newPoint);
}
lines.add(new Line(state.currPoint, newPoint));
state.currPoint = newPoint;
}
return lines;
}
static List<Shape> absolute(SvgState state, List<Float> args) {
return parseLineTo(state, args, true, true, true);
}
static List<Shape> relative(SvgState state, List<Float> args) {
return parseLineTo(state, args, false, true, true);
}
static List<Shape> horizontalAbsolute(SvgState state, List<Float> args) {
return parseLineTo(state, args, true, true, false);
}
static List<Shape> horizontalRelative(SvgState state, List<Float> args) {
return parseLineTo(state, args, false, true, false);
}
static List<Shape> verticalAbsolute(SvgState state, List<Float> args) {
return parseLineTo(state, args, true, false, true);
}
static List<Shape> verticalRelative(SvgState state, List<Float> args) {
return parseLineTo(state, args, false, false, true);
}
}
package sh.ball.parser.svg;
import java.util.ArrayList;
import java.util.List;
import sh.ball.shapes.Line;
import sh.ball.shapes.Shape;
import sh.ball.shapes.Vector2;
class LineTo {
// Parses lineto commands (L, l, H, h, V, and v commands)
// isHorizontal and isVertical should be true for parsing L and l commands
// Only isHorizontal should be true for parsing H and h commands
// Only isVertical should be true for parsing V and v commands
private static List<Shape> parseLineTo(SvgState state, List<Float> args, boolean isAbsolute,
boolean isHorizontal, boolean isVertical) {
int expectedArgs = isHorizontal && isVertical ? 2 : 1;
if (args.size() % expectedArgs != 0 || args.size() < expectedArgs) {
throw new IllegalArgumentException("SVG lineto command has incorrect number of arguments.");
}
List<Shape> lines = new ArrayList<>();
for (int i = 0; i < args.size(); i += expectedArgs) {
Vector2 newPoint;
if (expectedArgs == 1) {
newPoint = new Vector2(args.get(i), args.get(i));
} else {
newPoint = new Vector2(args.get(i), args.get(i + 1));
}
if (isHorizontal && !isVertical) {
newPoint = isAbsolute ? newPoint.setY(state.currPoint.getY()) : newPoint.setY(0);
} else if (isVertical && !isHorizontal) {
newPoint = isAbsolute ? newPoint.setX(state.currPoint.getX()) : newPoint.setX(0);
}
if (!isAbsolute) {
newPoint = state.currPoint.translate(newPoint);
}
lines.add(new Line(state.currPoint, newPoint));
state.currPoint = newPoint;
}
return lines;
}
static List<Shape> absolute(SvgState state, List<Float> args) {
return parseLineTo(state, args, true, true, true);
}
static List<Shape> relative(SvgState state, List<Float> args) {
return parseLineTo(state, args, false, true, true);
}
static List<Shape> horizontalAbsolute(SvgState state, List<Float> args) {
return parseLineTo(state, args, true, true, false);
}
static List<Shape> horizontalRelative(SvgState state, List<Float> args) {
return parseLineTo(state, args, false, true, false);
}
static List<Shape> verticalAbsolute(SvgState state, List<Float> args) {
return parseLineTo(state, args, true, false, true);
}
static List<Shape> verticalRelative(SvgState state, List<Float> args) {
return parseLineTo(state, args, false, false, true);
}
}

Wyświetl plik

@ -1,43 +1,43 @@
package sh.ball.parser.svg;
import java.util.ArrayList;
import java.util.List;
import sh.ball.shapes.Shape;
import sh.ball.shapes.Vector2;
class MoveTo {
// Parses moveto commands (M and m commands)
private static List<Shape> parseMoveTo(SvgState state, List<Float> args, boolean isAbsolute) {
if (args.size() % 2 != 0 || args.size() < 2) {
throw new IllegalArgumentException("SVG moveto command has incorrect number of arguments.");
}
Vector2 vec = new Vector2(args.get(0), args.get(1));
if (isAbsolute) {
state.currPoint = vec;
state.initialPoint = state.currPoint;
if (args.size() > 2) {
return LineTo.absolute(state, args.subList(2, args.size() - 1));
}
} else {
state.currPoint = state.currPoint.translate(vec);
state.initialPoint = state.currPoint;
if (args.size() > 2) {
return LineTo.relative(state, args.subList(2, args.size() - 1));
}
}
return new ArrayList<>();
}
static List<Shape> absolute(SvgState state, List<Float> args) {
return parseMoveTo(state, args, true);
}
static List<Shape> relative(SvgState state, List<Float> args) {
return parseMoveTo(state, args, false);
}
}
package sh.ball.parser.svg;
import java.util.ArrayList;
import java.util.List;
import sh.ball.shapes.Shape;
import sh.ball.shapes.Vector2;
class MoveTo {
// Parses moveto commands (M and m commands)
private static List<Shape> parseMoveTo(SvgState state, List<Float> args, boolean isAbsolute) {
if (args.size() % 2 != 0 || args.size() < 2) {
throw new IllegalArgumentException("SVG moveto command has incorrect number of arguments.");
}
Vector2 vec = new Vector2(args.get(0), args.get(1));
if (isAbsolute) {
state.currPoint = vec;
state.initialPoint = state.currPoint;
if (args.size() > 2) {
return LineTo.absolute(state, args.subList(2, args.size() - 1));
}
} else {
state.currPoint = state.currPoint.translate(vec);
state.initialPoint = state.currPoint;
if (args.size() > 2) {
return LineTo.relative(state, args.subList(2, args.size() - 1));
}
}
return new ArrayList<>();
}
static List<Shape> absolute(SvgState state, List<Float> args) {
return parseMoveTo(state, args, true);
}
static List<Shape> relative(SvgState state, List<Float> args) {
return parseMoveTo(state, args, false);
}
}

Wyświetl plik

@ -1,210 +1,210 @@
package sh.ball.parser.svg;
import static sh.ball.parser.XmlUtil.asList;
import static sh.ball.parser.XmlUtil.getAttributesOnTags;
import static sh.ball.parser.XmlUtil.getNodeValue;
import static sh.ball.parser.XmlUtil.getXMLDocument;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import javax.xml.parsers.ParserConfigurationException;
import org.unbescape.html.HtmlEscape;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;
import sh.ball.audio.FrameSet;
import sh.ball.parser.FileParser;
import sh.ball.shapes.ShapeFrameSet;
import sh.ball.shapes.Shape;
import sh.ball.shapes.Vector2;
public class SvgParser extends FileParser<FrameSet<List<Shape>>> {
private final Map<Character, BiFunction<SvgState, List<Float>, List<Shape>>> commandMap;
private final SvgState state;
private final InputStream input;
private Document svg;
public SvgParser(InputStream input) {
this.input = input;
this.state = new SvgState();
this.commandMap = new HashMap<>();
initialiseCommandMap();
}
public SvgParser(String path) throws FileNotFoundException {
this(new FileInputStream(path));
checkFileExtension(path);
}
// Map command chars to function calls.
private void initialiseCommandMap() {
commandMap.put('M', MoveTo::absolute);
commandMap.put('m', MoveTo::relative);
commandMap.put('L', LineTo::absolute);
commandMap.put('l', LineTo::relative);
commandMap.put('H', LineTo::horizontalAbsolute);
commandMap.put('h', LineTo::horizontalRelative);
commandMap.put('V', LineTo::verticalAbsolute);
commandMap.put('v', LineTo::verticalRelative);
commandMap.put('C', CurveTo::absolute);
commandMap.put('c', CurveTo::relative);
commandMap.put('S', CurveTo::smoothAbsolute);
commandMap.put('s', CurveTo::smoothRelative);
commandMap.put('Q', CurveTo::quarticAbsolute);
commandMap.put('q', CurveTo::quarticRelative);
commandMap.put('T', CurveTo::quarticSmoothAbsolute);
commandMap.put('t', CurveTo::quarticSmoothRelative);
commandMap.put('A', EllipticalArcTo::absolute);
commandMap.put('a', EllipticalArcTo::relative);
commandMap.put('Z', ClosePath::absolute);
commandMap.put('z', ClosePath::relative);
}
// Does error checking against SVG path and returns array of SVG commands and arguments
private String[] preProcessPath(String path) throws IllegalArgumentException {
// Replace all commas with spaces and then remove unnecessary whitespace
path = path.replace(',', ' ');
path = path.replace("-", " -");
path = path.replaceAll("\\s+", " ");
path = path.replaceAll("(^\\s|\\s$)", "");
// If there are any characters in the path that are illegal
if (path.matches("[^mlhvcsqtazMLHVCSQTAZ\\-.\\d\\s]")) {
throw new IllegalArgumentException("Illegal characters in SVG path.");
// If there are more than 1 letters or delimiters next to one another
} else if (path.matches("[a-zA-Z.\\-]{2,}")) {
throw new IllegalArgumentException(
"Multiple letters or delimiters found next to one another in SVG path.");
// First character in path must be a command
} else if (path.matches("^[a-zA-Z]")) {
throw new IllegalArgumentException("Start of SVG path is not a letter.");
}
// Split on SVG path characters to get a list of instructions, keeping the SVG commands
return path.split("(?=[mlhvcsqtazMLHVCSQTAZ])");
}
private static List<Float> splitCommand(String command) {
List<Float> nums = new ArrayList<>();
String[] decimalSplit = command.split("\\.");
try {
if (decimalSplit.length == 1) {
nums.add(Float.parseFloat(decimalSplit[0]));
} else {
nums.add(Float.parseFloat(decimalSplit[0] + "." + decimalSplit[1]));
for (int i = 2; i < decimalSplit.length; i++) {
nums.add(Float.parseFloat("." + decimalSplit[i]));
}
}
} catch (Exception e) {
System.out.println(Arrays.toString(decimalSplit));
System.out.println(command);
}
return nums;
}
@Override
public String getFileExtension() {
return "svg";
}
@Override
public FrameSet<List<Shape>> parse()
throws ParserConfigurationException, IOException, SAXException, IllegalArgumentException {
this.svg = getXMLDocument(input);
List<Node> svgElem = asList(svg.getElementsByTagName("svg"));
List<Shape> shapes = new ArrayList<>();
if (svgElem.size() != 1) {
throw new IllegalArgumentException("SVG has either zero or more than one svg element.");
}
// Get all d attributes within path elements in the SVG file.
for (Node node : getAttributesOnTags(svg, "path", "d")) {
shapes.addAll(parsePath(node.getNodeValue()));
}
return new ShapeFrameSet(Shape.normalize(shapes));
}
/* Given a character, will return the glyph associated with it.
* Assumes that the .svg loaded has character glyphs. */
public List<Shape> parseGlyphsWithUnicode(char unicode) {
for (Node node : asList(svg.getElementsByTagName("glyph"))) {
String unicodeString = getNodeValue(node, "unicode");
if (unicodeString == null) {
throw new IllegalArgumentException("Glyph should have unicode attribute.");
}
/* Removes all html escaped characters, allowing it to be directly compared to the unicode
* parameter. */
String decodedString = HtmlEscape.unescapeHtml(unicodeString);
if (String.valueOf(unicode).equals(decodedString)) {
return parsePath(getNodeValue(node, "d"));
}
}
return List.of();
}
// Performs path parsing on a single d=path
private List<Shape> parsePath(String path) {
if (path == null) {
return List.of();
}
state.currPoint = new Vector2();
state.prevCubicControlPoint = null;
state.prevQuadraticControlPoint = null;
String[] commands = preProcessPath(path);
List<Shape> svgShapes = new ArrayList<>();
for (String command : commands) {
char commandChar = command.charAt(0);
List<Float> nums = null;
if (commandChar != 'z' && commandChar != 'Z') {
// Split the command into number strings and convert them into floats.
nums = Arrays.stream(command.substring(1).split(" "))
.filter(Predicate.not(String::isBlank))
.flatMap((numString) -> splitCommand(numString).stream())
.collect(Collectors.toList());
}
// Use the nums to get a list of sh.ball.shapes, using the first character in the command to specify
// the function to use.
svgShapes.addAll(commandMap.get(commandChar).apply(state, nums));
if (!String.valueOf(commandChar).matches("[csCS]")) {
state.prevCubicControlPoint = null;
}
if (!String.valueOf(commandChar).matches("[qtQT]")) {
state.prevQuadraticControlPoint = null;
}
}
return svgShapes;
}
public static boolean isSvgFile(String path) {
return path.matches(".*\\.svg");
}
}
package sh.ball.parser.svg;
import static sh.ball.parser.XmlUtil.asList;
import static sh.ball.parser.XmlUtil.getAttributesOnTags;
import static sh.ball.parser.XmlUtil.getNodeValue;
import static sh.ball.parser.XmlUtil.getXMLDocument;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import javax.xml.parsers.ParserConfigurationException;
import org.unbescape.html.HtmlEscape;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;
import sh.ball.audio.FrameSet;
import sh.ball.parser.FileParser;
import sh.ball.shapes.ShapeFrameSet;
import sh.ball.shapes.Shape;
import sh.ball.shapes.Vector2;
public class SvgParser extends FileParser<FrameSet<List<Shape>>> {
private final Map<Character, BiFunction<SvgState, List<Float>, List<Shape>>> commandMap;
private final SvgState state;
private final InputStream input;
private Document svg;
public SvgParser(InputStream input) {
this.input = input;
this.state = new SvgState();
this.commandMap = new HashMap<>();
initialiseCommandMap();
}
public SvgParser(String path) throws FileNotFoundException {
this(new FileInputStream(path));
checkFileExtension(path);
}
// Map command chars to function calls.
private void initialiseCommandMap() {
commandMap.put('M', MoveTo::absolute);
commandMap.put('m', MoveTo::relative);
commandMap.put('L', LineTo::absolute);
commandMap.put('l', LineTo::relative);
commandMap.put('H', LineTo::horizontalAbsolute);
commandMap.put('h', LineTo::horizontalRelative);
commandMap.put('V', LineTo::verticalAbsolute);
commandMap.put('v', LineTo::verticalRelative);
commandMap.put('C', CurveTo::absolute);
commandMap.put('c', CurveTo::relative);
commandMap.put('S', CurveTo::smoothAbsolute);
commandMap.put('s', CurveTo::smoothRelative);
commandMap.put('Q', CurveTo::quarticAbsolute);
commandMap.put('q', CurveTo::quarticRelative);
commandMap.put('T', CurveTo::quarticSmoothAbsolute);
commandMap.put('t', CurveTo::quarticSmoothRelative);
commandMap.put('A', EllipticalArcTo::absolute);
commandMap.put('a', EllipticalArcTo::relative);
commandMap.put('Z', ClosePath::absolute);
commandMap.put('z', ClosePath::relative);
}
// Does error checking against SVG path and returns array of SVG commands and arguments
private String[] preProcessPath(String path) throws IllegalArgumentException {
// Replace all commas with spaces and then remove unnecessary whitespace
path = path.replace(',', ' ');
path = path.replace("-", " -");
path = path.replaceAll("\\s+", " ");
path = path.replaceAll("(^\\s|\\s$)", "");
// If there are any characters in the path that are illegal
if (path.matches("[^mlhvcsqtazMLHVCSQTAZ\\-.\\d\\s]")) {
throw new IllegalArgumentException("Illegal characters in SVG path.");
// If there are more than 1 letters or delimiters next to one another
} else if (path.matches("[a-zA-Z.\\-]{2,}")) {
throw new IllegalArgumentException(
"Multiple letters or delimiters found next to one another in SVG path.");
// First character in path must be a command
} else if (path.matches("^[a-zA-Z]")) {
throw new IllegalArgumentException("Start of SVG path is not a letter.");
}
// Split on SVG path characters to get a list of instructions, keeping the SVG commands
return path.split("(?=[mlhvcsqtazMLHVCSQTAZ])");
}
private static List<Float> splitCommand(String command) {
List<Float> nums = new ArrayList<>();
String[] decimalSplit = command.split("\\.");
try {
if (decimalSplit.length == 1) {
nums.add(Float.parseFloat(decimalSplit[0]));
} else {
nums.add(Float.parseFloat(decimalSplit[0] + "." + decimalSplit[1]));
for (int i = 2; i < decimalSplit.length; i++) {
nums.add(Float.parseFloat("." + decimalSplit[i]));
}
}
} catch (Exception e) {
System.out.println(Arrays.toString(decimalSplit));
System.out.println(command);
}
return nums;
}
@Override
public String getFileExtension() {
return "svg";
}
@Override
public FrameSet<List<Shape>> parse()
throws ParserConfigurationException, IOException, SAXException, IllegalArgumentException {
this.svg = getXMLDocument(input);
List<Node> svgElem = asList(svg.getElementsByTagName("svg"));
List<Shape> shapes = new ArrayList<>();
if (svgElem.size() != 1) {
throw new IllegalArgumentException("SVG has either zero or more than one svg element.");
}
// Get all d attributes within path elements in the SVG file.
for (Node node : getAttributesOnTags(svg, "path", "d")) {
shapes.addAll(parsePath(node.getNodeValue()));
}
return new ShapeFrameSet(Shape.normalize(shapes));
}
/* Given a character, will return the glyph associated with it.
* Assumes that the .svg loaded has character glyphs. */
public List<Shape> parseGlyphsWithUnicode(char unicode) {
for (Node node : asList(svg.getElementsByTagName("glyph"))) {
String unicodeString = getNodeValue(node, "unicode");
if (unicodeString == null) {
throw new IllegalArgumentException("Glyph should have unicode attribute.");
}
/* Removes all html escaped characters, allowing it to be directly compared to the unicode
* parameter. */
String decodedString = HtmlEscape.unescapeHtml(unicodeString);
if (String.valueOf(unicode).equals(decodedString)) {
return parsePath(getNodeValue(node, "d"));
}
}
return List.of();
}
// Performs path parsing on a single d=path
private List<Shape> parsePath(String path) {
if (path == null) {
return List.of();
}
state.currPoint = new Vector2();
state.prevCubicControlPoint = null;
state.prevQuadraticControlPoint = null;
String[] commands = preProcessPath(path);
List<Shape> svgShapes = new ArrayList<>();
for (String command : commands) {
char commandChar = command.charAt(0);
List<Float> nums = null;
if (commandChar != 'z' && commandChar != 'Z') {
// Split the command into number strings and convert them into floats.
nums = Arrays.stream(command.substring(1).split(" "))
.filter(Predicate.not(String::isBlank))
.flatMap((numString) -> splitCommand(numString).stream())
.collect(Collectors.toList());
}
// Use the nums to get a list of sh.ball.shapes, using the first character in the command to specify
// the function to use.
svgShapes.addAll(commandMap.get(commandChar).apply(state, nums));
if (!String.valueOf(commandChar).matches("[csCS]")) {
state.prevCubicControlPoint = null;
}
if (!String.valueOf(commandChar).matches("[qtQT]")) {
state.prevQuadraticControlPoint = null;
}
}
return svgShapes;
}
public static boolean isSvgFile(String path) {
return path.matches(".*\\.svg");
}
}

Wyświetl plik

@ -1,12 +1,12 @@
package sh.ball.parser.svg;
import sh.ball.shapes.Vector2;
/* Intentionally package-private class for carrying around the current state of SVG parsing. */
class SvgState {
Vector2 currPoint;
Vector2 initialPoint;
Vector2 prevCubicControlPoint;
Vector2 prevQuadraticControlPoint;
}
package sh.ball.parser.svg;
import sh.ball.shapes.Vector2;
/* Intentionally package-private class for carrying around the current state of SVG parsing. */
class SvgState {
Vector2 currPoint;
Vector2 initialPoint;
Vector2 prevCubicControlPoint;
Vector2 prevQuadraticControlPoint;
}

Wyświetl plik

@ -1,85 +1,85 @@
package sh.ball.parser.txt;
import java.io.*;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException;
import sh.ball.audio.FrameSet;
import sh.ball.parser.FileParser;
import sh.ball.shapes.ShapeFrameSet;
import sh.ball.parser.svg.SvgParser;
import sh.ball.shapes.Shape;
import sh.ball.shapes.Vector2;
public class TextParser extends FileParser<FrameSet<List<Shape>>> {
private static final char WIDE_CHAR = 'W';
private static final double HEIGHT_SCALAR = 1.6;
private static final String DEFAULT_FONT = "/fonts/SourceCodePro-ExtraLight.svg";
private final Map<Character, List<Shape>> charToShape;
private final InputStream input;
private final InputStream font;
public TextParser(InputStream input, InputStream font) {
this.input = input;
this.font = font;
this.charToShape = new HashMap<>();
}
public TextParser(String path) throws FileNotFoundException {
this(new FileInputStream(path), TextParser.class.getResourceAsStream(DEFAULT_FONT));
checkFileExtension(path);
}
@Override
public String getFileExtension() {
return "txt";
}
@Override
public FrameSet<List<Shape>> parse() throws IllegalArgumentException, IOException, ParserConfigurationException, SAXException {
List<String> text = new BufferedReader(new InputStreamReader(input, Charset.defaultCharset())).lines().collect(Collectors.toList());
SvgParser fontParser = new SvgParser(font);
fontParser.parse();
List<Shape> shapes = new ArrayList<>();
/* WIDE_CHAR used as an example character that will be wide in most languages.
* This helps determine the correct character width for the font chosen. */
charToShape.put(WIDE_CHAR, fontParser.parseGlyphsWithUnicode(WIDE_CHAR));
for (String line : text) {
for (char c : line.toCharArray()) {
if (!charToShape.containsKey(c)) {
List<Shape> glyph = fontParser.parseGlyphsWithUnicode(c);
charToShape.put(c, glyph);
}
}
}
double width = Shape.width(charToShape.get(WIDE_CHAR));
double height = HEIGHT_SCALAR * Shape.height(charToShape.get(WIDE_CHAR));
for (int i = 0; i < text.size(); i++) {
char[] lineChars = text.get(i).toCharArray();
for (int j = 0; j < lineChars.length; j++) {
shapes.addAll(Shape.translate(
charToShape.get(lineChars[j]),
new Vector2(j * width, -i * height)
));
}
}
return new ShapeFrameSet(Shape.flip(Shape.normalize(shapes)));
}
public static boolean isTxtFile(String path) {
return path.matches(".*\\.txt");
}
}
package sh.ball.parser.txt;
import java.io.*;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException;
import sh.ball.audio.FrameSet;
import sh.ball.parser.FileParser;
import sh.ball.shapes.ShapeFrameSet;
import sh.ball.parser.svg.SvgParser;
import sh.ball.shapes.Shape;
import sh.ball.shapes.Vector2;
public class TextParser extends FileParser<FrameSet<List<Shape>>> {
private static final char WIDE_CHAR = 'W';
private static final double HEIGHT_SCALAR = 1.6;
private static final String DEFAULT_FONT = "/fonts/SourceCodePro-ExtraLight.svg";
private final Map<Character, List<Shape>> charToShape;
private final InputStream input;
private final InputStream font;
public TextParser(InputStream input, InputStream font) {
this.input = input;
this.font = font;
this.charToShape = new HashMap<>();
}
public TextParser(String path) throws FileNotFoundException {
this(new FileInputStream(path), TextParser.class.getResourceAsStream(DEFAULT_FONT));
checkFileExtension(path);
}
@Override
public String getFileExtension() {
return "txt";
}
@Override
public FrameSet<List<Shape>> parse() throws IllegalArgumentException, IOException, ParserConfigurationException, SAXException {
List<String> text = new BufferedReader(new InputStreamReader(input, Charset.defaultCharset())).lines().collect(Collectors.toList());
SvgParser fontParser = new SvgParser(font);
fontParser.parse();
List<Shape> shapes = new ArrayList<>();
/* WIDE_CHAR used as an example character that will be wide in most languages.
* This helps determine the correct character width for the font chosen. */
charToShape.put(WIDE_CHAR, fontParser.parseGlyphsWithUnicode(WIDE_CHAR));
for (String line : text) {
for (char c : line.toCharArray()) {
if (!charToShape.containsKey(c)) {
List<Shape> glyph = fontParser.parseGlyphsWithUnicode(c);
charToShape.put(c, glyph);
}
}
}
double width = Shape.width(charToShape.get(WIDE_CHAR));
double height = HEIGHT_SCALAR * Shape.height(charToShape.get(WIDE_CHAR));
for (int i = 0; i < text.size(); i++) {
char[] lineChars = text.get(i).toCharArray();
for (int j = 0; j < lineChars.length; j++) {
shapes.addAll(Shape.translate(
charToShape.get(lineChars[j]),
new Vector2(j * width, -i * height)
));
}
}
return new ShapeFrameSet(Shape.flip(Shape.normalize(shapes)));
}
public static boolean isTxtFile(String path) {
return path.matches(".*\\.txt");
}
}

Wyświetl plik

@ -1,58 +1,58 @@
package sh.ball.shapes;
public class CubicBezierCurve extends Shape {
private final Vector2 p0;
private final Vector2 p1;
private final Vector2 p2;
private final Vector2 p3;
public CubicBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, double weight) {
this.p0 = p0;
this.p1 = p1;
this.p2 = p2;
this.p3 = p3;
this.weight = weight;
this.length = new Line(p0, p3).length;
}
public CubicBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3) {
this(p0, p1, p2, p3, DEFAULT_WEIGHT);
}
@Override
public Vector2 nextVector(double t) {
return p0.scale(Math.pow(1 - t, 3))
.add(p1.scale(3 * Math.pow(1 - t, 2) * t))
.add(p2.scale(3 * (1 - t) * Math.pow(t, 2)))
.add(p3.scale(Math.pow(t, 3)));
}
@Override
public CubicBezierCurve rotate(double theta) {
return new CubicBezierCurve(p0.rotate(theta), p1.rotate(theta), p2.rotate(theta),
p3.rotate(theta), weight);
}
@Override
public CubicBezierCurve scale(double factor) {
return scale(new Vector2(factor));
}
@Override
public CubicBezierCurve scale(Vector2 vector) {
return new CubicBezierCurve(p0.scale(vector), p1.scale(vector), p2.scale(vector),
p3.scale(vector), weight);
}
@Override
public CubicBezierCurve translate(Vector2 vector) {
return new CubicBezierCurve(p0.translate(vector), p1.translate(vector), p2.translate(vector),
p3.translate(vector), weight);
}
@Override
public CubicBezierCurve setWeight(double weight) {
return new CubicBezierCurve(p0, p1, p2, p3, weight);
}
}
package sh.ball.shapes;
public class CubicBezierCurve extends Shape {
private final Vector2 p0;
private final Vector2 p1;
private final Vector2 p2;
private final Vector2 p3;
public CubicBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, double weight) {
this.p0 = p0;
this.p1 = p1;
this.p2 = p2;
this.p3 = p3;
this.weight = weight;
this.length = new Line(p0, p3).length;
}
public CubicBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3) {
this(p0, p1, p2, p3, DEFAULT_WEIGHT);
}
@Override
public Vector2 nextVector(double t) {
return p0.scale(Math.pow(1 - t, 3))
.add(p1.scale(3 * Math.pow(1 - t, 2) * t))
.add(p2.scale(3 * (1 - t) * Math.pow(t, 2)))
.add(p3.scale(Math.pow(t, 3)));
}
@Override
public CubicBezierCurve rotate(double theta) {
return new CubicBezierCurve(p0.rotate(theta), p1.rotate(theta), p2.rotate(theta),
p3.rotate(theta), weight);
}
@Override
public CubicBezierCurve scale(double factor) {
return scale(new Vector2(factor));
}
@Override
public CubicBezierCurve scale(Vector2 vector) {
return new CubicBezierCurve(p0.scale(vector), p1.scale(vector), p2.scale(vector),
p3.scale(vector), weight);
}
@Override
public CubicBezierCurve translate(Vector2 vector) {
return new CubicBezierCurve(p0.translate(vector), p1.translate(vector), p2.translate(vector),
p3.translate(vector), weight);
}
@Override
public CubicBezierCurve setWeight(double weight) {
return new CubicBezierCurve(p0, p1, p2, p3, weight);
}
}

Wyświetl plik

@ -1,52 +1,52 @@
package sh.ball.shapes;
public class QuadraticBezierCurve extends Shape {
private final Vector2 p0;
private final Vector2 p1;
private final Vector2 p2;
public QuadraticBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, double weight) {
this.p0 = p0;
this.p1 = p1;
this.p2 = p2;
this.weight = weight;
this.length = new Line(p0, p2).length;
}
public QuadraticBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2) {
this(p0, p1, p2, DEFAULT_WEIGHT);
}
@Override
public Vector2 nextVector(double t) {
return p1.add(p0.sub(p1).scale(Math.pow(1 - t, 2)))
.add(p2.sub(p1).scale(Math.pow(t, 2)));
}
@Override
public QuadraticBezierCurve rotate(double theta) {
return new QuadraticBezierCurve(p0.rotate(theta), p1.rotate(theta), p2.rotate(theta), weight);
}
@Override
public QuadraticBezierCurve scale(double factor) {
return new QuadraticBezierCurve(p0.scale(factor), p1.scale(factor), p2.scale(factor), weight);
}
@Override
public QuadraticBezierCurve scale(Vector2 vector) {
return new QuadraticBezierCurve(p0.scale(vector), p1.scale(vector), p2.scale(vector), weight);
}
@Override
public QuadraticBezierCurve translate(Vector2 vector) {
return new QuadraticBezierCurve(p0.translate(vector), p1.translate(vector),
p2.translate(vector), weight);
}
@Override
public QuadraticBezierCurve setWeight(double weight) {
return new QuadraticBezierCurve(p0, p1, p2, weight);
}
}
package sh.ball.shapes;
public class QuadraticBezierCurve extends Shape {
private final Vector2 p0;
private final Vector2 p1;
private final Vector2 p2;
public QuadraticBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, double weight) {
this.p0 = p0;
this.p1 = p1;
this.p2 = p2;
this.weight = weight;
this.length = new Line(p0, p2).length;
}
public QuadraticBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2) {
this(p0, p1, p2, DEFAULT_WEIGHT);
}
@Override
public Vector2 nextVector(double t) {
return p1.add(p0.sub(p1).scale(Math.pow(1 - t, 2)))
.add(p2.sub(p1).scale(Math.pow(t, 2)));
}
@Override
public QuadraticBezierCurve rotate(double theta) {
return new QuadraticBezierCurve(p0.rotate(theta), p1.rotate(theta), p2.rotate(theta), weight);
}
@Override
public QuadraticBezierCurve scale(double factor) {
return new QuadraticBezierCurve(p0.scale(factor), p1.scale(factor), p2.scale(factor), weight);
}
@Override
public QuadraticBezierCurve scale(Vector2 vector) {
return new QuadraticBezierCurve(p0.scale(vector), p1.scale(vector), p2.scale(vector), weight);
}
@Override
public QuadraticBezierCurve translate(Vector2 vector) {
return new QuadraticBezierCurve(p0.translate(vector), p1.translate(vector),
p2.translate(vector), weight);
}
@Override
public QuadraticBezierCurve setWeight(double weight) {
return new QuadraticBezierCurve(p0, p1, p2, weight);
}
}

Wyświetl plik

@ -1,84 +1,87 @@
.root {
dark_color: #282828;
darker_color: #1d1d1d;
very_dark: #111111;
grey_color: #555555;
accent_color: #00CC00;
-fx-background-color: dark_color;
-fx-text-background-color: white;
-fx-text-inner-color: white;
}
.titled-pane, .text {
-fx-font-size: 13;
-fx-font-smoothing-type: gray;
-fx-text-fill: white;
}
.titled-pane > .title
{
-fx-background-color: very_dark;
-fx-background-radius: 0;
}
.titled-pane > *.content
{
-fx-background-color: darker_color;
-fx-border-width: 0;
}
.titled-pane:focused > .title > .arrow-button .arrow
{
-fx-background-color: darker_color;
}
.button {
-fx-background-color: very_dark;
-fx-border-color: white;
-fx-border-width: 1;
-fx-text-fill: white;
}
.slider .thumb {
-fx-background-color: very_dark;
-fx-border-color: white;
-fx-border-radius: 1.0em; /* makes sure this remains circular */
-fx-effect: inherit;
}
.slider .track {
-fx-background-color: grey_color;
-fx-padding: 0.2em;
}
.check-box > .box {
-fx-background-radius: 0;
-fx-background-color: very_dark;
-fx-border-color: white;
-fx-border-width: 1;
}
.check-box > .box > .mark {
-fx-shape: "M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z";
}
.check-box:selected > .box > .mark,
.check-box:indeterminate > .box > .mark {
-fx-background-color: white;
}
.text-field {
-fx-background-color: very_dark;
-fx-border-color: white;
-fx-border-width: 1;
-fx-border-radius: 0;
-fx-padding: 0.2em;
}
.text-field .text {
-fx-text-fill: white;
}
#frequency .text {
-fx-font-size: 20;
.root {
dark_color: #424242;
darker_color: #212121;
very_dark: #111111;
grey_color: #555555;
accent_color: #00CC00;
-fx-background-color: dark_color;
-fx-text-background-color: white;
-fx-text-inner-color: white;
}
.titled-pane, .text {
-fx-font-size: 13;
-fx-font-smoothing-type: gray;
-fx-text-fill: white;
}
.titled-pane > .title
{
-fx-background-color: very_dark;
-fx-background-radius: 0;
}
.titled-pane > *.content
{
-fx-background-color: darker_color;
-fx-border-width: 0;
}
.titled-pane:focused > .title > .arrow-button .arrow
{
-fx-background-color: darker_color;
}
.button {
-fx-background-color: very_dark;
-fx-border-color: white;
-fx-border-width: 1;
-fx-text-fill: white;
}
.slider .thumb {
-fx-background-color: very_dark;
-fx-border-color: white;
-fx-border-radius: 1.0em; /* makes sure this remains circular */
-fx-effect: inherit;
}
.slider .track {
-fx-background-color: grey_color;
-fx-padding: 0.2em;
}
.check-box > .box {
-fx-background-radius: 0;
-fx-background-color: very_dark;
-fx-border-color: white;
-fx-border-width: 1;
}
.check-box > .box > .mark {
-fx-shape: "M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z";
}
.check-box:selected > .box > .mark,
.check-box:indeterminate > .box > .mark {
-fx-background-color: white;
}
.text-field {
-fx-background-color: very_dark;
-fx-border-color: white;
-fx-border-width: 1;
-fx-border-radius: 0;
-fx-padding: 0.2em;
}
.text-field .text {
-fx-text-fill: white;
}
#frequency .text {
-fx-font-size: 20;
}
#control-pane, .titled-pane {
-fx-background-color: darker_color;
-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.8), 10, 0, 0, 0);
}

Wyświetl plik

@ -7,88 +7,86 @@
<?import javafx.scene.control.TextField?>
<?import javafx.scene.control.TitledPane?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.text.Font?>
<GridPane alignment="center" hgap="10" prefHeight="458.0" prefWidth="836.0" vgap="10" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1">
<columnConstraints>
<ColumnConstraints />
</columnConstraints>
<rowConstraints>
<RowConstraints />
</rowConstraints>
<AnchorPane prefHeight="458.0" prefWidth="885.0">
<AnchorPane layoutX="422.0" layoutY="26.0">
<children>
<Button fx:id="chooseFileButton" mnemonicParsing="false" prefHeight="26.0" prefWidth="114.0" text="Choose File" />
<Label fx:id="fileLabel" layoutX="132.0" layoutY="5.0" maxWidth="270.0" />
<Button fx:id="recordButton" layoutY="40.0" mnemonicParsing="false" prefHeight="26.0" prefWidth="114.0" text="Record" />
<Label fx:id="recordLabel" layoutX="132.0" layoutY="45.0" maxWidth="270.0" />
<Label id="frequency" fx:id="frequencyLabel" layoutY="80.0" />
</children>
</AnchorPane>
<TitledPane animated="false" collapsible="false" layoutX="422.0" layoutY="174.0" prefHeight="272.0" prefWidth="402.0" text="Effects">
<content>
<AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="246.0" prefWidth="442.0">
<children>
<CheckBox fx:id="vectorCancellingCheckBox" layoutX="14.0" layoutY="22.0" mnemonicParsing="false" text="Vector cancelling" />
<Slider fx:id="vectorCancellingSlider" blockIncrement="0.05" disable="true" layoutX="167.0" layoutY="16.0" majorTickUnit="1.0" max="10.0" min="2.0" prefHeight="38.0" prefWidth="222.0" showTickLabels="true" showTickMarks="true" snapToTicks="true" value="2.0" />
<CheckBox fx:id="bitCrushCheckBox" layoutX="14.0" layoutY="60.0" mnemonicParsing="false" text="Bit crush" />
<Slider fx:id="bitCrushSlider" blockIncrement="0.01" disable="true" layoutX="167.0" layoutY="55.0" majorTickUnit="0.5" max="3.0" prefHeight="38.0" prefWidth="222.0" showTickLabels="true" showTickMarks="true" value="2.0" />
<CheckBox fx:id="verticalDistortCheckBox" layoutX="14.0" layoutY="101.0" mnemonicParsing="false" text="Vertical Distort" />
<Slider fx:id="verticalDistortSlider" blockIncrement="0.005" disable="true" layoutX="167.0" layoutY="96.0" majorTickUnit="0.1" max="1.0" prefHeight="38.0" prefWidth="222.0" showTickLabels="true" showTickMarks="true" value="0.2" />
<CheckBox fx:id="horizontalDistortCheckBox" layoutX="14.0" layoutY="144.0" mnemonicParsing="false" text="Horizontal Distort" />
<Slider fx:id="horizontalDistortSlider" blockIncrement="0.005" disable="true" layoutX="167.0" layoutY="139.0" majorTickUnit="0.1" max="1.0" prefHeight="38.0" prefWidth="222.0" showTickLabels="true" showTickMarks="true" value="0.2" />
<CheckBox fx:id="wobbleCheckBox" layoutX="14.0" layoutY="186.0" mnemonicParsing="false" text="Wobble" />
<Slider fx:id="wobbleSlider" blockIncrement="0.005" disable="true" layoutX="167.0" layoutY="181.0" majorTickUnit="0.1" max="1.0" prefHeight="38.0" prefWidth="222.0" showTickLabels="true" showTickMarks="true" value="0.2" />
</children>
</AnchorPane>
</content>
</TitledPane>
<TitledPane fx:id="objTitledPane" animated="false" collapsible="false" layoutX="11.0" layoutY="240.0" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="206.0" prefWidth="402.0" text="3D .obj file settings">
<AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="180.0" prefWidth="359.0">
<Slider fx:id="focalLengthSlider" blockIncrement="0.01" layoutX="116.0" layoutY="15.0" majorTickUnit="0.2" max="2.0" min="1.0E-5" prefHeight="38.0" prefWidth="270.0" showTickLabels="true" showTickMarks="true" value="1.0" />
<Label layoutX="30.0" layoutY="14.0" text="Focal length" />
<TextField fx:id="cameraXTextField" layoutX="134.0" layoutY="57.0" prefHeight="26.0" prefWidth="65.0" text="0" />
<Label layoutX="31.0" layoutY="61.0" text="Camera pos" />
<Label layoutX="118.0" layoutY="61.0" text="x :" />
<Label layoutX="207.0" layoutY="60.0" text="y :" />
<TextField fx:id="cameraYTextField" layoutX="224.0" layoutY="57.0" prefHeight="26.0" prefWidth="65.0" text="0" />
<Label layoutX="299.0" layoutY="60.0" text="z :" />
<TextField fx:id="cameraZTextField" layoutX="315.0" layoutY="57.0" prefHeight="26.0" prefWidth="65.0" text="-2.5" />
<Slider fx:id="objectRotateSpeedSlider" blockIncrement="0.005" layoutX="116.0" layoutY="97.0" majorTickUnit="0.1" max="1.0" prefHeight="38.0" prefWidth="270.0" showTickLabels="true" showTickMarks="true" />
<Label layoutX="24.0" layoutY="96.0" text="Rotate speed" />
<CheckBox fx:id="rotateCheckBox" layoutX="90.0" layoutY="147.0" mnemonicParsing="false" text="Rotate with Mouse (Esc to disable)" />
</AnchorPane>
</TitledPane>
<AnchorPane layoutX="11.0" layoutY="14.0" minHeight="0.0" minWidth="0.0">
<Slider fx:id="rotateSpeedSlider" blockIncrement="0.05" layoutX="116.0" layoutY="90.0" majorTickUnit="1.0" max="10.0" prefHeight="38.0" prefWidth="270.0" showTickLabels="true" showTickMarks="true" />
<Label layoutX="33.0" layoutY="88.0" text="Rotate speed">
<font>
<Font size="13.0" />
</font></Label>
<Slider fx:id="translationSpeedSlider" blockIncrement="0.05" layoutX="116.0" layoutY="128.0" majorTickUnit="1.0" max="10.0" prefHeight="38.0" prefWidth="270.0" showTickLabels="true" showTickMarks="true" />
<Label layoutX="7.0" layoutY="126.0" text="Translation speed">
<font>
<Font size="13.0" />
</font></Label>
<Slider fx:id="scaleSlider" blockIncrement="0.05" layoutX="116.0" layoutY="167.0" majorTickUnit="1.0" max="10.0" prefHeight="38.0" prefWidth="270.0" showTickLabels="true" showTickMarks="true" value="1.0" />
<Label layoutX="79.0" layoutY="164.0" text="Scale">
<font>
<Font size="13.0" />
</font></Label>
<Label layoutX="43.0" layoutY="49.0" text="Line weight">
<font>
<Font size="13.0" />
</font></Label>
<TextField fx:id="translationXTextField" layoutX="138.0" layoutY="15.0" prefHeight="26.0" prefWidth="70.0" text="0" />
<Label layoutX="47.0" layoutY="18.0" text="Translation" />
<Label layoutX="120.0" layoutY="18.0" text="x :" />
<Label layoutX="219.0" layoutY="18.0" text="y :" />
<TextField fx:id="translationYTextField" layoutX="238.0" layoutY="15.0" prefHeight="26.0" prefWidth="70.0" text="0" />
<Slider fx:id="weightSlider" blockIncrement="1.0" layoutX="116.0" layoutY="52.0" majorTickUnit="100.0" max="1000.0" prefHeight="38.0" prefWidth="269.0" showTickLabels="true" showTickMarks="true" value="100.0" />
<AnchorPane prefHeight="498.0" prefWidth="837.0" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1">
<AnchorPane id="control-pane" layoutX="422.0" layoutY="19.0" prefHeight="185.0" prefWidth="402.0">
<children>
<Button fx:id="chooseFileButton" layoutX="14.0" layoutY="14.0" mnemonicParsing="false" prefHeight="26.0" prefWidth="114.0" text="Choose File" />
<Label fx:id="fileLabel" layoutX="146.0" layoutY="19.0" maxWidth="270.0" prefHeight="18.0" prefWidth="246.0" text="cube.obj" />
<Button fx:id="recordButton" layoutX="14.0" layoutY="54.0" mnemonicParsing="false" prefHeight="26.0" prefWidth="114.0" text="Record" />
<Label fx:id="recordLabel" layoutX="146.0" layoutY="59.0" maxWidth="270.0" prefHeight="18.0" prefWidth="245.0" />
<Label id="frequency" fx:id="frequencyLabel" layoutX="14.0" layoutY="113.0" prefHeight="58.0" prefWidth="376.0" text="L/R Frequency:&#10; &#10;" />
</children>
</AnchorPane>
<TitledPane animated="false" collapsible="false" layoutX="422.0" layoutY="213.0" prefHeight="272.0" prefWidth="402.0" text="Effects">
<content>
<AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="246.0" prefWidth="442.0">
<children>
<CheckBox fx:id="vectorCancellingCheckBox" layoutX="14.0" layoutY="15.0" mnemonicParsing="false" text="Vector cancelling" />
<Slider fx:id="vectorCancellingSlider" blockIncrement="0.05" disable="true" layoutX="167.0" layoutY="16.0" majorTickUnit="1.0" max="10.0" min="2.0" prefHeight="38.0" prefWidth="222.0" showTickLabels="true" showTickMarks="true" snapToTicks="true" value="2.0" />
<CheckBox fx:id="bitCrushCheckBox" layoutX="14.0" layoutY="53.0" mnemonicParsing="false" text="Bit crush" />
<Slider fx:id="bitCrushSlider" blockIncrement="0.01" disable="true" layoutX="167.0" layoutY="55.0" majorTickUnit="0.5" max="3.0" prefHeight="38.0" prefWidth="222.0" showTickLabels="true" showTickMarks="true" value="2.0" />
<CheckBox fx:id="verticalDistortCheckBox" layoutX="14.0" layoutY="94.0" mnemonicParsing="false" text="Vertical Distort" />
<Slider fx:id="verticalDistortSlider" blockIncrement="0.005" disable="true" layoutX="167.0" layoutY="96.0" majorTickUnit="0.1" max="1.0" prefHeight="38.0" prefWidth="222.0" showTickLabels="true" showTickMarks="true" value="0.2" />
<CheckBox fx:id="horizontalDistortCheckBox" layoutX="14.0" layoutY="137.0" mnemonicParsing="false" text="Horizontal Distort" />
<Slider fx:id="horizontalDistortSlider" blockIncrement="0.005" disable="true" layoutX="167.0" layoutY="139.0" majorTickUnit="0.1" max="1.0" prefHeight="38.0" prefWidth="222.0" showTickLabels="true" showTickMarks="true" value="0.2" />
<CheckBox fx:id="wobbleCheckBox" layoutX="14.0" layoutY="179.0" mnemonicParsing="false" text="Wobble" />
<Slider fx:id="wobbleSlider" blockIncrement="0.005" disable="true" layoutX="167.0" layoutY="181.0" majorTickUnit="0.1" max="1.0" prefHeight="38.0" prefWidth="222.0" showTickLabels="true" showTickMarks="true" value="0.2" />
</children>
</AnchorPane>
</content>
</TitledPane>
<TitledPane fx:id="objTitledPane" animated="false" collapsible="false" layoutX="11.0" layoutY="275.0" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="210.0" prefWidth="402.0" text="3D .obj file settings">
<AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="180.0" prefWidth="359.0">
<Slider fx:id="focalLengthSlider" blockIncrement="0.01" layoutX="116.0" layoutY="15.0" majorTickUnit="0.2" max="2.0" min="1.0E-5" prefHeight="38.0" prefWidth="270.0" showTickLabels="true" showTickMarks="true" value="1.0" />
<Label layoutX="30.0" layoutY="14.0" text="Focal length" />
<TextField fx:id="cameraXTextField" layoutX="134.0" layoutY="57.0" prefHeight="26.0" prefWidth="65.0" text="0" />
<Label layoutX="31.0" layoutY="61.0" text="Camera pos" />
<Label layoutX="118.0" layoutY="61.0" text="x :" />
<Label layoutX="207.0" layoutY="60.0" text="y :" />
<TextField fx:id="cameraYTextField" layoutX="224.0" layoutY="57.0" prefHeight="26.0" prefWidth="65.0" text="0" />
<Label layoutX="299.0" layoutY="60.0" text="z :" />
<TextField fx:id="cameraZTextField" layoutX="315.0" layoutY="57.0" prefHeight="26.0" prefWidth="65.0" text="-2.5" />
<Slider fx:id="objectRotateSpeedSlider" blockIncrement="0.005" layoutX="116.0" layoutY="97.0" majorTickUnit="0.1" max="1.0" prefHeight="38.0" prefWidth="270.0" showTickLabels="true" showTickMarks="true" />
<Label layoutX="24.0" layoutY="96.0" text="Rotate speed" />
<CheckBox fx:id="rotateCheckBox" layoutX="90.0" layoutY="147.0" mnemonicParsing="false" text="Rotate with Mouse (Esc to disable)" />
</AnchorPane>
</GridPane>
</TitledPane>
<TitledPane collapsible="false" layoutX="11.0" layoutY="18.0" prefHeight="248.0" prefWidth="402.0" text="Image settings">
<content>
<AnchorPane minHeight="0.0" minWidth="0.0">
<Slider fx:id="rotateSpeedSlider" blockIncrement="0.05" layoutX="116.0" layoutY="90.0" majorTickUnit="1.0" max="10.0" prefHeight="38.0" prefWidth="270.0" showTickLabels="true" showTickMarks="true" />
<Label layoutX="33.0" layoutY="88.0" text="Rotate speed">
<font>
<Font size="13.0" />
</font>
</Label>
<Slider fx:id="translationSpeedSlider" blockIncrement="0.05" layoutX="116.0" layoutY="128.0" majorTickUnit="1.0" max="10.0" prefHeight="38.0" prefWidth="270.0" showTickLabels="true" showTickMarks="true" />
<Label layoutX="7.0" layoutY="126.0" text="Translation speed">
<font>
<Font size="13.0" />
</font>
</Label>
<Slider fx:id="scaleSlider" blockIncrement="0.05" layoutX="116.0" layoutY="167.0" majorTickUnit="1.0" max="10.0" prefHeight="38.0" prefWidth="270.0" showTickLabels="true" showTickMarks="true" value="1.0" />
<Label layoutX="79.0" layoutY="164.0" text="Scale">
<font>
<Font size="13.0" />
</font>
</Label>
<Label layoutX="43.0" layoutY="49.0" text="Line weight">
<font>
<Font size="13.0" />
</font>
</Label>
<TextField fx:id="translationXTextField" layoutX="138.0" layoutY="15.0" prefHeight="26.0" prefWidth="70.0" text="0" />
<Label layoutX="47.0" layoutY="18.0" text="Translation" />
<Label layoutX="120.0" layoutY="18.0" text="x :" />
<Label layoutX="219.0" layoutY="18.0" text="y :" />
<TextField fx:id="translationYTextField" layoutX="238.0" layoutY="15.0" prefHeight="26.0" prefWidth="70.0" text="0" />
<Slider fx:id="weightSlider" blockIncrement="1.0" layoutX="116.0" layoutY="52.0" majorTickUnit="100.0" max="1000.0" prefHeight="38.0" prefWidth="269.0" showTickLabels="true" showTickMarks="true" value="100.0" />
</AnchorPane>
</content>
</TitledPane>
</AnchorPane>

Wyświetl plik

@ -1,4 +1,4 @@
αβγΓηθ
ικλΛμν
πΠρσΣτ
αβγΓηθ
ικλΛμν
πΠρσΣτ
υϕχψΨω

Wyświetl plik

@ -1,2 +1,2 @@
hello
hello
world