/**************************************************************************** ** ** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). ** Contact: Qt Software Information (qt-info@nokia.com) ** ** This file is part of the QtDeclarative module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** No Commercial Usage ** This file contains pre-release code and may not be distributed. ** You may use this file in accordance with the terms and conditions ** contained in the either Technology Preview License Agreement or the ** Beta Release License Agreement. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 2.1 requirements ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, Nokia gives you certain ** additional rights. These rights are described in the Nokia Qt LGPL ** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this ** package. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3.0 as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU General Public License version 3.0 requirements will be ** met: http://www.gnu.org/copyleft/gpl.html. ** ** If you are unsure which license is appropriate for your use, please ** contact the sales department at qt-sales@nokia.com. ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "gfxtimeline.h" #include #include #include #include #include #include #include "gfxeasing.h" #include QT_BEGIN_NAMESPACE #define TIMETICK 5 // // Timeline stuff // struct Update { Update(GfxValue *_g, qreal _v) : g(_g), v(_v) {} Update(const GfxEvent &_e) : g(0), v(0), e(_e) {} GfxValue *g; qreal v; GfxEvent e; }; struct QmlTimeLinePrivate { QmlTimeLinePrivate(QmlTimeLine *); struct Op { enum Type { Pause, Set, Move, MoveBy, Accel, AccelDistance, Execute }; Op() {} Op(Type t, int l, qreal v, qreal v2, int o, const GfxEvent &ev = GfxEvent(), const GfxEasing &es = GfxEasing()) : type(t), length(l), value(v), value2(v2), order(o), event(ev), easing(es) {} Op(const Op &o) : type(o.type), length(o.length), value(o.value), value2(o.value2), order(o.order), event(o.event), easing(o.easing) {} Op &operator=(const Op &o) { type = o.type; length = o.length; value = o.value; value2 = o.value2; order = o.order; event = o.event; easing = o.easing; return *this; } Type type; int length; qreal value; qreal value2; int order; GfxEvent event; GfxEasing easing; }; struct TimeLine { TimeLine() : length(0), consumedOpLength(0), base(0.) {} QList ops; int length; int consumedOpLength; qreal base; }; int length; int syncPoint; typedef QHash Ops; Ops ops; QmlTimeLine *q; void add(QmlTimeLineObject &, const Op &); qreal value(const Op &op, int time, qreal base, bool *) const; int advance(int); bool clockRunning; int prevTime; int order; QmlTimeLine::SyncMode syncMode; int syncAdj; QList > *updateQueue; }; QmlTimeLinePrivate::QmlTimeLinePrivate(QmlTimeLine *parent) : length(0), syncPoint(0), q(parent), clockRunning(false), prevTime(0), order(0), syncMode(QmlTimeLine::LocalSync), syncAdj(0), updateQueue(0) { } void QmlTimeLinePrivate::add(QmlTimeLineObject &g, const Op &o) { if(g._t && g._t != q) { qWarning() << "QmlTimeLine: Cannot modify a GfxValue owned by" << "another timeline."; return; } g._t = q; Ops::Iterator iter = ops.find(&g); if(iter == ops.end()) { iter = ops.insert(&g, TimeLine()); if(syncPoint > 0) q->pause(g, syncPoint); } if(!iter->ops.isEmpty() && o.type == Op::Pause && iter->ops.last().type == Op::Pause) { iter->ops.last().length += o.length; iter->length += o.length; } else { iter->ops.append(o); iter->length += o.length; } if(iter->length > length) length = iter->length; if(!clockRunning) { q->stop(); prevTime = 0; clockRunning = true; if(syncMode == QmlTimeLine::LocalSync) { syncAdj = -1; } else { syncAdj = 0; } q->start(); /* q->tick(0); if(syncMode == QmlTimeLine::LocalSync) { syncAdj = -1; } else { syncAdj = 0; } */ } } qreal QmlTimeLinePrivate::value(const Op &op, int time, qreal base, bool *changed) const { Q_ASSERT(time >= 0); Q_ASSERT(time <= op.length); *changed = true; switch(op.type) { case Op::Pause: *changed = false; return base; case Op::Set: return op.value; case Op::Move: if(time == 0) { return base; } else if(time == (op.length)) { return op.value; } else { if(op.easing.isLinear()) { qreal delta = op.value - base; qreal pTime = (qreal)(time) / (qreal)op.length; return base + delta * pTime; } else { return op.easing.valueAt(time, base, op.value, op.length); } } case Op::MoveBy: if(time == 0) { return base; } else if(time == (op.length)) { return base + op.value; } else { if(op.easing.isLinear()) { qreal delta = op.value; qreal pTime = (qreal)(time) / (qreal)op.length; return base + delta * pTime; } else { return op.easing.valueAt(time, base, base + op.value, op.length); } } case Op::Accel: if(time == 0) { return base; } else { qreal t = (qreal)(time) / 1000.0f; qreal delta = op.value * t + 0.5f * op.value2 * t * t; return base + delta; } case Op::AccelDistance: if(time == 0) { return base; } else if(time == (op.length)) { return base + op.value2; } else { qreal t = (qreal)(time) / 1000.0f; qreal accel = -1.0f * 1000.0f * op.value / (qreal)op.length; qreal delta = op.value * t + 0.5f * accel * t * t; return base + delta; } case Op::Execute: op.event.execute(); *changed = false; return -1; } return base; } /*! \class QmlTimeLine \ingroup group_animation \brief The QmlTimeLine class provides a timeline for controlling animations. QmlTimeLine is similar to QTimeLine except: \list \i It updates GfxValue instances directly, rather than maintaining a single current value. For example, the following animates a simple value over 200 milliseconds: \code GfxValue v(); QmlTimeLine tl; tl.move(v, 100., 200); tl.start() \endcode If your program needs to know when values are changed, it can either connect to the QmlTimeLine's updated() signal, or inherit from GfxValue and reimplement the GfxValue::setValue() method. \i Supports multiple GfxValue, arbitrary start and end values and allows animations to be strung together for more complex effects. For example, the following animation moves the x and y coordinates of an object from wherever they are to the position (100, 100) in 50 milliseconds and then further animates them to (100, 200) in 50 milliseconds: \code GfxValue x(); GfxValue y(); QmlTimeLine tl; tl.start(); tl.move(x, 100., 50); tl.move(y, 100., 50); tl.move(y, 200., 50); \endcode \i All QmlTimeLine instances share a single, synchronized clock. Actions scheduled within the same event loop tick are scheduled synchronously against each other, regardless of the wall time between the scheduling. Synchronized scheduling applies both to within the same QmlTimeLine and across separate QmlTimeLine's within the same process. \endlist Currently easing functions are not supported. */ /*! Construct a new QmlTimeLine with the specified \a parent. */ QmlTimeLine::QmlTimeLine(QObject *parent) : QAbstractAnimation(parent) { d = new QmlTimeLinePrivate(this); } /*! Destroys the time line. Any inprogress animations are canceled, but not completed. */ QmlTimeLine::~QmlTimeLine() { for(QmlTimeLinePrivate::Ops::Iterator iter = d->ops.begin(); iter != d->ops.end(); ++iter) iter.key()->_t = 0; delete d; d = 0; } /*! \enum QmlTimeLine::SyncMode */ /*! Return the timeline's synchronization mode. */ QmlTimeLine::SyncMode QmlTimeLine::syncMode() const { return d->syncMode; } /*! Set the timeline's synchronization mode to \a syncMode. */ void QmlTimeLine::setSyncMode(SyncMode syncMode) { d->syncMode = syncMode; } /*! Pause \a gfxValue for \a time milliseconds. */ void QmlTimeLine::pause(QmlTimeLineObject &obj, int time) { if(time <= 0) return; QmlTimeLinePrivate::Op op(QmlTimeLinePrivate::Op::Pause, time, 0., 0., d->order++); d->add(obj, op); } /*! Execute the \a event. */ void QmlTimeLine::execute(const GfxEvent &event) { QmlTimeLinePrivate::Op op(QmlTimeLinePrivate::Op::Execute, 0, 0, 0., d->order++, event); d->add(*event.eventObject(), op); } /*! Set the \a value of \a gfxValue. */ void QmlTimeLine::set(GfxValue &gfxValue, qreal value) { QmlTimeLinePrivate::Op op(QmlTimeLinePrivate::Op::Set, 0, value, 0., d->order++); d->add(gfxValue, op); } /*! Decelerate \a gfxValue from the starting \a velocity to zero at the given \a acceleration rate. Although the \a acceleration is technically a deceleration, it should always be positive. The QmlTimeLine will ensure that the deceleration is in the opposite direction to the initial velocity. */ int QmlTimeLine::accel(GfxValue &gfxValue, qreal velocity, qreal acceleration) { if((velocity > 0.0f) == (acceleration > 0.0f)) acceleration = acceleration * -1.0f; int time = static_cast(-1000 * velocity / acceleration); QmlTimeLinePrivate::Op op(QmlTimeLinePrivate::Op::Accel, time, velocity, acceleration, d->order++); d->add(gfxValue, op); return time; } /*! \overload Decelerate \a gfxValue from the starting \a velocity to zero at the given \a acceleration rate over a maximum distance of maxDistance. If necessary, QmlTimeLine will reduce the acceleration to ensure that the entire operation does not require a move of more than \a maxDistance. \a maxDistance should always be positive. */ int QmlTimeLine::accel(GfxValue &gfxValue, qreal velocity, qreal acceleration, qreal maxDistance) { Q_ASSERT(acceleration >= 0.0f && maxDistance >= 0.0f); qreal maxAccel = (velocity * velocity) / (2.0f * maxDistance); if(maxAccel > acceleration) acceleration = maxAccel; if((velocity > 0.0f) == (acceleration > 0.0f)) acceleration = acceleration * -1.0f; int time = static_cast(-1000 * velocity / acceleration); QmlTimeLinePrivate::Op op(QmlTimeLinePrivate::Op::Accel, time, velocity, acceleration, d->order++); d->add(gfxValue, op); return time; } /*! Decelerate \a gfxValue from the starting \a velocity to zero over the given \a distance. This is like accel(), but the QmlTimeLine calculates the exact deceleration to use. \a distance should be positive. */ int QmlTimeLine::accelDistance(GfxValue &gfxValue, qreal velocity, qreal distance) { if (distance == 0.0f || velocity == 0.0f) return -1; Q_ASSERT((distance >= 0.0f) == (velocity >= 0.0f)); int time = static_cast(1000 * (2.0f * distance) / velocity); QmlTimeLinePrivate::Op op(QmlTimeLinePrivate::Op::AccelDistance, time, velocity, distance, d->order++); d->add(gfxValue, op); return time; } /*! Linearly change the \a gfxValue from its current value to the given \a destination value over \a time milliseconds. */ void QmlTimeLine::move(GfxValue &gfxValue, qreal destination, int time) { if(time <= 0) return; QmlTimeLinePrivate::Op op(QmlTimeLinePrivate::Op::Move, time, destination, 0.0f, d->order++); d->add(gfxValue, op); } /*! Change the \a gfxValue from its current value to the given \a destination value over \a time milliseconds using the \a easing curve. */ void QmlTimeLine::move(GfxValue &gfxValue, qreal destination, const GfxEasing &easing, int time) { if(time <= 0) return; QmlTimeLinePrivate::Op op(QmlTimeLinePrivate::Op::Move, time, destination, 0.0f, d->order++, GfxEvent(), easing); d->add(gfxValue, op); } /*! Linearly change the \a gfxValue from its current value by the \a change amount over \a time milliseconds. */ void QmlTimeLine::moveBy(GfxValue &gfxValue, qreal change, int time) { if(time <= 0) return; QmlTimeLinePrivate::Op op(QmlTimeLinePrivate::Op::MoveBy, time, change, 0.0f, d->order++); d->add(gfxValue, op); } /*! Change the \a gfxValue from its current value by the \a change amount over \a time milliseconds using the \a easing curve. */ void QmlTimeLine::moveBy(GfxValue &gfxValue, qreal change, const GfxEasing &easing, int time) { if(time <= 0) return; QmlTimeLinePrivate::Op op(QmlTimeLinePrivate::Op::MoveBy, time, change, 0.0f, d->order++, GfxEvent(), easing); d->add(gfxValue, op); } /*! Cancel (but don't complete) all scheduled actions for \a gfxValue. */ void QmlTimeLine::reset(GfxValue &gfxValue) { if(!gfxValue._t) return; if(gfxValue._t != this) { qWarning() << "QmlTimeLine: Cannot reset a GfxValue owned by another timeline."; return; } remove(&gfxValue); gfxValue._t = 0; } int QmlTimeLine::duration() const { return -1; } /*! Synchronize the end point of \a gfxValue to the endpoint of \a syncTo within this timeline. Following operations on \a gfxValue in this timeline will be scheduled after all the currently scheduled actions on \a syncTo are complete. In psuedo-code this is equivalent to: \code QmlTimeLine::pause(gfxValue, min(0, length_of(syncTo) - length_of(gfxValue))) \endcode */ void QmlTimeLine::sync(GfxValue &gfxValue, GfxValue &syncTo) { QmlTimeLinePrivate::Ops::Iterator iter = d->ops.find(&syncTo); if(iter == d->ops.end()) return; int length = iter->length; iter = d->ops.find(&gfxValue); if(iter == d->ops.end()) { pause(gfxValue, length); } else { int glength = iter->length; pause(gfxValue, length - glength); } } /*! Synchronize the end point of \a gfxValue to the endpoint of the longest action currently scheduled in the timeline. In psuedo-code, this is equivalent to: \code QmlTimeLine::pause(gfxValue, length_of(timeline) - length_of(gfxValue)) \endcode */ void QmlTimeLine::sync(GfxValue &gfxValue) { QmlTimeLinePrivate::Ops::Iterator iter = d->ops.find(&gfxValue); if(iter == d->ops.end()) { pause(gfxValue, d->length); } else { pause(gfxValue, d->length - iter->length); } } /*! Synchronize all currently and future scheduled values in this timeline to the longest action currently scheduled. For example: \code value1->setValue(0.); value2->setValue(0.); value3->setValue(0.); QmlTimeLine tl; ... tl.move(value1, 10, 200); tl.move(value2, 10, 100); tl.sync(); tl.move(value2, 20, 100); tl.move(value3, 20, 100); \endcode will result in: \table \header \o \o 0ms \o 50ms \o 100ms \o 150ms \o 200ms \o 250ms \o 300ms \row \o value1 \o 0 \o 2.5 \o 5.0 \o 7.5 \o 10 \o 10 \o 10 \row \o value2 \o 0 \o 5.0 \o 10.0 \o 10.0 \o 10.0 \o 15.0 \o 20.0 \row \o value2 \o 0 \o 0 \o 0 \o 0 \o 0 \o 10.0 \o 20.0 \endtable */ /*void QmlTimeLine::sync() { for(QmlTimeLinePrivate::Ops::Iterator iter = d->ops.begin(); iter != d->ops.end(); ++iter) pause(*iter.key(), d->length - iter->length); d->syncPoint = d->length; }*/ /*! \internal Temporary hack. */ void QmlTimeLine::setSyncPoint(int sp) { d->syncPoint = sp; } /*! \internal Temporary hack. */ int QmlTimeLine::syncPoint() const { return d->syncPoint; } /*! Returns true if the timeline is active. An active timeline is one where GfxValue actions are still pending. */ bool QmlTimeLine::isActive() const { return !d->ops.isEmpty(); } /*! Completes the timeline. All queued actions are played to completion, and then discarded. For example, \code GfxValue v(0.); QmlTimeLine tl; tl.move(v, 100., 1000.); // 500 ms passes // v.value() == 50. tl.complete(); // v.value() == 100. \endcode */ void QmlTimeLine::complete() { d->advance(d->length); } /*! Resets the timeline. All queued actions are discarded and GfxValue's retain their current value. For example, \code GfxValue v(0.); QmlTimeLine tl; tl.move(v, 100., 1000.); // 500 ms passes // v.value() == 50. tl.clear(); // v.value() == 50. \endcode */ void QmlTimeLine::clear() { for(QmlTimeLinePrivate::Ops::ConstIterator iter = d->ops.begin(); iter != d->ops.end(); ++iter) iter.key()->_t = 0; d->ops.clear(); d->length = 0; d->syncPoint = 0; //XXX need stop here? } int QmlTimeLine::time() const { return d->prevTime; } /*! \fn void QmlTimeLine::updated() Emitted each time the timeline modifies GfxValues. Even if multiple GfxValues are changed, this signal is only emitted once for each clock tick. */ void QmlTimeLine::updateCurrentTime(int v) { if(d->syncAdj == -1) d->syncAdj = v; v -= d->syncAdj; int timeChanged = v - d->prevTime; #if 0 if(!timeChanged) return; #endif d->prevTime = v; d->advance(timeChanged); emit updated(); // Do we need to stop the clock? if(d->ops.isEmpty()) { stop(); d->prevTime = 0; d->clockRunning = false; emit completed(); } /*else if(pauseTime > 0) { GfxClock::cancelClock(); d->prevTime = 0; GfxClock::pauseFor(pauseTime); d->syncAdj = 0; d->clockRunning = false; }*/ else if(/*!GfxClock::isActive()*/ state() != Running) { stop(); d->prevTime = 0; d->clockRunning = true; d->syncAdj = 0; start(); } } bool operator<(const QPair &lhs, const QPair &rhs) { return lhs.first < rhs.first; } int QmlTimeLinePrivate::advance(int t) { int pauseTime = -1; // XXX - surely there is a more efficient way? do { pauseTime = -1; // Minimal advance time int advanceTime = t; for(Ops::Iterator iter = ops.begin(); iter != ops.end(); ++iter) { TimeLine &tl = *iter; Op &op = tl.ops.first(); int length = op.length - tl.consumedOpLength; if(length < advanceTime) { advanceTime = length; if(advanceTime == 0) break; } } t -= advanceTime; // Process until then. A zero length advance time will only process // sets. QList > updates; for(Ops::Iterator iter = ops.begin(); iter != ops.end(); ) { GfxValue *v = static_cast(iter.key()); TimeLine &tl = *iter; Q_ASSERT(!tl.ops.isEmpty()); do { Op &op = tl.ops.first(); if(advanceTime == 0 && op.length != 0) continue; if(tl.consumedOpLength == 0 && op.type != Op::Pause && op.type != Op::Execute) tl.base = v->value(); if((tl.consumedOpLength + advanceTime) == op.length) { if(op.type == Op::Execute) { updates << qMakePair(op.order, Update(op.event)); } else { bool changed = false; qreal val = value(op, op.length, tl.base, &changed); if(changed) updates << qMakePair(op.order, Update(v, val)); } tl.length -= qMin(advanceTime, tl.length); tl.consumedOpLength = 0; tl.ops.removeFirst(); } else { tl.consumedOpLength += advanceTime; bool changed = false; qreal val = value(op, tl.consumedOpLength, tl.base, &changed); if(changed) updates << qMakePair(op.order, Update(v, val)); tl.length -= qMin(advanceTime, tl.length); break; } } while(!tl.ops.isEmpty() && advanceTime == 0 && tl.ops.first().length == 0); if(tl.ops.isEmpty()) { iter = ops.erase(iter); v->_t = 0; } else { if(tl.ops.first().type == Op::Pause && pauseTime != 0) { int opPauseTime = tl.ops.first().length - tl.consumedOpLength; if(pauseTime == -1 || opPauseTime < pauseTime) pauseTime = opPauseTime; } else { pauseTime = 0; } ++iter; } } length -= qMin(length, advanceTime); syncPoint -= advanceTime; qSort(updates.begin(), updates.end()); updateQueue = &updates; for(int ii = 0; ii < updates.count(); ++ii) { const Update &v = updates.at(ii).second; if(v.g) v.g->setValue(v.v); else v.e.execute(); } updateQueue = 0; } while(t); return pauseTime; } void QmlTimeLine::remove(QmlTimeLineObject *v) { QmlTimeLinePrivate::Ops::Iterator iter = d->ops.find(v); Q_ASSERT(iter != d->ops.end()); int len = iter->length; d->ops.erase(iter); if(len == d->length) { // We need to recalculate the length d->length = 0; for(QmlTimeLinePrivate::Ops::Iterator iter = d->ops.begin(); iter != d->ops.end(); ++iter) { if(iter->length > d->length) d->length = iter->length; } } if(d->ops.isEmpty()) { stop(); d->clockRunning = false; } else if(/*!GfxClock::isActive()*/ state() != Running) { stop(); d->prevTime = 0; d->clockRunning = true; if(d->syncMode == QmlTimeLine::LocalSync) { d->syncAdj = -1; } else { d->syncAdj = 0; } start(); } if(d->updateQueue) { for(int ii = 0; ii < d->updateQueue->count(); ++ii) { if(d->updateQueue->at(ii).second.g == v || d->updateQueue->at(ii).second.e.eventObject() == v) { d->updateQueue->removeAt(ii); --ii; } } } } /*! \class GfxValue \ingroup group_animation \brief The GfxValue class is modified by QmlTimeLine. */ /*! \fn GfxValue::GfxValue(qreal value = 0) Construct a new GfxValue with an initial \a value. */ /*! \fn qreal GfxValue::value() const Return the current value. */ /*! \fn void GfxValue::setValue(qreal value) Set the current \a value. */ /*! \fn QmlTimeLine *GfxValue::timeLine() const If a QmlTimeLine is operating on this value, return a pointer to it, otherwise return null. */ QmlTimeLineObject::QmlTimeLineObject() : _t(0) { } QmlTimeLineObject::~QmlTimeLineObject() { if(_t) { _t->remove(this); _t = 0; } } GfxEvent::GfxEvent() : d0(0), d1(0), d2(0) { } GfxEvent::GfxEvent(const GfxEvent &o) : d0(o.d0), d1(o.d1), d2(o.d2) { } GfxEvent &GfxEvent::operator=(const GfxEvent &o) { d0 = o.d0; d1 = o.d1; d2 = o.d2; return *this; } void GfxEvent::execute() const { d0(d1); } QmlTimeLineObject *GfxEvent::eventObject() const { return d2; } QT_END_NAMESPACE