[skottie] 2D spatial interpolation support

AE discriminates [1] between basic 2-dimensional properties
(PropertyValueType.TwoD - e.g. scale), and spatial 2D properties
(PropertyValueType.TwoD_SPATIAL - e.g. position).

For the latter it provides additional keyframe controls (tangent in &
tangent out) to describe a non-linear interpolation path ("spatial
interpolation").  This composes on top of the usual temporal
interpolation (with its own optional cubic mapping).

To support spatial interpolation:

  - introduce a new Skottie value type (Vec2Value), to represent
    TwoD and TwoD_SPATIAL properties
  - introduce a KeyframeAnimator specialization for Vec2Value, which
    tracks per-keyframe tangent information
  - for spatial keyframes, instantiate/store an SkContourMeasure, and
    use instead of straight Vec2 LERP
  - switch interesting 2D properties to the new value type (transform
    position, anchor point, scale)

(we could look into separating TwoD/TwoD_SPATIAL if needed, but the new
specialization is already more efficient than the old
opaque-vector-with-late-binding approach)

[1] http://docs.aenhancers.com/properties/property/#property-propertyvaluetype

Change-Id: I0863fd970cec4c5ff15cf01b2fb5c6602a468179
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/274283
Reviewed-by: Mike Klein <mtklein@google.com>
Commit-Queue: Florin Malita <fmalita@chromium.org>
This commit is contained in:
Florin Malita 2020-03-10 13:21:19 -04:00 committed by Skia Commit-Bot
parent dd86fb3531
commit 92ec801a2b
6 changed files with 179 additions and 23 deletions

View File

@ -7,6 +7,7 @@
#include "modules/skottie/src/Animator.h"
#include "include/core/SkContourMeasure.h"
#include "include/core/SkCubicMap.h"
#include "modules/skottie/src/SkottieJson.h"
#include "modules/skottie/src/SkottiePriv.h"
@ -48,6 +49,8 @@ void AnimatablePropertyContainer::shrink_to_fit() {
namespace {
static float lerp(float a, float b, float t) { return a + (b - a) * t; }
struct Keyframe {
// We can store scalar values inline; other types are stored externally,
// and we track them by index.
@ -116,14 +119,14 @@ public:
// frame(n).e == frame(n+1).s
const auto parse_value = [&](const skjson::ObjectValue& jkf, size_t i, Keyframe::Value* v) {
auto parsed = this->parseValue(abuilder, jkf["s"], v);
auto parsed = this->parseValue(abuilder, jkf, jkf["s"], v);
// A missing value is only OK for the last legacy KF
// (where it is pulled from prev KF 'end' value).
if (!parsed && i > 0 && i == jkfs.size() - 1) {
const skjson::ObjectValue* prev_kf = jkfs[i - 1];
SkASSERT(prev_kf);
parsed = this->parseValue(abuilder, (*prev_kf)["e"], v);
parsed = this->parseValue(abuilder, jkf, (*prev_kf)["e"], v);
}
return parsed;
@ -188,7 +191,10 @@ public:
}
protected:
virtual bool parseValue(const AnimationBuilder&, const skjson::Value&, Keyframe::Value*) = 0;
virtual bool parseValue(const AnimationBuilder&,
const skjson::ObjectValue&,
const skjson::Value&,
Keyframe::Value*) = 0;
std::vector<Keyframe> fKFs; // Keyframe records, one per AE/Lottie keyframe.
std::vector<SkCubicMap> fCMs; // Optional cubic mappers (Bezier interpolation).
@ -363,6 +369,7 @@ public:
private:
bool parseValue(const AnimationBuilder& abuilder,
const skjson::ObjectValue&,
const skjson::Value& jv,
Keyframe::Value* v) override {
T val;
@ -385,8 +392,8 @@ public:
};
private:
explicit KeyframeAnimator(std::vector<Keyframe> kfs, std::vector<SkCubicMap> cms,
std::vector<T> vs, T* target_value)
KeyframeAnimator(std::vector<Keyframe> kfs, std::vector<SkCubicMap> cms,
std::vector<T> vs, T* target_value)
: INHERITED(std::move(kfs), std::move(cms))
, fValues(std::move(vs))
, fTarget(target_value) {}
@ -433,6 +440,7 @@ public:
private:
bool parseValue(const AnimationBuilder&,
const skjson::ObjectValue&,
const skjson::Value& jv,
Keyframe::Value* v) override {
return Parse(jv, &v->flt);
@ -449,8 +457,7 @@ private:
void onTick(float t) override {
const auto& lerp_info = this->getLERPInfo(t);
*fTarget = lerp_info.vrec0.flt +
(lerp_info.vrec1.flt - lerp_info.vrec0.flt) * lerp_info.weight;
*fTarget = lerp(lerp_info.vrec0.flt, lerp_info.vrec1.flt, lerp_info.weight);
}
ScalarValue* fTarget;
@ -458,6 +465,115 @@ private:
using INHERITED = KeyframeAnimatorBase;
};
// Spatial 2D specialization: stores SkV2s and optional contour interpolators externally.
class Vec2KeyframeAnimator final : public KeyframeAnimatorBase {
struct SpatialValue {
Vec2Value v2;
sk_sp<SkContourMeasure> cmeasure;
};
public:
class Builder final : public KeyframeBuilderBase {
public:
sk_sp<Vec2KeyframeAnimator> make(const AnimationBuilder& abuilder,
const skjson::ArrayValue* jkfs,
Vec2Value* target_value) {
if (!jkfs || jkfs->size() < 1) {
return nullptr;
}
fValues.reserve(jkfs->size());
if (!this->parseKeyframes(abuilder, *jkfs)) {
return nullptr;
}
fValues.shrink_to_fit();
return sk_sp<Vec2KeyframeAnimator>(new Vec2KeyframeAnimator(std::move(fKFs),
std::move(fCMs),
std::move(fValues),
target_value));
}
private:
bool parseValue(const AnimationBuilder&,
const skjson::ObjectValue& jkf,
const skjson::Value& jv,
Keyframe::Value* v) override {
SpatialValue val;
if (!Parse(jv, &val.v2)) {
return false;
}
if (fTi != SkV2{0,0} || fTo != SkV2{0,0}) {
// The previous keyframe is spatial: back-fill its contour interpolator
// (now that we know the end point).
SkASSERT(!fValues.empty());
auto& prev_val = fValues.back();
SkASSERT(!prev_val.cmeasure);
// spatial interpolation only make sense for noncoincident values
if (val.v2 != prev_val.v2) {
SkPath p;
p.moveTo (prev_val.v2.x , prev_val.v2.y);
p.cubicTo(prev_val.v2.x + fTo.x, prev_val.v2.y + fTo.y,
val.v2.x + fTi.x, val.v2.y + fTi.y,
val.v2.x, val.v2.y);
prev_val.cmeasure = SkContourMeasureIter(p, false).next();
}
}
// Track the last keyframe spatial tangents (checked on next parseValue).
fTi = ParseDefault<SkV2>(jkf["ti"], {0,0});
fTo = ParseDefault<SkV2>(jkf["to"], {0,0});
if (fValues.empty() || val.v2 != fValues.back().v2) {
fValues.push_back(std::move(val));
}
v->idx = SkToU32(fValues.size() - 1);
return true;
}
std::vector<SpatialValue> fValues;
SkV2 fTi{0,0},
fTo{0,0};
};
private:
Vec2KeyframeAnimator(std::vector<Keyframe> kfs, std::vector<SkCubicMap> cms,
std::vector<SpatialValue> vs, Vec2Value* target_value)
: INHERITED(std::move(kfs), std::move(cms))
, fValues(std::move(vs))
, fTarget(target_value) {}
void onTick(float t) override {
const auto& lerp_info = this->getLERPInfo(t);
const auto& v0 = fValues[lerp_info.vrec0.idx];
if (v0.cmeasure) {
// Spatial keyframe: the computed weight is relative to the interpolation path
// arc length.
SkPoint pos;
if (v0.cmeasure->getPosTan(lerp_info.weight * v0.cmeasure->length(), &pos, nullptr)) {
*fTarget = { pos.fX, pos.fY };
return;
}
}
const auto& v1 = fValues[lerp_info.vrec1.idx];
*fTarget = {
lerp(v0.v2.x, v1.v2.x, lerp_info.weight),
lerp(v0.v2.y, v1.v2.y, lerp_info.weight),
};
}
const std::vector<SpatialValue> fValues;
Vec2Value* fTarget;
using INHERITED = KeyframeAnimatorBase;
};
template <typename T>
auto make_animator(const AnimationBuilder& abuilder,
const skjson::ArrayValue* jkfs,
@ -471,6 +587,12 @@ auto make_animator(const AnimationBuilder& abuilder,
return ScalarKeyframeAnimator::Builder().make(abuilder, jkfs, target_value);
}
auto make_animator(const AnimationBuilder& abuilder,
const skjson::ArrayValue* jkfs,
Vec2Value* target_value) {
return Vec2KeyframeAnimator::Builder().make(abuilder, jkfs, target_value);
}
template <typename T>
bool BindPropertyImpl(const AnimationBuilder& abuilder,
const skjson::ObjectValue* jprop,
@ -531,6 +653,24 @@ bool AnimatablePropertyContainer::bind<ScalarValue>(const AnimationBuilder& abui
return BindPropertyImpl(abuilder, jprop, &fAnimators, v);
}
template <>
bool AnimatablePropertyContainer::bind<Vec2Value>(const AnimationBuilder& abuilder,
const skjson::ObjectValue* jprop,
Vec2Value* v) {
if (!jprop) {
return false;
}
if (!ParseDefault<bool>((*jprop)["s"], false)) {
// Regular (static or keyframed) 2D value.
return BindPropertyImpl(abuilder, jprop, &fAnimators, v);
}
// Separate-dimensions vector value: each component is animated independently.
return this->bind(abuilder, (*jprop)["x"], &v->x)
| this->bind(abuilder, (*jprop)["y"], &v->y);
}
template <>
bool AnimatablePropertyContainer::bind<ShapeValue>(const AnimationBuilder& abuilder,
const skjson::ObjectValue* jprop,

View File

@ -84,6 +84,18 @@ bool Parse<SkString>(const Value& v, SkString* s) {
return false;
}
template <>
bool Parse<SkV2>(const Value& v, SkV2* v2) {
if (!v.is<ArrayValue>())
return false;
const auto& av = v.as<ArrayValue>();
// We need at least two scalars (BM sometimes exports a third value == 0).
return av.size() >= 2
&& Parse<SkScalar>(av[0], &v2->x)
&& Parse<SkScalar>(av[1], &v2->y);
}
template <>
bool Parse<SkPoint>(const Value& v, SkPoint* pt) {
if (!v.is<ObjectValue>())

View File

@ -23,6 +23,12 @@ bool ValueTraits<ScalarValue>::FromJSON(const skjson::Value& jv, const internal:
return Parse(jv, v);
}
template <>
bool ValueTraits<Vec2Value>::FromJSON(const skjson::Value& jv, const internal::AnimationBuilder*,
Vec2Value* v) {
return Parse(jv, v);
}
template <>
bool ValueTraits<ScalarValue>::CanLerp(const ScalarValue&, const ScalarValue&) {
return true;

View File

@ -9,6 +9,7 @@
#define SkottieValue_DEFINED
#include "include/core/SkColor.h"
#include "include/core/SkM44.h"
#include "include/core/SkPaint.h"
#include "include/core/SkPath.h"
#include "include/core/SkScalar.h"
@ -35,6 +36,7 @@ struct ValueTraits {
};
using ScalarValue = SkScalar;
using Vec2Value = SkV2;
using VectorValue = std::vector<ScalarValue>;
struct BezierVertex {

View File

@ -38,22 +38,18 @@ void TransformAdapter2D::onSync() {
}
SkMatrix TransformAdapter2D::totalMatrix() const {
const auto anchor_point = ValueTraits<VectorValue>::As<SkPoint>(fAnchorPoint),
position = ValueTraits<VectorValue>::As<SkPoint>(fPosition),
scale = ValueTraits<VectorValue>::As<SkPoint>(fScale);
SkMatrix t = SkMatrix::MakeTrans(-fAnchorPoint.x, -fAnchorPoint.y);
SkMatrix t = SkMatrix::MakeTrans(-anchor_point.x(), -anchor_point.y());
t.postScale(scale.x() / 100, scale.y() / 100); // 100% based
t.postScale(fScale.x / 100, fScale.y / 100); // 100% based
t.postRotate(fRotation);
t.postTranslate(position.x(), position.y());
t.postTranslate(fPosition.x, fPosition.y);
// TODO: skew
return t;
}
SkPoint TransformAdapter2D::getAnchorPoint() const {
return ValueTraits<VectorValue>::As<SkPoint>(fAnchorPoint);
return { fAnchorPoint.x, fAnchorPoint.y };
}
void TransformAdapter2D::setAnchorPoint(const SkPoint& ap) {
@ -62,7 +58,7 @@ void TransformAdapter2D::setAnchorPoint(const SkPoint& ap) {
}
SkPoint TransformAdapter2D::getPosition() const {
return ValueTraits<VectorValue>::As<SkPoint>(fPosition);
return { fPosition.x, fPosition.y };
}
void TransformAdapter2D::setPosition(const SkPoint& p) {
@ -71,7 +67,7 @@ void TransformAdapter2D::setPosition(const SkPoint& p) {
}
SkVector TransformAdapter2D::getScale() const {
return ValueTraits<VectorValue>::As<SkVector>(fScale);
return { fScale.x, fScale.y };
}
void TransformAdapter2D::setScale(const SkVector& s) {

View File

@ -61,12 +61,12 @@ public:
private:
void onSync() override;
VectorValue fAnchorPoint,
fPosition,
fScale = { 100, 100 };
ScalarValue fRotation = 0,
fSkew = 0,
fSkewAxis = 0;
Vec2Value fAnchorPoint = { 0, 0 },
fPosition = { 0, 0 },
fScale = { 100, 100 };
ScalarValue fRotation = 0,
fSkew = 0,
fSkewAxis = 0;
using INHERITED = DiscardableAdapterBase<TransformAdapter2D, sksg::Matrix<SkMatrix>>;
};