blob: c1afbbb4414300e4ebeca93bdd5d18285b066c9f [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the tools applications of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "splineeditor.h"
#include "segmentproperties.h"
#include <QPainter>
#include <QPainterPath>
#include <QMouseEvent>
#include <QContextMenuEvent>
#include <QDebug>
#include <QApplication>
#include <QVector>
#include <QPainterPath>
const int canvasWidth = 640;
const int canvasHeight = 320;
const int canvasMargin = 160;
SplineEditor::SplineEditor(QWidget *parent) :
QWidget(parent), m_pointListWidget(nullptr), m_block(false)
{
setFixedSize(canvasWidth + canvasMargin * 2, canvasHeight + canvasMargin * 2);
m_controlPoints.append(QPointF(0.4, 0.075));
m_controlPoints.append(QPointF(0.45,0.24));
m_controlPoints.append(QPointF(0.5,0.5));
m_controlPoints.append(QPointF(0.55,0.76));
m_controlPoints.append(QPointF(0.7,0.9));
m_controlPoints.append(QPointF(1.0, 1.0));
m_numberOfSegments = 2;
m_activeControlPoint = -1;
m_mouseDrag = false;
m_pointContextMenu = new QMenu(this);
m_deleteAction = new QAction(tr("Delete point"), m_pointContextMenu);
m_smoothAction = new QAction(tr("Smooth point"), m_pointContextMenu);
m_cornerAction = new QAction(tr("Corner point"), m_pointContextMenu);
m_smoothAction->setCheckable(true);
m_pointContextMenu->addAction(m_deleteAction);
m_pointContextMenu->addAction(m_smoothAction);
m_pointContextMenu->addAction(m_cornerAction);
m_curveContextMenu = new QMenu(this);
m_addPoint = new QAction(tr("Add point"), m_pointContextMenu);
m_curveContextMenu->addAction(m_addPoint);
initPresets();
invalidateSmoothList();
}
static inline QPointF mapToCanvas(const QPointF &point)
{
return QPointF(point.x() * canvasWidth + canvasMargin,
canvasHeight - point.y() * canvasHeight + canvasMargin);
}
static inline QPointF mapFromCanvas(const QPointF &point)
{
return QPointF((point.x() - canvasMargin) / canvasWidth ,
1 - (point.y() - canvasMargin) / canvasHeight);
}
static inline void paintControlPoint(const QPointF &controlPoint, QPainter *painter, bool edit,
bool realPoint, bool active, bool smooth)
{
int pointSize = 4;
if (active)
painter->setBrush(QColor(140, 140, 240, 255));
else
painter->setBrush(QColor(120, 120, 220, 255));
if (realPoint) {
pointSize = 6;
painter->setBrush(QColor(80, 80, 210, 150));
}
painter->setPen(QColor(50, 50, 50, 140));
if (!edit)
painter->setBrush(QColor(160, 80, 80, 250));
if (smooth) {
painter->drawEllipse(QRectF(mapToCanvas(controlPoint).x() - pointSize + 0.5,
mapToCanvas(controlPoint).y() - pointSize + 0.5,
pointSize * 2, pointSize * 2));
} else {
painter->drawRect(QRectF(mapToCanvas(controlPoint).x() - pointSize + 0.5,
mapToCanvas(controlPoint).y() - pointSize + 0.5,
pointSize * 2, pointSize * 2));
}
}
static inline bool indexIsRealPoint(int i)
{
return !((i + 1) % 3);
}
static inline int pointForControlPoint(int i)
{
if ((i % 3) == 0)
return i - 1;
if ((i % 3) == 1)
return i + 1;
return i;
}
void drawCleanLine(QPainter *painter, const QPoint p1, QPoint p2)
{
painter->drawLine(p1 + QPointF(0.5 , 0.5), p2 + QPointF(0.5, 0.5));
}
void SplineEditor::paintEvent(QPaintEvent *)
{
QPainter painter(this);
QPen pen(Qt::black);
pen.setWidth(1);
painter.fillRect(0,0,width() - 1, height() - 1, QBrush(Qt::white));
painter.drawRect(0,0,width() - 1, height() - 1);
painter.setRenderHint(QPainter::Antialiasing);
pen = QPen(Qt::gray);
pen.setWidth(1);
pen.setStyle(Qt::DashLine);
painter.setPen(pen);
drawCleanLine(&painter,mapToCanvas(QPoint(0, 0)).toPoint(), mapToCanvas(QPoint(1, 0)).toPoint());
drawCleanLine(&painter,mapToCanvas(QPoint(0, 1)).toPoint(), mapToCanvas(QPoint(1, 1)).toPoint());
for (int i = 0; i < m_numberOfSegments; i++) {
QPainterPath path;
QPointF p0;
if (i == 0)
p0 = mapToCanvas(QPointF(0.0, 0.0));
else
p0 = mapToCanvas(m_controlPoints.at(i * 3 - 1));
path.moveTo(p0);
QPointF p1 = mapToCanvas(m_controlPoints.at(i * 3));
QPointF p2 = mapToCanvas(m_controlPoints.at(i * 3 + 1));
QPointF p3 = mapToCanvas(m_controlPoints.at(i * 3 + 2));
path.cubicTo(p1, p2, p3);
painter.strokePath(path, QPen(QBrush(Qt::black), 2));
QPen pen(Qt::black);
pen.setWidth(1);
pen.setStyle(Qt::DashLine);
painter.setPen(pen);
painter.drawLine(p0, p1);
painter.drawLine(p3, p2);
}
paintControlPoint(QPointF(0.0, 0.0), &painter, false, true, false, false);
paintControlPoint(QPointF(1.0, 1.0), &painter, false, true, false, false);
for (int i = 0; i < m_controlPoints.count() - 1; ++i)
paintControlPoint(m_controlPoints.at(i),
&painter,
true,
indexIsRealPoint(i),
i == m_activeControlPoint,
isControlPointSmooth(i));
}
void SplineEditor::mousePressEvent(QMouseEvent *e)
{
if (e->button() == Qt::LeftButton) {
m_activeControlPoint = findControlPoint(e->pos());
if (m_activeControlPoint != -1) {
mouseMoveEvent(e);
}
m_mousePress = e->pos();
e->accept();
}
}
void SplineEditor::mouseReleaseEvent(QMouseEvent *e)
{
if (e->button() == Qt::LeftButton) {
m_activeControlPoint = -1;
m_mouseDrag = false;
e->accept();
}
}
#if QT_CONFIG(contextmenu)
void SplineEditor::contextMenuEvent(QContextMenuEvent *e)
{
int index = findControlPoint(e->pos());
if (index > 0 && indexIsRealPoint(index)) {
m_smoothAction->setChecked(isControlPointSmooth(index));
QAction* action = m_pointContextMenu->exec(e->globalPos());
if (action == m_deleteAction)
deletePoint(index);
else if (action == m_smoothAction)
smoothPoint(index);
else if (action == m_cornerAction)
cornerPoint(index);
} else {
QAction* action = m_curveContextMenu->exec(e->globalPos());
if (action == m_addPoint)
addPoint(e->pos());
}
}
#endif // contextmenu
void SplineEditor::invalidate()
{
QEasingCurve easingCurve(QEasingCurve::BezierSpline);
for (int i = 0; i < m_numberOfSegments; ++i) {
easingCurve.addCubicBezierSegment(m_controlPoints.at(i * 3),
m_controlPoints.at(i * 3 + 1),
m_controlPoints.at(i * 3 + 2));
}
setEasingCurve(easingCurve);
invalidateSegmentProperties();
}
void SplineEditor::invalidateSmoothList()
{
m_smoothList.clear();
for (int i = 0; i < (m_numberOfSegments - 1); ++i)
m_smoothList.append(isSmooth(i * 3 + 2));
}
void SplineEditor::invalidateSegmentProperties()
{
for (int i = 0; i < m_numberOfSegments; ++i) {
SegmentProperties *segmentProperties = m_segmentProperties.at(i);
bool smooth = false;
if (i < (m_numberOfSegments - 1)) {
smooth = m_smoothList.at(i);
}
segmentProperties->setSegment(i, m_controlPoints.mid(i * 3, 3), smooth, i == (m_numberOfSegments - 1));
}
}
QHash<QString, QEasingCurve> SplineEditor::presets() const
{
return m_presets;
}
QString SplineEditor::generateCode()
{
QString s = QLatin1String("[");
for (const QPointF &point : qAsConst(m_controlPoints)) {
s += QString::number(point.x(), 'g', 2) + QLatin1Char(',')
+ QString::number(point.y(), 'g', 3) + QLatin1Char(',');
}
s.chop(1); //removing last ","
s += QLatin1Char(']');
return s;
}
QStringList SplineEditor::presetNames() const
{
return m_presets.keys();
}
QWidget *SplineEditor::pointListWidget()
{
if (!m_pointListWidget) {
setupPointListWidget();
}
return m_pointListWidget;
}
int SplineEditor::findControlPoint(const QPoint &point)
{
int pointIndex = -1;
qreal distance = -1;
for (int i = 0; i<m_controlPoints.size() - 1; ++i) {
qreal d = QLineF(point, mapToCanvas(m_controlPoints.at(i))).length();
if ((distance < 0 && d < 10) || d < distance) {
distance = d;
pointIndex = i;
}
}
return pointIndex;
}
static inline bool veryFuzzyCompare(qreal r1, qreal r2)
{
if (qFuzzyCompare(r1, 2))
return true;
int r1i = qRound(r1 * 20);
int r2i = qRound(r2 * 20);
if (qFuzzyCompare(qreal(r1i) / 20, qreal(r2i) / 20))
return true;
return false;
}
bool SplineEditor::isSmooth(int i) const
{
if (i == 0)
return false;
QPointF p = m_controlPoints.at(i);
QPointF p_before = m_controlPoints.at(i - 1);
QPointF p_after = m_controlPoints.at(i + 1);
QPointF v1 = p_after - p;
v1 = v1 / v1.manhattanLength(); //normalize
QPointF v2 = p - p_before;
v2 = v2 / v2.manhattanLength(); //normalize
return veryFuzzyCompare(v1.x(), v2.x()) && veryFuzzyCompare(v1.y(), v2.y());
}
void SplineEditor::smoothPoint(int index)
{
if (m_smoothAction->isChecked()) {
QPointF before = QPointF(0,0);
if (index > 3)
before = m_controlPoints.at(index - 3);
QPointF after = QPointF(1.0, 1.0);
if ((index + 3) < m_controlPoints.count())
after = m_controlPoints.at(index + 3);
QPointF tangent = (after - before) / 6;
QPointF thisPoint = m_controlPoints.at(index);
if (index > 0)
m_controlPoints[index - 1] = thisPoint - tangent;
if (index + 1 < m_controlPoints.count())
m_controlPoints[index + 1] = thisPoint + tangent;
m_smoothList[index / 3] = true;
} else {
m_smoothList[index / 3] = false;
}
invalidate();
update();
}
void SplineEditor::cornerPoint(int index)
{
QPointF before = QPointF(0,0);
if (index > 3)
before = m_controlPoints.at(index - 3);
QPointF after = QPointF(1.0, 1.0);
if ((index + 3) < m_controlPoints.count())
after = m_controlPoints.at(index + 3);
QPointF thisPoint = m_controlPoints.at(index);
if (index > 0)
m_controlPoints[index - 1] = (before - thisPoint) / 3 + thisPoint;
if (index + 1 < m_controlPoints.count())
m_controlPoints[index + 1] = (after - thisPoint) / 3 + thisPoint;
m_smoothList[(index) / 3] = false;
invalidate();
}
void SplineEditor::deletePoint(int index)
{
m_controlPoints.remove(index - 1, 3);
m_numberOfSegments--;
invalidateSmoothList();
setupPointListWidget();
invalidate();
}
void SplineEditor::addPoint(const QPointF point)
{
QPointF newPos = mapFromCanvas(point);
int splitIndex = 0;
for (int i=0; i < m_controlPoints.size() - 1; ++i) {
if (indexIsRealPoint(i) && m_controlPoints.at(i).x() > newPos.x()) {
break;
} else if (indexIsRealPoint(i))
splitIndex = i;
}
QPointF before = QPointF(0,0);
if (splitIndex > 0)
before = m_controlPoints.at(splitIndex);
QPointF after = QPointF(1.0, 1.0);
if ((splitIndex + 3) < m_controlPoints.count())
after = m_controlPoints.at(splitIndex + 3);
if (splitIndex > 0) {
m_controlPoints.insert(splitIndex + 2, (newPos + after) / 2);
m_controlPoints.insert(splitIndex + 2, newPos);
m_controlPoints.insert(splitIndex + 2, (newPos + before) / 2);
} else {
m_controlPoints.insert(splitIndex + 1, (newPos + after) / 2);
m_controlPoints.insert(splitIndex + 1, newPos);
m_controlPoints.insert(splitIndex + 1, (newPos + before) / 2);
}
m_numberOfSegments++;
invalidateSmoothList();
setupPointListWidget();
invalidate();
}
void SplineEditor::initPresets()
{
const QPointF endPoint(1.0, 1.0);
{
QEasingCurve easingCurve(QEasingCurve::BezierSpline);
easingCurve.addCubicBezierSegment(QPointF(0.4, 0.075), QPointF(0.45, 0.24), QPointF(0.5, 0.5));
easingCurve.addCubicBezierSegment(QPointF(0.55, 0.76), QPointF(0.7, 0.9), endPoint);
m_presets.insert(tr("Standard Easing"), easingCurve);
}
{
QEasingCurve easingCurve(QEasingCurve::BezierSpline);
easingCurve.addCubicBezierSegment(QPointF(0.43, 0.0025), QPointF(0.65, 1), endPoint);
m_presets.insert(tr("Simple"), easingCurve);
}
{
QEasingCurve easingCurve(QEasingCurve::BezierSpline);
easingCurve.addCubicBezierSegment(QPointF(0.43, 0.0025), QPointF(0.38, 0.51), QPointF(0.57, 0.99));
easingCurve.addCubicBezierSegment(QPointF(0.8, 0.69), QPointF(0.65, 1), endPoint);
m_presets.insert(tr("Simple Bounce"), easingCurve);
}
{
QEasingCurve easingCurve(QEasingCurve::BezierSpline);
easingCurve.addCubicBezierSegment(QPointF(0.4, 0.075), QPointF(0.64, -0.0025), QPointF(0.74, 0.23));
easingCurve.addCubicBezierSegment(QPointF(0.84, 0.46), QPointF(0.91, 0.77), endPoint);
m_presets.insert(tr("Slow in fast out"), easingCurve);
}
{
QEasingCurve easingCurve(QEasingCurve::BezierSpline);
easingCurve.addCubicBezierSegment(QPointF(0.43, 0.0025), QPointF(0.47, 0.51), QPointF(0.59, 0.94));
easingCurve.addCubicBezierSegment(QPointF(0.84, 0.95), QPointF( 0.99, 0.94), endPoint);
m_presets.insert(tr("Snapping"), easingCurve);
}
{
QEasingCurve easingCurve(QEasingCurve::BezierSpline);
easingCurve.addCubicBezierSegment(QPointF( 0.38, 0.35),QPointF(0.38, 0.7), QPointF(0.45, 0.99));
easingCurve.addCubicBezierSegment(QPointF(0.48, 0.66), QPointF(0.62, 0.62), QPointF(0.66, 0.99));
easingCurve.addCubicBezierSegment(QPointF(0.69, 0.76), QPointF(0.77, 0.76), QPointF(0.79, 0.99));
easingCurve.addCubicBezierSegment(QPointF(0.83, 0.91), QPointF(0.87, 0.92), QPointF(0.91, 0.99));
easingCurve.addCubicBezierSegment(QPointF(0.95, 0.95), QPointF(0.97, 0.94), endPoint);
m_presets.insert(tr("Complex Bounce"), easingCurve);
}
{
QEasingCurve easingCurve4(QEasingCurve::BezierSpline);
easingCurve4.addCubicBezierSegment(QPointF(0.12, -0.12),QPointF(0.23, -0.19), QPointF( 0.35, -0.09));
easingCurve4.addCubicBezierSegment(QPointF(0.47, 0.005), QPointF(0.52, 1), QPointF(0.62, 1.1));
easingCurve4.addCubicBezierSegment(QPointF(0.73, 1.2), QPointF(0.91,1 ), endPoint);
m_presets.insert(tr("Overshoot"), easingCurve4);
}
}
void SplineEditor::setupPointListWidget()
{
if (!m_pointListWidget)
m_pointListWidget = new QScrollArea(this);
if (m_pointListWidget->widget())
delete m_pointListWidget->widget();
m_pointListWidget->setFrameStyle(QFrame::NoFrame);
m_pointListWidget->setWidgetResizable(true);
m_pointListWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_pointListWidget->setWidget(new QWidget(m_pointListWidget));
QVBoxLayout *layout = new QVBoxLayout(m_pointListWidget->widget());
layout->setContentsMargins(QMargins());
layout->setSpacing(2);
m_pointListWidget->widget()->setLayout(layout);
m_segmentProperties.clear();
{ //implicit 0,0
QWidget *widget = new QWidget(m_pointListWidget->widget());
Ui_Pane pane;
pane.setupUi(widget);
pane.p1_x->setValue(0);
pane.p1_y->setValue(0);
layout->addWidget(widget);
pane.label->setText("p0");
widget->setEnabled(false);
}
for (int i = 0; i < m_numberOfSegments; ++i) {
SegmentProperties *segmentProperties = new SegmentProperties(m_pointListWidget->widget());
layout->addWidget(segmentProperties);
bool smooth = false;
if (i < (m_numberOfSegments - 1)) {
smooth = m_smoothList.at(i);
}
segmentProperties->setSegment(i, m_controlPoints.mid(i * 3, 3), smooth, i == (m_numberOfSegments - 1));
segmentProperties->setSplineEditor(this);
m_segmentProperties << segmentProperties;
}
layout->addSpacerItem(new QSpacerItem(10, 10, QSizePolicy::Expanding, QSizePolicy::Expanding));
m_pointListWidget->viewport()->show();
m_pointListWidget->viewport()->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
m_pointListWidget->show();
}
bool SplineEditor::isControlPointSmooth(int i) const
{
if (i == 0)
return false;
if (i == m_controlPoints.count() - 1)
return false;
if (m_numberOfSegments == 1)
return false;
int index = pointForControlPoint(i);
if (index == 0)
return false;
if (index == m_controlPoints.count() - 1)
return false;
return m_smoothList.at(index / 3);
}
QPointF limitToCanvas(const QPointF point)
{
qreal left = -qreal( canvasMargin) / qreal(canvasWidth);
qreal width = 1.0 - 2.0 * left;
qreal top = -qreal( canvasMargin) / qreal(canvasHeight);
qreal height = 1.0 - 2.0 * top;
QPointF p = point;
QRectF r(left, top, width, height);
if (p.x() > r.right()) {
p.setX(r.right());
}
if (p.x() < r.left()) {
p.setX(r.left());
}
if (p.y() < r.top()) {
p.setY(r.top());
}
if (p.y() > r.bottom()) {
p.setY(r.bottom());
}
return p;
}
void SplineEditor::mouseMoveEvent(QMouseEvent *e)
{
// If we've moved more then 25 pixels, assume user is dragging
if (!m_mouseDrag && QPoint(m_mousePress - e->pos()).manhattanLength() > qApp->startDragDistance())
m_mouseDrag = true;
QPointF p = mapFromCanvas(e->pos());
if (m_mouseDrag && m_activeControlPoint >= 0 && m_activeControlPoint < m_controlPoints.size()) {
p = limitToCanvas(p);
if (indexIsRealPoint(m_activeControlPoint)) {
//move also the tangents
QPointF targetPoint = p;
QPointF distance = targetPoint - m_controlPoints.at(m_activeControlPoint);
m_controlPoints[m_activeControlPoint] = targetPoint;
m_controlPoints[m_activeControlPoint - 1] += distance;
m_controlPoints[m_activeControlPoint + 1] += distance;
} else {
if (!isControlPointSmooth(m_activeControlPoint)) {
m_controlPoints[m_activeControlPoint] = p;
} else {
QPointF targetPoint = p;
QPointF distance = targetPoint - m_controlPoints.at(m_activeControlPoint);
m_controlPoints[m_activeControlPoint] = p;
if ((m_activeControlPoint > 1) && (m_activeControlPoint % 3) == 0) { //right control point
m_controlPoints[m_activeControlPoint - 2] -= distance;
} else if ((m_activeControlPoint < (m_controlPoints.count() - 2)) //left control point
&& (m_activeControlPoint % 3) == 1) {
m_controlPoints[m_activeControlPoint + 2] -= distance;
}
}
}
invalidate();
}
}
void SplineEditor::setEasingCurve(const QEasingCurve &easingCurve)
{
if (m_easingCurve == easingCurve)
return;
m_block = true;
m_easingCurve = easingCurve;
m_controlPoints = m_easingCurve.toCubicSpline();
m_numberOfSegments = m_controlPoints.count() / 3;
update();
emit easingCurveChanged();
const QString code = generateCode();
emit easingCurveCodeChanged(code);
m_block = false;
}
void SplineEditor::setPreset(const QString &name)
{
setEasingCurve(m_presets.value(name));
invalidateSmoothList();
setupPointListWidget();
}
void SplineEditor::setEasingCurve(const QString &code)
{
if (m_block)
return;
if (code.startsWith(QLatin1Char('[')) && code.endsWith(QLatin1Char(']'))) {
const QStringRef cleanCode(&code, 1, code.size() - 2);
const auto stringList = cleanCode.split(QLatin1Char(','), Qt::SkipEmptyParts);
if (stringList.count() >= 6 && (stringList.count() % 6 == 0)) {
QVector<qreal> realList;
realList.reserve(stringList.count());
for (const QStringRef &string : stringList) {
bool ok;
realList.append(string.toDouble(&ok));
if (!ok)
return;
}
QVector<QPointF> points;
const int count = realList.count() / 2;
points.reserve(count);
for (int i = 0; i < count; ++i)
points.append(QPointF(realList.at(i * 2), realList.at(i * 2 + 1)));
if (points.constLast() == QPointF(1.0, 1.0)) {
QEasingCurve easingCurve(QEasingCurve::BezierSpline);
for (int i = 0; i < points.count() / 3; ++i) {
easingCurve.addCubicBezierSegment(points.at(i * 3),
points.at(i * 3 + 1),
points.at(i * 3 + 2));
}
setEasingCurve(easingCurve);
invalidateSmoothList();
setupPointListWidget();
}
}
}
}
#include "moc_splineeditor.cpp"