[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:
parent
dd86fb3531
commit
92ec801a2b
@ -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,
|
||||
|
@ -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>())
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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>>;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user