带控制手柄的贝塞尔曲线控件——历史、原理、实现

© 2013-2023 Conmajia
Updated on 9th March, 2023
Initiated on 22nd February, 2015

摘要 本文介绍贝塞尔曲线历史和基本原理,并以 .NET Frameworks GDI+ 为例简单实现了一个带控制手柄和可扩展样式的曲线控件.

1 简介

绘图软件中常常用到带控制手柄的曲线,用于精确调整连续变化的数值. 贝塞尔曲线源自一族广泛应用于工程 CAD 和计算机图形学的曲线参数方程,由法国雷诺汽车公司工程师皮埃尔·艾蒂安·贝塞尔(Pierre Étienne Bézier,1910-1999)于 1962 年提出,并基于此开发了 UNISURF 建模系统(1968). 有关贝塞尔函数的详细内容,可以参阅贝塞尔本人撰写的《UNISURF CAD 系统的数学基础》一书(Butterworths,1986,56页).

1.1 贝塞尔函数

贝塞尔函数是一族形为

x 2 y ′ ′ + x y ′ + ( x 2 − n 2 ) y = 0 x^2y''+xy'+\left(x^2-n^2\right)y=0 x2y′′+xy+(x2n2)y=0

的函数,其中 n n n 为阶数。该函数族的解为

y ( x ) = A J n ( x ) + B Y n ( x ) , y(x)=AJ_n(x)+BY_n(x), y(x)=AJn(x)+BYn(x),

其中 J n ( x ) J_n(x) Jn(x) 称为第一类贝塞尔函数 Y n ( x ) Y_n(x) Yn(x) 称为第二类贝塞尔函数.

贝塞尔函数可以看作一族衰减的正弦函数,如图 1 所示,其中 J 0 ( 0 ) = 1 J_0(0)=1 J0(0)=1 Y n ( 0 ) = ∞ Y_n(0)=\infty Yn(0)=.

图 1. 第一类、第二类贝塞尔函数的图形

利用弗罗贝尼乌斯(Frobenius)方法求解可得,

J n ( x ) = ∑ k = 0 ∞ ( − 1 ) k k ! ( n + k ) ! ( x 2 ) n + 2 k Y n ( x ) = 2 π J n ( x ) [ ln ⁡ x 2 + γ ] − 1 π ∑ k = 0 n − 1 ( n − k − 1 ) ! k ! ( x 2 ) 2 k − n − 1 π ∑ k = 0 ∞ ( − 1 ) k [ Φ ( k ) + Φ ( n + k ) ] 1 k ! ( n + k ) ! ( x 2 ) 2 k + n , J_n(x)=\sum_{k=0}^\infty\frac{(-1)^k}{k!(n+k)!}\left(\frac{x}{2}\right)^{n+2k}\\ \begin{aligned} Y_n(x)=&\frac{2}{\pi}J_n(x)\left[\ln\frac{x}{2}+\gamma\right]-\frac{1}{\pi}\sum_{k=0}^{n-1}\frac{(n-k-1)!}{k!}\left(\frac{x}{2}\right)^{2k-n}\\ &-\frac{1}{\pi}\sum_{k=0}^\infty(-1)^k\left[\Phi(k)+\Phi(n+k)\right]\frac{1}{k!(n+k)!}\left(\frac{x}{2}\right)^{2k+n}, \end{aligned} Jn(x)=k=0k!(n+k)!(1)k(2x)n+2kYn(x)=π2Jn(x)[ln2x+γ]π1k=0n1k!(nk1)!(2x)2knπ1k=0(1)k[Φ(k)+Φ(n+k)]k!(n+k)!1(2x)2k+n,

其中 γ = . 0.577216 \gamma\stackrel{.}{=}0.577216 γ=.0.577216 Φ ( p ) = 1 + 1 2 + 1 3 + ⋯ + 1 p \Phi(p)=1+\dfrac{1}{2}+\dfrac{1}{3}+\cdots+\dfrac{1}{p} Φ(p)=1+21+31++p1 p > 0 p>0 p>0 Φ ( 0 ) = 0 \Phi(0)=0 Φ(0)=0.

于是可得当 x → ∞ x\to\infty x 时贝塞尔函数的渐近线方程近似为

J n ( x ) ∼ 2 π x cos ⁡ ( x − n π 2 − π 4 ) Y n ( x ) ∼ 2 π x sin ⁡ ( x − n π 2 − π 4 ) . \begin{aligned} J_n(x)&\sim\sqrt\frac{2}{\pi x}\cos\left(x-\frac{n\pi}{2}-\frac{\pi}{4}\right)\\ Y_n(x)&\sim\sqrt\frac{2}{\pi x}\sin\left(x-\frac{n\pi}{2}-\frac{\pi}{4}\right). \end{aligned} Jn(x)Yn(x)πx2 cos(x24π)πx2 sin(x24π).

另有工程上常用的汉可尔函数(Hankel),也被称为第三类贝塞尔函数

H n ( 1 ) ( x ) ≡ J n ( x ) + j Y n ( x ) H n ( 2 ) ( x ) ≡ J n ( x ) − j Y n ( x ) , \begin{aligned} H_n^{(1)}(x)&\equiv J_n(x)+jY_n(x)\\ H_n^{(2)}(x)&\equiv J_n(x)-jY_n(x), \end{aligned} Hn(1)(x)Hn(2)(x)Jn(x)+jYn(x)Jn(x)jYn(x),

其中上标 ( 1 ) ^{(1)} (1) ( 2 ) ^{(2)} (2) 分别针对第一、二类贝塞尔函数. 当 x → ∞ x\to\infty x 时,

H n ( 1 ) ( x ) ∼ 2 π x e + j ( x − n π 2 − π 4 ) H n ( 2 ) ( x ) ∼ 2 π x e − j ( x − n π 2 − π 4 ) . \begin{aligned} H_n^{(1)}(x)&\sim\sqrt\frac{2}{\pi x}e^{+j\left(x-\frac{n\pi}{2}-\frac{\pi}{4}\right)}\\ H_n^{(2)}(x)&\sim\sqrt\frac{2}{\pi x}e^{-j\left(x-\frac{n\pi}{2}-\frac{\pi}{4}\right)}. \end{aligned} Hn(1)(x)Hn(2)(x)πx2 e+j(x24π)πx2 ej(x24π).

第三类贝塞尔函数常用于处理多坐标系的工程或图形学问题.

1.2 贝塞尔曲线

贝塞尔曲线的数学基础实际上源自 1912 年发表的伯恩斯坦多项式,即以前苏联数学家谢尔盖·纳塔诺维奇·伯恩斯坦(Серге́й Ната́нович Бернште́йн,1880-1968)命名的一族基底多项式及其线性组合. 最早计算伯恩斯坦多项式的数值方法由法国雪铁龙公司工程师保尔·德·卡斯特里奥(Paul de Casteljau,1930-2022)于 1959 年提出. 因该方法被雪铁龙注册为专利,其主要内容直到 1980 年代才公开,故如今形如 B ( t ) = ∑ i = 0 n β i b i , n ( t ) B(t)=\sum_{i=0}^n\beta_i b_{i,n}(t) B(t)=i=0nβibi,n(t) 的伯恩斯坦形式曲线以最先公开的贝塞尔命名,称为贝塞尔曲线. 式中 β i \beta_i βi 为第 i i i 个控制点,

b i , n ( t ) = ( n i ) ( 1 − t ) n − i t i , b_{i,n}(t)=\binom{n}{i}(1-t)^{n-i}t^i, bi,n(t)=(in)(1t)niti,

为伯恩斯坦基底多项式.

1.2.1 德·卡斯特里奥算法

德·卡斯特里奥算法实际上将单个伯恩斯坦函数分解为两个贝塞尔曲线, t 0 t_0 t0 处的曲线可用如下递归关系求得,

β i ( 0 ) ≡ β i ,    i = 0 , 1 , ⋯   , n β i ( j ) ≡ β i ( j − 1 ) ( 1 − t 0 ) + β i + 1 ( j − 1 ) t 0 , \begin{aligned} \beta_i^{(0)}&\equiv\beta_i,\;i=0,1,\cdots,n \\ \beta_i^{(j)}&\equiv\beta_i^{(j-1)}(1-t_0)+\beta_{i+1}^{(j-1)}t_0, \end{aligned} βi(0)βi(j)βi,i=0,1,,nβi(j1)(1t0)+βi+1(j1)t0,

其中,对 β i ( j ) \beta_i^{(j)} βi(j),有 i = 0 , 1 , ⋯   , n − j i=0,1,\cdots,n-j i=0,1,,nj j = 1 , 2 , ⋯   , n j=1,2,\cdots,n j=1,2,,n. 此时 t 0 t_0 t0 处的贝塞尔曲线 B ( t 0 ) = β 0 ( n ) B(t_0)=\beta_0^{(n)} B(t0)=β0(n) ,且可分解为

β 0 ( 0 ) , β 0 ( 1 ) , β 0 ( 2 ) , ⋯   , β 0 ( n ) β 0 ( n ) , β 1 ( n − 1 ) , β 2 ( n − 2 ) , ⋯   , β n ( 0 ) . \begin{aligned} &\beta_0^{(0)},\beta_0^{(1)},\beta_0^{(2)},\cdots,\beta_0^{(n)}\\ &\beta_0^{(n)},\beta_1^{(n-1)},\beta_2^{(n-2)},\cdots,\beta_n^{(0)}. \end{aligned} β0(0),β0(1),β0(2),,β0(n)β0(n),β1(n1),β2(n2),,βn(0).

德·卡斯特里奥算法的优点是具有数值稳定的特性,缺点是相较于直接计算效率偏低. 以下是使用 Python 语言简单实现的德·卡斯特里奥算法,供读者参考1.

def de_casteljau(t, coefs):
    beta = [c for c in coefs] # 此列表中的数据将于后续计算中覆盖
    n = len(beta)
    for j in range(1, n):
        for k in range(n - j):
            beta[k] = beta[k] * (1 - t) + beta[k + 1] * t
    return beta[0]

1.2.2 贝塞尔曲线2

如前所述, n n n 阶贝塞尔曲线通式为

B ( t ) = ∑ i = 0 n β i b i , n ( t ) , B(t)=\sum_{i=0}^n\beta_i b_{i,n}(t), B(t)=i=0nβibi,n(t),

其中 t ∈ [ 0 , 1 ] t\in[0,1] t[0,1]. 显然,当 n = 1 n=1 n=1 时,可得一次贝塞尔曲线

B ( t ) = β 0 + ( β 1 − β 0 ) t = ( 1 − t ) β 0 + t β 1 , \begin{aligned} B(t)&=\beta_0+(\beta_1-\beta_0)t\\ &=(1-t)\beta_0+t\beta_1, \end{aligned} B(t)=β0+(β1β0)t=(1t)β0+tβ1,

其图形为端点自 β 0 \beta_0 β0 运动至 β 1 \beta_1 β1 的直线,如图 2 所示,也被称为线性贝塞尔曲线. 注意图中 β i \beta_i βi 均用 P i P_i Pi 表示,下同.

图 2. 一次贝塞尔曲线

n = 2 n=2 n=2 时, B ( t ) = ( 1 − t ) 2 β 0 + 2 t ( 1 − t ) β 1 + t 2 β 2 B(t)=(1-t)^2\beta_0+2t(1-t)\beta_1+t^2\beta_2 B(t)=(1t)2β0+2t(1t)β1+t2β2 为一动点 Q 0 Q_0 Q0 Q 1 Q_1 Q1——也称为中介点——自 β 1 \beta_1 β1 运动至 β 2 \beta_2 β2 形成,如图 3 所示.

图 3. 二次贝塞尔曲线

计算机系统中常用的 TrueType 字体即采用了二次贝塞尔曲线实现.

n = 3 n=3 n=3 或更高时,为构建 n n n 次贝塞尔曲线,需要引入 ( n n − 1 ) \dbinom{n}{n-1} (n1n) 个中介点. 图 4 演示了三次、四次贝塞尔曲线构建过程.

图 4. (甲)三次贝塞尔曲线;(乙)四次贝塞尔曲线

以上图形及动画均由德·卡斯特里奥算法实现.

2 设计基于贝塞尔曲线的控件

目前市面上所有常见图形操作系统均在其图形系统中内置了绘制贝塞尔曲线的函数. 以 Windows 为例,其图形系统 GDI/GDI+ 实现了如图 5 所示四点式贝塞尔曲线,即在三次贝塞尔曲线中将 β 1 \beta_1 β1 β 2 \beta_2 β2 分别用作 β 0 \beta_0 β0 β 3 \beta_3 β3 的控制手. 曲线表达式中 β 0 \beta_0 β0 β 1 \beta_1 β1 β 2 \beta_2 β2 β 3 \beta_3 β3 分别对应图上的点“First”、“Second”、“Third”、“Fourth”.

图 5. 计算机绘图中的贝塞尔曲线

2.1 绘制贝塞尔曲线3

通过调用在 GDI+ 中封装的用于绘制三次贝塞尔曲线的函数 DrawBezier

public void DrawBezier(Pen pen,
                       Point pt1,
                       Point pt2,
                       Point pt3,
                       Point pt4);
public void DrawBezier(Pen pen,
                       PointF pt1,
                       PointF pt2,
                       PointF pt3,
                       PointF pt4);
public void DrawBezier(Pen pen,
                       float x1,
                       float y1,
                       float x2,
                       float y2,
                       float x3,
                       float y3,
                       float x4,
                       float y4);

用户可以很方便地在窗体上绘制出所需贝塞尔曲线. 图 6 便是以如下代码绘制而成.

private void Exercise_Paint(object sender, PaintEventArgs e)
{
    
    
        Pen penCurrent = new Pen(Color.Blue);
        Point pt1 = new Point(20, 12),
                    pt2 = new Point(88, 246),
                    pt3 = new Point(364, 192),
                    pt4 = new Point(250, 48);

        e.Graphics.DrawBezier(penCurrent, pt1, pt2, pt3, pt4);
}

图 6. GDI+ 绘制的贝塞尔曲线

2.2 在图形化控件中定义贝塞尔曲线

为了实现用户与控件的图形化交互功能,首先对三次贝塞尔曲线的控制点进行管理. 若以“模型-视图-控制”(MVC)的概念来表达,则此处贝塞尔曲线图形控件可以按如下章节进行设计.

2.2.1 模型

通过前述章节可知,三次贝塞尔曲线具有 2 个端点和 2 个中介点,如图 7 所示,这里分别命名为锚点(anchor)和手柄点(handler).

图 7. 贝塞尔曲线控件上的端点,图中控制点为文中所述手柄点

贝塞尔曲线端点定义如下:

// 贝塞尔曲线点
public class BezierPoint {
    
    
	public Point Anchor {
    
     get; set; }
	public Point Handler {
    
     get; set; }
}

每个贝塞尔曲线的端点包含了一个锚点和对应的手柄点,即曲线表达式中的 ( β 0 , β 1 ) (\beta_0,\beta_1) (β0,β1) ( β 2 , β 3 ) (\beta_2,\beta_3) (β2,β3). 于是一条三次贝塞尔曲线线段可以定义为:

// 贝塞尔曲线段
public class BezierSegment {
    
    
    public BezierPoint Begin;
    public BezierPoint End;
}

通过改变锚点位置和手柄点位置即可实现任意形状的贝塞尔曲线,其使用效果应如图 8 动画所示.

图 8. 可控贝塞尔曲线控件外观及交互演示

2.2.2 视图

曲线外观和曲线的端点无关,可以使用独立的可扩展渲染器对其进行绘制,例如以下简单渲染器:

public class BezierRenderer {
    
    
    public void DrawSegment(Graphics g, BezierSegment s, Color color) {
    
    
    using (Pen p = new Pen(color)) {
    
    
        g.DrawBezier(
            p,
            s.Begin.Anchor,
            s.Begin.Handler,
            s.End.Anchor,
            s.End.Handler);
    }
    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);
    }
}

这个渲染器只提供了可自定义各点颜色和手柄点到锚点连线颜色的功能. 下面是一个使用它的例子:

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

// 画锚点,灰色
renderer.DrawHandle(g, s.Begin.Anchor, false, Color.DimGray);
renderer.DrawHandle(g, s.End.Anchor, false, Color.DimGray);

// 画手柄点,黑色
renderer.DrawBar(g, s.Begin.Anchor, s.Begin.Handler, Color.Black);
renderer.DrawHandle(g, s.Begin.Handler, false, Color.Black);
renderer.DrawBar(g, s.End.Anchor, s.End.Handler, Color.Black);
renderer.DrawHandle(g, s.End.Handler, false, Color.Black);

// 被选中的手柄点为实心
if (target != null)
    renderer.DrawHandle(g, target.Handler, true, Color.Black);

这样就能画出贝塞尔曲线了,如图 9 所示. 注意此时锚点和手柄点重合,看起来类似一次贝塞尔曲线.

图 9. 图形控件中绘制的贝塞尔曲线,锚点与手柄点重合

2.2.3 控制

以上实现了贝塞尔曲线段的数据存储和外观表达,现在为其添加控制功能,即修改数据的能力,这里采用鼠标光标交互完成. 简单的交互逻辑可以认为如下:

  1. 按下鼠标按键,若落点在某端点上(或其邻域内),则该点应被标记为选中
  2. 移动鼠标光标,如果某端点处于选中状态,则根据光标位置修改该端点坐标
  3. 抬起鼠标按键,取消所有端点选中状态

如此,需要在控件的 MouseDownMouseMoveMouseUp 事件中对控件进行相应处理. 下面演示了一个简单的实现. 注意这里有关移动的部分只考虑了手柄点,实际上在完整的实现中应该同时考虑锚点.

首先使用热点监测区(即 1 中提及的邻域)稍微扩大一下选取,降低光标点选难度. 另外定义一个指针指向被选中的端点.

Rectangle rectBegin, rectEnd;
BezierPoint target = null;

现在开始处理鼠标交互事件.

// MouseDown,若落点位于某端点邻域内,则将其标记为已选中
if (rectBegin.Contains(e.Location))
    target = s.Begin;
else if (rectEnd.Contains(e.Location))
    target = s.End;

// MouseMove,若有选中状态的端点,则用光标位置更新其坐标
if (target != null) 
	target.ControlPoint = e.Location;

// MouseUp,取消选择
target = null;

如此即可实现图 8 所演示之贝塞尔曲线控件.

2.4 额外的效果

如果修改渲染器,可以得到更多的图像效果,例如图 10 所示彩色曲线.

图 10. 改变渲染器后的控件运行效果演示

总结

贝塞尔曲线是强大的图形化工具,在现代计算机操作系统中,可以很方便地通过调用系统绘图函数实现可交互、可扩展的贝塞尔曲线控件. 本文介绍了贝塞尔函数、曲线及相关的计算、设计方法,提供了一个简单的基于 Windows GDI+ 的实现,并演示了通过渲染器实现控件外观的改变. 用户可以连接多个锚点以实现任意规模的复杂曲线效果. 此外也可以采用更高效的绘制方法优化控件,并为控件添加自定义的事件处理机制,使其可以用于商业实践.

(完)

© 2013-2023 Conmajia


  1. 参阅 德·卡斯特里奥算法. ↩︎

  2. 参阅 贝塞尔曲线. ↩︎

  3. 参阅 GDI+ 形状:贝塞尔曲线 ↩︎

猜你喜欢

转载自blog.csdn.net/conmajia/article/details/43907499