本文將告訴大家一些筆跡算法,從用戶輸入的點(diǎn)集,即鼠標(biāo)軌跡點(diǎn)或觸摸軌跡點(diǎn)等,轉(zhuǎn)換為一個(gè)可在界面繪制顯示筆跡畫面的基礎(chǔ)數(shù)學(xué)算法。盡管本文標(biāo)記的是 WPF 的筆跡算法,然而實(shí)際上本文更側(cè)重基礎(chǔ)數(shù)學(xué)計(jì)算,理論上可以適用于任何能夠支持幾何繪制的 UI 框架上,包括 UWP 或 WinUI 或 UNO 或 MAUI 或 Eto 等框架
我將從簡(jiǎn)單到復(fù)雜的順序描述筆跡算法,本文屬于比較偏算法底層,閱讀之前請(qǐng)先確保初中的數(shù)學(xué)知識(shí)還沒忘了
本文適合于想要了解筆跡繪制更多細(xì)節(jié)的伙伴,以及期望自己設(shè)計(jì)出更好看的筆跡的伙伴,以及沒事干摸魚看博客的伙伴
最簡(jiǎn)單的筆跡軌跡算法
大家都知道,無論是鼠標(biāo)還是觸摸還是筆,所產(chǎn)生的數(shù)據(jù)基本都是點(diǎn)數(shù)據(jù)。根據(jù)點(diǎn)集創(chuàng)建一條筆跡軌跡的一個(gè)實(shí)現(xiàn)方式是創(chuàng)建一條幾何圖形,將幾何圖形繪制到界面上。在 UI 框架的底層里,是不存在筆跡的概念的,只有畫圖、畫文本、畫幾何圖形等基礎(chǔ)繪制原語而已。從點(diǎn)集構(gòu)建出一條幾何軌跡最簡(jiǎn)單的方法是構(gòu)建一條折線,代碼也非常簡(jiǎn)單,只是將所有的輸入點(diǎn)當(dāng)成折線即可
也就是創(chuàng)建一個(gè) Polyline 對(duì)象,不斷將輸出的點(diǎn)集加入到折線里面。以下是例子代碼,先新建一個(gè)空 WPF 項(xiàng)目,在 MainWindow.xaml 里添加事件監(jiān)聽,如以下代碼
<Window x:Class="YegeenurcairwheBeahealelbewe.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:YegeenurcairwheBeahealelbewe"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800" StylusDown="MainWindow_OnStylusDown" StylusMove="MainWindow_OnStylusMove" StylusUp="MainWindow_OnStylusUp">
<Canvas x:Name="InkCanvas">
</Canvas>
</Window>
在后臺(tái)代碼里面,實(shí)現(xiàn)事件,以下的代碼很簡(jiǎn)單,相信大家一看就明白
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void MainWindow_OnStylusDown(object sender, StylusDownEventArgs e)
{
var polyline = new Polyline()
{
Stroke = Brushes.Black,
StrokeThickness = 5
};
InkCanvas.Children.Add(polyline);
_pointCache[e.StylusDevice.Id] = polyline;
foreach (var stylusPoint in e.GetStylusPoints(this))
{
polyline.Points.Add(stylusPoint.ToPoint());
}
}
private void MainWindow_OnStylusMove(object sender, StylusEventArgs e)
{
if (_pointCache.TryGetValue(e.StylusDevice.Id,out var polyline))
{
foreach (var stylusPoint in e.GetStylusPoints(this))
{
polyline.Points.Add(stylusPoint.ToPoint());
}
}
}
private void MainWindow_OnStylusUp(object sender, StylusEventArgs e)
{
if (_pointCache.Remove(e.StylusDevice.Id, out var polyline))
{
foreach (var stylusPoint in e.GetStylusPoints(this))
{
polyline.Points.Add(stylusPoint.ToPoint());
}
}
}
private readonly Dictionary<int/*StylusDeviceId*/, Polyline> _pointCache=new Dictionary<int, Polyline>();
}
以上的代碼放在github 和 gitee 歡迎訪問
可以通過如下方式獲取本文的源代碼,先創(chuàng)建一個(gè)空文件夾,接著使用命令行 cd 命令進(jìn)入此空文件夾,在命令行里面輸入以下代碼,即可獲取到本文的代碼
git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin d76fffd214ed5b3aeb99f3593c441b7a12f10d55
以上使用的是 gitee 的源,如果 gitee 不能訪問,請(qǐng)?zhí)鎿Q為 github 的源。請(qǐng)?jiān)诿钚欣^續(xù)輸入以下代碼
git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin d76fffd214ed5b3aeb99f3593c441b7a12f10d55
獲取代碼之后,進(jìn)入 HallgaiwhiyiwaLejucona\YegeenurcairwheBeahealelbewe 文件夾
盡管以上的代碼很簡(jiǎn)單,但是大家將會(huì)發(fā)現(xiàn)筆跡不夠順滑,至少比 WPF 最簡(jiǎn)邏輯實(shí)現(xiàn)多指順滑的筆跡書寫 調(diào)用 WPF 自帶的筆跡繪制的方法不順滑好多,而且繪制速度也差好多
先忘掉 WPF 的上層調(diào)用,假如現(xiàn)在咱想要自己編寫算法來畫一條比 WPF 不會(huì)差太多的筆跡軌跡,可以如何做呢。接下來我將繼續(xù)從簡(jiǎn)單到復(fù)雜的順序告訴大家不同的算法
用兩條折線繪制筆跡
上文使用折線的方式可以很簡(jiǎn)單繪制出筆跡,但是無法實(shí)現(xiàn)一條粗細(xì)變化的筆跡軌跡。筆跡的粗細(xì)變更一般來說和觸摸壓感相關(guān),換句話說,想要實(shí)現(xiàn)跟隨觸摸壓感變更而變更粗細(xì)的筆跡軌跡輪廓就需要用到至少比折線更加復(fù)雜的方式
接下來介紹的方式是用兩條線段繪制筆跡,可以將筆跡元素理解為一個(gè)由兩條折線構(gòu)成的閉合 Path 幾何形狀。如下圖所示,筆跡軌跡就是一個(gè) Path 幾何形狀的填充
這里如果看完還沒理解的話,推薦先暫停下來,先想一想。因?yàn)檫@里有點(diǎn)難描述哈
在這個(gè)的基礎(chǔ)上,咱的問題就轉(zhuǎn)換為根據(jù)輸入的點(diǎn)集轉(zhuǎn)換為 Path 幾何形狀
接下來我將介紹根據(jù)輸入的點(diǎn)集轉(zhuǎn)換為 Path 幾何形狀的最簡(jiǎn)單方法之一,期望以下的方法能夠給大家?guī)硪恍﹩⑹?。我將快速給出一些圖和文字描述給到大家,方便快速理解整體的思想。然后再給出具體的實(shí)現(xiàn)
下圖的藍(lán)色的點(diǎn)表示的是當(dāng)前所輸入收到的點(diǎn)集
接下來求每個(gè)點(diǎn)與下一個(gè)點(diǎn)相連的射線向量,再算出射線向量的法線方向,在此法線方向上以觸摸點(diǎn)的中心向法線兩端延伸線段,延伸的線段長(zhǎng)度由筆跡粗細(xì)配置以及當(dāng)前觸摸點(diǎn)的壓感系數(shù)決定,如下圖,藍(lán)色的線就是射線向量,黃色的線是射線向量的法線方向延伸的線段
再獲取線段的兩個(gè)端點(diǎn),如下圖,紅色的圓點(diǎn)就是延伸的線段的兩個(gè)端點(diǎn)
接著將各個(gè)線段的端點(diǎn)按照如下圖的方式連接起來,各個(gè)線段的兩個(gè)端點(diǎn)分別按照兩邊連接成兩條折線,再將這兩條折線和起始點(diǎn)和結(jié)束點(diǎn)連接到一起,構(gòu)成閉合的 Path 幾何形狀,紅色的折線就可以被當(dāng)成筆跡軌跡的 Path 幾何形狀
最后將紅色的折線組成的筆跡軌跡的 Path 幾何形狀填充,填充之后看起來的效果還行
相信大家看到這里就理解了用兩條折線繪制筆跡的方法
接下來我將告訴大家如何使用具體的代碼實(shí)現(xiàn)用兩條折線繪制筆跡
原本我是想繼續(xù)采用 WPF 項(xiàng)目完成此步驟的演示,但剛好我打開了一個(gè) UNO 框架的項(xiàng)目,于是我就使用 UNO 框架項(xiàng)目作為演示。這里需要說明的是 UNO 和 WPF 之間的關(guān)系不是重復(fù)的存在,而是相互引用的關(guān)系,如下圖可以看到 UNO 可以處于 WPF 的上層,換句話說就是使用 UNO 框架時(shí)可以將 WPF 當(dāng)成底層,從這個(gè)方面來說,最后構(gòu)建輸出的也依然是一個(gè) WPF 應(yīng)用
新建一個(gè) UNO 項(xiàng)目,在 MainPage.xaml 里面監(jiān)聽事件,制作一些準(zhǔn)備輔助筆跡繪制的界面邏輯,簡(jiǎn)單的代碼如下
<Canvas x:Name="InkCanvas" Background="Transparent" PointerPressed="InkCanvas_OnPointerPressed" PointerMoved="InkCanvas_OnPointerMoved" PointerReleased="InkCanvas_OnPointerReleased" PointerCanceled="InkCanvas_OnPointerCanceled"/>
在 MainPage.xaml.cs 后臺(tái)代碼里面,根據(jù)輸入事件的監(jiān)聽,獲取到當(dāng)前的輸入點(diǎn)集。這部分代碼預(yù)計(jì)大家一看就明白,我這里就快速跳過
private void InkCanvas_OnPointerPressed(object sender, PointerRoutedEventArgs e)
{
var pointerPoint = e.GetCurrentPoint(InkCanvas);
Point position = pointerPoint.Position;
var inkInfo = new InkInfo();
_inkInfoCache[e.Pointer.PointerId] = inkInfo;
inkInfo.PointList.Add(position);
DrawStroke(inkInfo);
}
private void InkCanvas_OnPointerMoved(object sender, PointerRoutedEventArgs e)
{
if (_inkInfoCache.TryGetValue(e.Pointer.PointerId, out var inkInfo))
{
var pointerPoint = e.GetCurrentPoint(InkCanvas);
Point position = pointerPoint.Position;
inkInfo.PointList.Add(position);
DrawStroke(inkInfo);
}
}
private void InkCanvas_OnPointerReleased(object sender, PointerRoutedEventArgs e)
{
if (_inkInfoCache.Remove(e.Pointer.PointerId, out var inkInfo))
{
var pointerPoint = e.GetCurrentPoint(InkCanvas);
Point position = pointerPoint.Position;
inkInfo.PointList.Add(position);
DrawStroke(inkInfo);
}
}
private void InkCanvas_OnPointerCanceled(object sender, PointerRoutedEventArgs e)
{
if (_inkInfoCache.Remove(e.Pointer.PointerId, out var inkInfo))
{
RemoveInkElement(inkInfo.InkElement);
}
}
private void RemoveInkElement(FrameworkElement? inkElement)
{
if (inkElement != null)
{
InkCanvas.Children.Remove(inkElement);
}
}
private readonly Dictionary<uint /*PointerId*/, InkInfo> _inkInfoCache = new Dictionary<uint, InkInfo>();
public class InkInfo
{
public FrameworkElement? InkElement { set; get; }
public List<StrokePoint> PointList { get; } = new List<StrokePoint>();
}
public readonly record struct StrokePoint(Point Point, float Pressure = 0.5f)
{
public static implicit operator StrokePoint(Point point) => new StrokePoint(point);
}
以上代碼沒給出的 DrawStroke 則是核心算法,在 InkInfo 里面存放了 PointList 點(diǎn)集。在 DrawStroke 需要根據(jù)此點(diǎn)集信息構(gòu)建出一個(gè) FrameworkElement 類型的對(duì)象,這個(gè)對(duì)象就是筆跡元素對(duì)象。按照本文以上的算法原理描述,這個(gè)筆跡對(duì)象就是在數(shù)學(xué)上由兩段折線組合而成的閉合 Path 幾何形狀。這里為了簡(jiǎn)單使用,就使用了內(nèi)建的 Microsoft.UI.Xaml.Shapes.Polygon
類型
使用 Polygon 類型時(shí),最重要的就是獲取按照預(yù)期順序的筆跡輪廓點(diǎn),也就是上文的各個(gè)線段的兩個(gè)端點(diǎn),也就是如下圖里黃色的點(diǎn)
為了計(jì)算筆跡輪廓點(diǎn)集,以下代碼封裝了 GetOutlinePointList 方法,這個(gè)方法需要傳入 InkInfo 的 PointList 點(diǎn)集,也就是輸入的點(diǎn)集,以及筆跡的大小
public static Point[] GetOutlinePointList(List<StrokePoint> pointList, int inkSize)
{
... // 忽略代碼
}
由于咱需要計(jì)算射線向量方向,這就意味著至少需要兩個(gè)點(diǎn)才能計(jì)算,于是先加上如下判斷邏輯
public static Point[] GetOutlinePointList(List<StrokePoint> pointList, int inkSize)
{
if (pointList.Count < 2)
{
throw new ArgumentException("小于兩個(gè)點(diǎn)的無法應(yīng)用算法");
}
... // 忽略代碼
}
如上文的算法,可以看到輸出的筆跡輪廓點(diǎn)集,也就是 GetOutlinePointList 的返回值,的元素個(gè)數(shù)將會(huì)是 pointList
點(diǎn)集的兩倍加二。為什么會(huì)是 pointList
點(diǎn)集的兩倍加二的值?因?yàn)槿缟衔牡乃惴ǎ總€(gè)原始輸入點(diǎn)都可以算出兩個(gè)端點(diǎn),再加上最后將首末兩個(gè)點(diǎn)一共就是兩倍加二的值
var pointCount = pointList.Count * 2 /*兩邊的筆跡軌跡*/ + 1 /*首點(diǎn)重復(fù)*/ + 1 /*末重復(fù)*/;
var outlinePointList = new Point[pointCount];
接著進(jìn)行輸入的原始點(diǎn)集的循環(huán),計(jì)算每個(gè)點(diǎn)的射線向量
for (var i = 0; i < pointList.Count; i++)
{
var currentPoint = pointList[i];
var nextPoint = pointList[i + 1]; // 先忽略最后一個(gè)點(diǎn)的錯(cuò)誤計(jì)算
var x = nextPoint.Point.X - currentPoint.Point.X;
var y = nextPoint.Point.Y - currentPoint.Point.Y;
// 拿著紙筆自己畫一下吧,這個(gè)是簡(jiǎn)單的數(shù)學(xué)計(jì)算
double angle = Math.Atan2(y, x) - Math.PI / 2;
}
以上代碼的 angle 就是向量角度,于是再計(jì)算端點(diǎn)距離輸入原始點(diǎn)的距離,即可算出端點(diǎn)坐標(biāo)
// 筆跡粗細(xì)的一半,一邊用一半,合起來就是筆跡粗細(xì)了
var halfThickness = inkSize / 2d;
// 壓感這里是直接乘法而已
halfThickness *= currentPoint.Pressure;
// 不能讓筆跡粗細(xì)太小
halfThickness = Math.Max(0.01, halfThickness);
var leftX = currentPoint.Point.X + (Math.Cos(angle) * halfThickness);
var leftY = currentPoint.Point.Y + (Math.Sin(angle) * halfThickness);
var rightX = currentPoint.Point.X - (Math.Cos(angle) * halfThickness);
var rightY = currentPoint.Point.Y - (Math.Sin(angle) * halfThickness);
outlinePointList[i + 1] = new Point(leftX, leftY);
outlinePointList[pointCount - i - 1] = new Point(rightX, rightY);
以上代碼只是簡(jiǎn)單的初中函數(shù)計(jì)算,相信大家一看就知道
以上的代碼實(shí)際上是不能運(yùn)行的,因?yàn)樽詈笠粋€(gè)點(diǎn)的計(jì)算還沒有加上。這里就簡(jiǎn)單將最后一個(gè)點(diǎn)的向量方向記錄為前一個(gè)點(diǎn)的方向,修改之后的代碼如下
double angle = 0.0;
for (var i = 0; i < pointList.Count; i++)
{
var currentPoint = pointList[i];
// 如果不是最后一點(diǎn),那就可以和筆跡當(dāng)前軌跡點(diǎn)的下一點(diǎn)進(jìn)行計(jì)算向量角度
if (i < pointList.Count - 1)
{
var nextPoint = pointList[i + 1];
var x = nextPoint.Point.X - currentPoint.Point.X;
var y = nextPoint.Point.Y - currentPoint.Point.Y;
// 拿著紙筆自己畫一下吧,這個(gè)是簡(jiǎn)單的數(shù)學(xué)計(jì)算
angle = Math.Atan2(y, x) - Math.PI / 2;
}
// 筆跡粗細(xì)的一半,一邊用一半,合起來就是筆跡粗細(xì)了
var halfThickness = inkSize / 2d;
// 壓感這里是直接乘法而已
halfThickness *= currentPoint.Pressure;
// 不能讓筆跡粗細(xì)太小
halfThickness = Math.Max(0.01, halfThickness);
var leftX = currentPoint.Point.X + (Math.Cos(angle) * halfThickness);
var leftY = currentPoint.Point.Y + (Math.Sin(angle) * halfThickness);
var rightX = currentPoint.Point.X - (Math.Cos(angle) * halfThickness);
var rightY = currentPoint.Point.Y - (Math.Sin(angle) * halfThickness);
outlinePointList[i + 1] = new Point(leftX, leftY);
outlinePointList[pointCount - i - 1] = new Point(rightX, rightY);
}
接著再加上首末兩個(gè)點(diǎn)就完成了方法
public static Point[] GetOutlinePointList(List<StrokePoint> pointList, int inkSize)
{
if (pointList.Count < 2)
{
throw new ArgumentException("小于兩個(gè)點(diǎn)的無法應(yīng)用算法");
}
var pointCount = pointList.Count * 2 /*兩邊的筆跡軌跡*/ + 1 /*首點(diǎn)重復(fù)*/ + 1 /*末重復(fù)*/;
var outlinePointList = new Point[pointCount];
// 用來計(jì)算筆跡點(diǎn)的兩點(diǎn)之間的向量角度
double angle = 0.0;
for (var i = 0; i < pointList.Count; i++)
{
var currentPoint = pointList[i];
// 如果不是最后一點(diǎn),那就可以和筆跡當(dāng)前軌跡點(diǎn)的下一點(diǎn)進(jìn)行計(jì)算向量角度
if (i < pointList.Count - 1)
{
var nextPoint = pointList[i + 1];
var x = nextPoint.Point.X - currentPoint.Point.X;
var y = nextPoint.Point.Y - currentPoint.Point.Y;
// 拿著紙筆自己畫一下吧,這個(gè)是簡(jiǎn)單的數(shù)學(xué)計(jì)算
angle = Math.Atan2(y, x) - Math.PI / 2;
}
// 筆跡粗細(xì)的一半,一邊用一半,合起來就是筆跡粗細(xì)了
var halfThickness = inkSize / 2d;
// 壓感這里是直接乘法而已
halfThickness *= currentPoint.Pressure;
// 不能讓筆跡粗細(xì)太小
halfThickness = Math.Max(0.01, halfThickness);
var leftX = currentPoint.Point.X + (Math.Cos(angle) * halfThickness);
var leftY = currentPoint.Point.Y + (Math.Sin(angle) * halfThickness);
var rightX = currentPoint.Point.X - (Math.Cos(angle) * halfThickness);
var rightY = currentPoint.Point.Y - (Math.Sin(angle) * halfThickness);
outlinePointList[i + 1] = new Point(leftX, leftY);
outlinePointList[pointCount - i - 1] = new Point(rightX, rightY);
}
outlinePointList[0] = pointList[0].Point;
outlinePointList[pointList.Count + 1] = pointList[^1].Point;
return outlinePointList;
}
在通過 GetOutlinePointList 拿到筆跡輪廓點(diǎn)之后,即可構(gòu)建出 Polygon 對(duì)象,如以下代碼
public static Polygon CreatePath(InkInfo inkInfo, int inkSize)
{
List<StrokePoint> pointList = inkInfo.PointList;
var outlinePointList = GetOutlinePointList(pointList, inkSize);
var polygon = new Polygon();
foreach (var point in outlinePointList)
{
polygon.Points.Add(point);
}
polygon.Fill = new SolidColorBrush(Colors.Red);
return polygon;
}
盡管以上代碼是在 UNO 框架下編寫的,但可以直接拷貝代碼在 UWP 應(yīng)用上直接運(yùn)行
拿到 Polygon 對(duì)象之后,將此對(duì)象加入到界面里面,如以下代碼,即可完成筆跡的繪制。在不斷落點(diǎn)輸入點(diǎn)數(shù)據(jù)過程中,將不斷執(zhí)行 Polygon 的 Points 的清理和重新添加,于是就可以不斷跟隨落點(diǎn)更新筆跡內(nèi)容,完成筆跡書寫的功能
private void DrawStroke(InkInfo inkInfo)
{
var pointList = inkInfo.PointList;
if (pointList.Count < 2)
{
// 小于兩個(gè)點(diǎn)的無法應(yīng)用算法
return;
}
var inkElement = MyInkRender.CreatePath(inkInfo, inkSize);
if (inkInfo.InkElement is null)
{
InkCanvas.Children.Add(inkElement);
}
inkInfo.InkElement = inkElement;
}
完成到這里,其實(shí)就算完成了一個(gè)簡(jiǎn)單的在繪制的過程,可根據(jù)壓感參數(shù)變更筆跡粗細(xì)的算法了
但是一般的輸入設(shè)備,比如鼠標(biāo)或者渣觸摸屏都是沒有壓感的,或者是沒有正確的壓感的,那這個(gè)時(shí)候似乎體現(xiàn)不出以上算法的優(yōu)勢(shì)。這時(shí)候可以繼續(xù)和大家介紹另一個(gè)有趣的功能實(shí)現(xiàn),模擬筆鋒
很多人都喜歡寫字的時(shí)候帶筆鋒,無論是寫中文還是寫英文的時(shí)候。模擬筆鋒也許可以讓用戶感謝寫出來的字更好看,通過壓感模擬筆鋒是一個(gè)非常簡(jiǎn)單的實(shí)現(xiàn)。實(shí)現(xiàn)思路就是從筆尖到筆身的順序,讓輸入的點(diǎn)集的壓感從小到大,大概如下圖所示,如此即可做出類似筆鋒的效果
大概的實(shí)現(xiàn)代碼如下
// 模擬筆鋒
// 用于當(dāng)成筆鋒的點(diǎn)的數(shù)量
var tipCount = 20;
for (int i = 0; i < pointList.Count; i++)
{
if ((pointList.Count - i) < tipCount)
{
pointList[i] = pointList[i] with
{
Pressure = (pointList.Count - i) * 1f / tipCount
};
}
else
{
pointList[i] = pointList[i] with
{
Pressure = 1.0f
};
}
}
加上模擬筆鋒之后,即可使用以上的算法畫出如下圖的筆跡效果
上圖是我開了調(diào)試模式的效果,調(diào)試模式就是在原筆跡元素的基礎(chǔ)上,繪制出藍(lán)色的原始輸入的點(diǎn)集,以及黃色的端點(diǎn)
以上的代碼放在github 和 gitee 歡迎訪問
可以通過如下方式獲取本文的源代碼,先創(chuàng)建一個(gè)空文件夾,接著使用命令行 cd 命令進(jìn)入此空文件夾,在命令行里面輸入以下代碼,即可獲取到本文的代碼
git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 8d59a96e0d4e390ae78946ff556a759901961856
以上使用的是 gitee 的源,如果 gitee 不能訪問,請(qǐng)?zhí)鎿Q為 github 的源。請(qǐng)?jiān)诿钚欣^續(xù)輸入以下代碼
git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 8d59a96e0d4e390ae78946ff556a759901961856
獲取代碼之后,進(jìn)入 HallgaiwhiyiwaLejucona 文件夾
歡迎大家將代碼拉下來,運(yùn)行看試試效果。以上代碼是寫在 UNO 框架里的,可以在 Windows 平臺(tái)上使用 WinUI 或 WPF 運(yùn)行,也可以在 Linux 系統(tǒng)使用 GTK 運(yùn)行
但大家也可以很輕松就看出來以上算法存在的不足還是有很多的,比如是采用折線連接筆跡輪廓的點(diǎn)集,這就導(dǎo)致了在觸摸采樣不夠密或鼠標(biāo)精度很低的情況下,畫出來的筆跡存在很明顯的折線效果,不夠順滑。另外,從以上的簡(jiǎn)單數(shù)學(xué)計(jì)算上,也存在著輸入軌跡大角度的折彎時(shí)存在計(jì)算錯(cuò)誤
接下來我將和大家介紹更加進(jìn)階的算法,解決以上簡(jiǎn)單算法所遇到的問題
順滑的筆跡算法
以上的用兩條折線繪制筆跡的算法被我稱為十字法筆跡算法,這是一個(gè)簡(jiǎn)單的算法,無法作出順滑的筆跡效果。接下來將和大家介紹被我命名為米字法筆跡算法的算法
接下來介紹的米字法筆跡算法是為觸摸設(shè)計(jì)的筆跡書寫軌跡算法,可以實(shí)現(xiàn)比較順滑的筆跡繪制效果,同時(shí)可以有多組參數(shù)可配置,配合高階擬合函數(shù)可以寫出特別多不同的筆跡效果,比如毛筆字、鋼筆字等
以下介紹的算法被我申請(qǐng)了專利保護(hù),現(xiàn)在專利已經(jīng)公開授權(quán),我就不放出來具體的代碼了。原本專利里面是有詳細(xì)公開信息的,但是專利本身寫得難以閱讀,為了讓大家能夠更清晰知道具體的筆跡實(shí)現(xiàn)算法,我就準(zhǔn)備使用更白話的方式向大家介紹算法內(nèi)容
必須提醒大家的是,如果在商業(yè)軟件上使用,必須繞過本文接下來介紹的方法,本文接下來介紹的方法只能借鑒不能抄哦。不然等你大賺時(shí),法務(wù)小姐姐會(huì)去找你麻煩的
當(dāng)然,非商業(yè)用途等不怕專利的情況,那就隨意咯
接下來介紹的方法按照順序分別是 CN109284059A 和 CN115373534A 兩篇專利里面包含的內(nèi)容,你可以認(rèn)為本文只是記錄讀了以上兩篇專利之后的自己所學(xué)到的內(nèi)容。我自己發(fā)布博客在我自己的非盈利非商業(yè)的博客上是可以的,屬于非營(yíng)利實(shí)施,但是如果有伙伴想要在商業(yè)用途上轉(zhuǎn)載本文,那就是侵犯專利的權(quán)利,違法的哦
本文以下介紹的算法部分只介紹大概思路,不會(huì)包含具體實(shí)現(xiàn)細(xì)節(jié)以及代碼,更詳細(xì)的實(shí)施方法還請(qǐng)自行參閱專利的內(nèi)容
本文以下的算法將默認(rèn)是為觸摸設(shè)計(jì)的筆跡書寫軌跡算法,輸入的原始點(diǎn)被稱為原始觸摸點(diǎn)。觸摸點(diǎn)數(shù)據(jù)將包含 X Y 信息,以及可選的壓感和寬度高度信息,還有一個(gè)隱含的速度信息。如下圖,藍(lán)色的點(diǎn)就是觸摸過來的觸摸點(diǎn)信息,觸摸點(diǎn)是一些離散的點(diǎn)。我這里產(chǎn)品里主打的觸摸框都是紅外觸摸框,紅外觸摸框從原理上也只能獲取到離散的觸摸點(diǎn),但如果點(diǎn)足夠密,那將離散的點(diǎn)視為連續(xù)的線段也是沒有問題的
在進(jìn)入實(shí)際算法之前,還需要進(jìn)行一步點(diǎn)的過濾。也就是將一些奇怪的點(diǎn)給過濾掉,比如在一些渣觸摸框上,可能存在報(bào)點(diǎn)存在離群點(diǎn)的情況,或者是出現(xiàn)在 0 0 點(diǎn)的情況,需要自己根據(jù)具體的硬件設(shè)備進(jìn)行丟點(diǎn)處理。這一步不是必須的,基本只有在大屏幕觸摸框下才需要進(jìn)行
骨架計(jì)算
完成點(diǎn)集的處理之后,即可開始計(jì)算筆跡的骨架??梢詫⒐P跡骨架認(rèn)為是一個(gè)最簡(jiǎn)單展示一段順滑筆跡的軌跡,也就是當(dāng)筆跡各處的粗細(xì)都一致時(shí),即沒有棱角和筆鋒時(shí)的一段幾何軌跡。實(shí)際上的算法后續(xù)的棱角和筆鋒、跟隨壓感變更等等都是在筆跡的骨架的基礎(chǔ)上,修改筆跡某一段的粗細(xì)變化。骨架的計(jì)算十分簡(jiǎn)單,可以采用貝塞爾等算法將收到的觸摸點(diǎn)進(jìn)行平滑計(jì)算,此過程如果需要補(bǔ)點(diǎn),即在觸摸點(diǎn)不夠密集時(shí)進(jìn)行補(bǔ)點(diǎn),則可以自己再疊加一些魔改的貝塞爾算法,比如 一種簡(jiǎn)單的貝塞爾擬合算法_貝塞爾曲線擬合-CSDN博客 介紹的方法
一般是將收集到的觸摸點(diǎn)每?jī)蓚€(gè)點(diǎn)的中心做定點(diǎn),使用收集到的觸摸點(diǎn)做控制點(diǎn),如下圖
對(duì)于許多業(yè)務(wù)情況來說,只需要到這一步就可以算畫出一段平滑的筆跡了
接下來的步驟將和大家介紹如何畫出更好看的筆跡效果
棱角優(yōu)化
棱角優(yōu)化步驟是一個(gè)專門為中文書寫筆跡軌跡優(yōu)化的方法。用途是讓寫出來的漢字比較有棱角,適合用戶手寫類似黑體或楷體,不適合用在草書的情況。大概的算法思路如下,假定有類似如下的輸入觸摸點(diǎn)
這時(shí)需要把這些點(diǎn)分為兩個(gè)線段,分為兩個(gè)線段的大概效果如下圖
對(duì)于漢字而言,我認(rèn)為如果以上兩個(gè)線段構(gòu)成的內(nèi)角在 90 度以下時(shí),有棱的好看,超過 90 度時(shí),使用圓角的好看
通過輸入可以拿到觸摸點(diǎn),按照兩個(gè)觸摸點(diǎn)連接為線,求相鄰線段的夾角,判斷角度可以知道用戶是否希望畫出棱還是畫出圓。加上這個(gè)優(yōu)化之后就可以在寫漢字時(shí),比微軟默認(rèn)的 WPF 或 UWP 的筆跡算法在棱角方面處理更好
如圖的 α 就是兩個(gè)線段的角度,判卷角度如果大于 90° 就是用戶希望畫圓的角,使用貝塞爾算法。如果小于 90° 那就可以判斷用戶希望畫有棱的,直接把點(diǎn)分開為兩個(gè)線段
當(dāng)然了,上文提到的 90° 是我自己測(cè)試發(fā)現(xiàn)的數(shù)值,大家可以根據(jù)自己的實(shí)際需要修改參數(shù)。在不需要讓筆跡有筆鋒以及跟隨壓感時(shí),以上的棱角優(yōu)化步驟可以用在骨架計(jì)算的步驟上,直接作用到使用骨架繪制出的筆跡上。也可以在帶壓感時(shí)的在下文繼續(xù)介紹的更復(fù)雜的米字法筆跡算法的最后呈現(xiàn)時(shí)使用
筆跡軌跡寬度優(yōu)化
無論是否有壓感,都可以應(yīng)用上筆跡軌跡寬度的優(yōu)化,筆跡軌跡的寬度可以認(rèn)為是在骨架的基礎(chǔ)上,進(jìn)行填充,讓原本只有骨架的很細(xì)的筆跡變粗??梢哉J(rèn)為在骨架計(jì)算步驟拿到的是一條沒有寬度的線條,進(jìn)行筆跡軌跡寬度優(yōu)化計(jì)算就可以畫出更好看的筆跡效果。比如說寫一個(gè)漢字的“一”字,就可以寫出兩端寬度比較大,中間寬度比較小的筆效果
簡(jiǎn)單的筆跡軌跡寬度優(yōu)化算法大概如下,下面將會(huì)用到一點(diǎn)點(diǎn)公式,相信大家一看就明白,以下使用到的公式
用戶可以設(shè)置筆跡軌跡線條的寬度,這個(gè)設(shè)置的寬度為初始寬度,將用戶設(shè)置的筆跡粗細(xì)寬度記為 T 參數(shù)。速度參數(shù) v 的計(jì)算有些取巧,因?yàn)槭占降狞c(diǎn)的時(shí)間間隔是只有很小的誤差,為了優(yōu)化計(jì)算,就把兩個(gè)點(diǎn)直接的距離作為用戶的畫線速度
上圖公式里面的 u(v) 函數(shù)計(jì)算方法就是取用戶正常最慢速度,記為 w 值,這里的 w 為常量 1 的值。為了防止在靜止距離獲得最小的點(diǎn)為負(fù)數(shù),這里使用 u(v)=Max(v-w,x) 限制最小值為 x 的值,按照經(jīng)驗(yàn),這里取 x 為常量 2 的值。為了防止用戶的畫線速度太快,所以按照經(jīng)驗(yàn)取最高的速度只能是 5 的值。以上的效果就是在用戶書寫速度超過最高速度 5 單位長(zhǎng)度 1 毫秒的時(shí)候取 80% 的用戶設(shè)置粗細(xì)。在用戶使用很慢速度畫線的時(shí)候采用120%的用戶設(shè)置粗細(xì)
最后的常量 a 我按照經(jīng)驗(yàn)取的是 T/0.12
的值
以上的常量部分指的不是 C# 里面的常量,而是參與數(shù)學(xué)計(jì)算公式里面的常量,即和自變量對(duì)應(yīng)的常量。這些常量大家都可以根據(jù)自己的經(jīng)驗(yàn)進(jìn)行修改,或者寫一個(gè)修改參數(shù)的工具讓美工或設(shè)計(jì)師去優(yōu)化
下圖是我這邊靠真人工“智能”調(diào)出來的高階擬合函數(shù)的參數(shù),橫坐標(biāo)是不同的系數(shù)計(jì)算值,即壓感正相關(guān)、速度負(fù)相關(guān)、觸摸面積正相關(guān)系數(shù)值,對(duì)應(yīng)輸出是參數(shù) k 取值。大概就是速度越慢、壓感越大、觸摸面積越大,則畫出來的筆跡粗細(xì)越大。只是這個(gè)變化不是線性變化的過程,是一個(gè)高階擬合的方式
經(jīng)過這一個(gè)步驟之后,就可以實(shí)現(xiàn)在用戶使用快速畫線,畫出來的線就會(huì)變細(xì),在用戶畫線的速度變慢,就會(huì)畫出寬度比較大的線
米字法
這部分屬于寫出順滑的筆跡的核心算法。在經(jīng)過了筆跡軌跡寬度優(yōu)化之后,盡管看起來已經(jīng)有些順滑了,其實(shí)依然無法寫出毛筆字效果,比如刀鋒等效果,最多只能寫出有粗細(xì)變更的筆跡。接下來的算法部分將使用到棱角優(yōu)化步驟處理的骨架軌跡算出的骨架點(diǎn),以及筆跡軌跡寬度優(yōu)化步驟輸出的每個(gè)點(diǎn)的筆跡粗細(xì)大小信息,進(jìn)行更高級(jí)的優(yōu)化
通過上文的描述,大家也知道筆跡元素可以由筆跡輪廓兩邊的曲線組合而成,因此求筆跡的幾何圖形本質(zhì)就是求筆跡的輪廓線,由筆跡的輪廓線填充即可獲取筆跡。如下圖,只需要將如下兩條曲線相連接,那么將獲得一條筆跡的幾何圖形
在經(jīng)過骨架計(jì)算步驟之后,即可拿到骨架軌跡,通過骨架軌跡即可拿到相應(yīng)的骨架點(diǎn)。拿到相應(yīng)的骨架點(diǎn)的算法不固定,可以是求均勻的距離下的骨架軌跡上的點(diǎn),也可以求對(duì)原始觸摸點(diǎn)的骨架校正點(diǎn)。如果難以理解如何通過骨架軌跡拿到相應(yīng)的骨架點(diǎn),那可以將骨架點(diǎn)當(dāng)成原始的觸摸點(diǎn)來看,因?yàn)槿鄙俟羌茳c(diǎn)這一步不用影響對(duì)接下來的算法的理解
如下圖,假定以下拿到的藍(lán)色的點(diǎn)就是骨架點(diǎn)
根據(jù)觸摸點(diǎn)的每個(gè)點(diǎn)的狀態(tài)可以決定骨架點(diǎn)的每個(gè)點(diǎn)的狀態(tài),對(duì)應(yīng)的就是每個(gè)點(diǎn)的上下左右邊距,如下圖。決定每個(gè)點(diǎn)的上下左右邊距算法叫做慣性邊距算法,這個(gè)慣性邊距算法將放在下文再描述
經(jīng)過了慣性邊距算法,可以獲取骨架點(diǎn)的上下左右邊距,取邊距的端點(diǎn),作為筆廓點(diǎn)。如下圖,筆廓點(diǎn)就是藍(lán)色的圓圈
如下圖,連接筆跡的筆廓點(diǎn)就可以獲得筆跡的輪廓線,也就是獲得筆跡的幾何圖形。但僅僅采用如上述算法,可以看到筆跡的輪廓相對(duì)粗糙,雖然比上文給的算法好了一點(diǎn),但也沒好多少。想要實(shí)現(xiàn)更好的效果,還需要繼續(xù)添加更多邏輯
在開始介紹算法之前,需要引入不對(duì)稱橢圓的概念,默認(rèn)的橢圓都是對(duì)稱的,如上下對(duì)稱或左右對(duì)稱。而不對(duì)稱橢圓是上下左右都不對(duì)稱的橢圓。如下圖,從不對(duì)稱橢圓的圓心的上下左右四個(gè)方向有著不同的長(zhǎng)度
不對(duì)稱橢圓的算法相當(dāng)于繪制出四個(gè)對(duì)稱的橢圓,分別取其中的四分之一拼接起來的橢圓
如下圖,是將繪制出來的四個(gè)對(duì)稱的橢圓各取四分之一部分拼接起來,其中填充部分就是非對(duì)稱橢圓
這里的非對(duì)稱橢圓是用在將筆跡的骨架點(diǎn)按照慣性邊距算法上下左右分別采用不同的長(zhǎng)度,創(chuàng)建出來的橢圓
沿著橢圓的切線方向連接的線段就可以作出平滑的筆跡輪廓線,如下圖。下圖繪制僅僅只是參考,部分線段連接不是采用橢圓的切線
特別的,為了性能優(yōu)化部分,因?yàn)楣P跡的粗細(xì)一般都很小,在筆跡粗細(xì)很小的時(shí)候,可以使用多邊形近似代替橢圓。因?yàn)閷?duì)多邊形的求值計(jì)算的性能要遠(yuǎn)遠(yuǎn)高于橢圓,同時(shí)求橢圓切線的代碼也不好寫。如下圖,采用如 米 字的方式代替橢圓
只需要連接橢圓的外接輪廓點(diǎn)即可作出筆跡效果,如下圖
當(dāng)骨架點(diǎn)足夠密集的時(shí)候,這時(shí)候連接橢圓的外接輪廓點(diǎn)使用線段連接,再將這個(gè)線段組成閉合的折線即可寫出十分順滑的筆跡效果了。經(jīng)過我的實(shí)際測(cè)試,通過骨架軌跡算出比較密集的骨架點(diǎn),從而讓外接輪廓點(diǎn)連接畫出的筆跡效果,既順滑且渲染性能高。在骨架點(diǎn)不夠密集時(shí),如直接將觸摸點(diǎn)當(dāng)骨架點(diǎn)時(shí),可以使用貝賽爾曲線形式連接外接輪廓點(diǎn),從而畫出順滑的筆跡效果,但經(jīng)過實(shí)際測(cè)試我發(fā)現(xiàn)此方法無論是筆跡的順滑還是渲染性能都不如讓骨架點(diǎn)足夠密集的方法
此算法除了能夠讓筆跡效果十分順滑之外,還能實(shí)現(xiàn)筆跡刀鋒效果。核心實(shí)現(xiàn)是根據(jù)慣性邊距算法可以決定邊距,通過邊距的不同,可以實(shí)現(xiàn)出如毛筆的刀鋒效果,如下圖所示。在運(yùn)筆繪制刀鋒效果時(shí),如圖情況將會(huì)更改左邊距距離,讓筆跡的一邊貼近直線而另一邊是曲線的效果。采用此算法可以做到更好的寫出毛筆字效果
慣性邊距算法就是通過一系列的代碼處理,決定每個(gè)骨架點(diǎn)的上下左右邊距的值,比如運(yùn)動(dòng)軌跡方向,比如運(yùn)動(dòng)速度,比如預(yù)測(cè)字形等等。這部分更多的是靠設(shè)計(jì)師或美工進(jìn)行優(yōu)化
以下是我給出的一個(gè)認(rèn)為簡(jiǎn)單的算法例子,大家也可以自行發(fā)揮
在筆跡軌跡寬度優(yōu)化的基礎(chǔ)上,將筆跡軌跡寬度優(yōu)化的輸出結(jié)果作為筆跡粗細(xì)參考值。將每個(gè)骨架點(diǎn)的上下左右邊距先采用筆跡粗細(xì)的一半作為基準(zhǔn)值,然后分別附加各自的縮放系數(shù)。根據(jù)筆跡的運(yùn)動(dòng)軌跡方向,可以將方向分為上下左右四個(gè)方向,再按照運(yùn)動(dòng)的速度以及多個(gè)筆跡點(diǎn)的偏移累計(jì)值決定縮放系數(shù)的值。如上圖,按照筆跡軌跡是向左下方向,將會(huì)取筆跡的多個(gè)觸摸點(diǎn),計(jì)算累計(jì)的偏移值,如取筆跡的距離當(dāng)前的前n個(gè)觸摸點(diǎn),如上圖是取5個(gè)觸摸點(diǎn)的坐標(biāo),求出距離當(dāng)前坐標(biāo)的偏移值也就是相當(dāng)于求當(dāng)前點(diǎn)和前第5個(gè)點(diǎn)的距離。如果在這前5個(gè)觸摸點(diǎn)中,有方向不一致的觸摸點(diǎn)存在,如第三個(gè)觸摸點(diǎn)的方向和其他點(diǎn)的觸摸方向不同,那么將偏移值減去方向不一致的觸摸點(diǎn)的相對(duì)于其下一個(gè)觸摸點(diǎn)的距離。再根據(jù)觸摸偏移值決定對(duì)應(yīng)方向的縮放系數(shù),決定縮放系數(shù)的方法就是取n個(gè)觸摸點(diǎn)的對(duì)應(yīng)方向的最大距離數(shù),如發(fā)現(xiàn)是存在左右方向的偏移那么取水平方向距離值,將距離值減去偏移值除的值處以距離值乘以給特定觸摸框優(yōu)化的常數(shù),即可獲取方向上的觸摸偏移縮放系數(shù)。根據(jù)不同的上下左右邊距的不同縮放系數(shù)就可以實(shí)現(xiàn)如上圖的效果。同樣的,采用此方法進(jìn)行不同縮放系數(shù)最終還是需要乘以筆跡觸摸點(diǎn)壓感變化的縮放系數(shù),才是最終的各個(gè)方向的縮放系數(shù)
通過以上的算法即可實(shí)現(xiàn)比較好看的筆跡效果
更多筆跡算法
基于輪廓方法的實(shí)時(shí)手寫美化技術(shù)及應(yīng)用
我的伙伴在閱讀了我的博客之后,告訴我除了以上我介紹的我提出的順滑的筆跡算法之外,他還看到了陳露開大佬在 2014 年的 《基于輪廓方法的實(shí)時(shí)手寫美化技術(shù)及應(yīng)用》 文章,他說這篇文章和本文介紹的 CN109284059A 和 CN115373534A 比較相似
我仔細(xì)閱讀了 《基于輪廓方法的實(shí)時(shí)手寫美化技術(shù)及應(yīng)用》 和 CN104268915A - 一種手寫漢字的實(shí)時(shí)輪廓美化方法 專利之后,我發(fā)現(xiàn)確實(shí)從過程處理步驟上講是相似的。兩邊都需要先根據(jù)輸入的數(shù)據(jù)進(jìn)行預(yù)處理,兩邊都需要執(zhí)行粗細(xì)控制,兩邊都需要求閉包曲線。但兩邊的具體實(shí)現(xiàn)以及繪制的本質(zhì)是不相同的,以下列舉出各個(gè)步驟的實(shí)現(xiàn)上的差異,由于專利保護(hù),本文接下來對(duì)陳露開大佬的算法部分的介紹將只進(jìn)行粗略的介紹,更多側(cè)重于與我的算法的對(duì)比上,更詳細(xì)的關(guān)于陳露開大佬的算法還請(qǐng)自行參閱大佬的論文和專利
在開始之前需要先感謝陳露開大佬提出的這么優(yōu)秀的算法
筆跡軌跡預(yù)處理
對(duì)應(yīng)于我的骨架點(diǎn)處理階段,陳露開大佬使用的是貝賽爾曲線一條路,配合求曲線分裂等算法。由于我這邊的算法上不需要也不存在曲線分裂及拐點(diǎn)檢測(cè)的算法,于是我的算法可以忽略陳露開大佬的 Bezier 曲線擬合步驟。具體的陳露開大佬的實(shí)現(xiàn)細(xì)節(jié),還請(qǐng)自行參閱 《基于輪廓方法的實(shí)時(shí)手寫美化技術(shù)及應(yīng)用》 的第二章部分
粗細(xì)控制
相比于我算法這邊滿滿的工程經(jīng)驗(yàn)值來說,陳露開大佬提出了寬度控制模型,給出了計(jì)算寬度的公式。同時(shí)為了消除算出來的瞬間某個(gè)點(diǎn)出現(xiàn)比較大的抖動(dòng),也就是類似葫蘆的軌跡的問題(這話是我說的,不一定他是真的處理這個(gè)問題,只是我推導(dǎo)他的算法能夠處理這個(gè)問題),使用了平滑濾波方法,由于筆跡繪制是實(shí)時(shí)的,也就是后面的點(diǎn)是不可知的,于是更具體的采用的是前向?yàn)V波
我認(rèn)為十分具體的前向?yàn)V波算法本身不重要,重要的是在這個(gè)步驟引入前向?yàn)V波算法。至于具體如何配置參數(shù),這個(gè)可以依靠工程經(jīng)驗(yàn)修改
由于我這邊的算法上本身不存在較大的抖動(dòng),或者具體來說是在我所使用的硬件設(shè)備上不會(huì)上報(bào)如此離譜的點(diǎn)、以及系統(tǒng)層及應(yīng)用框架層會(huì)攔截許多亂來的數(shù)據(jù),導(dǎo)致了我這邊的算法本身不需要帶上濾波算法即可實(shí)現(xiàn)平滑的效果
但無論怎么說,在性能允許的情況下加上濾波算法還是能夠有比較好的提升的
更優(yōu)的是陳露開大佬針對(duì)毛筆、鵝毛筆風(fēng)格,提出了不同的算法,如此可以更好的控制。相比之下,我這邊是直接上高階擬合函數(shù),靠設(shè)計(jì)師調(diào)許多參數(shù)實(shí)現(xiàn)粗細(xì)控制
我感覺陳露開大佬的算法可以在任意設(shè)備上都獲得比較好的效果,而我的算法需要根據(jù)設(shè)備修改參數(shù),但由于我這邊有人工介入,可能可以調(diào)出更好看的效果,且由于我的算法是應(yīng)用在大屏幕的觸摸屏上,如 90 寸等大屏幕上,且大部分都是紅外觸摸框,可能在大屏幕觸摸屏上我的算法更合適。而在小尺寸更精準(zhǔn)的屏幕上陳露開大佬的算法更合適
輪廓生成
不同于我的算法的筆刷模型采用的是米字法的方式,陳露開大佬的算法比較好的效果上使用的是水滴的方式。兩邊算法相同的點(diǎn)在于都是采用某個(gè)形狀作為筆段,由多個(gè)筆段連成筆跡輪廓
使用水滴的效果能夠更好的實(shí)現(xiàn)毛筆字效果的起末端的效果,特別在寫“一”字的時(shí)候,兩端的效果會(huì)更好。水滴形狀可以比米字法更好的模擬出毛筆和紙張接觸的截面,效果上更貼近現(xiàn)實(shí)的毛筆截面效果
但使用水滴形狀作為筆刷將不能和我的采用米字法有取巧的方式,使用水滴形狀將需要比較復(fù)雜的求包算法,詳細(xì)請(qǐng)參閱 《基于輪廓方法的實(shí)時(shí)手寫美化技術(shù)及應(yīng)用》 論文里面的第三章
我的算法相比來說的另一個(gè)優(yōu)勢(shì)是可以控制米字的各個(gè)方向的長(zhǎng)度,從而達(dá)到實(shí)現(xiàn)毛筆刀鋒等效果
有趣的是我的算法和陳露開大佬算法在輪廓生成部分是可以疊加的,取兩者的優(yōu)勢(shì)。以下部分僅僅是我的想法,我沒有對(duì)此做任何的實(shí)踐。在不考慮專利版權(quán)問題下具體的實(shí)現(xiàn)就是采用米字法實(shí)現(xiàn)筆跡的段支部分,采用陳露開大佬的水滴生成首末輪廓,兩者通過較復(fù)雜的幾何計(jì)算進(jìn)行筆段輪廓合并,如此即可同時(shí)兼顧我的算法實(shí)現(xiàn)的刀鋒以及帶棱角拐角效果,又能使用到陳露開大佬的水滴在筆跡首末的更近毛筆效果
更多亮點(diǎn)
除了以上介紹的大步驟上的差異之外,閱讀了大佬的論文和專利之后,我還發(fā)現(xiàn)了大佬的算法的更多亮點(diǎn)部分
比如優(yōu)化曲線分裂的算法,在進(jìn)行筆跡算法過程中,對(duì)性能是敏感的,于是大佬提出了利用弧弦比用來代替弦括面積,從而減少計(jì)算復(fù)雜度。這部分還請(qǐng)參閱大佬論文的第 15 頁部分的描述。盡管我現(xiàn)在沒有用到此算法的需求,但也收藏了起來
再比如論文里面在筆段輪廓合并里提出的消除筆畫中冗余的輪廓的細(xì)節(jié)算法,盡管我看不懂但感覺應(yīng)該對(duì)渲染性能有所幫助
再次感謝陳露開大佬提出的這么優(yōu)秀的算法。在閱讀了大佬的論文和專利之后,給我?guī)淼氖斋@是:
- 寬度計(jì)算(粗細(xì)計(jì)算)可以帶上前向?yàn)V波
- 首末輪廓可以使用水滴形狀,且提供了水滴形狀能夠?qū)崿F(xiàn)的效果展示
但最大的收獲是我發(fā)現(xiàn)在沒有了解到大佬的算法之前,我自己提出的算法具體在處理步驟上和大佬的算法的筆跡步驟是如此的貼近,這就證明了我的路線也是正確的,至少方向是正確的文章來源:http://www.zghlxwxcb.cn/news/detail-711646.html
本文只討論了筆跡的算法,而不包含如何優(yōu)化筆跡繪制的性能以及更多的觸摸相關(guān)內(nèi)容。如果大家對(duì)這部分感興趣,請(qǐng)參閱 WPF 觸摸相關(guān)文章來源地址http://www.zghlxwxcb.cn/news/detail-711646.html
到了這里,關(guān)于WPF 筆跡算法 從點(diǎn)集轉(zhuǎn)筆跡輪廓的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!