From 094ccde2380bfbb615e25d0d80208148fcd47f17 Mon Sep 17 00:00:00 2001 From: Florin Malita Date: Sat, 30 Dec 2017 12:27:00 -0500 Subject: Initial Lottie loader impl (Skotty) Coarse workflow: * Construction 1) build a Json tree 2) collect asset IDs (for preComp/image layer resolution) 3) "attach" pass - traverse the Json tree - build an SkSG dom, one fragment at a time - attach "animator" objects to the dom, for each animated prop 4) done, we can throw away the Json tree * For each animation tick 1) iterate over active animators and poke their respective dom nodes/attributes 2) revalidate the SkSG dom 3) draw the SkSG dom Note: post construction, things are super-simple - we just poke SkSG DOM attributes with interpolated values, and everything else is handled by SkSG (invalidation, revalidation, render). Change-Id: I96a02be7eb4fb4cb3831f59bf2b3908ea190c0dd Reviewed-on: https://skia-review.googlesource.com/89420 Reviewed-by: Mike Reed Commit-Queue: Florin Malita --- BUILD.gn | 17 ++ experimental/skotty/Skotty.cpp | 503 +++++++++++++++++++++++++++++++ experimental/skotty/Skotty.h | 66 ++++ experimental/skotty/SkottyAnimator.cpp | 59 ++++ experimental/skotty/SkottyAnimator.h | 169 +++++++++++ experimental/skotty/SkottyPriv.h | 41 +++ experimental/skotty/SkottyProperties.cpp | 157 ++++++++++ experimental/skotty/SkottyProperties.h | 128 ++++++++ resources/skotty/skotty_sample_1.json | 112 +++++++ tools/flags/SkCommandLineFlags.h | 3 + tools/viewer/SkottySlide.cpp | 75 +++++ tools/viewer/SkottySlide.h | 39 +++ tools/viewer/Viewer.cpp | 16 + 13 files changed, 1385 insertions(+) create mode 100644 experimental/skotty/Skotty.cpp create mode 100644 experimental/skotty/Skotty.h create mode 100644 experimental/skotty/SkottyAnimator.cpp create mode 100644 experimental/skotty/SkottyAnimator.h create mode 100644 experimental/skotty/SkottyPriv.h create mode 100644 experimental/skotty/SkottyProperties.cpp create mode 100644 experimental/skotty/SkottyProperties.h create mode 100644 resources/skotty/skotty_sample_1.json create mode 100644 tools/viewer/SkottySlide.cpp create mode 100644 tools/viewer/SkottySlide.h diff --git a/BUILD.gn b/BUILD.gn index 31537cf744..a61bff7ea7 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -1320,6 +1320,21 @@ if (skia_enable_tools) { ] } + test_lib("experimental_skotty") { + public_include_dirs = [ "experimental/skotty" ] + include_dirs = [ "tools" ] + sources = [ + "experimental/skotty/Skotty.cpp", + "experimental/skotty/SkottyAnimator.cpp", + "experimental/skotty/SkottyProperties.cpp", + ] + deps = [ + ":experimental_sksg", + ":skia", + "//third_party/jsoncpp", + ] + } + test_lib("experimental_svg_model") { public_include_dirs = [ "experimental/svg/model" ] sources = [ @@ -1877,6 +1892,7 @@ if (skia_enable_tools) { "tools/viewer/ImageSlide.cpp", "tools/viewer/SKPSlide.cpp", "tools/viewer/SampleSlide.cpp", + "tools/viewer/SkottySlide.cpp", "tools/viewer/StatsLayer.cpp", "tools/viewer/Viewer.cpp", ] @@ -1884,6 +1900,7 @@ if (skia_enable_tools) { include_dirs = [] deps = [ + ":experimental_skotty", ":flags", ":gm", ":gpu_tool_utils", diff --git a/experimental/skotty/Skotty.cpp b/experimental/skotty/Skotty.cpp new file mode 100644 index 0000000000..f9f574172f --- /dev/null +++ b/experimental/skotty/Skotty.cpp @@ -0,0 +1,503 @@ +/* + * 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 "Skotty.h" + +#include "SkCanvas.h" +#include "SkottyAnimator.h" +#include "SkottyPriv.h" +#include "SkottyProperties.h" +#include "SkData.h" +#include "SkMakeUnique.h" +#include "SkPaint.h" +#include "SkPath.h" +#include "SkPoint.h" +#include "SkSGColor.h" +#include "SkSGDraw.h" +#include "SkSGInvalidationController.h" +#include "SkSGGroup.h" +#include "SkSGPath.h" +#include "SkSGTransform.h" +#include "SkStream.h" +#include "SkTArray.h" +#include "SkTHash.h" + +#include +#include "stdlib.h" + +namespace skotty { + +namespace { + +using AssetMap = SkTHashMap; + +struct AttachContext { + const AssetMap& fAssets; + SkTArray>& fAnimators; +}; + +bool LogFail(const Json::Value& json, const char* msg) { + const auto dump = json.toStyledString(); + LOG("!! %s: %s", msg, dump.c_str()); + return false; +} + +// This is the workhorse for binding properties: depending on whether the property is animated, +// it will either apply immediately or instantiate and attach a keyframe animator. +template +bool AttachProperty(const Json::Value& jprop, AttachContext* ctx, const sk_sp& node, + ApplyFuncT&& apply) { + if (!jprop.isObject()) + return false; + + if (!ParseBool(jprop["a"], false)) { + // Static property. + ValueT val; + if (!ValueT::Parse(jprop["k"], &val)) { + return LogFail(jprop, "Could not parse static property"); + } + + apply(node, val.template as()); + } else { + // Keyframe property. + using AnimatorT = Animator; + auto animator = AnimatorT::Make(jprop["k"], node, std::move(apply)); + + if (!animator) { + return LogFail(jprop, "Could not instantiate keyframe animator"); + } + + ctx->fAnimators.push_back(std::move(animator)); + } + + return true; +} + +sk_sp AttachTransform(const Json::Value& t, AttachContext* ctx, + sk_sp wrapped_node) { + if (!t.isObject()) + return wrapped_node; + + auto xform = sk_make_sp(wrapped_node); + auto anchor_attached = AttachProperty(t["a"], ctx, xform, + [](const sk_sp& node, const SkPoint& a) { + node->setAnchorPoint(a); + }); + auto position_attached = AttachProperty(t["p"], ctx, xform, + [](const sk_sp& node, const SkPoint& p) { + node->setPosition(p); + }); + auto scale_attached = AttachProperty(t["s"], ctx, xform, + [](const sk_sp& node, const SkVector& s) { + node->setScale(s); + }); + auto rotation_attached = AttachProperty(t["r"], ctx, xform, + [](const sk_sp& node, SkScalar r) { + node->setRotation(r); + }); + auto skew_attached = AttachProperty(t["sk"], ctx, xform, + [](const sk_sp& node, SkScalar sk) { + node->setSkew(sk); + }); + auto skewaxis_attached = AttachProperty(t["sa"], ctx, xform, + [](const sk_sp& node, SkScalar sa) { + node->setSkewAxis(sa); + }); + + if (!anchor_attached && + !position_attached && + !scale_attached && + !rotation_attached && + !skew_attached && + !skewaxis_attached) { + LogFail(t, "Could not parse transform"); + return wrapped_node; + } + + return xform->node(); +} + +sk_sp AttachShape(const Json::Value&, AttachContext* ctx); +sk_sp AttachComposition(const Json::Value&, AttachContext* ctx); + +sk_sp AttachShapeGroup(const Json::Value& jgroup, AttachContext* ctx) { + SkASSERT(jgroup.isObject()); + + return AttachShape(jgroup["it"], ctx); +} + +sk_sp AttachPathGeometry(const Json::Value& jpath, AttachContext* ctx) { + SkASSERT(jpath.isObject()); + + auto path_node = sksg::Path::Make(); + auto path_attached = AttachProperty(jpath["ks"], ctx, path_node, + [](const sk_sp& node, const SkPath& p) { node->setPath(p); }); + + if (path_attached) + LOG("** Attached path geometry - verbs: %d\n", path_node->getPath().countVerbs()); + + return path_attached ? path_node : nullptr; +} + +sk_sp AttachColorPaint(const Json::Value& obj, AttachContext* ctx) { + SkASSERT(obj.isObject()); + + auto color_node = sksg::Color::Make(SK_ColorBLACK); + color_node->setAntiAlias(true); + + auto color_attached = AttachProperty(obj["c"], ctx, color_node, + [](const sk_sp& node, SkColor c) { node->setColor(c); }); + + return color_attached ? color_node : nullptr; +} + +sk_sp AttachFillPaint(const Json::Value& jfill, AttachContext* ctx) { + SkASSERT(jfill.isObject()); + + auto color = AttachColorPaint(jfill, ctx); + if (color) { + LOG("** Attached color fill: 0x%x\n", color->getColor()); + } + return color; +} + +sk_sp AttachStrokePaint(const Json::Value& jstroke, AttachContext* ctx) { + SkASSERT(jstroke.isObject()); + + auto stroke_node = AttachColorPaint(jstroke, ctx); + if (!stroke_node) + return nullptr; + + LOG("** Attached color stroke: 0x%x\n", stroke_node->getColor()); + + stroke_node->setStyle(SkPaint::kStroke_Style); + + auto width_attached = AttachProperty(jstroke["w"], ctx, stroke_node, + [](const sk_sp& node, SkScalar width) { node->setStrokeWidth(width); }); + if (!width_attached) + return nullptr; + + stroke_node->setStrokeMiter(ParseScalar(jstroke["ml"], 4)); + + static constexpr SkPaint::Join gJoins[] = { + SkPaint::kMiter_Join, + SkPaint::kRound_Join, + SkPaint::kBevel_Join, + }; + stroke_node->setStrokeJoin(gJoins[SkTPin(ParseInt(jstroke["lj"], 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(ParseInt(jstroke["lc"], 1) - 1, + 0, SK_ARRAY_COUNT(gCaps) - 1)]); + + return stroke_node; +} + +using GeometryAttacherT = sk_sp (*)(const Json::Value&, AttachContext*); +static constexpr GeometryAttacherT gGeometryAttachers[] = { + AttachPathGeometry, +}; + +using PaintAttacherT = sk_sp (*)(const Json::Value&, AttachContext*); +static constexpr PaintAttacherT gPaintAttachers[] = { + AttachFillPaint, + AttachStrokePaint, +}; + +using GroupAttacherT = sk_sp (*)(const Json::Value&, AttachContext*); +static constexpr GroupAttacherT gGroupAttachers[] = { + AttachShapeGroup, +}; + +enum class ShapeType { + kGeometry, + kPaint, + kGroup, +}; + +struct ShapeInfo { + const char* fTypeString; + ShapeType fShapeType; + uint32_t fAttacherIndex; // index into respective attacher tables +}; + +const ShapeInfo* FindShapeInfo(const Json::Value& shape) { + static constexpr ShapeInfo gShapeInfo[] = { + { "fl", ShapeType::kPaint , 0 }, // fill -> AttachFillPaint + { "gr", ShapeType::kGroup , 0 }, // group -> AttachShapeGroup + { "sh", ShapeType::kGeometry, 0 }, // shape -> AttachPathGeometry + { "st", ShapeType::kPaint , 1 }, // stroke -> AttachStrokePaint + }; + + if (!shape.isObject()) + return nullptr; + + const auto& type = shape["ty"]; + if (!type.isString()) + return nullptr; + + const auto* info = bsearch(type.asCString(), + gShapeInfo, + SK_ARRAY_COUNT(gShapeInfo), + sizeof(ShapeInfo), + [](const void* key, const void* info) { + return strcmp(static_cast(key), + static_cast(info)->fTypeString); + }); + + return static_cast(info); +} + +sk_sp AttachShape(const Json::Value& shapeArray, AttachContext* ctx) { + if (!shapeArray.isArray()) + return nullptr; + + sk_sp shape_group = sksg::Group::Make(); + + SkSTArray<16, sk_sp, true> geos; + SkSTArray<16, sk_sp , true> paints; + + for (const auto& s : shapeArray) { + const auto* info = FindShapeInfo(s); + if (!info) { + LogFail(s.isObject() ? s["ty"] : s, "Unknown shape"); + continue; + } + + switch (info->fShapeType) { + case ShapeType::kGeometry: { + SkASSERT(info->fAttacherIndex < SK_ARRAY_COUNT(gGeometryAttachers)); + if (auto geo = gGeometryAttachers[info->fAttacherIndex](s, ctx)) { + geos.push_back(std::move(geo)); + } + } break; + case ShapeType::kPaint: { + SkASSERT(info->fAttacherIndex < SK_ARRAY_COUNT(gPaintAttachers)); + if (auto paint = gPaintAttachers[info->fAttacherIndex](s, ctx)) { + paints.push_back(std::move(paint)); + } + } break; + case ShapeType::kGroup: { + SkASSERT(info->fAttacherIndex < SK_ARRAY_COUNT(gGroupAttachers)); + if (auto group = gGroupAttachers[info->fAttacherIndex](s, ctx)) { + shape_group->addChild(std::move(group)); + } + } break; + } + } + + for (const auto& geo : geos) { + for (int i = paints.count() - 1; i >= 0; --i) { + shape_group->addChild(sksg::Draw::Make(geo, paints[i])); + } + } + + LOG("** Attached shape - geometries: %d, paints: %d\n", geos.count(), paints.count()); + return shape_group; +} + +sk_sp AttachCompLayer(const Json::Value& layer, AttachContext* ctx) { + SkASSERT(layer.isObject()); + + auto refId = ParseString(layer["refId"], ""); + if (refId.isEmpty()) { + LOG("!! Comp layer missing refId\n"); + return nullptr; + } + + const auto* comp = ctx->fAssets.find(refId); + if (!comp) { + LOG("!! Pre-comp not found: '%s'\n", refId.c_str()); + return nullptr; + } + + // TODO: cycle detection + return AttachComposition(**comp, ctx); +} + +sk_sp AttachSolidLayer(const Json::Value& layer, AttachContext*) { + SkASSERT(layer.isObject()); + + LOG("?? Solid layer stub\n"); + return nullptr; +} + +sk_sp AttachImageLayer(const Json::Value& layer, AttachContext*) { + SkASSERT(layer.isObject()); + + LOG("?? Image layer stub\n"); + return nullptr; +} + +sk_sp AttachNullLayer(const Json::Value& layer, AttachContext*) { + SkASSERT(layer.isObject()); + + LOG("?? Null layer stub\n"); + return nullptr; +} + +sk_sp AttachShapeLayer(const Json::Value& layer, AttachContext* ctx) { + SkASSERT(layer.isObject()); + + LOG("** Attaching shape layer ind: %d\n", ParseInt(layer["ind"], 0)); + + return AttachShape(layer["shapes"], ctx); +} + +sk_sp AttachTextLayer(const Json::Value& layer, AttachContext*) { + SkASSERT(layer.isObject()); + + LOG("?? Text layer stub\n"); + return nullptr; +} + +sk_sp AttachLayer(const Json::Value& layer, AttachContext* ctx) { + if (!layer.isObject()) + return nullptr; + + using LayerAttacher = sk_sp (*)(const Json::Value&, AttachContext*); + static constexpr LayerAttacher gLayerAttachers[] = { + AttachCompLayer, // 'ty': 0 + AttachSolidLayer, // 'ty': 1 + AttachImageLayer, // 'ty': 2 + AttachNullLayer, // 'ty': 3 + AttachShapeLayer, // 'ty': 4 + AttachTextLayer, // 'ty': 5 + }; + + int type = ParseInt(layer["ty"], -1); + if (type < 0 || type >= SkTo(SK_ARRAY_COUNT(gLayerAttachers))) { + return nullptr; + } + + return AttachTransform(layer["ks"], ctx, gLayerAttachers[type](layer, ctx)); +} + +sk_sp AttachComposition(const Json::Value& comp, AttachContext* ctx) { + if (!comp.isObject()) + return nullptr; + + LOG("** Attaching composition '%s'\n", ParseString(comp["id"], "").c_str()); + + auto comp_group = sksg::Group::Make(); + + for (const auto& l : comp["layers"]) { + if (auto layer_fragment = AttachLayer(l, ctx)) { + comp_group->addChild(std::move(layer_fragment)); + } + } + + return comp_group; +} + +} // namespace + +std::unique_ptr Animation::Make(SkStream* stream) { + if (!stream->hasLength()) { + // TODO: handle explicit buffering? + LOG("!! cannot parse streaming content\n"); + return nullptr; + } + + Json::Value json; + { + auto data = SkData::MakeFromStream(stream, stream->getLength()); + if (!data) { + LOG("!! could not read stream\n"); + return nullptr; + } + + Json::Reader reader; + + auto dataStart = static_cast(data->data()); + if (!reader.parse(dataStart, dataStart + data->size(), json, false) || !json.isObject()) { + LOG("!! failed to parse json: %s\n", reader.getFormattedErrorMessages().c_str()); + return nullptr; + } + } + + const auto version = ParseString(json["v"], ""); + const auto size = SkSize::Make(ParseScalar(json["w"], -1), ParseScalar(json["h"], -1)); + const auto fps = ParseScalar(json["fr"], -1); + + 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; + } + + return std::unique_ptr(new Animation(std::move(version), size, fps, json)); +} + +Animation::Animation(SkString version, const SkSize& size, SkScalar fps, const Json::Value& json) + : fVersion(std::move(version)) + , fSize(size) + , fFrameRate(fps) + , fInPoint(ParseScalar(json["ip"], 0)) + , fOutPoint(SkTMax(ParseScalar(json["op"], SK_ScalarMax), fInPoint)) { + + AssetMap assets; + for (const auto& asset : json["assets"]) { + if (!asset.isObject()) { + continue; + } + + assets.set(ParseString(asset["id"], ""), &asset); + } + + AttachContext ctx = { assets, fAnimators }; + fDom = AttachComposition(json, &ctx); + + LOG("** Attached %d animators\n", fAnimators.count()); +} + +Animation::~Animation() = default; + +void Animation::render(SkCanvas* canvas) const { + if (!fDom) + return; + + sksg::InvalidationController ic; + fDom->revalidate(&ic, SkMatrix::I()); + + // TODO: proper inval + fDom->render(canvas); + + if (!fShowInval) + return; + + SkPaint fill, stroke; + fill.setAntiAlias(true); + fill.setColor(0x40ff0000); + stroke.setAntiAlias(true); + stroke.setColor(0xffff0000); + stroke.setStyle(SkPaint::kStroke_Style); + + for (const auto& r : ic) { + canvas->drawRect(r, fill); + canvas->drawRect(r, stroke); + } +} + +void Animation::animationTick(SkMSec ms) { + // 't' in the BM model really means 'frame #' + auto t = static_cast(ms) * fFrameRate / 1000; + + t = fInPoint + std::fmod(t, fOutPoint - fInPoint); + + // TODO: this can be optimized quite a bit with some sorting/state tracking. + for (const auto& a : fAnimators) { + a->tick(t); + } +} + +} // namespace skotty diff --git a/experimental/skotty/Skotty.h b/experimental/skotty/Skotty.h new file mode 100644 index 0000000000..5cdffb829d --- /dev/null +++ b/experimental/skotty/Skotty.h @@ -0,0 +1,66 @@ +/* + * 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 Skotty_DEFINED +#define Skotty_DEFINED + +#include "SkRefCnt.h" +#include "SkSize.h" +#include "SkString.h" +#include "SkTArray.h" +#include "SkTHash.h" +#include "SkTypes.h" + +#include + +class SkCanvas; +class SkStream; + +namespace Json { class Value; } + +namespace sksg { class RenderNode; } + +namespace skotty { + +class AnimatorBase; + +class Animation : public SkNoncopyable { +public: + static std::unique_ptr Make(SkStream*); + + ~Animation(); + + void render(SkCanvas*) const; + + void animationTick(SkMSec); + + const SkString& version() const { return fVersion; } + const SkSize& size() const { return fSize; } + SkScalar frameRate() const { return fFrameRate; } + + void setShowInval(bool show) { fShowInval = show; } + +private: + Animation(SkString ver, const SkSize& size, SkScalar fps, const Json::Value&); + + SkString fVersion; + SkSize fSize; + SkScalar fFrameRate, + fInPoint, + fOutPoint; + + sk_sp fDom; + SkTArray> fAnimators; + + bool fShowInval = false; + + typedef SkNoncopyable INHERITED; +}; + +} // namespace skotty + +#endif // Skotty_DEFINED diff --git a/experimental/skotty/SkottyAnimator.cpp b/experimental/skotty/SkottyAnimator.cpp new file mode 100644 index 0000000000..e08cf35995 --- /dev/null +++ b/experimental/skotty/SkottyAnimator.cpp @@ -0,0 +1,59 @@ +/* + * 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 "SkottyAnimator.h" + +namespace skotty { + +namespace { + +SkScalar lerp_scalar(SkScalar v0, SkScalar v1, float t) { + SkASSERT(t >= 0 && t <= 1); + return v0 * (1 - t) + v1 * t; +} + +SkPoint lerp_point(const SkPoint& v0, const SkPoint& v1, float t) { + SkASSERT(t >= 0 && t <= 1); + return SkPoint::Make(lerp_scalar(v0.x(), v1.x(), t), + lerp_scalar(v0.y(), v1.y(), t)); +} + +} // namespace + +template <> +void KeyframeInterval::lerp(float t, ScalarValue* v) const { + *v = lerp_scalar(fV0, fV1, t); +} + +template <> +void KeyframeInterval::lerp(float t, VectorValue* v) const { + SkASSERT(fV0.cardinality() == fV1.cardinality()); + SkASSERT(v->cardinality() == 0); + + v->fVals.reserve(fV0.cardinality()); + for (int i = 0; i < fV0.fVals.count(); ++i) { + v->fVals.emplace_back(lerp_scalar(fV0.fVals[i], fV1.fVals[i], t)); + } +} + +template <> +void KeyframeInterval::lerp(float t, ShapeValue* v) const { + SkASSERT(fV0.cardinality() == fV1.cardinality()); + SkASSERT(v->cardinality() == 0); + + v->fVertices.reserve(fV0.cardinality()); + for (int i = 0; i < fV0.fVertices.count(); ++i) { + v->fVertices.push_back( + BezierVertex({ + lerp_point(fV0.fVertices[i].fInPoint , fV1.fVertices[i].fInPoint , t), + lerp_point(fV0.fVertices[i].fOutPoint, fV1.fVertices[i].fOutPoint, t), + lerp_point(fV0.fVertices[i].fVertex , fV1.fVertices[i].fVertex , t) + })); + } +} + +} // namespace skotty diff --git a/experimental/skotty/SkottyAnimator.h b/experimental/skotty/SkottyAnimator.h new file mode 100644 index 0000000000..833390e744 --- /dev/null +++ b/experimental/skotty/SkottyAnimator.h @@ -0,0 +1,169 @@ +/* + * 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 SkottyAnimator_DEFINED +#define SkottyAnimator_DEFINED + +#include "SkottyPriv.h" +#include "SkottyProperties.h" +#include "SkTArray.h" +#include "SkTypes.h" + +#include +#include + +namespace skotty { + +class AnimatorBase : public SkNoncopyable { +public: + virtual ~AnimatorBase() = default; + + virtual void tick(SkMSec) = 0; + +protected: + AnimatorBase() = default; +}; + +// Describes a keyframe interpolation interval (v0@t0) -> (v1@t1). +// TODO: add interpolation params. +template +struct KeyframeInterval { + T fV0, + fV1; + float fT0 = 0, + fT1 = 0; + + void lerp(float t, T*) const; +}; + +// Binds an animated/keyframed property to a node attribute. +template +class Animator : public AnimatorBase { +public: + static std::unique_ptr Make(const Json::Value& frames, sk_sp node, + std::function&, const AttrT&)> applyFunc); + + void tick(SkMSec t) override { + const auto& frame = this->findInterval(t); + const auto rel_t = (t - frame.fT0) / (frame.fT1 - frame.fT0); + + ValT val; + frame.lerp(SkTPin(rel_t, 0, 1), &val); + + fFunc(fTarget, val.template as()); + } + +private: + Animator(SkTArray>&& intervals, sk_sp node, + std::function&, const AttrT&)> applyFunc) + : fIntervals(std::move(intervals)) + , fTarget(std::move(node)) + , fFunc(std::move(applyFunc)) {} + + const KeyframeInterval& findInterval(float t) const; + + const SkTArray> fIntervals; + sk_sp fTarget; + std::function&, const AttrT&)> fFunc; +}; + +template +std::unique_ptr> +Animator::Make(const Json::Value& frames, + sk_sp node, std::function&, const AttrT&)> applyFunc) { + + if (!frames.isArray()) + return nullptr; + + SkTArray> intervals; + for (const auto& frame : frames) { + if (!frame.isObject()) + return nullptr; + + const auto t = ParseScalar(frame["t"], SK_ScalarMin); + if (t == SK_ScalarMin) + break; + + auto* prev_interval = intervals.empty() ? nullptr : &intervals.back(); + if (prev_interval) { + if (prev_interval->fT0 >= t) { + LOG("!! Ignoring out-of-order key frame (t: %f < t: %f)\n", t, prev_interval->fT0); + continue; + } + // Back-fill the prev interval t1. + prev_interval->fT1 = t; + } + + auto& curr_interval = intervals.push_back(); + if (!ValT::Parse(frame["s"], &curr_interval.fV0) || + !ValT::Parse(frame["e"], &curr_interval.fV1) || + curr_interval.fV0.cardinality() != curr_interval.fV1.cardinality() || + (prev_interval && + curr_interval.fV0.cardinality() != prev_interval->fV0.cardinality())) { + // Invalid frame, or "t"-only frame. + intervals.pop_back(); + continue; + } + + curr_interval.fT0 = curr_interval.fT1 = t; + } + + // If we couldn't determine a t1 for the last interval, discard it. + if (!intervals.empty() && intervals.back().fT0 == intervals.back().fT1) { + intervals.pop_back(); + } + + if (intervals.empty()) { + return nullptr; + } + + return std::unique_ptr( + new Animator(std::move(intervals), node, std::move(applyFunc))); +} + +template +const KeyframeInterval& Animator::findInterval(float t) const { + SkASSERT(!fIntervals.empty()); + + // TODO: cache last/current frame? + + auto f0 = fIntervals.begin(), + f1 = fIntervals.end() - 1; + + SkASSERT(f0->fT0 < f0->fT1); + SkASSERT(f1->fT0 < f1->fT1); + + if (t < f0->fT0) { + return *f0; + } + + if (t > f1->fT1) { + return *f1; + } + + while (f0 != f1) { + SkASSERT(f0 < f1); + SkASSERT(t >= f0->fT0 && t <= f1->fT1); + + const auto f = f0 + (f1 - f0) / 2; + SkASSERT(f->fT0 < f->fT1); + + if (t > f->fT1) { + f0 = f + 1; + } else { + f1 = f; + } + } + + SkASSERT(f0 == f1); + SkASSERT(t >= f0->fT0 && t <= f1->fT1); + return *f0; +} + +} // namespace skotty + +#endif // SkottyAnimator_DEFINED diff --git a/experimental/skotty/SkottyPriv.h b/experimental/skotty/SkottyPriv.h new file mode 100644 index 0000000000..35b1e847a3 --- /dev/null +++ b/experimental/skotty/SkottyPriv.h @@ -0,0 +1,41 @@ +/* + * 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 SkottyPriv_DEFINED +#define SkottyPriv_DEFINED + +#include "SkJSONCPP.h" +#include "SkScalar.h" +#include "SkString.h" + +namespace skotty { + +#define LOG SkDebugf + +static inline SkScalar ParseScalar(const Json::Value& v, SkScalar defaultValue) { + return !v.isNull() && v.isConvertibleTo(Json::realValue) + ? v.asFloat() : defaultValue; +} + +static inline SkString ParseString(const Json::Value& v, const char defaultValue[]) { + return SkString(!v.isNull() && v.isConvertibleTo(Json::stringValue) + ? v.asCString() : defaultValue); +} + +static inline int ParseInt(const Json::Value& v, int defaultValue) { + return !v.isNull() && v.isConvertibleTo(Json::intValue) + ? v.asInt() : defaultValue; +} + +static inline bool ParseBool(const Json::Value& v, bool defaultValue) { + return !v.isNull() && v.isConvertibleTo(Json::booleanValue) + ? v.asBool() : defaultValue; +} + +} // namespace + +#endif // SkottyPriv_DEFINED diff --git a/experimental/skotty/SkottyProperties.cpp b/experimental/skotty/SkottyProperties.cpp new file mode 100644 index 0000000000..f65a106735 --- /dev/null +++ b/experimental/skotty/SkottyProperties.cpp @@ -0,0 +1,157 @@ +/* + * 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 "SkottyProperties.h" + +#include "SkColor.h" +#include "SkottyPriv.h" +#include "SkPath.h" +#include "SkSGTransform.h" + +namespace skotty { + +namespace { + +using PointArray = SkSTArray<64, SkPoint, true>; + +bool ParsePoints(const Json::Value& v, PointArray* pts) { + if (!v.isArray()) { + return false; + } + + for (Json::ArrayIndex i = 0; i < v.size(); ++i) { + const auto& pt = v[i]; + if (!pt.isArray() || pt.size() != 2 || + !pt[0].isConvertibleTo(Json::realValue) || + !pt[1].isConvertibleTo(Json::realValue)) { + return false; + } + + pts->push_back(SkPoint::Make(ParseScalar(pt[0], 0), ParseScalar(pt[1], 0))); + } + return true; +} + +} // namespace + +bool ScalarValue::Parse(const Json::Value& v, ScalarValue* scalar) { + if (v.isNull() || !v.isConvertibleTo(Json::realValue)) + return false; + + scalar->fVal = ParseScalar(v, 0); + return true; +} + +bool VectorValue::Parse(const Json::Value& v, VectorValue* vec) { + SkASSERT(vec->fVals.empty()); + + if (!v.isArray()) + return false; + + for (Json::ArrayIndex i = 0; i < v.size(); ++i) { + const auto& el = v[i]; + if (el.isNull() || !el.isConvertibleTo(Json::realValue)) + return false; + + vec->fVals.emplace_back(ParseScalar(el, 0)); + } + + return true; +} + +bool ShapeValue::Parse(const Json::Value& v, ShapeValue* shape) { + PointArray inPts, + outPts, + verts; + + // Some files appear to wrap these in arrays for no reason. + if (v.isArray()) { + return Parse(v[0], shape); + } + + if (!v.isObject() || + !ParsePoints(v["i"], &inPts) || + !ParsePoints(v["o"], &outPts) || + !ParsePoints(v["v"], &verts) || + inPts.count() != outPts.count() || + inPts.count() != verts.count()) { + + return false; + } + + SkASSERT(shape->fVertices.empty()); + for (int i = 0; i < inPts.count(); ++i) { + shape->fVertices.emplace_back(BezierVertex({inPts[i], outPts[i], verts[i]})); + } + + shape->fClose = ParseBool(v["c"], false); + + return true; +} + +template <> +SkColor VectorValue::as() const { + // best effort to turn this into a color + const auto r = fVals.count() > 0 ? fVals[0].as() : 0, + g = fVals.count() > 1 ? fVals[1].as() : 0, + b = fVals.count() > 2 ? fVals[2].as() : 0, + a = fVals.count() > 3 ? fVals[3].as() : 1; + + return SkColorSetARGB(SkTPin(a, 0, 1) * 255, + SkTPin(r, 0, 1) * 255, + SkTPin(g, 0, 1) * 255, + SkTPin(b, 0, 1) * 255); +} + +template <> +SkPoint VectorValue::as() const { + // best effort to turn this into a point + const auto x = fVals.count() > 0 ? fVals[0].as() : 0, + y = fVals.count() > 1 ? fVals[1].as() : 0; + return SkPoint::Make(x, y); +} + +template <> +SkPath ShapeValue::as() const { + SkPath path; + + if (!fVertices.empty()) { + path.moveTo(fVertices.front().fVertex); + } + + const auto& addCubic = [](const BezierVertex& from, const BezierVertex& to, SkPath* path) { + path->cubicTo(from.fVertex + from.fOutPoint, + to.fVertex + to.fInPoint, + to.fVertex); + }; + + for (int i = 1; i < fVertices.count(); ++i) { + addCubic(fVertices[i - 1], fVertices[i], &path); + } + + if (fClose) { + addCubic(fVertices.back(), fVertices.front(), &path); + } + + return path; +} + +CompositeTransform::CompositeTransform(sk_sp wrapped_node) + : fTransformNode(sksg::Transform::Make(std::move(wrapped_node), SkMatrix::I())) {} + +void CompositeTransform::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 + + fTransformNode->setMatrix(t); +} + +} // namespace skotty diff --git a/experimental/skotty/SkottyProperties.h b/experimental/skotty/SkottyProperties.h new file mode 100644 index 0000000000..0273aea2f4 --- /dev/null +++ b/experimental/skotty/SkottyProperties.h @@ -0,0 +1,128 @@ +/* + * 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 SkottyProperties_DEFINED +#define SkottyProperties_DEFINED + +#include "SkPoint.h" +#include "SkSize.h" +#include "SkottyPriv.h" +#include "SkRefCnt.h" +#include "SkTArray.h" +#include "SkTypes.h" + +#include + +class SkPath; + +namespace sksg { +class RenderNode; +class Transform; +} + +namespace skotty { + +struct BezierVertex { + SkPoint fInPoint, // "in" control point, relative to the vertex + fOutPoint, // "out" control point, relative to the vertex + fVertex; +}; + +struct ScalarValue { + float fVal; + + static bool Parse(const Json::Value&, ScalarValue*); + + ScalarValue() : fVal(0) {} + explicit ScalarValue(SkScalar v) : fVal(v) {} + + ScalarValue& operator=(SkScalar v) { fVal = v; return *this; } + + operator SkScalar() const { return fVal; } + + size_t cardinality() const { return 1; } + + template + T as() const; +}; + +template <> +inline SkScalar ScalarValue::as() const { + return fVal; +} + +struct VectorValue { + SkTArray fVals; + + static bool Parse(const Json::Value&, VectorValue*); + + VectorValue() = default; + VectorValue(const VectorValue&) = delete; + VectorValue(VectorValue&&) = default; + VectorValue& operator==(const VectorValue&) = delete; + + size_t cardinality() const { return SkTo(fVals.count()); } + + template + T as() const; +}; + +struct ShapeValue { + SkTArray fVertices; + bool fClose = false; + + ShapeValue() = default; + ShapeValue(const ShapeValue&) = delete; + ShapeValue(ShapeValue&&) = default; + ShapeValue& operator==(const ShapeValue&) = delete; + + static bool Parse(const Json::Value&, ShapeValue*); + + size_t cardinality() const { return SkTo(fVertices.count()); } + + template + T as() const; +}; + +// Composite properties. + +#define COMPOSITE_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 CompositeTransform final : public SkRefCnt { +public: + explicit CompositeTransform(sk_sp); + + const sk_sp& node() const { return fTransformNode; } + + COMPOSITE_PROPERTY(AnchorPoint, SkPoint , SkPoint::Make(0, 0)) + COMPOSITE_PROPERTY(Position , SkPoint , SkPoint::Make(0, 0)) + COMPOSITE_PROPERTY(Scale , SkVector, SkPoint::Make(100, 100)) + COMPOSITE_PROPERTY(Rotation , SkScalar, 0) + COMPOSITE_PROPERTY(Skew , SkScalar, 0) + COMPOSITE_PROPERTY(SkewAxis , SkScalar, 0) + +private: + void apply(); + + sk_sp fTransformNode; + + using INHERITED = SkRefCnt; +}; + +#undef COMPOSITE_PROPERTY + +} // namespace skotty + +#endif // SkottyProperties_DEFINED diff --git a/resources/skotty/skotty_sample_1.json b/resources/skotty/skotty_sample_1.json new file mode 100644 index 0000000000..6010eb82b3 --- /dev/null +++ b/resources/skotty/skotty_sample_1.json @@ -0,0 +1,112 @@ +{ + "v":"4.6.9", + "fr":60, + "ip":0, + "op":200, + "w":800, + "h":600, + "nm":"Loader 1 JSON", + "ddd":0, + + + "layers":[ + { + "ddd":0, + "ind":1, + "ty":4, + "nm":"Custom Path 1", + "ao": 0, + "ip": 0, + "op": 300, + "st": 0, + "sr": 1, + "bm": 0, + "ks": { + "o": { "a":0, "k":100 }, + "r": { "a":0, "k":0 }, + "p": { "a":0, "k":[ 0, 0, 0 ] }, + "a": { "a":0, "k":[ 0, 0, 0 ] }, + "s": { "a":0, "k":[ 100, 100, 100 ] } + }, + + "shapes":[ + { + "ty":"gr", + "it":[ + { + "ty" : "sh", + "nm" : "Path 1", + "ks" : { + "a" : 1, + "k" : [ + { + "s": [ { + "i": [ [ -50, 0 ], [ -50, 0 ], [ -50, 0 ], [ -50, 0 ] ], + "o": [ [ 50, 0 ], [ 50, 0 ], [ 50, 0 ], [ 50, 0 ] ], + "v": [ [ 0, 100 ], [ 100, 0 ], [ 300, 200 ], [ 400, 100 ] ], + "c": false + } ], + "e": [ { + "i": [ [ -50, 0 ], [ -50, 0 ], [ -50, 0 ], [ -50, 0 ] ], + "o": [ [ 50, 0 ], [ 50, 0 ], [ 50, 0 ], [ 50, 0 ] ], + "v": [ [ 0, 100 ], [ 100, 200 ], [ 300, 0 ], [ 400, 100 ] ], + "c": false + } ], + "i": { "x":0.5, "y":0.5 }, + "o": { "x":0.5, "y":0.5 }, + "t": 0 + }, + { + "s": [ { + "i": [ [ -50, 0 ], [ -50, 0 ], [ -50, 0 ], [ -50, 0 ] ], + "o": [ [ 50, 0 ], [ 50, 0 ], [ 50, 0 ], [ 50, 0 ] ], + "v": [ [ 0, 100 ], [ 100, 200 ], [ 300, 0 ], [ 400, 100 ] ], + "c": false + } ], + "e": [ { + "i": [ [ -50, 0 ], [ -50, 0 ], [ -50, 0 ], [ -50, 0 ] ], + "o": [ [ 50, 0 ], [ 50, 0 ], [ 50, 0 ], [ 50, 0 ] ], + "v": [ [ 0, 100 ], [ 100, 0 ], [ 300, 200 ], [ 400, 100 ] ], + "c": false + } ], + "i": { "x":0.5, "y":0.5 }, + "o": { "x":0.5, "y":0.5 }, + "t": 100 + }, + { + "t": 200 + } + ] + } + }, + + { + "ty": "st", + "nm": "Stroke 1", + "lc": 1, + "lj": 1, + "ml": 4, + "w" : { "a": 0, "k": 30 }, + "o" : { "a": 0, "k": 100 }, + "c" : { "a": 1, "k": [ + { "s": [ 1, 0, 0 ], "e": [ 0, 1, 0 ], "i": { "x":0.5, "y":0.5 }, "o": { "x":0.5, "y":0.5 }, "t": 0 }, + { "s": [ 0, 1, 0 ], "e": [ 1, 0, 0 ], "i": { "x":0.5, "y":0.5 }, "o": { "x":0.5, "y":0.5 }, "t": 100 }, + { "t": 200 } + ] } + }, + + { + "ty":"tr", + "p" : { "a":0, "k":[ 0, 0 ] }, + "a" : { "a":0, "k":[ 0, 0 ] }, + "s" : { "a":0, "k":[ 100, 100 ] }, + "r" : { "a":0, "k": 0 }, + "o" : { "a":0, "k":100 }, + "nm": "Transform" + } + ] + } + ] + } + ] +} diff --git a/tools/flags/SkCommandLineFlags.h b/tools/flags/SkCommandLineFlags.h index 15f12d915b..4a22c3f4bf 100644 --- a/tools/flags/SkCommandLineFlags.h +++ b/tools/flags/SkCommandLineFlags.h @@ -155,6 +155,9 @@ public: fStrings[i].set(str); } + const SkString* begin() const { return fStrings.begin(); } + const SkString* end() const { return fStrings.end(); } + private: void reset() { fStrings.reset(); } diff --git a/tools/viewer/SkottySlide.cpp b/tools/viewer/SkottySlide.cpp new file mode 100644 index 0000000000..d65b7ecbc1 --- /dev/null +++ b/tools/viewer/SkottySlide.cpp @@ -0,0 +1,75 @@ +/* + * 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 "SkottySlide.h" + +#include "SkAnimTimer.h" +#include "Skotty.h" +#include "SkStream.h" + +SkottySlide::SkottySlide(const SkString& name, const SkString& path) + : fPath(path) { + fName = name; +} + +void SkottySlide::load(SkScalar, SkScalar) { + auto stream = SkStream::MakeFromFile(fPath.c_str()); + fAnimation = skotty::Animation::Make(stream.get()); + fTimeBase = 0; // force a time reset + + if (fAnimation) { + SkDebugf("loaded Bodymovin animation v: %s, size: [%f %f], fr: %f\n", + fAnimation->version().c_str(), + fAnimation->size().width(), + fAnimation->size().height(), + fAnimation->frameRate()); + } else { + SkDebugf("failed to load Bodymovin animation: %s\n", fPath.c_str()); + } +} + +void SkottySlide::unload() { + fAnimation.reset(); +} + +SkISize SkottySlide::getDimensions() const { + return fAnimation? fAnimation->size().toCeil() : SkISize::Make(0, 0); +} + +void SkottySlide::draw(SkCanvas* canvas) { + if (fAnimation) { + fAnimation->render(canvas); + } +} + +bool SkottySlide::animate(const SkAnimTimer& timer) { + if (fTimeBase == 0) { + // Reset the animation time. + fTimeBase = timer.msec(); + } + + if (fAnimation) { + auto t = timer.msec() - fTimeBase; + fAnimation->animationTick(t); + } + return true; +} + +bool SkottySlide::onChar(SkUnichar c) { + switch (c) { + case 'I': + if (fAnimation) { + fShowAnimationInval = !fShowAnimationInval; + fAnimation->setShowInval(fShowAnimationInval); + } + break; + default: + break; + } + + return INHERITED::onChar(c); +} diff --git a/tools/viewer/SkottySlide.h b/tools/viewer/SkottySlide.h new file mode 100644 index 0000000000..9b7ae7c890 --- /dev/null +++ b/tools/viewer/SkottySlide.h @@ -0,0 +1,39 @@ +/* + * 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 SkottySlide_DEFINED +#define SkottySlide_DEFINED + +#include "Slide.h" + +namespace skotty { class Animation; } + +class SkottySlide : public Slide { +public: + SkottySlide(const SkString& name, const SkString& path); + ~SkottySlide() override = default; + + void load(SkScalar winWidth, SkScalar winHeight) override; + void unload() override; + + SkISize getDimensions() const override; + + void draw(SkCanvas*) override; + bool animate(const SkAnimTimer&) override; + + bool onChar(SkUnichar) override; + +private: + SkString fPath; + std::unique_ptr fAnimation; + SkMSec fTimeBase = 0; + bool fShowAnimationInval = false; + + typedef Slide INHERITED; +}; + +#endif // SkottySlide_DEFINED diff --git a/tools/viewer/Viewer.cpp b/tools/viewer/Viewer.cpp index 1f701e25b3..55a025c1ed 100644 --- a/tools/viewer/Viewer.cpp +++ b/tools/viewer/Viewer.cpp @@ -11,6 +11,7 @@ #include "ImageSlide.h" #include "Resources.h" #include "SampleSlide.h" +#include "SkottySlide.h" #include "SKPSlide.h" #include "GrContext.h" @@ -68,9 +69,11 @@ static DEFINE_bool(list, false, "List samples?"); #ifdef SK_BUILD_FOR_ANDROID static DEFINE_string(skps, "/data/local/tmp/skps", "Directory to read skps from."); static DEFINE_string(jpgs, "/data/local/tmp/resources", "Directory to read jpgs from."); +static DEFINE_string(jsons, "/data/local/tmp/jsons", "Directory to read (Bodymovin) jsons from."); #else static DEFINE_string(skps, "skps", "Directory to read skps from."); static DEFINE_string(jpgs, "jpgs", "Directory to read jpgs from."); +static DEFINE_string(jsons, "jsons", "Directory to read (Bodymovin) jsons from."); #endif static DEFINE_string2(backend, b, "sw", "Backend to use. Allowed values are " BACKENDS_STR "."); @@ -480,6 +483,19 @@ void Viewer::initSlides() { } } } + + // JSONs + for (const auto& json : FLAGS_jsons) { + SkOSFile::Iter it(json.c_str(), ".json"); + SkString jsonName; + while (it.next(&jsonName)) { + if (SkCommandLineFlags::ShouldSkip(FLAGS_match, jsonName.c_str())) { + continue; + } + fSlides.push_back(sk_make_sp(jsonName, SkOSPath::Join(json.c_str(), + jsonName.c_str()))); + } + } } -- cgit v1.2.3