多段Bazier(贝塞尔曲线)、BSpline(B样条曲线)的绘制与鼠标交互控制

我们的目标是类似 sketch 的曲线点编辑功能。

 

1. 分析

如图,这是一个3个点的曲线,每个点都提供了两个控制把柄,可以拖拽其编辑曲线。

(本来想传gif动画,但太大了传不上去)

要实现该逻辑,我们有以下几个任务

  • 定义曲线点
  • 定义曲线
  • 绘制曲线
  • 绘制控制把柄
  • 与鼠标交互,可拖拽曲线点及控制把柄

2. 曲线的绘制

绘制曲线段很容易实现,用GDI+ 的 Graphics.DrawBazier() 方法就行了。其原型是:

// p1、p2 是曲线段终点,ctrl1和ctrl2是两个控制点
void DrawBazier(Point p1, Point ctl1, Point ctrl2, Point p2);

一段Bazier(B样条)曲线由2个端点,2个控制点实现。以下为简图:

(p1, ctrl1)--------(p2, ctrl2)

而多段段曲线无非就是把这些曲线连接起来。要实现这个逻辑,每个曲线点就需要两个控制点,一个是绘制前面的曲线段用的,一个是绘制后面的曲线段用的。以3个点的曲线为例,其简图如下:

(p1, p1.NextCtrl)--------(p2, p2.PrevCtrl)(p2, p2.NextCtrl)--------(p3,p3.PrevCtrl)

那么我们可以定义曲线的点结构体为:

    public class BPoint
    {
        /// <summary>曲线上的点</summary>
        public PointF Point { get; set; }

        /// <summary>前控制点</summary>
        public PointF PrevCtrl { get; set; }

        /// <summary>后控制点</summary>
        public PointF NextCtrl { get; set; }
    }

而曲线的定义也很简单:

public class BSpline
{
    public List<BPoint> Points { get; set; }

    // 绘制
    public void Draw(Graphics g){}

    // 点击判断
    public void HitTest(PointF point) {}
}

根据前面的分析,我们可以很容易的遍历曲线点,然后把曲线给画出来。

 

3. 用鼠标与曲线点交互

要实现该目标,我们分解一下任务

  • 鼠标按下后,对曲线进行点击判断(HitTest),判断是哪个部分被点中了,记录下来,并重绘曲线。
  • 拖动鼠标时,将被点中的部分移动到光标位置上。重绘曲线。
  • 绘制曲线时,根据点中的部分,绘制相应的控制句柄及连线。

涉及到的技术点是:

  • 如何判断点被点中:这个容易,让点膨胀成矩形,判断是否容纳了光标就行。
  • 如何判断曲线被点中:用Path对象,添加曲线,并膨胀一下,转化为Region,判断Region是否容纳了光标。
  • 如何不闪烁:用双缓冲即可。

中间过程懒得写(其实是没留档),看代码吧:

点的代码:

    public class BPoint
    {
        /// <summary>曲线上的点</summary>
        public PointF Point { get; set; }

        /// <summary>前控制点</summary>
        public PointF PrevCtrl { get; set; }

        /// <summary>后控制点</summary>
        public PointF NextCtrl { get; set; }

        /// <summary>选中的部分</summary>
        public BPointPart HitPart { get; set; } = BPointPart.None;

        /// <summary>构造函数</summary>
        public BPoint(PointF point, PointF prevCtrl, PointF nextCtrl)
        {
            this.Point = point;
            this.PrevCtrl = prevCtrl;
            this.NextCtrl = nextCtrl;
        }

        /// <summary>测试点击的位置(并设置SelectedPart值)</summary>
        /// <param name="ctrls">需要测试的控制部件</param>
        public void HitTest(PointF p, BPointPart ctrls=BPointPart.PrevCtrl | BPointPart.NextCtrl)
        {
            this.HitPart = BPointPart.None;
            if (HitPoint(this.Point, p)) 
                this.HitPart = BPointPart.Point;

            if (ctrls.HasFlag(BPointPart.PrevCtrl) && HitPoint(this.PrevCtrl, p))      
                this.HitPart = BPointPart.PrevCtrl;

            if (ctrls.HasFlag(BPointPart.NextCtrl) && HitPoint(this.NextCtrl, p)) 
                this.HitPart = BPointPart.NextCtrl;
        }

        /// <summary>测试点是否被点中</summary>
        /// <param name="target">目标点</param>
        /// <param name="cursor">光标点</param>
        static bool HitPoint(PointF target, PointF cursor)
        {
            var rect = new RectangleF(target, SizeF.Empty);
            rect.Inflate(5, 5);
            return rect.Contains(cursor);
        }

        /// <summary>绘制贝塞尔曲线控制点</summary>
        /// <param name="ctrls">要显示的控制点</param>
        public void Draw(Graphics g, BPointPart ctrls = BPointPart.PrevCtrl | BPointPart.NextCtrl)
        {
            var s = 4;  // 点宽度
            var pen = new Pen(Color.Black);
            if (HitPart == BPointPart.None)
            {
                Painter.DrawDot(g, this.Point, s, Color.Black, false);
            }
            else
            {
                if (ctrls.HasFlag(BPointPart.PrevCtrl))
                {
                    g.DrawLine(pen, this.Point, this.PrevCtrl);
                    Painter.DrawDot(g, this.PrevCtrl, s, Color.Black, HitPart == BPointPart.PrevCtrl);
                }
                if (ctrls.HasFlag(BPointPart.NextCtrl))
                {
                    g.DrawLine(pen, this.Point, this.NextCtrl);
                    Painter.DrawDot(g, this.NextCtrl, s, Color.Black, HitPart == BPointPart.NextCtrl);
                }
                Painter.DrawDot(g, this.Point, s, Color.Black, HitPart == BPointPart.Point);
            }
        }
}

    /// <summary>B样条曲线点的组成部分(点击判断时用)</summary>
    [Flags]
    public enum BPointPart
    {
        None = 0,
        Point = 1,
        PrevCtrl = 2,
        NextCtrl = 4,
    }

 

曲线的代码:

    /// <summary>
    /// B 样条曲线
    /// </summary>
    public class BSpline
    {
        /// <summary>包含的曲线点</summary>
        public List<BPoint> Points { get; set; }

        /// <summary>选中的线段编号</summary>
        public int SelectedSeg { get; set; } = -1;

        /// <summary>创建新 B 样条曲线</summary>
        public BSpline(List<BPoint> points)
        {
            this.Points = points;
        }

        /// <summary>点击测试(会设置每个点被选中的部件、被选中的曲线段)</summary>
        public void HitTest(PointF point)
        {
            // 测试点
            foreach (var p in Points)
                p.HitTest(point);
            Points.First().HitTest(point, BPointPart.NextCtrl);
            Points.Last().HitTest(point, BPointPart.PrevCtrl);

            // 测试曲线段
            this.SelectedSeg = HitTestSeg(point);
        }

        /// <summary>测试点中了哪条曲线</summary>
        int HitTestSeg(PointF point)
        {
            for (int i = 0; i < Points.Count - 1; i++)
            {
                var p1 = Points[i];
                var p2 = Points[i + 1];
                var path = new GraphicsPath();
                path.AddBezier(p1.Point, p1.NextCtrl, p2.PrevCtrl, p2.Point);
                path.Widen(new Pen(Color.Black, 4));
                var region = new Region(path);
                if (region.IsVisible(point))
                    return i;
            }
            return -1;
        }

        /// <summary>绘制</summary>
        public void Draw(Graphics g, Pen penNormal, Pen penSelected)
        {
            // 绘制曲线
            for (int i = 0; i < Points.Count - 1; i++)
            {
                var p1 = Points[i];
                var p2 = Points[i + 1];
                var pen = (SelectedSeg == i) ? penSelected : penNormal;
                g.DrawBezier(pen, p1.Point, p1.NextCtrl, p2.PrevCtrl, p2.Point);
            }

            // 绘制点(包括句柄)
            for (int i = 0; i < Points.Count; i++)
            {
                var p = Points[i];
                if (i == 0)
                    p.Draw(g, BPointPart.NextCtrl);   // 第一个点只绘制后句柄
                else if (i == Points.Count - 1)
                    p.Draw(g, BPointPart.PrevCtrl);   // 最后一个点只绘制前句柄
                else
                    p.Draw(g);
            }

        }
    }

窗体交互的代码:

public partial class Form1 : Form
    {
        BSpline _curve =  new BSpline(new List<BPoint>()
        {
            new BPoint(new PointF(50, 50),     new PointF(50, 50),    new PointF(150, 50)),
            new BPoint(new PointF(100, 100),   new PointF(50, 100),   new PointF(150, 100)),
            new BPoint(new PointF(200, 200),   new PointF(150, 200),  new PointF(200, 200)),
        });


        public Form1()
        {
            InitializeComponent();
            SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.DoubleBuffer, true);  // 双缓冲绘图
        }

        /// <summary>根据贝塞尔曲线数据绘制曲线</summary>
        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            var g = e.Graphics;
            g.SmoothingMode = SmoothingMode.AntiAlias;
            var pen1 = new Pen(Color.Black);
            var pen2 = new Pen(Color.Blue);
            _curve.Draw(g, pen1, pen2);
        }

        /// <summary>鼠标按下时,测试点击位置并记录下来</summary>
        private void Form1_MouseDown(object sender, MouseEventArgs e)
        {
            _curve.HitTest(e.Location);
            Refresh();
        }

        /// <summary>鼠标按下并移动时,设置选中的控制点位置等于当前光标位置</summary>
        private void Form1_MouseMove(object sender, MouseEventArgs e)
        {
            if (e.Button != MouseButtons.Left)
                return;

            // 拖动中心点、前控制点、后控制点
            foreach (var p in _curve.Points)
            {
                if (p.HitPart == BPointPart.None)
                    continue;
                if (p.HitPart == BPointPart.Point)
                {
                    var dx = e.Location.X - p.Point.X;
                    var dy = e.Location.Y - p.Point.Y;
                    p.Point = e.Location;
                    p.PrevCtrl = new PointF(p.PrevCtrl.X + dx, p.PrevCtrl.Y + dy);
                    p.NextCtrl = new PointF(p.NextCtrl.X + dx, p.NextCtrl.Y + dy);
                }
                else if (p.HitPart == BPointPart.PrevCtrl) 
                    p.PrevCtrl = e.Location;
                else if (p.HitPart == BPointPart.NextCtrl) 
                    p.NextCtrl = e.Location;
            }

            Refresh();
        }

        /// <summary>鼠标提起时</summary>
        private void Form1_MouseUp(object sender, MouseEventArgs e)
        {
            Refresh();
        }
    }

这是源代码:https://download.csdn.net/download/surfsky/12654549
转载请标明出处:https://blog.csdn.net/surfsky/article/details/107533869

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
贝塞尔曲线是一种经典的曲线绘制方法,在MATLAB中可以使用bezier函数来实现。贝塞尔曲线由若干个控制点决定,通过调整控制点的位置和数量,可以得到不同形状的曲线。具体操作步骤如下: 1.确定控制点的坐标:首先需要确定贝塞尔曲线控制点,可以自行设置控制点的坐标。 2.使用bezier函数绘制曲线:将控制点的坐标作为输入,使用bezier函数绘制贝塞尔曲线。该函数返回一个包含曲线上离散点坐标的向量,可以通过plot函数将这些点连接起来形成曲线b样条曲线是一种平滑的曲线绘制方法,在MATLAB中可以使用splinetool来实现。B样条曲线由若干个节点和控制点决定,通过调整节点和控制点的位置和数量,可以得到不同形状的曲线。具体操作步骤如下: 1.打开MATLAB的Spline工具箱:在MATLAB的命令窗口输入splinetool,打开Spline工具箱。 2.确定节点的位置:在工具箱的界面中,可以通过鼠标在图形区域上点击来设定节点的位置,可以自行调整节点的数量和位置。 3.确定控制点的位置:在工具箱的界面中,可以通过在图形区域上点击来确定控制点的位置,可以自行调整控制点的数量和位置。 4.在工具箱中点击“生成”按钮,可以得到生成的B样条曲线。 5.使用plot函数绘制曲线:将B样条曲线的坐标作为输入,使用plot函数绘制曲线。 通过以上步骤,我们可以用MATLAB绘制贝塞尔曲线B样条曲线

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

土豆湿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值