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 | |