From 988af60a3019cc3d3f0033b40da5ed84c9b9cc32 Mon Sep 17 00:00:00 2001 From: CJKmkp <2564608840@qq.com> Date: Thu, 30 Apr 2026 14:25:32 +0800 Subject: [PATCH] =?UTF-8?q?improve:=E5=AE=9E=E6=97=B6=E7=AC=94=E9=94=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ink Canvas/MainWindow.xaml.cs | 1 + .../MainWindow_cs/MW_FloatingBarIcons.cs | 5 +- Ink Canvas/MainWindow_cs/MW_Settings.cs | 5 +- Ink Canvas/MainWindow_cs/MW_ShapeDrawing.cs | 64 ++++++- Ink Canvas/MainWindow_cs/MW_TouchEvents.cs | 169 ++++++++++++++++-- 5 files changed, 229 insertions(+), 15 deletions(-) diff --git a/Ink Canvas/MainWindow.xaml.cs b/Ink Canvas/MainWindow.xaml.cs index 863395c7..260a9373 100644 --- a/Ink Canvas/MainWindow.xaml.cs +++ b/Ink Canvas/MainWindow.xaml.cs @@ -1320,6 +1320,7 @@ namespace Ink_Canvas LogHelper.WriteLogToFile("Ink Canvas Loaded", LogHelper.LogType.Event); isLoaded = true; + EnsureRealtimeStylusPipelineBinding(); BlackBoardLeftSidePageListView.ItemsSource = blackBoardSidePageListViewObservableCollection; BlackBoardRightSidePageListView.ItemsSource = blackBoardSidePageListViewObservableCollection; diff --git a/Ink Canvas/MainWindow_cs/MW_FloatingBarIcons.cs b/Ink Canvas/MainWindow_cs/MW_FloatingBarIcons.cs index a5a11d03..5c7f6623 100644 --- a/Ink Canvas/MainWindow_cs/MW_FloatingBarIcons.cs +++ b/Ink Canvas/MainWindow_cs/MW_FloatingBarIcons.cs @@ -2246,7 +2246,8 @@ namespace Ink_Canvas SetFloatingBarHighlightPosition("pen"); // 记录当前是否已经是批注模式且是否为高光显示模式 - bool wasInInkMode = inkCanvas.EditingMode == InkCanvasEditingMode.Ink; + bool wasInInkMode = inkCanvas.EditingMode == InkCanvasEditingMode.Ink + || (Pen_Icon.Background != null && StackPanelCanvasControls.Visibility == Visibility.Visible); bool wasHighlighter = drawingAttributes.IsHighlighter; if (drawingShapeMode != 0 && !isLongPressSelected) @@ -2451,6 +2452,8 @@ namespace Ink_Canvas forceEraser = false; forcePointEraser = false; drawingShapeMode = 0; + + EnsureRealtimeStylusPipelineBinding(); } /// diff --git a/Ink Canvas/MainWindow_cs/MW_Settings.cs b/Ink Canvas/MainWindow_cs/MW_Settings.cs index c37b7f3a..5d4d7eef 100644 --- a/Ink Canvas/MainWindow_cs/MW_Settings.cs +++ b/Ink Canvas/MainWindow_cs/MW_Settings.cs @@ -1,4 +1,4 @@ -using H.NotifyIcon; +using H.NotifyIcon; using Ink_Canvas.Helpers; using Ink_Canvas.Windows.SettingsViews.Helpers; using Newtonsoft.Json; @@ -517,6 +517,8 @@ namespace Ink_Canvas else ComboBoxPenStyle.SelectedIndex = uiIndex; + EnsureRealtimeStylusPipelineBinding(); + SaveSettingsToFile(); } @@ -721,6 +723,7 @@ namespace Ink_Canvas } Settings.Gesture.IsEnableMultiTouchMode = ToggleSwitchEnableMultiTouchMode.IsOn; + EnsureRealtimeStylusPipelineBinding(); // 如果启用多指书写模式,强制禁用所有双指手势 if (ToggleSwitchEnableMultiTouchMode.IsOn) diff --git a/Ink Canvas/MainWindow_cs/MW_ShapeDrawing.cs b/Ink Canvas/MainWindow_cs/MW_ShapeDrawing.cs index 17d8fe4d..4947edd9 100644 --- a/Ink Canvas/MainWindow_cs/MW_ShapeDrawing.cs +++ b/Ink Canvas/MainWindow_cs/MW_ShapeDrawing.cs @@ -2489,6 +2489,7 @@ namespace Ink_Canvas /// 用于标识鼠标是否处于按下状态,在绘制过程中使用 /// private bool isMouseDown; + private bool _isMouseRealtimeInking; /// /// 触摸按下状态标志 @@ -2512,6 +2513,17 @@ namespace Ink_Canvas /// private void inkCanvas_MouseDown(object sender, MouseButtonEventArgs e) { + if (e.ChangedButton == MouseButton.Left && ShouldUseRealtimeVelocityBrushTip() && drawingShapeMode == 0) + { + _isMouseRealtimeInking = true; + inkCanvas.EditingMode = InkCanvasEditingMode.None; + var p = e.GetPosition(inkCanvas); + InitializeRealtimeBrushTipStateFromPoint(MouseRealtimeStrokeId, p); + var sv = GetStrokeVisual(MouseRealtimeStrokeId); + TryAppendRealtimeVelocityBrushTipPoint(sv, MouseRealtimeStrokeId, p); + sv.ForceRedraw(); + } + inkCanvas.CaptureMouse(); ViewboxFloatingBar.IsHitTestVisible = false; BlackboardUIGridForInkReplay.IsHitTestVisible = false; @@ -2532,7 +2544,24 @@ namespace Ink_Canvas /// private void inkCanvas_MouseMove(object sender, MouseEventArgs e) { - if (isMouseDown) MouseTouchMove(e.GetPosition(inkCanvas)); + if (_isMouseRealtimeInking && isMouseDown) + { + var sv = GetStrokeVisual(MouseRealtimeStrokeId); + var handled = TryAppendRealtimeVelocityBrushTipPoint(sv, MouseRealtimeStrokeId, e.GetPosition(inkCanvas)); + if (handled) + { + sv.ForceRedraw(); + } + else + { + _isMouseRealtimeInking = false; + MouseTouchMove(e.GetPosition(inkCanvas)); + } + } + else if (isMouseDown) + { + MouseTouchMove(e.GetPosition(inkCanvas)); + } if (Settings.Canvas.IsShowCursor) { @@ -2560,6 +2589,39 @@ namespace Ink_Canvas /// private void inkCanvas_MouseUp(object sender, MouseButtonEventArgs e) { + if (_isMouseRealtimeInking) + { + try + { + var sv = GetStrokeVisual(MouseRealtimeStrokeId); + sv?.ForceRedraw(); + var stroke = sv?.Stroke; + if (stroke != null) + { + if (stroke.DrawingAttributes != null) stroke.DrawingAttributes.IgnorePressure = false; + if (!stroke.ContainsPropertyData(RealtimeVelocityBrushTipAppliedGuid)) + stroke.AddPropertyData(RealtimeVelocityBrushTipAppliedGuid, true); + inkCanvas.Strokes.Add(stroke); + inkCanvas_StrokeCollected(inkCanvas, new InkCanvasStrokeCollectedEventArgs(stroke)); + } + + if (VisualCanvasList.TryGetValue(MouseRealtimeStrokeId, out var visualCanvas) && inkCanvas.Children.Contains(visualCanvas)) + inkCanvas.Children.Remove(visualCanvas); + StrokeVisualList.Remove(MouseRealtimeStrokeId); + VisualCanvasList.Remove(MouseRealtimeStrokeId); + TouchDownPointsList.Remove(MouseRealtimeStrokeId); + CleanupRealtimeBrushTipState(MouseRealtimeStrokeId); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine(ex); + } + finally + { + _isMouseRealtimeInking = false; + } + } + HandleEraserOperationEnded(); inkCanvas.ReleaseMouseCapture(); ViewboxFloatingBar.IsHitTestVisible = true; diff --git a/Ink Canvas/MainWindow_cs/MW_TouchEvents.cs b/Ink Canvas/MainWindow_cs/MW_TouchEvents.cs index b3cdcfd1..a313904f 100644 --- a/Ink Canvas/MainWindow_cs/MW_TouchEvents.cs +++ b/Ink Canvas/MainWindow_cs/MW_TouchEvents.cs @@ -50,7 +50,7 @@ namespace Ink_Canvas private InkCanvasEditingMode palmEraserPreviousEditingMode = InkCanvasEditingMode.Ink; private readonly Dictionary _realtimeBrushTipStates = new Dictionary(); private readonly Guid RealtimeVelocityBrushTipAppliedGuid = new Guid("74E57D95-945F-4A8C-B52A-7D3EF2D4FD5B"); - + internal const int MouseRealtimeStrokeId = -100001; private sealed class OneEuroFilter { private readonly float _minCutoff; @@ -102,6 +102,7 @@ namespace Ink_Canvas public float LastRawY { get; set; } public long LastTimestampMs { get; set; } public float SmoothedSampleRateHz { get; set; } = 120f; + public bool SawPressureVariation { get; set; } public bool HasSeed { get; set; } public float LastSmoothX { get; set; } public float LastSmoothY { get; set; } @@ -120,12 +121,44 @@ namespace Ink_Canvas return x; } + private static float WidthToPressure(float width, float baseWidth) + { + if (baseWidth <= 1e-4f) return 0.5f; + var scale = width / baseWidth; + return RealtimeClamp((scale - 0.42f) / 1.16f, 0.08f, 1f); + } + private bool ShouldUseRealtimeVelocityBrushTip() { return Settings.Canvas.InkStyle == 3 && Settings.Canvas.VelocityBrushTipMix > 0 - && !Settings.Canvas.DisablePressure - && penType == 0; + && !Settings.Canvas.DisablePressure; + } + + internal void EnsureRealtimeStylusPipelineBinding() + { + if (inkCanvas == null) return; + + inkCanvas.StylusDown -= MainWindow_StylusDown; + inkCanvas.StylusMove -= MainWindow_StylusMove; + inkCanvas.StylusUp -= MainWindow_StylusUp; + + inkCanvas.StylusDown += MainWindow_StylusDown; + inkCanvas.StylusMove += MainWindow_StylusMove; + inkCanvas.StylusUp += MainWindow_StylusUp; + + if (ShouldUseRealtimeVelocityBrushTip() + && inkCanvas.EditingMode != InkCanvasEditingMode.EraseByPoint + && inkCanvas.EditingMode != InkCanvasEditingMode.EraseByStroke + && inkCanvas.EditingMode != InkCanvasEditingMode.Select) + { + inkCanvas.EditingMode = InkCanvasEditingMode.None; + } + else if (!ShouldUseRealtimeVelocityBrushTip() + && inkCanvas.EditingMode == InkCanvasEditingMode.None) + { + inkCanvas.EditingMode = InkCanvasEditingMode.Ink; + } } private void InitializeRealtimeBrushTipState(int stylusId, StylusDownEventArgs e) @@ -145,6 +178,22 @@ namespace Ink_Canvas }; } + private void InitializeRealtimeBrushTipStateFromPoint(int strokeId, Point startPoint) + { + if (!ShouldUseRealtimeVelocityBrushTip()) + { + _realtimeBrushTipStates.Remove(strokeId); + return; + } + + _realtimeBrushTipStates[strokeId] = new RealtimeBrushTipState + { + LastRawX = (float)startPoint.X, + LastRawY = (float)startPoint.Y, + LastTimestampMs = RealtimeNowMs() + }; + } + private void CleanupRealtimeBrushTipState(int stylusId) { _realtimeBrushTipStates.Remove(stylusId); @@ -164,6 +213,8 @@ namespace Ink_Canvas var mix = RealtimeClamp((float)Settings.Canvas.VelocityBrushTipMix, 0f, 1f); var appended = false; + var baseWidth = (float)Math.Max(0.35, + strokeVisual.Stroke?.DrawingAttributes?.Width ?? inkCanvas.DefaultDrawingAttributes.Width); foreach (StylusPoint rawPoint in stylusPointCollection) { @@ -183,15 +234,24 @@ namespace Ink_Canvas var filteredX = state.FilterX.Filter(rawX, dt, speed); var filteredY = state.FilterY.Filter(rawY, dt, speed); - var speedPressure = RealtimeBrushTipMixRatePressureFromSpeed(GetPointSpeed( - new Point(state.LastRawX, state.LastRawY), - new Point(rawX, rawY), - new Point(filteredX, filteredY))); - var pressure = (1f - mix) * (float)rawPoint.PressureFactor + mix * speedPressure; + var hwPressure = RealtimeClamp((float)rawPoint.PressureFactor, 0f, 1f); + if (Math.Abs(hwPressure - 0.5f) > 0.02f) + state.SawPressureVariation = true; + var usePressure = state.SawPressureVariation && hwPressure > 0f; + + var width = baseWidth; + if (usePressure) + width *= 0.25f + 0.75f * hwPressure; + var speedNormalization = 1800f + state.SmoothedSampleRateHz * 3.5f; + width *= RealtimeClamp(1.15f - (speed / speedNormalization), 0.45f, 1.25f); + var speedPressure = WidthToPressure(width, baseWidth); + + var pressure = usePressure + ? ((1f - mix) * hwPressure + mix * speedPressure) + : speedPressure; pressure = RealtimeClamp(pressure, 0.08f, 1f); pressure = state.FilterPressure.Filter(pressure, dt, speed); - // 高频采样时做最小距离门限,避免点爆炸导致实时重绘卡顿 var minDist = state.SmoothedSampleRateHz > 160f ? 0.55f : state.SmoothedSampleRateHz > 90f ? 0.4f : 0.25f; @@ -213,7 +273,6 @@ namespace Ink_Canvas } else { - // 采用中点链减抖:保持实时笔锋同时降低折线锯齿 var midX = (state.LastSmoothX + filteredX) * 0.5f; var midY = (state.LastSmoothY + filteredY) * 0.5f; var midPressure = (state.LastSmoothPressure + pressure) * 0.5f; @@ -241,6 +300,86 @@ namespace Ink_Canvas return true; } + private bool TryAppendRealtimeVelocityBrushTipPoint(StrokeVisual strokeVisual, int strokeId, Point point, float rawPressure = 0.5f) + { + if (!ShouldUseRealtimeVelocityBrushTip() || strokeVisual == null) + return false; + if (!_realtimeBrushTipStates.TryGetValue(strokeId, out var state)) + return false; + + var mix = RealtimeClamp((float)Settings.Canvas.VelocityBrushTipMix, 0f, 1f); + var nowMs = RealtimeNowMs(); + var dtMs = Math.Max(1L, nowMs - state.LastTimestampMs); + var dt = dtMs / 1000f; + var sampleRate = 1f / Math.Max(1e-4f, dt); + state.SmoothedSampleRateHz = state.SmoothedSampleRateHz * 0.85f + sampleRate * 0.15f; + var baseWidth = (float)Math.Max(0.35, + strokeVisual.Stroke?.DrawingAttributes?.Width ?? inkCanvas.DefaultDrawingAttributes.Width); + + var rawX = (float)point.X; + var rawY = (float)point.Y; + var dx = rawX - state.LastRawX; + var dy = rawY - state.LastRawY; + var dist = (float)Math.Sqrt(dx * dx + dy * dy); + var speed = dist / dt; + + var filteredX = state.FilterX.Filter(rawX, dt, speed); + var filteredY = state.FilterY.Filter(rawY, dt, speed); + + rawPressure = RealtimeClamp(rawPressure, 0f, 1f); + if (Math.Abs(rawPressure - 0.5f) > 0.02f) + state.SawPressureVariation = true; + var usePressure = state.SawPressureVariation && rawPressure > 0f; + + var width = baseWidth; + if (usePressure) + width *= 0.25f + 0.75f * rawPressure; + var speedNormalization = 1800f + state.SmoothedSampleRateHz * 3.5f; + width *= RealtimeClamp(1.15f - (speed / speedNormalization), 0.45f, 1.25f); + var speedPressure = WidthToPressure(width, baseWidth); + + var pressure = usePressure + ? ((1f - mix) * rawPressure + mix * speedPressure) + : speedPressure; + pressure = RealtimeClamp(pressure, 0.08f, 1f); + pressure = state.FilterPressure.Filter(pressure, dt, speed); + + var minDist = state.SmoothedSampleRateHz > 160f ? 0.55f + : state.SmoothedSampleRateHz > 90f ? 0.4f + : 0.25f; + if (dist < minDist && state.HasSeed) + { + state.LastRawX = rawX; + state.LastRawY = rawY; + state.LastTimestampMs = nowMs; + return true; + } + + if (!state.HasSeed) + { + state.HasSeed = true; + state.LastSmoothX = filteredX; + state.LastSmoothY = filteredY; + state.LastSmoothPressure = pressure; + strokeVisual.Add(new StylusPoint(filteredX, filteredY, pressure)); + } + else + { + var midX = (state.LastSmoothX + filteredX) * 0.5f; + var midY = (state.LastSmoothY + filteredY) * 0.5f; + var midPressure = (state.LastSmoothPressure + pressure) * 0.5f; + strokeVisual.Add(new StylusPoint(midX, midY, midPressure)); + state.LastSmoothX = filteredX; + state.LastSmoothY = filteredY; + state.LastSmoothPressure = pressure; + } + + state.LastRawX = rawX; + state.LastRawY = rawY; + state.LastTimestampMs = nowMs; + return true; + } + /// /// 保存画布上的非笔画元素(如图片、媒体元素等) /// @@ -566,7 +705,9 @@ namespace Ink_Canvas } if (inkCanvas.EditingMode != InkCanvasEditingMode.EraseByStroke) { - inkCanvas.EditingMode = InkCanvasEditingMode.Ink; + inkCanvas.EditingMode = ShouldUseRealtimeVelocityBrushTip() + ? InkCanvasEditingMode.None + : InkCanvasEditingMode.Ink; } else { @@ -754,7 +895,11 @@ namespace Ink_Canvas strokeVisual.Add(new StylusPoint(stylusPoint.X, stylusPoint.Y, stylusPoint.PressureFactor)); } - strokeVisual.Redraw(); + if (isHandledByRealtime) + strokeVisual.ForceRedraw(); + else + strokeVisual.Redraw(); + } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } }