diff --git a/src/ConfigManager.cs b/src/ConfigManager.cs index 7e5a9eb..c8ff05b 100644 --- a/src/ConfigManager.cs +++ b/src/ConfigManager.cs @@ -72,6 +72,7 @@ public static class ConfigManager public static double EffectOpacity { get; set; } = 1.0; public static double EffectSpeed { get; set; } = 1.0; public static bool UseLinkedAnimationSpeed { get; set; } = true; + public static bool ApplyCurveDraw { get; set; } = false; public static double TrailAnimationSpeed { get; set; } = 1.0; public static double ClickAnimationSpeed { get; set; } = 1.0; public static int TrailRefreshRate { get; set; } = 40; @@ -130,6 +131,7 @@ public static void Load() EffectOpacity = Math.Clamp(Convert.ToDouble(key.GetValue("EffectOpacity", 1.0)), 0.1, 1.0); EffectSpeed = Math.Clamp(Convert.ToDouble(key.GetValue("EffectSpeed", 1.0)), 0.2, 3.0); UseLinkedAnimationSpeed = Convert.ToBoolean(key.GetValue("UseLinkedAnimationSpeed", true)); + ApplyCurveDraw = Convert.ToBoolean(key.GetValue("ApplyCurveDraw", false)); TrailAnimationSpeed = Math.Clamp(Convert.ToDouble(key.GetValue("TrailAnimationSpeed", EffectSpeed)), 0.2, 3.0); ClickAnimationSpeed = Math.Clamp(Convert.ToDouble(key.GetValue("ClickAnimationSpeed", EffectSpeed)), 0.2, 3.0); TrailRefreshRate = Math.Clamp(Convert.ToInt32(key.GetValue("TrailRefreshRate", 40)), 10, 240); @@ -532,6 +534,7 @@ public static void ResetAndClear() EffectOpacity = 1.0; EffectSpeed = 1.0; UseLinkedAnimationSpeed = true; + ApplyCurveDraw = false; TrailAnimationSpeed = 1.0; ClickAnimationSpeed = 1.0; TrailRefreshRate = 40; diff --git a/src/ControlPanelWindow.xaml b/src/ControlPanelWindow.xaml index 03f3cda..f24ae04 100644 --- a/src/ControlPanelWindow.xaml +++ b/src/ControlPanelWindow.xaml @@ -524,6 +524,10 @@ Checked="LinkedAnimationSpeed_Changed" Unchecked="LinkedAnimationSpeed_Changed"/> + + + diff --git a/src/ControlPanelWindow.xaml.cs b/src/ControlPanelWindow.xaml.cs index 2e9e43a..f292419 100644 --- a/src/ControlPanelWindow.xaml.cs +++ b/src/ControlPanelWindow.xaml.cs @@ -584,6 +584,7 @@ private void LoadSettingsCore() SliderScale.Value = ConfigManager.EffectScale; SliderOpacity.Value = ConfigManager.EffectOpacity * 100; CheckLinkedAnimationSpeed.IsChecked = ConfigManager.UseLinkedAnimationSpeed; + CheckApplyCurveDraw.IsChecked = ConfigManager.ApplyCurveDraw; SliderSpeed.Value = ConfigManager.EffectSpeed; SliderTrailAnimSpeed.Value = ConfigManager.TrailAnimationSpeed; SliderClickAnimSpeed.Value = ConfigManager.ClickAnimationSpeed; @@ -1317,6 +1318,7 @@ private void ConfirmVisualReset_Click(object sender, RoutedEventArgs e) App.Overlay?.UpdateColor(ConfigManager.ParticleColor); App.Overlay?.UpdateEffectSettings(effectScale, effectOpacity, trailSp, clickSp); App.Overlay?.UpdateTrailRefreshRate(trailRefreshRate); + App.Overlay?.SetCurveDraw(CheckApplyCurveDraw.IsChecked ?? false); VisualResetOverlay.Visibility = Visibility.Collapsed; System.Windows.MessageBox.Show( @@ -1423,6 +1425,7 @@ private void SaveSettings_Click(object sender, RoutedEventArgs e) ConfigManager.Save("ClickTriggerType", clickType); ConfigManager.Save("EnableMiddleClickTrigger", middleClickEnabled); ConfigManager.Save("ScreenshotCompatibilityMode", screenshotCompatibilityEnabled); + ConfigManager.Save("ApplyCurveDraw", CheckApplyCurveDraw.IsChecked ?? false); string sidebarBackgroundPath = TxtSidebarBackgroundPath?.Text?.Trim() ?? string.Empty; if (!string.IsNullOrWhiteSpace(sidebarBackgroundPath)) @@ -1484,6 +1487,7 @@ private void SaveSettings_Click(object sender, RoutedEventArgs e) App.Overlay?.RefreshEnvironmentFilterState(); App.Overlay?.UpdateTouchMode(isTouchscreenEnabled); App.Overlay?.UpdateScreenshotCompatibilityMode(screenshotCompatibilityEnabled); + App.Overlay?.SetCurveDraw(CheckApplyCurveDraw.IsChecked ?? false); if (!previousEnabledScreenIds.SetEquals(selectedIds)) { App.Overlay?.RefreshScreenSelection(); @@ -1694,6 +1698,11 @@ private void EffectSlider_ValueChanged(object sender, RoutedPropertyChangedEvent if (!IsLoaded) return; } + private void CurveDraw_Changed(object sender, RoutedEventArgs e) + { + if (!IsLoaded) return; + } + private void LinkedAnimationSpeed_Changed(object sender, RoutedEventArgs e) { if (!IsLoaded || _suspendLinkedAnimationUiHandlers) diff --git a/src/MainWindow.xaml.cs b/src/MainWindow.xaml.cs index 14ca5cb..1ef778e 100644 --- a/src/MainWindow.xaml.cs +++ b/src/MainWindow.xaml.cs @@ -213,6 +213,11 @@ public void UpdateTrailRefreshRate(int hz) _ = hz; } + public void SetCurveDraw(bool enabled) + { + ExecuteScript($"window.ApplyCurveDraw = {(enabled ? "true" : "false")};"); + } + public void UpdateTouchMode(bool enabled) { ConfigManager.IsTouchscreenMode = enabled; diff --git a/src/OverlayManager.cs b/src/OverlayManager.cs index 6f59a6a..d6f20a3 100644 --- a/src/OverlayManager.cs +++ b/src/OverlayManager.cs @@ -146,6 +146,7 @@ public void UpdateScreenshotCompatibilityMode(bool enabled) ForEachOverlay(w => w.UpdateScreenshotCompatibilityMode(enabled)); SyncScreenshotCompatCaptureSurfaces(); } + public void SetCurveDraw(bool enabled) => ForEachOverlay(w => w.SetCurveDraw(enabled)); public bool IsEffectSuppressedByEnvironment() => _isSuppressedByEnvironment; public void RefreshEnvironmentFilterState() { diff --git a/src/Resources/Strings.en.resx b/src/Resources/Strings.en.resx index 0b7d3c8..b9c56b0 100644 --- a/src/Resources/Strings.en.resx +++ b/src/Resources/Strings.en.resx @@ -586,6 +586,12 @@ Download now? When off, trail and click speeds are independent of trail refresh rate below. + + Use curved trail lines + + + Recommended for 60Hz and above + Global opacity diff --git a/src/Resources/Strings.ja.resx b/src/Resources/Strings.ja.resx index 20b087c..99781ac 100644 --- a/src/Resources/Strings.ja.resx +++ b/src/Resources/Strings.ja.resx @@ -586,6 +586,12 @@ オフにするとトレイルとクリックの速度を個別に設定できます(下の更新率とは別)。 + + 曲線トレイルを使用 + + + 60Hz以上のリフレッシュレートでの使用を推奨します + 全体の不透明度 diff --git a/src/Resources/Strings.resx b/src/Resources/Strings.resx index fcafb17..1d36971 100644 --- a/src/Resources/Strings.resx +++ b/src/Resources/Strings.resx @@ -586,6 +586,12 @@ 关闭此项后可单独加快或减慢拖尾衰减与移动粒子,而不影响点击波纹与爆发粒子,与下方「拖尾刷新率」不同。 + + 采用曲线拖尾 + + + 建议在60Hz及以上刷新率开启 + 全局不透明度 diff --git a/src/UiLocalizer.cs b/src/UiLocalizer.cs index 3b56a73..5d481c7 100644 --- a/src/UiLocalizer.cs +++ b/src/UiLocalizer.cs @@ -93,6 +93,8 @@ public static void ApplyControlPanel(ControlPanelWindow w) w.TxtVisualOpacity.Text = Localization.Get("Visual_Opacity"); w.CheckLinkedAnimationSpeed.Content = Localization.Get("Visual_LinkedSpeed"); w.TxtLinkedSpeedHint.Text = Localization.Get("Visual_LinkedSpeedHint"); + w.CheckApplyCurveDraw.Content = Localization.Get("Visual_CurveDraw"); + w.TxtCurveDrawHint.Text = Localization.Get("Visual_CurveDrawHint"); w.TxtAnimSpeed.Text = Localization.Get("Visual_AnimSpeed"); w.TxtTrailAnimSpeed.Text = Localization.Get("Visual_TrailAnimSpeed"); w.TxtClickAnimSpeed.Text = Localization.Get("Visual_ClickAnimSpeed"); diff --git a/src/Web/index.html b/src/Web/index.html index 80951b4..74b1878 100644 --- a/src/Web/index.html +++ b/src/Web/index.html @@ -337,23 +337,53 @@ ctx.shadowOffsetY = 0; const lastIdx = pts.length - 1; - for (let i = 0; i < lastIdx; i++) { - const alphaStart = i / lastIdx; - const alphaEnd = (i + 1) / lastIdx; - const a0 = pts[i]; - const a1 = pts[i + 1]; + // 每段的透明度基于数组索引位置,而非空间位置 + if (window.ApplyCurveDraw) { + // 逐段渲染(Catmull-Rom → Cubic Bezier 曲线版) + for (let i = 0; i < lastIdx; i++) { + const a0 = pts[i]; + const a1 = pts[i + 1]; + + // Catmull-Rom 控制点:CP1 = P_i + (P_{i+1} - P_{i-1}) / 6 + // CP2 = P_{i+1} - (P_{i+2} - P_i) / 6 + const prev = i > 0 ? pts[i - 1] : a0; + const next = i < lastIdx - 1 ? pts[i + 2] : a1; + const cp1x = a0.x + (a1.x - prev.x) / 6; + const cp1y = a0.y + (a1.y - prev.y) / 6; + const cp2x = a1.x - (next.x - a0.x) / 6; + const cp2y = a1.y - (next.y - a0.y) / 6; + + const alphaStart = i / lastIdx; + const alphaEnd = (i + 1) / lastIdx; + const segGrad = ctx.createLinearGradient(a0.x, a0.y, a1.x, a1.y); + segGrad.addColorStop(0, `rgba(${this.color}, ${alphaStart})`); + segGrad.addColorStop(1, `rgba(${this.color}, ${alphaEnd})`); - const segGrad = ctx.createLinearGradient(a0.x, a0.y, a1.x, a1.y); - segGrad.addColorStop(0, `rgba(${this.color}, ${alphaStart})`); - segGrad.addColorStop(1, `rgba(${this.color}, ${alphaEnd})`); - - ctx.beginPath(); - ctx.moveTo(a0.x, a0.y); - ctx.lineTo(a1.x, a1.y); - ctx.strokeStyle = segGrad; - ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(a0.x, a0.y); + ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, a1.x, a1.y); + ctx.strokeStyle = segGrad; + ctx.stroke(); + } + } else { + // 逐段渲染(直线版) + for (let i = 0; i < lastIdx; i++) { + const alphaStart = i / lastIdx; + const alphaEnd = (i + 1) / lastIdx; + const a0 = pts[i]; + const a1 = pts[i + 1]; + + const segGrad = ctx.createLinearGradient(a0.x, a0.y, a1.x, a1.y); + segGrad.addColorStop(0, `rgba(${this.color}, ${alphaStart})`); + segGrad.addColorStop(1, `rgba(${this.color}, ${alphaEnd})`); + + ctx.beginPath(); + ctx.moveTo(a0.x, a0.y); + ctx.lineTo(a1.x, a1.y); + ctx.strokeStyle = segGrad; + ctx.stroke(); + } } - ctx.shadowColor = 'transparent'; }