模拟PS的钢笔路径

大三的时候上计算机图形学没认真听过课,书也不喜欢看,一直不懂Bezier曲线。这两天突然想起来,决定再看看书。

其实是挺简单的公式,每个点的位置P(u)(0<=u<=1)是通过每个控制点Pk(k是控制点的索引从0到n,总共n+1个控制点)与相应的混合函数BEZ k,n(u)的乘积累加得到的。而一般的应用都是四个控制点的三次方程,保证曲线穿过第1和第4个控制点。

既然知道了控制点确定为4个,就可以得到BEZ公式:

BEZ 0,3 = (1-u)^3

BEZ 1,3 = 3u((1-u)^2)

BEZ 2,3 = 3(u^2)(1-u)

BEZ 3,3 = u^3

大三写课设的时候给的参考程序看不懂,不明白为什么明明是描述的是四个控制点的Bezier曲线,却可以绘制无数个控制点来生成Bezier曲线。现在想想,那只是对三次Bezier曲线的拼接罢了,保证两条曲线之间的零阶连续。

这样看过后,突然想知道photoshop的钢笔是怎样实现的,思考了一会儿,发现两个锚点和它们的控制点便构成了一段三次Bezier曲线,如果没有进行角拖移,那么曲线保证一阶连续。下面便是一段三次Bezier曲线:

image

这样想其实挺简单的,于是决定实现之。之前看了一点代码大全和设计模式,原本想在这里小试牛刀,结果还是学艺不精。代码写着写着,又乱了。

大概的设计是这样的,定义了CtrlPoint类,只包含位置信息及相关操作,还有绘制函数,然后锚点EndPoint类和控制点PathPoint类继承自CtrlPoint,一个锚点包含两个指向控制点的指针和两个曲线ID,而一个控制点包含一个指向冒险的指针。

class CtrlPoint

{

public:

    CtrlPoint(void);

    ~CtrlPoint(void);

    void SetPosition(const Point& p);

    void SetPosition(Real x,Real y);

    Point GetPosition(){return position;}

    virtual void Draw(QPainter* painter)=0;

protected:

    Point position;

};



typedef CtrlPoint* CPPointer;



class EndPoint: public CtrlPoint

{

public:

    EndPoint(void);

    ~EndPoint(void);

    virtual void Draw(QPainter* painter);

    CPPointer GetPathPoint(int index){return pathPoints[index];}

    int GetCurve(int index){return curveID[index];}

    void SetPathPoint(CPPointer p, int index);

    void SetCurve(int cID,int index){curveID[index] = cID;}

    void MovePathPoint(CPPointer p, const Point& movement);

    void MoveEndPoint(const Point& movement);

private:

    int curveID[2];

    CPPointer pathPoints[2];

};



class PathPoint: public CtrlPoint

{

public:

    PathPoint(void);

    ~PathPoint(void);

    virtual void Draw(QPainter* painter);

    void SetEndPoint(CPPointer p);

    CPPointer GetEndPoint(){return theEndPoint;}

private:

    CPPointer theEndPoint;

};

而曲线类包含4个控制点的指针,以及30个采样点,以及一些计算信息;绘制函数可以在采样点绘制和QtPath之间切换:

class Curve

{

public:

    Curve(void);

    ~Curve(void);

    void Draw(QPainter* painter, bool useQtPath);

    void SetControlPoint(CPPointer p, int index);

    void GenerateAllPoint();

    

private:

    static Real paraC[];

    static int pointsNum;

    CPPointer ctrlPoints[4];

    vector<Point> allPoints;

};

Bezier曲线的计算:

void Curve::GenerateAllPoint()

{

    Real u;

    allPoints[0] = ctrlPoints[0]->GetPosition();

    allPoints[pointsNum-1] = ctrlPoints[3]->GetPosition();

    for(int i=1;i<pointsNum-1; i++)

    {

        u = Real(i)/Real(pointsNum-1);

        allPoints[i].SetZero();

        for(int j=0;j<4;j++)

        {

            Real bez = paraC[j]*pow(u,j)*pow(1-u,3-j);

            allPoints[i].x += ctrlPoints[j]->GetPosition().x * bez;

            allPoints[i].y += ctrlPoints[j]->GetPosition().y * bez;

        }

    }

}

代码量不多,其实上面的很快就写好了,只是UI和操作花了一些时间。QT用的不熟,起初QPainter是类成员,结果一直没办法绘制到窗口,后来把QPainter的声明写在了paintEvent()里,就可以绘制了。至今也不明白为什么,如果有哪位大侠知道,希望能指点一二。。。

鼠标左键是添加锚点,Alt+鼠标左键拖移控制点,Ctrl+鼠标左键拖移锚点。

这是效果图,Bezier曲线是自己计算的,采样数30,虽然开了抗锯齿,还是不够平滑:

image

如果用QT自带的path来绘制,就平滑多了

image

还有很多地方需要改进,目前虽然支持拖移控制点,但锚点两端的控制点始终是对称的。删除锚点还没写,不支持曲线闭合。至于角拖移有点不想写,我不喜欢零阶连续= =|||。。如果有时间,我想试一试用锚点的权值来修改曲线的粗细。

 

本文原创,转载请著名出处:

http://www.cnblogs.com/luluathena/

你可能感兴趣的:(路径)