| 1 | // Copyright (C) 2016 The Qt Company Ltd. |
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 |
| 3 | |
| 4 | #include "splineeditor.h" |
| 5 | #include "segmentproperties.h" |
| 6 | |
| 7 | #include <QPainter> |
| 8 | #include <QPainterPath> |
| 9 | #include <QMouseEvent> |
| 10 | #include <QContextMenuEvent> |
| 11 | #include <QDebug> |
| 12 | #include <QApplication> |
| 13 | #include <QVector> |
| 14 | #include <QPainterPath> |
| 15 | |
| 16 | const int canvasWidth = 640; |
| 17 | const int canvasHeight = 320; |
| 18 | |
| 19 | const int canvasMargin = 160; |
| 20 | |
| 21 | SplineEditor::SplineEditor(QWidget *parent) : |
| 22 | QWidget(parent), m_pointListWidget(nullptr), m_block(false) |
| 23 | { |
| 24 | setFixedSize(w: canvasWidth + canvasMargin * 2, h: canvasHeight + canvasMargin * 2); |
| 25 | |
| 26 | m_controlPoints.append(t: QPointF(0.4, 0.075)); |
| 27 | m_controlPoints.append(t: QPointF(0.45,0.24)); |
| 28 | m_controlPoints.append(t: QPointF(0.5,0.5)); |
| 29 | |
| 30 | m_controlPoints.append(t: QPointF(0.55,0.76)); |
| 31 | m_controlPoints.append(t: QPointF(0.7,0.9)); |
| 32 | m_controlPoints.append(t: QPointF(1.0, 1.0)); |
| 33 | |
| 34 | m_numberOfSegments = 2; |
| 35 | |
| 36 | m_activeControlPoint = -1; |
| 37 | |
| 38 | m_mouseDrag = false; |
| 39 | |
| 40 | m_pointContextMenu = new QMenu(this); |
| 41 | m_deleteAction = new QAction(tr(s: "Delete point" ), m_pointContextMenu); |
| 42 | m_smoothAction = new QAction(tr(s: "Smooth point" ), m_pointContextMenu); |
| 43 | m_cornerAction = new QAction(tr(s: "Corner point" ), m_pointContextMenu); |
| 44 | |
| 45 | m_smoothAction->setCheckable(true); |
| 46 | |
| 47 | m_pointContextMenu->addAction(action: m_deleteAction); |
| 48 | m_pointContextMenu->addAction(action: m_smoothAction); |
| 49 | m_pointContextMenu->addAction(action: m_cornerAction); |
| 50 | |
| 51 | m_curveContextMenu = new QMenu(this); |
| 52 | |
| 53 | m_addPoint = new QAction(tr(s: "Add point" ), m_pointContextMenu); |
| 54 | |
| 55 | m_curveContextMenu->addAction(action: m_addPoint); |
| 56 | |
| 57 | initPresets(); |
| 58 | |
| 59 | invalidateSmoothList(); |
| 60 | } |
| 61 | |
| 62 | static inline QPointF mapToCanvas(const QPointF &point) |
| 63 | { |
| 64 | return QPointF(point.x() * canvasWidth + canvasMargin, |
| 65 | canvasHeight - point.y() * canvasHeight + canvasMargin); |
| 66 | } |
| 67 | |
| 68 | static inline QPointF mapFromCanvas(const QPointF &point) |
| 69 | { |
| 70 | return QPointF((point.x() - canvasMargin) / canvasWidth , |
| 71 | 1 - (point.y() - canvasMargin) / canvasHeight); |
| 72 | } |
| 73 | |
| 74 | static inline void paintControlPoint(const QPointF &controlPoint, QPainter *painter, bool edit, |
| 75 | bool realPoint, bool active, bool smooth) |
| 76 | { |
| 77 | int pointSize = 4; |
| 78 | |
| 79 | if (active) |
| 80 | painter->setBrush(QColor(140, 140, 240, 255)); |
| 81 | else |
| 82 | painter->setBrush(QColor(120, 120, 220, 255)); |
| 83 | |
| 84 | if (realPoint) { |
| 85 | pointSize = 6; |
| 86 | painter->setBrush(QColor(80, 80, 210, 150)); |
| 87 | } |
| 88 | |
| 89 | painter->setPen(QColor(50, 50, 50, 140)); |
| 90 | |
| 91 | if (!edit) |
| 92 | painter->setBrush(QColor(160, 80, 80, 250)); |
| 93 | |
| 94 | if (smooth) { |
| 95 | painter->drawEllipse(r: QRectF(mapToCanvas(point: controlPoint).x() - pointSize + 0.5, |
| 96 | mapToCanvas(point: controlPoint).y() - pointSize + 0.5, |
| 97 | pointSize * 2, pointSize * 2)); |
| 98 | } else { |
| 99 | painter->drawRect(rect: QRectF(mapToCanvas(point: controlPoint).x() - pointSize + 0.5, |
| 100 | mapToCanvas(point: controlPoint).y() - pointSize + 0.5, |
| 101 | pointSize * 2, pointSize * 2)); |
| 102 | } |
| 103 | } |
| 104 | |
| 105 | static inline bool indexIsRealPoint(int i) |
| 106 | { |
| 107 | return !((i + 1) % 3); |
| 108 | } |
| 109 | |
| 110 | static inline int pointForControlPoint(int i) |
| 111 | { |
| 112 | if ((i % 3) == 0) |
| 113 | return i - 1; |
| 114 | |
| 115 | if ((i % 3) == 1) |
| 116 | return i + 1; |
| 117 | |
| 118 | return i; |
| 119 | } |
| 120 | |
| 121 | void drawCleanLine(QPainter *painter, const QPoint p1, QPoint p2) |
| 122 | { |
| 123 | painter->drawLine(p1: p1 + QPointF(0.5 , 0.5), p2: p2 + QPointF(0.5, 0.5)); |
| 124 | } |
| 125 | |
| 126 | void SplineEditor::paintEvent(QPaintEvent *) |
| 127 | { |
| 128 | QPainter painter(this); |
| 129 | |
| 130 | QPen pen(Qt::black); |
| 131 | pen.setWidth(1); |
| 132 | painter.fillRect(x: 0,y: 0,w: width() - 1, h: height() - 1, b: QBrush(Qt::white)); |
| 133 | painter.drawRect(x: 0,y: 0,w: width() - 1, h: height() - 1); |
| 134 | |
| 135 | painter.setRenderHint(hint: QPainter::Antialiasing); |
| 136 | |
| 137 | pen = QPen(Qt::gray); |
| 138 | pen.setWidth(1); |
| 139 | pen.setStyle(Qt::DashLine); |
| 140 | painter.setPen(pen); |
| 141 | drawCleanLine(painter: &painter,p1: mapToCanvas(point: QPoint(0, 0)).toPoint(), p2: mapToCanvas(point: QPoint(1, 0)).toPoint()); |
| 142 | drawCleanLine(painter: &painter,p1: mapToCanvas(point: QPoint(0, 1)).toPoint(), p2: mapToCanvas(point: QPoint(1, 1)).toPoint()); |
| 143 | |
| 144 | for (int i = 0; i < m_numberOfSegments; i++) { |
| 145 | QPainterPath path; |
| 146 | QPointF p0; |
| 147 | |
| 148 | if (i == 0) |
| 149 | p0 = mapToCanvas(point: QPointF(0.0, 0.0)); |
| 150 | else |
| 151 | p0 = mapToCanvas(point: m_controlPoints.at(i: i * 3 - 1)); |
| 152 | |
| 153 | path.moveTo(p: p0); |
| 154 | |
| 155 | QPointF p1 = mapToCanvas(point: m_controlPoints.at(i: i * 3)); |
| 156 | QPointF p2 = mapToCanvas(point: m_controlPoints.at(i: i * 3 + 1)); |
| 157 | QPointF p3 = mapToCanvas(point: m_controlPoints.at(i: i * 3 + 2)); |
| 158 | path.cubicTo(ctrlPt1: p1, ctrlPt2: p2, endPt: p3); |
| 159 | painter.strokePath(path, pen: QPen(QBrush(Qt::black), 2)); |
| 160 | |
| 161 | QPen pen(Qt::black); |
| 162 | pen.setWidth(1); |
| 163 | pen.setStyle(Qt::DashLine); |
| 164 | painter.setPen(pen); |
| 165 | painter.drawLine(p1: p0, p2: p1); |
| 166 | painter.drawLine(p1: p3, p2); |
| 167 | } |
| 168 | |
| 169 | paintControlPoint(controlPoint: QPointF(0.0, 0.0), painter: &painter, edit: false, realPoint: true, active: false, smooth: false); |
| 170 | paintControlPoint(controlPoint: QPointF(1.0, 1.0), painter: &painter, edit: false, realPoint: true, active: false, smooth: false); |
| 171 | |
| 172 | for (int i = 0; i < m_controlPoints.size() - 1; ++i) |
| 173 | paintControlPoint(controlPoint: m_controlPoints.at(i), |
| 174 | painter: &painter, |
| 175 | edit: true, |
| 176 | realPoint: indexIsRealPoint(i), |
| 177 | active: i == m_activeControlPoint, |
| 178 | smooth: isControlPointSmooth(i)); |
| 179 | } |
| 180 | |
| 181 | void SplineEditor::mousePressEvent(QMouseEvent *e) |
| 182 | { |
| 183 | if (e->button() == Qt::LeftButton) { |
| 184 | m_activeControlPoint = findControlPoint(point: e->position().toPoint()); |
| 185 | |
| 186 | if (m_activeControlPoint != -1) { |
| 187 | mouseMoveEvent(e); |
| 188 | } |
| 189 | m_mousePress = e->position().toPoint(); |
| 190 | e->accept(); |
| 191 | } |
| 192 | } |
| 193 | |
| 194 | void SplineEditor::mouseReleaseEvent(QMouseEvent *e) |
| 195 | { |
| 196 | if (e->button() == Qt::LeftButton) { |
| 197 | m_activeControlPoint = -1; |
| 198 | |
| 199 | m_mouseDrag = false; |
| 200 | e->accept(); |
| 201 | } |
| 202 | } |
| 203 | |
| 204 | #if QT_CONFIG(contextmenu) |
| 205 | void SplineEditor::(QContextMenuEvent *e) |
| 206 | { |
| 207 | int index = findControlPoint(point: e->pos()); |
| 208 | |
| 209 | if (index > 0 && indexIsRealPoint(i: index)) { |
| 210 | m_smoothAction->setChecked(isControlPointSmooth(i: index)); |
| 211 | QAction* action = m_pointContextMenu->exec(pos: e->globalPos()); |
| 212 | if (action == m_deleteAction) |
| 213 | deletePoint(index); |
| 214 | else if (action == m_smoothAction) |
| 215 | smoothPoint(index); |
| 216 | else if (action == m_cornerAction) |
| 217 | cornerPoint(index); |
| 218 | } else { |
| 219 | QAction* action = m_curveContextMenu->exec(pos: e->globalPos()); |
| 220 | if (action == m_addPoint) |
| 221 | addPoint(point: e->pos()); |
| 222 | } |
| 223 | } |
| 224 | #endif // contextmenu |
| 225 | |
| 226 | void SplineEditor::invalidate() |
| 227 | { |
| 228 | QEasingCurve easingCurve(QEasingCurve::BezierSpline); |
| 229 | |
| 230 | for (int i = 0; i < m_numberOfSegments; ++i) { |
| 231 | easingCurve.addCubicBezierSegment(c1: m_controlPoints.at(i: i * 3), |
| 232 | c2: m_controlPoints.at(i: i * 3 + 1), |
| 233 | endPoint: m_controlPoints.at(i: i * 3 + 2)); |
| 234 | } |
| 235 | setEasingCurve(easingCurve); |
| 236 | invalidateSegmentProperties(); |
| 237 | } |
| 238 | |
| 239 | void SplineEditor::invalidateSmoothList() |
| 240 | { |
| 241 | m_smoothList.clear(); |
| 242 | |
| 243 | for (int i = 0; i < (m_numberOfSegments - 1); ++i) |
| 244 | m_smoothList.append(t: isSmooth(i: i * 3 + 2)); |
| 245 | |
| 246 | } |
| 247 | |
| 248 | void SplineEditor::invalidateSegmentProperties() |
| 249 | { |
| 250 | for (int i = 0; i < m_numberOfSegments; ++i) { |
| 251 | SegmentProperties *segmentProperties = m_segmentProperties.at(i); |
| 252 | bool smooth = false; |
| 253 | if (i < (m_numberOfSegments - 1)) { |
| 254 | smooth = m_smoothList.at(i); |
| 255 | } |
| 256 | segmentProperties->setSegment(i, m_controlPoints.mid(pos: i * 3, len: 3), smooth, i == (m_numberOfSegments - 1)); |
| 257 | } |
| 258 | } |
| 259 | |
| 260 | QHash<QString, QEasingCurve> SplineEditor::presets() const |
| 261 | { |
| 262 | return m_presets; |
| 263 | } |
| 264 | |
| 265 | QString SplineEditor::generateCode() |
| 266 | { |
| 267 | QString s = QLatin1String("[" ); |
| 268 | for (const QPointF &point : std::as_const(t&: m_controlPoints)) { |
| 269 | s += QString::number(point.x(), format: 'g', precision: 2) + QLatin1Char(',') |
| 270 | + QString::number(point.y(), format: 'g', precision: 3) + QLatin1Char(','); |
| 271 | } |
| 272 | s.chop(n: 1); //removing last "," |
| 273 | s += QLatin1Char(']'); |
| 274 | |
| 275 | return s; |
| 276 | } |
| 277 | |
| 278 | QStringList SplineEditor::presetNames() const |
| 279 | { |
| 280 | return m_presets.keys(); |
| 281 | } |
| 282 | |
| 283 | QWidget *SplineEditor::pointListWidget() |
| 284 | { |
| 285 | if (!m_pointListWidget) { |
| 286 | setupPointListWidget(); |
| 287 | } |
| 288 | |
| 289 | return m_pointListWidget; |
| 290 | } |
| 291 | |
| 292 | int SplineEditor::findControlPoint(const QPoint &point) |
| 293 | { |
| 294 | int pointIndex = -1; |
| 295 | qreal distance = -1; |
| 296 | for (int i = 0; i<m_controlPoints.size() - 1; ++i) { |
| 297 | qreal d = QLineF(point, mapToCanvas(point: m_controlPoints.at(i))).length(); |
| 298 | if ((distance < 0 && d < 10) || d < distance) { |
| 299 | distance = d; |
| 300 | pointIndex = i; |
| 301 | } |
| 302 | } |
| 303 | return pointIndex; |
| 304 | } |
| 305 | |
| 306 | static inline bool veryFuzzyCompare(qreal r1, qreal r2) |
| 307 | { |
| 308 | if (qFuzzyCompare(p1: r1, p2: 2)) |
| 309 | return true; |
| 310 | |
| 311 | int r1i = qRound(d: r1 * 20); |
| 312 | int r2i = qRound(d: r2 * 20); |
| 313 | |
| 314 | if (qFuzzyCompare(p1: qreal(r1i) / 20, p2: qreal(r2i) / 20)) |
| 315 | return true; |
| 316 | |
| 317 | return false; |
| 318 | } |
| 319 | |
| 320 | bool SplineEditor::isSmooth(int i) const |
| 321 | { |
| 322 | if (i == 0) |
| 323 | return false; |
| 324 | |
| 325 | QPointF p = m_controlPoints.at(i); |
| 326 | QPointF p_before = m_controlPoints.at(i: i - 1); |
| 327 | QPointF p_after = m_controlPoints.at(i: i + 1); |
| 328 | |
| 329 | QPointF v1 = p_after - p; |
| 330 | v1 = v1 / v1.manhattanLength(); //normalize |
| 331 | |
| 332 | QPointF v2 = p - p_before; |
| 333 | v2 = v2 / v2.manhattanLength(); //normalize |
| 334 | |
| 335 | return veryFuzzyCompare(r1: v1.x(), r2: v2.x()) && veryFuzzyCompare(r1: v1.y(), r2: v2.y()); |
| 336 | } |
| 337 | |
| 338 | void SplineEditor::smoothPoint(int index) |
| 339 | { |
| 340 | if (m_smoothAction->isChecked()) { |
| 341 | |
| 342 | QPointF before = QPointF(0,0); |
| 343 | if (index > 3) |
| 344 | before = m_controlPoints.at(i: index - 3); |
| 345 | |
| 346 | QPointF after = QPointF(1.0, 1.0); |
| 347 | if ((index + 3) < m_controlPoints.size()) |
| 348 | after = m_controlPoints.at(i: index + 3); |
| 349 | |
| 350 | QPointF tangent = (after - before) / 6; |
| 351 | |
| 352 | QPointF thisPoint = m_controlPoints.at(i: index); |
| 353 | |
| 354 | if (index > 0) |
| 355 | m_controlPoints[index - 1] = thisPoint - tangent; |
| 356 | |
| 357 | if (index + 1 < m_controlPoints.size()) |
| 358 | m_controlPoints[index + 1] = thisPoint + tangent; |
| 359 | |
| 360 | m_smoothList[index / 3] = true; |
| 361 | } else { |
| 362 | m_smoothList[index / 3] = false; |
| 363 | } |
| 364 | invalidate(); |
| 365 | update(); |
| 366 | } |
| 367 | |
| 368 | void SplineEditor::cornerPoint(int index) |
| 369 | { |
| 370 | QPointF before = QPointF(0,0); |
| 371 | if (index > 3) |
| 372 | before = m_controlPoints.at(i: index - 3); |
| 373 | |
| 374 | QPointF after = QPointF(1.0, 1.0); |
| 375 | if ((index + 3) < m_controlPoints.size()) |
| 376 | after = m_controlPoints.at(i: index + 3); |
| 377 | |
| 378 | QPointF thisPoint = m_controlPoints.at(i: index); |
| 379 | |
| 380 | if (index > 0) |
| 381 | m_controlPoints[index - 1] = (before - thisPoint) / 3 + thisPoint; |
| 382 | |
| 383 | if (index + 1 < m_controlPoints.size()) |
| 384 | m_controlPoints[index + 1] = (after - thisPoint) / 3 + thisPoint; |
| 385 | |
| 386 | m_smoothList[(index) / 3] = false; |
| 387 | invalidate(); |
| 388 | } |
| 389 | |
| 390 | void SplineEditor::deletePoint(int index) |
| 391 | { |
| 392 | m_controlPoints.remove(i: index - 1, n: 3); |
| 393 | m_numberOfSegments--; |
| 394 | |
| 395 | invalidateSmoothList(); |
| 396 | setupPointListWidget(); |
| 397 | invalidate(); |
| 398 | } |
| 399 | |
| 400 | void SplineEditor::addPoint(const QPointF point) |
| 401 | { |
| 402 | QPointF newPos = mapFromCanvas(point); |
| 403 | int splitIndex = 0; |
| 404 | for (int i=0; i < m_controlPoints.size() - 1; ++i) { |
| 405 | if (indexIsRealPoint(i) && m_controlPoints.at(i).x() > newPos.x()) { |
| 406 | break; |
| 407 | } else if (indexIsRealPoint(i)) |
| 408 | splitIndex = i; |
| 409 | } |
| 410 | QPointF before = QPointF(0,0); |
| 411 | if (splitIndex > 0) |
| 412 | before = m_controlPoints.at(i: splitIndex); |
| 413 | |
| 414 | QPointF after = QPointF(1.0, 1.0); |
| 415 | if ((splitIndex + 3) < m_controlPoints.size()) |
| 416 | after = m_controlPoints.at(i: splitIndex + 3); |
| 417 | |
| 418 | if (splitIndex > 0) { |
| 419 | m_controlPoints.insert(i: splitIndex + 2, t: (newPos + after) / 2); |
| 420 | m_controlPoints.insert(i: splitIndex + 2, t: newPos); |
| 421 | m_controlPoints.insert(i: splitIndex + 2, t: (newPos + before) / 2); |
| 422 | } else { |
| 423 | m_controlPoints.insert(i: splitIndex + 1, t: (newPos + after) / 2); |
| 424 | m_controlPoints.insert(i: splitIndex + 1, t: newPos); |
| 425 | m_controlPoints.insert(i: splitIndex + 1, t: (newPos + before) / 2); |
| 426 | } |
| 427 | m_numberOfSegments++; |
| 428 | |
| 429 | invalidateSmoothList(); |
| 430 | setupPointListWidget(); |
| 431 | invalidate(); |
| 432 | } |
| 433 | |
| 434 | void SplineEditor::initPresets() |
| 435 | { |
| 436 | const QPointF endPoint(1.0, 1.0); |
| 437 | { |
| 438 | QEasingCurve easingCurve(QEasingCurve::BezierSpline); |
| 439 | easingCurve.addCubicBezierSegment(c1: QPointF(0.4, 0.075), c2: QPointF(0.45, 0.24), endPoint: QPointF(0.5, 0.5)); |
| 440 | easingCurve.addCubicBezierSegment(c1: QPointF(0.55, 0.76), c2: QPointF(0.7, 0.9), endPoint); |
| 441 | m_presets.insert(key: tr(s: "Standard Easing" ), value: easingCurve); |
| 442 | } |
| 443 | |
| 444 | { |
| 445 | QEasingCurve easingCurve(QEasingCurve::BezierSpline); |
| 446 | easingCurve.addCubicBezierSegment(c1: QPointF(0.43, 0.0025), c2: QPointF(0.65, 1), endPoint); |
| 447 | m_presets.insert(key: tr(s: "Simple" ), value: easingCurve); |
| 448 | } |
| 449 | |
| 450 | { |
| 451 | QEasingCurve easingCurve(QEasingCurve::BezierSpline); |
| 452 | easingCurve.addCubicBezierSegment(c1: QPointF(0.43, 0.0025), c2: QPointF(0.38, 0.51), endPoint: QPointF(0.57, 0.99)); |
| 453 | easingCurve.addCubicBezierSegment(c1: QPointF(0.8, 0.69), c2: QPointF(0.65, 1), endPoint); |
| 454 | m_presets.insert(key: tr(s: "Simple Bounce" ), value: easingCurve); |
| 455 | } |
| 456 | |
| 457 | { |
| 458 | QEasingCurve easingCurve(QEasingCurve::BezierSpline); |
| 459 | easingCurve.addCubicBezierSegment(c1: QPointF(0.4, 0.075), c2: QPointF(0.64, -0.0025), endPoint: QPointF(0.74, 0.23)); |
| 460 | easingCurve.addCubicBezierSegment(c1: QPointF(0.84, 0.46), c2: QPointF(0.91, 0.77), endPoint); |
| 461 | m_presets.insert(key: tr(s: "Slow in fast out" ), value: easingCurve); |
| 462 | } |
| 463 | |
| 464 | { |
| 465 | QEasingCurve easingCurve(QEasingCurve::BezierSpline); |
| 466 | easingCurve.addCubicBezierSegment(c1: QPointF(0.43, 0.0025), c2: QPointF(0.47, 0.51), endPoint: QPointF(0.59, 0.94)); |
| 467 | easingCurve.addCubicBezierSegment(c1: QPointF(0.84, 0.95), c2: QPointF( 0.99, 0.94), endPoint); |
| 468 | m_presets.insert(key: tr(s: "Snapping" ), value: easingCurve); |
| 469 | } |
| 470 | |
| 471 | { |
| 472 | QEasingCurve easingCurve(QEasingCurve::BezierSpline); |
| 473 | easingCurve.addCubicBezierSegment(c1: QPointF( 0.38, 0.35),c2: QPointF(0.38, 0.7), endPoint: QPointF(0.45, 0.99)); |
| 474 | easingCurve.addCubicBezierSegment(c1: QPointF(0.48, 0.66), c2: QPointF(0.62, 0.62), endPoint: QPointF(0.66, 0.99)); |
| 475 | easingCurve.addCubicBezierSegment(c1: QPointF(0.69, 0.76), c2: QPointF(0.77, 0.76), endPoint: QPointF(0.79, 0.99)); |
| 476 | easingCurve.addCubicBezierSegment(c1: QPointF(0.83, 0.91), c2: QPointF(0.87, 0.92), endPoint: QPointF(0.91, 0.99)); |
| 477 | easingCurve.addCubicBezierSegment(c1: QPointF(0.95, 0.95), c2: QPointF(0.97, 0.94), endPoint); |
| 478 | m_presets.insert(key: tr(s: "Complex Bounce" ), value: easingCurve); |
| 479 | } |
| 480 | |
| 481 | { |
| 482 | QEasingCurve easingCurve4(QEasingCurve::BezierSpline); |
| 483 | easingCurve4.addCubicBezierSegment(c1: QPointF(0.12, -0.12),c2: QPointF(0.23, -0.19), endPoint: QPointF( 0.35, -0.09)); |
| 484 | easingCurve4.addCubicBezierSegment(c1: QPointF(0.47, 0.005), c2: QPointF(0.52, 1), endPoint: QPointF(0.62, 1.1)); |
| 485 | easingCurve4.addCubicBezierSegment(c1: QPointF(0.73, 1.2), c2: QPointF(0.91,1 ), endPoint); |
| 486 | m_presets.insert(key: tr(s: "Overshoot" ), value: easingCurve4); |
| 487 | } |
| 488 | } |
| 489 | |
| 490 | void SplineEditor::setupPointListWidget() |
| 491 | { |
| 492 | if (!m_pointListWidget) |
| 493 | m_pointListWidget = new QScrollArea(this); |
| 494 | |
| 495 | if (m_pointListWidget->widget()) |
| 496 | delete m_pointListWidget->widget(); |
| 497 | |
| 498 | m_pointListWidget->setFrameStyle(QFrame::NoFrame); |
| 499 | m_pointListWidget->setWidgetResizable(true); |
| 500 | m_pointListWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); |
| 501 | |
| 502 | m_pointListWidget->setWidget(new QWidget(m_pointListWidget)); |
| 503 | QVBoxLayout *layout = new QVBoxLayout(m_pointListWidget->widget()); |
| 504 | layout->setContentsMargins(QMargins()); |
| 505 | layout->setSpacing(2); |
| 506 | m_pointListWidget->widget()->setLayout(layout); |
| 507 | |
| 508 | m_segmentProperties.clear(); |
| 509 | |
| 510 | { //implicit 0,0 |
| 511 | QWidget *widget = new QWidget(m_pointListWidget->widget()); |
| 512 | Ui_Pane pane; |
| 513 | pane.setupUi(widget); |
| 514 | pane.p1_x->setValue(0); |
| 515 | pane.p1_y->setValue(0); |
| 516 | layout->addWidget(widget); |
| 517 | pane.label->setText("p0" ); |
| 518 | widget->setEnabled(false); |
| 519 | } |
| 520 | |
| 521 | for (int i = 0; i < m_numberOfSegments; ++i) { |
| 522 | SegmentProperties *segmentProperties = new SegmentProperties(m_pointListWidget->widget()); |
| 523 | layout->addWidget(segmentProperties); |
| 524 | bool smooth = false; |
| 525 | if (i < (m_numberOfSegments - 1)) { |
| 526 | smooth = m_smoothList.at(i); |
| 527 | } |
| 528 | segmentProperties->setSegment(i, m_controlPoints.mid(pos: i * 3, len: 3), smooth, i == (m_numberOfSegments - 1)); |
| 529 | segmentProperties->setSplineEditor(this); |
| 530 | m_segmentProperties << segmentProperties; |
| 531 | } |
| 532 | layout->addSpacerItem(new QSpacerItem(10, 10, QSizePolicy::Expanding, QSizePolicy::Expanding)); |
| 533 | |
| 534 | m_pointListWidget->viewport()->show(); |
| 535 | m_pointListWidget->viewport()->setSizePolicy(hor: QSizePolicy::Minimum, ver: QSizePolicy::Minimum); |
| 536 | m_pointListWidget->show(); |
| 537 | } |
| 538 | |
| 539 | bool SplineEditor::isControlPointSmooth(int i) const |
| 540 | { |
| 541 | if (i == 0) |
| 542 | return false; |
| 543 | |
| 544 | if (i == m_controlPoints.size() - 1) |
| 545 | return false; |
| 546 | |
| 547 | if (m_numberOfSegments == 1) |
| 548 | return false; |
| 549 | |
| 550 | int index = pointForControlPoint(i); |
| 551 | |
| 552 | if (index == 0) |
| 553 | return false; |
| 554 | |
| 555 | if (index == m_controlPoints.size() - 1) |
| 556 | return false; |
| 557 | |
| 558 | return m_smoothList.at(i: index / 3); |
| 559 | } |
| 560 | |
| 561 | QPointF limitToCanvas(const QPointF point) |
| 562 | { |
| 563 | qreal left = -qreal( canvasMargin) / qreal(canvasWidth); |
| 564 | qreal width = 1.0 - 2.0 * left; |
| 565 | |
| 566 | qreal top = -qreal( canvasMargin) / qreal(canvasHeight); |
| 567 | qreal height = 1.0 - 2.0 * top; |
| 568 | |
| 569 | QPointF p = point; |
| 570 | QRectF r(left, top, width, height); |
| 571 | |
| 572 | if (p.x() > r.right()) { |
| 573 | p.setX(r.right()); |
| 574 | } |
| 575 | if (p.x() < r.left()) { |
| 576 | p.setX(r.left()); |
| 577 | } |
| 578 | if (p.y() < r.top()) { |
| 579 | p.setY(r.top()); |
| 580 | } |
| 581 | if (p.y() > r.bottom()) { |
| 582 | p.setY(r.bottom()); |
| 583 | } |
| 584 | return p; |
| 585 | } |
| 586 | |
| 587 | void SplineEditor::mouseMoveEvent(QMouseEvent *e) |
| 588 | { |
| 589 | // If we've moved more then 25 pixels, assume user is dragging |
| 590 | if (!m_mouseDrag && QPoint(m_mousePress - e->position().toPoint()).manhattanLength() > qApp->startDragDistance()) |
| 591 | m_mouseDrag = true; |
| 592 | |
| 593 | QPointF p = mapFromCanvas(point: e->position().toPoint()); |
| 594 | |
| 595 | if (m_mouseDrag && m_activeControlPoint >= 0 && m_activeControlPoint < m_controlPoints.size()) { |
| 596 | p = limitToCanvas(point: p); |
| 597 | if (indexIsRealPoint(i: m_activeControlPoint)) { |
| 598 | //move also the tangents |
| 599 | QPointF targetPoint = p; |
| 600 | QPointF distance = targetPoint - m_controlPoints.at(i: m_activeControlPoint); |
| 601 | m_controlPoints[m_activeControlPoint] = targetPoint; |
| 602 | m_controlPoints[m_activeControlPoint - 1] += distance; |
| 603 | m_controlPoints[m_activeControlPoint + 1] += distance; |
| 604 | } else { |
| 605 | if (!isControlPointSmooth(i: m_activeControlPoint)) { |
| 606 | m_controlPoints[m_activeControlPoint] = p; |
| 607 | } else { |
| 608 | QPointF targetPoint = p; |
| 609 | QPointF distance = targetPoint - m_controlPoints.at(i: m_activeControlPoint); |
| 610 | m_controlPoints[m_activeControlPoint] = p; |
| 611 | |
| 612 | if ((m_activeControlPoint > 1) && (m_activeControlPoint % 3) == 0) { //right control point |
| 613 | m_controlPoints[m_activeControlPoint - 2] -= distance; |
| 614 | } else if ((m_activeControlPoint < (m_controlPoints.size() - 2)) //left control point |
| 615 | && (m_activeControlPoint % 3) == 1) { |
| 616 | m_controlPoints[m_activeControlPoint + 2] -= distance; |
| 617 | } |
| 618 | } |
| 619 | } |
| 620 | invalidate(); |
| 621 | } |
| 622 | } |
| 623 | |
| 624 | void SplineEditor::setEasingCurve(const QEasingCurve &easingCurve) |
| 625 | { |
| 626 | if (m_easingCurve == easingCurve) |
| 627 | return; |
| 628 | m_block = true; |
| 629 | m_easingCurve = easingCurve; |
| 630 | m_controlPoints = m_easingCurve.toCubicSpline(); |
| 631 | m_numberOfSegments = m_controlPoints.size() / 3; |
| 632 | update(); |
| 633 | emit easingCurveChanged(); |
| 634 | |
| 635 | const QString code = generateCode(); |
| 636 | emit easingCurveCodeChanged(code); |
| 637 | |
| 638 | m_block = false; |
| 639 | } |
| 640 | |
| 641 | void SplineEditor::setPreset(const QString &name) |
| 642 | { |
| 643 | setEasingCurve(m_presets.value(key: name)); |
| 644 | invalidateSmoothList(); |
| 645 | setupPointListWidget(); |
| 646 | } |
| 647 | |
| 648 | void SplineEditor::setEasingCurve(const QString &code) |
| 649 | { |
| 650 | if (m_block) |
| 651 | return; |
| 652 | if (code.startsWith(c: QLatin1Char('[')) && code.endsWith(c: QLatin1Char(']'))) { |
| 653 | const auto cleanCode = QStringView(code).mid(pos: 1, n: code.size() - 2); |
| 654 | const auto stringList = cleanCode.split(sep: QLatin1Char(','), behavior: Qt::SkipEmptyParts); |
| 655 | if (stringList.size() >= 6 && (stringList.size() % 6 == 0)) { |
| 656 | QVector<qreal> realList; |
| 657 | realList.reserve(asize: stringList.size()); |
| 658 | for (const QStringView &string : stringList) { |
| 659 | bool ok; |
| 660 | realList.append(t: string.toDouble(ok: &ok)); |
| 661 | if (!ok) |
| 662 | return; |
| 663 | } |
| 664 | QVector<QPointF> points; |
| 665 | const int count = realList.size() / 2; |
| 666 | points.reserve(asize: count); |
| 667 | for (int i = 0; i < count; ++i) |
| 668 | points.append(t: QPointF(realList.at(i: i * 2), realList.at(i: i * 2 + 1))); |
| 669 | if (points.constLast() == QPointF(1.0, 1.0)) { |
| 670 | QEasingCurve easingCurve(QEasingCurve::BezierSpline); |
| 671 | |
| 672 | for (int i = 0; i < points.size() / 3; ++i) { |
| 673 | easingCurve.addCubicBezierSegment(c1: points.at(i: i * 3), |
| 674 | c2: points.at(i: i * 3 + 1), |
| 675 | endPoint: points.at(i: i * 3 + 2)); |
| 676 | } |
| 677 | setEasingCurve(easingCurve); |
| 678 | invalidateSmoothList(); |
| 679 | setupPointListWidget(); |
| 680 | } |
| 681 | } |
| 682 | } |
| 683 | } |
| 684 | |
| 685 | #include "moc_splineeditor.cpp" |
| 686 | |