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';
}