improve:实时笔锋

This commit is contained in:
2026-04-30 15:14:17 +08:00
parent c68596b91e
commit 6e68fa9cfc
5 changed files with 15 additions and 230 deletions
-2
View File
@@ -4,7 +4,6 @@ using Ink_Canvas.Windows.SettingsViews.Helpers;
using iNKORE.UI.WPF.Modern;
using iNKORE.UI.WPF.Modern.Controls;
using Microsoft.Win32;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -1222,7 +1221,6 @@ namespace Ink_Canvas
}), DispatcherPriority.ApplicationIdle);
isLoaded = true;
EnsureRealtimeStylusPipelineBinding();
BlackBoardLeftSidePageListView.ItemsSource = blackBoardSidePageListViewObservableCollection;
BlackBoardRightSidePageListView.ItemsSource = blackBoardSidePageListViewObservableCollection;
@@ -2245,8 +2245,7 @@ namespace Ink_Canvas
SetFloatingBarHighlightPosition("pen");
// 记录当前是否已经是批注模式且是否为高光显示模式
bool wasInInkMode = inkCanvas.EditingMode == InkCanvasEditingMode.Ink
|| (Pen_Icon.Background != null && StackPanelCanvasControls.Visibility == Visibility.Visible);
bool wasInInkMode = inkCanvas.EditingMode == InkCanvasEditingMode.Ink;
bool wasHighlighter = drawingAttributes.IsHighlighter;
if (drawingShapeMode != 0 && !isLongPressSelected)
@@ -2451,8 +2450,6 @@ namespace Ink_Canvas
forceEraser = false;
forcePointEraser = false;
drawingShapeMode = 0;
EnsureRealtimeStylusPipelineBinding();
}
/// <summary>
+1 -4
View File
@@ -1,4 +1,4 @@
using Ink_Canvas.Helpers;
using Ink_Canvas.Helpers;
using Ink_Canvas.Windows.SettingsViews.Helpers;
using System;
using System.Collections.Generic;
@@ -502,8 +502,6 @@ namespace Ink_Canvas
else
ComboBoxPenStyle.SelectedIndex = uiIndex;
EnsureRealtimeStylusPipelineBinding();
SaveSettingsToFile();
}
@@ -708,7 +706,6 @@ namespace Ink_Canvas
}
Settings.Gesture.IsEnableMultiTouchMode = ToggleSwitchEnableMultiTouchMode.IsOn;
EnsureRealtimeStylusPipelineBinding();
// 如果启用多指书写模式,强制禁用所有双指手势
if (ToggleSwitchEnableMultiTouchMode.IsOn)
+1 -63
View File
@@ -2488,7 +2488,6 @@ namespace Ink_Canvas
/// 用于标识鼠标是否处于按下状态,在绘制过程中使用
/// </remarks>
private bool isMouseDown;
private bool _isMouseRealtimeInking;
/// <summary>
/// 触摸按下状态标志
@@ -2512,17 +2511,6 @@ namespace Ink_Canvas
/// </remarks>
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;
@@ -2543,24 +2531,7 @@ namespace Ink_Canvas
/// </remarks>
private void inkCanvas_MouseMove(object sender, MouseEventArgs e)
{
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 (isMouseDown) MouseTouchMove(e.GetPosition(inkCanvas));
if (Settings.Canvas.IsShowCursor)
{
@@ -2588,39 +2559,6 @@ namespace Ink_Canvas
/// </remarks>
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;
+12 -157
View File
@@ -50,7 +50,7 @@ namespace Ink_Canvas
private InkCanvasEditingMode palmEraserPreviousEditingMode = InkCanvasEditingMode.Ink;
private readonly Dictionary<int, RealtimeBrushTipState> _realtimeBrushTipStates = new Dictionary<int, RealtimeBrushTipState>();
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,7 +102,6 @@ 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; }
@@ -121,44 +120,12 @@ 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;
}
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;
}
&& !Settings.Canvas.DisablePressure
&& penType == 0;
}
private void InitializeRealtimeBrushTipState(int stylusId, StylusDownEventArgs e)
@@ -178,22 +145,6 @@ 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);
@@ -213,8 +164,6 @@ 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)
{
@@ -234,24 +183,15 @@ namespace Ink_Canvas
var filteredX = state.FilterX.Filter(rawX, dt, speed);
var filteredY = state.FilterY.Filter(rawY, dt, speed);
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;
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;
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;
@@ -273,6 +213,7 @@ 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;
@@ -300,86 +241,6 @@ 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;
}
/// <summary>
/// 保存画布上的非笔画元素(如图片、媒体元素等)
/// </summary>
@@ -705,9 +566,7 @@ namespace Ink_Canvas
}
if (inkCanvas.EditingMode != InkCanvasEditingMode.EraseByStroke)
{
inkCanvas.EditingMode = ShouldUseRealtimeVelocityBrushTip()
? InkCanvasEditingMode.None
: InkCanvasEditingMode.Ink;
inkCanvas.EditingMode = InkCanvasEditingMode.Ink;
}
else
{
@@ -895,11 +754,7 @@ namespace Ink_Canvas
strokeVisual.Add(new StylusPoint(stylusPoint.X, stylusPoint.Y, stylusPoint.PressureFactor));
}
if (isHandledByRealtime)
strokeVisual.ForceRedraw();
else
strokeVisual.Redraw();
strokeVisual.Redraw();
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
}