1// Copyright (C) 2022 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 "qquickframeanimation_p.h"
5
6#include <QtCore/qcoreapplication.h>
7#include <QtCore/qelapsedtimer.h>
8#include "private/qabstractanimationjob_p.h"
9#include <private/qobject_p.h>
10#include <qdebug.h>
11
12QT_BEGIN_NAMESPACE
13
14class QFrameAnimationJob : public QAbstractAnimationJob
15{
16 int duration() const override {
17 return 1;
18 }
19};
20
21class QQuickFrameAnimationPrivate : public QObjectPrivate, public QAnimationJobChangeListener
22{
23 Q_DECLARE_PUBLIC(QQuickFrameAnimation)
24public:
25 QQuickFrameAnimationPrivate() {}
26
27 void animationCurrentLoopChanged(QAbstractAnimationJob *) override {
28 maybeTick();
29 }
30
31 void maybeTick()
32 {
33 Q_Q(QQuickFrameAnimation);
34 if (!running || paused)
35 return;
36
37 qint64 elapsedTimeNs = elapsedTimer.nsecsElapsed();
38 qint64 frameTimeNs = elapsedTimeNs - prevElapsedTimeNs;
39 if (prevFrameTimeNs != frameTimeNs) {
40 frameTime = qreal(frameTimeNs) / 1000000000.0;
41 Q_EMIT q->frameTimeChanged();
42 }
43
44 const qreal f = 0.1;
45 qreal newSmoothFrameTime = f * frameTime + (1.0 - f) * smoothFrameTime;
46 if (!qFuzzyCompare(p1: newSmoothFrameTime, p2: smoothFrameTime)) {
47 smoothFrameTime = newSmoothFrameTime;
48 Q_EMIT q->smoothFrameTimeChanged();
49 }
50
51 q->setElapsedTime(elapsedTime + frameTime);
52
53 const int frame = (firstTick && currentFrame > 0) ? 0 : currentFrame + 1;
54 q->setCurrentFrame(frame);
55
56 prevElapsedTimeNs = elapsedTimeNs;
57 prevFrameTimeNs = frameTimeNs;
58 firstTick = false;
59
60 Q_EMIT q->triggered();
61 }
62
63 // Handle the running/pausing state updates.
64 void updateState()
65 {
66 if (!componentComplete)
67 return;
68
69 if (running && !paused) {
70 if (firstTick) {
71 elapsedTime = 0;
72 elapsedTimer.start();
73 }
74 prevElapsedTimeNs = elapsedTimer.nsecsElapsed();
75 frameJob.start();
76 } else {
77 frameJob.stop();
78 }
79 }
80
81private:
82 QFrameAnimationJob frameJob;
83 QElapsedTimer elapsedTimer;
84 int currentFrame = 0;
85 qreal frameTime = 0.0;
86 qreal smoothFrameTime = 0.0;
87 qreal elapsedTime = 0.0;
88 qint64 prevFrameTimeNs = 0;
89 qint64 prevElapsedTimeNs = 0;
90 bool running = false;
91 bool paused = false;
92 bool componentComplete = false;
93 bool firstTick = true;
94};
95
96/*!
97 \qmltype FrameAnimation
98 \instantiates QQuickFrameAnimation
99 \inqmlmodule QtQuick
100 \ingroup qtquick-interceptors
101 \since 6.4
102 \brief Triggers a handler at every animation frame update.
103
104 A FrameAnimation can be used to trigger an action every time animations
105 have progressed and an animation frame has been rendered. See the documentation
106 about the \l{qtquick-visualcanvas-scenegraph.html}{Scene Graph} for in-depth
107 information about the threaded and basic render loops.
108
109 For general animations, prefer using \c NumberAnimation and other \c Animation
110 elements as those provide declarative way to describe the animations.
111
112 FrameAnimation on the other hand should be used for custom imperative animations
113 and in use-cases like these:
114 \list
115 \li When you need to run some code on every frame update. Or e.g. every other frame,
116 maybe using progressive rendering.
117 \li When the speed / target is changing during the animation, normal QML animations
118 can be too limiting.
119 \li When more accurate frame update time is needed, e.g. for fps counter.
120 \endlist
121
122 Compared to \c Timer which allows to set the \c interval time, FrameAnimation runs
123 always in synchronization with the animation updates. If you have used \c Timer
124 with a short interval for custom animations like below, please consider switching
125 to use FrameAnimation instead for smoother animations.
126 \code
127 // BAD
128 Timer {
129 interval: 16
130 repeat: true
131 running: true
132 onTriggered: {
133 // Animate something
134 }
135 }
136
137 // GOOD
138 FrameAnimation {
139 running: true
140 onTriggered: {
141 // Animate something
142 }
143 }
144 \endcode
145*/
146QQuickFrameAnimation::QQuickFrameAnimation(QObject *parent)
147 : QObject(*(new QQuickFrameAnimationPrivate), parent)
148{
149 Q_D(QQuickFrameAnimation);
150 d->frameJob.addAnimationChangeListener(listener: d, QAbstractAnimationJob::CurrentLoop);
151 d->frameJob.setLoopCount(-1);
152}
153
154/*!
155 \qmlsignal QtQuick::FrameAnimation::triggered()
156
157 This signal is emitted when the FrameAnimation has progressed to a new frame.
158*/
159
160/*!
161 \qmlproperty bool QtQuick::FrameAnimation::running
162
163 If set to true, starts the frame animation; otherwise stops it.
164
165 \a running defaults to false.
166
167 \sa stop(), start(), restart()
168*/
169bool QQuickFrameAnimation::isRunning() const
170{
171 Q_D(const QQuickFrameAnimation);
172 return d->running;
173}
174
175void QQuickFrameAnimation::setRunning(bool running)
176{
177 Q_D(QQuickFrameAnimation);
178 if (d->running != running) {
179 d->running = running;
180 d->firstTick = true;
181 Q_EMIT runningChanged();
182 d->updateState();
183 }
184}
185
186/*!
187 \qmlproperty bool QtQuick::FrameAnimation::paused
188
189 If set to true, pauses the frame animation; otherwise resumes it.
190
191 \a paused defaults to false.
192
193 \sa pause(), resume()
194*/
195bool QQuickFrameAnimation::isPaused() const
196{
197 Q_D(const QQuickFrameAnimation);
198 return d->paused;
199}
200
201void QQuickFrameAnimation::setPaused(bool paused)
202{
203 Q_D(QQuickFrameAnimation);
204 if (d->paused != paused) {
205 d->paused = paused;
206 Q_EMIT pausedChanged();
207 d->updateState();
208 }
209}
210
211/*!
212 \qmlproperty int QtQuick::FrameAnimation::currentFrame
213 \readonly
214
215 This property holds the number of frame updates since the start.
216 When the frame animation is restarted, currentFrame starts from \c 0.
217
218 The following example shows how to react on frame updates.
219
220 \code
221 FrameAnimation {
222 running: true
223 onTriggered: {
224 // Run code on every frame update.
225 }
226 }
227 \endcode
228
229 This property can also be used for rendering only every nth frame. Consider an
230 advanced usage where the UI contains two heavy elements and to reach smooth 60fps
231 overall frame rate, you decide to render these heavy elements at 30fps, first one
232 on every even frames and second one on every odd frames:
233
234 \code
235 FrameAnimation {
236 running: true
237 onTriggered: {
238 if (currentFrame % 2 == 0)
239 updateUIElement1();
240 else
241 updateUIElement2();
242 }
243 }
244 \endcode
245
246 By default, \c frame is 0.
247*/
248int QQuickFrameAnimation::currentFrame() const
249{
250 Q_D(const QQuickFrameAnimation);
251 return d->currentFrame;
252}
253
254/*!
255 \qmlproperty qreal QtQuick::FrameAnimation::frameTime
256 \readonly
257
258 This property holds the time (in seconds) since the previous frame update.
259
260 The following example shows how to use frameTime to animate item with
261 varying speed, adjusting to screen refresh rates and possible fps drops.
262
263 \code
264 Rectangle {
265 id: rect
266 property real speed: 90
267 width: 100
268 height: 100
269 color: "red"
270 anchors.centerIn: parent
271 }
272
273 FrameAnimation {
274 id: frameAnimation
275 running: true
276 onTriggered: {
277 // Rotate the item speed-degrees / second.
278 rect.rotation += rect.speed * frameTime
279 }
280 }
281 \endcode
282
283 By default, \c frameTime is 0.
284*/
285qreal QQuickFrameAnimation::frameTime() const
286{
287 Q_D(const QQuickFrameAnimation);
288 return d->frameTime;
289}
290
291/*!
292 \qmlproperty qreal QtQuick::FrameAnimation::smoothFrameTime
293 \readonly
294
295 This property holds the smoothed time (in seconds) since the previous frame update.
296
297 The following example shows how to use smoothFrameTime to show average fps.
298
299 \code
300 Text {
301 text: "fps: " + frameAnimation.fps.toFixed(0)
302 }
303
304 FrameAnimation {
305 id: frameAnimation
306 property real fps: smoothFrameTime > 0 ? (1.0 / smoothFrameTime) : 0
307 running: true
308 }
309 \endcode
310
311 By default, \c smoothFrameTime is 0.
312*/
313qreal QQuickFrameAnimation::smoothFrameTime() const
314{
315 Q_D(const QQuickFrameAnimation);
316 return d->smoothFrameTime;
317}
318
319/*!
320 \qmlproperty qreal QtQuick::FrameAnimation::elapsedTime
321 \readonly
322
323 This property holds the time (in seconds) since the previous start.
324
325 By default, \c elapsedTime is 0.
326*/
327qreal QQuickFrameAnimation::elapsedTime() const
328{
329 Q_D(const QQuickFrameAnimation);
330 return d->elapsedTime;
331}
332
333/*!
334 \qmlmethod QtQuick::FrameAnimation::start()
335 \brief Starts the frame animation
336
337 If the frame animation is already running, calling this method has no effect. The
338 \c running property will be true following a call to \c start().
339*/
340void QQuickFrameAnimation::start()
341{
342 setRunning(true);
343}
344
345/*!
346 \qmlmethod QtQuick::FrameAnimation::stop()
347 \brief Stops the frame animation
348
349 If the frame animation is not running, calling this method has no effect. Both the \c running and
350 \c paused properties will be false following a call to \c stop().
351*/
352void QQuickFrameAnimation::stop()
353{
354 setRunning(false);
355 setPaused(false);
356}
357
358/*!
359 \qmlmethod QtQuick::FrameAnimation::restart()
360 \brief Restarts the frame animation
361
362 If the FrameAnimation is not running it will be started, otherwise it will be
363 stopped, reset to initial state and started. The \c running property
364 will be true following a call to \c restart().
365*/
366void QQuickFrameAnimation::restart()
367{
368 stop();
369 start();
370}
371
372/*!
373 \qmlmethod QtQuick::FrameAnimation::pause()
374 \brief Pauses the frame animation
375
376 If the frame animation is already paused or not \c running, calling this method has no effect.
377 The \c paused property will be true following a call to \c pause().
378*/
379void QQuickFrameAnimation::pause()
380{
381 setPaused(true);
382}
383
384/*!
385 \qmlmethod QtQuick::FrameAnimation::resume()
386 \brief Resumes a paused frame animation
387
388 If the frame animation is not paused or not \c running, calling this method has no effect.
389 The \c paused property will be false following a call to \c resume().
390*/
391void QQuickFrameAnimation::resume()
392{
393 setPaused(false);
394}
395
396/*!
397 \qmlmethod QtQuick::FrameAnimation::reset()
398 \brief Resets the frame animation properties
399
400 Calling this method resets the \c frame and \c elapsedTime to their initial
401 values (0). This method has no effect on \c running or \c paused properties
402 and can be called while they are true or false.
403
404 The difference between calling \c reset() and \c restart() is that \c reset()
405 will always initialize the properties while \c restart() initializes them only
406 at the next frame update which doesn't happen e.g. if \c restart() is
407 immediately followed by \c pause().
408*/
409void QQuickFrameAnimation::reset()
410{
411 Q_D(QQuickFrameAnimation);
412 setElapsedTime(0);
413 setCurrentFrame(0);
414 d->prevElapsedTimeNs = 0;
415 d->elapsedTimer.start();
416}
417
418/*!
419 \internal
420 */
421void QQuickFrameAnimation::classBegin()
422{
423 Q_D(QQuickFrameAnimation);
424 d->componentComplete = false;
425}
426
427/*!
428 \internal
429 */
430void QQuickFrameAnimation::componentComplete()
431{
432 Q_D(QQuickFrameAnimation);
433 d->componentComplete = true;
434 d->updateState();
435}
436
437/*!
438 \internal
439 */
440void QQuickFrameAnimation::setCurrentFrame(int frame)
441{
442 Q_D(QQuickFrameAnimation);
443 if (d->currentFrame != frame) {
444 d->currentFrame = frame;
445 Q_EMIT currentFrameChanged();
446 }
447}
448
449/*!
450 \internal
451 */
452void QQuickFrameAnimation::setElapsedTime(qreal elapsedTime)
453{
454 Q_D(QQuickFrameAnimation);
455 if (!qFuzzyCompare(p1: d->elapsedTime, p2: elapsedTime)) {
456 d->elapsedTime = elapsedTime;
457 Q_EMIT elapsedTimeChanged();
458 }
459}
460
461QT_END_NAMESPACE
462
463#include "moc_qquickframeanimation_p.cpp"
464

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