Preview easing curves beside curve dropdowns
CurveProperty holds the easing curves in the order Animation.getCurve takes them, replacing three copies of the list in zoom, freelook and the animation config; it's saved the same way. Its dropdown opens a CurveOptions popup: a CurvePreview graph beside the list, with a dot running along the hovered curve, or the current one. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -4,9 +4,9 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.saturnclient.config.property.CurveProperty;
|
||||
import org.saturnclient.config.property.IntProperty;
|
||||
import org.saturnclient.config.property.Property;
|
||||
import org.saturnclient.config.property.SelectProperty;
|
||||
|
||||
public class AnimationConfig {
|
||||
public static ConfigManager config;
|
||||
@@ -47,12 +47,12 @@ public class AnimationConfig {
|
||||
// Animation properties
|
||||
public final IntProperty duration;
|
||||
public final IntProperty stagger;
|
||||
public final SelectProperty curve;
|
||||
public final CurveProperty curve;
|
||||
|
||||
public AnimationConfig(boolean enabled, int delay, int stagger) {
|
||||
this.duration = Property.integer(delay).range(0, 2000, 50).unit("ms");
|
||||
this.stagger = Property.integer(stagger).range(0, 300, 10).unit("ms");
|
||||
this.curve = Property.select(1, "Ease In Out Cubic", "Ease Out Cubic", "Ease In Out Back");
|
||||
this.curve = Property.curve(1);
|
||||
}
|
||||
|
||||
/** The menu this animates, like "Mod Menu" */
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.saturnclient.config.property;
|
||||
|
||||
/**
|
||||
* An animation's easing curve. The options are in the order
|
||||
* {@code Animation.getCurve} takes them, and it's saved like any select.
|
||||
*/
|
||||
public class CurveProperty extends SelectProperty {
|
||||
|
||||
public CurveProperty(int defaultValue) {
|
||||
super(defaultValue, "Ease In Out Cubic", "Ease Out Cubic", "Ease In Out Back");
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,10 @@ public abstract class Property {
|
||||
return select(value, "Default", "Inter", "Inter bold");
|
||||
}
|
||||
|
||||
public static CurveProperty curve(int value) {
|
||||
return new CurveProperty(value);
|
||||
}
|
||||
|
||||
public static SelectProperty select(Integer value, String... availableValues) {
|
||||
return new SelectProperty(value, availableValues);
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@ import java.util.function.Function;
|
||||
import org.saturnclient.common.provider.Providers;
|
||||
import org.saturnclient.common.provider.GLFWProvider;
|
||||
import org.saturnclient.config.property.BoolProperty;
|
||||
import org.saturnclient.config.property.CurveProperty;
|
||||
import org.saturnclient.config.property.FloatProperty;
|
||||
import org.saturnclient.config.property.IntProperty;
|
||||
import org.saturnclient.config.property.KeybindingProperty;
|
||||
import org.saturnclient.config.property.Property;
|
||||
import org.saturnclient.config.property.SelectProperty;
|
||||
import org.saturnclient.mod.Mod;
|
||||
import org.saturnclient.mod.ModSpec;
|
||||
import org.saturnclient.common.feature.RenderFeature;
|
||||
@@ -25,7 +25,7 @@ public class FreelookMod extends Mod {
|
||||
public static final FloatProperty zoomLevel = Property.floatProp(2.0f).range(1f, 5f, 0.1f).unit("×");
|
||||
public static final IntProperty zoomInDuration = Property.integer(500).range(0, 2000, 50).unit("ms");
|
||||
public static final IntProperty zoomOutDuration = Property.integer(300).range(0, 2000, 50).unit("ms");
|
||||
public static final SelectProperty curve = Property.select(1, "Ease In Out Cubic", "Ease Out Cubic", "Ease In Out Back");
|
||||
public static final CurveProperty curve = Property.curve(1);
|
||||
|
||||
// --- State Variables ---
|
||||
private static boolean isFreeLooking = false;
|
||||
|
||||
@@ -4,11 +4,11 @@ import java.util.function.Function;
|
||||
|
||||
import org.saturnclient.common.provider.GLFWProvider;
|
||||
import org.saturnclient.config.property.BoolProperty;
|
||||
import org.saturnclient.config.property.CurveProperty;
|
||||
import org.saturnclient.config.property.FloatProperty;
|
||||
import org.saturnclient.config.property.IntProperty;
|
||||
import org.saturnclient.config.property.KeybindingProperty;
|
||||
import org.saturnclient.config.property.Property;
|
||||
import org.saturnclient.config.property.SelectProperty;
|
||||
import org.saturnclient.mod.Mod;
|
||||
import org.saturnclient.mod.ModSpec;
|
||||
import org.saturnclient.ui.anim.Animation;
|
||||
@@ -21,8 +21,7 @@ public class ZoomMod extends Mod {
|
||||
public static final FloatProperty zoomLevel = Property.floatProp(3.0f).range(1f, 10f, 0.5f).unit("×");
|
||||
public static final IntProperty zoomInDuration = Property.integer(700).range(0, 2000, 50).unit("ms");
|
||||
public static final IntProperty zoomOutDuration = Property.integer(300).range(0, 2000, 50).unit("ms");
|
||||
public static final SelectProperty curve = Property.select(1, "Ease In Out Cubic", "Ease Out Cubic",
|
||||
"Ease In Out Back");
|
||||
public static final CurveProperty curve = Property.curve(1);
|
||||
|
||||
public ZoomMod() {
|
||||
super(
|
||||
|
||||
@@ -44,6 +44,14 @@ public final class Popup {
|
||||
* @param ownerContext the owner's context from its last render
|
||||
*/
|
||||
public static void openBelow(Element owner, Element popup, ElementContext ownerContext) {
|
||||
openBelow(owner, popup, ownerContext, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@link #openBelow(Element, Element, ElementContext)}, with the popup
|
||||
* moved {@code offsetX} from the owner's left edge.
|
||||
*/
|
||||
public static void openBelow(Element owner, Element popup, ElementContext ownerContext, int offsetX) {
|
||||
int top = Math.max(ownerContext.visibleTop, SCREEN_MARGIN);
|
||||
int bottom = Math.min(ownerContext.visibleBottom, screenHeight - SCREEN_MARGIN);
|
||||
|
||||
@@ -55,7 +63,7 @@ public final class Popup {
|
||||
boolean moreRoomBelow = bottom - below >= ownerContext.originY - OWNER_GAP - top;
|
||||
|
||||
int y = fitsBelow || (!fitsAbove && moreRoomBelow) ? below : above;
|
||||
open(owner, popup, ownerContext.originX, y);
|
||||
open(owner, popup, ownerContext.originX + offsetX, y);
|
||||
}
|
||||
|
||||
public static void close() {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.saturnclient.ui.components.inputs;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.saturnclient.ui.Element;
|
||||
import org.saturnclient.ui.ElementContext;
|
||||
import org.saturnclient.ui.ElementRenderer;
|
||||
import org.saturnclient.ui.RenderScope;
|
||||
|
||||
/**
|
||||
* A curve dropdown's popup: a {@link CurvePreview} beside the list of curves,
|
||||
* showing the hovered curve, or the current one when none is hovered.
|
||||
*/
|
||||
public class CurveOptions extends Element {
|
||||
public static final int GAP = 8;
|
||||
|
||||
private final List<Element> children;
|
||||
|
||||
public CurveOptions(OptionList list, int current) {
|
||||
CurvePreview preview = new CurvePreview(() -> list.getHovered() >= 0 ? list.getHovered() : current);
|
||||
|
||||
preview.position(0, 0);
|
||||
list.position(preview.width + GAP, 0);
|
||||
this.children = List.of(preview, list);
|
||||
|
||||
this.width = list.x + list.width;
|
||||
this.height = Math.max(preview.height, list.height);
|
||||
}
|
||||
|
||||
/**
|
||||
* How far left of the list this popup starts, so the list can line up
|
||||
* under the dropdown.
|
||||
*/
|
||||
public int getListX() {
|
||||
return children.get(1).x;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
ElementRenderer.render(children, ctx.elapsed, renderScope, ctx.mouseX, ctx.mouseY, ctx.childFrame());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseClicked(double mouseX, double mouseY, int button) {
|
||||
ElementRenderer.mouseClicked(children, mouseX, mouseY, button);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scroll(int mouseX, int mouseY, double horizontalAmount, double verticalAmount) {
|
||||
ElementRenderer.mouseScrolled(children, mouseX, mouseY, horizontalAmount, verticalAmount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package org.saturnclient.ui.components.inputs;
|
||||
|
||||
import java.util.function.Function;
|
||||
import java.util.function.IntSupplier;
|
||||
|
||||
import org.saturnclient.config.Theme;
|
||||
import org.saturnclient.ui.Element;
|
||||
import org.saturnclient.ui.ElementContext;
|
||||
import org.saturnclient.ui.RenderScope;
|
||||
import org.saturnclient.ui.anim.Animation;
|
||||
|
||||
/**
|
||||
* A graph of an easing curve, with a dot running along it on a loop to show
|
||||
* how the animation moves.
|
||||
*/
|
||||
public class CurvePreview extends Element {
|
||||
private static final int PADDING = 14;
|
||||
private static final int TITLE_HEIGHT = 20;
|
||||
private static final float TITLE_SCALE = 0.56f;
|
||||
private static final int DOT = 7;
|
||||
|
||||
/** How long the dot takes to run the curve, then how long it rests at the end, in ms */
|
||||
private static final int RUN = 1200;
|
||||
private static final int REST = 500;
|
||||
|
||||
private final IntSupplier curve;
|
||||
|
||||
/**
|
||||
* @param curve the curve to show, as an index {@link Animation#getCurve}
|
||||
* takes; read every frame
|
||||
*/
|
||||
public CurvePreview(IntSupplier curve) {
|
||||
this.curve = curve;
|
||||
this.width = 190;
|
||||
this.height = 150;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, 1,
|
||||
Theme.withAlpha(0.97f, Theme.BACKGROUND.value), Theme.withAlpha(0.14f, Theme.FOREGROUND.value));
|
||||
|
||||
renderScope.drawText(TITLE_SCALE, "PREVIEW", PADDING, PADDING - 2, Theme.FONT.value, Theme.getMutedFg());
|
||||
|
||||
Function<Double, Double> ease = Animation.getCurve(curve.getAsInt());
|
||||
|
||||
int graphX = PADDING;
|
||||
int graphY = PADDING + TITLE_HEIGHT;
|
||||
int graphWidth = width - PADDING * 2;
|
||||
int graphHeight = height - PADDING * 2 - TITLE_HEIGHT;
|
||||
|
||||
// Curves like "back" overshoot, so fit the graph to the curve's own range.
|
||||
double min = 0, max = 1;
|
||||
for (int i = 0; i <= graphWidth; i++) {
|
||||
double value = ease.apply(i / (double) graphWidth);
|
||||
min = Math.min(min, value);
|
||||
max = Math.max(max, value);
|
||||
}
|
||||
|
||||
double range = max - min;
|
||||
int startY = toY(0, min, range, graphY, graphHeight);
|
||||
int endY = toY(1, min, range, graphY, graphHeight);
|
||||
|
||||
// Where the animation starts and ends
|
||||
int guide = Theme.withAlpha(0.1f, Theme.FOREGROUND.value);
|
||||
renderScope.fill(graphX, startY, graphX + graphWidth, startY + 1, guide);
|
||||
renderScope.fill(graphX, endY, graphX + graphWidth, endY + 1, guide);
|
||||
|
||||
// The curve, as small squares close enough to read as a line
|
||||
int steps = graphWidth * 3;
|
||||
for (int i = 0; i <= steps; i++) {
|
||||
double t = i / (double) steps;
|
||||
int x = graphX + (int) (t * graphWidth);
|
||||
int y = toY(ease.apply(t), min, range, graphY, graphHeight);
|
||||
renderScope.fill(x - 1, y - 1, x + 1, y + 1, Theme.ACCENT.value);
|
||||
}
|
||||
|
||||
// The dot, looping along it
|
||||
double t = Math.min(1.0, (ctx.elapsed % (RUN + REST)) / (double) RUN);
|
||||
int dotX = graphX + (int) (t * graphWidth);
|
||||
int dotY = toY(ease.apply(t), min, range, graphY, graphHeight);
|
||||
renderScope.drawRoundedRectangle(dotX - DOT / 2, dotY - DOT / 2, DOT, DOT, DOT / 2, 2,
|
||||
Theme.FOREGROUND.value, Theme.BACKGROUND.value);
|
||||
}
|
||||
|
||||
private static int toY(double value, double min, double range, int graphY, int graphHeight) {
|
||||
return graphY + graphHeight - (int) ((value - min) / range * graphHeight);
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,8 @@ public class OptionList extends Element {
|
||||
private int first;
|
||||
/** Wheel movement not yet turned into whole options, so trackpads scroll smoothly */
|
||||
private double pendingScroll = 0;
|
||||
/** The option under the mouse on the last frame, or -1 */
|
||||
private int hovered = -1;
|
||||
|
||||
public OptionList(String[] options, int selected, int width, IntConsumer onPick) {
|
||||
this.options = options;
|
||||
@@ -46,7 +48,7 @@ public class OptionList extends Element {
|
||||
Theme.withAlpha(0.97f, Theme.BACKGROUND.value), Theme.withAlpha(0.14f, Theme.FOREGROUND.value));
|
||||
|
||||
int visible = Math.min(options.length, MAX_VISIBLE);
|
||||
int hovered = getIndexAt(ctx.mouseX, ctx.mouseY);
|
||||
hovered = getIndexAt(ctx.mouseX, ctx.mouseY);
|
||||
|
||||
for (int row = 0; row < visible; row++) {
|
||||
int index = first + row;
|
||||
@@ -95,6 +97,13 @@ public class OptionList extends Element {
|
||||
first = clampFirst(first + whole);
|
||||
}
|
||||
|
||||
/**
|
||||
* The option under the mouse on the last frame, or -1.
|
||||
*/
|
||||
public int getHovered() {
|
||||
return hovered;
|
||||
}
|
||||
|
||||
/**
|
||||
* The option under the mouse, or -1.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.saturnclient.ui.components.inputs;
|
||||
|
||||
import org.saturnclient.config.Theme;
|
||||
import org.saturnclient.config.property.CurveProperty;
|
||||
import org.saturnclient.config.property.SelectProperty;
|
||||
import org.saturnclient.ui.Element;
|
||||
import org.saturnclient.ui.ElementContext;
|
||||
@@ -54,6 +55,12 @@ public class Select extends Element {
|
||||
Popup.close();
|
||||
});
|
||||
|
||||
Popup.openBelow(this, list, lastContext);
|
||||
if (prop instanceof CurveProperty) {
|
||||
// The preview goes left of the list, so the list still lines up under this.
|
||||
CurveOptions options = new CurveOptions(list, prop.value);
|
||||
Popup.openBelow(this, options, lastContext, -options.getListX());
|
||||
} else {
|
||||
Popup.openBelow(this, list, lastContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user