Overhaul animation UX

pull/147/head v1.33.0
James Ball 2023-01-06 19:21:41 +00:00 zatwierdzone przez James H Ball
rodzic 16acb3857c
commit 27215863ef
15 zmienionych plików z 177 dodań i 128 usunięć

Wyświetl plik

@ -6,7 +6,7 @@
<groupId>sh.ball</groupId>
<artifactId>osci-render</artifactId>
<version>1.32.3</version>
<version>1.33.0</version>
<name>osci-render</name>

Wyświetl plik

@ -1,16 +1,20 @@
package sh.ball.audio.effect;
public enum AnimationType {
STATIC("Static"),
SEESAW("Seesaw"),
LINEAR("Linear"),
FORWARD("Forward"),
REVERSE("Reverse");
STATIC("Static", "Static"),
SINE("Sine", "Sine"),
SQUARE("Square", "Square"),
SEESAW("Seesaw", "Seesaw"),
LINEAR("Linear", "Triangle"),
FORWARD("Forward", "Sawtooth"),
REVERSE("Reverse", "Reverse Sawtooth");
private final String name;
private final String displayName;
AnimationType(String name) {
AnimationType(String name, String displayName) {
this.name = name;
this.displayName = displayName;
}
public static AnimationType fromString(String name) {
@ -22,8 +26,12 @@ public enum AnimationType {
return null;
}
@Override
public String toString() {
public String getName() {
return name;
}
@Override
public String toString() {
return displayName;
}
}

Wyświetl plik

@ -6,15 +6,12 @@ public class EffectAnimator extends PhaseEffect implements SettableEffect {
public static final int DEFAULT_SAMPLE_RATE = 192000;
private static final double SPEED_SCALE = 20.0;
private final SettableEffect effect;
private AnimationType type = AnimationType.STATIC;
private boolean justSetToStatic = true;
private double targetValue = 0.5;
private double actualValue = 0.5;
private boolean linearDirection = true;
private double min;
private double max;
@ -31,7 +28,6 @@ public class EffectAnimator extends PhaseEffect implements SettableEffect {
public void setAnimation(AnimationType type) {
this.type = type;
this.linearDirection = true;
if (type == AnimationType.STATIC) {
justSetToStatic = true;
}
@ -58,48 +54,26 @@ public class EffectAnimator extends PhaseEffect implements SettableEffect {
double minValue = min;
double maxValue = max;
double range = maxValue - minValue;
if (range <= 0) {
return vector;
}
double normalisedTargetValue = (targetValue - minValue) / range;
double normalisedActualValue = (actualValue - minValue) / range;
double phase = nextTheta();
double percentage = phase / (2 * Math.PI);
switch (type) {
case SEESAW -> {
double scalar = 10 * Math.max(Math.min(normalisedActualValue, 1 - normalisedActualValue), 0.01);
double change = range * scalar * SPEED_SCALE * normalisedTargetValue / sampleRate;
if (actualValue + change > maxValue || actualValue - change < minValue) {
linearDirection = !linearDirection;
}
if (linearDirection) {
actualValue += change;
} else {
actualValue -= change;
}
// modified sigmoid function
actualValue = (percentage < 0.5) ? percentage * 2 : (1 - percentage) * 2;
actualValue = 1 / (1 + Math.exp(-16 * (actualValue - 0.5)));
actualValue = actualValue * (maxValue - minValue) + minValue;
}
case SINE -> {
actualValue = Math.sin(phase) * 0.5 + 0.5;
actualValue = actualValue * (maxValue - minValue) + minValue;
}
case LINEAR -> {
double change = range * SPEED_SCALE * normalisedTargetValue / sampleRate;
if (actualValue + change > maxValue || actualValue - change < minValue) {
linearDirection = !linearDirection;
}
if (linearDirection) {
actualValue += change;
} else {
actualValue -= change;
}
}
case FORWARD -> {
actualValue += range * 0.5 * SPEED_SCALE * normalisedTargetValue / sampleRate;
if (actualValue > maxValue) {
actualValue = minValue;
}
}
case REVERSE -> {
actualValue -= range * 0.5 * SPEED_SCALE * normalisedTargetValue / sampleRate;
if (actualValue < minValue) {
actualValue = maxValue;
}
actualValue = (percentage < 0.5) ? percentage * 2 : (1 - percentage) * 2;
actualValue = actualValue * (maxValue - minValue) + minValue;
}
case SQUARE -> actualValue = (percentage < 0.5) ? maxValue : minValue;
case FORWARD -> actualValue = percentage * (maxValue - minValue) + minValue;
case REVERSE -> actualValue = (1 - percentage) * (maxValue - minValue) + minValue;
}
if (actualValue > maxValue) {
actualValue = maxValue;

Wyświetl plik

@ -2,14 +2,10 @@ package sh.ball.audio.effect;
public abstract class PhaseEffect implements Effect {
// sufficiently large so that nextTheta doesn't commonly reach it, otherwise
// there are audio artifacts
private static final double LARGE_VAL = 2 << 20;
protected int sampleRate;
protected double speed;
private double phase = -LARGE_VAL;
private double phase = 0;
protected PhaseEffect(int sampleRate, double speed) {
this.sampleRate = sampleRate;
@ -17,17 +13,17 @@ public abstract class PhaseEffect implements Effect {
}
public void resetTheta() {
phase = -LARGE_VAL;
phase = 0;
}
protected double nextTheta() {
phase += speed / sampleRate;
if (phase >= LARGE_VAL) {
phase = -LARGE_VAL;
if (phase > 1) {
phase -= 1;
}
return phase * Math.PI;
return phase * 2 * Math.PI;
}
public void setSpeed(double speed) {

Wyświetl plik

@ -1,39 +0,0 @@
package sh.ball.audio.effect;
import sh.ball.shapes.Vector2;
// Plays a sine wave at the given frequency and volume
public class SineEffect extends PhaseEffect {
private static final double DEFAULT_VOLUME = 1;
private double frequency;
private double volume;
public SineEffect(int sampleRate, double frequency, double volume) {
super(sampleRate, 2);
this.frequency = frequency;
this.volume = Math.max(Math.min(volume, 1), 0);
}
public SineEffect(int sampleRate, double frequency) {
this(sampleRate, frequency, DEFAULT_VOLUME);
}
public void setVolume(double volume) {
this.volume = volume;
}
public void setFrequency(double frequency) {
this.frequency = frequency;
}
@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.cos(frequency * theta);
return new Vector2(x, y);
}
}

Wyświetl plik

@ -42,7 +42,7 @@ public class GitHubReleaseDetector {
String latestRelease = null;
for (int i = 0; i < releases.length(); i++) {
JSONObject release = releases.getJSONObject(i);
String version = release.getString("tag_name").replaceAll("^v", "");
String version = release.getString("tag_name");
// Compare versions and update the latestRelease variable if necessary
if (compareVersions(version, latestRelease) > 0) {
@ -58,12 +58,14 @@ public class GitHubReleaseDetector {
return null;
}
private static int compareVersions(String v1, String v2) {
public static int compareVersions(String v1, String v2) {
if (v1 == null) {
return -1;
} else if (v2 == null) {
return 1;
}
v1 = v1.replaceAll("^v", "");
v2 = v2.replaceAll("^v", "");
// Split the version strings on "." characters
String[] v1Parts = v1.split("\\.");
String[] v2Parts = v2.split("\\.");

Wyświetl plik

@ -34,7 +34,6 @@ public class EffectComponentGroup extends HBox {
private final DoubleProperty min = new SimpleDoubleProperty();
private final DoubleProperty max = new SimpleDoubleProperty();
private final BooleanProperty micSelected = new SimpleBooleanProperty(false);
private final BooleanProperty enabled = new SimpleBooleanProperty(false);
public EffectComponentGroup() {
EffectComponentGroupController temp;
@ -67,8 +66,12 @@ public class EffectComponentGroup extends HBox {
public synchronized void hideSpinner() {
controller.spinner.setVisible(false);
controller.spinner.setPrefWidth(0);
controller.animationSpinner.setVisible(false);
controller.animationSpinner.setPrefWidth(0);
controller.label.setPrefWidth(90);
controller.slider.setPrefWidth(170);
controller.animationSlider.setPrefWidth(170);
controller.animationSlider.setMajorTickUnit(20);
HBox.setMargin(controller.midi, new Insets(11, 5, 0, -7));
}

Wyświetl plik

@ -70,8 +70,12 @@ public class EffectComponentGroupController implements Initializable, SubControl
@FXML
public Slider slider;
@FXML
public Slider animationSlider;
@FXML
public Spinner<Double> spinner;
@FXML
public Spinner<Double> animationSpinner;
@FXML
public SVGPath midi;
@FXML
public ComboBox<AnimationType> comboBox;
@ -138,7 +142,12 @@ public class EffectComponentGroupController implements Initializable, SubControl
spinner.valueProperty().addListener((o, oldValue, newValue) -> slider.setValue(newValue));
slider.valueProperty().addListener((o, oldValue, newValue) -> spinner.getValueFactory().setValue(newValue.doubleValue()));
List<AnimationType> animations = List.of(AnimationType.STATIC, AnimationType.SEESAW, AnimationType.LINEAR, AnimationType.FORWARD, AnimationType.REVERSE);
animationSpinner.valueProperty().addListener((o, oldValue, newValue) -> animationSlider.setValue(newValue));
animationSlider.valueProperty().addListener((o, oldValue, newValue) -> animationSpinner.getValueFactory().setValue(newValue.doubleValue()));
animationSpinner.setValueFactory(new SpinnerValueFactory.DoubleSpinnerValueFactory(0, 100, 1, 0.05));
List<AnimationType> animations = List.of(AnimationType.values());
comboBox.setItems(FXCollections.observableList(animations));
comboBox.setValue(AnimationType.STATIC);
@ -180,9 +189,12 @@ public class EffectComponentGroupController implements Initializable, SubControl
document.createTextNode(effectCheckBox.selectedProperty().getValue().toString())
);
Element animation = document.createElement("animation");
animation.appendChild(document.createTextNode(comboBox.getValue().toString()));
animation.appendChild(document.createTextNode(comboBox.getValue().getName()));
Element animationSpeed = document.createElement("animationSpeed");
animationSpeed.appendChild(document.createTextNode(Double.toString(animationSlider.getValue())));
checkBox.appendChild(selected);
checkBox.appendChild(animation);
checkBox.appendChild(animationSpeed);
return List.of(checkBox);
}
@ -203,11 +215,23 @@ public class EffectComponentGroupController implements Initializable, SubControl
selected = checkBox.getElementsByTagName("selected").item(0).getTextContent();
if (checkBox.getElementsByTagName("animation").getLength() > 0) {
String animation = checkBox.getElementsByTagName("animation").item(0).getTextContent();
comboBox.setValue(AnimationType.fromString(animation));
AnimationType type = AnimationType.fromString(animation);
comboBox.setValue(type);
if (checkBox.getElementsByTagName("animationSpeed").getLength() > 0) {
String animationSpeed = checkBox.getElementsByTagName("animationSpeed").item(0).getTextContent();
animationSlider.setValue(Double.parseDouble(animationSpeed));
} else if (type != AnimationType.STATIC) {
// backwards compatibility - must translate the old animation that uses the slider value,
// to the new animation that uses the animation slider value
double percentage = (slider.getValue() - slider.getMin()) / (slider.getMax() - slider.getMin());
animationSlider.setValue(percentage * 10);
}
}
} else {
selected = checkBox.getTextContent();
comboBox.setValue(AnimationType.STATIC);
animationSlider.setValue(1.0);
}
effectCheckBox.setSelected(Boolean.parseBoolean(selected) || model.isAlwaysEnabled());
}
@ -227,7 +251,9 @@ public class EffectComponentGroupController implements Initializable, SubControl
this.animator.setValue(slider.getValue());
updater.run(model.getType(), selected, this.animator);
slider.setDisable(!selected);
animationSlider.setDisable(!selected);
spinner.setDisable(!selected);
animationSpinner.setDisable(!selected);
};
effectCheckBox.selectedProperty().addListener(listener);
@ -250,12 +276,23 @@ public class EffectComponentGroupController implements Initializable, SubControl
slider.minProperty().addListener((e, old, min) -> this.animator.setMin(min.doubleValue()));
slider.maxProperty().addListener((e, old, max) -> this.animator.setMax(max.doubleValue()));
slider.valueProperty().addListener((e, old, value) -> this.animator.setValue(value.doubleValue()));
animationSlider.valueProperty().addListener((e, old, value) -> this.animator.setSpeed(value.doubleValue()));
comboBox.valueProperty().addListener((options, old, animationType) -> {
this.animator.setAnimation(animationType);
if (animationType != AnimationType.STATIC) {
slider.setStyle("-thumb-color: #00ff00;");
animationSlider.setVisible(true);
slider.setVisible(false);
if (!model.isSpinnerHidden()) {
animationSpinner.setVisible(true);
}
spinner.setVisible(false);
} else {
slider.setStyle("");
animationSlider.setVisible(false);
slider.setVisible(true);
animationSpinner.setVisible(false);
if (!model.isSpinnerHidden()) {
spinner.setVisible(true);
}
}
});
}
@ -284,7 +321,9 @@ public class EffectComponentGroupController implements Initializable, SubControl
public void setInactive(boolean inactive) {
slider.setDisable(inactive);
animationSlider.setDisable(inactive);
spinner.setDisable(inactive);
animationSpinner.setDisable(inactive);
}
public void setValue(double value) {

Wyświetl plik

@ -55,6 +55,7 @@ import sh.ball.audio.midi.MidiListener;
import sh.ball.audio.midi.MidiNote;
import sh.ball.engine.ObjectServer;
import sh.ball.engine.ObjectSet;
import sh.ball.gui.GitHubReleaseDetector;
import sh.ball.gui.Gui;
import sh.ball.gui.components.EffectComponentGroup;
import sh.ball.oscilloscope.ByteWebSocketServer;
@ -1095,9 +1096,14 @@ public class MainController implements Initializable, FrequencyListener, MidiLis
}
}
private void loadSliderValues(List<Slider> sliders, List<String> labels, Element root) {
private void loadSliderValues(List<Slider> sliders, List<String> labels, Element root, String version) {
for (int i = 0; i < sliders.size(); i++) {
NodeList nodes = root.getElementsByTagName(labels.get(i));
String label = labels.get(i);
NodeList nodes = root.getElementsByTagName(label);
// backwards compatibility (PhaseEffect speed was doubled in v1.33.0)
boolean phaseEffectSpeedDoubled = (label.equals("translationSpeed") || label.equals("rotateSpeed")) && (version == null || GitHubReleaseDetector.compareVersions(version, "1.33.0") < 0);
// backwards compatibility
if (nodes.getLength() > 0) {
Element sliderElement = (Element) nodes.item(0);
@ -1110,13 +1116,24 @@ public class MainController implements Initializable, FrequencyListener, MidiLis
value = sliderElement.getElementsByTagName("value").item(0).getTextContent();
String min = sliderElement.getElementsByTagName("min").item(0).getTextContent();
String max = sliderElement.getElementsByTagName("max").item(0).getTextContent();
slider.setMin(Double.parseDouble(min));
slider.setMax(Double.parseDouble(max));
double minValue = Double.parseDouble(min);
double maxValue = Double.parseDouble(max);
if (phaseEffectSpeedDoubled) {
minValue /= 2;
maxValue /= 2;
}
slider.setMin(minValue);
slider.setMax(maxValue);
updateSliderUnits(slider);
}
slider.setValue(Double.parseDouble(value));
targetSliderValue[i] = Double.parseDouble(value);
double sliderValue = Double.parseDouble(value);
if (phaseEffectSpeedDoubled) {
sliderValue /= 2;
}
slider.setValue(sliderValue);
targetSliderValue[i] = sliderValue;
}
}
@ -1310,8 +1327,12 @@ public class MainController implements Initializable, FrequencyListener, MidiLis
generalController.disablePlayback();
Element root = document.getDocumentElement();
String version = null;
if (root.getElementsByTagName("version").getLength() > 0) {
version = root.getElementsByTagName("version").item(0).getTextContent();
}
Element slidersElement = (Element) root.getElementsByTagName("sliders").item(0);
loadSliderValues(sliders, labels, slidersElement);
loadSliderValues(sliders, labels, slidersElement, version);
// doesn't exist on newer projects - backwards compatibility
Element objectRotation = (Element) root.getElementsByTagName("objectRotation").item(0);

Wyświetl plik

@ -95,7 +95,7 @@ public class ProjectSelectController implements Initializable {
Platform.runLater(() -> {
String currentVersion = versionLabel.getText().replaceAll("^v", "");
if (!latestRelease.equals(currentVersion)) {
latestReleaseHyperlink.setText("v" + latestRelease + " is the latest version on GitHub!");
latestReleaseHyperlink.setText(latestRelease + " is the latest version on GitHub!");
projectVBox.getChildren().add(projectVBox.getChildren().size() - 1, latestReleaseHyperlink);
}
});

Wyświetl plik

@ -1,3 +1,19 @@
- 1.33.0
- Overhaul how animations work in the interface for more precise and intuitive control over the animation speed
- When the animation type changes, the range of the slider changes and changing the slider changes the frequency of the animation
- The value of the slider when there is no animation (Static) is saved and restored when the animation is removed
- Animation speeds vary from 0Hz to 100Hz
- You should not notice any difference in how animation is enabled or controlled, but the animation speed slider now goes much faster
- As before, to control the range of the animation, just change the range of the slider from the "Slider" menu
- Add new animation type: "Sine"
- Add new animation type: "Square"
- Rename "Linear" animation to "Triangle"
- Rename "Forward" animation to "Sawtooth"
- Rename "Reverse" animation to "Reverse Sawtooth"
- These new changes should be backwards compatible with projects in prior versions
- If you notice any issues or differences in projects loaded from older versions, please let me know!
- 1.32.3
- Only check for XTAudio devices if not on macOS as it is not available on macOS

Wyświetl plik

@ -129,6 +129,10 @@
-fx-shape: "M460,530.874 1,265.87 460,0.866z";
}
.animationSlider > .thumb {
-thumb-color: #00ff00;
}
.thresholdSlider > .track {
-fx-background-color: transparent;
}

Wyświetl plik

@ -7,9 +7,10 @@
<?import javafx.scene.control.Spinner?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.shape.SVGPath?>
<fx:root maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="41.0" prefWidth="567.0" type="HBox" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sh.ball.gui.components.EffectComponentGroupController">
<fx:root maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="41.0" prefWidth="567.0" type="HBox" xmlns="http://javafx.com/javafx/19" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sh.ball.gui.components.EffectComponentGroupController">
<children>
<Region prefHeight="0.0" prefWidth="5.0" />
<CheckBox fx:id="effectCheckBox" mnemonicParsing="false" prefHeight="18.0" prefWidth="125.0" text="Vector cancel">
@ -17,16 +18,40 @@
<Insets top="5.0" />
</HBox.margin>
</CheckBox>
<Slider fx:id="slider" blockIncrement="0.005" disable="true" majorTickUnit="0.1" max="1.0" prefHeight="42.0" prefWidth="208.0" showTickLabels="true" showTickMarks="true" value="0.11111111">
<HBox.margin>
<Insets right="2.0" />
</HBox.margin>
</Slider>
<Spinner fx:id="spinner" disable="true" editable="true" prefHeight="26.0" prefWidth="66.0">
<HBox.margin>
<Insets top="1.0" />
</HBox.margin>
</Spinner>
<StackPane>
<children>
<Slider fx:id="slider" blockIncrement="0.005" disable="true" majorTickUnit="0.1" max="1.0" prefHeight="42.0" prefWidth="208.0" showTickLabels="true" showTickMarks="true" value="0.11111111">
<HBox.margin>
<Insets right="2.0" />
</HBox.margin>
</Slider>
<Slider fx:id="animationSlider" blockIncrement="0.05" disable="true" majorTickUnit="10" max="100.0" prefHeight="42.0" prefWidth="208.0" showTickLabels="true" showTickMarks="true" styleClass="animationSlider" value="1" visible="false">
<HBox.margin>
<Insets right="2.0" />
</HBox.margin>
</Slider>
</children>
</StackPane>
<StackPane>
<children>
<Spinner fx:id="spinner" disable="true" editable="true" prefHeight="26.0" prefWidth="66.0">
<HBox.margin>
<Insets top="1.0" />
</HBox.margin>
<StackPane.margin>
<Insets top="-14.0" />
</StackPane.margin>
</Spinner>
<Spinner fx:id="animationSpinner" disable="true" editable="true" prefHeight="26.0" prefWidth="66.0" visible="false">
<HBox.margin>
<Insets top="1.0" />
</HBox.margin>
<StackPane.margin>
<Insets top="-14.0" />
</StackPane.margin>
</Spinner>
</children>
</StackPane>
<SVGPath fx:id="midi" content="M20.15 8.26H22V15.74H20.15M13 8.26H18.43C19 8.26 19.3 8.74 19.3 9.3V14.81C19.3 15.5 19 15.74 18.38 15.74H13V11H14.87V13.91H17.5V9.95H13M10.32 8.26H12.14V15.74H10.32M2 8.26H8.55C9.1 8.26 9.41 8.74 9.41 9.3V15.74H7.59V10.15H6.5V15.74H4.87V10.15H3.83V15.74H2Z" fill="WHITE" pickOnBounds="true">
<HBox.margin>
<Insets left="3.0" right="3.0" top="11.0" />

Wyświetl plik

@ -68,7 +68,7 @@
</children>
</AnchorPane>
<EffectComponentGroup fx:id="translationScale" alwaysEnabled="true" increment="0.05" label="translationScale" majorTickUnit="1.0" max="10.0" min="0.0" name="Translation scale" type="TRANSLATE" value="1.0" />
<EffectComponentGroup fx:id="translationSpeed" alwaysEnabled="true" increment="0.05" label="translationSpeed" majorTickUnit="1.0" max="10.0" min="0.0" name="Translation speed" type="TRANSLATE_SPEED" value="1.0" />
<EffectComponentGroup fx:id="translationSpeed" alwaysEnabled="true" increment="0.01" label="translationSpeed" majorTickUnit="0.5" max="5.0" min="0.0" name="Translation speed" type="TRANSLATE_SPEED" value="1.0" />
<EffectComponentGroup fx:id="backingMidi" alwaysEnabled="true" increment="0.005" label="visibility" majorTickUnit="0.1" max="1.0" min="0.0" name="MIDI volume" type="VISIBILITY" value="0.0" />
</children>
</VBox>
@ -127,7 +127,7 @@
<Insets bottom="5.0" />
</padding>
</Separator>
<EffectComponentGroup fx:id="rotateSpeed" increment="0.05" label="rotateSpeed" majorTickUnit="1.0" max="10.0" min="0.0" name="2D Rotate speed" type="ROTATE" value="0.0" />
<EffectComponentGroup fx:id="rotateSpeed" increment="0.01" label="rotateSpeed" majorTickUnit="0.5" max="5.0" min="0.0" name="2D Rotate speed" type="ROTATE" value="0.0" />
<AnchorPane prefHeight="34.0" prefWidth="602.0">
<children>
<Button fx:id="resetRotationButton" layoutX="245.0" layoutY="1.0" mnemonicParsing="false" text="Reset 2D Rotation" />

Wyświetl plik

@ -48,7 +48,7 @@
<CheckBox fx:id="startMutedCheckBox" mnemonicParsing="false" text="Start muted" />
<HBox alignment="CENTER" spacing="20.0">
<children>
<Label fx:id="versionLabel" minWidth="42.0" prefHeight="18.0" prefWidth="42.0" text="v1.32.3" />
<Label fx:id="versionLabel" minWidth="42.0" prefHeight="18.0" prefWidth="42.0" text="v1.33.0" />
<Label prefHeight="54.0" prefWidth="379.0" text="Email me at james@ball.sh or create an issue on GitHub for feature suggestions, issues, or opportunites I might be interested in!" textAlignment="CENTER" wrapText="true" />
<Button fx:id="logButton" minWidth="102.0" mnemonicParsing="false" prefWidth="102.0" text="Open log folder" />
</children>