diff --git a/ControlPad/EventHandler.cs b/ControlPad/EventHandler.cs
index f7a37ca..847e940 100644
--- a/ControlPad/EventHandler.cs
+++ b/ControlPad/EventHandler.cs
@@ -183,7 +183,14 @@ private void ButtonEvent(CustomButton button, int currentValue, int oldValue)
private float SliderToFloat(int value, int mode = 0)
{
- return SliderValueConverter.SliderToFloat(value, Settings.TranslationExponent);
+ value -= 1;
+ float normalized = Math.Clamp((float)value / 1022.0f, 0f, 1f);
+
+ // Clamp to zero when slider is at or near the bottom to ensure complete silence
+ if (normalized < 0.005f)
+ return 0f;
+
+ return SliderTranslationCurve.Apply(normalized);
}
}
}
diff --git a/ControlPad/Settings.cs b/ControlPad/Settings.cs
index c1d97dd..46f3005 100644
--- a/ControlPad/Settings.cs
+++ b/ControlPad/Settings.cs
@@ -15,6 +15,11 @@ public static class Settings
private static bool _startMinimized = false;
private static bool _minimizeToSystemTray = true;
private static double _translationExponent = 1d;
+ private static string _translationCurvePreset = "linear";
+ private static double _translationCurveX1 = 0d;
+ private static double _translationCurveY1 = 0d;
+ private static double _translationCurveX2 = 1d;
+ private static double _translationCurveY2 = 1d;
private static int _selectedThemeIndex = 0;
private static int _selectedBackgroundIndex = 3;
private static int _sliderDeadZone = 4;
@@ -115,6 +120,56 @@ public static double TranslationExponent
}
}
+ public static string TranslationCurvePreset
+ {
+ get => _translationCurvePreset;
+ set
+ {
+ _translationCurvePreset = SliderTranslationCurve.IsSupportedPreset(value) ? value : "linear";
+ Save();
+ }
+ }
+
+ public static double TranslationCurveX1
+ {
+ get => _translationCurveX1;
+ set
+ {
+ _translationCurveX1 = Math.Clamp(value, 0d, 1d);
+ Save();
+ }
+ }
+
+ public static double TranslationCurveY1
+ {
+ get => _translationCurveY1;
+ set
+ {
+ _translationCurveY1 = Math.Clamp(value, 0d, 1d);
+ Save();
+ }
+ }
+
+ public static double TranslationCurveX2
+ {
+ get => _translationCurveX2;
+ set
+ {
+ _translationCurveX2 = Math.Clamp(value, 0d, 1d);
+ Save();
+ }
+ }
+
+ public static double TranslationCurveY2
+ {
+ get => _translationCurveY2;
+ set
+ {
+ _translationCurveY2 = Math.Clamp(value, 0d, 1d);
+ Save();
+ }
+ }
+
private class Data
{
public bool TrayIconMessageShown { get; set; } = false;
@@ -122,6 +177,11 @@ private class Data
public bool StartMinimized { get; set; } = true;
public bool MinimizeToSystemTray { get; set; } = true;
public double TranslationExponent { get; set; } = 1d;
+ public string TranslationCurvePreset { get; set; } = "linear";
+ public double TranslationCurveX1 { get; set; } = 0d;
+ public double TranslationCurveY1 { get; set; } = 0d;
+ public double TranslationCurveX2 { get; set; } = 1d;
+ public double TranslationCurveY2 { get; set; } = 1d;
public int SelectedThemeIndex { get; set; } = 0;
public int SelectedBackgroundIndex { get; set; } = 3;
public int SliderDeadZone { get; set; } = 4;
@@ -148,6 +208,11 @@ public static void Load()
_selectedBackgroundIndex = data.SelectedBackgroundIndex;
_sliderDeadZone = data.SliderDeadZone;
_translationExponent = data.TranslationExponent;
+ _translationCurvePreset = SliderTranslationCurve.IsSupportedPreset(data.TranslationCurvePreset) ? data.TranslationCurvePreset : "linear";
+ _translationCurveX1 = Math.Clamp(data.TranslationCurveX1, 0d, 1d);
+ _translationCurveY1 = Math.Clamp(data.TranslationCurveY1, 0d, 1d);
+ _translationCurveX2 = Math.Clamp(data.TranslationCurveX2, 0d, 1d);
+ _translationCurveY2 = Math.Clamp(data.TranslationCurveY2, 0d, 1d);
_unmuteOnSliderChange = data.UnmuteOnSliderChange;
}
catch
@@ -170,6 +235,11 @@ private static void Save()
SelectedBackgroundIndex = _selectedBackgroundIndex,
SliderDeadZone = _sliderDeadZone,
TranslationExponent = _translationExponent,
+ TranslationCurvePreset = _translationCurvePreset,
+ TranslationCurveX1 = _translationCurveX1,
+ TranslationCurveY1 = _translationCurveY1,
+ TranslationCurveX2 = _translationCurveX2,
+ TranslationCurveY2 = _translationCurveY2,
UnmuteOnSliderChange = _unmuteOnSliderChange,
};
@@ -183,4 +253,4 @@ private static void Save()
}
}
}
-}
\ No newline at end of file
+}
diff --git a/ControlPad/SliderTranslationCurve.cs b/ControlPad/SliderTranslationCurve.cs
new file mode 100644
index 0000000..700f2ec
--- /dev/null
+++ b/ControlPad/SliderTranslationCurve.cs
@@ -0,0 +1,104 @@
+namespace ControlPad
+{
+ public static class SliderTranslationCurve
+ {
+ private const double Epsilon = 1e-6;
+ private const int MaxBisectionIterations = 20;
+ private static string? _lastPreset;
+ private static double _lastX1;
+ private static double _lastY1;
+ private static double _lastX2;
+ private static double _lastY2;
+ private static (double x1, double y1, double x2, double y2) _cachedControlPoints = (0d, 0d, 1d, 1d);
+
+ public static bool IsSupportedPreset(string? preset)
+ {
+ return preset is "ease" or "linear" or "ease-in" or "ease-out" or "ease-in-out" or "custom";
+ }
+
+ public static (double x1, double y1, double x2, double y2) GetPresetControlPoints(string preset)
+ {
+ return preset switch
+ {
+ "ease" => (0.25d, 0.1d, 0.25d, 1d),
+ "linear" => (0d, 0d, 1d, 1d),
+ "ease-in" => (0.42d, 0d, 1d, 1d),
+ "ease-out" => (0d, 0d, 0.58d, 1d),
+ "ease-in-out" => (0.42d, 0d, 0.58d, 1d),
+ _ => (0d, 0d, 1d, 1d),
+ };
+ }
+
+ public static float Apply(float input)
+ {
+ double t = Math.Clamp(input, 0f, 1f);
+ var controlPoints = GetControlPoints();
+
+ return (float)Evaluate(controlPoints.Item1, controlPoints.Item2, controlPoints.Item3, controlPoints.Item4, t);
+ }
+
+ private static (double x1, double y1, double x2, double y2) GetControlPoints()
+ {
+ string preset = Settings.TranslationCurvePreset;
+ double x1 = Settings.TranslationCurveX1;
+ double y1 = Settings.TranslationCurveY1;
+ double x2 = Settings.TranslationCurveX2;
+ double y2 = Settings.TranslationCurveY2;
+
+ if (_lastPreset == preset && _lastX1 == x1 && _lastY1 == y1 && _lastX2 == x2 && _lastY2 == y2)
+ return _cachedControlPoints;
+
+ _cachedControlPoints = preset == "custom"
+ ? (x1, y1, x2, y2)
+ : GetPresetControlPoints(preset);
+
+ _lastPreset = preset;
+ _lastX1 = x1;
+ _lastY1 = y1;
+ _lastX2 = x2;
+ _lastY2 = y2;
+ return _cachedControlPoints;
+ }
+
+ private static double Evaluate(double x1, double y1, double x2, double y2, double xTarget)
+ {
+ x1 = Math.Clamp(x1, 0d, 1d);
+ x2 = Math.Clamp(x2, 0d, 1d);
+ y1 = Math.Clamp(y1, 0d, 1d);
+ y2 = Math.Clamp(y2, 0d, 1d);
+ xTarget = Math.Clamp(xTarget, 0d, 1d);
+
+ if (xTarget <= 0d)
+ return 0d;
+ if (xTarget >= 1d)
+ return 1d;
+
+ double lower = 0d;
+ double upper = 1d;
+ double t = xTarget;
+
+ for (int i = 0; i < MaxBisectionIterations; i++)
+ {
+ t = (lower + upper) * 0.5d;
+ double x = Bezier(t, 0d, x1, x2, 1d);
+ if (Math.Abs(x - xTarget) < Epsilon)
+ break;
+ if (x < xTarget)
+ lower = t;
+ else
+ upper = t;
+ }
+
+ return Bezier(t, 0d, y1, y2, 1d);
+ }
+
+ private static double Bezier(double t, double p0, double p1, double p2, double p3)
+ {
+ double oneMinusT = 1d - t;
+ return oneMinusT * oneMinusT * oneMinusT * p0
+ + 3d * oneMinusT * oneMinusT * t * p1
+ + 3d * oneMinusT * t * t * p2
+ + t * t * t * p3;
+ }
+ }
+}
diff --git a/ControlPad/UI Elements/SettingsUserControl.xaml b/ControlPad/UI Elements/SettingsUserControl.xaml
index 73ea140..13d6ca6 100644
--- a/ControlPad/UI Elements/SettingsUserControl.xaml
+++ b/ControlPad/UI Elements/SettingsUserControl.xaml
@@ -130,7 +130,7 @@
Unchecked="cb_UnmuteOnSliderChange_Checked"/>
-
+
@@ -138,23 +138,66 @@
-
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ControlPad/UI Elements/SettingsUserControl.xaml.cs b/ControlPad/UI Elements/SettingsUserControl.xaml.cs
index f8a82c7..322a4cc 100644
--- a/ControlPad/UI Elements/SettingsUserControl.xaml.cs
+++ b/ControlPad/UI Elements/SettingsUserControl.xaml.cs
@@ -21,6 +21,8 @@ public partial class SettingsUserControl : UserControl
{
private readonly bool _isInitialized = false;
private readonly MainWindow _mainWindow;
+ private static readonly string[] TranslationCurvePresets = { "ease", "linear", "ease-in", "ease-out", "ease-in-out", "custom" };
+ private bool _suppressCustomCurveEvents = false;
public SettingsUserControl(MainWindow mainWindow)
{
@@ -89,15 +91,36 @@ private void BackgroundComboBox_SelectionChanged(object sender, SelectionChanged
Settings.SelectedBackgroundIndex = BackgroundComboBox.SelectedIndex;
}
- private void nb_TranslationExponent_ValueChanged(object sender, Wpf.Ui.Controls.NumberBoxValueChangedEventArgs e)
+ private void TranslationCurvePresetComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (!_isInitialized)
return;
- if (nb_TranslationExponent.Value != null)
- Settings.TranslationExponent = (double)nb_TranslationExponent.Value;
- else
- Settings.TranslationExponent = 1.0d;
+ string preset = TranslationCurvePresets[Math.Clamp(TranslationCurvePresetComboBox.SelectedIndex, 0, TranslationCurvePresets.Length - 1)];
+ Settings.TranslationCurvePreset = preset;
+
+ if (preset != "custom")
+ {
+ var cp = SliderTranslationCurve.GetPresetControlPoints(preset);
+ Settings.TranslationCurveX1 = cp.x1;
+ Settings.TranslationCurveY1 = cp.y1;
+ Settings.TranslationCurveX2 = cp.x2;
+ Settings.TranslationCurveY2 = cp.y2;
+ SetCustomCurveControls(cp.x1, cp.y1, cp.x2, cp.y2);
+ }
+
+ CustomCurveGrid.IsEnabled = preset == "custom";
+ }
+
+ private void nb_CustomCurve_ValueChanged(object sender, Wpf.Ui.Controls.NumberBoxValueChangedEventArgs e)
+ {
+ if (!_isInitialized || _suppressCustomCurveEvents || Settings.TranslationCurvePreset != "custom")
+ return;
+
+ Settings.TranslationCurveX1 = nb_CurveX1.Value ?? 0d;
+ Settings.TranslationCurveY1 = nb_CurveY1.Value ?? 0d;
+ Settings.TranslationCurveX2 = nb_CurveX2.Value ?? 1d;
+ Settings.TranslationCurveY2 = nb_CurveY2.Value ?? 1d;
}
public static void ChangeAppTheme(int index)
@@ -146,13 +169,32 @@ public void SetControls()
cb_UnmuteOnSliderChange.IsChecked = Settings.UnmuteOnSliderChange;
ThemeComboBox.SelectedIndex = Settings.SelectedThemeIndex;
BackgroundComboBox.SelectedIndex = Settings.SelectedBackgroundIndex;
- nb_TranslationExponent.Value = Settings.TranslationExponent;
+ int presetIndex = Array.IndexOf(TranslationCurvePresets, Settings.TranslationCurvePreset);
+ TranslationCurvePresetComboBox.SelectedIndex = presetIndex >= 0 ? presetIndex : 1;
+ SetCustomCurveControls(Settings.TranslationCurveX1, Settings.TranslationCurveY1, Settings.TranslationCurveX2, Settings.TranslationCurveY2);
+ CustomCurveGrid.IsEnabled = Settings.TranslationCurvePreset == "custom";
var infoVersion = Assembly.GetExecutingAssembly()
.GetCustomAttribute()?.InformationalVersion;
lbl_AppVersion.Content = !string.IsNullOrEmpty(infoVersion) ? infoVersion : "Unknown";
}
+ private void SetCustomCurveControls(double x1, double y1, double x2, double y2)
+ {
+ _suppressCustomCurveEvents = true;
+ try
+ {
+ nb_CurveX1.Value = x1;
+ nb_CurveY1.Value = y1;
+ nb_CurveX2.Value = x2;
+ nb_CurveY2.Value = y2;
+ }
+ finally
+ {
+ _suppressCustomCurveEvents = false;
+ }
+ }
+
private void Btn_Presets_Click(object sender, RoutedEventArgs e)
{
var dialog = new PresetManagerWindow(this);