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