1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qquickspringanimation_p.h"
5
6#include "qquickanimation_p_p.h"
7#include <private/qqmlproperty_p.h>
8#include "private/qcontinuinganimationgroupjob_p.h"
9
10#include <QtCore/qdebug.h>
11
12#include <private/qobject_p.h>
13
14#include <cmath>
15
16#define DELAY_STOP_TIMER_INTERVAL 32
17
18QT_BEGIN_NAMESPACE
19
20class QQuickSpringAnimationPrivate;
21class Q_AUTOTEST_EXPORT QSpringAnimation : public QAbstractAnimationJob
22{
23 Q_DISABLE_COPY(QSpringAnimation)
24public:
25 QSpringAnimation(QQuickSpringAnimationPrivate * = nullptr);
26
27 ~QSpringAnimation();
28 int duration() const override;
29 void restart();
30 void init();
31
32 qreal currentValue;
33 qreal to;
34 qreal velocity;
35 int startTime;
36 int dura;
37 int lastTime;
38 int stopTime;
39 enum Mode {
40 Track,
41 Velocity,
42 Spring
43 };
44 Mode mode;
45 QQmlProperty target;
46
47 qreal velocityms;
48 qreal maxVelocity;
49 qreal mass;
50 qreal spring;
51 qreal damping;
52 qreal epsilon;
53 qreal modulus;
54
55 bool useMass : 1;
56 bool haveModulus : 1;
57 bool skipUpdate : 1;
58 typedef QHash<QQmlProperty, QSpringAnimation*> ActiveAnimationHash;
59 typedef ActiveAnimationHash::Iterator ActiveAnimationHashIt;
60
61 void clearTemplate() { animationTemplate = nullptr; }
62
63protected:
64 void updateCurrentTime(int time) override;
65 void updateState(QAbstractAnimationJob::State, QAbstractAnimationJob::State) override;
66 void debugAnimation(QDebug d) const override;
67
68private:
69 QQuickSpringAnimationPrivate *animationTemplate;
70};
71
72class QQuickSpringAnimationPrivate : public QQuickPropertyAnimationPrivate
73{
74 Q_DECLARE_PUBLIC(QQuickSpringAnimation)
75public:
76 QQuickSpringAnimationPrivate()
77 : QQuickPropertyAnimationPrivate()
78 , velocityms(0)
79 , maxVelocity(0)
80 , mass(1.0)
81 , spring(0.)
82 , damping(0.)
83 , epsilon(0.01)
84 , modulus(0.0)
85 , useMass(false)
86 , haveModulus(false)
87 , mode(QSpringAnimation::Track)
88 { elapsed.start(); }
89
90 void updateMode();
91 qreal velocityms;
92 qreal maxVelocity;
93 qreal mass;
94 qreal spring;
95 qreal damping;
96 qreal epsilon;
97 qreal modulus;
98
99 bool useMass : 1;
100 bool haveModulus : 1;
101 QSpringAnimation::Mode mode;
102
103 QSpringAnimation::ActiveAnimationHash activeAnimations;
104 QElapsedTimer elapsed;
105};
106
107QSpringAnimation::QSpringAnimation(QQuickSpringAnimationPrivate *priv)
108 : QAbstractAnimationJob()
109 , currentValue(0)
110 , to(0)
111 , velocity(0)
112 , startTime(0)
113 , dura(0)
114 , lastTime(0)
115 , stopTime(-1)
116 , mode(Track)
117 , velocityms(0)
118 , maxVelocity(0)
119 , mass(1.0)
120 , spring(0.)
121 , damping(0.)
122 , epsilon(0.01)
123 , modulus(0.0)
124 , useMass(false)
125 , haveModulus(false)
126 , skipUpdate(false)
127 , animationTemplate(priv)
128{
129}
130
131QSpringAnimation::~QSpringAnimation()
132{
133 if (animationTemplate) {
134 if (target.object()) {
135 ActiveAnimationHashIt it = animationTemplate->activeAnimations.find(key: target);
136 if (it != animationTemplate->activeAnimations.end() && it.value() == this)
137 animationTemplate->activeAnimations.erase(it);
138 } else {
139 //target is no longer valid, need to search linearly
140 for (ActiveAnimationHashIt it = animationTemplate->activeAnimations.begin(); it != animationTemplate->activeAnimations.end(); ++it) {
141 if (it.value() == this) {
142 animationTemplate->activeAnimations.erase(it);
143 break;
144 }
145 }
146 }
147 }
148}
149
150int QSpringAnimation::duration() const
151{
152 return -1;
153}
154
155void QSpringAnimation::restart()
156{
157 if (isRunning() || (stopTime != -1 && (animationTemplate->elapsed.elapsed() - stopTime) < DELAY_STOP_TIMER_INTERVAL)) {
158 skipUpdate = true;
159 init();
160 } else {
161 skipUpdate = false;
162 //init() will be triggered when group starts
163 }
164}
165
166void QSpringAnimation::init()
167{
168 lastTime = startTime = 0;
169 stopTime = -1;
170}
171
172void QSpringAnimation::updateCurrentTime(int time)
173{
174 if (skipUpdate) {
175 skipUpdate = false;
176 return;
177 }
178
179 if (mode == Track) {
180 stop();
181 return;
182 }
183
184 int elapsed = time - lastTime;
185
186 if (!elapsed)
187 return;
188
189 int count = elapsed / 16;
190
191 if (mode == Spring) {
192 if (elapsed < 16) // capped at 62fps.
193 return;
194 lastTime = time - (elapsed - count * 16);
195 } else {
196 lastTime = time;
197 }
198
199 qreal srcVal = to;
200
201 bool stopped = false;
202
203 if (haveModulus) {
204 currentValue = fmod(x: currentValue, y: modulus);
205 srcVal = fmod(x: srcVal, y: modulus);
206 }
207 if (mode == Spring) {
208 // Real men solve the spring DEs using RK4.
209 // We'll do something much simpler which gives a result that looks fine.
210 for (int i = 0; i < count; ++i) {
211 qreal diff = srcVal - currentValue;
212 if (haveModulus && qAbs(t: diff) > modulus / 2) {
213 if (diff < 0)
214 diff += modulus;
215 else
216 diff -= modulus;
217 }
218 if (useMass)
219 velocity = velocity + (spring * diff - damping * velocity) / mass;
220 else
221 velocity = velocity + spring * diff - damping * velocity;
222 if (maxVelocity > 0.) {
223 // limit velocity
224 if (velocity > maxVelocity)
225 velocity = maxVelocity;
226 else if (velocity < -maxVelocity)
227 velocity = -maxVelocity;
228 }
229 currentValue += velocity * 16.0 / 1000.0;
230 if (haveModulus) {
231 currentValue = fmod(x: currentValue, y: modulus);
232 if (currentValue < 0.0)
233 currentValue += modulus;
234 }
235 }
236 if (qAbs(t: velocity) < epsilon && qAbs(t: srcVal - currentValue) < epsilon) {
237 velocity = 0.0;
238 currentValue = srcVal;
239 stopped = true;
240 }
241 } else {
242 qreal moveBy = elapsed * velocityms;
243 qreal diff = srcVal - currentValue;
244 if (haveModulus && qAbs(t: diff) > modulus / 2) {
245 if (diff < 0)
246 diff += modulus;
247 else
248 diff -= modulus;
249 }
250 if (diff > 0) {
251 currentValue += moveBy;
252 if (haveModulus)
253 currentValue = std::fmod(x: currentValue, y: modulus);
254 } else {
255 currentValue -= moveBy;
256 if (haveModulus && currentValue < 0.0)
257 currentValue = std::fmod(x: currentValue, y: modulus) + modulus;
258 }
259 if (lastTime - startTime >= dura) {
260 currentValue = to;
261 stopped = true;
262 }
263 }
264
265 qreal old_to = to;
266
267 QQmlPropertyPrivate::write(that: target, currentValue,
268 QQmlPropertyData::BypassInterceptor |
269 QQmlPropertyData::DontRemoveBinding);
270
271 if (stopped && old_to == to) { // do not stop if we got restarted
272 if (animationTemplate)
273 stopTime = animationTemplate->elapsed.elapsed();
274 stop();
275 }
276}
277
278void QSpringAnimation::updateState(QAbstractAnimationJob::State newState, QAbstractAnimationJob::State /*oldState*/)
279{
280 if (newState == QAbstractAnimationJob::Running)
281 init();
282}
283
284void QSpringAnimation::debugAnimation(QDebug d) const
285{
286 d << "SpringAnimationJob(" << Qt::hex << (const void *) this << Qt::dec << ")" << "velocity:" << maxVelocity
287 << "spring:" << spring << "damping:" << damping << "epsilon:" << epsilon << "modulus:" << modulus
288 << "mass:" << mass << "target:" << target.object() << "property:" << target.name()
289 << "to:" << to << "current velocity:" << velocity;
290}
291
292
293void QQuickSpringAnimationPrivate::updateMode()
294{
295 if (spring == 0. && maxVelocity == 0.)
296 mode = QSpringAnimation::Track;
297 else if (spring > 0.)
298 mode = QSpringAnimation::Spring;
299 else {
300 mode = QSpringAnimation::Velocity;
301 for (QSpringAnimation::ActiveAnimationHashIt it = activeAnimations.begin(), end = activeAnimations.end(); it != end; ++it) {
302 QSpringAnimation *animation = *it;
303 animation->startTime = animation->lastTime;
304 qreal dist = qAbs(t: animation->currentValue - animation->to);
305 if (haveModulus && dist > modulus / 2)
306 dist = modulus - fmod(x: dist, y: modulus);
307 animation->dura = dist / velocityms;
308 }
309 }
310}
311
312/*!
313 \qmltype SpringAnimation
314 \instantiates QQuickSpringAnimation
315 \inqmlmodule QtQuick
316 \ingroup qtquick-transitions-animations
317 \inherits NumberAnimation
318
319 \brief Allows a property to track a value in a spring-like motion.
320
321 SpringAnimation mimics the oscillatory behavior of a spring, with the appropriate \l spring constant to
322 control the acceleration and the \l damping to control how quickly the effect dies away.
323
324 You can also limit the maximum \l velocity of the animation.
325
326 The following \l Rectangle moves to the position of the mouse using a
327 SpringAnimation when the mouse is clicked. The use of the \l Behavior
328 on the \c x and \c y values indicates that whenever these values are
329 changed, a SpringAnimation should be applied.
330
331 \snippet qml/springanimation.qml 0
332
333 Like any other animation type, a SpringAnimation can be applied in a
334 number of ways, including transitions, behaviors and property value
335 sources. The \l {Animation and Transitions in Qt Quick} documentation shows a
336 variety of methods for creating animations.
337
338 \sa SmoothedAnimation, {Animation and Transitions in Qt Quick}, {Qt Quick Examples - Animation}, {Qt Quick Demo - Clocks}
339*/
340
341QQuickSpringAnimation::QQuickSpringAnimation(QObject *parent)
342: QQuickNumberAnimation(*(new QQuickSpringAnimationPrivate),parent)
343{
344}
345
346QQuickSpringAnimation::~QQuickSpringAnimation()
347{
348 Q_D(QQuickSpringAnimation);
349 for (QSpringAnimation::ActiveAnimationHashIt it = d->activeAnimations.begin(), end = d->activeAnimations.end(); it != end; ++it)
350 it.value()->clearTemplate();
351}
352
353/*!
354 \qmlproperty real QtQuick::SpringAnimation::velocity
355
356 This property holds the maximum velocity allowed when tracking the source.
357
358 The default value is 0 (no maximum velocity).
359*/
360
361qreal QQuickSpringAnimation::velocity() const
362{
363 Q_D(const QQuickSpringAnimation);
364 return d->maxVelocity;
365}
366
367void QQuickSpringAnimation::setVelocity(qreal velocity)
368{
369 Q_D(QQuickSpringAnimation);
370 d->maxVelocity = velocity;
371 d->velocityms = velocity / 1000.0;
372 d->updateMode();
373}
374
375/*!
376 \qmlproperty real QtQuick::SpringAnimation::spring
377
378 This property describes how strongly the target is pulled towards the
379 source. The default value is 0 (that is, the spring-like motion is disabled).
380
381 The useful value range is 0 - 5.0.
382
383 When this property is set and the \l velocity value is greater than 0,
384 the \l velocity limits the maximum speed.
385*/
386qreal QQuickSpringAnimation::spring() const
387{
388 Q_D(const QQuickSpringAnimation);
389 return d->spring;
390}
391
392void QQuickSpringAnimation::setSpring(qreal spring)
393{
394 Q_D(QQuickSpringAnimation);
395 d->spring = spring;
396 d->updateMode();
397}
398
399/*!
400 \qmlproperty real QtQuick::SpringAnimation::damping
401 This property holds the spring damping value.
402
403 This value describes how quickly the spring-like motion comes to rest.
404 The default value is 0.
405
406 The useful value range is 0 - 1.0. The lower the value, the faster it
407 comes to rest.
408*/
409qreal QQuickSpringAnimation::damping() const
410{
411 Q_D(const QQuickSpringAnimation);
412 return d->damping;
413}
414
415void QQuickSpringAnimation::setDamping(qreal damping)
416{
417 Q_D(QQuickSpringAnimation);
418 if (damping > 1.)
419 damping = 1.;
420
421 d->damping = damping;
422}
423
424
425/*!
426 \qmlproperty real QtQuick::SpringAnimation::epsilon
427 This property holds the spring epsilon.
428
429 The epsilon is the rate and amount of change in the value which is close enough
430 to 0 to be considered equal to zero. This will depend on the usage of the value.
431 For pixel positions, 0.25 would suffice. For scale, 0.005 will suffice.
432
433 The default is 0.01. Tuning this value can provide small performance improvements.
434*/
435qreal QQuickSpringAnimation::epsilon() const
436{
437 Q_D(const QQuickSpringAnimation);
438 return d->epsilon;
439}
440
441void QQuickSpringAnimation::setEpsilon(qreal epsilon)
442{
443 Q_D(QQuickSpringAnimation);
444 d->epsilon = epsilon;
445}
446
447/*!
448 \qmlproperty real QtQuick::SpringAnimation::modulus
449 This property holds the modulus value. The default value is 0.
450
451 Setting a \a modulus forces the target value to "wrap around" at the modulus.
452 For example, setting the modulus to 360 will cause a value of 370 to wrap around to 10.
453*/
454qreal QQuickSpringAnimation::modulus() const
455{
456 Q_D(const QQuickSpringAnimation);
457 return d->modulus;
458}
459
460void QQuickSpringAnimation::setModulus(qreal modulus)
461{
462 Q_D(QQuickSpringAnimation);
463 if (d->modulus != modulus) {
464 d->haveModulus = modulus != 0.0;
465 d->modulus = modulus;
466 d->updateMode();
467 emit modulusChanged();
468 }
469}
470
471/*!
472 \qmlproperty real QtQuick::SpringAnimation::mass
473 This property holds the "mass" of the property being moved.
474
475 The value is 1.0 by default.
476
477 A greater mass causes slower movement and a greater spring-like
478 motion when an item comes to rest.
479*/
480qreal QQuickSpringAnimation::mass() const
481{
482 Q_D(const QQuickSpringAnimation);
483 return d->mass;
484}
485
486void QQuickSpringAnimation::setMass(qreal mass)
487{
488 Q_D(QQuickSpringAnimation);
489 if (d->mass != mass && mass > 0.0) {
490 d->useMass = mass != 1.0;
491 d->mass = mass;
492 emit massChanged();
493 }
494}
495
496QAbstractAnimationJob* QQuickSpringAnimation::transition(QQuickStateActions &actions,
497 QQmlProperties &modified,
498 TransitionDirection direction,
499 QObject *defaultTarget)
500{
501 Q_D(QQuickSpringAnimation);
502 Q_UNUSED(direction);
503
504 QContinuingAnimationGroupJob *wrapperGroup = new QContinuingAnimationGroupJob();
505
506 QQuickStateActions dataActions = QQuickNumberAnimation::createTransitionActions(actions, modified, defaultTarget);
507 if (!dataActions.isEmpty()) {
508 QSet<QAbstractAnimationJob*> anims;
509 for (int i = 0; i < dataActions.size(); ++i) {
510 QSpringAnimation *animation;
511 bool needsRestart = false;
512 const QQmlProperty &property = dataActions.at(i).property;
513 if (d->activeAnimations.contains(key: property)) {
514 animation = d->activeAnimations[property];
515 needsRestart = true;
516 } else {
517 animation = new QSpringAnimation(d);
518 d->activeAnimations.insert(key: property, value: animation);
519 animation->target = property;
520 }
521 wrapperGroup->appendAnimation(animation: initInstance(animation));
522
523 animation->to = dataActions.at(i).toValue.toReal();
524 animation->startTime = 0;
525 animation->velocityms = d->velocityms;
526 animation->mass = d->mass;
527 animation->spring = d->spring;
528 animation->damping = d->damping;
529 animation->epsilon = d->epsilon;
530 animation->modulus = d->modulus;
531 animation->useMass = d->useMass;
532 animation->haveModulus = d->haveModulus;
533 animation->mode = d->mode;
534 animation->dura = -1;
535 animation->maxVelocity = d->maxVelocity;
536
537 if (d->fromIsDefined)
538 animation->currentValue = dataActions.at(i).fromValue.toReal();
539 else
540 animation->currentValue = property.read().toReal();
541 if (animation->mode == QSpringAnimation::Velocity) {
542 qreal dist = qAbs(t: animation->currentValue - animation->to);
543 if (d->haveModulus && dist > d->modulus / 2)
544 dist = d->modulus - fmod(x: dist, y: d->modulus);
545 animation->dura = dist / animation->velocityms;
546 }
547
548 if (needsRestart)
549 animation->restart();
550 anims.insert(value: animation);
551 }
552 const auto copy = d->activeAnimations;
553 for (QSpringAnimation *anim : copy) {
554 if (!anims.contains(value: anim)) {
555 anim->clearTemplate();
556 d->activeAnimations.remove(key: anim->target);
557 }
558 }
559 }
560 return wrapperGroup;
561}
562
563QT_END_NAMESPACE
564
565#include "moc_qquickspringanimation_p.cpp"
566

source code of qtdeclarative/src/quick/util/qquickspringanimation.cpp