add:实时笔锋及墨迹预测

This commit is contained in:
2026-03-28 16:59:02 +08:00
parent fd137ae787
commit d325a58f17
10 changed files with 347 additions and 1 deletions
@@ -0,0 +1,187 @@
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
namespace Ink_Canvas
{
/// <summary>
/// 墨迹预测(书写中速度外推预览线)与笔锋相关输入状态,思路参考智绘教 Inkeys 的 RTS 速度与低延迟手感。
/// </summary>
public partial class MainWindow
{
private bool _inkPredictionStrokeActive;
private bool _inkPredictionHasSample;
private bool _inkPredictionHasVelocity;
private Point _inkPredictionLastPos;
private int _inkPredictionLastTime;
private double _inkPredictionVx;
private double _inkPredictionVy;
private void ResetInkPredictionState()
{
_inkPredictionStrokeActive = false;
_inkPredictionHasSample = false;
_inkPredictionHasVelocity = false;
ClearInkPredictionOverlay();
}
private void ClearInkPredictionOverlay()
{
try
{
if (InkPredictionPolyline == null) return;
InkPredictionPolyline.Visibility = Visibility.Collapsed;
InkPredictionPolyline.Points.Clear();
}
catch
{
// ignore
}
}
private void BeginInkPredictionStrokeIfNeeded()
{
try
{
if (Settings?.Canvas == null || !Settings.Canvas.EnableInkStrokePrediction)
{
_inkPredictionStrokeActive = false;
return;
}
_inkPredictionStrokeActive = inkCanvas != null
&& inkCanvas.EditingMode == InkCanvasEditingMode.Ink
&& penType != 1
&& !_isBoardBrushMode;
_inkPredictionHasSample = false;
_inkPredictionHasVelocity = false;
ClearInkPredictionOverlay();
}
catch
{
_inkPredictionStrokeActive = false;
}
}
private void EndInkPredictionStroke()
{
_inkPredictionStrokeActive = false;
_inkPredictionHasSample = false;
_inkPredictionHasVelocity = false;
ClearInkPredictionOverlay();
}
private void inkCanvas_PreviewStylusMove(object sender, StylusEventArgs e)
{
try
{
if (Settings?.Canvas == null || !Settings.Canvas.EnableInkStrokePrediction) return;
if (inkCanvas == null || InkPredictionPolyline == null) return;
if (!_inkPredictionStrokeActive || penType == 1) return;
if (inkCanvas.EditingMode != InkCanvasEditingMode.Ink) return;
if (e.InAir)
{
ClearInkPredictionOverlay();
return;
}
var pos = e.GetPosition(inkCanvas);
UpdateInkPredictionCore(pos, e.Timestamp);
}
catch
{
// ignore
}
}
private void inkCanvas_LostStylusCapture(object sender, StylusEventArgs e)
{
EndInkPredictionStroke();
}
private void inkCanvas_PreviewMouseMoveForPrediction(object sender, MouseEventArgs e)
{
try
{
if (Settings?.Canvas == null || !Settings.Canvas.EnableInkStrokePrediction) return;
if (inkCanvas == null || InkPredictionPolyline == null) return;
if (!_inkPredictionStrokeActive || penType == 1) return;
if (inkCanvas.EditingMode != InkCanvasEditingMode.Ink) return;
if (e.LeftButton != MouseButtonState.Pressed) return;
if (e.StylusDevice != null) return;
var pos = e.GetPosition(inkCanvas);
UpdateInkPredictionCore(pos, Environment.TickCount & int.MaxValue);
}
catch
{
// ignore
}
}
private void UpdateInkPredictionCore(Point pos, int timestamp)
{
if (InkPredictionPolyline == null || Settings?.Canvas == null) return;
if (!_inkPredictionHasSample)
{
_inkPredictionLastPos = pos;
_inkPredictionLastTime = timestamp;
_inkPredictionHasSample = true;
return;
}
double dtMs = timestamp - _inkPredictionLastTime;
if (dtMs <= 0 || dtMs > 120) dtMs = 16;
double vx = (pos.X - _inkPredictionLastPos.X) / dtMs * 1000.0;
double vy = (pos.Y - _inkPredictionLastPos.Y) / dtMs * 1000.0;
const double velocitySmooth = 0.62;
if (!_inkPredictionHasVelocity)
{
_inkPredictionVx = vx;
_inkPredictionVy = vy;
_inkPredictionHasVelocity = true;
}
else
{
_inkPredictionVx = velocitySmooth * _inkPredictionVx + (1.0 - velocitySmooth) * vx;
_inkPredictionVy = velocitySmooth * _inkPredictionVy + (1.0 - velocitySmooth) * vy;
}
const double leadMs = 24.0;
double predX = pos.X + _inkPredictionVx * (leadMs / 1000.0);
double predY = pos.Y + _inkPredictionVy * (leadMs / 1000.0);
double maxDist = Math.Max(4.0, Settings.Canvas.InkStrokePredictionMaxDistance);
double dx = predX - pos.X;
double dy = predY - pos.Y;
double len = Math.Sqrt(dx * dx + dy * dy);
if (len > maxDist && len > 1e-6)
{
double s = maxDist / len;
predX = pos.X + dx * s;
predY = pos.Y + dy * s;
}
_inkPredictionLastPos = pos;
_inkPredictionLastTime = timestamp;
var da = inkCanvas.DefaultDrawingAttributes;
var c = da.Color;
InkPredictionPolyline.Stroke = new SolidColorBrush(Color.FromArgb(110, c.R, c.G, c.B));
InkPredictionPolyline.StrokeThickness = Math.Max(1.0, da.Width * 0.42);
InkPredictionPolyline.Points.Clear();
InkPredictionPolyline.Points.Add(pos);
InkPredictionPolyline.Points.Add(new Point(predX, predY));
InkPredictionPolyline.Visibility = Visibility.Visible;
}
}
}
+18
View File
@@ -2700,6 +2700,24 @@ namespace Ink_Canvas
SaveSettingsToFile();
}
private void ToggleSwitchEnableInkStrokePrediction_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
Settings.Canvas.EnableInkStrokePrediction = ToggleSwitchEnableInkStrokePrediction.IsOn;
if (!Settings.Canvas.EnableInkStrokePrediction)
EndInkPredictionStroke();
SaveSettingsToFile();
}
private void ToggleSwitchEnableVelocityBrushTip_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
Settings.Canvas.EnableVelocityBrushTip = ToggleSwitchEnableVelocityBrushTip.IsOn;
SaveSettingsToFile();
}
private void ToggleSwitchAutoStraightenLine_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
@@ -941,6 +941,11 @@ namespace Ink_Canvas
// 初始化直线端点吸附相关设置
ToggleSwitchLineEndpointSnapping.IsOn = Settings.Canvas.LineEndpointSnapping;
ToggleSwitchCompressPicturesUploaded.IsOn = Settings.Canvas.IsCompressPicturesUploaded;
if (ToggleSwitchEnableInkStrokePrediction != null)
ToggleSwitchEnableInkStrokePrediction.IsOn = Settings.Canvas.EnableInkStrokePrediction;
if (ToggleSwitchEnableVelocityBrushTip != null)
ToggleSwitchEnableVelocityBrushTip.IsOn = Settings.Canvas.EnableVelocityBrushTip;
}
else
{
@@ -2565,6 +2565,7 @@ namespace Ink_Canvas
/// </remarks>
private void inkCanvas_MouseUp(object sender, MouseButtonEventArgs e)
{
EndInkPredictionStroke();
HandleEraserOperationEnded(); // 橡皮擦自动切换回批注模式:松手后启动/重置计时
inkCanvas.ReleaseMouseCapture();
ViewboxFloatingBar.IsHitTestVisible = true;
@@ -204,6 +204,7 @@ namespace Ink_Canvas
try
{
inkCanvas.Opacity = 1;
var touchPressureSimulationApplied = false;
if (Settings.Canvas.DisablePressure)
{
@@ -256,6 +257,7 @@ namespace Ink_Canvas
stylusPoints.Add(point);
}
touchPressureSimulationApplied = true;
e.Stroke.StylusPoints = stylusPoints;
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
@@ -304,6 +306,7 @@ namespace Ink_Canvas
}
}
touchPressureSimulationApplied = true;
e.Stroke.StylusPoints = stylusPoints;
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
@@ -312,6 +315,18 @@ namespace Ink_Canvas
}
}
if (Settings.Canvas.EnableVelocityBrushTip
&& !touchPressureSimulationApplied
&& !Settings.Canvas.DisablePressure
&& penType != 1
&& e.Stroke?.DrawingAttributes != null
&& !e.Stroke.DrawingAttributes.IsHighlighter
&& !e.Stroke.DrawingAttributes.IgnorePressure
&& e.Stroke.StylusPoints.Count >= 3)
{
ApplyVelocityBrushTipFromSpeed(e.Stroke);
}
// Apply line straightening and endpoint snapping if ink-to-shape is enabled
if (Settings.InkToShape.IsInkToShapeEnabled)
@@ -2033,6 +2048,56 @@ namespace Ink_Canvas
/ 20;
}
/// <summary>
/// 将沿线速度映射为压感并与硬件压感混合,快写略细、慢写略粗;与 Inkeys 中 RTSSpeed 驱动的笔锋类似,在落笔后统一施加。
/// </summary>
private void ApplyVelocityBrushTipFromSpeed(Stroke stroke)
{
try
{
var mix = Settings.Canvas.VelocityBrushTipMix;
if (mix <= 0 || stroke == null) return;
if (mix > 1) mix = 1;
var pts = stroke.StylusPoints;
if (pts.Count < 3) return;
var n = pts.Count - 1;
var stylusPoints = new StylusPointCollection();
for (var i = 0; i <= n; i++)
{
var speed = GetPointSpeed(
pts[Math.Max(i - 1, 0)].ToPoint(),
pts[i].ToPoint(),
pts[Math.Min(i + 1, n)].ToPoint());
float speedPressure;
if (speed >= 0.25)
speedPressure = (float)(0.5 - 0.3 * (Math.Min(speed, 1.5) - 0.3) / 1.2);
else if (speed >= 0.05)
speedPressure = 0.5f;
else
speedPressure = (float)(0.5 + 0.4 * (0.05 - speed) / 0.05);
speedPressure = (float)Math.Max(0.08, Math.Min(1.0, speedPressure));
var basePf = pts[i].PressureFactor;
var blended = (float)((1.0 - mix) * basePf + mix * speedPressure);
blended = (float)Math.Max(0.08, Math.Min(1.0, blended));
var p = new StylusPoint(pts[i].X, pts[i].Y, blended);
stylusPoints.Add(p);
}
stroke.StylusPoints = stylusPoints;
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
}
public Point[] FixPointsDirection(Point p1, Point p2)
{
double deltaY = Math.Abs(p1.Y - p2.Y);