/**************************************************************************** ** ** Copyright (C) 2018 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the lottie-qt module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:GPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 or (at your option) any later version ** approved by the KDE Free Qt Foundation. The licenses are as published by ** the Free Software Foundation and appearing in the file LICENSE.GPL3 ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #ifndef BMPROPERTY_P_H #define BMPROPERTY_P_H // // W A R N I N G // ------------- // // This file is not part of the Qt API. It exists purely as an // implementation detail. This header file may change from version to // version without notice, or even be removed. // // We mean it. // #include #include #include #include #include #include #include #include #include #include QT_BEGIN_NAMESPACE enum class EasingSegmentState : char { Complete, Incomplete, Final, }; template struct EasingSegment { EasingSegmentState state = EasingSegmentState::Incomplete; double startFrame = 0; double endFrame = 0; T startValue; T endValue; BezierEasing easing; QPainterPath bezier; }; template class BODYMOVIN_EXPORT BMProperty { public: virtual ~BMProperty() = default; virtual void construct(const QJsonObject &definition) { if (definition.value(QLatin1String("s")).toVariant().toInt()) qCWarning(lcLottieQtBodymovinParser) << "Property is split into separate x and y but it is not supported"; bool fromExpression = definition.value(QLatin1String("fromExpression")).toBool(); m_animated = definition.value(QLatin1String("a")).toDouble() > 0; if (m_animated) { QJsonArray keyframes = definition.value(QLatin1String("k")).toArray(); QJsonArray::const_iterator it = keyframes.constBegin(); QJsonArray::const_iterator previous; while (it != keyframes.constEnd()) { QJsonObject keyframe = (*it).toObject(); EasingSegment easing = parseKeyframe(keyframe, fromExpression); addEasing(easing); if (m_easingCurves.length() > 1) { postprocessEasingCurve( m_easingCurves[m_easingCurves.length() - 2], (*previous).toObject(), fromExpression); } previous = it; ++it; } finalizeEasingCurves(); if (m_easingCurves.length() > 0) { EasingSegment &last = m_easingCurves.last(); if (last.state == EasingSegmentState::Complete) { postprocessEasingCurve( last, (*previous).toObject(), fromExpression); } } m_value = T(); } else m_value = getValue(definition.value(QLatin1String("k"))); } void setValue(const T& value) { m_value = value; } const T& value() const { return m_value; } virtual bool update(int frame) { if (!m_animated) return false; int adjustedFrame = qBound(m_startFrame, frame, m_endFrame); if (const EasingSegment *easing = getEasingSegment(adjustedFrame)) { qreal progress; if (easing->endFrame == easing->startFrame) progress = 1; else progress = ((adjustedFrame - easing->startFrame) * 1.0) / (easing->endFrame - easing->startFrame); qreal easedValue = easing->easing.valueForProgress(progress); m_value = easing->startValue + easedValue * ((easing->endValue - easing->startValue)); return true; } return false; } protected: void addEasing(EasingSegment& easing) { if (m_easingCurves.length()) { EasingSegment &prevEase = m_easingCurves.last(); // The end value has to be hand picked to the // previous easing segment, as the json data does // not contain end values for segments prevEase.endFrame = easing.startFrame; if (prevEase.state == EasingSegmentState::Incomplete) { prevEase.endValue = easing.startValue; prevEase.state = EasingSegmentState::Complete; } } m_easingCurves.push_back(easing); } void finalizeEasingCurves() { if (m_easingCurves.length()) { EasingSegment &last = m_easingCurves.last(); if (last.state == EasingSegmentState::Incomplete) { last.endValue = last.startValue; last.endFrame = last.startFrame; this->m_endFrame = last.startFrame; last.state = EasingSegmentState::Final; } } } const EasingSegment* getEasingSegment(int frame) { // TODO: Improve with a faster search algorithm const EasingSegment *easing = m_currentEasing; if (!easing || easing->startFrame < frame || easing->endFrame > frame) { for (int i=0; i < m_easingCurves.length(); i++) { if (m_easingCurves.at(i).startFrame <= frame && m_easingCurves.at(i).endFrame >= frame) { m_currentEasing = &m_easingCurves.at(i); break; } } } if (!m_currentEasing) { qCWarning(lcLottieQtBodymovinParser) << "Property is animated but easing cannot be found"; } return m_currentEasing; } virtual EasingSegment parseKeyframe(const QJsonObject keyframe, bool fromExpression) { Q_UNUSED(fromExpression); EasingSegment easing; int startTime = keyframe.value(QLatin1String("t")).toVariant().toInt(); // AE exported Bodymovin file includes the last // key frame but no other properties. // No need to process in that case if (!keyframe.contains(QLatin1String("s")) && !keyframe.contains(QLatin1String("e"))) { // In this case start time is the last frame for the property this->m_endFrame = startTime; easing.startFrame = startTime; easing.endFrame = startTime; easing.state = EasingSegmentState::Final; if (m_easingCurves.length()) { const EasingSegment &last = m_easingCurves.last(); if (last.state == EasingSegmentState::Complete) { easing.startValue = last.endValue; easing.endValue = last.endValue; } else { qCWarning(lcLottieQtBodymovinParser()) << "Last keyframe found after an incomplete one"; } } return easing; } if (m_startFrame > startTime) m_startFrame = startTime; easing.startFrame = startTime; easing.startValue = getValue(keyframe.value(QLatin1String("s")).toArray()); if (keyframe.contains(QLatin1String("e"))) { easing.endValue = getValue(keyframe.value(QLatin1String("e")).toArray()); easing.state = EasingSegmentState::Complete; } QJsonObject easingIn = keyframe.value(QLatin1String("i")).toObject(); QJsonObject easingOut = keyframe.value(QLatin1String("o")).toObject(); qreal eix = easingIn.value(QLatin1String("x")).toArray().at(0).toDouble(); qreal eiy = easingIn.value(QLatin1String("y")).toArray().at(0).toDouble(); qreal eox = easingOut.value(QLatin1String("x")).toArray().at(0).toDouble(); qreal eoy = easingOut.value(QLatin1String("y")).toArray().at(0).toDouble(); QPointF c1 = QPointF(eox, eoy); QPointF c2 = QPointF(eix, eiy); easing.easing.addCubicBezierSegment(c1, c2, QPointF(1.0, 1.0)); return easing; } virtual void postprocessEasingCurve( EasingSegment &easing, const QJsonObject keyframe, bool fromExpression) { } virtual T getValue(const QJsonValue &value) { if (value.isArray()) return getValue(value.toArray()); else { QVariant val = value.toVariant(); if (val.canConvert()) { T t = val.value(); return t; } else return T(); } } virtual T getValue(const QJsonArray &value) { QVariant val = value.at(0).toVariant(); if (val.canConvert()) { T t = val.value(); return t; } else return T(); } protected: bool m_animated = false; QList> m_easingCurves; const EasingSegment *m_currentEasing = nullptr; int m_startFrame = INT_MAX; int m_endFrame = 0; T m_value; }; template class BODYMOVIN_EXPORT BMProperty2D : public BMProperty { protected: T getValue(const QJsonArray &value) override { if (value.count() > 1) return T(value.at(0).toDouble(), value.at(1).toDouble()); else return T(); } EasingSegment parseKeyframe(const QJsonObject keyframe, bool fromExpression) override { QJsonArray startValues = keyframe.value(QLatin1String("s")).toArray(); QJsonArray endValues = keyframe.value(QLatin1String("e")).toArray(); int startTime = keyframe.value(QLatin1String("t")).toVariant().toInt(); EasingSegment easingCurve; easingCurve.startFrame = startTime; // AE exported Bodymovin file includes the last // key frame but no other properties. // No need to process in that case if (startValues.isEmpty() && endValues.isEmpty()) { // In this case start time is the last frame for the property this->m_endFrame = startTime; easingCurve.startFrame = startTime; easingCurve.endFrame = startTime; easingCurve.state = EasingSegmentState::Final; if (this->m_easingCurves.length()) { const EasingSegment &last = this->m_easingCurves.last(); if (last.state == EasingSegmentState::Complete) { easingCurve.startValue = last.endValue; easingCurve.endValue = last.endValue; } else { qCWarning(lcLottieQtBodymovinParser()) << "Last keyframe found after an incomplete one"; } } return easingCurve; } if (this->m_startFrame > startTime) this->m_startFrame = startTime; qreal xs, ys; // Keyframes originating from an expression use only scalar values. // They must be expanded for both x and y coordinates if (fromExpression) { xs = startValues.at(0).toDouble(); ys = startValues.at(0).toDouble(); } else { xs = startValues.at(0).toDouble(); ys = startValues.at(1).toDouble(); } T s(xs, ys); QJsonObject easingIn = keyframe.value(QLatin1String("i")).toObject(); QJsonObject easingOut = keyframe.value(QLatin1String("o")).toObject(); easingCurve.startFrame = startTime; easingCurve.startValue = s; if (!endValues.isEmpty()) { qreal xe, ye; // Keyframes originating from an expression use only scalar values. // They must be expanded for both x and y coordinates if (fromExpression) { xe = endValues.at(0).toDouble(); ye = endValues.at(0).toDouble(); } else { xe = endValues.at(0).toDouble(); ye = endValues.at(1).toDouble(); } T e(xe, ye); easingCurve.endValue = e; easingCurve.state = EasingSegmentState::Complete; } if (easingIn.value(QLatin1String("x")).isArray()) { QJsonArray eixArr = easingIn.value(QLatin1String("x")).toArray(); QJsonArray eiyArr = easingIn.value(QLatin1String("y")).toArray(); QJsonArray eoxArr = easingOut.value(QLatin1String("x")).toArray(); QJsonArray eoyArr = easingOut.value(QLatin1String("y")).toArray(); while (!eixArr.isEmpty() && !eiyArr.isEmpty()) { qreal eix = eixArr.takeAt(0).toDouble(); qreal eiy = eiyArr.takeAt(0).toDouble(); qreal eox = eoxArr.takeAt(0).toDouble(); qreal eoy = eoyArr.takeAt(0).toDouble(); QPointF c1 = QPointF(eox, eoy); QPointF c2 = QPointF(eix, eiy); easingCurve.easing.addCubicBezierSegment(c1, c2, QPointF(1.0, 1.0)); } } else { qreal eix = easingIn.value(QLatin1String("x")).toDouble(); qreal eiy = easingIn.value(QLatin1String("y")).toDouble(); qreal eox = easingOut.value(QLatin1String("x")).toDouble(); qreal eoy = easingOut.value(QLatin1String("y")).toDouble(); QPointF c1 = QPointF(eox, eoy); QPointF c2 = QPointF(eix, eiy); easingCurve.easing.addCubicBezierSegment(c1, c2, QPointF(1.0, 1.0)); } return easingCurve; } }; template class BODYMOVIN_EXPORT BMProperty4D : public BMProperty { public: bool update(int frame) override { if (!this->m_animated) return false; int adjustedFrame = qBound(this->m_startFrame, frame, this->m_endFrame); if (const EasingSegment *easing = BMProperty::getEasingSegment(adjustedFrame)) { qreal progress; if (easing->endFrame == easing->startFrame) progress = 1; else progress = ((adjustedFrame - easing->startFrame) * 1.0) / (easing->endFrame - easing->startFrame); qreal easedValue = easing->easing.valueForProgress(progress); // For the time being, 4D vectors are used only for colors, and // the value must be restricted to between [0, 1] easedValue = qBound(qreal(0.0), easedValue, qreal(1.0)); T sv = easing->startValue; T ev = easing->endValue; qreal x = sv.x() + easedValue * (ev.x() - sv.x()); qreal y = sv.y() + easedValue * (ev.y() - sv.y()); qreal z = sv.z() + easedValue * (ev.z() - sv.z()); qreal w = sv.w() + easedValue * (ev.w() - sv.w()); this->m_value = T(x, y, z, w); } return true; } protected: T getValue(const QJsonArray &value) override { if (value.count() > 3) return T(value.at(0).toDouble(), value.at(1).toDouble(), value.at(2).toDouble(), value.at(3).toDouble()); else return T(); } }; QT_END_NAMESPACE #endif // BMPROPERTY_P_H