From 54f65c473fd6bf6919dfcbad22e924dff7586568 Mon Sep 17 00:00:00 2001 From: Florin Malita Date: Tue, 16 Jan 2018 17:04:30 -0500 Subject: Skotty -> Skottie Change-Id: If8b6516024c69b0fc256208874f6666a4e70e12c Reviewed-on: https://skia-review.googlesource.com/95241 Reviewed-by: Mike Reed Commit-Queue: Florin Malita --- BUILD.gn | 18 +- dm/DM.cpp | 2 +- dm/DMSrcSink.cpp | 14 +- dm/DMSrcSink.h | 8 +- experimental/skottie/Skottie.cpp | 1051 ++++++++++++++++++++++++++++ experimental/skottie/Skottie.h | 80 +++ experimental/skottie/SkottieAnimator.cpp | 94 +++ experimental/skottie/SkottieAnimator.h | 193 +++++ experimental/skottie/SkottiePriv.h | 49 ++ experimental/skottie/SkottieProperties.cpp | 290 ++++++++ experimental/skottie/SkottieProperties.h | 168 +++++ experimental/skotty/Skotty.cpp | 1051 ---------------------------- experimental/skotty/Skotty.h | 80 --- experimental/skotty/SkottyAnimator.cpp | 94 --- experimental/skotty/SkottyAnimator.h | 193 ----- experimental/skotty/SkottyPriv.h | 49 -- experimental/skotty/SkottyProperties.cpp | 290 -------- experimental/skotty/SkottyProperties.h | 168 ----- experimental/sksg/geometry/SkSGMerge.cpp | 2 +- tools/viewer/SkottieSlide.cpp | 77 ++ tools/viewer/SkottieSlide.h | 73 ++ tools/viewer/SkottieSlide2.cpp | 128 ++++ tools/viewer/SkottySlide.cpp | 77 -- tools/viewer/SkottySlide.h | 73 -- tools/viewer/SkottySlide2.cpp | 129 ---- tools/viewer/Viewer.cpp | 8 +- 26 files changed, 2229 insertions(+), 2230 deletions(-) create mode 100644 experimental/skottie/Skottie.cpp create mode 100644 experimental/skottie/Skottie.h create mode 100644 experimental/skottie/SkottieAnimator.cpp create mode 100644 experimental/skottie/SkottieAnimator.h create mode 100644 experimental/skottie/SkottiePriv.h create mode 100644 experimental/skottie/SkottieProperties.cpp create mode 100644 experimental/skottie/SkottieProperties.h delete mode 100644 experimental/skotty/Skotty.cpp delete mode 100644 experimental/skotty/Skotty.h delete mode 100644 experimental/skotty/SkottyAnimator.cpp delete mode 100644 experimental/skotty/SkottyAnimator.h delete mode 100644 experimental/skotty/SkottyPriv.h delete mode 100644 experimental/skotty/SkottyProperties.cpp delete mode 100644 experimental/skotty/SkottyProperties.h create mode 100644 tools/viewer/SkottieSlide.cpp create mode 100644 tools/viewer/SkottieSlide.h create mode 100644 tools/viewer/SkottieSlide2.cpp delete mode 100644 tools/viewer/SkottySlide.cpp delete mode 100644 tools/viewer/SkottySlide.h delete mode 100644 tools/viewer/SkottySlide2.cpp diff --git a/BUILD.gn b/BUILD.gn index b17544fba6..5addfe7b25 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -1321,13 +1321,13 @@ if (skia_enable_tools) { ] } - test_lib("experimental_skotty") { - public_include_dirs = [ "experimental/skotty" ] + test_lib("experimental_skottie") { + public_include_dirs = [ "experimental/skottie" ] include_dirs = [ "tools" ] sources = [ - "experimental/skotty/Skotty.cpp", - "experimental/skotty/SkottyAnimator.cpp", - "experimental/skotty/SkottyProperties.cpp", + "experimental/skottie/Skottie.cpp", + "experimental/skottie/SkottieAnimator.cpp", + "experimental/skottie/SkottieProperties.cpp", ] deps = [ ":experimental_sksg", @@ -1511,7 +1511,7 @@ if (skia_enable_tools) { include_dirs = [ "tests" ] deps = [ ":common_flags", - ":experimental_skotty", + ":experimental_skottie", ":experimental_sksg", ":experimental_svg_model", ":flags", @@ -1913,8 +1913,8 @@ if (skia_enable_tools) { "tools/viewer/ImageSlide.cpp", "tools/viewer/SKPSlide.cpp", "tools/viewer/SampleSlide.cpp", - "tools/viewer/SkottySlide.cpp", - "tools/viewer/SkottySlide2.cpp", + "tools/viewer/SkottieSlide.cpp", + "tools/viewer/SkottieSlide2.cpp", "tools/viewer/StatsLayer.cpp", "tools/viewer/Viewer.cpp", ] @@ -1922,7 +1922,7 @@ if (skia_enable_tools) { include_dirs = [] deps = [ - ":experimental_skotty", + ":experimental_skottie", ":flags", ":gm", ":gpu_tool_utils", diff --git a/dm/DM.cpp b/dm/DM.cpp index ee9a7f5678..61a75951d1 100644 --- a/dm/DM.cpp +++ b/dm/DM.cpp @@ -785,7 +785,7 @@ static bool gather_srcs() { } gather_file_srcs(FLAGS_mskps, "mskp"); #if !defined(SK_BUILD_FOR_GOOGLE3) - gather_file_srcs(FLAGS_jsons, "json"); + gather_file_srcs(FLAGS_jsons, "json"); #endif #if defined(SK_XML) gather_file_srcs(FLAGS_svgs, "svg"); diff --git a/dm/DMSrcSink.cpp b/dm/DMSrcSink.cpp index d0c468159e..506d3fb52e 100644 --- a/dm/DMSrcSink.cpp +++ b/dm/DMSrcSink.cpp @@ -61,7 +61,7 @@ #endif #if !defined(SK_BUILD_FOR_GOOGLE3) - #include "Skotty.h" + #include "Skottie.h" #endif #if defined(SK_XML) @@ -1317,10 +1317,10 @@ Name DDLSKPSrc::name() const { return SkOSPath::Basename(fPath.c_str()); } /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ #if !defined(SK_BUILD_FOR_GOOGLE3) -SkottySrc::SkottySrc(Path path) +SkottieSrc::SkottieSrc(Path path) : fName(SkOSPath::Basename(path.c_str())) { - fAnimation = skotty::Animation::MakeFromFile(path.c_str()); + fAnimation = skottie::Animation::MakeFromFile(path.c_str()); if (!fAnimation) { return; } @@ -1334,7 +1334,7 @@ SkottySrc::SkottySrc(Path path) } -Error SkottySrc::draw(SkCanvas* canvas) const { +Error SkottieSrc::draw(SkCanvas* canvas) const { if (!fAnimation) { return SkStringPrintf("Unable to parse file: %s", fName.c_str()); } @@ -1383,15 +1383,15 @@ Error SkottySrc::draw(SkCanvas* canvas) const { return ""; } -SkISize SkottySrc::size() const { +SkISize SkottieSrc::size() const { // Padding for grid. return SkISize::Make(kTileCount * (fTileSize.width() + 1), kTileCount * (fTileSize.height() + 1)); } -Name SkottySrc::name() const { return fName; } +Name SkottieSrc::name() const { return fName; } -bool SkottySrc::veto(SinkFlags flags) const { +bool SkottieSrc::veto(SinkFlags flags) const { // No need to test to non-(raster||gpu||vector) or indirect backends. bool type_ok = flags.type == SinkFlags::kRaster || flags.type == SinkFlags::kGPU diff --git a/dm/DMSrcSink.h b/dm/DMSrcSink.h index 9a6cb0c6ec..d44655a477 100644 --- a/dm/DMSrcSink.h +++ b/dm/DMSrcSink.h @@ -21,7 +21,7 @@ //#define TEST_VIA_SVG -namespace skotty { class Animation; } +namespace skottie { class Animation; } namespace DM { @@ -263,9 +263,9 @@ private: }; #if !defined(SK_BUILD_FOR_GOOGLE3) -class SkottySrc final : public Src { +class SkottieSrc final : public Src { public: - explicit SkottySrc(Path path); + explicit SkottieSrc(Path path); Error draw(SkCanvas*) const override; SkISize size() const override; @@ -278,7 +278,7 @@ private: Name fName; SkISize fTileSize = SkISize::MakeEmpty(); - std::unique_ptr fAnimation; + std::unique_ptr fAnimation; }; #endif diff --git a/experimental/skottie/Skottie.cpp b/experimental/skottie/Skottie.cpp new file mode 100644 index 0000000000..f8c5ea021c --- /dev/null +++ b/experimental/skottie/Skottie.cpp @@ -0,0 +1,1051 @@ +/* + * 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 "SkottieAnimator.h" +#include "SkottiePriv.h" +#include "SkottieProperties.h" +#include "SkData.h" +#include "SkImage.h" +#include "SkMakeUnique.h" +#include "SkOSPath.h" +#include "SkPaint.h" +#include "SkParse.h" +#include "SkPath.h" +#include "SkPoint.h" +#include "SkSGColor.h" +#include "SkSGDraw.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 "SkSGTransform.h" +#include "SkSGTrimEffect.h" +#include "SkStream.h" +#include "SkTArray.h" +#include "SkTHash.h" + +#include +#include +#include + +#include "stdlib.h" + +namespace skottie { + +namespace { + +using AssetMap = SkTHashMap; + +struct AttachContext { + const ResourceProvider& fResources; + 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 BindProperty(const Json::Value& jprop, AttachContext* ctx, const sk_sp& node, + typename Animator::ApplyFuncT&& apply) { + 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.isNull() || !ParseBool(jpropA, "false")) { + ValT val; + if (ValueTraits::Parse(jpropK, &val)) { + // Static property. + apply(node.get(), val); + return true; + } + + if (!jpropA.isNull()) { + return LogFail(jprop, "Could not parse (explicit) static property"); + } + } + + // Keyframe property. + using AnimatorT = Animator; + auto animator = AnimatorT::Make(ParseFrames(jpropK), node, std::move(apply)); + + if (!animator) { + return LogFail(jprop, "Could not parse keyframed property"); + } + + ctx->fAnimators.push_back(std::move(animator)); + + return true; +} + +sk_sp AttachMatrix(const Json::Value& t, AttachContext* ctx, + sk_sp parentMatrix) { + if (!t.isObject()) + return nullptr; + + auto matrix = sksg::Matrix::Make(SkMatrix::I(), std::move(parentMatrix)); + auto composite = sk_make_sp(matrix); + auto anchor_attached = BindProperty(t["a"], ctx, composite, + [](CompositeTransform* node, const VectorValue& a) { + node->setAnchorPoint(ValueTraits::As(a)); + }); + auto position_attached = BindProperty(t["p"], ctx, composite, + [](CompositeTransform* node, const VectorValue& p) { + node->setPosition(ValueTraits::As(p)); + }); + auto scale_attached = BindProperty(t["s"], ctx, composite, + [](CompositeTransform* node, const VectorValue& s) { + node->setScale(ValueTraits::As(s)); + }); + auto rotation_attached = BindProperty(t["r"], ctx, composite, + [](CompositeTransform* node, const ScalarValue& r) { + node->setRotation(r); + }); + auto skew_attached = BindProperty(t["sk"], ctx, composite, + [](CompositeTransform* node, const ScalarValue& sk) { + node->setSkew(sk); + }); + auto skewaxis_attached = BindProperty(t["sa"], ctx, composite, + [](CompositeTransform* node, const ScalarValue& sa) { + node->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 AttachOpacity(const Json::Value& jtransform, AttachContext* ctx, + sk_sp childNode) { + if (!jtransform.isObject() || !childNode) + return childNode; + + // This is more peeky than other attachers, because we want to avoid redundant opacity + // nodes for the extremely common case of static opaciy == 100. + const auto& opacity = jtransform["o"]; + if (opacity.isObject() && + !ParseBool(opacity["a"], true) && + ParseScalar(opacity["k"], -1) == 100) { + // Ignoring static full opacity. + return childNode; + } + + auto opacityNode = sksg::OpacityEffect::Make(childNode); + BindProperty(opacity, ctx, opacityNode, + [](sksg::OpacityEffect* node, const ScalarValue& o) { + // BM opacity is [0..100] + node->setOpacity(o * 0.01f); + }); + + return opacityNode; +} + +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 = BindProperty(jpath["ks"], ctx, path_node, + [](sksg::Path* node, const ShapeValue& 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 AttachRRectGeometry(const Json::Value& jrect, AttachContext* ctx) { + SkASSERT(jrect.isObject()); + + auto rect_node = sksg::RRect::Make(); + auto composite = sk_make_sp(rect_node); + + auto p_attached = BindProperty(jrect["p"], ctx, composite, + [](CompositeRRect* node, const VectorValue& p) { + node->setPosition(ValueTraits::As(p)); + }); + auto s_attached = BindProperty(jrect["s"], ctx, composite, + [](CompositeRRect* node, const VectorValue& s) { + node->setSize(ValueTraits::As(s)); + }); + auto r_attached = BindProperty(jrect["r"], ctx, composite, + [](CompositeRRect* node, const ScalarValue& r) { + node->setRadius(SkSize::Make(r, r)); + }); + + if (!p_attached && !s_attached && !r_attached) { + return nullptr; + } + + LOG("** Attached (r)rect geometry\n"); + + return rect_node; +} + +sk_sp AttachEllipseGeometry(const Json::Value& jellipse, AttachContext* ctx) { + SkASSERT(jellipse.isObject()); + + auto rect_node = sksg::RRect::Make(); + auto composite = sk_make_sp(rect_node); + + auto p_attached = BindProperty(jellipse["p"], ctx, composite, + [](CompositeRRect* node, const VectorValue& p) { + node->setPosition(ValueTraits::As(p)); + }); + auto s_attached = BindProperty(jellipse["s"], ctx, composite, + [](CompositeRRect* node, const VectorValue& s) { + const auto sz = ValueTraits::As(s); + node->setSize(sz); + node->setRadius(SkSize::Make(sz.width() / 2, sz.height() / 2)); + }); + + if (!p_attached && !s_attached) { + return nullptr; + } + + LOG("** Attached ellipse geometry\n"); + + return rect_node; +} + +sk_sp AttachPolystarGeometry(const Json::Value& jstar, AttachContext* ctx) { + SkASSERT(jstar.isObject()); + + static constexpr CompositePolyStar::Type gTypes[] = { + CompositePolyStar::Type::kStar, // "sy": 1 + CompositePolyStar::Type::kPoly, // "sy": 2 + }; + + const auto type = ParseInt(jstar["sy"], 0) - 1; + if (type < 0 || type >= SkTo(SK_ARRAY_COUNT(gTypes))) { + LogFail(jstar, "Unknown polystar type"); + return nullptr; + } + + auto path_node = sksg::Path::Make(); + auto composite = sk_make_sp(path_node, gTypes[type]); + + BindProperty(jstar["p"], ctx, composite, + [](CompositePolyStar* node, const VectorValue& p) { + node->setPosition(ValueTraits::As(p)); + }); + BindProperty(jstar["pt"], ctx, composite, + [](CompositePolyStar* node, const ScalarValue& pt) { + node->setPointCount(pt); + }); + BindProperty(jstar["ir"], ctx, composite, + [](CompositePolyStar* node, const ScalarValue& ir) { + node->setInnerRadius(ir); + }); + BindProperty(jstar["or"], ctx, composite, + [](CompositePolyStar* node, const ScalarValue& otr) { + node->setOuterRadius(otr); + }); + BindProperty(jstar["is"], ctx, composite, + [](CompositePolyStar* node, const ScalarValue& is) { + node->setInnerRoundness(is); + }); + BindProperty(jstar["os"], ctx, composite, + [](CompositePolyStar* node, const ScalarValue& os) { + node->setOuterRoundness(os); + }); + BindProperty(jstar["r"], ctx, composite, + [](CompositePolyStar* node, const ScalarValue& r) { + node->setRotation(r); + }); + + return path_node; +} + +sk_sp AttachColor(const Json::Value& obj, AttachContext* ctx) { + SkASSERT(obj.isObject()); + + auto color_node = sksg::Color::Make(SK_ColorBLACK); + auto color_attached = BindProperty(obj["c"], ctx, color_node, + [](sksg::Color* node, const VectorValue& c) { + node->setColor(ValueTraits::As(c)); + }); + + return color_attached ? color_node : nullptr; +} + +sk_sp AttachGradient(const Json::Value& obj, AttachContext* ctx) { + SkASSERT(obj.isObject()); + + const auto& stops = obj["g"]; + if (!stops.isObject()) + return nullptr; + + const auto stopCount = ParseInt(stops["p"], -1); + if (stopCount < 0) + return nullptr; + + sk_sp gradient_node; + sk_sp composite; + + if (ParseInt(obj["t"], 1) == 1) { + auto linear_node = sksg::LinearGradient::Make(); + composite = sk_make_sp(linear_node, stopCount); + gradient_node = std::move(linear_node); + } else { + auto radial_node = sksg::RadialGradient::Make(); + composite = sk_make_sp(radial_node, stopCount); + + // TODO: highlight, angle + gradient_node = std::move(radial_node); + } + + BindProperty(stops["k"], ctx, composite, + [](CompositeGradient* node, const VectorValue& stops) { + node->setColorStops(stops); + }); + BindProperty(obj["s"], ctx, composite, + [](CompositeGradient* node, const VectorValue& s) { + node->setStartPoint(ValueTraits::As(s)); + }); + BindProperty(obj["e"], ctx, composite, + [](CompositeGradient* node, const VectorValue& e) { + node->setEndPoint(ValueTraits::As(e)); + }); + + return gradient_node; +} + +sk_sp AttachPaint(const Json::Value& jpaint, AttachContext* ctx, + sk_sp paint_node) { + if (paint_node) { + paint_node->setAntiAlias(true); + + BindProperty(jpaint["o"], ctx, paint_node, + [](sksg::PaintNode* node, const ScalarValue& o) { + // BM opacity is [0..100] + node->setOpacity(o * 0.01f); + }); + } + + return paint_node; +} + +sk_sp AttachStroke(const Json::Value& jstroke, AttachContext* ctx, + sk_sp stroke_node) { + SkASSERT(jstroke.isObject()); + + if (!stroke_node) + return nullptr; + + stroke_node->setStyle(SkPaint::kStroke_Style); + + auto width_attached = BindProperty(jstroke["w"], ctx, stroke_node, + [](sksg::PaintNode* node, const ScalarValue& w) { + node->setStrokeWidth(w); + }); + 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; +} + +sk_sp AttachColorFill(const Json::Value& jfill, AttachContext* ctx) { + SkASSERT(jfill.isObject()); + + return AttachPaint(jfill, ctx, AttachColor(jfill, ctx)); +} + +sk_sp AttachGradientFill(const Json::Value& jfill, AttachContext* ctx) { + SkASSERT(jfill.isObject()); + + return AttachPaint(jfill, ctx, AttachGradient(jfill, ctx)); +} + +sk_sp AttachColorStroke(const Json::Value& jstroke, AttachContext* ctx) { + SkASSERT(jstroke.isObject()); + + return AttachStroke(jstroke, ctx, AttachPaint(jstroke, ctx, AttachColor(jstroke, ctx))); +} + +sk_sp AttachGradientStroke(const Json::Value& jstroke, AttachContext* ctx) { + SkASSERT(jstroke.isObject()); + + return AttachStroke(jstroke, ctx, AttachPaint(jstroke, ctx, AttachGradient(jstroke, ctx))); +} + +std::vector> AttachMergeGeometryEffect( + const Json::Value& jmerge, AttachContext* ctx, std::vector>&& geos) { + std::vector> 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(ParseInt(jmerge["mm"], 1) - 1, + 0, SK_ARRAY_COUNT(gModes) - 1)]; + merged.push_back(sksg::Merge::Make(std::move(geos), mode)); + + LOG("** Attached merge path effect, mode: %d\n", mode); + + return merged; +} + +std::vector> AttachTrimGeometryEffect( + const Json::Value& jtrim, AttachContext* ctx, std::vector>&& geos) { + + enum class Mode { + kMerged, // "m": 1 + kSeparate, // "m": 2 + } gModes[] = { Mode::kMerged, Mode::kSeparate }; + + const auto mode = gModes[SkTPin(ParseInt(jtrim["m"], 1) - 1, + 0, SK_ARRAY_COUNT(gModes) - 1)]; + + std::vector> 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> trimmed; + trimmed.reserve(inputs.size()); + for (const auto& i : inputs) { + const auto trim = sksg::TrimEffect::Make(i); + trimmed.push_back(trim); + BindProperty(jtrim["s"], ctx, trim, + [](sksg::TrimEffect* node, const ScalarValue& s) { + node->setStart(s * 0.01f); + }); + BindProperty(jtrim["e"], ctx, trim, + [](sksg::TrimEffect* node, const ScalarValue& e) { + node->setEnd(e * 0.01f); + }); + // TODO: "offset" doesn't currently work the same as BM - figure out what's going on. + BindProperty(jtrim["o"], ctx, trim, + [](sksg::TrimEffect* node, const ScalarValue& o) { + node->setOffset(o * 0.01f); + }); + } + + return trimmed; +} + +using GeometryAttacherT = sk_sp (*)(const Json::Value&, AttachContext*); +static constexpr GeometryAttacherT gGeometryAttachers[] = { + AttachPathGeometry, + AttachRRectGeometry, + AttachEllipseGeometry, + AttachPolystarGeometry, +}; + +using PaintAttacherT = sk_sp (*)(const Json::Value&, AttachContext*); +static constexpr PaintAttacherT gPaintAttachers[] = { + AttachColorFill, + AttachColorStroke, + AttachGradientFill, + AttachGradientStroke, +}; + +using GroupAttacherT = sk_sp (*)(const Json::Value&, AttachContext*); +static constexpr GroupAttacherT gGroupAttachers[] = { + AttachShapeGroup, +}; + +using GeometryEffectAttacherT = + std::vector> (*)(const Json::Value&, + AttachContext*, + std::vector>&&); +static constexpr GeometryEffectAttacherT gGeometryEffectAttachers[] = { + AttachMergeGeometryEffect, + AttachTrimGeometryEffect, +}; + +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::Value& 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 -> AttachShapeGroup + { "gs", ShapeType::kPaint , 3 }, // gstroke -> AttachGradientStroke + { "mm", ShapeType::kGeometryEffect, 0 }, // merge -> AttachMergeGeometryEffect + { "rc", ShapeType::kGeometry , 1 }, // rrect -> AttachRRectGeometry + { "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 -> In-place handler + }; + + 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; + + // (https://helpx.adobe.com/after-effects/using/overview-shape-layers-paths-vector.html#groups_and_render_order_for_shapes_and_shape_attributes) + // + // Render order for shapes within a shape layer + // + // The rules for rendering a shape layer are similar to the rules for rendering a composition + // that contains nested compositions: + // + // * Within a group, the shape at the bottom of the Timeline panel stacking order is rendered + // first. + // + // * All path operations within a group are performed before paint operations. This means, + // for example, that the stroke follows the distortions in the path made by the Wiggle Paths + // path operation. Path operations within a group are performed from top to bottom. + // + // * Paint operations within a group are performed from the bottom to the top in the Timeline + // panel stacking order. This means, for example, that a stroke is rendered on top of + // (in front of) a stroke that appears after it in the Timeline panel. + // + sk_sp shape_group = sksg::Group::Make(); + sk_sp xformed_group = shape_group; + + std::vector> geos; + std::vector> draws; + + 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::kGeometryEffect: { + SkASSERT(info->fAttacherIndex < SK_ARRAY_COUNT(gGeometryEffectAttachers)); + geos = gGeometryEffectAttachers[info->fAttacherIndex](s, ctx, std::move(geos)); + } break; + case ShapeType::kPaint: { + SkASSERT(info->fAttacherIndex < SK_ARRAY_COUNT(gPaintAttachers)); + if (auto paint = gPaintAttachers[info->fAttacherIndex](s, ctx)) { + for (const auto& geo : geos) { + draws.push_back(sksg::Draw::Make(geo, paint)); + } + } + } break; + case ShapeType::kGroup: { + SkASSERT(info->fAttacherIndex < SK_ARRAY_COUNT(gGroupAttachers)); + if (auto group = gGroupAttachers[info->fAttacherIndex](s, ctx)) { + draws.push_back(std::move(group)); + } + } break; + case ShapeType::kTransform: { + // TODO: BM appears to transform the geometry, not the draw op itself. + if (auto matrix = AttachMatrix(s, ctx, nullptr)) { + xformed_group = sksg::Transform::Make(std::move(xformed_group), + std::move(matrix)); + } + xformed_group = AttachOpacity(s, ctx, std::move(xformed_group)); + } break; + } + } + + if (draws.empty()) { + return nullptr; + } + + for (auto draw = draws.rbegin(); draw != draws.rend(); ++draw) { + shape_group->addChild(std::move(*draw)); + } + + LOG("** Attached shape: %zd draws.\n", draws.size()); + return xformed_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& jlayer, AttachContext*) { + SkASSERT(jlayer.isObject()); + + const auto size = SkSize::Make(ParseScalar(jlayer["sw"], -1), + ParseScalar(jlayer["sh"], -1)); + const auto hex = ParseString(jlayer["sc"], ""); + 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 AttachImageAsset(const Json::Value& jimage, AttachContext* ctx) { + SkASSERT(jimage.isObject()); + + const auto name = ParseString(jimage["p"], ""), + path = ParseString(jimage["u"], ""); + 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 AttachImageLayer(const Json::Value& layer, AttachContext* ctx) { + SkASSERT(layer.isObject()); + + auto refId = ParseString(layer["refId"], ""); + if (refId.isEmpty()) { + LOG("!! Image layer missing refId\n"); + return nullptr; + } + + const auto* jimage = ctx->fAssets.find(refId); + if (!jimage) { + LOG("!! Image asset not found: '%s'\n", refId.c_str()); + return nullptr; + } + + return AttachImageAsset(**jimage, ctx); +} + +sk_sp AttachNullLayer(const Json::Value& layer, AttachContext*) { + 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 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; +} + +struct AttachLayerContext { + AttachLayerContext(const Json::Value& jlayers, AttachContext* ctx) + : fLayerList(jlayers), fCtx(ctx) {} + + const Json::Value& fLayerList; + AttachContext* fCtx; + std::unordered_map> fLayerMatrixCache; + std::unordered_map fLayerIndexCache; + sk_sp fCurrentMatte; + + const Json::Value* findLayer(int index) { + SkASSERT(fLayerList.isArray()); + + if (index < 0) { + return nullptr; + } + + const auto cached = fLayerIndexCache.find(index); + if (cached != fLayerIndexCache.end()) { + return cached->second; + } + + for (const auto& l : fLayerList) { + if (!l.isObject()) { + continue; + } + + if (ParseInt(l["ind"], -1) == index) { + fLayerIndexCache.insert(std::make_pair(index, &l)); + return &l; + } + } + + return nullptr; + } + + sk_sp AttachLayerMatrix(const Json::Value& jlayer) { + SkASSERT(jlayer.isObject()); + + const auto cached = fLayerMatrixCache.find(&jlayer); + if (cached != fLayerMatrixCache.end()) { + return cached->second; + } + + const auto* parentLayer = this->findLayer(ParseInt(jlayer["parent"], -1)); + + // TODO: cycle detection? + auto parentMatrix = (parentLayer && parentLayer != &jlayer) + ? this->AttachLayerMatrix(*parentLayer) : nullptr; + + auto layerMatrix = AttachMatrix(jlayer["ks"], fCtx, std::move(parentMatrix)); + fLayerMatrixCache.insert(std::make_pair(&jlayer, layerMatrix)); + + return layerMatrix; + } +}; + +sk_sp AttachLayer(const Json::Value& jlayer, + AttachLayerContext* layerCtx) { + if (!jlayer.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(jlayer["ty"], -1); + if (type < 0 || type >= SkTo(SK_ARRAY_COUNT(gLayerAttachers))) { + return nullptr; + } + + // Layer content. + auto layer = gLayerAttachers[type](jlayer, layerCtx->fCtx); + if (auto layerMatrix = layerCtx->AttachLayerMatrix(jlayer)) { + // Optional layer transform. + layer = sksg::Transform::Make(std::move(layer), std::move(layerMatrix)); + } + // Optional layer opacity. + layer = AttachOpacity(jlayer["ks"], layerCtx->fCtx, std::move(layer)); + + // TODO: we should also disable related/inactive animators. + class Activator final : public AnimatorBase { + public: + Activator(sk_sp controlNode, float in, float out) + : fControlNode(std::move(controlNode)) + , fIn(in) + , fOut(out) {} + + void tick(float t) override { + // Keep the layer fully transparent except for its [in..out] lifespan. + // (note: opacity == 0 disables rendering, while opacity == 1 is a noop) + fControlNode->setOpacity(t >= fIn && t <= fOut ? 1 : 0); + } + + private: + const sk_sp fControlNode; + const float fIn, + fOut; + }; + + auto layerControl = sksg::OpacityEffect::Make(std::move(layer)); + const auto in = ParseScalar(jlayer["ip"], 0), + out = ParseScalar(jlayer["op"], in); + + if (in >= out || ! layerControl) + return nullptr; + + layerCtx->fCtx->fAnimators.push_back(skstd::make_unique(layerControl, in, out)); + + if (ParseBool(jlayer["td"], false)) { + // This layer is a matte. We apply it as a mask to the next layer. + layerCtx->fCurrentMatte = std::move(layerControl); + return nullptr; + } + + if (layerCtx->fCurrentMatte) { + // There is a pending matte. Apply and reset. + return sksg::MaskEffect::Make(std::move(layerControl), std::move(layerCtx->fCurrentMatte)); + } + + return layerControl; +} + +sk_sp AttachComposition(const Json::Value& comp, AttachContext* ctx) { + if (!comp.isObject()) + return nullptr; + + const auto& jlayers = comp["layers"]; + if (!jlayers.isArray()) + return nullptr; + + SkSTArray<16, sk_sp, true> layers; + AttachLayerContext layerCtx(jlayers, ctx); + + for (const auto& 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])); + } + + LOG("** Attached composition '%s': %d layers.\n", + ParseString(comp["id"], "").c_str(), layers.count()); + + return comp_group; +} + +} // namespace + +std::unique_ptr Animation::Make(SkStream* stream, const ResourceProvider& res) { + 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(res, std::move(version), size, fps, json)); +} + +std::unique_ptr Animation::MakeFromFile(const char path[], const ResourceProvider* res) { + class DirectoryResourceProvider final : public ResourceProvider { + public: + explicit DirectoryResourceProvider(SkString dir) : fDir(std::move(dir)) {} + + std::unique_ptr 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 defaultProvider; + if (!res) { + defaultProvider = skstd::make_unique(SkOSPath::Dirname(path)); + } + + return Make(jsonStream.get(), res ? *res : *defaultProvider); +} + +Animation::Animation(const ResourceProvider& resources, + 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 = { resources, assets, fAnimators }; + fDom = AttachComposition(json, &ctx); + + // In case the client calls render before the first tick. + this->animationTick(0); + + LOG("** Attached %d animators\n", fAnimators.count()); +} + +Animation::~Animation() = default; + +void Animation::render(SkCanvas* canvas, const SkRect* dstR) const { + if (!fDom) + return; + + sksg::InvalidationController ic; + fDom->revalidate(&ic, SkMatrix::I()); + + // TODO: proper inval + 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); + 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 skottie diff --git a/experimental/skottie/Skottie.h b/experimental/skottie/Skottie.h new file mode 100644 index 0000000000..e13bd0ae98 --- /dev/null +++ b/experimental/skottie/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 "SkTArray.h" +#include "SkTHash.h" +#include "SkTypes.h" + +#include + +class SkCanvas; +struct SkRect; +class SkStream; + +namespace Json { class Value; } + +namespace sksg { class RenderNode; } + +namespace skottie { + +class AnimatorBase; + +class ResourceProvider : public SkNoncopyable { +public: + virtual ~ResourceProvider() = default; + + virtual std::unique_ptr openStream(const char resource[]) const = 0; +}; + +class Animation : public SkNoncopyable { +public: + static std::unique_ptr Make(SkStream*, const ResourceProvider&); + static std::unique_ptr MakeFromFile(const char path[], + const ResourceProvider* = nullptr); + + ~Animation(); + + 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) { fShowInval = show; } + +private: + Animation(const ResourceProvider&, + 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 skottie + +#endif // Skottie_DEFINED diff --git a/experimental/skottie/SkottieAnimator.cpp b/experimental/skottie/SkottieAnimator.cpp new file mode 100644 index 0000000000..296a96b1fd --- /dev/null +++ b/experimental/skottie/SkottieAnimator.cpp @@ -0,0 +1,94 @@ +/* + * 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" + +namespace skottie { + +namespace { + +SkScalar lerp_scalar(float v0, float v1, float t) { + SkASSERT(t >= 0 && t <= 1); + return v0 * (1 - t) + v1 * t; +} + +} // namespace + +bool KeyframeIntervalBase::parse(const Json::Value& k, KeyframeIntervalBase* prev) { + SkASSERT(k.isObject()); + + fT0 = fT1 = ParseScalar(k["t"], SK_ScalarMin); + if (fT0 == SK_ScalarMin) { + return false; + } + + if (prev) { + if (prev->fT1 >= fT0) { + LOG("!! Dropping out-of-order key frame (t: %f < t: %f)\n", fT0, prev->fT1); + return false; + } + // 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). + prev->fT1 = fT0; + } + + // default is linear lerp + static constexpr SkPoint kDefaultC0 = { 0, 0 }, + kDefaultC1 = { 1, 1 }; + const auto c0 = ParsePoint(k["i"], kDefaultC0), + c1 = ParsePoint(k["o"], kDefaultC1); + + if (c0 != kDefaultC0 || c1 != kDefaultC1) { + fCubicMap = skstd::make_unique(); + // TODO: why do we have to plug these inverted? + fCubicMap->setPts(c1, c0); + } + + return true; +} + +float KeyframeIntervalBase::localT(float t) const { + SkASSERT(this->isValid()); + auto lt = (t - fT0) / (fT1 - fT0); + + if (fCubicMap) { + lt = fCubicMap->computeYFromX(lt); + } + + return SkTPin(lt, 0, 1); +} + +template <> +void KeyframeInterval::lerp(float t, ScalarValue* v) const { + const auto lt = this->localT(t); + *v = lerp_scalar(fV0, fV1, lt); +} + +template <> +void KeyframeInterval::lerp(float t, VectorValue* v) const { + SkASSERT(fV0.size() == fV1.size()); + SkASSERT(v->size() == 0); + + const auto lt = this->localT(t); + + v->reserve(fV0.size()); + for (size_t i = 0; i < fV0.size(); ++i) { + v->push_back(lerp_scalar(fV0[i], fV1[i], lt)); + } +} + +template <> +void KeyframeInterval::lerp(float t, ShapeValue* v) const { + SkASSERT(fV0.countVerbs() == fV1.countVerbs()); + SkASSERT(v->isEmpty()); + + const auto lt = this->localT(t); + SkAssertResult(fV1.interpolate(fV0, lt, v)); + v->setIsVolatile(true); +} + +} // namespace skottie diff --git a/experimental/skottie/SkottieAnimator.h b/experimental/skottie/SkottieAnimator.h new file mode 100644 index 0000000000..4e63e12833 --- /dev/null +++ b/experimental/skottie/SkottieAnimator.h @@ -0,0 +1,193 @@ +/* + * 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 "SkCubicMap.h" +#include "SkMakeUnique.h" +#include "SkottiePriv.h" +#include "SkottieProperties.h" +#include "SkTypes.h" + +#include +#include + +namespace skottie { + +class AnimatorBase : public SkNoncopyable { +public: + virtual ~AnimatorBase() = default; + + virtual void tick(float) = 0; + +protected: + AnimatorBase() = default; +}; + +class KeyframeIntervalBase : public SkNoncopyable { +public: + KeyframeIntervalBase() = default; + KeyframeIntervalBase(KeyframeIntervalBase&&) = default; + KeyframeIntervalBase& operator=(KeyframeIntervalBase&&) = default; + + float t0() const { return fT0; } + float t1() const { return fT1; } + + bool isValid() const { return fT0 < fT1; } + bool contains(float t) const { return t >= fT0 && t <= fT1; } + +protected: + // Parse the current interval AND back-fill prev interval t1. + bool parse(const Json::Value&, KeyframeIntervalBase* prev); + + // Computes a "local" t (relative to [fT0..fT1]), and mapped + // through the cubic (if applicable). + float localT(float t) const; + +private: + // Initialized for non-linear interpolation. + std::unique_ptr fCubicMap; + + // Start/end times. + float fT0 = 0, + fT1 = 0; +}; + +// Describes a keyframe interpolation interval (v0@t0) -> (v1@t1). +template +class KeyframeInterval final : public KeyframeIntervalBase { +public: + bool parse(const Json::Value& k, KeyframeInterval* prev) { + SkASSERT(k.isObject()); + + return this->INHERITED::parse(k, prev) && + ValueTraits::Parse(k["s"], &fV0) && + ValueTraits::Parse(k["e"], &fV1) && + ValueTraits::Cardinality(fV0) == ValueTraits::Cardinality(fV1) && + (!prev || ValueTraits::Cardinality(fV0) == ValueTraits::Cardinality(prev->fV0)); + } + + void lerp(float t, T*) const; + +private: + // Start/end values. + T fV0, + fV1; + + using INHERITED = KeyframeIntervalBase; +}; + +template +std::vector> ParseFrames(const Json::Value& jframes) { + std::vector> frames; + + if (jframes.isArray()) { + frames.reserve(jframes.size()); + + KeyframeInterval* prev_frame = nullptr; + for (const auto& jframe : jframes) { + if (!jframe.isObject()) + continue; + + KeyframeInterval frame; + if (frame.parse(jframe, prev_frame)) { + frames.push_back(std::move(frame)); + prev_frame = &frames.back(); + } + } + } + + // If we couldn't determine a t1 for the last frame, discard it. + if (!frames.empty() && !frames.back().isValid()) { + frames.pop_back(); + } + + return frames; +} + +// Binds an animated/keyframed property to a node attribute setter. +template +class Animator : public AnimatorBase { +public: + using ApplyFuncT = void(*)(NodeT*, const ValT&); + static std::unique_ptr Make(std::vector>&& frames, + sk_sp node, + ApplyFuncT&& applyFunc) { + return (node && !frames.empty()) + ? std::unique_ptr(new Animator(std::move(frames), + std::move(node), + std::move(applyFunc))) + : nullptr; + } + + void tick(float t) override { + const auto& frame = this->findFrame(t); + + ValT val; + frame.lerp(t, &val); + + fFunc(fTarget.get(), val); + } + +private: + Animator(std::vector>&& frames, sk_sp node, + ApplyFuncT&& applyFunc) + : fFrames(std::move(frames)) + , fTarget(std::move(node)) + , fFunc(std::move(applyFunc)) {} + + const KeyframeInterval& findFrame(float t) const; + + const std::vector> fFrames; + sk_sp fTarget; + ApplyFuncT fFunc; +}; + +template +const KeyframeInterval& Animator::findFrame(float t) const { + SkASSERT(!fFrames.empty()); + + // TODO: cache last/current frame? + + auto f0 = fFrames.begin(), + f1 = fFrames.end() - 1; + + 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; +} + +} // namespace skottie + +#endif // SkottieAnimator_DEFINED diff --git a/experimental/skottie/SkottiePriv.h b/experimental/skottie/SkottiePriv.h new file mode 100644 index 0000000000..0d047c5d37 --- /dev/null +++ b/experimental/skottie/SkottiePriv.h @@ -0,0 +1,49 @@ +/* + * 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 SkottiePriv_DEFINED +#define SkottiePriv_DEFINED + +#include "SkJSONCPP.h" +#include "SkPoint.h" +#include "SkScalar.h" +#include "SkString.h" + +namespace skottie { + +#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; +} + +static inline SkPoint ParsePoint(const Json::Value& v, const SkPoint& defaultValue) { + return v.isObject() + ? SkPoint::Make(ParseScalar(v["x"], defaultValue.x()), + ParseScalar(v["y"], defaultValue.y())) + : defaultValue; +} + +} // namespace + +#endif // SkottiePriv_DEFINED diff --git a/experimental/skottie/SkottieProperties.cpp b/experimental/skottie/SkottieProperties.cpp new file mode 100644 index 0000000000..b9535c428c --- /dev/null +++ b/experimental/skottie/SkottieProperties.cpp @@ -0,0 +1,290 @@ +/* + * 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 "SkottieProperties.h" + +#include "SkColor.h" +#include "SkottiePriv.h" +#include "SkPath.h" +#include "SkSGColor.h" +#include "SkSGGradient.h" +#include "SkSGPath.h" +#include "SkSGRect.h" +#include "SkSGTransform.h" + +#include + +namespace skottie { + +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; +} + +SkColor VecToColor(const float* v, size_t size) { + // best effort to turn this into a color + const auto r = size > 0 ? v[0] : 0, + g = size > 1 ? v[1] : 0, + b = size > 2 ? v[2] : 0, + a = size > 3 ? v[3] : 1; + + return SkColorSetARGB(SkTPin(a, 0, 1) * 255, + SkTPin(r, 0, 1) * 255, + SkTPin(g, 0, 1) * 255, + SkTPin(b, 0, 1) * 255); +} + +} // namespace + +template <> +bool ValueTraits::Parse(const Json::Value& v, ScalarValue* scalar) { + // Some files appear to wrap keyframes in arrays for no reason. + if (v.isArray() && v.size() == 1) { + return Parse(v[0], scalar); + } + + if (v.isNull() || !v.isConvertibleTo(Json::realValue)) + return false; + + *scalar = v.asFloat(); + return true; +} + +template <> +size_t ValueTraits::Cardinality(const ScalarValue&) { + return 1; +} + +template <> +template <> +SkScalar ValueTraits::As(const ScalarValue& v) { + return v; +} + +template <> +bool ValueTraits::Parse(const Json::Value& v, VectorValue* vec) { + SkASSERT(vec->empty()); + + if (!v.isArray()) + return false; + + for (Json::ArrayIndex i = 0; i < v.size(); ++i) { + ScalarValue scalar; + if (!ValueTraits::Parse(v[i], &scalar)) + return false; + + vec->push_back(std::move(scalar)); + } + + return true; +} + +template <> +size_t ValueTraits::Cardinality(const VectorValue& vec) { + return vec.size(); +} + +template <> +template <> +SkColor ValueTraits::As(const VectorValue& vec) { + return VecToColor(vec.data(), vec.size()); +} + +template <> +template <> +SkPoint ValueTraits::As(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::As(const VectorValue& vec) { + const auto pt = ValueTraits::As(vec); + return SkSize::Make(pt.x(), pt.y()); +} + +template<> +bool ValueTraits::Parse(const Json::Value& v, ShapeValue* shape) { + PointArray inPts, // Cubic Bezier "in" control points, relative to vertices. + outPts, // Cubic Bezier "out" control points, relative to vertices. + verts; // Cubic Bezier vertices. + + // Some files appear to wrap keyframes in arrays for no reason. + if (v.isArray() && v.size() == 1) { + 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->isEmpty()); + + if (!verts.empty()) { + shape->moveTo(verts.front()); + } + + const auto& addCubic = [&](int from, int to) { + shape->cubicTo(verts[from] + outPts[from], + verts[to] + inPts[to], + verts[to]); + }; + + for (int i = 1; i < verts.count(); ++i) { + addCubic(i - 1, i); + } + + if (!verts.empty() && ParseBool(v["c"], false)) { + addCubic(verts.count() - 1, 0); + shape->close(); + } + + return true; +} + +template <> +size_t ValueTraits::Cardinality(const ShapeValue& path) { + return SkTo(path.countVerbs()); +} + +template <> +template <> +SkPath ValueTraits::As(const ShapeValue& path) { + return path; +} + +CompositeRRect::CompositeRRect(sk_sp wrapped_node) + : fRRectNode(std::move(wrapped_node)) {} + +void CompositeRRect::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); +} + +CompositeTransform::CompositeTransform(sk_sp matrix) + : fMatrixNode(std::move(matrix)) {} + +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 + + fMatrixNode->setMatrix(t); +} + +CompositePolyStar::CompositePolyStar(sk_sp wrapped_node, Type t) + : fPathNode(std::move(wrapped_node)) + , fType(t) {} + +void CompositePolyStar::apply() { + const auto count = SkScalarTruncToInt(fPointCount); + const auto arc = 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)); + + for (int 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); +} + +CompositeGradient::CompositeGradient(sk_sp grad, size_t stopCount) + : fGradient(std::move(grad)) + , fStopCount(stopCount) {} + +void CompositeGradient::apply() { + this->onApply(); + + // |fColorStops| holds |fStopCount| x [ pos, r, g, g ] + ? x [ pos, alpha ] + + if (fColorStops.size() < fStopCount * 4 || ((fColorStops.size() - fStopCount * 4) % 2)) { + LOG("!! Invalid gradient stop array size: %zu", fColorStops.size()); + return; + } + + std::vector stops; + + // TODO: merge/lerp opacity stops + const auto csEnd = fColorStops.cbegin() + fStopCount * 4; + for (auto cs = fColorStops.cbegin(); cs != csEnd; cs += 4) { + stops.push_back({ *cs, VecToColor(&*(cs + 1), 3) }); + } + + fGradient->setColorStops(std::move(stops)); +} + +CompositeLinearGradient::CompositeLinearGradient(sk_sp grad, size_t stopCount) + : INHERITED(std::move(grad), stopCount) {} + +void CompositeLinearGradient::onApply() { + auto* grad = static_cast(fGradient.get()); + grad->setStartPoint(this->startPoint()); + grad->setEndPoint(this->endPoint()); +} + +CompositeRadialGradient::CompositeRadialGradient(sk_sp grad, size_t stopCount) + : INHERITED(std::move(grad), stopCount) {} + +void CompositeRadialGradient::onApply() { + auto* grad = static_cast(fGradient.get()); + grad->setStartCenter(this->startPoint()); + grad->setEndCenter(this->startPoint()); + grad->setStartRadius(0); + grad->setEndRadius(SkPoint::Distance(this->startPoint(), this->endPoint())); +} + +} // namespace skottie diff --git a/experimental/skottie/SkottieProperties.h b/experimental/skottie/SkottieProperties.h new file mode 100644 index 0000000000..c5e14d14cb --- /dev/null +++ b/experimental/skottie/SkottieProperties.h @@ -0,0 +1,168 @@ +/* + * 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 SkottieProperties_DEFINED +#define SkottieProperties_DEFINED + +#include "SkColor.h" +#include "SkPath.h" +#include "SkPoint.h" +#include "SkSize.h" +#include "SkottiePriv.h" +#include "SkRefCnt.h" +#include "SkTArray.h" +#include "SkTypes.h" + +#include +#include + +namespace sksg { +class Color; +class Gradient; +class LinearGradient; +class Matrix; +class Path; +class RadialGradient; +class RRect; +class RenderNode;; +} + +namespace skottie { + +template +struct ValueTraits { + static bool Parse(const Json::Value&, T*); + static size_t Cardinality(const T&); + + template + static U As(const T&); +}; + +using ScalarValue = SkScalar; +using VectorValue = std::vector; +using ShapeValue = SkPath; + +// 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 CompositeRRect final : public SkRefCnt { +public: + explicit CompositeRRect(sk_sp); + + COMPOSITE_PROPERTY(Position, SkPoint , SkPoint::Make(0, 0)) + COMPOSITE_PROPERTY(Size , SkSize , SkSize::Make(0, 0)) + COMPOSITE_PROPERTY(Radius , SkSize , SkSize::Make(0, 0)) + +private: + void apply(); + + sk_sp fRRectNode; + + using INHERITED = SkRefCnt; +}; + +class CompositePolyStar final : public SkRefCnt { +public: + enum class Type { + kStar, kPoly, + }; + + CompositePolyStar(sk_sp, Type); + + COMPOSITE_PROPERTY(Position , SkPoint , SkPoint::Make(0, 0)) + COMPOSITE_PROPERTY(PointCount , SkScalar, 0) + COMPOSITE_PROPERTY(InnerRadius , SkScalar, 0) + COMPOSITE_PROPERTY(OuterRadius , SkScalar, 0) + COMPOSITE_PROPERTY(InnerRoundness, SkScalar, 0) + COMPOSITE_PROPERTY(OuterRoundness, SkScalar, 0) + COMPOSITE_PROPERTY(Rotation , SkScalar, 0) + +private: + void apply(); + + sk_sp fPathNode; + Type fType; + + using INHERITED = SkRefCnt; +}; + +class CompositeTransform final : public SkRefCnt { +public: + explicit CompositeTransform(sk_sp); + + 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 fMatrixNode; + + using INHERITED = SkRefCnt; +}; + +class CompositeGradient : public SkRefCnt { +public: + COMPOSITE_PROPERTY(StartPoint, SkPoint , SkPoint::Make(0, 0) ) + COMPOSITE_PROPERTY(EndPoint , SkPoint , SkPoint::Make(0, 0) ) + COMPOSITE_PROPERTY(ColorStops, std::vector, std::vector()) + +protected: + CompositeGradient(sk_sp, size_t stopCount); + + const SkPoint& startPoint() const { return fStartPoint; } + const SkPoint& endPoint() const { return fEndPoint; } + + sk_sp fGradient; + size_t fStopCount; + + virtual void onApply() = 0; + +private: + void apply(); + + using INHERITED = SkRefCnt; +}; + +class CompositeLinearGradient final : public CompositeGradient { +public: + CompositeLinearGradient(sk_sp, size_t stopCount); + +private: + void onApply() override; + + using INHERITED = CompositeGradient; +}; + +class CompositeRadialGradient final : public CompositeGradient { +public: + CompositeRadialGradient(sk_sp, size_t stopCount); + +private: + void onApply() override; + + using INHERITED = CompositeGradient; +}; + +#undef COMPOSITE_PROPERTY + +} // namespace skottie + +#endif // SkottieProperties_DEFINED diff --git a/experimental/skotty/Skotty.cpp b/experimental/skotty/Skotty.cpp deleted file mode 100644 index 72d12d6ac4..0000000000 --- a/experimental/skotty/Skotty.cpp +++ /dev/null @@ -1,1051 +0,0 @@ -/* - * 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 "SkImage.h" -#include "SkMakeUnique.h" -#include "SkOSPath.h" -#include "SkPaint.h" -#include "SkParse.h" -#include "SkPath.h" -#include "SkPoint.h" -#include "SkSGColor.h" -#include "SkSGDraw.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 "SkSGTransform.h" -#include "SkSGTrimEffect.h" -#include "SkStream.h" -#include "SkTArray.h" -#include "SkTHash.h" - -#include -#include -#include - -#include "stdlib.h" - -namespace skotty { - -namespace { - -using AssetMap = SkTHashMap; - -struct AttachContext { - const ResourceProvider& fResources; - 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 BindProperty(const Json::Value& jprop, AttachContext* ctx, const sk_sp& node, - typename Animator::ApplyFuncT&& apply) { - 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.isNull() || !ParseBool(jpropA, "false")) { - ValT val; - if (ValueTraits::Parse(jpropK, &val)) { - // Static property. - apply(node.get(), val); - return true; - } - - if (!jpropA.isNull()) { - return LogFail(jprop, "Could not parse (explicit) static property"); - } - } - - // Keyframe property. - using AnimatorT = Animator; - auto animator = AnimatorT::Make(ParseFrames(jpropK), node, std::move(apply)); - - if (!animator) { - return LogFail(jprop, "Could not parse keyframed property"); - } - - ctx->fAnimators.push_back(std::move(animator)); - - return true; -} - -sk_sp AttachMatrix(const Json::Value& t, AttachContext* ctx, - sk_sp parentMatrix) { - if (!t.isObject()) - return nullptr; - - auto matrix = sksg::Matrix::Make(SkMatrix::I(), std::move(parentMatrix)); - auto composite = sk_make_sp(matrix); - auto anchor_attached = BindProperty(t["a"], ctx, composite, - [](CompositeTransform* node, const VectorValue& a) { - node->setAnchorPoint(ValueTraits::As(a)); - }); - auto position_attached = BindProperty(t["p"], ctx, composite, - [](CompositeTransform* node, const VectorValue& p) { - node->setPosition(ValueTraits::As(p)); - }); - auto scale_attached = BindProperty(t["s"], ctx, composite, - [](CompositeTransform* node, const VectorValue& s) { - node->setScale(ValueTraits::As(s)); - }); - auto rotation_attached = BindProperty(t["r"], ctx, composite, - [](CompositeTransform* node, const ScalarValue& r) { - node->setRotation(r); - }); - auto skew_attached = BindProperty(t["sk"], ctx, composite, - [](CompositeTransform* node, const ScalarValue& sk) { - node->setSkew(sk); - }); - auto skewaxis_attached = BindProperty(t["sa"], ctx, composite, - [](CompositeTransform* node, const ScalarValue& sa) { - node->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 AttachOpacity(const Json::Value& jtransform, AttachContext* ctx, - sk_sp childNode) { - if (!jtransform.isObject() || !childNode) - return childNode; - - // This is more peeky than other attachers, because we want to avoid redundant opacity - // nodes for the extremely common case of static opaciy == 100. - const auto& opacity = jtransform["o"]; - if (opacity.isObject() && - !ParseBool(opacity["a"], true) && - ParseScalar(opacity["k"], -1) == 100) { - // Ignoring static full opacity. - return childNode; - } - - auto opacityNode = sksg::OpacityEffect::Make(childNode); - BindProperty(opacity, ctx, opacityNode, - [](sksg::OpacityEffect* node, const ScalarValue& o) { - // BM opacity is [0..100] - node->setOpacity(o * 0.01f); - }); - - return opacityNode; -} - -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 = BindProperty(jpath["ks"], ctx, path_node, - [](sksg::Path* node, const ShapeValue& 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 AttachRRectGeometry(const Json::Value& jrect, AttachContext* ctx) { - SkASSERT(jrect.isObject()); - - auto rect_node = sksg::RRect::Make(); - auto composite = sk_make_sp(rect_node); - - auto p_attached = BindProperty(jrect["p"], ctx, composite, - [](CompositeRRect* node, const VectorValue& p) { - node->setPosition(ValueTraits::As(p)); - }); - auto s_attached = BindProperty(jrect["s"], ctx, composite, - [](CompositeRRect* node, const VectorValue& s) { - node->setSize(ValueTraits::As(s)); - }); - auto r_attached = BindProperty(jrect["r"], ctx, composite, - [](CompositeRRect* node, const ScalarValue& r) { - node->setRadius(SkSize::Make(r, r)); - }); - - if (!p_attached && !s_attached && !r_attached) { - return nullptr; - } - - LOG("** Attached (r)rect geometry\n"); - - return rect_node; -} - -sk_sp AttachEllipseGeometry(const Json::Value& jellipse, AttachContext* ctx) { - SkASSERT(jellipse.isObject()); - - auto rect_node = sksg::RRect::Make(); - auto composite = sk_make_sp(rect_node); - - auto p_attached = BindProperty(jellipse["p"], ctx, composite, - [](CompositeRRect* node, const VectorValue& p) { - node->setPosition(ValueTraits::As(p)); - }); - auto s_attached = BindProperty(jellipse["s"], ctx, composite, - [](CompositeRRect* node, const VectorValue& s) { - const auto sz = ValueTraits::As(s); - node->setSize(sz); - node->setRadius(SkSize::Make(sz.width() / 2, sz.height() / 2)); - }); - - if (!p_attached && !s_attached) { - return nullptr; - } - - LOG("** Attached ellipse geometry\n"); - - return rect_node; -} - -sk_sp AttachPolystarGeometry(const Json::Value& jstar, AttachContext* ctx) { - SkASSERT(jstar.isObject()); - - static constexpr CompositePolyStar::Type gTypes[] = { - CompositePolyStar::Type::kStar, // "sy": 1 - CompositePolyStar::Type::kPoly, // "sy": 2 - }; - - const auto type = ParseInt(jstar["sy"], 0) - 1; - if (type < 0 || type >= SkTo(SK_ARRAY_COUNT(gTypes))) { - LogFail(jstar, "Unknown polystar type"); - return nullptr; - } - - auto path_node = sksg::Path::Make(); - auto composite = sk_make_sp(path_node, gTypes[type]); - - BindProperty(jstar["p"], ctx, composite, - [](CompositePolyStar* node, const VectorValue& p) { - node->setPosition(ValueTraits::As(p)); - }); - BindProperty(jstar["pt"], ctx, composite, - [](CompositePolyStar* node, const ScalarValue& pt) { - node->setPointCount(pt); - }); - BindProperty(jstar["ir"], ctx, composite, - [](CompositePolyStar* node, const ScalarValue& ir) { - node->setInnerRadius(ir); - }); - BindProperty(jstar["or"], ctx, composite, - [](CompositePolyStar* node, const ScalarValue& otr) { - node->setOuterRadius(otr); - }); - BindProperty(jstar["is"], ctx, composite, - [](CompositePolyStar* node, const ScalarValue& is) { - node->setInnerRoundness(is); - }); - BindProperty(jstar["os"], ctx, composite, - [](CompositePolyStar* node, const ScalarValue& os) { - node->setOuterRoundness(os); - }); - BindProperty(jstar["r"], ctx, composite, - [](CompositePolyStar* node, const ScalarValue& r) { - node->setRotation(r); - }); - - return path_node; -} - -sk_sp AttachColor(const Json::Value& obj, AttachContext* ctx) { - SkASSERT(obj.isObject()); - - auto color_node = sksg::Color::Make(SK_ColorBLACK); - auto color_attached = BindProperty(obj["c"], ctx, color_node, - [](sksg::Color* node, const VectorValue& c) { - node->setColor(ValueTraits::As(c)); - }); - - return color_attached ? color_node : nullptr; -} - -sk_sp AttachGradient(const Json::Value& obj, AttachContext* ctx) { - SkASSERT(obj.isObject()); - - const auto& stops = obj["g"]; - if (!stops.isObject()) - return nullptr; - - const auto stopCount = ParseInt(stops["p"], -1); - if (stopCount < 0) - return nullptr; - - sk_sp gradient_node; - sk_sp composite; - - if (ParseInt(obj["t"], 1) == 1) { - auto linear_node = sksg::LinearGradient::Make(); - composite = sk_make_sp(linear_node, stopCount); - gradient_node = std::move(linear_node); - } else { - auto radial_node = sksg::RadialGradient::Make(); - composite = sk_make_sp(radial_node, stopCount); - - // TODO: highlight, angle - gradient_node = std::move(radial_node); - } - - BindProperty(stops["k"], ctx, composite, - [](CompositeGradient* node, const VectorValue& stops) { - node->setColorStops(stops); - }); - BindProperty(obj["s"], ctx, composite, - [](CompositeGradient* node, const VectorValue& s) { - node->setStartPoint(ValueTraits::As(s)); - }); - BindProperty(obj["e"], ctx, composite, - [](CompositeGradient* node, const VectorValue& e) { - node->setEndPoint(ValueTraits::As(e)); - }); - - return gradient_node; -} - -sk_sp AttachPaint(const Json::Value& jpaint, AttachContext* ctx, - sk_sp paint_node) { - if (paint_node) { - paint_node->setAntiAlias(true); - - BindProperty(jpaint["o"], ctx, paint_node, - [](sksg::PaintNode* node, const ScalarValue& o) { - // BM opacity is [0..100] - node->setOpacity(o * 0.01f); - }); - } - - return paint_node; -} - -sk_sp AttachStroke(const Json::Value& jstroke, AttachContext* ctx, - sk_sp stroke_node) { - SkASSERT(jstroke.isObject()); - - if (!stroke_node) - return nullptr; - - stroke_node->setStyle(SkPaint::kStroke_Style); - - auto width_attached = BindProperty(jstroke["w"], ctx, stroke_node, - [](sksg::PaintNode* node, const ScalarValue& w) { - node->setStrokeWidth(w); - }); - 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; -} - -sk_sp AttachColorFill(const Json::Value& jfill, AttachContext* ctx) { - SkASSERT(jfill.isObject()); - - return AttachPaint(jfill, ctx, AttachColor(jfill, ctx)); -} - -sk_sp AttachGradientFill(const Json::Value& jfill, AttachContext* ctx) { - SkASSERT(jfill.isObject()); - - return AttachPaint(jfill, ctx, AttachGradient(jfill, ctx)); -} - -sk_sp AttachColorStroke(const Json::Value& jstroke, AttachContext* ctx) { - SkASSERT(jstroke.isObject()); - - return AttachStroke(jstroke, ctx, AttachPaint(jstroke, ctx, AttachColor(jstroke, ctx))); -} - -sk_sp AttachGradientStroke(const Json::Value& jstroke, AttachContext* ctx) { - SkASSERT(jstroke.isObject()); - - return AttachStroke(jstroke, ctx, AttachPaint(jstroke, ctx, AttachGradient(jstroke, ctx))); -} - -std::vector> AttachMergeGeometryEffect( - const Json::Value& jmerge, AttachContext* ctx, std::vector>&& geos) { - std::vector> 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(ParseInt(jmerge["mm"], 1) - 1, - 0, SK_ARRAY_COUNT(gModes) - 1)]; - merged.push_back(sksg::Merge::Make(std::move(geos), mode)); - - LOG("** Attached merge path effect, mode: %d\n", mode); - - return merged; -} - -std::vector> AttachTrimGeometryEffect( - const Json::Value& jtrim, AttachContext* ctx, std::vector>&& geos) { - - enum class Mode { - kMerged, // "m": 1 - kSeparate, // "m": 2 - } gModes[] = { Mode::kMerged, Mode::kSeparate }; - - const auto mode = gModes[SkTPin(ParseInt(jtrim["m"], 1) - 1, - 0, SK_ARRAY_COUNT(gModes) - 1)]; - - std::vector> 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> trimmed; - trimmed.reserve(inputs.size()); - for (const auto& i : inputs) { - const auto trim = sksg::TrimEffect::Make(i); - trimmed.push_back(trim); - BindProperty(jtrim["s"], ctx, trim, - [](sksg::TrimEffect* node, const ScalarValue& s) { - node->setStart(s * 0.01f); - }); - BindProperty(jtrim["e"], ctx, trim, - [](sksg::TrimEffect* node, const ScalarValue& e) { - node->setEnd(e * 0.01f); - }); - // TODO: "offset" doesn't currently work the same as BM - figure out what's going on. - BindProperty(jtrim["o"], ctx, trim, - [](sksg::TrimEffect* node, const ScalarValue& o) { - node->setOffset(o * 0.01f); - }); - } - - return trimmed; -} - -using GeometryAttacherT = sk_sp (*)(const Json::Value&, AttachContext*); -static constexpr GeometryAttacherT gGeometryAttachers[] = { - AttachPathGeometry, - AttachRRectGeometry, - AttachEllipseGeometry, - AttachPolystarGeometry, -}; - -using PaintAttacherT = sk_sp (*)(const Json::Value&, AttachContext*); -static constexpr PaintAttacherT gPaintAttachers[] = { - AttachColorFill, - AttachColorStroke, - AttachGradientFill, - AttachGradientStroke, -}; - -using GroupAttacherT = sk_sp (*)(const Json::Value&, AttachContext*); -static constexpr GroupAttacherT gGroupAttachers[] = { - AttachShapeGroup, -}; - -using GeometryEffectAttacherT = - std::vector> (*)(const Json::Value&, - AttachContext*, - std::vector>&&); -static constexpr GeometryEffectAttacherT gGeometryEffectAttachers[] = { - AttachMergeGeometryEffect, - AttachTrimGeometryEffect, -}; - -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::Value& 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 -> AttachShapeGroup - { "gs", ShapeType::kPaint , 3 }, // gstroke -> AttachGradientStroke - { "mm", ShapeType::kGeometryEffect, 0 }, // merge -> AttachMergeGeometryEffect - { "rc", ShapeType::kGeometry , 1 }, // rrect -> AttachRRectGeometry - { "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 -> In-place handler - }; - - 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; - - // (https://helpx.adobe.com/after-effects/using/overview-shape-layers-paths-vector.html#groups_and_render_order_for_shapes_and_shape_attributes) - // - // Render order for shapes within a shape layer - // - // The rules for rendering a shape layer are similar to the rules for rendering a composition - // that contains nested compositions: - // - // * Within a group, the shape at the bottom of the Timeline panel stacking order is rendered - // first. - // - // * All path operations within a group are performed before paint operations. This means, - // for example, that the stroke follows the distortions in the path made by the Wiggle Paths - // path operation. Path operations within a group are performed from top to bottom. - // - // * Paint operations within a group are performed from the bottom to the top in the Timeline - // panel stacking order. This means, for example, that a stroke is rendered on top of - // (in front of) a stroke that appears after it in the Timeline panel. - // - sk_sp shape_group = sksg::Group::Make(); - sk_sp xformed_group = shape_group; - - std::vector> geos; - std::vector> draws; - - 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::kGeometryEffect: { - SkASSERT(info->fAttacherIndex < SK_ARRAY_COUNT(gGeometryEffectAttachers)); - geos = gGeometryEffectAttachers[info->fAttacherIndex](s, ctx, std::move(geos)); - } break; - case ShapeType::kPaint: { - SkASSERT(info->fAttacherIndex < SK_ARRAY_COUNT(gPaintAttachers)); - if (auto paint = gPaintAttachers[info->fAttacherIndex](s, ctx)) { - for (const auto& geo : geos) { - draws.push_back(sksg::Draw::Make(geo, paint)); - } - } - } break; - case ShapeType::kGroup: { - SkASSERT(info->fAttacherIndex < SK_ARRAY_COUNT(gGroupAttachers)); - if (auto group = gGroupAttachers[info->fAttacherIndex](s, ctx)) { - draws.push_back(std::move(group)); - } - } break; - case ShapeType::kTransform: { - // TODO: BM appears to transform the geometry, not the draw op itself. - if (auto matrix = AttachMatrix(s, ctx, nullptr)) { - xformed_group = sksg::Transform::Make(std::move(xformed_group), - std::move(matrix)); - } - xformed_group = AttachOpacity(s, ctx, std::move(xformed_group)); - } break; - } - } - - if (draws.empty()) { - return nullptr; - } - - for (auto draw = draws.rbegin(); draw != draws.rend(); ++draw) { - shape_group->addChild(std::move(*draw)); - } - - LOG("** Attached shape: %zd draws.\n", draws.size()); - return xformed_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& jlayer, AttachContext*) { - SkASSERT(jlayer.isObject()); - - const auto size = SkSize::Make(ParseScalar(jlayer["sw"], -1), - ParseScalar(jlayer["sh"], -1)); - const auto hex = ParseString(jlayer["sc"], ""); - 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 AttachImageAsset(const Json::Value& jimage, AttachContext* ctx) { - SkASSERT(jimage.isObject()); - - const auto name = ParseString(jimage["p"], ""), - path = ParseString(jimage["u"], ""); - 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 AttachImageLayer(const Json::Value& layer, AttachContext* ctx) { - SkASSERT(layer.isObject()); - - auto refId = ParseString(layer["refId"], ""); - if (refId.isEmpty()) { - LOG("!! Image layer missing refId\n"); - return nullptr; - } - - const auto* jimage = ctx->fAssets.find(refId); - if (!jimage) { - LOG("!! Image asset not found: '%s'\n", refId.c_str()); - return nullptr; - } - - return AttachImageAsset(**jimage, ctx); -} - -sk_sp AttachNullLayer(const Json::Value& layer, AttachContext*) { - 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 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; -} - -struct AttachLayerContext { - AttachLayerContext(const Json::Value& jlayers, AttachContext* ctx) - : fLayerList(jlayers), fCtx(ctx) {} - - const Json::Value& fLayerList; - AttachContext* fCtx; - std::unordered_map> fLayerMatrixCache; - std::unordered_map fLayerIndexCache; - sk_sp fCurrentMatte; - - const Json::Value* findLayer(int index) { - SkASSERT(fLayerList.isArray()); - - if (index < 0) { - return nullptr; - } - - const auto cached = fLayerIndexCache.find(index); - if (cached != fLayerIndexCache.end()) { - return cached->second; - } - - for (const auto& l : fLayerList) { - if (!l.isObject()) { - continue; - } - - if (ParseInt(l["ind"], -1) == index) { - fLayerIndexCache.insert(std::make_pair(index, &l)); - return &l; - } - } - - return nullptr; - } - - sk_sp AttachLayerMatrix(const Json::Value& jlayer) { - SkASSERT(jlayer.isObject()); - - const auto cached = fLayerMatrixCache.find(&jlayer); - if (cached != fLayerMatrixCache.end()) { - return cached->second; - } - - const auto* parentLayer = this->findLayer(ParseInt(jlayer["parent"], -1)); - - // TODO: cycle detection? - auto parentMatrix = (parentLayer && parentLayer != &jlayer) - ? this->AttachLayerMatrix(*parentLayer) : nullptr; - - auto layerMatrix = AttachMatrix(jlayer["ks"], fCtx, std::move(parentMatrix)); - fLayerMatrixCache.insert(std::make_pair(&jlayer, layerMatrix)); - - return layerMatrix; - } -}; - -sk_sp AttachLayer(const Json::Value& jlayer, - AttachLayerContext* layerCtx) { - if (!jlayer.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(jlayer["ty"], -1); - if (type < 0 || type >= SkTo(SK_ARRAY_COUNT(gLayerAttachers))) { - return nullptr; - } - - // Layer content. - auto layer = gLayerAttachers[type](jlayer, layerCtx->fCtx); - if (auto layerMatrix = layerCtx->AttachLayerMatrix(jlayer)) { - // Optional layer transform. - layer = sksg::Transform::Make(std::move(layer), std::move(layerMatrix)); - } - // Optional layer opacity. - layer = AttachOpacity(jlayer["ks"], layerCtx->fCtx, std::move(layer)); - - // TODO: we should also disable related/inactive animators. - class Activator final : public AnimatorBase { - public: - Activator(sk_sp controlNode, float in, float out) - : fControlNode(std::move(controlNode)) - , fIn(in) - , fOut(out) {} - - void tick(float t) override { - // Keep the layer fully transparent except for its [in..out] lifespan. - // (note: opacity == 0 disables rendering, while opacity == 1 is a noop) - fControlNode->setOpacity(t >= fIn && t <= fOut ? 1 : 0); - } - - private: - const sk_sp fControlNode; - const float fIn, - fOut; - }; - - auto layerControl = sksg::OpacityEffect::Make(std::move(layer)); - const auto in = ParseScalar(jlayer["ip"], 0), - out = ParseScalar(jlayer["op"], in); - - if (in >= out || ! layerControl) - return nullptr; - - layerCtx->fCtx->fAnimators.push_back(skstd::make_unique(layerControl, in, out)); - - if (ParseBool(jlayer["td"], false)) { - // This layer is a matte. We apply it as a mask to the next layer. - layerCtx->fCurrentMatte = std::move(layerControl); - return nullptr; - } - - if (layerCtx->fCurrentMatte) { - // There is a pending matte. Apply and reset. - return sksg::MaskEffect::Make(std::move(layerControl), std::move(layerCtx->fCurrentMatte)); - } - - return layerControl; -} - -sk_sp AttachComposition(const Json::Value& comp, AttachContext* ctx) { - if (!comp.isObject()) - return nullptr; - - const auto& jlayers = comp["layers"]; - if (!jlayers.isArray()) - return nullptr; - - SkSTArray<16, sk_sp, true> layers; - AttachLayerContext layerCtx(jlayers, ctx); - - for (const auto& 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])); - } - - LOG("** Attached composition '%s': %d layers.\n", - ParseString(comp["id"], "").c_str(), layers.count()); - - return comp_group; -} - -} // namespace - -std::unique_ptr Animation::Make(SkStream* stream, const ResourceProvider& res) { - 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(res, std::move(version), size, fps, json)); -} - -std::unique_ptr Animation::MakeFromFile(const char path[], const ResourceProvider* res) { - class DirectoryResourceProvider final : public ResourceProvider { - public: - explicit DirectoryResourceProvider(SkString dir) : fDir(std::move(dir)) {} - - std::unique_ptr 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 defaultProvider; - if (!res) { - defaultProvider = skstd::make_unique(SkOSPath::Dirname(path)); - } - - return Make(jsonStream.get(), res ? *res : *defaultProvider); -} - -Animation::Animation(const ResourceProvider& resources, - 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 = { resources, assets, fAnimators }; - fDom = AttachComposition(json, &ctx); - - // In case the client calls render before the first tick. - this->animationTick(0); - - LOG("** Attached %d animators\n", fAnimators.count()); -} - -Animation::~Animation() = default; - -void Animation::render(SkCanvas* canvas, const SkRect* dstR) const { - if (!fDom) - return; - - sksg::InvalidationController ic; - fDom->revalidate(&ic, SkMatrix::I()); - - // TODO: proper inval - 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); - 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 deleted file mode 100644 index ebd103d60c..0000000000 --- a/experimental/skotty/Skotty.h +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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; -struct SkRect; -class SkStream; - -namespace Json { class Value; } - -namespace sksg { class RenderNode; } - -namespace skotty { - -class AnimatorBase; - -class ResourceProvider : public SkNoncopyable { -public: - virtual ~ResourceProvider() = default; - - virtual std::unique_ptr openStream(const char resource[]) const = 0; -}; - -class Animation : public SkNoncopyable { -public: - static std::unique_ptr Make(SkStream*, const ResourceProvider&); - static std::unique_ptr MakeFromFile(const char path[], - const ResourceProvider* = nullptr); - - ~Animation(); - - 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) { fShowInval = show; } - -private: - Animation(const ResourceProvider&, - 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 deleted file mode 100644 index 698e961b64..0000000000 --- a/experimental/skotty/SkottyAnimator.cpp +++ /dev/null @@ -1,94 +0,0 @@ -/* - * 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(float v0, float v1, float t) { - SkASSERT(t >= 0 && t <= 1); - return v0 * (1 - t) + v1 * t; -} - -} // namespace - -bool KeyframeIntervalBase::parse(const Json::Value& k, KeyframeIntervalBase* prev) { - SkASSERT(k.isObject()); - - fT0 = fT1 = ParseScalar(k["t"], SK_ScalarMin); - if (fT0 == SK_ScalarMin) { - return false; - } - - if (prev) { - if (prev->fT1 >= fT0) { - LOG("!! Dropping out-of-order key frame (t: %f < t: %f)\n", fT0, prev->fT1); - return false; - } - // 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). - prev->fT1 = fT0; - } - - // default is linear lerp - static constexpr SkPoint kDefaultC0 = { 0, 0 }, - kDefaultC1 = { 1, 1 }; - const auto c0 = ParsePoint(k["i"], kDefaultC0), - c1 = ParsePoint(k["o"], kDefaultC1); - - if (c0 != kDefaultC0 || c1 != kDefaultC1) { - fCubicMap = skstd::make_unique(); - // TODO: why do we have to plug these inverted? - fCubicMap->setPts(c1, c0); - } - - return true; -} - -float KeyframeIntervalBase::localT(float t) const { - SkASSERT(this->isValid()); - auto lt = (t - fT0) / (fT1 - fT0); - - if (fCubicMap) { - lt = fCubicMap->computeYFromX(lt); - } - - return SkTPin(lt, 0, 1); -} - -template <> -void KeyframeInterval::lerp(float t, ScalarValue* v) const { - const auto lt = this->localT(t); - *v = lerp_scalar(fV0, fV1, lt); -} - -template <> -void KeyframeInterval::lerp(float t, VectorValue* v) const { - SkASSERT(fV0.size() == fV1.size()); - SkASSERT(v->size() == 0); - - const auto lt = this->localT(t); - - v->reserve(fV0.size()); - for (size_t i = 0; i < fV0.size(); ++i) { - v->push_back(lerp_scalar(fV0[i], fV1[i], lt)); - } -} - -template <> -void KeyframeInterval::lerp(float t, ShapeValue* v) const { - SkASSERT(fV0.countVerbs() == fV1.countVerbs()); - SkASSERT(v->isEmpty()); - - const auto lt = this->localT(t); - SkAssertResult(fV1.interpolate(fV0, lt, v)); - v->setIsVolatile(true); -} - -} // namespace skotty diff --git a/experimental/skotty/SkottyAnimator.h b/experimental/skotty/SkottyAnimator.h deleted file mode 100644 index f21704a3d8..0000000000 --- a/experimental/skotty/SkottyAnimator.h +++ /dev/null @@ -1,193 +0,0 @@ -/* - * 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 "SkCubicMap.h" -#include "SkMakeUnique.h" -#include "SkottyPriv.h" -#include "SkottyProperties.h" -#include "SkTypes.h" - -#include -#include - -namespace skotty { - -class AnimatorBase : public SkNoncopyable { -public: - virtual ~AnimatorBase() = default; - - virtual void tick(float) = 0; - -protected: - AnimatorBase() = default; -}; - -class KeyframeIntervalBase : public SkNoncopyable { -public: - KeyframeIntervalBase() = default; - KeyframeIntervalBase(KeyframeIntervalBase&&) = default; - KeyframeIntervalBase& operator=(KeyframeIntervalBase&&) = default; - - float t0() const { return fT0; } - float t1() const { return fT1; } - - bool isValid() const { return fT0 < fT1; } - bool contains(float t) const { return t >= fT0 && t <= fT1; } - -protected: - // Parse the current interval AND back-fill prev interval t1. - bool parse(const Json::Value&, KeyframeIntervalBase* prev); - - // Computes a "local" t (relative to [fT0..fT1]), and mapped - // through the cubic (if applicable). - float localT(float t) const; - -private: - // Initialized for non-linear interpolation. - std::unique_ptr fCubicMap; - - // Start/end times. - float fT0 = 0, - fT1 = 0; -}; - -// Describes a keyframe interpolation interval (v0@t0) -> (v1@t1). -template -class KeyframeInterval final : public KeyframeIntervalBase { -public: - bool parse(const Json::Value& k, KeyframeInterval* prev) { - SkASSERT(k.isObject()); - - return this->INHERITED::parse(k, prev) && - ValueTraits::Parse(k["s"], &fV0) && - ValueTraits::Parse(k["e"], &fV1) && - ValueTraits::Cardinality(fV0) == ValueTraits::Cardinality(fV1) && - (!prev || ValueTraits::Cardinality(fV0) == ValueTraits::Cardinality(prev->fV0)); - } - - void lerp(float t, T*) const; - -private: - // Start/end values. - T fV0, - fV1; - - using INHERITED = KeyframeIntervalBase; -}; - -template -std::vector> ParseFrames(const Json::Value& jframes) { - std::vector> frames; - - if (jframes.isArray()) { - frames.reserve(jframes.size()); - - KeyframeInterval* prev_frame = nullptr; - for (const auto& jframe : jframes) { - if (!jframe.isObject()) - continue; - - KeyframeInterval frame; - if (frame.parse(jframe, prev_frame)) { - frames.push_back(std::move(frame)); - prev_frame = &frames.back(); - } - } - } - - // If we couldn't determine a t1 for the last frame, discard it. - if (!frames.empty() && !frames.back().isValid()) { - frames.pop_back(); - } - - return frames; -} - -// Binds an animated/keyframed property to a node attribute setter. -template -class Animator : public AnimatorBase { -public: - using ApplyFuncT = void(*)(NodeT*, const ValT&); - static std::unique_ptr Make(std::vector>&& frames, - sk_sp node, - ApplyFuncT&& applyFunc) { - return (node && !frames.empty()) - ? std::unique_ptr(new Animator(std::move(frames), - std::move(node), - std::move(applyFunc))) - : nullptr; - } - - void tick(float t) override { - const auto& frame = this->findFrame(t); - - ValT val; - frame.lerp(t, &val); - - fFunc(fTarget.get(), val); - } - -private: - Animator(std::vector>&& frames, sk_sp node, - ApplyFuncT&& applyFunc) - : fFrames(std::move(frames)) - , fTarget(std::move(node)) - , fFunc(std::move(applyFunc)) {} - - const KeyframeInterval& findFrame(float t) const; - - const std::vector> fFrames; - sk_sp fTarget; - ApplyFuncT fFunc; -}; - -template -const KeyframeInterval& Animator::findFrame(float t) const { - SkASSERT(!fFrames.empty()); - - // TODO: cache last/current frame? - - auto f0 = fFrames.begin(), - f1 = fFrames.end() - 1; - - 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; -} - -} // namespace skotty - -#endif // SkottyAnimator_DEFINED diff --git a/experimental/skotty/SkottyPriv.h b/experimental/skotty/SkottyPriv.h deleted file mode 100644 index e39994100c..0000000000 --- a/experimental/skotty/SkottyPriv.h +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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 "SkPoint.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; -} - -static inline SkPoint ParsePoint(const Json::Value& v, const SkPoint& defaultValue) { - return v.isObject() - ? SkPoint::Make(ParseScalar(v["x"], defaultValue.x()), - ParseScalar(v["y"], defaultValue.y())) - : defaultValue; -} - -} // namespace - -#endif // SkottyPriv_DEFINED diff --git a/experimental/skotty/SkottyProperties.cpp b/experimental/skotty/SkottyProperties.cpp deleted file mode 100644 index 6be2eebfa3..0000000000 --- a/experimental/skotty/SkottyProperties.cpp +++ /dev/null @@ -1,290 +0,0 @@ -/* - * 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 "SkSGColor.h" -#include "SkSGGradient.h" -#include "SkSGPath.h" -#include "SkSGRect.h" -#include "SkSGTransform.h" - -#include - -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; -} - -SkColor VecToColor(const float* v, size_t size) { - // best effort to turn this into a color - const auto r = size > 0 ? v[0] : 0, - g = size > 1 ? v[1] : 0, - b = size > 2 ? v[2] : 0, - a = size > 3 ? v[3] : 1; - - return SkColorSetARGB(SkTPin(a, 0, 1) * 255, - SkTPin(r, 0, 1) * 255, - SkTPin(g, 0, 1) * 255, - SkTPin(b, 0, 1) * 255); -} - -} // namespace - -template <> -bool ValueTraits::Parse(const Json::Value& v, ScalarValue* scalar) { - // Some files appear to wrap keyframes in arrays for no reason. - if (v.isArray() && v.size() == 1) { - return Parse(v[0], scalar); - } - - if (v.isNull() || !v.isConvertibleTo(Json::realValue)) - return false; - - *scalar = v.asFloat(); - return true; -} - -template <> -size_t ValueTraits::Cardinality(const ScalarValue&) { - return 1; -} - -template <> -template <> -SkScalar ValueTraits::As(const ScalarValue& v) { - return v; -} - -template <> -bool ValueTraits::Parse(const Json::Value& v, VectorValue* vec) { - SkASSERT(vec->empty()); - - if (!v.isArray()) - return false; - - for (Json::ArrayIndex i = 0; i < v.size(); ++i) { - ScalarValue scalar; - if (!ValueTraits::Parse(v[i], &scalar)) - return false; - - vec->push_back(std::move(scalar)); - } - - return true; -} - -template <> -size_t ValueTraits::Cardinality(const VectorValue& vec) { - return vec.size(); -} - -template <> -template <> -SkColor ValueTraits::As(const VectorValue& vec) { - return VecToColor(vec.data(), vec.size()); -} - -template <> -template <> -SkPoint ValueTraits::As(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::As(const VectorValue& vec) { - const auto pt = ValueTraits::As(vec); - return SkSize::Make(pt.x(), pt.y()); -} - -template<> -bool ValueTraits::Parse(const Json::Value& v, ShapeValue* shape) { - PointArray inPts, // Cubic Bezier "in" control points, relative to vertices. - outPts, // Cubic Bezier "out" control points, relative to vertices. - verts; // Cubic Bezier vertices. - - // Some files appear to wrap keyframes in arrays for no reason. - if (v.isArray() && v.size() == 1) { - 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->isEmpty()); - - if (!verts.empty()) { - shape->moveTo(verts.front()); - } - - const auto& addCubic = [&](int from, int to) { - shape->cubicTo(verts[from] + outPts[from], - verts[to] + inPts[to], - verts[to]); - }; - - for (int i = 1; i < verts.count(); ++i) { - addCubic(i - 1, i); - } - - if (!verts.empty() && ParseBool(v["c"], false)) { - addCubic(verts.count() - 1, 0); - shape->close(); - } - - return true; -} - -template <> -size_t ValueTraits::Cardinality(const ShapeValue& path) { - return SkTo(path.countVerbs()); -} - -template <> -template <> -SkPath ValueTraits::As(const ShapeValue& path) { - return path; -} - -CompositeRRect::CompositeRRect(sk_sp wrapped_node) - : fRRectNode(std::move(wrapped_node)) {} - -void CompositeRRect::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); -} - -CompositeTransform::CompositeTransform(sk_sp matrix) - : fMatrixNode(std::move(matrix)) {} - -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 - - fMatrixNode->setMatrix(t); -} - -CompositePolyStar::CompositePolyStar(sk_sp wrapped_node, Type t) - : fPathNode(std::move(wrapped_node)) - , fType(t) {} - -void CompositePolyStar::apply() { - const auto count = SkScalarTruncToInt(fPointCount); - const auto arc = 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)); - - for (int 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); -} - -CompositeGradient::CompositeGradient(sk_sp grad, size_t stopCount) - : fGradient(std::move(grad)) - , fStopCount(stopCount) {} - -void CompositeGradient::apply() { - this->onApply(); - - // |fColorStops| holds |fStopCount| x [ pos, r, g, g ] + ? x [ pos, alpha ] - - if (fColorStops.size() < fStopCount * 4 || ((fColorStops.size() - fStopCount * 4) % 2)) { - LOG("!! Invalid gradient stop array size: %zu", fColorStops.size()); - return; - } - - std::vector stops; - - // TODO: merge/lerp opacity stops - const auto csEnd = fColorStops.cbegin() + fStopCount * 4; - for (auto cs = fColorStops.cbegin(); cs != csEnd; cs += 4) { - stops.push_back({ *cs, VecToColor(&*(cs + 1), 3) }); - } - - fGradient->setColorStops(std::move(stops)); -} - -CompositeLinearGradient::CompositeLinearGradient(sk_sp grad, size_t stopCount) - : INHERITED(std::move(grad), stopCount) {} - -void CompositeLinearGradient::onApply() { - auto* grad = static_cast(fGradient.get()); - grad->setStartPoint(this->startPoint()); - grad->setEndPoint(this->endPoint()); -} - -CompositeRadialGradient::CompositeRadialGradient(sk_sp grad, size_t stopCount) - : INHERITED(std::move(grad), stopCount) {} - -void CompositeRadialGradient::onApply() { - auto* grad = static_cast(fGradient.get()); - grad->setStartCenter(this->startPoint()); - grad->setEndCenter(this->startPoint()); - grad->setStartRadius(0); - grad->setEndRadius(SkPoint::Distance(this->startPoint(), this->endPoint())); -} - -} // namespace skotty diff --git a/experimental/skotty/SkottyProperties.h b/experimental/skotty/SkottyProperties.h deleted file mode 100644 index 177ba35873..0000000000 --- a/experimental/skotty/SkottyProperties.h +++ /dev/null @@ -1,168 +0,0 @@ -/* - * 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 "SkColor.h" -#include "SkPath.h" -#include "SkPoint.h" -#include "SkSize.h" -#include "SkottyPriv.h" -#include "SkRefCnt.h" -#include "SkTArray.h" -#include "SkTypes.h" - -#include -#include - -namespace sksg { -class Color; -class Gradient; -class LinearGradient; -class Matrix; -class Path; -class RadialGradient; -class RRect; -class RenderNode;; -} - -namespace skotty { - -template -struct ValueTraits { - static bool Parse(const Json::Value&, T*); - static size_t Cardinality(const T&); - - template - static U As(const T&); -}; - -using ScalarValue = SkScalar; -using VectorValue = std::vector; -using ShapeValue = SkPath; - -// 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 CompositeRRect final : public SkRefCnt { -public: - explicit CompositeRRect(sk_sp); - - COMPOSITE_PROPERTY(Position, SkPoint , SkPoint::Make(0, 0)) - COMPOSITE_PROPERTY(Size , SkSize , SkSize::Make(0, 0)) - COMPOSITE_PROPERTY(Radius , SkSize , SkSize::Make(0, 0)) - -private: - void apply(); - - sk_sp fRRectNode; - - using INHERITED = SkRefCnt; -}; - -class CompositePolyStar final : public SkRefCnt { -public: - enum class Type { - kStar, kPoly, - }; - - CompositePolyStar(sk_sp, Type); - - COMPOSITE_PROPERTY(Position , SkPoint , SkPoint::Make(0, 0)) - COMPOSITE_PROPERTY(PointCount , SkScalar, 0) - COMPOSITE_PROPERTY(InnerRadius , SkScalar, 0) - COMPOSITE_PROPERTY(OuterRadius , SkScalar, 0) - COMPOSITE_PROPERTY(InnerRoundness, SkScalar, 0) - COMPOSITE_PROPERTY(OuterRoundness, SkScalar, 0) - COMPOSITE_PROPERTY(Rotation , SkScalar, 0) - -private: - void apply(); - - sk_sp fPathNode; - Type fType; - - using INHERITED = SkRefCnt; -}; - -class CompositeTransform final : public SkRefCnt { -public: - explicit CompositeTransform(sk_sp); - - 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 fMatrixNode; - - using INHERITED = SkRefCnt; -}; - -class CompositeGradient : public SkRefCnt { -public: - COMPOSITE_PROPERTY(StartPoint, SkPoint , SkPoint::Make(0, 0) ) - COMPOSITE_PROPERTY(EndPoint , SkPoint , SkPoint::Make(0, 0) ) - COMPOSITE_PROPERTY(ColorStops, std::vector, std::vector()) - -protected: - CompositeGradient(sk_sp, size_t stopCount); - - const SkPoint& startPoint() const { return fStartPoint; } - const SkPoint& endPoint() const { return fEndPoint; } - - sk_sp fGradient; - size_t fStopCount; - - virtual void onApply() = 0; - -private: - void apply(); - - using INHERITED = SkRefCnt; -}; - -class CompositeLinearGradient final : public CompositeGradient { -public: - CompositeLinearGradient(sk_sp, size_t stopCount); - -private: - void onApply() override; - - using INHERITED = CompositeGradient; -}; - -class CompositeRadialGradient final : public CompositeGradient { -public: - CompositeRadialGradient(sk_sp, size_t stopCount); - -private: - void onApply() override; - - using INHERITED = CompositeGradient; -}; - -#undef COMPOSITE_PROPERTY - -} // namespace skotty - -#endif // SkottyProperties_DEFINED diff --git a/experimental/sksg/geometry/SkSGMerge.cpp b/experimental/sksg/geometry/SkSGMerge.cpp index a9f06d464f..f75945d85e 100644 --- a/experimental/sksg/geometry/SkSGMerge.cpp +++ b/experimental/sksg/geometry/SkSGMerge.cpp @@ -78,4 +78,4 @@ SkRect Merge::onRevalidate(InvalidationController* ic, const SkMatrix& ctm) { return fMerged.computeTightBounds(); } -} // namespace skotty +} // namespace sksg diff --git a/tools/viewer/SkottieSlide.cpp b/tools/viewer/SkottieSlide.cpp new file mode 100644 index 0000000000..8880916a16 --- /dev/null +++ b/tools/viewer/SkottieSlide.cpp @@ -0,0 +1,77 @@ +/* + * 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 "SkottieSlide.h" + +#include "SkAnimTimer.h" +#include "SkCanvas.h" +#include "Skottie.h" + +SkottieSlide::SkottieSlide(const SkString& name, const SkString& path) + : fPath(path) { + fName = name; +} + +void SkottieSlide::load(SkScalar, SkScalar) { + fAnimation = skottie::Animation::MakeFromFile(fPath.c_str()); + fTimeBase = 0; // force a time reset + + if (fAnimation) { + fAnimation->setShowInval(fShowAnimationInval); + 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 SkottieSlide::unload() { + fAnimation.reset(); +} + +SkISize SkottieSlide::getDimensions() const { + return fAnimation? fAnimation->size().toCeil() : SkISize::Make(0, 0); +} + +void SkottieSlide::draw(SkCanvas* canvas) { + if (fAnimation) { + SkAutoCanvasRestore acr(canvas, true); + const SkRect dstR = SkRect::Make(canvas->imageInfo().bounds()); + fAnimation->render(canvas, &dstR); + } +} + +bool SkottieSlide::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 SkottieSlide::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/SkottieSlide.h b/tools/viewer/SkottieSlide.h new file mode 100644 index 0000000000..6c278d484f --- /dev/null +++ b/tools/viewer/SkottieSlide.h @@ -0,0 +1,73 @@ +/* + * 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 SkottieSlide_DEFINED +#define SkottieSlide_DEFINED + +#include "Slide.h" + +namespace skottie { class Animation; } + +class SkottieSlide : public Slide { +public: + SkottieSlide(const SkString& name, const SkString& path); + ~SkottieSlide() 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; +}; + +class SkottieSlide2 : public Slide { +public: + SkottieSlide2(const SkString& path); + ~SkottieSlide2() 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 onMouse(SkScalar x, SkScalar y, sk_app::Window::InputState, uint32_t modifiers) override; +private: + struct Rec { + std::unique_ptr fAnimation; + SkMSec fTimeBase = 0; + SkString fName; + bool fShowAnimationInval = false; + + Rec(std::unique_ptr anim); + Rec(Rec&& o); + }; + + int findCell(float x, float y) const; + + SkString fPath; + SkTArray fAnims; + + int fTrackingCell = -1; + + typedef Slide INHERITED; +}; + +#endif // SkottieSlide_DEFINED diff --git a/tools/viewer/SkottieSlide2.cpp b/tools/viewer/SkottieSlide2.cpp new file mode 100644 index 0000000000..0c643b724e --- /dev/null +++ b/tools/viewer/SkottieSlide2.cpp @@ -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. + */ + +#include "SkottieSlide.h" + +#include "SkAnimTimer.h" +#include "SkCanvas.h" +#include "Skottie.h" +#include "SkOSFile.h" +#include "SkOSPath.h" +#include "SkStream.h" + +const int CELL_WIDTH = 240; +const int CELL_HEIGHT = 160; +const int COL_COUNT = 4; +const int SPACER_X = 12; +const int SPACER_Y = 24; +const int MARGIN = 8; + +SkottieSlide2::Rec::Rec(std::unique_ptr anim) : fAnimation(std::move(anim)) +{} + +SkottieSlide2::Rec::Rec(Rec&& o) + : fAnimation(std::move(o.fAnimation)) + , fTimeBase(o.fTimeBase) + , fName(o.fName) + , fShowAnimationInval(o.fShowAnimationInval) +{} + +SkottieSlide2::SkottieSlide2(const SkString& path) + : fPath(path) +{ + fName.set("skottie-dir"); +} + +void SkottieSlide2::load(SkScalar, SkScalar) { + SkString name; + SkOSFile::Iter iter(fPath.c_str(), "json"); + while (iter.next(&name)) { + SkString path = SkOSPath::Join(fPath.c_str(), name.c_str()); + if (auto anim = skottie::Animation::MakeFromFile(path.c_str())) { + fAnims.push_back(Rec(std::move(anim))).fName = name; + } + } +} + +void SkottieSlide2::unload() { + fAnims.reset(); +} + +SkISize SkottieSlide2::getDimensions() const { + const int rows = (fAnims.count() + COL_COUNT - 1) / COL_COUNT; + return { + MARGIN + (COL_COUNT - 1) * SPACER_X + COL_COUNT * CELL_WIDTH + MARGIN, + MARGIN + (rows - 1) * SPACER_Y + rows * CELL_HEIGHT + MARGIN, + }; +} + +void SkottieSlide2::draw(SkCanvas* canvas) { + SkPaint paint; + paint.setTextSize(12); + paint.setAntiAlias(true); + paint.setTextAlign(SkPaint::kCenter_Align); + + const SkRect dst = SkRect::MakeIWH(CELL_WIDTH, CELL_HEIGHT); + int x = 0, y = 0; + + canvas->translate(MARGIN, MARGIN); + for (const auto& rec : fAnims) { + SkAutoCanvasRestore acr(canvas, true); + canvas->translate(x * (CELL_WIDTH + SPACER_X), y * (CELL_HEIGHT + SPACER_Y)); + canvas->drawText(rec.fName.c_str(), rec.fName.size(), + dst.centerX(), dst.bottom() + paint.getTextSize(), paint); + rec.fAnimation->render(canvas, &dst); + if (++x == COL_COUNT) { + x = 0; + y += 1; + } + } +} + +bool SkottieSlide2::animate(const SkAnimTimer& timer) { + for (auto& rec : fAnims) { + if (rec.fTimeBase == 0) { + // Reset the animation time. + rec.fTimeBase = timer.msec(); + } + rec.fAnimation->animationTick(timer.msec() - rec.fTimeBase); + } + return true; +} + +bool SkottieSlide2::onMouse(SkScalar x, SkScalar y, sk_app::Window::InputState state, + uint32_t modifiers) { + if (fTrackingCell < 0 && state == sk_app::Window::kDown_InputState) { + fTrackingCell = this->findCell(x, y); + } + if (fTrackingCell >= 0 && state == sk_app::Window::kUp_InputState) { + int index = this->findCell(x, y); + if (fTrackingCell == index) { + fAnims[index].fShowAnimationInval = !fAnims[index].fShowAnimationInval; + fAnims[index].fAnimation->setShowInval(fAnims[index].fShowAnimationInval); + } + fTrackingCell = -1; + } + return fTrackingCell >= 0; +} + +int SkottieSlide2::findCell(float x, float y) const { + x -= MARGIN; + y -= MARGIN; + int index = -1; + if (x >= 0 && y >= 0) { + int ix = (int)x; + int iy = (int)y; + int col = ix / (CELL_WIDTH + SPACER_X); + int row = iy / (CELL_HEIGHT + SPACER_Y); + index = row * COL_COUNT + col; + if (index >= fAnims.count()) { + index = -1; + } + } + return index; +} diff --git a/tools/viewer/SkottySlide.cpp b/tools/viewer/SkottySlide.cpp deleted file mode 100644 index 99f182a7bb..0000000000 --- a/tools/viewer/SkottySlide.cpp +++ /dev/null @@ -1,77 +0,0 @@ -/* - * 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 "SkCanvas.h" -#include "Skotty.h" - -SkottySlide::SkottySlide(const SkString& name, const SkString& path) - : fPath(path) { - fName = name; -} - -void SkottySlide::load(SkScalar, SkScalar) { - fAnimation = skotty::Animation::MakeFromFile(fPath.c_str()); - fTimeBase = 0; // force a time reset - - if (fAnimation) { - fAnimation->setShowInval(fShowAnimationInval); - 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) { - SkAutoCanvasRestore acr(canvas, true); - const SkRect dstR = SkRect::Make(canvas->imageInfo().bounds()); - fAnimation->render(canvas, &dstR); - } -} - -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 deleted file mode 100644 index ed0662f2ce..0000000000 --- a/tools/viewer/SkottySlide.h +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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; -}; - -class SkottySlide2 : public Slide { -public: - SkottySlide2(const SkString& path); - ~SkottySlide2() 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 onMouse(SkScalar x, SkScalar y, sk_app::Window::InputState, uint32_t modifiers) override; -private: - struct Rec { - std::unique_ptr fAnimation; - SkMSec fTimeBase = 0; - SkString fName; - bool fShowAnimationInval = false; - - Rec(std::unique_ptr anim); - Rec(Rec&& o); - }; - - int findCell(float x, float y) const; - - SkString fPath; - SkTArray fAnims; - - int fTrackingCell = -1; - - typedef Slide INHERITED; -}; - -#endif // SkottySlide_DEFINED diff --git a/tools/viewer/SkottySlide2.cpp b/tools/viewer/SkottySlide2.cpp deleted file mode 100644 index 180c7ad411..0000000000 --- a/tools/viewer/SkottySlide2.cpp +++ /dev/null @@ -1,129 +0,0 @@ -/* - * 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 "SkCanvas.h" -#include "Skotty.h" -#include "SkOSFile.h" -#include "SkOSPath.h" -#include "SkStream.h" - -const int CELL_WIDTH = 240; -const int CELL_HEIGHT = 160; -const int COL_COUNT = 4; -const int SPACER_X = 12; -const int SPACER_Y = 24; -const int MARGIN = 8; - -SkottySlide2::Rec::Rec(std::unique_ptr anim) : fAnimation(std::move(anim)) -{} - -SkottySlide2::Rec::Rec(Rec&& o) - : fAnimation(std::move(o.fAnimation)) - , fTimeBase(o.fTimeBase) - , fName(o.fName) - , fShowAnimationInval(o.fShowAnimationInval) -{} - -SkottySlide2::SkottySlide2(const SkString& path) - : fPath(path) -{ - fName.set("skotty-dir"); -} - -void SkottySlide2::load(SkScalar, SkScalar) { - SkString name; - SkOSFile::Iter iter(fPath.c_str(), "json"); - while (iter.next(&name)) { - SkString path = SkOSPath::Join(fPath.c_str(), name.c_str()); - if (auto anim = skotty::Animation::MakeFromFile(path.c_str())) { - fAnims.push_back(Rec(std::move(anim))).fName = name; - } - } -} - -void SkottySlide2::unload() { - fAnims.reset(); -} - -SkISize SkottySlide2::getDimensions() const { - const int rows = (fAnims.count() + COL_COUNT - 1) / COL_COUNT; - return { - MARGIN + (COL_COUNT - 1) * SPACER_X + COL_COUNT * CELL_WIDTH + MARGIN, - MARGIN + (rows - 1) * SPACER_Y + rows * CELL_HEIGHT + MARGIN, - }; -} - -void SkottySlide2::draw(SkCanvas* canvas) { - SkPaint paint; - paint.setTextSize(12); - paint.setAntiAlias(true); - paint.setTextAlign(SkPaint::kCenter_Align); - - const SkRect dst = SkRect::MakeIWH(CELL_WIDTH, CELL_HEIGHT); - int x = 0, y = 0; - - canvas->translate(MARGIN, MARGIN); - for (const auto& rec : fAnims) { - SkAutoCanvasRestore acr(canvas, true); - canvas->translate(x * (CELL_WIDTH + SPACER_X), y * (CELL_HEIGHT + SPACER_Y)); - canvas->drawText(rec.fName.c_str(), rec.fName.size(), - dst.centerX(), dst.bottom() + paint.getTextSize(), paint); - rec.fAnimation->render(canvas, &dst); - if (++x == COL_COUNT) { - x = 0; - y += 1; - } - } -} - -bool SkottySlide2::animate(const SkAnimTimer& timer) { - for (auto& rec : fAnims) { - if (rec.fTimeBase == 0) { - // Reset the animation time. - rec.fTimeBase = timer.msec(); - } - rec.fAnimation->animationTick(timer.msec() - rec.fTimeBase); - } - return true; -} - -bool SkottySlide2::onMouse(SkScalar x, SkScalar y, sk_app::Window::InputState state, - uint32_t modifiers) { - if (fTrackingCell < 0 && state == sk_app::Window::kDown_InputState) { - fTrackingCell = this->findCell(x, y); - } - if (fTrackingCell >= 0 && state == sk_app::Window::kUp_InputState) { - int index = this->findCell(x, y); - if (fTrackingCell == index) { - fAnims[index].fShowAnimationInval = !fAnims[index].fShowAnimationInval; - fAnims[index].fAnimation->setShowInval(fAnims[index].fShowAnimationInval); - } - fTrackingCell = -1; - } - return fTrackingCell >= 0; -} - -int SkottySlide2::findCell(float x, float y) const { - x -= MARGIN; - y -= MARGIN; - int index = -1; - if (x >= 0 && y >= 0) { - int ix = (int)x; - int iy = (int)y; - int col = ix / (CELL_WIDTH + SPACER_X); - int row = iy / (CELL_HEIGHT + SPACER_Y); - index = row * COL_COUNT + col; - if (index >= fAnims.count()) { - index = -1; - } - } - return index; -} - diff --git a/tools/viewer/Viewer.cpp b/tools/viewer/Viewer.cpp index 59d5e50d5a..1ba32eee9d 100644 --- a/tools/viewer/Viewer.cpp +++ b/tools/viewer/Viewer.cpp @@ -11,7 +11,7 @@ #include "ImageSlide.h" #include "Resources.h" #include "SampleSlide.h" -#include "SkottySlide.h" +#include "SkottieSlide.h" #include "SKPSlide.h" #include "GrContext.h" @@ -486,7 +486,7 @@ void Viewer::initSlides() { // JSONs for (const auto& json : FLAGS_jsons) { - fSlides.push_back(sk_make_sp(json)); + fSlides.push_back(sk_make_sp(json)); SkOSFile::Iter it(json.c_str(), ".json"); SkString jsonName; @@ -494,8 +494,8 @@ void Viewer::initSlides() { if (SkCommandLineFlags::ShouldSkip(FLAGS_match, jsonName.c_str())) { continue; } - fSlides.push_back(sk_make_sp(jsonName, SkOSPath::Join(json.c_str(), - jsonName.c_str()))); + fSlides.push_back(sk_make_sp(jsonName, SkOSPath::Join(json.c_str(), + jsonName.c_str()))); } } } -- cgit v1.2.3