目录
介绍
插值点有时是一项艰巨的数学工作,如果点是有序的,则更是如此。解决方案是使用点并使用表示时间维度的额外参数t创建一个函数。这通常称为曲线的参数表示。本文介绍了在WPF中使用贝塞尔曲线插值一组点的简单方法。
背景
这个解决方案的想法是在Stack Overflow中提出这个问题之后产生的。公认的答案引用了马克西姆·谢马纳列夫(Maxim Shemanarev)提出的一种简单而有趣的方法,其中控制点是从原始点(称为锚点)计算出来的。
在这里,我们创建一个WPF UserControl,它从任何点集合中绘制曲线。此控件可与模式MVVM一起使用。如果任何点的坐标发生变化,曲线也会自动改变。例如,它可以用于绘图应用程序,您可以在其中拖放用于更改绘图或曲线的点。
背后的算法
由于原来的反谷物网站已经关闭(我发现Sourceforge仍然支持这个库,我们可以在这里找到原始文章!)),我将解释 Maxim Shemanarev 提出的算法是什么。
贝塞尔曲线有两个锚点(起点和终点)和两个控制点(CP),用于确定其形状。我们给出了锚点,它们是多边形的一对顶点。问题是,如何计算控制点?很明显,两条相邻边的控制点与它们之间的顶点形成一条直线。
找到的解决方案是一种非常简单的方法,不需要任何复杂的数学运算。首先,我们取多边形并计算其边的中点Ai。
在这里,我们有线段C i,它们连接相邻线段的两个点Ai。然后,我们应该计算点Bi,如图所示。
第三步是最终的。我们只需移动线段C i,使它们的点Bi 与各自的顶点重合。就是这样,我们计算了贝塞尔曲线的控制点,结果看起来不错。
一点点改进。由于我们有一条直线来确定控制点的位置,因此我们可以根据需要移动它们,从而改变结果曲线的形状。我使用了一个简单的系数K,该系数相对于顶点和控制点之间的初始距离沿线移动点。控制点离顶点越近,将获得越清晰的图形。
该方法适用于自相交多边形。下面的示例表明结果非常有趣。
用于计算的类
下面是基于上面公开的算法计算样条线段的类。这个类被命名为InterpolationUtils,它有一个static方法(名为InterpolatePointWithBezierCurves),它返回一个BezierCurveSegment列表,这将是我们问题的解决方案。
该BezierCurveSegment类具有定义样条线段的四个属性:StartPoint、EndPoint、FirstControlPoint和SecondControlPoint。
由于上述算法最初是针对闭合曲线实现的,并且希望它也可以应用于开放曲线,因此需要进行一些更改。因此,该InterpolatePointWithBezierCurves方法接收第二个参数,即名为isClosedCurve的布尔变量,该变量确定算法是返回开曲线还是闭合曲线。由于我们取四个点(x1=当前点,x2=下一个点,但创建三条边还需要另外两个点。 x0=当前的前一个点,x3=下一个点),x0和x3点的选择将如下所示:
- 如果它是一条闭合曲线,如果x1是第一个点,那么x0将是最新的点(在此实现中,它是最新的,但是一个,因为最新的点与第一个点相同),如果x2是最新的点,那么x3将成为第一个点(以类似的方式, 在此实现中,将是第二点)。
- 如果它是一条开放曲线,则x0 = x1和x3 = x2对于前面的情况。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
namespace BezierCurveSample.View.Utils
{
public class InterpolationUtils
{
public class BezierCurveSegment
{
public Point StartPoint { get; set; }
public Point EndPoint { get; set; }
public Point FirstControlPoint { get; set; }
public Point SecondControlPoint { get; set; }
}
public static List<BezierCurveSegment>
InterpolatePointWithBezierCurves(List<Point> points, bool isClosedCurve)
{
if (points.Count < 3)
return null;
var toRet = new List<BezierCurveSegment>();
//if is close curve then add the first point at the end
if (isClosedCurve)
points.Add(points.First());
for (int i = 0; i < points.Count - 1; i++) //iterate for points
//but the last one
{
// Assume we need to calculate the control
// points between (x1,y1) and (x2,y2).
// Then x0,y0 - the previous vertex,
// x3,y3 - the next one.
double x1 = points[i].X;
double y1 = points[i].Y;
double x2 = points[i + 1].X;
double y2 = points[i + 1].Y;
double x0;
double y0;
if (i == 0) //if is first point
{
if (isClosedCurve)
{
var previousPoint = points[points.Count - 2]; //last Point,
//but one (due inserted the first at the end)
x0 = previousPoint.X;
y0 = previousPoint.Y;
}
else //Get some previouse point
{
var previousPoint = points[i];//if is the first point
//the previous one will be itself
x0 = previousPoint.X;
y0 = previousPoint.Y;
}
}
else
{
x0 = points[i - 1].X; //Previous Point
y0 = points[i - 1].Y;
}
double x3, y3;
if (i == points.Count - 2) //if is the last point
{
if (isClosedCurve)
{
var nextPoint = points[1]; //second Point(due inserted
//the first at the end)
x3 = nextPoint.X;
y3 = nextPoint.Y;
}
else //Get some next point
{
var nextPoint = points[i + 1]; //if is the last point
//the next point will be the last one
x3 = nextPoint.X;
y3 = nextPoint.Y;
}
}
else
{
x3 = points[i + 2].X; //Next Point
y3 = points[i + 2].Y;
}
double xc1 = (x0 + x1) / 2.0;
double yc1 = (y0 + y1) / 2.0;
double xc2 = (x1 + x2) / 2.0;
double yc2 = (y1 + y2) / 2.0;
double xc3 = (x2 + x3) / 2.0;
double yc3 = (y2 + y3) / 2.0;
double len1 = Math.Sqrt((x1 - x0) *
(x1 - x0) + (y1 - y0) * (y1 - y0));
double len2 = Math.Sqrt((x2 - x1) *
(x2 - x1) + (y2 - y1) * (y2 - y1));
double len3 = Math.Sqrt((x3 - x2) *
(x3 - x2) + (y3 - y2) * (y3 - y2));
double k1 = len1 / (len1 + len2);
double k2 = len2 / (len2 + len3);
double xm1 = xc1 + (xc2 - xc1) * k1;
double ym1 = yc1 + (yc2 - yc1) * k1;
double xm2 = xc2 + (xc3 - xc2) * k2;
double ym2 = yc2 + (yc3 - yc2) * k2;
const double smoothValue = 0.8;
// Resulting control points. Here smooth_value is mentioned
// above coefficient K whose value should be in range [0...1].
double ctrl1_x = xm1 + (xc2 - xm1) * smoothValue + x1 - xm1;
double ctrl1_y = ym1 + (yc2 - ym1) * smoothValue + y1 - ym1;
double ctrl2_x = xm2 + (xc2 - xm2) * smoothValue + x2 - xm2;
double ctrl2_y = ym2 + (yc2 - ym2) * smoothValue + y2 - ym2;
toRet.Add(new BezierCurveSegment
{
StartPoint = new Point(x1, y1),
EndPoint = new Point(x2, y2),
FirstControlPoint = i == 0 && !isClosedCurve ?
new Point(x1, y1) : new Point(ctrl1_x, ctrl1_y),
SecondControlPoint = i == points.Count - 2 &&
!isClosedCurve ? new Point(x2, y2) : new Point(ctrl2_x, ctrl2_y)
});
}
return toRet;
}
}
}
用户控件
我们提出的用户控件使用起来非常简单,并且适用于MVVM模式。
LandMarkControl只有两个依赖属性,一个用于点,另一个用于曲线的颜色。最重要的属性是Points附加属性。它是IEnumerable类型,它假定每个项都具X和Y属性。
如果点集合实现INotifyCollectionChanged接口,则控件将注册到CollectionChanged事件,如果每个点实现INotifyPropertyChanged接口,则控件也将注册到PropertyChanged事件。这样,每次添加或删除任何点或更改任何点的坐标时,控件都会刷新。
这是背后的完整用户控件代码:
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using BezierCurveSample.View.Utils;
namespace BezierCurveSample.View
{
/// <summary>
/// Interaction logic for LandmarkControl.xaml
/// </summary>
public partial class LandmarkControl : UserControl
{
#region Points
public IEnumerable Points
{
get { return (IEnumerable)GetValue(PointsProperty); }
set { SetValue(PointsProperty, value); }
}
// Using a DependencyProperty as the backing store for Points.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty PointsProperty =
DependencyProperty.Register("Points", typeof(IEnumerable),
typeof(LandmarkControl),
new PropertyMetadata(null, PropertyChangedCallback));
private static void PropertyChangedCallback(DependencyObject dependencyObject,
DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
var landmarkControl = dependencyObject as LandmarkControl;
if (landmarkControl == null)
return;
if (dependencyPropertyChangedEventArgs.NewValue is INotifyCollectionChanged)
{
(dependencyPropertyChangedEventArgs.NewValue as
INotifyCollectionChanged).CollectionChanged +=
landmarkControl.OnPointCollectionChanged;
landmarkControl.RegisterCollectionItemPropertyChanged
(dependencyPropertyChangedEventArgs.NewValue as IEnumerable);
}
if (dependencyPropertyChangedEventArgs.OldValue is INotifyCollectionChanged)
{
(dependencyPropertyChangedEventArgs.OldValue as
INotifyCollectionChanged).CollectionChanged -=
landmarkControl.OnPointCollectionChanged;
landmarkControl.UnRegisterCollectionItemPropertyChanged
(dependencyPropertyChangedEventArgs.OldValue as IEnumerable);
}
if (dependencyPropertyChangedEventArgs.NewValue != null)
landmarkControl.SetPathData();
}
#endregion
#region PathColor
public Brush PathColor
{
get { return (Brush)GetValue(PathColorProperty); }
set { SetValue(PathColorProperty, value); }
}
// Using a DependencyProperty as the backing store for PathColor.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty PathColorProperty =
DependencyProperty.Register("PathColor", typeof(Brush),
typeof(LandmarkControl),
new PropertyMetadata(Brushes.Black));
#endregion
#region IsClosedCurve
public static readonly DependencyProperty IsClosedCurveProperty =
DependencyProperty.Register("IsClosedCurve", typeof (bool),
typeof (LandmarkControl),
new PropertyMetadata(default(bool),
OnIsClosedCurveChanged));
private static void OnIsClosedCurveChanged
(DependencyObject dependencyObject,
DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
var landmarkControl = dependencyObject as LandmarkControl;
if (landmarkControl == null)
return;
landmarkControl.SetPathData();
}
public bool IsClosedCurve
{
get { return (bool) GetValue(IsClosedCurveProperty); }
set { SetValue(IsClosedCurveProperty, value); }
}
#endregion
public LandmarkControl()
{
InitializeComponent();
}
void SetPathData()
{
if (Points == null) return;
var points = new List<Point>();
foreach (var point in Points)
{
var pointProperties = point.GetType().GetProperties();
if (pointProperties.All(p => p.Name != "X") ||
pointProperties.All(p => p.Name != "Y"))
continue;
var x = (float)point.GetType().GetProperty("X").GetValue
(point, new object[] { });
var y = (float)point.GetType().GetProperty("Y").GetValue
(point, new object[] { });
points.Add(new Point(x, y));
}
if (points.Count <= 1)
return;
var myPathFigure = new PathFigure { StartPoint = points.FirstOrDefault() };
var myPathSegmentCollection = new PathSegmentCollection();
var bezierSegments = InterpolationUtils.InterpolatePointWithBezierCurves
(points, IsClosedCurve);
if (bezierSegments == null || bezierSegments.Count < 1)
{
//Add a line segment <this is generic for more than one line>
foreach (var point in points.GetRange(1, points.Count - 1))
{
var myLineSegment = new LineSegment { Point = point };
myPathSegmentCollection.Add(myLineSegment);
}
}
else
{
foreach (var bezierCurveSegment in bezierSegments)
{
var segment = new BezierSegment
{
Point1 = bezierCurveSegment.FirstControlPoint,
Point2 = bezierCurveSegment.SecondControlPoint,
Point3 = bezerCurveSegment.EndPoint
};
myPathSegmentCollection.Add(segment);
}
}
myPathFigure.Segments = myPathSegmentCollection;
var myPathFigureCollection = new PathFigureCollection {myPathFigure} ;
var myPathGeometry = new PathGeometry { Figures = myPathFigureCollection };
path.Data = myPathGeometry;
}
private void RegisterCollectionItemPropertyChanged(IEnumerable collection)
{
if (collection == null)
return;
foreach (INotifyPropertyChanged point in collection)
point.PropertyChanged += OnPointPropertyChanged;
}
private void UnRegisterCollectionItemPropertyChanged(IEnumerable collection)
{
if (collection == null)
return;
foreach (INotifyPropertyChanged point in collection)
point.PropertyChanged -= OnPointPropertyChanged;
}
private void OnPointCollectionChanged(object sender,
NotifyCollectionChangedEventArgs e)
{
RegisterCollectionItemPropertyChanged(e.NewItems);
UnRegisterCollectionItemPropertyChanged(e.OldItems);
SetPathData();
}
private void OnPointPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "X" || e.PropertyName == "Y")
SetPathData();
}
}
}
这是XAML代码:
<UserControl x:Class="BezierCurveSample.View.LandmarkControl"
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"
mc:Ignorable="d"
x:Name="UserControl"
d:DesignHeight="300" d:DesignWidth="300">
<Path x:Name="path" Stroke="{Binding PathColor,
ElementName=UserControl}" StrokeThickness="1"/>
</UserControl>
使用示例
使用控件为LandMarkViewModel创建数据模版:
<DataTemplate DataType="{x:Type ViewModel:LandmarkViewModel}">
<PointInterpolation.View:LandmarkControl x:Name="control"
Points="{Binding LandmarkPoints}" Visibility="{Binding IsVisible,
Converter={StaticResource BoolToVisibilityConverter}}" ToolTip="{Binding Label}"/>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<Setter Property="PathColor" TargetName="control" Value="Red"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
现在到处显示一个LandMarkViewModel,此数据模板都会将项目显示为LandMarkControl。它需要呈现在Canvas上:
<ListBox x:Name="landMarks" ItemsSource="{Binding Landmarks}">
<ListBox.Template>
<ControlTemplate>
<Canvas IsItemsHost="True"/>
</ControlTemplate>
</ListBox.Template>
</ListBox>
这是最后一个图像示例:
引用
https://www.codeproject.com/Articles/769055/Interpolate-2D-Points-Using-Bezier-Curves-in-WPF