鼠标操作贝塞尔曲线

1 贝塞尔曲线

贝塞尔曲线(The Bézier Curves),是一种在计算机图形学中相当重要的参数曲线(三维空间中称为贝塞尔曲面). 贝塞尔曲线由法国工程师皮埃尔·贝塞尔(Pierre Bézier)于1962年发表,他运用贝塞尔曲线来为汽车的主体进行设计.

Pierre Bézier

接下来将从一次贝塞尔曲线开始(以下简称一次曲线. 类似的, N 次贝塞尔曲线称为 N 次曲线),研究贝赛尔曲线的解析构造和原理。

1.1 一次贝塞尔曲线

给定两点 P0 P1 ,一次曲线在几何上是一条连接这两点的直线,可以用下面的参数方程表示:

B(t)=P0+(P1P0)t=(1t)P0+tP1

其中, t[0,1] .

当参数 t 在定义域内变化时,曲线变化过程如下:

一次贝塞尔曲线

一次贝塞尔曲线函数中,参数 t 会经过由 P0 14P0P1 的直线.

1.2 二次贝塞尔曲线

二次曲线的路径由给定点 P0 P1 P2 的函数 B(t) 确定:

B(t)=(1t)2P0+2t(1t)P1+t2P2

其中, t[0,1] .

为了生成二次曲线,可以借助于中间点 Q0 Q1

  • P0 P1 的连续点 Q0 ,描述一条一次曲线
  • P1 P2 的连续点 Q1 ,描述一条一次曲线
  • Q0 Q1 的连续点 B(t) ,描述一条二次曲线

t=0.25 为例,二次曲线如图:

t=0.25时的二次曲线

当参数 t 在定义域内变化时,其变化过程如图:

二次曲线变化

1.3 三次贝塞尔曲线

为构造高次曲线,需要借助更多的中间点. 对于三次曲线,可由一次曲线描述的中间点 Q0 Q1 Q2 ,以及由二次曲线描述的点 R0 R1 来生成.

P0 P1 P2 P3 四个点定义了一条三次方贝塞尔曲线. 曲线从 P0 开始,至 P3 结束,其方向从 P1 P2 . 一般来说,曲线不会经过 P1 P2 . P0 P1 之间的间距,决定了曲线在转而趋进 P3 之前,朝 P2 方向行走的长度.
曲线的参数形式为:

B(t)=P0(1t)3+3P1t(1t)2+3P2t2(1t)+P3t3

其中, t[0,1] .

t=0.25 为例,三次曲线如图:

t=0.25时的三次曲线

当参数 t 在定义域内变化时,其变化过程如图:

二次曲线变化

1.4 高次曲线

如果用 BP0P1Pn 表示由 P0P1Pn 决定的贝塞尔曲线,那么高次曲线可以表示为:

B(t)=BP0P1Pn(t)=(1t)BP0P1Pn(t)+tBP0P1Pn(t)

2 实现

2.1 计算机绘图

要“画”出贝塞尔曲线,一般使用逐次逼近方式,需要进行较多的计算,然后绘制出来,类似这样:

绘制曲线

绘制的代码可以在各类计算机图形学书籍中找到.

2.2 在GDI+中实现

GDI+已经封装好了贝塞尔曲线的绘制代码,如果你想画出贝塞尔曲线,调用Graphics.DrawBezier方法:

public void DrawBezier(Pen pen, Point pt1, Point ctrlPt1, Point pt2, Point ctrlPt2);
 
 
  • 1
  • 1

这是一个三次贝塞尔曲线,其中4个点分别为:起点,起点控制点,终点,终点控制点. 绘制出来的效果如下:

GDIplus

3 与鼠标交互

怎么实现Photoshop里那样可以调整的贝塞尔曲线呢?两种方法:

  1. 输入新参数生成曲线
  2. 用鼠标交互调整曲线

很显然第二种看起来比较好,那么就来试试看.
例如想要获得是这样的效果:

带控制柄的三次贝塞尔曲线

这是一条三次贝塞尔曲线,图中各点含义为:

三次曲线含义

几个需要解决的问题:

  1. 表示贝塞尔曲线的锚点和控制点
  2. 绘制曲线和控制点/控制柄
  3. 鼠标交互

接下来分别解决。.

3.1 锚点

对于三次贝塞尔曲线而言,有两个锚点:起始点结束点. 每个锚点有两个坐标:本身坐标和控制点坐标. 于是,可以用这样的类来描述:

// Anchor
public class AnchorPoint {
    public static AnchorPoint Empty { get; }
    public Point Point { get; set; }
    public Point ControlPoint { get; set; }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

3.2 三次贝塞尔曲线

三次曲线前面已经说过原理了,它的结构应该就是这样的:

public class BezierSegment {
    public AnchorPoint Begin;
    public AnchorPoint End;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

3.3 一个简单的渲染器

为了简单,就用一个最基本的渲染器来画,代码如下:

public class BezierRenderer {
    public void DrawSegment(Graphics g, BezierSegment seg, Color color) {
    using (Pen p = new Pen(color)) {
        g.DrawBezier(
            p,
            seg.Begin.Point,
            seg.Begin.ControlPoint,
            seg.End.Point,
            seg.End.ControlPoint);
    }
    public void DrawHandle(Graphics g, Point pt, bool solid, Color color) {
        if (solid)
            using (SolidBrush b = new SolidBrush(color))
                g.FillRectangle(b, pt.X - 2, pt.Y - 2, 4, 4);
        else
            using (Pen p = new Pen(color))
                g.DrawRectangle(b, pt.X - 2, pt.Y - 2, 4, 4);
    }
    public void DrawBar(Graphics g, Point pt1, Point pt2, Color color) {
        using (Pen p = new Pen(color))
            g.DrawLine(p, pt1, pt2);
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

绘制曲线和控制点及其手柄:

// 画曲线
renderer.DrawSegment(g, seg, Color.DimGray);

// 画锚点
renderer.DrawHandle(g, seg.Begin.Point, false, Color.DimGray);
renderer.DrawHandle(g, seg.End.Point, false, Color.DimGray);

// 画控制柄
renderer.DrawBar(g, seg.Begin.Point, seg.Begin.ControlPoint, Color.Black);
renderer.DrawHandle(g, seg.Begin.ControlPoint, false, Color.Black);
renderer.DrawBar(g, seg.End.Point, seg.End.ControlPoint, Color.Black);
renderer.DrawHandle(g, seg.End.ControlPoint, false, Color.Black);

// 当前控制柄为实心
if (target != null)
    renderer.DrawHandle(g, target.ControlPoint, true, Color.Black);
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

画出来的效果如下:

贝塞尔效果

3.4 鼠标交互

和鼠标实际上是这样交互的:

  1. 按下鼠标,如果落点在某个控制点上,就表示选中了该点,否则落空
  2. 移动鼠标,如果选中了某个控制点,则调整控制点坐标至新坐标,否则忽略
  3. 放开鼠标,取消任何选择

看起来只需要处理MouseDownMouseMoveMouseUp这三个事件就够了.

先弄一个全局标记:

// 热点检测区
Rectangle rectBegin, rectEnd;

// 激活标识
AnchorPoint target = null;
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

然后处理鼠标事件.

3.4.1 MouseDown事件
if (rectBegin.Contains(e.Location))
    target = seg.Begin;
else if (rectEnd.Contains(e.Location))
    target = seg.End;
refresh();
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5
3.4.2 MouseMove事件
if (target == null) return;
if (!this.ClientRectangle.Contains(e.Location)) return;
target.ControlPoint = e.Location;
refresh();
 
 
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4
3.4.3 MouseUp事件
target = null;
refresh();
 
 
  • 1
  • 2
  • 1
  • 2
3.4.4 refresh()方法

简便起见,refresh()方法只作简单的刷新:

Invalidate();
 
 
  • 1
  • 1

更高效的刷新应该只处理脏区.

3.5 效果

最后的效果如图:

绘制效果图

修改渲染器,可以得到更多的图像效果:

其他效果

4 扩展和优化

要实现Photoshop那种曲线效果,需要多个锚点连接起来,用和本文类似的方法来画。上面的方法在效率上也还有可以提高的地方。

(完)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值