aboutsummaryrefslogtreecommitdiffhomepage
path: root/modules
diff options
context:
space:
mode:
authorGravatar Florin Malita <fmalita@chromium.org>2018-05-26 09:49:28 -0400
committerGravatar Skia Commit-Bot <skia-commit-bot@chromium.org>2018-05-27 02:21:33 +0000
commit3d856bdeee7fae2ff36cdb6a9807c588fc030eb1 (patch)
treeb26aa52b2d2f8877bdc7a7c647e4a34fd3e96f35 /modules
parentd8eb7b6b12d5b155214031d4aa4d8f582ebb91a1 (diff)
[skottie] Relocate to modules/skottie
TBR= Change-Id: I218d251ca56578a3a7fd4fb86cba9abdc10fb3bd Reviewed-on: https://skia-review.googlesource.com/130322 Reviewed-by: Florin Malita <fmalita@chromium.org> Commit-Queue: Florin Malita <fmalita@chromium.org>
Diffstat (limited to 'modules')
-rw-r--r--modules/skottie/BUILD.gn49
-rw-r--r--modules/skottie/fuzz/FuzzSkottieJSON.cpp35
-rw-r--r--modules/skottie/include/Skottie.h80
-rw-r--r--modules/skottie/src/Skottie.cpp1327
-rw-r--r--modules/skottie/src/SkottieAdapter.cpp166
-rw-r--r--modules/skottie/src/SkottieAdapter.h164
-rw-r--r--modules/skottie/src/SkottieAnimator.cpp374
-rw-r--r--modules/skottie/src/SkottieAnimator.h29
-rw-r--r--modules/skottie/src/SkottieJson.cpp243
-rw-r--r--modules/skottie/src/SkottieJson.h76
-rw-r--r--modules/skottie/src/SkottieValue.cpp161
-rw-r--r--modules/skottie/src/SkottieValue.h64
12 files changed, 2768 insertions, 0 deletions
diff --git a/modules/skottie/BUILD.gn b/modules/skottie/BUILD.gn
new file mode 100644
index 0000000000..b3c532c00c
--- /dev/null
+++ b/modules/skottie/BUILD.gn
@@ -0,0 +1,49 @@
+# Copyright 2018 Google Inc.
+#
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+declare_args() {
+ skia_enable_skottie = true
+}
+
+config("public_config") {
+ if (skia_enable_skottie) {
+ defines = [ "SK_ENABLE_SKOTTIE" ]
+ include_dirs = [ "include" ]
+ }
+}
+
+source_set("skottie") {
+ if (skia_enable_skottie) {
+ public_configs = [ ":public_config" ]
+ sources = [
+ "src/Skottie.cpp",
+ "src/SkottieAdapter.cpp",
+ "src/SkottieAnimator.cpp",
+ "src/SkottieJson.cpp",
+ "src/SkottieValue.cpp",
+ ]
+ configs += [ "../../:skia_private" ]
+ deps = [
+ "../..:skia",
+ "../../third_party/rapidjson",
+ "../sksg:sksg",
+ ]
+ }
+}
+
+source_set("fuzz") {
+ if (skia_enable_skottie) {
+ testonly = true
+
+ configs += [ "../..:skia_private" ]
+ sources = [
+ "fuzz/FuzzSkottieJSON.cpp",
+ ]
+ deps = [
+ ":skottie",
+ "../..:skia", # TODO: refactor to make this nicer
+ ]
+ }
+}
diff --git a/modules/skottie/fuzz/FuzzSkottieJSON.cpp b/modules/skottie/fuzz/FuzzSkottieJSON.cpp
new file mode 100644
index 0000000000..e4f19ccad7
--- /dev/null
+++ b/modules/skottie/fuzz/FuzzSkottieJSON.cpp
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2018 Google, LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "SkData.h"
+#include "Skottie.h"
+#include "SkStream.h"
+
+void FuzzSkottieJSON(sk_sp<SkData> bytes) {
+ // Always returns nullptr to any resource
+ class EmptyResourceProvider final : public skottie::ResourceProvider {
+ public:
+ std::unique_ptr<SkStream> openStream(const char resource[]) const override {
+ return nullptr;
+ }
+ };
+ SkMemoryStream stream(bytes);
+ EmptyResourceProvider erp;
+ auto animation = skottie::Animation::Make(&stream, erp);
+ if (!animation) {
+ return;
+ }
+ animation->animationTick(1337); // A "nothing up my sleeve" number
+}
+
+#if defined(IS_FUZZING_WITH_LIBFUZZER)
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
+ auto bytes = SkData::MakeWithoutCopy(data, size);
+ FuzzSkottieJSON(bytes);
+ return 0;
+}
+#endif
diff --git a/modules/skottie/include/Skottie.h b/modules/skottie/include/Skottie.h
new file mode 100644
index 0000000000..0a89ca0438
--- /dev/null
+++ b/modules/skottie/include/Skottie.h
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef Skottie_DEFINED
+#define Skottie_DEFINED
+
+#include "SkRefCnt.h"
+#include "SkSize.h"
+#include "SkString.h"
+#include "SkTypes.h"
+
+#include <memory>
+
+class SkCanvas;
+struct SkRect;
+class SkStream;
+
+namespace sksg { class Scene; }
+
+namespace skottie {
+
+namespace json { class ValueRef; }
+
+class SK_API ResourceProvider : public SkNoncopyable {
+public:
+ virtual ~ResourceProvider() = default;
+
+ virtual std::unique_ptr<SkStream> openStream(const char resource[]) const = 0;
+};
+
+class SK_API Animation : public SkRefCnt {
+public:
+ struct Stats {
+ float fTotalLoadTimeMS,
+ fJsonParseTimeMS,
+ fSceneParseTimeMS;
+ size_t fJsonSize,
+ fAnimatorCount;
+ };
+
+ static sk_sp<Animation> Make(SkStream*, const ResourceProvider&, Stats* = nullptr);
+ static sk_sp<Animation> MakeFromFile(const char path[], const ResourceProvider* = nullptr,
+ Stats* = nullptr);
+
+ ~Animation() override;
+
+ void render(SkCanvas*, const SkRect* dst = nullptr) const;
+
+ void animationTick(SkMSec);
+
+ const SkString& version() const { return fVersion; }
+ const SkSize& size() const { return fSize; }
+ SkScalar frameRate() const { return fFrameRate; }
+ SkScalar inPoint() const { return fInPoint; }
+ SkScalar outPoint() const { return fOutPoint; }
+
+ void setShowInval(bool show);
+
+private:
+ Animation(const ResourceProvider&, SkString ver, const SkSize& size, SkScalar fps,
+ const json::ValueRef&, Stats*);
+
+ SkString fVersion;
+ SkSize fSize;
+ SkScalar fFrameRate,
+ fInPoint,
+ fOutPoint;
+
+ std::unique_ptr<sksg::Scene> fScene;
+
+ typedef SkRefCnt INHERITED;
+};
+
+} // namespace skottie
+
+#endif // Skottie_DEFINED
diff --git a/modules/skottie/src/Skottie.cpp b/modules/skottie/src/Skottie.cpp
new file mode 100644
index 0000000000..8396b5ba90
--- /dev/null
+++ b/modules/skottie/src/Skottie.cpp
@@ -0,0 +1,1327 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "Skottie.h"
+
+#include "SkCanvas.h"
+#include "SkottieAdapter.h"
+#include "SkottieAnimator.h"
+#include "SkottieJson.h"
+#include "SkottieValue.h"
+#include "SkData.h"
+#include "SkImage.h"
+#include "SkMakeUnique.h"
+#include "SkOSPath.h"
+#include "SkPaint.h"
+#include "SkParse.h"
+#include "SkPoint.h"
+#include "SkSGClipEffect.h"
+#include "SkSGColor.h"
+#include "SkSGDraw.h"
+#include "SkSGGeometryTransform.h"
+#include "SkSGGradient.h"
+#include "SkSGGroup.h"
+#include "SkSGImage.h"
+#include "SkSGInvalidationController.h"
+#include "SkSGMaskEffect.h"
+#include "SkSGMerge.h"
+#include "SkSGOpacityEffect.h"
+#include "SkSGPath.h"
+#include "SkSGRect.h"
+#include "SkSGRoundEffect.h"
+#include "SkSGScene.h"
+#include "SkSGTransform.h"
+#include "SkSGTrimEffect.h"
+#include "SkStream.h"
+#include "SkTArray.h"
+#include "SkTime.h"
+#include "SkTHash.h"
+
+#include <cmath>
+#include <vector>
+
+#include "stdlib.h"
+
+namespace skottie {
+
+#define LOG SkDebugf
+
+namespace {
+
+struct AssetInfo {
+ json::ValueRef fAsset;
+ mutable bool fIsAttaching; // Used for cycle detection
+};
+
+using AssetMap = SkTHashMap<SkString, AssetInfo>;
+
+struct AttachContext {
+ const ResourceProvider& fResources;
+ const AssetMap& fAssets;
+ const float fFrameRate;
+ sksg::AnimatorList& fAnimators;
+};
+
+bool LogFail(const json::ValueRef& json, const char* msg) {
+ const auto dump = json.toString();
+ LOG("!! %s: %s\n", msg, dump.c_str());
+ return false;
+}
+
+sk_sp<sksg::Matrix> AttachMatrix(const json::ValueRef& t, AttachContext* ctx,
+ sk_sp<sksg::Matrix> parentMatrix) {
+ if (!t.isObject())
+ return nullptr;
+
+ auto matrix = sksg::Matrix::Make(SkMatrix::I(), std::move(parentMatrix));
+ auto adapter = sk_make_sp<TransformAdapter>(matrix);
+ auto anchor_attached = BindProperty<VectorValue>(t["a"], &ctx->fAnimators,
+ [adapter](const VectorValue& a) {
+ adapter->setAnchorPoint(ValueTraits<VectorValue>::As<SkPoint>(a));
+ });
+ auto position_attached = BindProperty<VectorValue>(t["p"], &ctx->fAnimators,
+ [adapter](const VectorValue& p) {
+ adapter->setPosition(ValueTraits<VectorValue>::As<SkPoint>(p));
+ });
+ auto scale_attached = BindProperty<VectorValue>(t["s"], &ctx->fAnimators,
+ [adapter](const VectorValue& s) {
+ adapter->setScale(ValueTraits<VectorValue>::As<SkVector>(s));
+ });
+
+ auto jrotation = t["r"];
+ if (jrotation.isNull()) {
+ // 3d rotations have separate rx,ry,rz components. While we don't fully support them,
+ // we can still make use of rz.
+ jrotation = t["rz"];
+ }
+ auto rotation_attached = BindProperty<ScalarValue>(jrotation, &ctx->fAnimators,
+ [adapter](const ScalarValue& r) {
+ adapter->setRotation(r);
+ });
+ auto skew_attached = BindProperty<ScalarValue>(t["sk"], &ctx->fAnimators,
+ [adapter](const ScalarValue& sk) {
+ adapter->setSkew(sk);
+ });
+ auto skewaxis_attached = BindProperty<ScalarValue>(t["sa"], &ctx->fAnimators,
+ [adapter](const ScalarValue& sa) {
+ adapter->setSkewAxis(sa);
+ });
+
+ if (!anchor_attached &&
+ !position_attached &&
+ !scale_attached &&
+ !rotation_attached &&
+ !skew_attached &&
+ !skewaxis_attached) {
+ LogFail(t, "Could not parse transform");
+ return nullptr;
+ }
+
+ return matrix;
+}
+
+sk_sp<sksg::RenderNode> AttachOpacity(const json::ValueRef& jtransform, AttachContext* ctx,
+ sk_sp<sksg::RenderNode> childNode) {
+ if (!jtransform.isObject() || !childNode)
+ return childNode;
+
+ static constexpr ScalarValue kNoopOpacity = 100;
+ auto opacityNode = sksg::OpacityEffect::Make(childNode);
+
+ if (!BindProperty<ScalarValue>(jtransform["o"], &ctx->fAnimators,
+ [opacityNode](const ScalarValue& o) {
+ // BM opacity is [0..100]
+ opacityNode->setOpacity(o * 0.01f);
+ }, &kNoopOpacity)) {
+ // We can ignore static full opacity.
+ return childNode;
+ }
+
+ return std::move(opacityNode);
+}
+
+sk_sp<sksg::RenderNode> AttachComposition(const json::ValueRef&, AttachContext* ctx);
+
+sk_sp<sksg::Path> AttachPath(const json::ValueRef& jpath, AttachContext* ctx) {
+ auto path_node = sksg::Path::Make();
+ return BindProperty<ShapeValue>(jpath, &ctx->fAnimators,
+ [path_node](const ShapeValue& p) {
+ path_node->setPath(ValueTraits<ShapeValue>::As<SkPath>(p));
+ })
+ ? path_node
+ : nullptr;
+}
+
+sk_sp<sksg::GeometryNode> AttachPathGeometry(const json::ValueRef& jpath, AttachContext* ctx) {
+ SkASSERT(jpath.isObject());
+
+ return AttachPath(jpath["ks"], ctx);
+}
+
+sk_sp<sksg::GeometryNode> AttachRRectGeometry(const json::ValueRef& jrect, AttachContext* ctx) {
+ SkASSERT(jrect.isObject());
+
+ auto rect_node = sksg::RRect::Make();
+ auto adapter = sk_make_sp<RRectAdapter>(rect_node);
+
+ auto p_attached = BindProperty<VectorValue>(jrect["p"], &ctx->fAnimators,
+ [adapter](const VectorValue& p) {
+ adapter->setPosition(ValueTraits<VectorValue>::As<SkPoint>(p));
+ });
+ auto s_attached = BindProperty<VectorValue>(jrect["s"], &ctx->fAnimators,
+ [adapter](const VectorValue& s) {
+ adapter->setSize(ValueTraits<VectorValue>::As<SkSize>(s));
+ });
+ auto r_attached = BindProperty<ScalarValue>(jrect["r"], &ctx->fAnimators,
+ [adapter](const ScalarValue& r) {
+ adapter->setRadius(SkSize::Make(r, r));
+ });
+
+ if (!p_attached && !s_attached && !r_attached) {
+ return nullptr;
+ }
+
+ return std::move(rect_node);
+}
+
+sk_sp<sksg::GeometryNode> AttachEllipseGeometry(const json::ValueRef& jellipse, AttachContext* ctx) {
+ SkASSERT(jellipse.isObject());
+
+ auto rect_node = sksg::RRect::Make();
+ auto adapter = sk_make_sp<RRectAdapter>(rect_node);
+
+ auto p_attached = BindProperty<VectorValue>(jellipse["p"], &ctx->fAnimators,
+ [adapter](const VectorValue& p) {
+ adapter->setPosition(ValueTraits<VectorValue>::As<SkPoint>(p));
+ });
+ auto s_attached = BindProperty<VectorValue>(jellipse["s"], &ctx->fAnimators,
+ [adapter](const VectorValue& s) {
+ const auto sz = ValueTraits<VectorValue>::As<SkSize>(s);
+ adapter->setSize(sz);
+ adapter->setRadius(SkSize::Make(sz.width() / 2, sz.height() / 2));
+ });
+
+ if (!p_attached && !s_attached) {
+ return nullptr;
+ }
+
+ return std::move(rect_node);
+}
+
+sk_sp<sksg::GeometryNode> AttachPolystarGeometry(const json::ValueRef& jstar, AttachContext* ctx) {
+ SkASSERT(jstar.isObject());
+
+ static constexpr PolyStarAdapter::Type gTypes[] = {
+ PolyStarAdapter::Type::kStar, // "sy": 1
+ PolyStarAdapter::Type::kPoly, // "sy": 2
+ };
+
+ const auto type = jstar["sy"].toDefault<int>(0) - 1;
+ if (type < 0 || type >= SkTo<int>(SK_ARRAY_COUNT(gTypes))) {
+ LogFail(jstar, "Unknown polystar type");
+ return nullptr;
+ }
+
+ auto path_node = sksg::Path::Make();
+ auto adapter = sk_make_sp<PolyStarAdapter>(path_node, gTypes[type]);
+
+ BindProperty<VectorValue>(jstar["p"], &ctx->fAnimators,
+ [adapter](const VectorValue& p) {
+ adapter->setPosition(ValueTraits<VectorValue>::As<SkPoint>(p));
+ });
+ BindProperty<ScalarValue>(jstar["pt"], &ctx->fAnimators,
+ [adapter](const ScalarValue& pt) {
+ adapter->setPointCount(pt);
+ });
+ BindProperty<ScalarValue>(jstar["ir"], &ctx->fAnimators,
+ [adapter](const ScalarValue& ir) {
+ adapter->setInnerRadius(ir);
+ });
+ BindProperty<ScalarValue>(jstar["or"], &ctx->fAnimators,
+ [adapter](const ScalarValue& otr) {
+ adapter->setOuterRadius(otr);
+ });
+ BindProperty<ScalarValue>(jstar["is"], &ctx->fAnimators,
+ [adapter](const ScalarValue& is) {
+ adapter->setInnerRoundness(is);
+ });
+ BindProperty<ScalarValue>(jstar["os"], &ctx->fAnimators,
+ [adapter](const ScalarValue& os) {
+ adapter->setOuterRoundness(os);
+ });
+ BindProperty<ScalarValue>(jstar["r"], &ctx->fAnimators,
+ [adapter](const ScalarValue& r) {
+ adapter->setRotation(r);
+ });
+
+ return std::move(path_node);
+}
+
+sk_sp<sksg::Color> AttachColor(const json::ValueRef& obj, AttachContext* ctx) {
+ SkASSERT(obj.isObject());
+
+ auto color_node = sksg::Color::Make(SK_ColorBLACK);
+ auto color_attached = BindProperty<VectorValue>(obj["c"], &ctx->fAnimators,
+ [color_node](const VectorValue& c) {
+ color_node->setColor(ValueTraits<VectorValue>::As<SkColor>(c));
+ });
+
+ return color_attached ? color_node : nullptr;
+}
+
+sk_sp<sksg::Gradient> AttachGradient(const json::ValueRef& obj, AttachContext* ctx) {
+ SkASSERT(obj.isObject());
+
+ const auto stops = obj["g"];
+ if (!stops.isObject())
+ return nullptr;
+
+ const auto stopCount = stops["p"].toDefault<int>(-1);
+ if (stopCount < 0)
+ return nullptr;
+
+ sk_sp<sksg::Gradient> gradient_node;
+ sk_sp<GradientAdapter> adapter;
+
+ if (obj["t"].toDefault<int>(1) == 1) {
+ auto linear_node = sksg::LinearGradient::Make();
+ adapter = sk_make_sp<LinearGradientAdapter>(linear_node, stopCount);
+ gradient_node = std::move(linear_node);
+ } else {
+ auto radial_node = sksg::RadialGradient::Make();
+ adapter = sk_make_sp<RadialGradientAdapter>(radial_node, stopCount);
+
+ // TODO: highlight, angle
+ gradient_node = std::move(radial_node);
+ }
+
+ BindProperty<VectorValue>(stops["k"], &ctx->fAnimators,
+ [adapter](const VectorValue& stops) {
+ adapter->setColorStops(stops);
+ });
+ BindProperty<VectorValue>(obj["s"], &ctx->fAnimators,
+ [adapter](const VectorValue& s) {
+ adapter->setStartPoint(ValueTraits<VectorValue>::As<SkPoint>(s));
+ });
+ BindProperty<VectorValue>(obj["e"], &ctx->fAnimators,
+ [adapter](const VectorValue& e) {
+ adapter->setEndPoint(ValueTraits<VectorValue>::As<SkPoint>(e));
+ });
+
+ return gradient_node;
+}
+
+sk_sp<sksg::PaintNode> AttachPaint(const json::ValueRef& jpaint, AttachContext* ctx,
+ sk_sp<sksg::PaintNode> paint_node) {
+ if (paint_node) {
+ paint_node->setAntiAlias(true);
+
+ BindProperty<ScalarValue>(jpaint["o"], &ctx->fAnimators,
+ [paint_node](const ScalarValue& o) {
+ // BM opacity is [0..100]
+ paint_node->setOpacity(o * 0.01f);
+ });
+ }
+
+ return paint_node;
+}
+
+sk_sp<sksg::PaintNode> AttachStroke(const json::ValueRef& jstroke, AttachContext* ctx,
+ sk_sp<sksg::PaintNode> stroke_node) {
+ SkASSERT(jstroke.isObject());
+
+ if (!stroke_node)
+ return nullptr;
+
+ stroke_node->setStyle(SkPaint::kStroke_Style);
+
+ auto width_attached = BindProperty<ScalarValue>(jstroke["w"], &ctx->fAnimators,
+ [stroke_node](const ScalarValue& w) {
+ stroke_node->setStrokeWidth(w);
+ });
+ if (!width_attached)
+ return nullptr;
+
+ stroke_node->setStrokeMiter(jstroke["ml"].toDefault(4.0f));
+
+ static constexpr SkPaint::Join gJoins[] = {
+ SkPaint::kMiter_Join,
+ SkPaint::kRound_Join,
+ SkPaint::kBevel_Join,
+ };
+ stroke_node->setStrokeJoin(gJoins[SkTPin<int>(jstroke["lj"].toDefault<int>(1) - 1,
+ 0, SK_ARRAY_COUNT(gJoins) - 1)]);
+
+ static constexpr SkPaint::Cap gCaps[] = {
+ SkPaint::kButt_Cap,
+ SkPaint::kRound_Cap,
+ SkPaint::kSquare_Cap,
+ };
+ stroke_node->setStrokeCap(gCaps[SkTPin<int>(jstroke["lc"].toDefault<int>(1) - 1,
+ 0, SK_ARRAY_COUNT(gCaps) - 1)]);
+
+ return stroke_node;
+}
+
+sk_sp<sksg::PaintNode> AttachColorFill(const json::ValueRef& jfill, AttachContext* ctx) {
+ SkASSERT(jfill.isObject());
+
+ return AttachPaint(jfill, ctx, AttachColor(jfill, ctx));
+}
+
+sk_sp<sksg::PaintNode> AttachGradientFill(const json::ValueRef& jfill, AttachContext* ctx) {
+ SkASSERT(jfill.isObject());
+
+ return AttachPaint(jfill, ctx, AttachGradient(jfill, ctx));
+}
+
+sk_sp<sksg::PaintNode> AttachColorStroke(const json::ValueRef& jstroke, AttachContext* ctx) {
+ SkASSERT(jstroke.isObject());
+
+ return AttachStroke(jstroke, ctx, AttachPaint(jstroke, ctx, AttachColor(jstroke, ctx)));
+}
+
+sk_sp<sksg::PaintNode> AttachGradientStroke(const json::ValueRef& jstroke, AttachContext* ctx) {
+ SkASSERT(jstroke.isObject());
+
+ return AttachStroke(jstroke, ctx, AttachPaint(jstroke, ctx, AttachGradient(jstroke, ctx)));
+}
+
+std::vector<sk_sp<sksg::GeometryNode>> AttachMergeGeometryEffect(
+ const json::ValueRef& jmerge, AttachContext* ctx, std::vector<sk_sp<sksg::GeometryNode>>&& geos) {
+ std::vector<sk_sp<sksg::GeometryNode>> merged;
+
+ static constexpr sksg::Merge::Mode gModes[] = {
+ sksg::Merge::Mode::kMerge, // "mm": 1
+ sksg::Merge::Mode::kUnion, // "mm": 2
+ sksg::Merge::Mode::kDifference, // "mm": 3
+ sksg::Merge::Mode::kIntersect, // "mm": 4
+ sksg::Merge::Mode::kXOR , // "mm": 5
+ };
+
+ const auto mode = gModes[SkTPin<int>(jmerge["mm"].toDefault(1) - 1,
+ 0, SK_ARRAY_COUNT(gModes) - 1)];
+ merged.push_back(sksg::Merge::Make(std::move(geos), mode));
+
+ return merged;
+}
+
+std::vector<sk_sp<sksg::GeometryNode>> AttachTrimGeometryEffect(
+ const json::ValueRef& jtrim, AttachContext* ctx, std::vector<sk_sp<sksg::GeometryNode>>&& geos) {
+
+ enum class Mode {
+ kMerged, // "m": 1
+ kSeparate, // "m": 2
+ } gModes[] = { Mode::kMerged, Mode::kSeparate };
+
+ const auto mode = gModes[SkTPin<int>(jtrim["m"].toDefault(1) - 1,
+ 0, SK_ARRAY_COUNT(gModes) - 1)];
+
+ std::vector<sk_sp<sksg::GeometryNode>> inputs;
+ if (mode == Mode::kMerged) {
+ inputs.push_back(sksg::Merge::Make(std::move(geos), sksg::Merge::Mode::kMerge));
+ } else {
+ inputs = std::move(geos);
+ }
+
+ std::vector<sk_sp<sksg::GeometryNode>> trimmed;
+ trimmed.reserve(inputs.size());
+ for (const auto& i : inputs) {
+ const auto trimEffect = sksg::TrimEffect::Make(i);
+ trimmed.push_back(trimEffect);
+
+ const auto adapter = sk_make_sp<TrimEffectAdapter>(std::move(trimEffect));
+ BindProperty<ScalarValue>(jtrim["s"], &ctx->fAnimators,
+ [adapter](const ScalarValue& s) {
+ adapter->setStart(s);
+ });
+ BindProperty<ScalarValue>(jtrim["e"], &ctx->fAnimators,
+ [adapter](const ScalarValue& e) {
+ adapter->setEnd(e);
+ });
+ BindProperty<ScalarValue>(jtrim["o"], &ctx->fAnimators,
+ [adapter](const ScalarValue& o) {
+ adapter->setOffset(o);
+ });
+ }
+
+ return trimmed;
+}
+
+std::vector<sk_sp<sksg::GeometryNode>> AttachRoundGeometryEffect(
+ const json::ValueRef& jtrim, AttachContext* ctx, std::vector<sk_sp<sksg::GeometryNode>>&& geos) {
+
+ std::vector<sk_sp<sksg::GeometryNode>> rounded;
+ rounded.reserve(geos.size());
+
+ for (const auto& g : geos) {
+ const auto roundEffect = sksg::RoundEffect::Make(std::move(g));
+ rounded.push_back(roundEffect);
+
+ BindProperty<ScalarValue>(jtrim["r"], &ctx->fAnimators,
+ [roundEffect](const ScalarValue& r) {
+ roundEffect->setRadius(r);
+ });
+ }
+
+ return rounded;
+}
+
+using GeometryAttacherT = sk_sp<sksg::GeometryNode> (*)(const json::ValueRef&, AttachContext*);
+static constexpr GeometryAttacherT gGeometryAttachers[] = {
+ AttachPathGeometry,
+ AttachRRectGeometry,
+ AttachEllipseGeometry,
+ AttachPolystarGeometry,
+};
+
+using PaintAttacherT = sk_sp<sksg::PaintNode> (*)(const json::ValueRef&, AttachContext*);
+static constexpr PaintAttacherT gPaintAttachers[] = {
+ AttachColorFill,
+ AttachColorStroke,
+ AttachGradientFill,
+ AttachGradientStroke,
+};
+
+using GeometryEffectAttacherT =
+ std::vector<sk_sp<sksg::GeometryNode>> (*)(const json::ValueRef&,
+ AttachContext*,
+ std::vector<sk_sp<sksg::GeometryNode>>&&);
+static constexpr GeometryEffectAttacherT gGeometryEffectAttachers[] = {
+ AttachMergeGeometryEffect,
+ AttachTrimGeometryEffect,
+ AttachRoundGeometryEffect,
+};
+
+enum class ShapeType {
+ kGeometry,
+ kGeometryEffect,
+ kPaint,
+ kGroup,
+ kTransform,
+};
+
+struct ShapeInfo {
+ const char* fTypeString;
+ ShapeType fShapeType;
+ uint32_t fAttacherIndex; // index into respective attacher tables
+};
+
+const ShapeInfo* FindShapeInfo(const json::ValueRef& shape) {
+ static constexpr ShapeInfo gShapeInfo[] = {
+ { "el", ShapeType::kGeometry , 2 }, // ellipse -> AttachEllipseGeometry
+ { "fl", ShapeType::kPaint , 0 }, // fill -> AttachColorFill
+ { "gf", ShapeType::kPaint , 2 }, // gfill -> AttachGradientFill
+ { "gr", ShapeType::kGroup , 0 }, // group -> Inline handler
+ { "gs", ShapeType::kPaint , 3 }, // gstroke -> AttachGradientStroke
+ { "mm", ShapeType::kGeometryEffect, 0 }, // merge -> AttachMergeGeometryEffect
+ { "rc", ShapeType::kGeometry , 1 }, // rrect -> AttachRRectGeometry
+ { "rd", ShapeType::kGeometryEffect, 2 }, // round -> AttachRoundGeometryEffect
+ { "sh", ShapeType::kGeometry , 0 }, // shape -> AttachPathGeometry
+ { "sr", ShapeType::kGeometry , 3 }, // polystar -> AttachPolyStarGeometry
+ { "st", ShapeType::kPaint , 1 }, // stroke -> AttachColorStroke
+ { "tm", ShapeType::kGeometryEffect, 1 }, // trim -> AttachTrimGeometryEffect
+ { "tr", ShapeType::kTransform , 0 }, // transform -> Inline handler
+ };
+
+ SkString type;
+ if (!shape["ty"].to(&type) || type.isEmpty())
+ return nullptr;
+
+ const auto* info = bsearch(type.c_str(),
+ gShapeInfo,
+ SK_ARRAY_COUNT(gShapeInfo),
+ sizeof(ShapeInfo),
+ [](const void* key, const void* info) {
+ return strcmp(static_cast<const char*>(key),
+ static_cast<const ShapeInfo*>(info)->fTypeString);
+ });
+
+ return static_cast<const ShapeInfo*>(info);
+}
+
+struct GeometryEffectRec {
+ const json::ValueRef fJson;
+ GeometryEffectAttacherT fAttach;
+};
+
+struct AttachShapeContext {
+ AttachShapeContext(AttachContext* ctx,
+ std::vector<sk_sp<sksg::GeometryNode>>* geos,
+ std::vector<GeometryEffectRec>* effects,
+ size_t committedAnimators)
+ : fCtx(ctx)
+ , fGeometryStack(geos)
+ , fGeometryEffectStack(effects)
+ , fCommittedAnimators(committedAnimators) {}
+
+ AttachContext* fCtx;
+ std::vector<sk_sp<sksg::GeometryNode>>* fGeometryStack;
+ std::vector<GeometryEffectRec>* fGeometryEffectStack;
+ size_t fCommittedAnimators;
+};
+
+sk_sp<sksg::RenderNode> AttachShape(const json::ValueRef& jshape, AttachShapeContext* shapeCtx) {
+ if (!jshape.isArray())
+ return nullptr;
+
+ SkDEBUGCODE(const auto initialGeometryEffects = shapeCtx->fGeometryEffectStack->size();)
+
+ sk_sp<sksg::Group> shape_group = sksg::Group::Make();
+ sk_sp<sksg::RenderNode> shape_wrapper = shape_group;
+ sk_sp<sksg::Matrix> shape_matrix;
+
+ struct ShapeRec {
+ const json::ValueRef fJson;
+ const ShapeInfo& fInfo;
+ };
+
+ // First pass (bottom->top):
+ //
+ // * pick up the group transform and opacity
+ // * push local geometry effects onto the stack
+ // * store recs for next pass
+ //
+ std::vector<ShapeRec> recs;
+ for (size_t i = 0; i < jshape.size(); ++i) {
+ const auto s = jshape[jshape.size() - 1 - i];
+ const auto* info = FindShapeInfo(s);
+ if (!info) {
+ LogFail(s["ty"], "Unknown shape");
+ continue;
+ }
+
+ recs.push_back({ s, *info });
+
+ switch (info->fShapeType) {
+ case ShapeType::kTransform:
+ if ((shape_matrix = AttachMatrix(s, shapeCtx->fCtx, nullptr))) {
+ shape_wrapper = sksg::Transform::Make(std::move(shape_wrapper), shape_matrix);
+ }
+ shape_wrapper = AttachOpacity(s, shapeCtx->fCtx, std::move(shape_wrapper));
+ break;
+ case ShapeType::kGeometryEffect:
+ SkASSERT(info->fAttacherIndex < SK_ARRAY_COUNT(gGeometryEffectAttachers));
+ shapeCtx->fGeometryEffectStack->push_back(
+ { s, gGeometryEffectAttachers[info->fAttacherIndex] });
+ break;
+ default:
+ break;
+ }
+ }
+
+ // Second pass (top -> bottom, after 2x reverse):
+ //
+ // * track local geometry
+ // * emit local paints
+ //
+ std::vector<sk_sp<sksg::GeometryNode>> geos;
+ std::vector<sk_sp<sksg::RenderNode >> draws;
+ for (auto rec = recs.rbegin(); rec != recs.rend(); ++rec) {
+ switch (rec->fInfo.fShapeType) {
+ case ShapeType::kGeometry: {
+ SkASSERT(rec->fInfo.fAttacherIndex < SK_ARRAY_COUNT(gGeometryAttachers));
+ if (auto geo = gGeometryAttachers[rec->fInfo.fAttacherIndex](rec->fJson,
+ shapeCtx->fCtx)) {
+ geos.push_back(std::move(geo));
+ }
+ } break;
+ case ShapeType::kGeometryEffect: {
+ // Apply the current effect and pop from the stack.
+ SkASSERT(rec->fInfo.fAttacherIndex < SK_ARRAY_COUNT(gGeometryEffectAttachers));
+ if (!geos.empty()) {
+ geos = gGeometryEffectAttachers[rec->fInfo.fAttacherIndex](rec->fJson,
+ shapeCtx->fCtx,
+ std::move(geos));
+ }
+
+ SkASSERT(shapeCtx->fGeometryEffectStack->back().fJson == rec->fJson);
+ SkASSERT(shapeCtx->fGeometryEffectStack->back().fAttach ==
+ gGeometryEffectAttachers[rec->fInfo.fAttacherIndex]);
+ shapeCtx->fGeometryEffectStack->pop_back();
+ } break;
+ case ShapeType::kGroup: {
+ AttachShapeContext groupShapeCtx(shapeCtx->fCtx,
+ &geos,
+ shapeCtx->fGeometryEffectStack,
+ shapeCtx->fCommittedAnimators);
+ if (auto subgroup = AttachShape(rec->fJson["it"], &groupShapeCtx)) {
+ draws.push_back(std::move(subgroup));
+ SkASSERT(groupShapeCtx.fCommittedAnimators >= shapeCtx->fCommittedAnimators);
+ shapeCtx->fCommittedAnimators = groupShapeCtx.fCommittedAnimators;
+ }
+ } break;
+ case ShapeType::kPaint: {
+ SkASSERT(rec->fInfo.fAttacherIndex < SK_ARRAY_COUNT(gPaintAttachers));
+ auto paint = gPaintAttachers[rec->fInfo.fAttacherIndex](rec->fJson, shapeCtx->fCtx);
+ if (!paint || geos.empty())
+ break;
+
+ auto drawGeos = geos;
+
+ // Apply all pending effects from the stack.
+ for (auto it = shapeCtx->fGeometryEffectStack->rbegin();
+ it != shapeCtx->fGeometryEffectStack->rend(); ++it) {
+ drawGeos = it->fAttach(it->fJson, shapeCtx->fCtx, std::move(drawGeos));
+ }
+
+ // If we still have multiple geos, reduce using 'merge'.
+ auto geo = drawGeos.size() > 1
+ ? sksg::Merge::Make(std::move(drawGeos), sksg::Merge::Mode::kMerge)
+ : drawGeos[0];
+
+ SkASSERT(geo);
+ draws.push_back(sksg::Draw::Make(std::move(geo), std::move(paint)));
+ shapeCtx->fCommittedAnimators = shapeCtx->fCtx->fAnimators.size();
+ } break;
+ default:
+ break;
+ }
+ }
+
+ // By now we should have popped all local geometry effects.
+ SkASSERT(shapeCtx->fGeometryEffectStack->size() == initialGeometryEffects);
+
+ // Push transformed local geometries to parent list, for subsequent paints.
+ for (const auto& geo : geos) {
+ shapeCtx->fGeometryStack->push_back(shape_matrix
+ ? sksg::GeometryTransform::Make(std::move(geo), shape_matrix)
+ : std::move(geo));
+ }
+
+ // Emit local draws reversed (bottom->top, per spec).
+ for (auto it = draws.rbegin(); it != draws.rend(); ++it) {
+ shape_group->addChild(std::move(*it));
+ }
+
+ return draws.empty() ? nullptr : shape_wrapper;
+}
+
+sk_sp<sksg::RenderNode> AttachNestedAnimation(const char* path, AttachContext* ctx) {
+ class SkottieSGAdapter final : public sksg::RenderNode {
+ public:
+ explicit SkottieSGAdapter(sk_sp<Animation> animation)
+ : fAnimation(std::move(animation)) {
+ SkASSERT(fAnimation);
+ }
+
+ protected:
+ SkRect onRevalidate(sksg::InvalidationController*, const SkMatrix&) override {
+ return SkRect::MakeSize(fAnimation->size());
+ }
+
+ void onRender(SkCanvas* canvas) const override {
+ fAnimation->render(canvas);
+ }
+
+ private:
+ const sk_sp<Animation> fAnimation;
+ };
+
+ class SkottieAnimatorAdapter final : public sksg::Animator {
+ public:
+ SkottieAnimatorAdapter(sk_sp<Animation> animation, float frameRate)
+ : fAnimation(std::move(animation))
+ , fFrameRate(frameRate) {
+ SkASSERT(fAnimation);
+ SkASSERT(fFrameRate > 0);
+ }
+
+ protected:
+ void onTick(float t) {
+ // map back from frame # to ms.
+ const auto t_ms = t * 1000 / fFrameRate;
+ fAnimation->animationTick(t_ms);
+ }
+
+ private:
+ const sk_sp<Animation> fAnimation;
+ const float fFrameRate;
+ };
+
+ const auto resStream = ctx->fResources.openStream(path);
+ if (!resStream || !resStream->hasLength()) {
+ LOG("!! Could not open: %s\n", path);
+ return nullptr;
+ }
+
+ auto animation = Animation::Make(resStream.get(), ctx->fResources);
+ if (!animation) {
+ LOG("!! Could not load nested animation: %s\n", path);
+ return nullptr;
+ }
+
+ ctx->fAnimators.push_back(skstd::make_unique<SkottieAnimatorAdapter>(animation,
+ ctx->fFrameRate));
+
+ return sk_make_sp<SkottieSGAdapter>(std::move(animation));
+}
+
+sk_sp<sksg::RenderNode> AttachAssetRef(const json::ValueRef& jlayer, AttachContext* ctx,
+ sk_sp<sksg::RenderNode>(*attach_proc)(const json::ValueRef& comp, AttachContext* ctx)) {
+
+ const auto refId = jlayer["refId"].toDefault(SkString());
+ if (refId.isEmpty()) {
+ LOG("!! Layer missing refId\n");
+ return nullptr;
+ }
+
+ if (refId.startsWith("$")) {
+ return AttachNestedAnimation(refId.c_str() + 1, ctx);
+ }
+
+ const auto* asset_info = ctx->fAssets.find(refId);
+ if (!asset_info) {
+ LOG("!! Asset not found: '%s'\n", refId.c_str());
+ return nullptr;
+ }
+
+ if (asset_info->fIsAttaching) {
+ LOG("!! Asset cycle detected for: '%s'\n", refId.c_str());
+ return nullptr;
+ }
+
+ asset_info->fIsAttaching = true;
+ auto asset = attach_proc(asset_info->fAsset, ctx);
+ asset_info->fIsAttaching = false;
+
+ return asset;
+}
+
+sk_sp<sksg::RenderNode> AttachCompLayer(const json::ValueRef& jlayer, AttachContext* ctx,
+ float* time_bias, float* time_scale) {
+ SkASSERT(jlayer.isObject());
+
+ const auto start_time = jlayer["st"].toDefault(0.0f),
+ stretch_time = jlayer["sr"].toDefault(1.0f);
+
+ *time_bias = -start_time;
+ *time_scale = sk_ieee_float_divide(1, stretch_time);
+ if (SkScalarIsNaN(*time_scale)) {
+ *time_scale = 1;
+ }
+
+ return AttachAssetRef(jlayer, ctx, AttachComposition);
+}
+
+sk_sp<sksg::RenderNode> AttachSolidLayer(const json::ValueRef& jlayer, AttachContext*,
+ float*, float*) {
+ SkASSERT(jlayer.isObject());
+
+ const auto size = SkSize::Make(jlayer["sw"].toDefault(0.0f),
+ jlayer["sh"].toDefault(0.0f));
+ const auto hex = jlayer["sc"].toDefault(SkString());
+ uint32_t c;
+ if (size.isEmpty() ||
+ !hex.startsWith("#") ||
+ !SkParse::FindHex(hex.c_str() + 1, &c)) {
+ LogFail(jlayer, "Could not parse solid layer");
+ return nullptr;
+ }
+
+ const SkColor color = 0xff000000 | c;
+
+ return sksg::Draw::Make(sksg::Rect::Make(SkRect::MakeSize(size)),
+ sksg::Color::Make(color));
+}
+
+sk_sp<sksg::RenderNode> AttachImageAsset(const json::ValueRef& jimage, AttachContext* ctx) {
+ SkASSERT(jimage.isObject());
+
+ const auto name = jimage["p"].toDefault(SkString()),
+ path = jimage["u"].toDefault(SkString());
+ if (name.isEmpty())
+ return nullptr;
+
+ // TODO: plumb resource paths explicitly to ResourceProvider?
+ const auto resName = path.isEmpty() ? name : SkOSPath::Join(path.c_str(), name.c_str());
+ const auto resStream = ctx->fResources.openStream(resName.c_str());
+ if (!resStream || !resStream->hasLength()) {
+ LOG("!! Could not load image resource: %s\n", resName.c_str());
+ return nullptr;
+ }
+
+ // TODO: non-intrisic image sizing
+ return sksg::Image::Make(
+ SkImage::MakeFromEncoded(SkData::MakeFromStream(resStream.get(), resStream->getLength())));
+}
+
+sk_sp<sksg::RenderNode> AttachImageLayer(const json::ValueRef& jlayer, AttachContext* ctx,
+ float*, float*) {
+ SkASSERT(jlayer.isObject());
+
+ return AttachAssetRef(jlayer, ctx, AttachImageAsset);
+}
+
+sk_sp<sksg::RenderNode> AttachNullLayer(const json::ValueRef& layer, AttachContext*, float*, float*) {
+ SkASSERT(layer.isObject());
+
+ // Null layers are used solely to drive dependent transforms,
+ // but we use free-floating sksg::Matrices for that purpose.
+ return nullptr;
+}
+
+sk_sp<sksg::RenderNode> AttachShapeLayer(const json::ValueRef& layer, AttachContext* ctx,
+ float*, float*) {
+ SkASSERT(layer.isObject());
+
+ std::vector<sk_sp<sksg::GeometryNode>> geometryStack;
+ std::vector<GeometryEffectRec> geometryEffectStack;
+ AttachShapeContext shapeCtx(ctx, &geometryStack, &geometryEffectStack, ctx->fAnimators.size());
+ auto shapeNode = AttachShape(layer["shapes"], &shapeCtx);
+
+ // Trim uncommitted animators: AttachShape consumes effects on the fly, and greedily attaches
+ // geometries => at the end, we can end up with unused geometries, which are nevertheless alive
+ // due to attached animators. To avoid this, we track committed animators and discard the
+ // orphans here.
+ SkASSERT(shapeCtx.fCommittedAnimators <= ctx->fAnimators.size());
+ ctx->fAnimators.resize(shapeCtx.fCommittedAnimators);
+
+ return shapeNode;
+}
+
+sk_sp<sksg::RenderNode> AttachTextLayer(const json::ValueRef& layer, AttachContext*, float*, float*) {
+ SkASSERT(layer.isObject());
+
+ LOG("?? Text layer stub\n");
+ return nullptr;
+}
+
+struct AttachLayerContext {
+ AttachLayerContext(const json::ValueRef& jlayers, AttachContext* ctx)
+ : fLayerList(jlayers), fCtx(ctx) {
+ SkASSERT(fLayerList.isArray());
+ }
+
+ const json::ValueRef fLayerList;
+ AttachContext* fCtx;
+ SkTHashMap<int, sk_sp<sksg::Matrix>> fLayerMatrixMap;
+ sk_sp<sksg::RenderNode> fCurrentMatte;
+
+ sk_sp<sksg::Matrix> AttachLayerMatrix(const json::ValueRef& jlayer) {
+ SkASSERT(jlayer.isObject());
+
+ const auto layer_index = jlayer["ind"].toDefault<int>(-1);
+ if (layer_index < 0)
+ return nullptr;
+
+ if (auto* m = fLayerMatrixMap.find(layer_index))
+ return *m;
+
+ return this->AttachLayerMatrixImpl(jlayer, layer_index);
+ }
+
+private:
+ sk_sp<sksg::Matrix> AttachParentLayerMatrix(const json::ValueRef& jlayer, int layer_index) {
+ SkASSERT(jlayer.isObject());
+
+ const auto parent_index = jlayer["parent"].toDefault<int>(-1);
+ if (parent_index < 0 || parent_index == layer_index)
+ return nullptr;
+
+ if (auto* m = fLayerMatrixMap.find(parent_index))
+ return *m;
+
+ for (const json::ValueRef l : fLayerList) {
+ if (l["ind"].toDefault<int>(-1) == parent_index) {
+ return this->AttachLayerMatrixImpl(l, parent_index);
+ }
+ }
+
+ return nullptr;
+ }
+
+ sk_sp<sksg::Matrix> AttachLayerMatrixImpl(const json::ValueRef& jlayer, int layer_index) {
+ SkASSERT(!fLayerMatrixMap.find(layer_index));
+
+ // Add a stub entry to break recursion cycles.
+ fLayerMatrixMap.set(layer_index, nullptr);
+
+ auto parent_matrix = this->AttachParentLayerMatrix(jlayer, layer_index);
+
+ return *fLayerMatrixMap.set(layer_index, AttachMatrix(jlayer["ks"], fCtx, parent_matrix));
+ }
+};
+
+SkBlendMode MaskBlendMode(char mode) {
+ switch (mode) {
+ case 'a': return SkBlendMode::kSrcOver; // Additive
+ case 's': return SkBlendMode::kExclusion; // Subtract
+ case 'i': return SkBlendMode::kDstIn; // Intersect
+ case 'l': return SkBlendMode::kLighten; // Lighten
+ case 'd': return SkBlendMode::kDarken; // Darken
+ case 'f': return SkBlendMode::kDifference; // Difference
+ default: break;
+ }
+
+ return SkBlendMode::kSrcOver;
+}
+
+sk_sp<sksg::RenderNode> AttachMask(const json::ValueRef& jmask,
+ AttachContext* ctx,
+ sk_sp<sksg::RenderNode> childNode) {
+ if (!jmask.isArray())
+ return childNode;
+
+ struct MaskRecord {
+ sk_sp<sksg::Path> mask_path;
+ sk_sp<sksg::Color> mask_paint;
+ };
+
+ SkSTArray<4, MaskRecord, true> mask_stack;
+
+ bool opaque_mask = true;
+
+ for (const json::ValueRef m : jmask) {
+ if (!m.isObject())
+ continue;
+
+ auto mask_path = AttachPath(m["pt"], ctx);
+ if (!mask_path) {
+ LogFail(m, "Could not parse mask path");
+ continue;
+ }
+
+ mask_path->setFillType(m["inv"].toDefault(false)
+ ? SkPath::kInverseWinding_FillType
+ : SkPath::kWinding_FillType);
+
+ SkString mode;
+ if (!m["mode"].to(&mode) ||
+ mode.size() != 1 ||
+ !strcmp(mode.c_str(), "n")) { // "None" masks have no effect.
+ continue;
+ }
+
+ auto mask_paint = sksg::Color::Make(SK_ColorBLACK);
+ mask_paint->setAntiAlias(true);
+ mask_paint->setBlendMode(MaskBlendMode(mode.c_str()[0]));
+
+ const auto animator_count = ctx->fAnimators.size();
+ BindProperty<ScalarValue>(m["o"], &ctx->fAnimators,
+ [mask_paint](const ScalarValue& o) { mask_paint->setOpacity(o * 0.01f); });
+
+ opaque_mask &= (animator_count == ctx->fAnimators.size() && mask_paint->getOpacity() >= 1);
+
+ mask_stack.push_back({mask_path, mask_paint});
+ }
+
+ if (mask_stack.empty())
+ return childNode;
+
+ if (mask_stack.count() == 1 && opaque_mask) {
+ // Single opaque mask => clip path.
+ return sksg::ClipEffect::Make(std::move(childNode),
+ std::move(mask_stack.front().mask_path),
+ true);
+ }
+
+ auto mask_group = sksg::Group::Make();
+ for (const auto& rec : mask_stack) {
+ mask_group->addChild(sksg::Draw::Make(std::move(rec.mask_path),
+ std::move(rec.mask_paint)));
+
+ }
+
+ return sksg::MaskEffect::Make(std::move(childNode), std::move(mask_group));
+}
+
+sk_sp<sksg::RenderNode> AttachLayer(const json::ValueRef& jlayer, AttachLayerContext* layerCtx) {
+ if (!jlayer.isObject())
+ return nullptr;
+
+ using LayerAttacher = sk_sp<sksg::RenderNode> (*)(const json::ValueRef&, AttachContext*,
+ float* time_bias, float* time_scale);
+ static constexpr LayerAttacher gLayerAttachers[] = {
+ AttachCompLayer, // 'ty': 0
+ AttachSolidLayer, // 'ty': 1
+ AttachImageLayer, // 'ty': 2
+ AttachNullLayer, // 'ty': 3
+ AttachShapeLayer, // 'ty': 4
+ AttachTextLayer, // 'ty': 5
+ };
+
+ int type = jlayer["ty"].toDefault<int>(-1);
+ if (type < 0 || type >= SkTo<int>(SK_ARRAY_COUNT(gLayerAttachers))) {
+ return nullptr;
+ }
+
+ sksg::AnimatorList layer_animators;
+ AttachContext local_ctx = { layerCtx->fCtx->fResources,
+ layerCtx->fCtx->fAssets,
+ layerCtx->fCtx->fFrameRate,
+ layer_animators};
+
+ // Layer attachers may adjust these.
+ float time_bias = 0,
+ time_scale = 1;
+
+ // Layer content.
+ auto layer = gLayerAttachers[type](jlayer, &local_ctx, &time_bias, &time_scale);
+
+ // Clip layers with explicit dimensions.
+ float w = 0, h = 0;
+ if (jlayer["w"].to(&w) && jlayer["h"].to(&h)) {
+ layer = sksg::ClipEffect::Make(std::move(layer),
+ sksg::Rect::Make(SkRect::MakeWH(w, h)),
+ true);
+ }
+
+ // Optional layer mask.
+ layer = AttachMask(jlayer["masksProperties"], &local_ctx, std::move(layer));
+
+ // Optional layer transform.
+ if (auto layerMatrix = layerCtx->AttachLayerMatrix(jlayer)) {
+ layer = sksg::Transform::Make(std::move(layer), std::move(layerMatrix));
+ }
+
+ // Optional layer opacity.
+ layer = AttachOpacity(jlayer["ks"], &local_ctx, std::move(layer));
+
+ class LayerController final : public sksg::GroupAnimator {
+ public:
+ LayerController(sksg::AnimatorList&& layer_animators,
+ sk_sp<sksg::OpacityEffect> controlNode,
+ float in, float out,
+ float time_bias, float time_scale)
+ : INHERITED(std::move(layer_animators))
+ , fControlNode(std::move(controlNode))
+ , fIn(in)
+ , fOut(out)
+ , fTimeBias(time_bias)
+ , fTimeScale(time_scale) {}
+
+ void onTick(float t) override {
+ const auto active = (t >= fIn && t <= fOut);
+
+ // Keep the layer fully transparent except for its [in..out] lifespan.
+ // (note: opacity == 0 disables rendering, while opacity == 1 is a noop)
+ fControlNode->setOpacity(active ? 1 : 0);
+
+ // Dispatch ticks only while active.
+ if (active)
+ this->INHERITED::onTick((t + fTimeBias) * fTimeScale);
+ }
+
+ private:
+ const sk_sp<sksg::OpacityEffect> fControlNode;
+ const float fIn,
+ fOut,
+ fTimeBias,
+ fTimeScale;
+
+ using INHERITED = sksg::GroupAnimator;
+ };
+
+ auto controller_node = sksg::OpacityEffect::Make(std::move(layer));
+ const auto in = jlayer["ip"].toDefault(0.0f),
+ out = jlayer["op"].toDefault(in);
+
+ if (!jlayer["tm"].isNull()) {
+ LogFail(jlayer["tm"], "Unsupported time remapping");
+ }
+
+ if (in >= out || !controller_node)
+ return nullptr;
+
+ layerCtx->fCtx->fAnimators.push_back(
+ skstd::make_unique<LayerController>(std::move(layer_animators),
+ controller_node,
+ in,
+ out,
+ time_bias,
+ time_scale));
+
+ if (jlayer["td"].toDefault(false)) {
+ // This layer is a matte. We apply it as a mask to the next layer.
+ layerCtx->fCurrentMatte = std::move(controller_node);
+ return nullptr;
+ }
+
+ if (layerCtx->fCurrentMatte) {
+ // There is a pending matte. Apply and reset.
+ static constexpr sksg::MaskEffect::Mode gMaskModes[] = {
+ sksg::MaskEffect::Mode::kNormal, // tt: 1
+ sksg::MaskEffect::Mode::kInvert, // tt: 2
+ };
+ const auto matteType = jlayer["tt"].toDefault<int>(1) - 1;
+
+ if (matteType >= 0 && matteType < SkTo<int>(SK_ARRAY_COUNT(gMaskModes))) {
+ return sksg::MaskEffect::Make(std::move(controller_node),
+ std::move(layerCtx->fCurrentMatte),
+ gMaskModes[matteType]);
+ }
+ layerCtx->fCurrentMatte.reset();
+ }
+
+ return std::move(controller_node);
+}
+
+sk_sp<sksg::RenderNode> AttachComposition(const json::ValueRef& comp, AttachContext* ctx) {
+ if (!comp.isObject())
+ return nullptr;
+
+ const auto jlayers = comp["layers"];
+ if (!jlayers.isArray())
+ return nullptr;
+
+ SkSTArray<16, sk_sp<sksg::RenderNode>, true> layers;
+ AttachLayerContext layerCtx(jlayers, ctx);
+
+ for (const json::ValueRef l : jlayers) {
+ if (auto layer_fragment = AttachLayer(l, &layerCtx)) {
+ layers.push_back(std::move(layer_fragment));
+ }
+ }
+
+ if (layers.empty()) {
+ return nullptr;
+ }
+
+ // Layers are painted in bottom->top order.
+ auto comp_group = sksg::Group::Make();
+ for (int i = layers.count() - 1; i >= 0; --i) {
+ comp_group->addChild(std::move(layers[i]));
+ }
+
+ return std::move(comp_group);
+}
+
+} // namespace
+
+sk_sp<Animation> Animation::Make(SkStream* stream, const ResourceProvider& res, Stats* stats) {
+ Stats stats_storage;
+ if (!stats)
+ stats = &stats_storage;
+ memset(stats, 0, sizeof(struct Stats));
+
+ if (!stream->hasLength()) {
+ // TODO: handle explicit buffering?
+ LOG("!! cannot parse streaming content\n");
+ return nullptr;
+ }
+
+ stats->fJsonSize = stream->getLength();
+ const auto t0 = SkTime::GetMSecs();
+
+ const json::Document doc(stream);
+ const auto json = doc.root();
+ if (!json.isObject())
+ return nullptr;
+
+ const auto t1 = SkTime::GetMSecs();
+ stats->fJsonParseTimeMS = t1 - t0;
+
+ const auto version = json["v"].toDefault(SkString());
+ const auto size = SkSize::Make(json["w"].toDefault(0.0f),
+ json["h"].toDefault(0.0f));
+ const auto fps = json["fr"].toDefault(-1.0f);
+
+ if (size.isEmpty() || version.isEmpty() || fps <= 0) {
+ LOG("!! invalid animation params (version: %s, size: [%f %f], frame rate: %f)",
+ version.c_str(), size.width(), size.height(), fps);
+ return nullptr;
+ }
+
+ const auto anim =
+ sk_sp<Animation>(new Animation(res, std::move(version), size, fps, json, stats));
+ const auto t2 = SkTime::GetMSecs();
+ stats->fSceneParseTimeMS = t2 - t1;
+ stats->fTotalLoadTimeMS = t2 - t0;
+
+ return anim;
+}
+
+sk_sp<Animation> Animation::MakeFromFile(const char path[], const ResourceProvider* res,
+ Stats* stats) {
+ class DirectoryResourceProvider final : public ResourceProvider {
+ public:
+ explicit DirectoryResourceProvider(SkString dir) : fDir(std::move(dir)) {}
+
+ std::unique_ptr<SkStream> openStream(const char resource[]) const override {
+ const auto resPath = SkOSPath::Join(fDir.c_str(), resource);
+ return SkStream::MakeFromFile(resPath.c_str());
+ }
+
+ private:
+ const SkString fDir;
+ };
+
+ const auto jsonStream = SkStream::MakeFromFile(path);
+ if (!jsonStream)
+ return nullptr;
+
+ std::unique_ptr<ResourceProvider> defaultProvider;
+ if (!res) {
+ defaultProvider = skstd::make_unique<DirectoryResourceProvider>(SkOSPath::Dirname(path));
+ }
+
+ return Make(jsonStream.get(), res ? *res : *defaultProvider, stats);
+}
+
+Animation::Animation(const ResourceProvider& resources,
+ SkString version, const SkSize& size, SkScalar fps, const json::ValueRef& json,
+ Stats* stats)
+ : fVersion(std::move(version))
+ , fSize(size)
+ , fFrameRate(fps)
+ , fInPoint(json["ip"].toDefault(0.0f))
+ , fOutPoint(SkTMax(json["op"].toDefault(SK_ScalarMax), fInPoint)) {
+
+ AssetMap assets;
+ for (const json::ValueRef asset : json["assets"]) {
+ if (asset.isObject()) {
+ assets.set(asset["id"].toDefault(SkString()), { asset, false });
+ }
+ }
+
+ sksg::AnimatorList animators;
+ AttachContext ctx = { resources, assets, fFrameRate, animators };
+ auto root = AttachComposition(json, &ctx);
+
+ stats->fAnimatorCount = animators.size();
+
+ fScene = sksg::Scene::Make(std::move(root), std::move(animators));
+
+ // In case the client calls render before the first tick.
+ this->animationTick(0);
+}
+
+Animation::~Animation() = default;
+
+void Animation::setShowInval(bool show) {
+ if (fScene) {
+ fScene->setShowInval(show);
+ }
+}
+
+void Animation::render(SkCanvas* canvas, const SkRect* dstR) const {
+ if (!fScene)
+ return;
+
+ SkAutoCanvasRestore restore(canvas, true);
+ const SkRect srcR = SkRect::MakeSize(this->size());
+ if (dstR) {
+ canvas->concat(SkMatrix::MakeRectToRect(srcR, *dstR, SkMatrix::kCenter_ScaleToFit));
+ }
+ canvas->clipRect(srcR);
+ fScene->render(canvas);
+}
+
+void Animation::animationTick(SkMSec ms) {
+ if (!fScene)
+ return;
+
+ // 't' in the BM model really means 'frame #'
+ auto t = static_cast<float>(ms) * fFrameRate / 1000;
+
+ t = fInPoint + std::fmod(t, fOutPoint - fInPoint);
+
+ fScene->animate(t);
+}
+
+} // namespace skottie
diff --git a/modules/skottie/src/SkottieAdapter.cpp b/modules/skottie/src/SkottieAdapter.cpp
new file mode 100644
index 0000000000..a01599ccf6
--- /dev/null
+++ b/modules/skottie/src/SkottieAdapter.cpp
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2018 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "SkottieAdapter.h"
+
+#include "SkMatrix.h"
+#include "SkottieValue.h"
+#include "SkPath.h"
+#include "SkRRect.h"
+#include "SkSGGradient.h"
+#include "SkSGPath.h"
+#include "SkSGRect.h"
+#include "SkSGTransform.h"
+#include "SkSGTrimEffect.h"
+
+#include <cmath>
+
+namespace skottie {
+
+RRectAdapter::RRectAdapter(sk_sp<sksg::RRect> wrapped_node)
+ : fRRectNode(std::move(wrapped_node)) {}
+
+void RRectAdapter::apply() {
+ // BM "position" == "center position"
+ auto rr = SkRRect::MakeRectXY(SkRect::MakeXYWH(fPosition.x() - fSize.width() / 2,
+ fPosition.y() - fSize.height() / 2,
+ fSize.width(), fSize.height()),
+ fRadius.width(),
+ fRadius.height());
+ fRRectNode->setRRect(rr);
+}
+
+TransformAdapter::TransformAdapter(sk_sp<sksg::Matrix> matrix)
+ : fMatrixNode(std::move(matrix)) {}
+
+void TransformAdapter::apply() {
+ SkMatrix t = SkMatrix::MakeTrans(-fAnchorPoint.x(), -fAnchorPoint.y());
+
+ t.postScale(fScale.x() / 100, fScale.y() / 100); // 100% based
+ t.postRotate(fRotation);
+ t.postTranslate(fPosition.x(), fPosition.y());
+ // TODO: skew
+
+ fMatrixNode->setMatrix(t);
+}
+
+PolyStarAdapter::PolyStarAdapter(sk_sp<sksg::Path> wrapped_node, Type t)
+ : fPathNode(std::move(wrapped_node))
+ , fType(t) {}
+
+void PolyStarAdapter::apply() {
+ static constexpr int kMaxPointCount = 100000;
+ const auto count = SkToUInt(SkTPin(SkScalarRoundToInt(fPointCount), 0, kMaxPointCount));
+ const auto arc = sk_ieee_float_divide(SK_ScalarPI * 2, count);
+
+ const auto pt_on_circle = [](const SkPoint& c, SkScalar r, SkScalar a) {
+ return SkPoint::Make(c.x() + r * std::cos(a),
+ c.y() + r * std::sin(a));
+ };
+
+ // TODO: inner/outer "roundness"?
+
+ SkPath poly;
+
+ auto angle = SkDegreesToRadians(fRotation);
+ poly.moveTo(pt_on_circle(fPosition, fOuterRadius, angle));
+ poly.incReserve(fType == Type::kStar ? count * 2 : count);
+
+ for (unsigned i = 0; i < count; ++i) {
+ if (fType == Type::kStar) {
+ poly.lineTo(pt_on_circle(fPosition, fInnerRadius, angle + arc * 0.5f));
+ }
+ angle += arc;
+ poly.lineTo(pt_on_circle(fPosition, fOuterRadius, angle));
+ }
+
+ poly.close();
+ fPathNode->setPath(poly);
+}
+
+GradientAdapter::GradientAdapter(sk_sp<sksg::Gradient> grad, size_t stopCount)
+ : fGradient(std::move(grad))
+ , fStopCount(stopCount) {}
+
+void GradientAdapter::apply() {
+ this->onApply();
+
+ // |fColorStops| holds |fStopCount| x [ pos, r, g, g ] + ? x [ pos, alpha ]
+
+ if (fColorStops.size() < fStopCount * 4 || ((fColorStops.size() - fStopCount * 4) % 2)) {
+ SkDebugf("!! Invalid gradient stop array size: %zu", fColorStops.size());
+ return;
+ }
+
+ std::vector<sksg::Gradient::ColorStop> stops;
+
+ // TODO: merge/lerp opacity stops
+ const auto csEnd = fColorStops.cbegin() + fStopCount * 4;
+ for (auto cs = fColorStops.cbegin(); cs != csEnd; cs += 4) {
+ const auto pos = cs[0];
+ const VectorValue rgb({ cs[1], cs[2], cs[3] });
+
+ stops.push_back({ pos, ValueTraits<VectorValue>::As<SkColor>(rgb) });
+ }
+
+ fGradient->setColorStops(std::move(stops));
+}
+
+LinearGradientAdapter::LinearGradientAdapter(sk_sp<sksg::LinearGradient> grad, size_t stopCount)
+ : INHERITED(std::move(grad), stopCount) {}
+
+void LinearGradientAdapter::onApply() {
+ auto* grad = static_cast<sksg::LinearGradient*>(fGradient.get());
+ grad->setStartPoint(this->startPoint());
+ grad->setEndPoint(this->endPoint());
+}
+
+RadialGradientAdapter::RadialGradientAdapter(sk_sp<sksg::RadialGradient> grad, size_t stopCount)
+ : INHERITED(std::move(grad), stopCount) {}
+
+void RadialGradientAdapter::onApply() {
+ auto* grad = static_cast<sksg::RadialGradient*>(fGradient.get());
+ grad->setStartCenter(this->startPoint());
+ grad->setEndCenter(this->startPoint());
+ grad->setStartRadius(0);
+ grad->setEndRadius(SkPoint::Distance(this->startPoint(), this->endPoint()));
+}
+
+TrimEffectAdapter::TrimEffectAdapter(sk_sp<sksg::TrimEffect> trimEffect)
+ : fTrimEffect(std::move(trimEffect)) {
+ SkASSERT(fTrimEffect);
+}
+
+void TrimEffectAdapter::apply() {
+ // BM semantics: start/end are percentages, offset is "degrees" (?!).
+ const auto start = fStart / 100,
+ end = fEnd / 100,
+ offset = fOffset / 360;
+
+ auto startT = SkTMin(start, end) + offset,
+ stopT = SkTMax(start, end) + offset;
+ auto mode = SkTrimPathEffect::Mode::kNormal;
+
+ if (stopT - startT < 1) {
+ startT -= SkScalarFloorToScalar(startT);
+ stopT -= SkScalarFloorToScalar(stopT);
+
+ if (startT > stopT) {
+ SkTSwap(startT, stopT);
+ mode = SkTrimPathEffect::Mode::kInverted;
+ }
+ } else {
+ startT = 0;
+ stopT = 1;
+ }
+
+ fTrimEffect->setStart(startT);
+ fTrimEffect->setStop(stopT);
+ fTrimEffect->setMode(mode);
+}
+
+} // namespace skottie
diff --git a/modules/skottie/src/SkottieAdapter.h b/modules/skottie/src/SkottieAdapter.h
new file mode 100644
index 0000000000..e96c616fcf
--- /dev/null
+++ b/modules/skottie/src/SkottieAdapter.h
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2018 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef SkottieAdapter_DEFINED
+#define SkottieAdapter_DEFINED
+
+#include "SkPoint.h"
+#include "SkRefCnt.h"
+#include "SkSize.h"
+
+#include <vector>
+
+namespace sksg {
+
+class Gradient;
+class LinearGradient;
+class Matrix;
+class Path;
+class RadialGradient;
+class RRect;
+class TrimEffect;
+
+};
+
+namespace skottie {
+
+#define ADAPTER_PROPERTY(p_name, p_type, p_default) \
+ void set##p_name(const p_type& p) { \
+ if (p == f##p_name) return; \
+ f##p_name = p; \
+ this->apply(); \
+ } \
+ private: \
+ p_type f##p_name = p_default; \
+ public:
+
+class RRectAdapter final : public SkRefCnt {
+public:
+ explicit RRectAdapter(sk_sp<sksg::RRect>);
+
+ ADAPTER_PROPERTY(Position, SkPoint , SkPoint::Make(0, 0))
+ ADAPTER_PROPERTY(Size , SkSize , SkSize::Make(0, 0))
+ ADAPTER_PROPERTY(Radius , SkSize , SkSize::Make(0, 0))
+
+private:
+ void apply();
+
+ sk_sp<sksg::RRect> fRRectNode;
+
+ using INHERITED = SkRefCnt;
+};
+
+class PolyStarAdapter final : public SkRefCnt {
+public:
+ enum class Type {
+ kStar, kPoly,
+ };
+
+ PolyStarAdapter(sk_sp<sksg::Path>, Type);
+
+ ADAPTER_PROPERTY(Position , SkPoint , SkPoint::Make(0, 0))
+ ADAPTER_PROPERTY(PointCount , SkScalar, 0)
+ ADAPTER_PROPERTY(InnerRadius , SkScalar, 0)
+ ADAPTER_PROPERTY(OuterRadius , SkScalar, 0)
+ ADAPTER_PROPERTY(InnerRoundness, SkScalar, 0)
+ ADAPTER_PROPERTY(OuterRoundness, SkScalar, 0)
+ ADAPTER_PROPERTY(Rotation , SkScalar, 0)
+
+private:
+ void apply();
+
+ sk_sp<sksg::Path> fPathNode;
+ Type fType;
+
+ using INHERITED = SkRefCnt;
+};
+
+class TransformAdapter final : public SkRefCnt {
+public:
+ explicit TransformAdapter(sk_sp<sksg::Matrix>);
+
+ ADAPTER_PROPERTY(AnchorPoint, SkPoint , SkPoint::Make(0, 0))
+ ADAPTER_PROPERTY(Position , SkPoint , SkPoint::Make(0, 0))
+ ADAPTER_PROPERTY(Scale , SkVector, SkPoint::Make(100, 100))
+ ADAPTER_PROPERTY(Rotation , SkScalar, 0)
+ ADAPTER_PROPERTY(Skew , SkScalar, 0)
+ ADAPTER_PROPERTY(SkewAxis , SkScalar, 0)
+
+private:
+ void apply();
+
+ sk_sp<sksg::Matrix> fMatrixNode;
+
+ using INHERITED = SkRefCnt;
+};
+
+class GradientAdapter : public SkRefCnt {
+public:
+ ADAPTER_PROPERTY(StartPoint, SkPoint , SkPoint::Make(0, 0) )
+ ADAPTER_PROPERTY(EndPoint , SkPoint , SkPoint::Make(0, 0) )
+ ADAPTER_PROPERTY(ColorStops, std::vector<SkScalar>, std::vector<SkScalar>())
+
+protected:
+ GradientAdapter(sk_sp<sksg::Gradient>, size_t stopCount);
+
+ const SkPoint& startPoint() const { return fStartPoint; }
+ const SkPoint& endPoint() const { return fEndPoint; }
+
+ sk_sp<sksg::Gradient> fGradient;
+ size_t fStopCount;
+
+ virtual void onApply() = 0;
+
+private:
+ void apply();
+
+ using INHERITED = SkRefCnt;
+};
+
+class LinearGradientAdapter final : public GradientAdapter {
+public:
+ LinearGradientAdapter(sk_sp<sksg::LinearGradient>, size_t stopCount);
+
+private:
+ void onApply() override;
+
+ using INHERITED = GradientAdapter;
+};
+
+class RadialGradientAdapter final : public GradientAdapter {
+public:
+ RadialGradientAdapter(sk_sp<sksg::RadialGradient>, size_t stopCount);
+
+private:
+ void onApply() override;
+
+ using INHERITED = GradientAdapter;
+};
+
+class TrimEffectAdapter final : public SkRefCnt {
+public:
+ explicit TrimEffectAdapter(sk_sp<sksg::TrimEffect>);
+
+ ADAPTER_PROPERTY(Start , SkScalar, 0)
+ ADAPTER_PROPERTY(End , SkScalar, 100)
+ ADAPTER_PROPERTY(Offset, SkScalar, 0)
+
+private:
+ void apply();
+
+ sk_sp<sksg::TrimEffect> fTrimEffect;
+
+ using INHERITED = SkRefCnt;
+};
+
+#undef ADAPTER_PROPERTY
+
+} // namespace skottie
+
+#endif // SkottieAdapter_DEFINED
diff --git a/modules/skottie/src/SkottieAnimator.cpp b/modules/skottie/src/SkottieAnimator.cpp
new file mode 100644
index 0000000000..4554409761
--- /dev/null
+++ b/modules/skottie/src/SkottieAnimator.cpp
@@ -0,0 +1,374 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "SkottieAnimator.h"
+
+#include "SkCubicMap.h"
+#include "SkottieJson.h"
+#include "SkottieValue.h"
+#include "SkString.h"
+#include "SkTArray.h"
+
+#include <memory>
+
+namespace skottie {
+
+namespace {
+
+#define LOG SkDebugf
+
+bool LogFail(const json::ValueRef& json, const char* msg) {
+ const auto dump = json.toString();
+ LOG("!! %s: %s\n", msg, dump.c_str());
+ return false;
+}
+
+class KeyframeAnimatorBase : public sksg::Animator {
+public:
+ int count() const { return fRecs.count(); }
+
+protected:
+ KeyframeAnimatorBase() = default;
+
+ struct KeyframeRec {
+ float t0, t1;
+ int vidx0, vidx1, // v0/v1 indices
+ cmidx; // cubic map index
+
+ bool contains(float t) const { return t0 <= t && t <= t1; }
+ bool isConstant() const { return vidx0 == vidx1; }
+ bool isValid() const {
+ SkASSERT(t0 <= t1);
+ // Constant frames don't need/use t1 and vidx1.
+ return t0 < t1 || this->isConstant();
+ }
+ };
+
+ const KeyframeRec& frame(float t) {
+ if (!fCachedRec || !fCachedRec->contains(t)) {
+ fCachedRec = findFrame(t);
+ }
+ return *fCachedRec;
+ }
+
+ float localT(const KeyframeRec& rec, float t) const {
+ SkASSERT(rec.isValid());
+ SkASSERT(!rec.isConstant());
+ SkASSERT(t > rec.t0 && t < rec.t1);
+
+ auto lt = (t - rec.t0) / (rec.t1 - rec.t0);
+
+ return rec.cmidx < 0
+ ? lt
+ : SkTPin(fCubicMaps[rec.cmidx].computeYFromX(lt), 0.0f, 1.0f);
+ }
+
+ virtual int parseValue(const json::ValueRef&) = 0;
+
+ void parseKeyFrames(const json::ValueRef& jframes) {
+ if (!jframes.isArray())
+ return;
+
+ for (const json::ValueRef jframe : jframes) {
+ float t0;
+ if (!jframe["t"].to(&t0))
+ continue;
+
+ if (!fRecs.empty()) {
+ if (fRecs.back().t1 >= t0) {
+ LOG("!! Ignoring out-of-order key frame (t:%f < t:%f)\n", t0, fRecs.back().t1);
+ continue;
+ }
+ // Back-fill t1 in prev interval. Note: we do this even if we end up discarding
+ // the current interval (to support "t"-only final frames).
+ fRecs.back().t1 = t0;
+ }
+
+ const auto vidx0 = this->parseValue(jframe["s"]);
+ if (vidx0 < 0)
+ continue;
+
+ // Defaults for constant frames.
+ int vidx1 = vidx0, cmidx = -1;
+
+ if (!jframe["h"].toDefault(false)) {
+ // Regular frame, requires an end value.
+ vidx1 = this->parseValue(jframe["e"]);
+ if (vidx1 < 0)
+ continue;
+
+ // default is linear lerp
+ static constexpr SkPoint kDefaultC0 = { 0, 0 },
+ kDefaultC1 = { 1, 1 };
+ const auto c0 = jframe["i"].toDefault(kDefaultC0),
+ c1 = jframe["o"].toDefault(kDefaultC1);
+
+ if (c0 != kDefaultC0 || c1 != kDefaultC1) {
+ // TODO: is it worth de-duping these?
+ cmidx = fCubicMaps.count();
+ fCubicMaps.emplace_back();
+ // TODO: why do we have to plug these inverted?
+ fCubicMaps.back().setPts(c1, c0);
+ }
+ }
+
+ fRecs.push_back({t0, t0, vidx0, vidx1, cmidx });
+ }
+
+ // If we couldn't determine a valid t1 for the last frame, discard it.
+ if (!fRecs.empty() && !fRecs.back().isValid()) {
+ fRecs.pop_back();
+ }
+
+ SkASSERT(fRecs.empty() || fRecs.back().isValid());
+ }
+
+private:
+ const KeyframeRec* findFrame(float t) const {
+ SkASSERT(!fRecs.empty());
+
+ auto f0 = &fRecs.front(),
+ f1 = &fRecs.back();
+
+ SkASSERT(f0->isValid());
+ SkASSERT(f1->isValid());
+
+ if (t < f0->t0) {
+ return f0;
+ }
+
+ if (t > f1->t1) {
+ return f1;
+ }
+
+ while (f0 != f1) {
+ SkASSERT(f0 < f1);
+ SkASSERT(t >= f0->t0 && t <= f1->t1);
+
+ const auto f = f0 + (f1 - f0) / 2;
+ SkASSERT(f->isValid());
+
+ if (t > f->t1) {
+ f0 = f + 1;
+ } else {
+ f1 = f;
+ }
+ }
+
+ SkASSERT(f0 == f1);
+ SkASSERT(f0->contains(t));
+
+ return f0;
+ }
+
+ SkTArray<KeyframeRec> fRecs;
+ SkTArray<SkCubicMap> fCubicMaps;
+ const KeyframeRec* fCachedRec = nullptr;
+
+ using INHERITED = sksg::Animator;
+};
+
+template <typename T>
+class KeyframeAnimator final : public KeyframeAnimatorBase {
+public:
+ static std::unique_ptr<KeyframeAnimator> Make(const json::ValueRef& jframes,
+ std::function<void(const T&)>&& apply) {
+ std::unique_ptr<KeyframeAnimator> animator(new KeyframeAnimator(jframes, std::move(apply)));
+ if (!animator->count())
+ return nullptr;
+
+ return animator;
+ }
+
+protected:
+ void onTick(float t) override {
+ T val;
+ this->eval(this->frame(t), t, &val);
+
+ fApplyFunc(val);
+ }
+
+private:
+ KeyframeAnimator(const json::ValueRef& jframes,
+ std::function<void(const T&)>&& apply)
+ : fApplyFunc(std::move(apply)) {
+ this->parseKeyFrames(jframes);
+ }
+
+ int parseValue(const json::ValueRef& jv) override {
+ T val;
+ if (!jv.to(&val) || (!fVs.empty() &&
+ ValueTraits<T>::Cardinality(val) != ValueTraits<T>::Cardinality(fVs.back()))) {
+ return -1;
+ }
+
+ // TODO: full deduping?
+ if (fVs.empty() || val != fVs.back()) {
+ fVs.push_back(std::move(val));
+ }
+ return fVs.count() - 1;
+ }
+
+ void eval(const KeyframeRec& rec, float t, T* v) const {
+ SkASSERT(rec.isValid());
+ if (rec.isConstant() || t <= rec.t0) {
+ *v = fVs[rec.vidx0];
+ } else if (t >= rec.t1) {
+ *v = fVs[rec.vidx1];
+ } else {
+ const auto lt = this->localT(rec, t);
+ const auto& v0 = fVs[rec.vidx0];
+ const auto& v1 = fVs[rec.vidx1];
+ *v = ValueTraits<T>::Lerp(v0, v1, lt);
+ }
+ }
+
+ const std::function<void(const T&)> fApplyFunc;
+ SkTArray<T> fVs;
+
+
+ using INHERITED = KeyframeAnimatorBase;
+};
+
+template <typename T>
+static inline bool BindPropertyImpl(const json::ValueRef& jprop,
+ sksg::AnimatorList* animators,
+ std::function<void(const T&)>&& apply,
+ const T* noop = nullptr) {
+ if (!jprop.isObject())
+ return false;
+
+ const auto jpropA = jprop["a"];
+ const auto jpropK = jprop["k"];
+
+ // Older Json versions don't have an "a" animation marker.
+ // For those, we attempt to parse both ways.
+ if (!jpropA.toDefault(false)) {
+ T val;
+ if (jpropK.to<T>(&val)) {
+ // Static property.
+ if (noop && val == *noop)
+ return false;
+
+ apply(val);
+ return true;
+ }
+
+ if (!jpropA.isNull()) {
+ return LogFail(jprop, "Could not parse (explicit) static property");
+ }
+ }
+
+ // Keyframe property.
+ auto animator = KeyframeAnimator<T>::Make(jpropK, std::move(apply));
+
+ if (!animator) {
+ return LogFail(jprop, "Could not parse keyframed property");
+ }
+
+ animators->push_back(std::move(animator));
+
+ return true;
+}
+
+class SplitPointAnimator final : public sksg::Animator {
+public:
+ static std::unique_ptr<SplitPointAnimator> Make(const json::ValueRef& jprop,
+ std::function<void(const VectorValue&)>&& apply,
+ const VectorValue*) {
+ if (!jprop.isObject())
+ return nullptr;
+
+ std::unique_ptr<SplitPointAnimator> split_animator(
+ new SplitPointAnimator(std::move(apply)));
+
+ // This raw pointer is captured in lambdas below. But the lambdas are owned by
+ // the object itself, so the scope is bound to the life time of the object.
+ auto* split_animator_ptr = split_animator.get();
+
+ if (!BindPropertyImpl<ScalarValue>(jprop["x"], &split_animator->fAnimators,
+ [split_animator_ptr](const ScalarValue& x) { split_animator_ptr->setX(x); }) ||
+ !BindPropertyImpl<ScalarValue>(jprop["y"], &split_animator->fAnimators,
+ [split_animator_ptr](const ScalarValue& y) { split_animator_ptr->setY(y); })) {
+ LogFail(jprop, "Could not parse split property");
+ return nullptr;
+ }
+
+ if (split_animator->fAnimators.empty()) {
+ // Static split property, no need to hold on to the split animator.
+ return nullptr;
+ }
+
+ return split_animator;
+ }
+
+ void onTick(float t) override {
+ for (const auto& animator : fAnimators) {
+ animator->tick(t);
+ }
+
+ const VectorValue vec = { fX, fY };
+ fApplyFunc(vec);
+ }
+
+ void setX(const ScalarValue& x) { fX = x; }
+ void setY(const ScalarValue& y) { fY = y; }
+
+private:
+ explicit SplitPointAnimator(std::function<void(const VectorValue&)>&& apply)
+ : fApplyFunc(std::move(apply)) {}
+
+ const std::function<void(const VectorValue&)> fApplyFunc;
+ sksg::AnimatorList fAnimators;
+
+ ScalarValue fX = 0,
+ fY = 0;
+
+ using INHERITED = sksg::Animator;
+};
+
+bool BindSplitPositionProperty(const json::ValueRef& jprop,
+ sksg::AnimatorList* animators,
+ std::function<void(const VectorValue&)>&& apply,
+ const VectorValue* noop) {
+ if (auto split_animator = SplitPointAnimator::Make(jprop, std::move(apply), noop)) {
+ animators->push_back(std::unique_ptr<sksg::Animator>(split_animator.release()));
+ return true;
+ }
+
+ return false;
+}
+
+} // namespace
+
+template <>
+bool BindProperty(const json::ValueRef& jprop,
+ sksg::AnimatorList* animators,
+ std::function<void(const ScalarValue&)>&& apply,
+ const ScalarValue* noop) {
+ return BindPropertyImpl(jprop, animators, std::move(apply), noop);
+}
+
+template <>
+bool BindProperty(const json::ValueRef& jprop,
+ sksg::AnimatorList* animators,
+ std::function<void(const VectorValue&)>&& apply,
+ const VectorValue* noop) {
+ return jprop["s"].toDefault<bool>(false)
+ ? BindSplitPositionProperty(jprop, animators, std::move(apply), noop)
+ : BindPropertyImpl(jprop, animators, std::move(apply), noop);
+}
+
+template <>
+bool BindProperty(const json::ValueRef& jprop,
+ sksg::AnimatorList* animators,
+ std::function<void(const ShapeValue&)>&& apply,
+ const ShapeValue* noop) {
+ return BindPropertyImpl(jprop, animators, std::move(apply), noop);
+}
+
+} // namespace skottie
diff --git a/modules/skottie/src/SkottieAnimator.h b/modules/skottie/src/SkottieAnimator.h
new file mode 100644
index 0000000000..6dc8f6c759
--- /dev/null
+++ b/modules/skottie/src/SkottieAnimator.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef SkottieAnimator_DEFINED
+#define SkottieAnimator_DEFINED
+
+#include "SkSGScene.h"
+
+#include <functional>
+
+namespace skottie {
+
+namespace json { class ValueRef; }
+
+// This is the workhorse for property binding: depending on whether the property is animated,
+// it will either apply immediately or instantiate and attach a keyframe animator.
+template <typename T>
+bool BindProperty(const json::ValueRef&,
+ sksg::AnimatorList*,
+ std::function<void(const T&)>&&,
+ const T* noop = nullptr);
+
+} // namespace skottie
+
+#endif // SkottieAnimator_DEFINED
diff --git a/modules/skottie/src/SkottieJson.cpp b/modules/skottie/src/SkottieJson.cpp
new file mode 100644
index 0000000000..23e616d1ea
--- /dev/null
+++ b/modules/skottie/src/SkottieJson.cpp
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2018 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "SkottieJson.h"
+
+#include "SkData.h"
+#include "SkScalar.h"
+#include "SkPath.h"
+#include "SkPoint.h"
+#include "SkStream.h"
+#include "SkString.h"
+#include "SkottieValue.h"
+
+#include "rapidjson/error/en.h"
+#include "rapidjson/prettywriter.h"
+#include "rapidjson/stringbuffer.h"
+
+#include <vector>
+
+namespace skottie {
+
+namespace json {
+
+template <>
+bool ValueRef::to<SkScalar>(SkScalar* v) const {
+ if (!fValue) return false;
+
+ // Some versions wrap values as single-element arrays.
+ if (fValue->IsArray() && fValue->Size() == 1) {
+ return ValueRef(fValue->operator[](0)).to(v);
+ }
+
+ if (!fValue->IsNumber())
+ return false;
+
+ *v = static_cast<SkScalar>(fValue->GetDouble());
+
+ return true;
+}
+
+template <>
+bool ValueRef::to<bool>(bool* v) const {
+ if (!fValue) return false;
+
+ switch(fValue->GetType()) {
+ case rapidjson::kNumberType:
+ *v = SkToBool(fValue->GetDouble());
+ return true;
+ case rapidjson::kFalseType:
+ case rapidjson::kTrueType:
+ *v = fValue->GetBool();
+ return true;
+ default:
+ break;
+ }
+
+ return false;
+}
+
+template <>
+bool ValueRef::to<int>(int* v) const {
+ if (!fValue || !fValue->IsInt())
+ return false;
+
+ *v = fValue->GetInt();
+
+ return true;
+}
+
+template <>
+bool ValueRef::to<SkString>(SkString* v) const {
+ if (!fValue || !fValue->IsString())
+ return false;
+
+ v->set(fValue->GetString());
+
+ return true;
+}
+
+template <>
+bool ValueRef::to<SkPoint>(SkPoint* v) const {
+ if (!fValue || !fValue->IsObject())
+ return false;
+
+ const auto jvx = ValueRef(this->operator[]("x")),
+ jvy = ValueRef(this->operator[]("y"));
+
+ // Some BM versions seem to store x/y as single-element arrays.
+ return ValueRef(jvx.isArray() ? jvx.operator[](size_t(0)) : jvx).to(&v->fX)
+ && ValueRef(jvy.isArray() ? jvy.operator[](size_t(0)) : jvy).to(&v->fY);
+}
+
+template <>
+bool ValueRef::to<std::vector<float>>(std::vector<float>* v) const {
+ if (!fValue || !fValue->IsArray())
+ return false;
+
+ v->resize(fValue->Size());
+ for (size_t i = 0; i < fValue->Size(); ++i) {
+ if (!ValueRef(fValue->operator[](i)).to(v->data() + i)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+namespace {
+
+bool ParsePointVec(const ValueRef& jv, std::vector<SkPoint>* pts) {
+ if (!jv.isArray())
+ return false;
+
+ pts->clear();
+ pts->reserve(jv.size());
+
+ std::vector<float> vec;
+ for (size_t i = 0; i < jv.size(); ++i) {
+ if (!jv[i].to(&vec) || vec.size() != 2)
+ return false;
+ pts->push_back(SkPoint::Make(vec[0], vec[1]));
+ }
+
+ return true;
+}
+
+} // namespace
+
+template <>
+bool ValueRef::to<ShapeValue>(ShapeValue* v) const {
+ SkASSERT(v->fVertices.empty());
+
+ if (!fValue)
+ return false;
+
+ // Some versions wrap values as single-element arrays.
+ if (fValue->IsArray() && fValue->Size() == 1) {
+ return ValueRef(fValue->operator[](0)).to(v);
+ }
+
+ std::vector<SkPoint> inPts, // Cubic Bezier "in" control points, relative to vertices.
+ outPts, // Cubic Bezier "out" control points, relative to vertices.
+ verts; // Cubic Bezier vertices.
+
+ if (!fValue->IsObject() ||
+ !ParsePointVec(this->operator[]("i"), &inPts) ||
+ !ParsePointVec(this->operator[]("o"), &outPts) ||
+ !ParsePointVec(this->operator[]("v"), &verts) ||
+ inPts.size() != outPts.size() ||
+ inPts.size() != verts.size()) {
+
+ return false;
+ }
+
+ v->fVertices.reserve(inPts.size());
+ for (size_t i = 0; i < inPts.size(); ++i) {
+ v->fVertices.push_back(BezierVertex({inPts[i], outPts[i], verts[i]}));
+ }
+ v->fClosed = this->operator[]("c").toDefault<bool>(false);
+
+ return true;
+}
+
+size_t ValueRef::size() const {
+ return this->isArray() ? fValue->Size() : 0;
+}
+
+ValueRef ValueRef::operator[](size_t i) const {
+ return i < this->size() ? ValueRef(fValue->operator[](i)) : ValueRef();
+}
+
+ValueRef ValueRef::operator[](const char* key) const {
+ if (!this->isObject())
+ return ValueRef();
+
+ const auto m = fValue->FindMember(key);
+ return m == fValue->MemberEnd() ? ValueRef() : ValueRef(m->value);
+}
+
+const rapidjson::Value* ValueRef::begin() const {
+ return this->isArray() ? fValue->Begin() : nullptr;
+}
+
+const rapidjson::Value* ValueRef::end() const {
+ return this->isArray() ? fValue->End() : nullptr;
+}
+
+SkString ValueRef::toString() const {
+#ifdef SK_DEBUG
+ rapidjson::StringBuffer buf;
+ if (fValue) {
+ rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(buf);
+ fValue->Accept(writer);
+ }
+
+ return SkString(buf.GetString());
+#else
+ return SkString();
+#endif // SK_DEBUG
+}
+
+Document::Document(SkStream* stream) {
+ if (!stream->hasLength()) {
+ SkDebugf("!! unsupported unseekable json stream\n");
+ return;
+ }
+
+ // RapidJSON provides three DOM-builder approaches:
+ //
+ // 1) in-place : all data buffered, constructs the DOM in-place -- this is the fastest
+ // 2) from buffer: all data buffered, copies to DOM -- this is slightly slower
+ // 3) from stream: streamed data, reads/copies to DOM -- this is *significantly* slower
+ //
+ // We like fast, so #1 it is.
+
+ // The buffer needs to be C-string.
+ const auto size = stream->getLength();
+ fData = SkData::MakeUninitialized(size + 1);
+ if (stream->read(fData->writable_data(), size) < size) {
+ SkDebugf("!! could not read JSON stream\n");
+ return;
+ }
+
+ auto data = static_cast<char*>(fData->writable_data());
+ data[size] = '\0';
+
+ fDocument.ParseInsitu(data);
+
+#ifdef SK_DEBUG
+ if (fDocument.HasParseError()) {
+ SkDebugf("!! failed to parse json: %s\n",
+ rapidjson::GetParseError_En(fDocument.GetParseError()));
+ }
+#endif
+}
+
+} // namespace json
+
+} // namespace skottie
diff --git a/modules/skottie/src/SkottieJson.h b/modules/skottie/src/SkottieJson.h
new file mode 100644
index 0000000000..76e17c610e
--- /dev/null
+++ b/modules/skottie/src/SkottieJson.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2018 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef SkottieJson_DEFINED
+#define SkottieJson_DEFINED
+
+#include "SkRefCnt.h"
+
+#include "rapidjson/document.h"
+
+class SkData;
+class SkStream;
+class SkString;
+
+namespace skottie {
+
+namespace json {
+
+class ValueRef {
+public:
+ ValueRef() : fValue(nullptr) {}
+ ValueRef(const rapidjson::Value& v) : fValue(v.IsNull() ? nullptr : &v) {}
+
+ bool isNull() const { return !fValue; }
+ bool isObject() const { return fValue && fValue->IsObject(); }
+ bool isArray() const { return fValue && fValue->IsArray(); }
+
+ template <typename T>
+ bool to(T*) const;
+
+ template <typename T>
+ T toDefault(const T& defaultValue) const {
+ T v;
+ if (!this->to<T>(&v)) {
+ v = defaultValue;
+ }
+ return v;
+ }
+
+ size_t size() const;
+ ValueRef operator[](size_t i) const;
+ ValueRef operator[](const char* key) const;
+
+ bool operator==(const ValueRef& other) const { return fValue == other.fValue; }
+ bool operator!=(const ValueRef& other) const { return !(*this == other); }
+
+ const rapidjson::Value* begin() const;
+ const rapidjson::Value* end() const;
+
+ SkString toString() const;
+
+private:
+ const rapidjson::Value* fValue;
+};
+
+// Container for the json DOM
+class Document {
+public:
+ explicit Document(SkStream*);
+
+ ValueRef root() const { return fDocument; }
+
+private:
+ sk_sp<SkData> fData; // raw data
+ rapidjson::Document fDocument; // in-place json DOM
+};
+
+} // namespace json
+
+} // namespace skottie
+
+#endif // SkottieJson_DEFINED
diff --git a/modules/skottie/src/SkottieValue.cpp b/modules/skottie/src/SkottieValue.cpp
new file mode 100644
index 0000000000..edfa891aa1
--- /dev/null
+++ b/modules/skottie/src/SkottieValue.cpp
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "SkottieValue.h"
+
+#include "SkColor.h"
+#include "SkNx.h"
+#include "SkPoint.h"
+#include "SkSize.h"
+
+namespace skottie {
+
+template <>
+size_t ValueTraits<ScalarValue>::Cardinality(const ScalarValue&) {
+ return 1;
+}
+
+template <>
+ScalarValue ValueTraits<ScalarValue>::Lerp(const ScalarValue& v0, const ScalarValue& v1, float t) {
+ SkASSERT(t >= 0 && t <= 1);
+ return v0 + (v1 - v0) * t;
+}
+
+template <>
+template <>
+SkScalar ValueTraits<ScalarValue>::As<SkScalar>(const ScalarValue& v) {
+ return v;
+}
+
+template <>
+size_t ValueTraits<VectorValue>::Cardinality(const VectorValue& vec) {
+ return vec.size();
+}
+
+template <>
+VectorValue ValueTraits<VectorValue>::Lerp(const VectorValue& v0, const VectorValue& v1, float t) {
+ SkASSERT(v0.size() == v1.size());
+
+ VectorValue v;
+ v.reserve(v0.size());
+
+ for (size_t i = 0; i < v0.size(); ++i) {
+ v.push_back(ValueTraits<ScalarValue>::Lerp(v0[i], v1[i], t));
+ }
+
+ return v;
+}
+
+template <>
+template <>
+SkColor ValueTraits<VectorValue>::As<SkColor>(const VectorValue& v) {
+ // best effort to turn this into a color
+ const auto r = v.size() > 0 ? v[0] : 0,
+ g = v.size() > 1 ? v[1] : 0,
+ b = v.size() > 2 ? v[2] : 0,
+ a = v.size() > 3 ? v[3] : 1;
+
+ return SkColorSetARGB(SkTPin<SkScalar>(a, 0, 1) * 255,
+ SkTPin<SkScalar>(r, 0, 1) * 255,
+ SkTPin<SkScalar>(g, 0, 1) * 255,
+ SkTPin<SkScalar>(b, 0, 1) * 255);
+}
+
+template <>
+template <>
+SkPoint ValueTraits<VectorValue>::As<SkPoint>(const VectorValue& vec) {
+ // best effort to turn this into a point
+ const auto x = vec.size() > 0 ? vec[0] : 0,
+ y = vec.size() > 1 ? vec[1] : 0;
+ return SkPoint::Make(x, y);
+}
+
+template <>
+template <>
+SkSize ValueTraits<VectorValue>::As<SkSize>(const VectorValue& vec) {
+ const auto pt = ValueTraits::As<SkPoint>(vec);
+ return SkSize::Make(pt.x(), pt.y());
+}
+
+template <>
+size_t ValueTraits<ShapeValue>::Cardinality(const ShapeValue& shape) {
+ return shape.fVertices.size();
+}
+
+static SkPoint lerp_point(const SkPoint& v0, const SkPoint& v1, const Sk2f& t) {
+ const auto v2f0 = Sk2f::Load(&v0),
+ v2f1 = Sk2f::Load(&v1);
+
+ SkPoint v;
+ (v2f0 + (v2f1 - v2f0) * t).store(&v);
+
+ return v;
+}
+
+template <>
+ShapeValue ValueTraits<ShapeValue>::Lerp(const ShapeValue& v0, const ShapeValue& v1, float t) {
+ SkASSERT(t >= 0 && t <= 1);
+ SkASSERT(v0.fVertices.size() == v1.fVertices.size());
+ SkASSERT(v0.fClosed == v1.fClosed);
+
+ ShapeValue v;
+ v.fClosed = v0.fClosed;
+ v.fVolatile = true; // interpolated values are volatile
+
+ const auto t2f = Sk2f(t);
+ v.fVertices.reserve(v0.fVertices.size());
+
+ for (size_t i = 0; i < v0.fVertices.size(); ++i) {
+ v.fVertices.emplace_back(BezierVertex({
+ lerp_point(v0.fVertices[i].fInPoint , v1.fVertices[i].fInPoint , t2f),
+ lerp_point(v0.fVertices[i].fOutPoint, v1.fVertices[i].fOutPoint, t2f),
+ lerp_point(v0.fVertices[i].fVertex , v1.fVertices[i].fVertex , t2f)
+ }));
+ }
+
+ return v;
+}
+
+template <>
+template <>
+SkPath ValueTraits<ShapeValue>::As<SkPath>(const ShapeValue& shape) {
+ SkPath path;
+
+ if (!shape.fVertices.empty()) {
+ path.moveTo(shape.fVertices.front().fVertex);
+ }
+
+ const auto& addCubic = [&](size_t from, size_t to) {
+ const auto c0 = shape.fVertices[from].fVertex + shape.fVertices[from].fOutPoint,
+ c1 = shape.fVertices[to].fVertex + shape.fVertices[to].fInPoint;
+
+ if (c0 == shape.fVertices[from].fVertex &&
+ c1 == shape.fVertices[to].fVertex) {
+ // If the control points are coincident, we can power-reduce to a straight line.
+ // TODO: we could also do that when the controls are on the same line as the
+ // vertices, but it's unclear how common that case is.
+ path.lineTo(shape.fVertices[to].fVertex);
+ } else {
+ path.cubicTo(c0, c1, shape.fVertices[to].fVertex);
+ }
+ };
+
+ for (size_t i = 1; i < shape.fVertices.size(); ++i) {
+ addCubic(i - 1, i);
+ }
+
+ if (!shape.fVertices.empty() && shape.fClosed) {
+ addCubic(shape.fVertices.size() - 1, 0);
+ path.close();
+ }
+
+ path.setIsVolatile(shape.fVolatile);
+
+ return path;
+}
+
+} // namespace skottie
diff --git a/modules/skottie/src/SkottieValue.h b/modules/skottie/src/SkottieValue.h
new file mode 100644
index 0000000000..cfdbd7aba7
--- /dev/null
+++ b/modules/skottie/src/SkottieValue.h
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef SkottieValue_DEFINED
+#define SkottieValue_DEFINED
+
+#include "SkPath.h"
+#include "SkScalar.h"
+
+#include <vector>
+
+namespace skottie {
+
+template <typename T>
+struct ValueTraits {
+ static size_t Cardinality(const T&);
+
+ template <typename U>
+ static U As(const T&);
+
+ static T Lerp(const T&, const T&, float);
+};
+
+using ScalarValue = SkScalar;
+using VectorValue = std::vector<ScalarValue>;
+
+struct BezierVertex {
+ SkPoint fInPoint, // "in" control point, relative to the vertex
+ fOutPoint, // "out" control point, relative to the vertex
+ fVertex;
+
+ bool operator==(const BezierVertex& other) const {
+ return fInPoint == other.fInPoint
+ && fOutPoint == other.fOutPoint
+ && fVertex == other.fVertex;
+ }
+
+ bool operator!=(const BezierVertex& other) const { return !(*this == other); }
+};
+
+struct ShapeValue {
+ std::vector<BezierVertex> fVertices;
+ bool fClosed : 1,
+ fVolatile : 1;
+
+ ShapeValue() : fClosed(false), fVolatile(false) {}
+ ShapeValue(const ShapeValue&) = default;
+ ShapeValue(ShapeValue&&) = default;
+ ShapeValue& operator=(const ShapeValue&) = default;
+
+ bool operator==(const ShapeValue& other) const {
+ return fVertices == other.fVertices && fClosed == other.fClosed;
+ }
+
+ bool operator!=(const ShapeValue& other) const { return !(*this == other); }
+};
+
+} // namespace skottie
+
+#endif // SkottieValue_DEFINED