From 85c7057ba18150a23f39df4228ab70bc36b1323a Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Sun, 27 Sep 2026 18:19:18 +0200 Subject: [PATCH] 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 --- .../saturnclient/config/AnimationConfig.java | 6 +- .../config/property/CurveProperty.java | 12 +++ .../config/property/Property.java | 4 + .../saturnclient/mod/mods/FreelookMod.java | 4 +- .../org/saturnclient/mod/mods/ZoomMod.java | 5 +- src/main/java/org/saturnclient/ui/Popup.java | 10 ++- .../ui/components/inputs/CurveOptions.java | 52 +++++++++++ .../ui/components/inputs/CurvePreview.java | 89 +++++++++++++++++++ .../ui/components/inputs/OptionList.java | 11 ++- .../ui/components/inputs/Select.java | 9 +- 10 files changed, 191 insertions(+), 11 deletions(-) create mode 100644 src/main/java/org/saturnclient/config/property/CurveProperty.java create mode 100644 src/main/java/org/saturnclient/ui/components/inputs/CurveOptions.java create mode 100644 src/main/java/org/saturnclient/ui/components/inputs/CurvePreview.java diff --git a/src/main/java/org/saturnclient/config/AnimationConfig.java b/src/main/java/org/saturnclient/config/AnimationConfig.java index 32861de..3f429fb 100644 --- a/src/main/java/org/saturnclient/config/AnimationConfig.java +++ b/src/main/java/org/saturnclient/config/AnimationConfig.java @@ -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" */ diff --git a/src/main/java/org/saturnclient/config/property/CurveProperty.java b/src/main/java/org/saturnclient/config/property/CurveProperty.java new file mode 100644 index 0000000..12818d7 --- /dev/null +++ b/src/main/java/org/saturnclient/config/property/CurveProperty.java @@ -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"); + } +} diff --git a/src/main/java/org/saturnclient/config/property/Property.java b/src/main/java/org/saturnclient/config/property/Property.java index 09820dd..c380a7b 100644 --- a/src/main/java/org/saturnclient/config/property/Property.java +++ b/src/main/java/org/saturnclient/config/property/Property.java @@ -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); } diff --git a/src/main/java/org/saturnclient/mod/mods/FreelookMod.java b/src/main/java/org/saturnclient/mod/mods/FreelookMod.java index c4bba29..2e48d8e 100644 --- a/src/main/java/org/saturnclient/mod/mods/FreelookMod.java +++ b/src/main/java/org/saturnclient/mod/mods/FreelookMod.java @@ -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; diff --git a/src/main/java/org/saturnclient/mod/mods/ZoomMod.java b/src/main/java/org/saturnclient/mod/mods/ZoomMod.java index a0f0768..c403a34 100644 --- a/src/main/java/org/saturnclient/mod/mods/ZoomMod.java +++ b/src/main/java/org/saturnclient/mod/mods/ZoomMod.java @@ -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( diff --git a/src/main/java/org/saturnclient/ui/Popup.java b/src/main/java/org/saturnclient/ui/Popup.java index c487e4f..ab92e19 100644 --- a/src/main/java/org/saturnclient/ui/Popup.java +++ b/src/main/java/org/saturnclient/ui/Popup.java @@ -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() { diff --git a/src/main/java/org/saturnclient/ui/components/inputs/CurveOptions.java b/src/main/java/org/saturnclient/ui/components/inputs/CurveOptions.java new file mode 100644 index 0000000..29a7f3d --- /dev/null +++ b/src/main/java/org/saturnclient/ui/components/inputs/CurveOptions.java @@ -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 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); + } +} diff --git a/src/main/java/org/saturnclient/ui/components/inputs/CurvePreview.java b/src/main/java/org/saturnclient/ui/components/inputs/CurvePreview.java new file mode 100644 index 0000000..a5f2b45 --- /dev/null +++ b/src/main/java/org/saturnclient/ui/components/inputs/CurvePreview.java @@ -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 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); + } +} diff --git a/src/main/java/org/saturnclient/ui/components/inputs/OptionList.java b/src/main/java/org/saturnclient/ui/components/inputs/OptionList.java index 7c509fc..83a73d7 100644 --- a/src/main/java/org/saturnclient/ui/components/inputs/OptionList.java +++ b/src/main/java/org/saturnclient/ui/components/inputs/OptionList.java @@ -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. */ diff --git a/src/main/java/org/saturnclient/ui/components/inputs/Select.java b/src/main/java/org/saturnclient/ui/components/inputs/Select.java index e5f6cfc..09ba70e 100644 --- a/src/main/java/org/saturnclient/ui/components/inputs/Select.java +++ b/src/main/java/org/saturnclient/ui/components/inputs/Select.java @@ -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); + } } }