add:实时笔锋及墨迹预测

This commit is contained in:
2026-03-28 17:04:50 +08:00
parent d325a58f17
commit de3f5d16a2
8 changed files with 161 additions and 55 deletions
+54 -18
View File
@@ -836,22 +836,6 @@
Toggled="ToggleSwitchDisablePressure_Toggled" />
</ikw:SimpleStackPanel>
<TextBlock Text="{i18n:I18n Key=Canvas_DisablePressureHint}" TextWrapping="Wrap" Foreground="#a1a1aa" />
<ikw:SimpleStackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<TextBlock Foreground="#fafafa" Text="{i18n:I18n Key=Canvas_InkStrokePrediction}" VerticalAlignment="Center"
FontSize="14" Margin="0,0,16,0" />
<ui:ToggleSwitch OnContent="" OffContent="" Name="ToggleSwitchEnableInkStrokePrediction"
IsOn="True" FontFamily="Microsoft YaHei UI" FontWeight="Bold"
Toggled="ToggleSwitchEnableInkStrokePrediction_Toggled" />
</ikw:SimpleStackPanel>
<TextBlock Text="{i18n:I18n Key=Canvas_InkStrokePredictionHint}" TextWrapping="Wrap" Foreground="#a1a1aa" />
<ikw:SimpleStackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<TextBlock Foreground="#fafafa" Text="{i18n:I18n Key=Canvas_VelocityBrushTip}" VerticalAlignment="Center"
FontSize="14" Margin="0,0,16,0" />
<ui:ToggleSwitch OnContent="" OffContent="" Name="ToggleSwitchEnableVelocityBrushTip"
IsOn="True" FontFamily="Microsoft YaHei UI" FontWeight="Bold"
Toggled="ToggleSwitchEnableVelocityBrushTip_Toggled" />
</ikw:SimpleStackPanel>
<TextBlock Text="{i18n:I18n Key=Canvas_VelocityBrushTipHint}" TextWrapping="Wrap" Foreground="#a1a1aa" />
<ikw:SimpleStackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<TextBlock Foreground="#fafafa" Text="{i18n:I18n Key=Canvas_EraserSize}" VerticalAlignment="Center"
@@ -5641,6 +5625,8 @@
FontFamily="Microsoft YaHei UI" />
<ComboBoxItem Content="关闭笔锋"
FontFamily="Microsoft YaHei UI" />
<ComboBoxItem Content="实时"
FontFamily="Microsoft YaHei UI" />
</ComboBox>
</ikw:SimpleStackPanel>
<Controls:UniformGrid Columns="3" Width="300"
@@ -5688,7 +5674,31 @@
IsOn="False" />
</ikw:SimpleStackPanel>
</Controls:UniformGrid>
<ikw:SimpleStackPanel Orientation="Horizontal"
Margin="0,10,0,0" VerticalAlignment="Center">
<Label Content="墨迹预测" FontSize="12"
VerticalAlignment="Center"
Foreground="{DynamicResource FloatBarForeground}"
Margin="0,0,10,0" />
<ui:ToggleSwitch
x:Name="BoardToggleSwitchInkStrokePredictionPanel"
MinWidth="0" Width="80"
FontFamily="Microsoft YaHei UI"
Toggled="ToggleSwitchInkStrokePredictionPanel_Toggled" />
<ComboBox x:Name="BoardComboBoxInkStrokePredictionLead"
Width="130" Height="30" Margin="12,0,0,0"
FontFamily="Microsoft YaHei UI"
VerticalAlignment="Center"
Visibility="Collapsed"
SelectionChanged="ComboBoxInkStrokePredictionLead_SelectionChanged">
<ComboBoxItem Content="自动"
FontFamily="Microsoft YaHei UI" />
<ComboBoxItem Content="25ms"
FontFamily="Microsoft YaHei UI" />
<ComboBoxItem Content="50ms"
FontFamily="Microsoft YaHei UI" />
</ComboBox>
</ikw:SimpleStackPanel>
<StackPanel Orientation="Horizontal" Height="30">
<Label Margin="0,0,10,0" Content="粗细"
@@ -8689,6 +8699,8 @@
FontFamily="Microsoft YaHei UI" />
<ComboBoxItem Content="关闭笔锋"
FontFamily="Microsoft YaHei UI" />
<ComboBoxItem Content="实时"
FontFamily="Microsoft YaHei UI" />
</ComboBox>
</ikw:SimpleStackPanel>
<Controls:UniformGrid Columns="3" Width="300" Height="55">
@@ -8731,7 +8743,31 @@
IsOn="{Binding ElementName=ToggleSwitchInkFadeInPanel, Path=IsOn}" />
</ikw:SimpleStackPanel>
</Controls:UniformGrid>
<ikw:SimpleStackPanel Orientation="Horizontal"
Margin="0,10,0,0" VerticalAlignment="Center">
<Label Content="墨迹预测" FontSize="12"
VerticalAlignment="Center"
Foreground="{DynamicResource FloatBarForeground}"
Margin="0,0,10,0" />
<ui:ToggleSwitch
x:Name="ToggleSwitchInkStrokePredictionPanel"
MinWidth="0" Width="80"
FontFamily="Microsoft YaHei UI"
Toggled="ToggleSwitchInkStrokePredictionPanel_Toggled" />
<ComboBox x:Name="ComboBoxInkStrokePredictionLead"
Width="130" Height="30" Margin="12,0,0,0"
FontFamily="Microsoft YaHei UI"
VerticalAlignment="Center"
Visibility="Collapsed"
SelectionChanged="ComboBoxInkStrokePredictionLead_SelectionChanged">
<ComboBoxItem Content="自动"
FontFamily="Microsoft YaHei UI" />
<ComboBoxItem Content="25ms"
FontFamily="Microsoft YaHei UI" />
<ComboBoxItem Content="50ms"
FontFamily="Microsoft YaHei UI" />
</ComboBox>
</ikw:SimpleStackPanel>
<StackPanel Orientation="Horizontal" Height="30">
<Label Margin="0,0,10,0" Content="粗细" FontWeight="Bold"
+41 -9
View File
@@ -4,12 +4,11 @@ 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
{
@@ -21,12 +20,21 @@ namespace Ink_Canvas
private double _inkPredictionVx;
private double _inkPredictionVy;
private void ResetInkPredictionState()
private void SyncInkStrokePredictionLeadComboVisibility()
{
_inkPredictionStrokeActive = false;
_inkPredictionHasSample = false;
_inkPredictionHasVelocity = false;
ClearInkPredictionOverlay();
try
{
bool on = Settings?.Canvas != null && Settings.Canvas.EnableInkStrokePrediction;
var v = on ? Visibility.Visible : Visibility.Collapsed;
if (ComboBoxInkStrokePredictionLead != null)
ComboBoxInkStrokePredictionLead.Visibility = v;
if (BoardComboBoxInkStrokePredictionLead != null)
BoardComboBoxInkStrokePredictionLead.Visibility = v;
}
catch
{
// ignore
}
}
private void ClearInkPredictionOverlay()
@@ -75,6 +83,30 @@ namespace Ink_Canvas
ClearInkPredictionOverlay();
}
private double GetInkPredictionLeadMs()
{
int mode = Settings?.Canvas?.InkStrokePredictionLeadMode ?? 0;
if (mode == 1) return 25.0;
if (mode == 2) return 50.0;
double speed = Math.Sqrt(_inkPredictionVx * _inkPredictionVx + _inkPredictionVy * _inkPredictionVy);
double norm = Math.Min(1.0, speed / 2600.0);
double lead = 16.0 + norm * 34.0;
return Math.Max(14.0, Math.Min(52.0, lead));
}
private double GetInkPredictionMaxDistance(double leadMs)
{
double baseD = Math.Max(4.0, Settings?.Canvas?.InkStrokePredictionMaxDistance ?? 18.0);
int mode = Settings?.Canvas?.InkStrokePredictionLeadMode ?? 0;
if (mode != 0)
return Math.Max(6.0, Math.Min(42.0, baseD * (leadMs / 24.0)));
double speed = Math.Sqrt(_inkPredictionVx * _inkPredictionVx + _inkPredictionVy * _inkPredictionVy);
double norm = Math.Min(1.0, speed / 2200.0);
return Math.Max(6.0, Math.Min(48.0, baseD + norm * baseD * 0.9));
}
private void inkCanvas_PreviewStylusMove(object sender, StylusEventArgs e)
{
try
@@ -155,11 +187,11 @@ namespace Ink_Canvas
_inkPredictionVy = velocitySmooth * _inkPredictionVy + (1.0 - velocitySmooth) * vy;
}
const double leadMs = 24.0;
double leadMs = GetInkPredictionLeadMs();
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 maxDist = GetInkPredictionMaxDistance(leadMs);
double dx = predX - pos.X;
double dy = predY - pos.Y;
double len = Math.Sqrt(dx * dx + dy * dy);
+24 -5
View File
@@ -2700,21 +2700,40 @@ namespace Ink_Canvas
SaveSettingsToFile();
}
private void ToggleSwitchEnableInkStrokePrediction_Toggled(object sender, RoutedEventArgs e)
private void ToggleSwitchInkStrokePredictionPanel_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
Settings.Canvas.EnableInkStrokePrediction = ToggleSwitchEnableInkStrokePrediction.IsOn;
if (!Settings.Canvas.EnableInkStrokePrediction)
bool on = ReferenceEquals(sender, BoardToggleSwitchInkStrokePredictionPanel)
? BoardToggleSwitchInkStrokePredictionPanel.IsOn
: ToggleSwitchInkStrokePredictionPanel.IsOn;
if (ReferenceEquals(sender, ToggleSwitchInkStrokePredictionPanel) && BoardToggleSwitchInkStrokePredictionPanel != null)
BoardToggleSwitchInkStrokePredictionPanel.IsOn = on;
else if (ReferenceEquals(sender, BoardToggleSwitchInkStrokePredictionPanel) && ToggleSwitchInkStrokePredictionPanel != null)
ToggleSwitchInkStrokePredictionPanel.IsOn = on;
Settings.Canvas.EnableInkStrokePrediction = on;
SyncInkStrokePredictionLeadComboVisibility();
if (!on)
EndInkPredictionStroke();
SaveSettingsToFile();
}
private void ToggleSwitchEnableVelocityBrushTip_Toggled(object sender, RoutedEventArgs e)
private void ComboBoxInkStrokePredictionLead_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (!isLoaded) return;
var cb = sender as ComboBox;
if (cb?.SelectedIndex < 0) return;
int idx = cb.SelectedIndex;
Settings.Canvas.InkStrokePredictionLeadMode = idx;
if (ReferenceEquals(cb, ComboBoxInkStrokePredictionLead) && BoardComboBoxInkStrokePredictionLead != null)
BoardComboBoxInkStrokePredictionLead.SelectedIndex = idx;
else if (ReferenceEquals(cb, BoardComboBoxInkStrokePredictionLead) && ComboBoxInkStrokePredictionLead != null)
ComboBoxInkStrokePredictionLead.SelectedIndex = idx;
Settings.Canvas.EnableVelocityBrushTip = ToggleSwitchEnableVelocityBrushTip.IsOn;
SaveSettingsToFile();
}
+22 -4
View File
@@ -824,6 +824,15 @@ namespace Ink_Canvas
ToggleSwitchDisablePressure.IsOn = Settings.Canvas.DisablePressure;
inkCanvas.DefaultDrawingAttributes.IgnorePressure = Settings.Canvas.DisablePressure;
if (Settings.Canvas.EnableVelocityBrushTip)
{
Settings.Canvas.InkStyle = 3;
Settings.Canvas.EnableVelocityBrushTip = false;
}
if (Settings.Canvas.InkStyle < 0 || Settings.Canvas.InkStyle > 3)
Settings.Canvas.InkStyle = 0;
ComboBoxPenStyle.SelectedIndex = Settings.Canvas.InkStyle;
BoardComboBoxPenStyle.SelectedIndex = Settings.Canvas.InkStyle;
@@ -942,10 +951,19 @@ 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;
int leadMode = Settings.Canvas.InkStrokePredictionLeadMode;
if (leadMode < 0 || leadMode > 2) leadMode = 0;
Settings.Canvas.InkStrokePredictionLeadMode = leadMode;
if (ToggleSwitchInkStrokePredictionPanel != null)
ToggleSwitchInkStrokePredictionPanel.IsOn = Settings.Canvas.EnableInkStrokePrediction;
if (BoardToggleSwitchInkStrokePredictionPanel != null)
BoardToggleSwitchInkStrokePredictionPanel.IsOn = Settings.Canvas.EnableInkStrokePrediction;
if (ComboBoxInkStrokePredictionLead != null)
ComboBoxInkStrokePredictionLead.SelectedIndex = leadMode;
if (BoardComboBoxInkStrokePredictionLead != null)
BoardComboBoxInkStrokePredictionLead.SelectedIndex = leadMode;
SyncInkStrokePredictionLeadComboVisibility();
}
else
{
@@ -315,7 +315,7 @@ namespace Ink_Canvas
}
}
if (Settings.Canvas.EnableVelocityBrushTip
if (Settings.Canvas.InkStyle == 3
&& !touchPressureSimulationApplied
&& !Settings.Canvas.DisablePressure
&& penType != 1
@@ -825,6 +825,8 @@ namespace Ink_Canvas
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
break;
case 3:
break;
}
}
-4
View File
@@ -374,10 +374,6 @@
<data name="Canvas_EnablePressureTouchHint" xml:space="preserve"><value># When on, touch screens that support pressure will show pressure; for devices not recognized by the system.</value></data>
<data name="Canvas_DisablePressure" xml:space="preserve"><value>Ignore pressure</value></data>
<data name="Canvas_DisablePressureHint" xml:space="preserve"><value># When on, all strokes use uniform thickness; mutually exclusive with pressure-sensitive touch.</value></data>
<data name="Canvas_InkStrokePrediction" xml:space="preserve"><value>Ink stroke prediction (latency hint)</value></data>
<data name="Canvas_InkStrokePredictionHint" xml:space="preserve"><value># While inking, draws a short semi-transparent segment ahead along motion to reduce perceived latency (similar idea to Inkeys).</value></data>
<data name="Canvas_VelocityBrushTip" xml:space="preserve"><value>Velocity brush tip (pressure blend)</value></data>
<data name="Canvas_VelocityBrushTipHint" xml:space="preserve"><value># For pen pressure, blends speed with hardware pressure: faster strokes thinner, slower thicker. Touch simulated pressure is unchanged.</value></data>
<data name="Canvas_EraserSize" xml:space="preserve"><value>Eraser size</value></data>
<data name="Canvas_EraserSize_VerySmall" xml:space="preserve"><value>Very small</value></data>
<data name="Canvas_EraserSize_Small" xml:space="preserve"><value>Small</value></data>
-4
View File
@@ -389,10 +389,6 @@
<data name="Canvas_EnablePressureTouchHint" xml:space="preserve"><value># 开启后,触屏设备也将支持压感效果,适用于部分支持压感但无法被系统识别的触屏设备。</value></data>
<data name="Canvas_DisablePressure" xml:space="preserve"><value>屏蔽压感</value></data>
<data name="Canvas_DisablePressureHint" xml:space="preserve"><value># 开启后,将忽略所有设备的压感信息,使所有笔画具有统一的粗细。与压感触屏模式互斥。</value></data>
<data name="Canvas_InkStrokePrediction" xml:space="preserve"><value>墨迹预测(低延迟预览线)</value></data>
<data name="Canvas_InkStrokePredictionHint" xml:space="preserve"><value># 书写时沿运动方向外推一小段半透明预览线,减轻显示与采样延迟;思路类似智绘教 Inkeys 的流畅笔迹。</value></data>
<data name="Canvas_VelocityBrushTip" xml:space="preserve"><value>速度笔锋(压感混合)</value></data>
<data name="Canvas_VelocityBrushTipHint" xml:space="preserve"><value># 对触控笔等真实压感,按运笔速度与硬件压感混合:快画略细、慢画略粗;触屏模拟压感路径仍单独处理,避免重复叠加。</value></data>
<data name="Canvas_EraserSize" xml:space="preserve"><value>橡皮大小</value></data>
<data name="Canvas_EraserSize_VerySmall" xml:space="preserve"><value>很小</value></data>
<data name="Canvas_EraserSize_Small" xml:space="preserve"><value>较小</value></data>
+17 -10
View File
@@ -69,6 +69,7 @@ namespace Ink_Canvas
public double InkAlpha { get; set; } = 255;
[JsonProperty("isShowCursor")]
public bool IsShowCursor { get; set; }
/// <summary>笔锋:0 基于点集,1 基于速率,2 关闭,3 实时(速度与压感混合)。</summary>
[JsonProperty("inkStyle")]
public int InkStyle { get; set; }
[JsonProperty("eraserSize")]
@@ -147,29 +148,35 @@ namespace Ink_Canvas
public int EraserAutoSwitchBackDelaySeconds { get; set; } = 10; // 默认10秒
/// <summary>
/// 书写时根据速度外推一小段预览线,补偿显示/采样延迟(类似智绘教 Inkeys 的低延迟手感)。
/// 是否在笔工具中启用墨迹预测预览线(由笔属性面板「墨迹预测」开关控制)。
/// </summary>
[JsonProperty("enableInkStrokePrediction")]
public bool EnableInkStrokePrediction { get; set; } = true;
public bool EnableInkStrokePrediction { get; set; } = false;
/// <summary>
/// 预测线段最大长度(与设备无关的逻辑像素/DIP),过大易飘,过小不明显
/// 墨迹预测提前量模式:0 自动(随书写速度调整),1 固定 25ms,2 固定 50ms
/// </summary>
[JsonProperty("inkStrokePredictionLeadMode")]
public int InkStrokePredictionLeadMode { get; set; } = 0;
/// <summary>
/// 预测线段基准最大长度(逻辑像素/DIP);「自动」模式下会随速度在此值基础上略增。
/// </summary>
[JsonProperty("inkStrokePredictionMaxDistance")]
public double InkStrokePredictionMaxDistance { get; set; } = 18.0;
/// <summary>
/// 用笔等真实压感设备时,将速度与硬件压感按 <see cref="VelocityBrushTipMix"/> 混合,使快画偏细、慢画偏粗(参考 Inkeys RTSSpeed 思路)
/// </summary>
[JsonProperty("enableVelocityBrushTip")]
public bool EnableVelocityBrushTip { get; set; } = true;
/// <summary>
/// 速度笔锋混合比例 0–1,越大速度对粗细影响越明显。
/// 「实时」笔锋(<see cref="InkStyle"/> = 3)下,速度项与硬件压感的混合比例 0–1
/// </summary>
[JsonProperty("velocityBrushTipMix")]
public double VelocityBrushTipMix { get; set; } = 0.22;
/// <summary>
/// 已弃用:请使用 <see cref="InkStyle"/> = 3(笔锋下拉选「实时」)。仅用于反序列化旧版配置。
/// </summary>
[JsonProperty("enableVelocityBrushTip")]
public bool EnableVelocityBrushTip { get; set; }
}
public enum OptionalOperation