目录
1. 前言
2. 预备知识
3. 代码实现
1. 前言
在设计矢量图案的时候,我们常常需要用到曲线来表达物体造型,单纯用鼠标轨迹绘制显然是不足的。于是我们希望能够实现这样的方法:通过设计师手工选择控制点,再通过插值得到过控制点(或在附近)的一条平滑曲线。在这样的需求下,样条曲线诞生了。简而言之,样条曲线是由多个多项式按比例系数组成的多项式函数,而比例系数是由控制点决定的。Hermite曲线、Cardinal曲线在平时的开发中,经常用于模拟运动物体的轨迹,如下:
2. 预备知识
关于Hermite曲线、Cardinal曲线的数学理论,参见如下博文:
- [计算机动画] 路径曲线与运动物体控制(Cardinal样条曲线)。
- 三次参数样条曲线与Cardinal曲线。
3. 代码实现
如下为用Qt实现的Cardinal曲线
Cardinal.h
#pragma once
#include <QtWidgets/QWidget>
#include "ui_Cardinal.h"
QT_BEGIN_NAMESPACE
namespace Ui { class CardinalClass; };
QT_END_NAMESPACE
class Cardinal : public QWidget
{
Q_OBJECT
public:
Cardinal(QWidget *parent = nullptr);
~Cardinal();
private:
Ui::CardinalClass *ui;
};
Cardinal.cpp
#include "Cardinal.h"
Cardinal::Cardinal(QWidget *parent)
: QWidget(parent)
, ui(new Ui::CardinalClass())
{
ui->setupUi(this);
setWindowState(Qt::WindowMaximized);
ui->doubleSpinBox->setMinimum(0);
ui->doubleSpinBox->setMaximum(1);
ui->doubleSpinBox->setValue(0.5);
ui->doubleSpinBox->setSingleStep(0.1);
connect(ui->doubleSpinBox, QOverload<double>::of(&QDoubleSpinBox::valueChanged), ui->myCardinalPanel, &CardinalPanel::valueChanged);
connect(ui->startDrawBtn, &QAbstractButton::clicked, ui->myCardinalPanel, &CardinalPanel::startDraw);
connect(ui->clearBtn, &QAbstractButton::clicked, ui->myCardinalPanel, &CardinalPanel::clear);
}
Cardinal::~Cardinal()
{
delete ui;
}
CardinalPanel.h
#pragma once
#include <QWidget>
#include "ui_CardinalPanel.h"
#include<vector>
using std::list;
class CardinalPanel : public QWidget
{
Q_OBJECT
public:
CardinalPanel(QWidget *parent = nullptr);
~CardinalPanel();
public:
void valueChanged(double value);
void startDraw();
void clear();
private:
virtual void mousePressEvent(QMouseEvent* event) override;
virtual void paintEvent(QPaintEvent* event) override;
private:
// 画鼠标左键按下选中的点
void drawPoint();
// 画Cardinal曲线
void drawCardinal();
// 计算MC矩阵
void calMcMatrix(double s);
// 压入头部和尾部两个点,用于计算
void pushHeadAndTailPoint();
private:
Ui::CardinalPanelClass ui;
float m_fUValue{0.5};
bool m_bStartDraw{false};
double m_dfMcMatrix[4][4];
list<QPoint> m_lstPoint;
QPainterPath path;
bool m_bHasAddVirtPoint{false}; // 是否已经加入虚拟点
};
CardinalPanel.cpp
#include "CardinalPanel.h"
#include<QMouseEvent>
#include<QPainter>
#include <QPainterPath>
#include<vector>
using std::vector;
CardinalPanel::CardinalPanel(QWidget* parent)
: QWidget(parent)
{
ui.setupUi(this);
valueChanged(0.5);
}
CardinalPanel::~CardinalPanel()
{}
void CardinalPanel::valueChanged(double value)
{
auto s = (1 - value) / 2.0;
// 计算MC矩阵
calMcMatrix(s);
update();
}
// 计算MC矩阵
void CardinalPanel::calMcMatrix(double s)
{
m_dfMcMatrix[0][0] = -s, m_dfMcMatrix[0][1] = 2 - s, m_dfMcMatrix[0][2] = s - 2, m_dfMcMatrix[0][3] = s;//Mc矩阵
m_dfMcMatrix[1][0] = 2 * s, m_dfMcMatrix[1][1] = s - 3, m_dfMcMatrix[1][2] = 3 - 2 * s, m_dfMcMatrix[1][3] = -s;
m_dfMcMatrix[2][0] = -s, m_dfMcMatrix[2][1] = 0, m_dfMcMatrix[2][2] = s, m_dfMcMatrix[2][3] = 0;
m_dfMcMatrix[3][0] = 0, m_dfMcMatrix[3][1] = 1, m_dfMcMatrix[3][2] = 0, m_dfMcMatrix[3][3] = 0;
}
void CardinalPanel::clear()
{
m_bStartDraw = false;
m_lstPoint.clear();
update();
}
// 压入头部和尾部两个点,用于计算
void CardinalPanel::pushHeadAndTailPoint()
{
// 随便构造两个点
auto ptBegin = m_lstPoint.begin();
auto x = ptBegin->x() + 20;
auto y = ptBegin->y() + 20;
m_lstPoint.insert(m_lstPoint.begin(), QPoint(x, y));
auto ptEnd = m_lstPoint.back();
x = ptEnd.x() + 20;
y = ptEnd.y() + 20;
m_lstPoint.insert(m_lstPoint.end(), QPoint(x, y));
}
void CardinalPanel::startDraw()
{
m_bStartDraw = true;
pushHeadAndTailPoint();
update();
}
void CardinalPanel::mousePressEvent(QMouseEvent* event)
{
if ((Qt::LeftButton != event->button()))
{
return QWidget::mousePressEvent(event);
}
m_lstPoint.insert(m_lstPoint.end(), event->pos());
update();
QWidget::mousePressEvent(event);
}
// 画鼠标左键按下选中的点
void CardinalPanel::drawPoint()
{
QPainter painter(this);
painter.setBrush(QColor(Qt::red));
const auto iPointSize = 8;
// 先画鼠标左键按下选中的点
auto nPointIndex = 0;
for (auto iter = m_lstPoint.begin(); iter != m_lstPoint.end(); ++iter)
{
// 头部、尾部的两个控制点不绘制
if (m_bStartDraw && ( (iter == m_lstPoint.begin()) || (*iter == m_lstPoint.back()) ))
{
continue;
}
painter.drawEllipse(*iter, iPointSize, iPointSize);
}
}
// 画Cardinal曲线
void CardinalPanel::drawCardinal()
{
if (m_lstPoint.size() < 4)
{
return;
}
QPainter painter(this);
QPen pen(QColor(Qt::green), 6);
painter.setPen(pen);
path.clear();
auto iter = m_lstPoint.begin();
++iter; // 第1个点(基于0的索引)
path.moveTo(*iter);
--iter;
auto endIter = m_lstPoint.end();
int nIndex = 0;
while (true)
{
--endIter;
++nIndex;
if (3 == nIndex)
{
break;
}
}
for (; iter != endIter; ++iter)
{
auto& p0 = *iter;
auto& p1 = *(++iter);
auto& p2 = *(++iter);
auto& p3 = *(++iter);
--iter;
--iter;
--iter;
vector<QPoint>vtTempPoint;
vtTempPoint.push_back(p0);
vtTempPoint.push_back(p1);
vtTempPoint.push_back(p2);
vtTempPoint.push_back(p3);
//double value[4][1];
for (auto i = 0; i < 4; ++i)
{
vtTempPoint[i] = m_dfMcMatrix[i][0] * p0 + m_dfMcMatrix[i][1] * p1 + m_dfMcMatrix[i][2] * p2 + m_dfMcMatrix[i][3] * p3;
}
double t3, t2, t1, t0;
for (double t = 0.0; t < 1; t += 0.01)
{
t3 = t * t * t; t2 = t * t; t1 = t; t0 = 1;
auto newPoint = t3 * vtTempPoint[0] + t2 * vtTempPoint[1] + t1 * vtTempPoint[2] + t0 * vtTempPoint[3];
path.lineTo(newPoint);
}
}
painter.drawPath(path);
}
void CardinalPanel::paintEvent(QPaintEvent* event)
{
drawPoint();
// 再画Cardinal曲线
if (m_bStartDraw)
{
drawCardinal();
}
}
运行效果如下:
可以看到当u值越大时,曲线越尖锐,当变为1时,就成了直线;越小越光滑。