WPF Canvas 平滑笔迹
控件名:CanvasHandWriting
作者:小封(邝攀升)
原文链接: https://github.com/WPFDevelopersOrg/WPFDevelopers
编辑:驚鏵
完整的思路如下
收集路径点集。
平均采样路径点集。
将路径点集转为 LineB。
把 LineB 数据传给 Path。
1)Vector2D.cs 代码如下
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;namespace WPFDevelopers.Samples.ExampleViews.CanvasHandWriting
{public class Vector2D{public double X { get; set; } = 0;public double Y { get; set; } = 0;/// <summary>/// 向量的模/// </summary>public double Mold{get{//自身各分量平方运算.double X = this.X * this.X;double Y = this.Y * this.Y;return Math.Sqrt(X + Y);//开根号,最终返回向量的长度/模/大小.}}/// <summary>/// 单位向量/// </summary>public Vector2D UnitVector{get{double sumSquares = (X * X) + (Y * Y);return new Vector2D(X / Math.Sqrt(sumSquares), Y / Math.Sqrt(sumSquares));}}public Vector2D(){}public Vector2D(double x, double y){X = x;Y = y;}public Vector2D(System.Windows.Point point){X = point.X;Y = point.Y;}public void Offset(double angle, double distance, AngleType angleType = AngleType.Radian){var vector2D = Vector2D.CalculateVectorOffset(this, angle, distance, angleType);X = vector2D.X;Y = vector2D.Y;}public void Rotate(double angle, Vector2D vectorCenter = null, AngleType angleType = AngleType.Radian){vectorCenter = vectorCenter == null ? this : vectorCenter;var vector2D = Vector2D.CalculateVectorRotation(this, vectorCenter, angle, angleType);X = vector2D.X;Y = vector2D.Y;}#region 静态方法/// <summary>/// 计算两个向量之间的距离/// </summary>public static double CalculateVectorDistance(Vector2D vector2DA, Vector2D vector2DB){Vector2D vector2D = vector2DA - vector2DB;return vector2D.Mold;}/// <summary>/// 计算两点夹角,右侧X轴线为0度,向下为正,向上为负/// </summary>public static double IncludedAngleXAxis(Vector2D vector2DA, Vector2D vector2DB, AngleType angleType = AngleType.Radian){double radian = Math.Atan2(vector2DB.Y - vector2DA.Y, vector2DB.X - vector2DA.X); //弧度:1.1071487177940904return angleType == AngleType.Radian ? radian : ComputingHelper.RadianToAngle(radian);}/// <summary>/// 计算两点夹角,下侧Y轴线为0度,向右为正,向左为负/// </summary>public static double IncludedAngleYAxis(Vector2D vector2DA, Vector2D vector2DB, AngleType angleType = AngleType.Radian){double radian = Math.Atan2(vector2DB.X - vector2DA.X, vector2DB.Y - vector2DA.Y); //弧度:0.46364760900080609return angleType == AngleType.Radian ? radian : ComputingHelper.RadianToAngle(radian);}/// <summary>/// 偏移向量到指定角度,指定距离/// </summary>public static Vector2D CalculateVectorOffset(Vector2D vector2D, double angle, double distance, AngleType angleType = AngleType.Radian){Vector2D pointVector2D = new Vector2D();if (angleType == AngleType.Angle){angle = angle / (180 / Math.PI);//角度转弧度}double width = Math.Cos(Math.Abs(angle)) * distance;double height = Math.Sin(Math.Abs(angle)) * distance;if(angle <= Math.PI && angle >= 0)//if (angle is <= Math.PI and >= 0){pointVector2D.X = vector2D.X - width;pointVector2D.Y = vector2D.Y - height;}if (angle >= (-Math.PI) && angle <= 0)//if (angle is >= (-Math.PI) and <= 0){pointVector2D.X = vector2D.X - width;pointVector2D.Y = vector2D.Y + height;}return pointVector2D;}/// <summary>/// 围绕一个中心点,旋转一个向量,相对旋转/// </summary>public static Vector2D CalculateVectorRotation(Vector2D vector2D, Vector2D vectorCenter, double radian, AngleType angleType = AngleType.Radian){radian = angleType == AngleType.Radian ? radian : ComputingHelper.RadianToAngle(radian);double x1 = (vector2D.X - vectorCenter.X) * Math.Sin(radian) + (vector2D.Y - vectorCenter.Y) * Math.Cos(radian) + vectorCenter.X;double y1 = -(vector2D.X - vectorCenter.X) * Math.Cos(radian) + (vector2D.Y - vectorCenter.Y) * Math.Sin(radian) + vectorCenter.Y;return new Vector2D(x1, y1);}public static Vector2D CalculateVectorCenter(Vector2D vector2DA, Vector2D vector2DB){return new Vector2D((vector2DA.X + vector2DB.X) / 2, (vector2DA.Y + vector2DB.Y) / 2);}/// <summary>/// 判断坐标点是否在多边形区域内,射线法/// </summary>public static bool IsPointPolygonalArea(Vector2D vector2D, List<Vector2D> aolygonaArrayList){var N = aolygonaArrayList.Count;var boundOrVertex = true; //如果点位于多边形的顶点或边上,也算做点在多边形内,直接返回truevar crossNumber = 0; //x的交叉点计数var precision = 2e-10; //浮点类型计算时候与0比较时候的容差Vector2D p1, p2; //neighbour bound verticesvar p = vector2D; //测试点p1 = aolygonaArrayList[0]; //left vertex for (var i = 1; i <= N; ++i){//check all rays if (p.X.Equals(p1.X) && p.Y.Equals(p1.Y)){return boundOrVertex; //p is an vertex}p2 = aolygonaArrayList[i % N]; //right vertex if (p.X < Math.Min(p1.X, p2.X) || p.X > Math.Max(p1.X, p2.X)){//ray is outside of our interests p1 = p2;continue; //next ray left point}if (p.X > Math.Min(p1.X, p2.X) && p.X < Math.Max(p1.X, p2.X)){//ray is crossing over by the algorithm (common part of)if (p.Y <= Math.Max(p1.Y, p2.Y)){//x is before of ray if (p1.X == p2.X && p.Y >= Math.Min(p1.Y, p2.Y)){//overlies on a horizontal rayreturn boundOrVertex;}if (p1.Y == p2.Y){//ray is vertical if (p1.Y == p.Y){//overlies on a vertical rayreturn boundOrVertex;}else{//before ray++crossNumber;}}else{//cross point on the left side var xinters =(p.X - p1.X) * (p2.Y - p1.Y) / (p2.X - p1.X) +p1.Y; //cross point of Y if (Math.Abs(p.Y - xinters) < precision){//overlies on a rayreturn boundOrVertex;}if (p.Y < xinters){//before ray++crossNumber;}}}}else{//special case when ray is crossing through the vertex if (p.X == p2.X && p.Y <= p2.Y){//p crossing over p2 var p3 = aolygonaArrayList[(i + 1) % N]; //next vertex if (p.X >= Math.Min(p1.X, p3.X) && p.X <= Math.Max(p1.X, p3.X)){//p.X lies between p1.X & p3.X++crossNumber;}else{crossNumber += 2;}}}p1 = p2; //next ray left point}if (crossNumber % 2 == 0){//偶数在多边形外return false;}else{//奇数在多边形内return true;}}/// <summary>/// 判断一个点是否在一条边内/// </summary>public static bool IsPointEdge(Vector2D point, Vector2D startPoint, Vector2D endPoint){return (point.X - startPoint.X) * (endPoint.Y - startPoint.Y) == (endPoint.X - startPoint.X) * (point.Y - startPoint.Y)&& Math.Min(startPoint.X, endPoint.X) <= point.X && point.X <= Math.Max(startPoint.X, endPoint.X)&& Math.Min(startPoint.Y, endPoint.Y) <= point.Y && point.Y <= Math.Max(startPoint.Y, endPoint.Y);}#endregion 静态方法#region 运算符重载/// <summary>/// 重载运算符,和运算,可以用来计算两向量距离/// </summary>public static Vector2D operator +(Vector2D vector2DA, Vector2D vector2DB){Vector2D vector2D = new Vector2D();vector2D.X = vector2DA.X + vector2DB.X;vector2D.Y = vector2DA.Y + vector2DB.Y;return vector2D;}/// <summary>/// 重载运算符,差运算,可以用来计算两向量距离/// </summary>public static Vector2D operator -(Vector2D vector2DA, Vector2D vector2DB){Vector2D vector2D = new Vector2D();vector2D.X = vector2DA.X - vector2DB.X;vector2D.Y = vector2DA.Y - vector2DB.Y;return vector2D;}/// <summary>/// 重载运算符,差运算,可以用来计算两向量距离/// </summary>public static Vector2D operator -(Vector2D vector2D, double _float){return new Vector2D(vector2D.X - _float, vector2D.Y - _float);}/// <summary>/// 重载运算符,点积运算,可以用来计算两向量夹角/// </summary>public static double operator *(Vector2D vector2DA, Vector2D vector2DB){return (vector2DA.X * vector2DB.X) + (vector2DA.Y * vector2DB.Y);}public static double operator *(Vector2D vector2D, double _float){return (vector2D.X * _float) + (vector2D.Y * _float);}/// <summary>/// 重载运算符,点积运算,可以用来计算两向量夹角/// </summary>public static double operator /(Vector2D vector2D, double para){return (vector2D.X / para) + (vector2D.Y / para);}/// <summary>/// 重载运算符/// </summary>public static bool operator >=(Vector2D vector2D, double para){if (vector2D.Mold >= para){return true;}else{return false;}}public static bool operator <=(Vector2D vector2D, double para){if (vector2D.Mold <= para){return true;}else{return false;}}public static bool operator >(Vector2D vector2D, double para){if (vector2D.Mold > para){return true;}else{return false;}}public static bool operator <(Vector2D vector2D, double para){if (vector2D.Mold < para){return true;}else{return false;}}#endregion 运算符重载#region 隐式转换/// <summary>/// 重载隐式转换,可以直接使用Point/// </summary>/// <param name="v"></param>public static implicit operator Vector2D(System.Windows.Point v)//隐式转换{return new Vector2D(v.X, v.Y);}/// <summary>/// 重载隐式转换,可以直接使用Point/// </summary>/// <param name="v"></param>public static implicit operator System.Windows.Point(Vector2D v)//隐式转换{return new System.Windows.Point(v.X, v.Y);}/// <summary>/// 重载隐式转换,可以直接使用double/// </summary>/// <param name="v"></param>public static implicit operator Vector2D(double v)//隐式转换{return new Vector2D(v, v);}#endregion 隐式转换#region ToStringpublic override string ToString(){return X.ToString() + "," + Y.ToString();}public string ToString(string symbol){return X.ToString() + symbol + Y.ToString();}public string ToString(string sender, string symbol){return X.ToString(sender) + symbol + Y.ToString(sender);}#endregion}public enum AngleType {Angle,Radian}
}
2)ComputingHelper.cs 代码如下
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;namespace WPFDevelopers.Samples.ExampleViews.CanvasHandWriting
{public static class ComputingHelper{public static double AngleToRadian(double angle){return angle * (Math.PI / 180);}public static double RadianToAngle(double radian){return radian * (180 / Math.PI);}/// <summary>/// 将一个值从一个范围映射到另一个范围/// </summary>public static double RangeMapping(double inputValue, double enterLowerLimit, double enterUpperLimit, double outputLowerLimit, double OutputUpperLimit, CurveType curveType = CurveType.None){var percentage = (enterUpperLimit - inputValue) / (enterUpperLimit - enterLowerLimit);switch (curveType){case CurveType.Sine:percentage = Math.Sin(percentage);break;case CurveType.CoSine:percentage = Math.Cos(percentage);break;case CurveType.Tangent:percentage = Math.Tan(percentage);break;case CurveType.Cotangent:percentage = Math.Atan(percentage);break;default:break;}double outputValue = OutputUpperLimit - ((OutputUpperLimit - outputLowerLimit) * percentage);return outputValue;}public static string ByteToKB(double _byte){List<string> unit = new List<string>() { "B", "KB", "MB", "GB", "TB", "P", "PB" };int i = 0;while (_byte > 1024){_byte /= 1024;i++;}_byte = Math.Round(_byte, 3);//保留三位小数return _byte + unit[i];}/// <summary>/// 缩短一个数组,对其进行平均采样/// </summary>public static double[] AverageSampling(double[] sourceArray, int number){if (sourceArray.Length <= number){return sourceArray;//throw new Exception("新的数组必须比原有的要小!");}double[] arrayList = new double[number];double stride = (double)sourceArray.Length / number;for (int i = 0, jIndex = 0; i < number; i++, jIndex++){double strideIncrement = i * stride;strideIncrement = Math.Round(strideIncrement, 6);double sum = 0;int firstIndex = (int)(strideIncrement);double firstDecimal = strideIncrement - firstIndex;int tailIndex = (int)(strideIncrement + stride);double tailDecimal = (strideIncrement + stride) - tailIndex;if (firstDecimal != 0)sum += sourceArray[firstIndex] * (1 - firstDecimal);if (tailDecimal != 0 && tailIndex != sourceArray.Length)sum += sourceArray[tailIndex] * (tailDecimal);int startIndex = firstDecimal == 0 ? firstIndex : firstIndex + 1;int endIndex = tailIndex;for (int j = startIndex; j < endIndex; j++)sum += sourceArray[j];arrayList[jIndex] = sum / stride;}return arrayList;}public static List<Vector2D> AverageSampling(List<Vector2D> sourceArray, int number){if (sourceArray.Count <= number - 2){return sourceArray;}double[] x = new double[sourceArray.Count];double[] y = new double[sourceArray.Count];for (int i = 0; i < sourceArray.Count; i++){x[i] = sourceArray[i].X;y[i] = sourceArray[i].Y;}double[] X = AverageSampling(x, number - 2);double[] Y = AverageSampling(y, number - 2);List<Vector2D> arrayList = new List<Vector2D>();for (int i = 0; i < number - 2; i++){arrayList.Add(new Vector2D(X[i], Y[i]));}arrayList.Insert(0, sourceArray[0]);//添加首arrayList.Add(sourceArray[sourceArray.Count - 1]);//添加尾return arrayList;}}public enum CurveType {Sine,CoSine,Tangent,Cotangent,None}
}
3)LineB.cs 代码如下
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Media;namespace WPFDevelopers.Samples.ExampleViews.CanvasHandWriting
{public class LineB{private List<Vector2D> _vector2DList = new List<Vector2D>();public List<Vector2D> Vector2DList{get { return _vector2DList; }set{_vector2DList = value;}}private List<BezierCurve> _bezierCurveList = new List<BezierCurve>();public List<BezierCurve> BezierCurveList{get { return _bezierCurveList; }private set { _bezierCurveList = value; }}private double _tension = 0.618;public double Tension{get { return _tension; }set{_tension = value;if (_tension > 10)_tension = 10;if (_tension < 0)_tension = 0;}}private bool _isClosedCurve = true;public bool IsClosedCurve{get { return _isClosedCurve; }set { _isClosedCurve = value; }}private string _pathData = string.Empty;public string PathData{get{if (_pathData == string.Empty){_pathData = Vector2DToBezierCurve();}return _pathData;}}private string Vector2DToBezierCurve() {if (Vector2DList.Count < 3)return string.Empty;BezierCurveList.Clear();for (int i = 0; i < Vector2DList.Count; i++){int pointTwoIndex = i + 1 < Vector2DList.Count ? i + 1 : 0;int pointThreeIndex = i + 2 < Vector2DList.Count ? i + 2 : i + 2 - Vector2DList.Count;Vector2D vector2D1 = Vector2DList[i];Vector2D vector2D2 = Vector2DList[pointTwoIndex];Vector2D vector2D3 = Vector2DList[pointThreeIndex];Vector2D startVector2D = Vector2D.CalculateVectorCenter(vector2D1, vector2D2);double startAngle = Vector2D.IncludedAngleXAxis(vector2D1, vector2D2);double startDistance = Vector2D.CalculateVectorDistance(startVector2D, vector2D2) * (1 - Tension);Vector2D startControlPoint = Vector2D.CalculateVectorOffset(vector2D2, startAngle, startDistance);Vector2D endVector2D = Vector2D.CalculateVectorCenter(vector2D2, vector2D3);double endAngle = Vector2D.IncludedAngleXAxis(endVector2D, vector2D2);double endDistance = Vector2D.CalculateVectorDistance(endVector2D, vector2D2) * (1 - Tension);Vector2D endControlPoint = Vector2D.CalculateVectorOffset(endVector2D, endAngle, endDistance);BezierCurve bezierCurve = new BezierCurve();bezierCurve.StartVector2D = startVector2D;bezierCurve.StartControlPoint = startControlPoint;bezierCurve.EndVector2D = endVector2D;bezierCurve.EndControlPoint = endControlPoint;BezierCurveList.Add(bezierCurve);}if (!IsClosedCurve){BezierCurveList[0].StartVector2D = Vector2DList[0];BezierCurveList.RemoveAt(BezierCurveList.Count - 1);BezierCurveList[BezierCurveList.Count - 1].EndVector2D = Vector2DList[Vector2DList.Count - 1];BezierCurveList[BezierCurveList.Count - 1].EndControlPoint = BezierCurveList[BezierCurveList.Count - 1].EndVector2D;}string path = $"M {BezierCurveList[0].StartVector2D.ToString()} ";foreach (var item in BezierCurveList){path += $"C {item.StartControlPoint.ToString(" ")},{item.EndControlPoint.ToString(" ")},{item.EndVector2D.ToString(" ")} ";}return path;}public LineB(){}public LineB(List<Vector2D> verVector2DList, bool isClosedCurve = true){this.Vector2DList = verVector2DList;this.IsClosedCurve = isClosedCurve;}/// <summary>/// 重载隐式转换,可以直接使用Point/// </summary>/// <param name="v"></param>public static implicit operator Geometry(LineB lineB)//隐式转换{return Geometry.Parse(lineB.PathData);}}public class BezierCurve{private Vector2D _startVector2D = new Vector2D(0, 0);public Vector2D StartVector2D{get { return _startVector2D; }set { _startVector2D = value; }}private Vector2D _startControlPoint = new Vector2D(0, 100);public Vector2D StartControlPoint{get { return _startControlPoint; }set { _startControlPoint = value; }}private Vector2D _endControlPoint = new Vector2D(100, 0);public Vector2D EndControlPoint{get { return _endControlPoint; }set { _endControlPoint = value; }}private Vector2D _endVector2D = new Vector2D(100, 100);public Vector2D EndVector2D{get { return _endVector2D; }set { _endVector2D = value; }}}
}
4)CanvasHandWritingExample.xaml 代码如下
<UserControl x:Class="WPFDevelopers.Samples.ExampleViews.CanvasHandWriting.CanvasHandWritingExample"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:WPFDevelopers.Samples.ExampleViews.CanvasHandWriting"mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"><UserControl.Resources><Style TargetType="{x:Type TextBlock}"><Setter Property="Foreground" Value="{StaticResource PrimaryTextSolidColorBrush}" /></Style></UserControl.Resources><Grid><Grid.RowDefinitions><RowDefinition Height="auto"/><RowDefinition/></Grid.RowDefinitions><StackPanel Orientation="Horizontal" Margin="4"><TextBlock Text="张力:" VerticalAlignment="Center"/><TextBox Text="{Binding Tension,RelativeSource={RelativeSource AncestorType=local:CanvasHandWritingExample}}"/><Slider Width="100" SmallChange="0.01" Value="{Binding Tension,RelativeSource={RelativeSource AncestorType=local:CanvasHandWritingExample}}" Maximum="1" VerticalAlignment="Center" Margin="5,0"/><TextBlock Text="平滑采样:" VerticalAlignment="Center"/><TextBox Text="{Binding SmoothSampling,RelativeSource={RelativeSource AncestorType=local:CanvasHandWritingExample}}"Margin="5,0"/><Slider Value="{Binding SmoothSampling,RelativeSource={RelativeSource AncestorType=local:CanvasHandWritingExample}}"Width="100" VerticalAlignment="Center" SmallChange="0.01" Maximum="1" TickFrequency="0.1"/><CheckBox Content="橡皮擦" VerticalAlignment="Center"Margin="5,0"IsChecked="{Binding IsEraser,RelativeSource={RelativeSource AncestorType=local:CanvasHandWritingExample}}"/><Button Content="清空画布" Click="btnClertCanvas_Click"/></StackPanel><Canvas x:Name="drawingCanvas" Grid.Row="1" Background="Black" PreviewMouseLeftButtonDown="DrawingCanvas_PreviewMouseLeftButtonDown"PreviewMouseMove="DrawingCanvas_PreviewMouseMove"PreviewMouseLeftButtonUp="DrawingCanvas_PreviewMouseLeftButtonUp"/></Grid>
</UserControl>
5)CanvasHandWritingExample.xaml.cs 代码如下
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;namespace WPFDevelopers.Samples.ExampleViews.CanvasHandWriting
{/// <summary>/// CanvasHandWritingExample.xaml 的交互逻辑/// </summary>public partial class CanvasHandWritingExample : UserControl{public static readonly DependencyProperty TensionProperty =DependencyProperty.Register("Tension", typeof(double), typeof(CanvasHandWritingExample),new PropertyMetadata(0.618));public static readonly DependencyProperty SmoothSamplingProperty =DependencyProperty.Register("SmoothSampling", typeof(double), typeof(CanvasHandWritingExample),new UIPropertyMetadata(OnSmoothSamplingChanged));public static readonly DependencyProperty IsEraserProperty =DependencyProperty.Register("IsEraser", typeof(bool), typeof(CanvasHandWritingExample),new PropertyMetadata(false));private readonly Dictionary<Path, List<Vector2D>> _PathVector2DDictionary ;volatile bool _IsStart = false;Path _DrawingPath = default;private static void OnSmoothSamplingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e){var mWindow = (CanvasHandWritingExample)d;foreach (var item in mWindow._PathVector2DDictionary.Keys){mWindow.DrawLine(item);}}public CanvasHandWritingExample(){InitializeComponent();_PathVector2DDictionary = new Dictionary<Path, List<Vector2D>>();SmoothSampling = 0.8;}private void DrawingCanvas_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e){_IsStart = true;_DrawingPath = new Path(){StrokeDashCap = PenLineCap.Round,StrokeStartLineCap = PenLineCap.Round,StrokeEndLineCap = PenLineCap.Round,StrokeLineJoin = PenLineJoin.Round,};if (IsEraser){_DrawingPath.Stroke = new SolidColorBrush(Colors.Black);_DrawingPath.StrokeThickness = 40;}else{var random = new Random();var strokeBrush = new SolidColorBrush(Color.FromRgb((byte)random.Next(200, 255), (byte)random.Next(0, 255), (byte)random.Next(0, 255)));_DrawingPath.Stroke = strokeBrush;_DrawingPath.StrokeThickness = 10;}_PathVector2DDictionary.Add(_DrawingPath, new List<Vector2D>());drawingCanvas.Children.Add(_DrawingPath);}private void DrawingCanvas_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e){_IsStart = false;_DrawingPath = default;}private void DrawingCanvas_PreviewMouseMove(object sender, MouseEventArgs e){if (!_IsStart)return;if (_DrawingPath is null)return;Vector2D currenPoint = e.GetPosition(drawingCanvas);if (currenPoint.X < 0 || currenPoint.Y < 0)return;if (currenPoint.X > drawingCanvas.ActualWidth || currenPoint.Y > drawingCanvas.ActualHeight)return;if (_PathVector2DDictionary[_DrawingPath].Count > 0){if (Vector2D.CalculateVectorDistance(currenPoint, _PathVector2DDictionary[_DrawingPath][_PathVector2DDictionary[_DrawingPath].Count - 1]) > 1)_PathVector2DDictionary[_DrawingPath].Add(e.GetPosition(drawingCanvas));}else_PathVector2DDictionary[_DrawingPath].Add(e.GetPosition(drawingCanvas));DrawLine(_DrawingPath);}public double Tension{get => (double)GetValue(TensionProperty);set => SetValue(TensionProperty, value);}public double SmoothSampling{get => (double)GetValue(SmoothSamplingProperty);set => SetValue(SmoothSamplingProperty, value);}public bool IsEraser{get => (bool)GetValue(IsEraserProperty);set => SetValue(IsEraserProperty, value);}private void DrawLine(Path path){if (_PathVector2DDictionary[path].Count > 2){var pathVector2Ds = _PathVector2DDictionary[path];var smoothNum = (int)(_PathVector2DDictionary[path].Count * SmoothSampling);if (smoothNum > 1)pathVector2Ds = ComputingHelper.AverageSampling(_PathVector2DDictionary[path], smoothNum);var lineB = new LineB(pathVector2Ds, false);lineB.Tension = Tension;path.Data = lineB;}}private void btnClertCanvas_Click(object sender, RoutedEventArgs e){drawingCanvas.Children.Clear();_PathVector2DDictionary.Clear();}}
}