在WPF中使用贝塞尔曲线插值2D点

目录

介绍

背景

背后的算法

用于计算的类

用户控件

使用示例

引用


介绍

插值点有时是一项艰巨的数学工作,如果点是有序的,则更是如此。解决方案是使用点并使用表示时间维度的额外参数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类具有定义样条线段的四个属性:StartPointEndPointFirstControlPointSecondControlPoint

由于上述算法最初是针对闭合曲线实现的,并且希望它也可以应用于开放曲线,因此需要进行一些更改。因此,该InterpolatePointWithBezierCurves方法接收第二个参数,即名为isClosedCurve的布尔变量,该变量确定算法是返回开曲线还是闭合曲线。由于我们取四个点(x1=当前点,x2=下一个点,但创建三条边还需要另外两个点。 x0=当前的前一个点,x3=下一个点),x0x3的选择将如下所示:

  • 如果它是一条闭合曲线,如果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类型,它假定每个项都具XY属性。

如果点集合实现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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值