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);