diff options
Diffstat (limited to 'modules/skottie/src/Skottie.cpp')
-rw-r--r-- | modules/skottie/src/Skottie.cpp | 1327 |
1 files changed, 1327 insertions, 0 deletions
diff --git a/modules/skottie/src/Skottie.cpp b/modules/skottie/src/Skottie.cpp new file mode 100644 index 0000000000..8396b5ba90 --- /dev/null +++ b/modules/skottie/src/Skottie.cpp @@ -0,0 +1,1327 @@ +/* + * Copyright 2017 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include "Skottie.h" + +#include "SkCanvas.h" +#include "SkottieAdapter.h" +#include "SkottieAnimator.h" +#include "SkottieJson.h" +#include "SkottieValue.h" +#include "SkData.h" +#include "SkImage.h" +#include "SkMakeUnique.h" +#include "SkOSPath.h" +#include "SkPaint.h" +#include "SkParse.h" +#include "SkPoint.h" +#include "SkSGClipEffect.h" +#include "SkSGColor.h" +#include "SkSGDraw.h" +#include "SkSGGeometryTransform.h" +#include "SkSGGradient.h" +#include "SkSGGroup.h" +#include "SkSGImage.h" +#include "SkSGInvalidationController.h" +#include "SkSGMaskEffect.h" +#include "SkSGMerge.h" +#include "SkSGOpacityEffect.h" +#include "SkSGPath.h" +#include "SkSGRect.h" +#include "SkSGRoundEffect.h" +#include "SkSGScene.h" +#include "SkSGTransform.h" +#include "SkSGTrimEffect.h" +#include "SkStream.h" +#include "SkTArray.h" +#include "SkTime.h" +#include "SkTHash.h" + +#include <cmath> +#include <vector> + +#include "stdlib.h" + +namespace skottie { + +#define LOG SkDebugf + +namespace { + +struct AssetInfo { + json::ValueRef fAsset; + mutable bool fIsAttaching; // Used for cycle detection +}; + +using AssetMap = SkTHashMap<SkString, AssetInfo>; + +struct AttachContext { + const ResourceProvider& fResources; + const AssetMap& fAssets; + const float fFrameRate; + sksg::AnimatorList& fAnimators; +}; + +bool LogFail(const json::ValueRef& json, const char* msg) { + const auto dump = json.toString(); + LOG("!! %s: %s\n", msg, dump.c_str()); + return false; +} + +sk_sp<sksg::Matrix> AttachMatrix(const json::ValueRef& t, AttachContext* ctx, + sk_sp<sksg::Matrix> parentMatrix) { + if (!t.isObject()) + return nullptr; + + auto matrix = sksg::Matrix::Make(SkMatrix::I(), std::move(parentMatrix)); + auto adapter = sk_make_sp<TransformAdapter>(matrix); + auto anchor_attached = BindProperty<VectorValue>(t["a"], &ctx->fAnimators, + [adapter](const VectorValue& a) { + adapter->setAnchorPoint(ValueTraits<VectorValue>::As<SkPoint>(a)); + }); + auto position_attached = BindProperty<VectorValue>(t["p"], &ctx->fAnimators, + [adapter](const VectorValue& p) { + adapter->setPosition(ValueTraits<VectorValue>::As<SkPoint>(p)); + }); + auto scale_attached = BindProperty<VectorValue>(t["s"], &ctx->fAnimators, + [adapter](const VectorValue& s) { + adapter->setScale(ValueTraits<VectorValue>::As<SkVector>(s)); + }); + + auto jrotation = t["r"]; + if (jrotation.isNull()) { + // 3d rotations have separate rx,ry,rz components. While we don't fully support them, + // we can still make use of rz. + jrotation = t["rz"]; + } + auto rotation_attached = BindProperty<ScalarValue>(jrotation, &ctx->fAnimators, + [adapter](const ScalarValue& r) { + adapter->setRotation(r); + }); + auto skew_attached = BindProperty<ScalarValue>(t["sk"], &ctx->fAnimators, + [adapter](const ScalarValue& sk) { + adapter->setSkew(sk); + }); + auto skewaxis_attached = BindProperty<ScalarValue>(t["sa"], &ctx->fAnimators, + [adapter](const ScalarValue& sa) { + adapter->setSkewAxis(sa); + }); + + if (!anchor_attached && + !position_attached && + !scale_attached && + !rotation_attached && + !skew_attached && + !skewaxis_attached) { + LogFail(t, "Could not parse transform"); + return nullptr; + } + + return matrix; +} + +sk_sp<sksg::RenderNode> AttachOpacity(const json::ValueRef& jtransform, AttachContext* ctx, + sk_sp<sksg::RenderNode> childNode) { + if (!jtransform.isObject() || !childNode) + return childNode; + + static constexpr ScalarValue kNoopOpacity = 100; + auto opacityNode = sksg::OpacityEffect::Make(childNode); + + if (!BindProperty<ScalarValue>(jtransform["o"], &ctx->fAnimators, + [opacityNode](const ScalarValue& o) { + // BM opacity is [0..100] + opacityNode->setOpacity(o * 0.01f); + }, &kNoopOpacity)) { + // We can ignore static full opacity. + return childNode; + } + + return std::move(opacityNode); +} + +sk_sp<sksg::RenderNode> AttachComposition(const json::ValueRef&, AttachContext* ctx); + +sk_sp<sksg::Path> AttachPath(const json::ValueRef& jpath, AttachContext* ctx) { + auto path_node = sksg::Path::Make(); + return BindProperty<ShapeValue>(jpath, &ctx->fAnimators, + [path_node](const ShapeValue& p) { + path_node->setPath(ValueTraits<ShapeValue>::As<SkPath>(p)); + }) + ? path_node + : nullptr; +} + +sk_sp<sksg::GeometryNode> AttachPathGeometry(const json::ValueRef& jpath, AttachContext* ctx) { + SkASSERT(jpath.isObject()); + + return AttachPath(jpath["ks"], ctx); +} + +sk_sp<sksg::GeometryNode> AttachRRectGeometry(const json::ValueRef& jrect, AttachContext* ctx) { + SkASSERT(jrect.isObject()); + + auto rect_node = sksg::RRect::Make(); + auto adapter = sk_make_sp<RRectAdapter>(rect_node); + + auto p_attached = BindProperty<VectorValue>(jrect["p"], &ctx->fAnimators, + [adapter](const VectorValue& p) { + adapter->setPosition(ValueTraits<VectorValue>::As<SkPoint>(p)); + }); + auto s_attached = BindProperty<VectorValue>(jrect["s"], &ctx->fAnimators, + [adapter](const VectorValue& s) { + adapter->setSize(ValueTraits<VectorValue>::As<SkSize>(s)); + }); + auto r_attached = BindProperty<ScalarValue>(jrect["r"], &ctx->fAnimators, + [adapter](const ScalarValue& r) { + adapter->setRadius(SkSize::Make(r, r)); + }); + + if (!p_attached && !s_attached && !r_attached) { + return nullptr; + } + + return std::move(rect_node); +} + +sk_sp<sksg::GeometryNode> AttachEllipseGeometry(const json::ValueRef& jellipse, AttachContext* ctx) { + SkASSERT(jellipse.isObject()); + + auto rect_node = sksg::RRect::Make(); + auto adapter = sk_make_sp<RRectAdapter>(rect_node); + + auto p_attached = BindProperty<VectorValue>(jellipse["p"], &ctx->fAnimators, + [adapter](const VectorValue& p) { + adapter->setPosition(ValueTraits<VectorValue>::As<SkPoint>(p)); + }); + auto s_attached = BindProperty<VectorValue>(jellipse["s"], &ctx->fAnimators, + [adapter](const VectorValue& s) { + const auto sz = ValueTraits<VectorValue>::As<SkSize>(s); + adapter->setSize(sz); + adapter->setRadius(SkSize::Make(sz.width() / 2, sz.height() / 2)); + }); + + if (!p_attached && !s_attached) { + return nullptr; + } + + return std::move(rect_node); +} + +sk_sp<sksg::GeometryNode> AttachPolystarGeometry(const json::ValueRef& jstar, AttachContext* ctx) { + SkASSERT(jstar.isObject()); + + static constexpr PolyStarAdapter::Type gTypes[] = { + PolyStarAdapter::Type::kStar, // "sy": 1 + PolyStarAdapter::Type::kPoly, // "sy": 2 + }; + + const auto type = jstar["sy"].toDefault<int>(0) - 1; + if (type < 0 || type >= SkTo<int>(SK_ARRAY_COUNT(gTypes))) { + LogFail(jstar, "Unknown polystar type"); + return nullptr; + } + + auto path_node = sksg::Path::Make(); + auto adapter = sk_make_sp<PolyStarAdapter>(path_node, gTypes[type]); + + BindProperty<VectorValue>(jstar["p"], &ctx->fAnimators, + [adapter](const VectorValue& p) { + adapter->setPosition(ValueTraits<VectorValue>::As<SkPoint>(p)); + }); + BindProperty<ScalarValue>(jstar["pt"], &ctx->fAnimators, + [adapter](const ScalarValue& pt) { + adapter->setPointCount(pt); + }); + BindProperty<ScalarValue>(jstar["ir"], &ctx->fAnimators, + [adapter](const ScalarValue& ir) { + adapter->setInnerRadius(ir); + }); + BindProperty<ScalarValue>(jstar["or"], &ctx->fAnimators, + [adapter](const ScalarValue& otr) { + adapter->setOuterRadius(otr); + }); + BindProperty<ScalarValue>(jstar["is"], &ctx->fAnimators, + [adapter](const ScalarValue& is) { + adapter->setInnerRoundness(is); + }); + BindProperty<ScalarValue>(jstar["os"], &ctx->fAnimators, + [adapter](const ScalarValue& os) { + adapter->setOuterRoundness(os); + }); + BindProperty<ScalarValue>(jstar["r"], &ctx->fAnimators, + [adapter](const ScalarValue& r) { + adapter->setRotation(r); + }); + + return std::move(path_node); +} + +sk_sp<sksg::Color> AttachColor(const json::ValueRef& obj, AttachContext* ctx) { + SkASSERT(obj.isObject()); + + auto color_node = sksg::Color::Make(SK_ColorBLACK); + auto color_attached = BindProperty<VectorValue>(obj["c"], &ctx->fAnimators, + [color_node](const VectorValue& c) { + color_node->setColor(ValueTraits<VectorValue>::As<SkColor>(c)); + }); + + return color_attached ? color_node : nullptr; +} + +sk_sp<sksg::Gradient> AttachGradient(const json::ValueRef& obj, AttachContext* ctx) { + SkASSERT(obj.isObject()); + + const auto stops = obj["g"]; + if (!stops.isObject()) + return nullptr; + + const auto stopCount = stops["p"].toDefault<int>(-1); + if (stopCount < 0) + return nullptr; + + sk_sp<sksg::Gradient> gradient_node; + sk_sp<GradientAdapter> adapter; + + if (obj["t"].toDefault<int>(1) == 1) { + auto linear_node = sksg::LinearGradient::Make(); + adapter = sk_make_sp<LinearGradientAdapter>(linear_node, stopCount); + gradient_node = std::move(linear_node); + } else { + auto radial_node = sksg::RadialGradient::Make(); + adapter = sk_make_sp<RadialGradientAdapter>(radial_node, stopCount); + + // TODO: highlight, angle + gradient_node = std::move(radial_node); + } + + BindProperty<VectorValue>(stops["k"], &ctx->fAnimators, + [adapter](const VectorValue& stops) { + adapter->setColorStops(stops); + }); + BindProperty<VectorValue>(obj["s"], &ctx->fAnimators, + [adapter](const VectorValue& s) { + adapter->setStartPoint(ValueTraits<VectorValue>::As<SkPoint>(s)); + }); + BindProperty<VectorValue>(obj["e"], &ctx->fAnimators, + [adapter](const VectorValue& e) { + adapter->setEndPoint(ValueTraits<VectorValue>::As<SkPoint>(e)); + }); + + return gradient_node; +} + +sk_sp<sksg::PaintNode> AttachPaint(const json::ValueRef& jpaint, AttachContext* ctx, + sk_sp<sksg::PaintNode> paint_node) { + if (paint_node) { + paint_node->setAntiAlias(true); + + BindProperty<ScalarValue>(jpaint["o"], &ctx->fAnimators, + [paint_node](const ScalarValue& o) { + // BM opacity is [0..100] + paint_node->setOpacity(o * 0.01f); + }); + } + + return paint_node; +} + +sk_sp<sksg::PaintNode> AttachStroke(const json::ValueRef& jstroke, AttachContext* ctx, + sk_sp<sksg::PaintNode> stroke_node) { + SkASSERT(jstroke.isObject()); + + if (!stroke_node) + return nullptr; + + stroke_node->setStyle(SkPaint::kStroke_Style); + + auto width_attached = BindProperty<ScalarValue>(jstroke["w"], &ctx->fAnimators, + [stroke_node](const ScalarValue& w) { + stroke_node->setStrokeWidth(w); + }); + if (!width_attached) + return nullptr; + + stroke_node->setStrokeMiter(jstroke["ml"].toDefault(4.0f)); + + static constexpr SkPaint::Join gJoins[] = { + SkPaint::kMiter_Join, + SkPaint::kRound_Join, + SkPaint::kBevel_Join, + }; + stroke_node->setStrokeJoin(gJoins[SkTPin<int>(jstroke["lj"].toDefault<int>(1) - 1, + 0, SK_ARRAY_COUNT(gJoins) - 1)]); + + static constexpr SkPaint::Cap gCaps[] = { + SkPaint::kButt_Cap, + SkPaint::kRound_Cap, + SkPaint::kSquare_Cap, + }; + stroke_node->setStrokeCap(gCaps[SkTPin<int>(jstroke["lc"].toDefault<int>(1) - 1, + 0, SK_ARRAY_COUNT(gCaps) - 1)]); + + return stroke_node; +} + +sk_sp<sksg::PaintNode> AttachColorFill(const json::ValueRef& jfill, AttachContext* ctx) { + SkASSERT(jfill.isObject()); + + return AttachPaint(jfill, ctx, AttachColor(jfill, ctx)); +} + +sk_sp<sksg::PaintNode> AttachGradientFill(const json::ValueRef& jfill, AttachContext* ctx) { + SkASSERT(jfill.isObject()); + + return AttachPaint(jfill, ctx, AttachGradient(jfill, ctx)); +} + +sk_sp<sksg::PaintNode> AttachColorStroke(const json::ValueRef& jstroke, AttachContext* ctx) { + SkASSERT(jstroke.isObject()); + + return AttachStroke(jstroke, ctx, AttachPaint(jstroke, ctx, AttachColor(jstroke, ctx))); +} + +sk_sp<sksg::PaintNode> AttachGradientStroke(const json::ValueRef& jstroke, AttachContext* ctx) { + SkASSERT(jstroke.isObject()); + + return AttachStroke(jstroke, ctx, AttachPaint(jstroke, ctx, AttachGradient(jstroke, ctx))); +} + +std::vector<sk_sp<sksg::GeometryNode>> AttachMergeGeometryEffect( + const json::ValueRef& jmerge, AttachContext* ctx, std::vector<sk_sp<sksg::GeometryNode>>&& geos) { + std::vector<sk_sp<sksg::GeometryNode>> merged; + + static constexpr sksg::Merge::Mode gModes[] = { + sksg::Merge::Mode::kMerge, // "mm": 1 + sksg::Merge::Mode::kUnion, // "mm": 2 + sksg::Merge::Mode::kDifference, // "mm": 3 + sksg::Merge::Mode::kIntersect, // "mm": 4 + sksg::Merge::Mode::kXOR , // "mm": 5 + }; + + const auto mode = gModes[SkTPin<int>(jmerge["mm"].toDefault(1) - 1, + 0, SK_ARRAY_COUNT(gModes) - 1)]; + merged.push_back(sksg::Merge::Make(std::move(geos), mode)); + + return merged; +} + +std::vector<sk_sp<sksg::GeometryNode>> AttachTrimGeometryEffect( + const json::ValueRef& jtrim, AttachContext* ctx, std::vector<sk_sp<sksg::GeometryNode>>&& geos) { + + enum class Mode { + kMerged, // "m": 1 + kSeparate, // "m": 2 + } gModes[] = { Mode::kMerged, Mode::kSeparate }; + + const auto mode = gModes[SkTPin<int>(jtrim["m"].toDefault(1) - 1, + 0, SK_ARRAY_COUNT(gModes) - 1)]; + + std::vector<sk_sp<sksg::GeometryNode>> inputs; + if (mode == Mode::kMerged) { + inputs.push_back(sksg::Merge::Make(std::move(geos), sksg::Merge::Mode::kMerge)); + } else { + inputs = std::move(geos); + } + + std::vector<sk_sp<sksg::GeometryNode>> trimmed; + trimmed.reserve(inputs.size()); + for (const auto& i : inputs) { + const auto trimEffect = sksg::TrimEffect::Make(i); + trimmed.push_back(trimEffect); + + const auto adapter = sk_make_sp<TrimEffectAdapter>(std::move(trimEffect)); + BindProperty<ScalarValue>(jtrim["s"], &ctx->fAnimators, + [adapter](const ScalarValue& s) { + adapter->setStart(s); + }); + BindProperty<ScalarValue>(jtrim["e"], &ctx->fAnimators, + [adapter](const ScalarValue& e) { + adapter->setEnd(e); + }); + BindProperty<ScalarValue>(jtrim["o"], &ctx->fAnimators, + [adapter](const ScalarValue& o) { + adapter->setOffset(o); + }); + } + + return trimmed; +} + +std::vector<sk_sp<sksg::GeometryNode>> AttachRoundGeometryEffect( + const json::ValueRef& jtrim, AttachContext* ctx, std::vector<sk_sp<sksg::GeometryNode>>&& geos) { + + std::vector<sk_sp<sksg::GeometryNode>> rounded; + rounded.reserve(geos.size()); + + for (const auto& g : geos) { + const auto roundEffect = sksg::RoundEffect::Make(std::move(g)); + rounded.push_back(roundEffect); + + BindProperty<ScalarValue>(jtrim["r"], &ctx->fAnimators, + [roundEffect](const ScalarValue& r) { + roundEffect->setRadius(r); + }); + } + + return rounded; +} + +using GeometryAttacherT = sk_sp<sksg::GeometryNode> (*)(const json::ValueRef&, AttachContext*); +static constexpr GeometryAttacherT gGeometryAttachers[] = { + AttachPathGeometry, + AttachRRectGeometry, + AttachEllipseGeometry, + AttachPolystarGeometry, +}; + +using PaintAttacherT = sk_sp<sksg::PaintNode> (*)(const json::ValueRef&, AttachContext*); +static constexpr PaintAttacherT gPaintAttachers[] = { + AttachColorFill, + AttachColorStroke, + AttachGradientFill, + AttachGradientStroke, +}; + +using GeometryEffectAttacherT = + std::vector<sk_sp<sksg::GeometryNode>> (*)(const json::ValueRef&, + AttachContext*, + std::vector<sk_sp<sksg::GeometryNode>>&&); +static constexpr GeometryEffectAttacherT gGeometryEffectAttachers[] = { + AttachMergeGeometryEffect, + AttachTrimGeometryEffect, + AttachRoundGeometryEffect, +}; + +enum class ShapeType { + kGeometry, + kGeometryEffect, + kPaint, + kGroup, + kTransform, +}; + +struct ShapeInfo { + const char* fTypeString; + ShapeType fShapeType; + uint32_t fAttacherIndex; // index into respective attacher tables +}; + +const ShapeInfo* FindShapeInfo(const json::ValueRef& shape) { + static constexpr ShapeInfo gShapeInfo[] = { + { "el", ShapeType::kGeometry , 2 }, // ellipse -> AttachEllipseGeometry + { "fl", ShapeType::kPaint , 0 }, // fill -> AttachColorFill + { "gf", ShapeType::kPaint , 2 }, // gfill -> AttachGradientFill + { "gr", ShapeType::kGroup , 0 }, // group -> Inline handler + { "gs", ShapeType::kPaint , 3 }, // gstroke -> AttachGradientStroke + { "mm", ShapeType::kGeometryEffect, 0 }, // merge -> AttachMergeGeometryEffect + { "rc", ShapeType::kGeometry , 1 }, // rrect -> AttachRRectGeometry + { "rd", ShapeType::kGeometryEffect, 2 }, // round -> AttachRoundGeometryEffect + { "sh", ShapeType::kGeometry , 0 }, // shape -> AttachPathGeometry + { "sr", ShapeType::kGeometry , 3 }, // polystar -> AttachPolyStarGeometry + { "st", ShapeType::kPaint , 1 }, // stroke -> AttachColorStroke + { "tm", ShapeType::kGeometryEffect, 1 }, // trim -> AttachTrimGeometryEffect + { "tr", ShapeType::kTransform , 0 }, // transform -> Inline handler + }; + + SkString type; + if (!shape["ty"].to(&type) || type.isEmpty()) + return nullptr; + + const auto* info = bsearch(type.c_str(), + gShapeInfo, + SK_ARRAY_COUNT(gShapeInfo), + sizeof(ShapeInfo), + [](const void* key, const void* info) { + return strcmp(static_cast<const char*>(key), + static_cast<const ShapeInfo*>(info)->fTypeString); + }); + + return static_cast<const ShapeInfo*>(info); +} + +struct GeometryEffectRec { + const json::ValueRef fJson; + GeometryEffectAttacherT fAttach; +}; + +struct AttachShapeContext { + AttachShapeContext(AttachContext* ctx, + std::vector<sk_sp<sksg::GeometryNode>>* geos, + std::vector<GeometryEffectRec>* effects, + size_t committedAnimators) + : fCtx(ctx) + , fGeometryStack(geos) + , fGeometryEffectStack(effects) + , fCommittedAnimators(committedAnimators) {} + + AttachContext* fCtx; + std::vector<sk_sp<sksg::GeometryNode>>* fGeometryStack; + std::vector<GeometryEffectRec>* fGeometryEffectStack; + size_t fCommittedAnimators; +}; + +sk_sp<sksg::RenderNode> AttachShape(const json::ValueRef& jshape, AttachShapeContext* shapeCtx) { + if (!jshape.isArray()) + return nullptr; + + SkDEBUGCODE(const auto initialGeometryEffects = shapeCtx->fGeometryEffectStack->size();) + + sk_sp<sksg::Group> shape_group = sksg::Group::Make(); + sk_sp<sksg::RenderNode> shape_wrapper = shape_group; + sk_sp<sksg::Matrix> shape_matrix; + + struct ShapeRec { + const json::ValueRef fJson; + const ShapeInfo& fInfo; + }; + + // First pass (bottom->top): + // + // * pick up the group transform and opacity + // * push local geometry effects onto the stack + // * store recs for next pass + // + std::vector<ShapeRec> recs; + for (size_t i = 0; i < jshape.size(); ++i) { + const auto s = jshape[jshape.size() - 1 - i]; + const auto* info = FindShapeInfo(s); + if (!info) { + LogFail(s["ty"], "Unknown shape"); + continue; + } + + recs.push_back({ s, *info }); + + switch (info->fShapeType) { + case ShapeType::kTransform: + if ((shape_matrix = AttachMatrix(s, shapeCtx->fCtx, nullptr))) { + shape_wrapper = sksg::Transform::Make(std::move(shape_wrapper), shape_matrix); + } + shape_wrapper = AttachOpacity(s, shapeCtx->fCtx, std::move(shape_wrapper)); + break; + case ShapeType::kGeometryEffect: + SkASSERT(info->fAttacherIndex < SK_ARRAY_COUNT(gGeometryEffectAttachers)); + shapeCtx->fGeometryEffectStack->push_back( + { s, gGeometryEffectAttachers[info->fAttacherIndex] }); + break; + default: + break; + } + } + + // Second pass (top -> bottom, after 2x reverse): + // + // * track local geometry + // * emit local paints + // + std::vector<sk_sp<sksg::GeometryNode>> geos; + std::vector<sk_sp<sksg::RenderNode >> draws; + for (auto rec = recs.rbegin(); rec != recs.rend(); ++rec) { + switch (rec->fInfo.fShapeType) { + case ShapeType::kGeometry: { + SkASSERT(rec->fInfo.fAttacherIndex < SK_ARRAY_COUNT(gGeometryAttachers)); + if (auto geo = gGeometryAttachers[rec->fInfo.fAttacherIndex](rec->fJson, + shapeCtx->fCtx)) { + geos.push_back(std::move(geo)); + } + } break; + case ShapeType::kGeometryEffect: { + // Apply the current effect and pop from the stack. + SkASSERT(rec->fInfo.fAttacherIndex < SK_ARRAY_COUNT(gGeometryEffectAttachers)); + if (!geos.empty()) { + geos = gGeometryEffectAttachers[rec->fInfo.fAttacherIndex](rec->fJson, + shapeCtx->fCtx, + std::move(geos)); + } + + SkASSERT(shapeCtx->fGeometryEffectStack->back().fJson == rec->fJson); + SkASSERT(shapeCtx->fGeometryEffectStack->back().fAttach == + gGeometryEffectAttachers[rec->fInfo.fAttacherIndex]); + shapeCtx->fGeometryEffectStack->pop_back(); + } break; + case ShapeType::kGroup: { + AttachShapeContext groupShapeCtx(shapeCtx->fCtx, + &geos, + shapeCtx->fGeometryEffectStack, + shapeCtx->fCommittedAnimators); + if (auto subgroup = AttachShape(rec->fJson["it"], &groupShapeCtx)) { + draws.push_back(std::move(subgroup)); + SkASSERT(groupShapeCtx.fCommittedAnimators >= shapeCtx->fCommittedAnimators); + shapeCtx->fCommittedAnimators = groupShapeCtx.fCommittedAnimators; + } + } break; + case ShapeType::kPaint: { + SkASSERT(rec->fInfo.fAttacherIndex < SK_ARRAY_COUNT(gPaintAttachers)); + auto paint = gPaintAttachers[rec->fInfo.fAttacherIndex](rec->fJson, shapeCtx->fCtx); + if (!paint || geos.empty()) + break; + + auto drawGeos = geos; + + // Apply all pending effects from the stack. + for (auto it = shapeCtx->fGeometryEffectStack->rbegin(); + it != shapeCtx->fGeometryEffectStack->rend(); ++it) { + drawGeos = it->fAttach(it->fJson, shapeCtx->fCtx, std::move(drawGeos)); + } + + // If we still have multiple geos, reduce using 'merge'. + auto geo = drawGeos.size() > 1 + ? sksg::Merge::Make(std::move(drawGeos), sksg::Merge::Mode::kMerge) + : drawGeos[0]; + + SkASSERT(geo); + draws.push_back(sksg::Draw::Make(std::move(geo), std::move(paint))); + shapeCtx->fCommittedAnimators = shapeCtx->fCtx->fAnimators.size(); + } break; + default: + break; + } + } + + // By now we should have popped all local geometry effects. + SkASSERT(shapeCtx->fGeometryEffectStack->size() == initialGeometryEffects); + + // Push transformed local geometries to parent list, for subsequent paints. + for (const auto& geo : geos) { + shapeCtx->fGeometryStack->push_back(shape_matrix + ? sksg::GeometryTransform::Make(std::move(geo), shape_matrix) + : std::move(geo)); + } + + // Emit local draws reversed (bottom->top, per spec). + for (auto it = draws.rbegin(); it != draws.rend(); ++it) { + shape_group->addChild(std::move(*it)); + } + + return draws.empty() ? nullptr : shape_wrapper; +} + +sk_sp<sksg::RenderNode> AttachNestedAnimation(const char* path, AttachContext* ctx) { + class SkottieSGAdapter final : public sksg::RenderNode { + public: + explicit SkottieSGAdapter(sk_sp<Animation> animation) + : fAnimation(std::move(animation)) { + SkASSERT(fAnimation); + } + + protected: + SkRect onRevalidate(sksg::InvalidationController*, const SkMatrix&) override { + return SkRect::MakeSize(fAnimation->size()); + } + + void onRender(SkCanvas* canvas) const override { + fAnimation->render(canvas); + } + + private: + const sk_sp<Animation> fAnimation; + }; + + class SkottieAnimatorAdapter final : public sksg::Animator { + public: + SkottieAnimatorAdapter(sk_sp<Animation> animation, float frameRate) + : fAnimation(std::move(animation)) + , fFrameRate(frameRate) { + SkASSERT(fAnimation); + SkASSERT(fFrameRate > 0); + } + + protected: + void onTick(float t) { + // map back from frame # to ms. + const auto t_ms = t * 1000 / fFrameRate; + fAnimation->animationTick(t_ms); + } + + private: + const sk_sp<Animation> fAnimation; + const float fFrameRate; + }; + + const auto resStream = ctx->fResources.openStream(path); + if (!resStream || !resStream->hasLength()) { + LOG("!! Could not open: %s\n", path); + return nullptr; + } + + auto animation = Animation::Make(resStream.get(), ctx->fResources); + if (!animation) { + LOG("!! Could not load nested animation: %s\n", path); + return nullptr; + } + + ctx->fAnimators.push_back(skstd::make_unique<SkottieAnimatorAdapter>(animation, + ctx->fFrameRate)); + + return sk_make_sp<SkottieSGAdapter>(std::move(animation)); +} + +sk_sp<sksg::RenderNode> AttachAssetRef(const json::ValueRef& jlayer, AttachContext* ctx, + sk_sp<sksg::RenderNode>(*attach_proc)(const json::ValueRef& comp, AttachContext* ctx)) { + + const auto refId = jlayer["refId"].toDefault(SkString()); + if (refId.isEmpty()) { + LOG("!! Layer missing refId\n"); + return nullptr; + } + + if (refId.startsWith("$")) { + return AttachNestedAnimation(refId.c_str() + 1, ctx); + } + + const auto* asset_info = ctx->fAssets.find(refId); + if (!asset_info) { + LOG("!! Asset not found: '%s'\n", refId.c_str()); + return nullptr; + } + + if (asset_info->fIsAttaching) { + LOG("!! Asset cycle detected for: '%s'\n", refId.c_str()); + return nullptr; + } + + asset_info->fIsAttaching = true; + auto asset = attach_proc(asset_info->fAsset, ctx); + asset_info->fIsAttaching = false; + + return asset; +} + +sk_sp<sksg::RenderNode> AttachCompLayer(const json::ValueRef& jlayer, AttachContext* ctx, + float* time_bias, float* time_scale) { + SkASSERT(jlayer.isObject()); + + const auto start_time = jlayer["st"].toDefault(0.0f), + stretch_time = jlayer["sr"].toDefault(1.0f); + + *time_bias = -start_time; + *time_scale = sk_ieee_float_divide(1, stretch_time); + if (SkScalarIsNaN(*time_scale)) { + *time_scale = 1; + } + + return AttachAssetRef(jlayer, ctx, AttachComposition); +} + +sk_sp<sksg::RenderNode> AttachSolidLayer(const json::ValueRef& jlayer, AttachContext*, + float*, float*) { + SkASSERT(jlayer.isObject()); + + const auto size = SkSize::Make(jlayer["sw"].toDefault(0.0f), + jlayer["sh"].toDefault(0.0f)); + const auto hex = jlayer["sc"].toDefault(SkString()); + uint32_t c; + if (size.isEmpty() || + !hex.startsWith("#") || + !SkParse::FindHex(hex.c_str() + 1, &c)) { + LogFail(jlayer, "Could not parse solid layer"); + return nullptr; + } + + const SkColor color = 0xff000000 | c; + + return sksg::Draw::Make(sksg::Rect::Make(SkRect::MakeSize(size)), + sksg::Color::Make(color)); +} + +sk_sp<sksg::RenderNode> AttachImageAsset(const json::ValueRef& jimage, AttachContext* ctx) { + SkASSERT(jimage.isObject()); + + const auto name = jimage["p"].toDefault(SkString()), + path = jimage["u"].toDefault(SkString()); + if (name.isEmpty()) + return nullptr; + + // TODO: plumb resource paths explicitly to ResourceProvider? + const auto resName = path.isEmpty() ? name : SkOSPath::Join(path.c_str(), name.c_str()); + const auto resStream = ctx->fResources.openStream(resName.c_str()); + if (!resStream || !resStream->hasLength()) { + LOG("!! Could not load image resource: %s\n", resName.c_str()); + return nullptr; + } + + // TODO: non-intrisic image sizing + return sksg::Image::Make( + SkImage::MakeFromEncoded(SkData::MakeFromStream(resStream.get(), resStream->getLength()))); +} + +sk_sp<sksg::RenderNode> AttachImageLayer(const json::ValueRef& jlayer, AttachContext* ctx, + float*, float*) { + SkASSERT(jlayer.isObject()); + + return AttachAssetRef(jlayer, ctx, AttachImageAsset); +} + +sk_sp<sksg::RenderNode> AttachNullLayer(const json::ValueRef& layer, AttachContext*, float*, float*) { + SkASSERT(layer.isObject()); + + // Null layers are used solely to drive dependent transforms, + // but we use free-floating sksg::Matrices for that purpose. + return nullptr; +} + +sk_sp<sksg::RenderNode> AttachShapeLayer(const json::ValueRef& layer, AttachContext* ctx, + float*, float*) { + SkASSERT(layer.isObject()); + + std::vector<sk_sp<sksg::GeometryNode>> geometryStack; + std::vector<GeometryEffectRec> geometryEffectStack; + AttachShapeContext shapeCtx(ctx, &geometryStack, &geometryEffectStack, ctx->fAnimators.size()); + auto shapeNode = AttachShape(layer["shapes"], &shapeCtx); + + // Trim uncommitted animators: AttachShape consumes effects on the fly, and greedily attaches + // geometries => at the end, we can end up with unused geometries, which are nevertheless alive + // due to attached animators. To avoid this, we track committed animators and discard the + // orphans here. + SkASSERT(shapeCtx.fCommittedAnimators <= ctx->fAnimators.size()); + ctx->fAnimators.resize(shapeCtx.fCommittedAnimators); + + return shapeNode; +} + +sk_sp<sksg::RenderNode> AttachTextLayer(const json::ValueRef& layer, AttachContext*, float*, float*) { + SkASSERT(layer.isObject()); + + LOG("?? Text layer stub\n"); + return nullptr; +} + +struct AttachLayerContext { + AttachLayerContext(const json::ValueRef& jlayers, AttachContext* ctx) + : fLayerList(jlayers), fCtx(ctx) { + SkASSERT(fLayerList.isArray()); + } + + const json::ValueRef fLayerList; + AttachContext* fCtx; + SkTHashMap<int, sk_sp<sksg::Matrix>> fLayerMatrixMap; + sk_sp<sksg::RenderNode> fCurrentMatte; + + sk_sp<sksg::Matrix> AttachLayerMatrix(const json::ValueRef& jlayer) { + SkASSERT(jlayer.isObject()); + + const auto layer_index = jlayer["ind"].toDefault<int>(-1); + if (layer_index < 0) + return nullptr; + + if (auto* m = fLayerMatrixMap.find(layer_index)) + return *m; + + return this->AttachLayerMatrixImpl(jlayer, layer_index); + } + +private: + sk_sp<sksg::Matrix> AttachParentLayerMatrix(const json::ValueRef& jlayer, int layer_index) { + SkASSERT(jlayer.isObject()); + + const auto parent_index = jlayer["parent"].toDefault<int>(-1); + if (parent_index < 0 || parent_index == layer_index) + return nullptr; + + if (auto* m = fLayerMatrixMap.find(parent_index)) + return *m; + + for (const json::ValueRef l : fLayerList) { + if (l["ind"].toDefault<int>(-1) == parent_index) { + return this->AttachLayerMatrixImpl(l, parent_index); + } + } + + return nullptr; + } + + sk_sp<sksg::Matrix> AttachLayerMatrixImpl(const json::ValueRef& jlayer, int layer_index) { + SkASSERT(!fLayerMatrixMap.find(layer_index)); + + // Add a stub entry to break recursion cycles. + fLayerMatrixMap.set(layer_index, nullptr); + + auto parent_matrix = this->AttachParentLayerMatrix(jlayer, layer_index); + + return *fLayerMatrixMap.set(layer_index, AttachMatrix(jlayer["ks"], fCtx, parent_matrix)); + } +}; + +SkBlendMode MaskBlendMode(char mode) { + switch (mode) { + case 'a': return SkBlendMode::kSrcOver; // Additive + case 's': return SkBlendMode::kExclusion; // Subtract + case 'i': return SkBlendMode::kDstIn; // Intersect + case 'l': return SkBlendMode::kLighten; // Lighten + case 'd': return SkBlendMode::kDarken; // Darken + case 'f': return SkBlendMode::kDifference; // Difference + default: break; + } + + return SkBlendMode::kSrcOver; +} + +sk_sp<sksg::RenderNode> AttachMask(const json::ValueRef& jmask, + AttachContext* ctx, + sk_sp<sksg::RenderNode> childNode) { + if (!jmask.isArray()) + return childNode; + + struct MaskRecord { + sk_sp<sksg::Path> mask_path; + sk_sp<sksg::Color> mask_paint; + }; + + SkSTArray<4, MaskRecord, true> mask_stack; + + bool opaque_mask = true; + + for (const json::ValueRef m : jmask) { + if (!m.isObject()) + continue; + + auto mask_path = AttachPath(m["pt"], ctx); + if (!mask_path) { + LogFail(m, "Could not parse mask path"); + continue; + } + + mask_path->setFillType(m["inv"].toDefault(false) + ? SkPath::kInverseWinding_FillType + : SkPath::kWinding_FillType); + + SkString mode; + if (!m["mode"].to(&mode) || + mode.size() != 1 || + !strcmp(mode.c_str(), "n")) { // "None" masks have no effect. + continue; + } + + auto mask_paint = sksg::Color::Make(SK_ColorBLACK); + mask_paint->setAntiAlias(true); + mask_paint->setBlendMode(MaskBlendMode(mode.c_str()[0])); + + const auto animator_count = ctx->fAnimators.size(); + BindProperty<ScalarValue>(m["o"], &ctx->fAnimators, + [mask_paint](const ScalarValue& o) { mask_paint->setOpacity(o * 0.01f); }); + + opaque_mask &= (animator_count == ctx->fAnimators.size() && mask_paint->getOpacity() >= 1); + + mask_stack.push_back({mask_path, mask_paint}); + } + + if (mask_stack.empty()) + return childNode; + + if (mask_stack.count() == 1 && opaque_mask) { + // Single opaque mask => clip path. + return sksg::ClipEffect::Make(std::move(childNode), + std::move(mask_stack.front().mask_path), + true); + } + + auto mask_group = sksg::Group::Make(); + for (const auto& rec : mask_stack) { + mask_group->addChild(sksg::Draw::Make(std::move(rec.mask_path), + std::move(rec.mask_paint))); + + } + + return sksg::MaskEffect::Make(std::move(childNode), std::move(mask_group)); +} + +sk_sp<sksg::RenderNode> AttachLayer(const json::ValueRef& jlayer, AttachLayerContext* layerCtx) { + if (!jlayer.isObject()) + return nullptr; + + using LayerAttacher = sk_sp<sksg::RenderNode> (*)(const json::ValueRef&, AttachContext*, + float* time_bias, float* time_scale); + static constexpr LayerAttacher gLayerAttachers[] = { + AttachCompLayer, // 'ty': 0 + AttachSolidLayer, // 'ty': 1 + AttachImageLayer, // 'ty': 2 + AttachNullLayer, // 'ty': 3 + AttachShapeLayer, // 'ty': 4 + AttachTextLayer, // 'ty': 5 + }; + + int type = jlayer["ty"].toDefault<int>(-1); + if (type < 0 || type >= SkTo<int>(SK_ARRAY_COUNT(gLayerAttachers))) { + return nullptr; + } + + sksg::AnimatorList layer_animators; + AttachContext local_ctx = { layerCtx->fCtx->fResources, + layerCtx->fCtx->fAssets, + layerCtx->fCtx->fFrameRate, + layer_animators}; + + // Layer attachers may adjust these. + float time_bias = 0, + time_scale = 1; + + // Layer content. + auto layer = gLayerAttachers[type](jlayer, &local_ctx, &time_bias, &time_scale); + + // Clip layers with explicit dimensions. + float w = 0, h = 0; + if (jlayer["w"].to(&w) && jlayer["h"].to(&h)) { + layer = sksg::ClipEffect::Make(std::move(layer), + sksg::Rect::Make(SkRect::MakeWH(w, h)), + true); + } + + // Optional layer mask. + layer = AttachMask(jlayer["masksProperties"], &local_ctx, std::move(layer)); + + // Optional layer transform. + if (auto layerMatrix = layerCtx->AttachLayerMatrix(jlayer)) { + layer = sksg::Transform::Make(std::move(layer), std::move(layerMatrix)); + } + + // Optional layer opacity. + layer = AttachOpacity(jlayer["ks"], &local_ctx, std::move(layer)); + + class LayerController final : public sksg::GroupAnimator { + public: + LayerController(sksg::AnimatorList&& layer_animators, + sk_sp<sksg::OpacityEffect> controlNode, + float in, float out, + float time_bias, float time_scale) + : INHERITED(std::move(layer_animators)) + , fControlNode(std::move(controlNode)) + , fIn(in) + , fOut(out) + , fTimeBias(time_bias) + , fTimeScale(time_scale) {} + + void onTick(float t) override { + const auto active = (t >= fIn && t <= fOut); + + // Keep the layer fully transparent except for its [in..out] lifespan. + // (note: opacity == 0 disables rendering, while opacity == 1 is a noop) + fControlNode->setOpacity(active ? 1 : 0); + + // Dispatch ticks only while active. + if (active) + this->INHERITED::onTick((t + fTimeBias) * fTimeScale); + } + + private: + const sk_sp<sksg::OpacityEffect> fControlNode; + const float fIn, + fOut, + fTimeBias, + fTimeScale; + + using INHERITED = sksg::GroupAnimator; + }; + + auto controller_node = sksg::OpacityEffect::Make(std::move(layer)); + const auto in = jlayer["ip"].toDefault(0.0f), + out = jlayer["op"].toDefault(in); + + if (!jlayer["tm"].isNull()) { + LogFail(jlayer["tm"], "Unsupported time remapping"); + } + + if (in >= out || !controller_node) + return nullptr; + + layerCtx->fCtx->fAnimators.push_back( + skstd::make_unique<LayerController>(std::move(layer_animators), + controller_node, + in, + out, + time_bias, + time_scale)); + + if (jlayer["td"].toDefault(false)) { + // This layer is a matte. We apply it as a mask to the next layer. + layerCtx->fCurrentMatte = std::move(controller_node); + return nullptr; + } + + if (layerCtx->fCurrentMatte) { + // There is a pending matte. Apply and reset. + static constexpr sksg::MaskEffect::Mode gMaskModes[] = { + sksg::MaskEffect::Mode::kNormal, // tt: 1 + sksg::MaskEffect::Mode::kInvert, // tt: 2 + }; + const auto matteType = jlayer["tt"].toDefault<int>(1) - 1; + + if (matteType >= 0 && matteType < SkTo<int>(SK_ARRAY_COUNT(gMaskModes))) { + return sksg::MaskEffect::Make(std::move(controller_node), + std::move(layerCtx->fCurrentMatte), + gMaskModes[matteType]); + } + layerCtx->fCurrentMatte.reset(); + } + + return std::move(controller_node); +} + +sk_sp<sksg::RenderNode> AttachComposition(const json::ValueRef& comp, AttachContext* ctx) { + if (!comp.isObject()) + return nullptr; + + const auto jlayers = comp["layers"]; + if (!jlayers.isArray()) + return nullptr; + + SkSTArray<16, sk_sp<sksg::RenderNode>, true> layers; + AttachLayerContext layerCtx(jlayers, ctx); + + for (const json::ValueRef l : jlayers) { + if (auto layer_fragment = AttachLayer(l, &layerCtx)) { + layers.push_back(std::move(layer_fragment)); + } + } + + if (layers.empty()) { + return nullptr; + } + + // Layers are painted in bottom->top order. + auto comp_group = sksg::Group::Make(); + for (int i = layers.count() - 1; i >= 0; --i) { + comp_group->addChild(std::move(layers[i])); + } + + return std::move(comp_group); +} + +} // namespace + +sk_sp<Animation> Animation::Make(SkStream* stream, const ResourceProvider& res, Stats* stats) { + Stats stats_storage; + if (!stats) + stats = &stats_storage; + memset(stats, 0, sizeof(struct Stats)); + + if (!stream->hasLength()) { + // TODO: handle explicit buffering? + LOG("!! cannot parse streaming content\n"); + return nullptr; + } + + stats->fJsonSize = stream->getLength(); + const auto t0 = SkTime::GetMSecs(); + + const json::Document doc(stream); + const auto json = doc.root(); + if (!json.isObject()) + return nullptr; + + const auto t1 = SkTime::GetMSecs(); + stats->fJsonParseTimeMS = t1 - t0; + + const auto version = json["v"].toDefault(SkString()); + const auto size = SkSize::Make(json["w"].toDefault(0.0f), + json["h"].toDefault(0.0f)); + const auto fps = json["fr"].toDefault(-1.0f); + + if (size.isEmpty() || version.isEmpty() || fps <= 0) { + LOG("!! invalid animation params (version: %s, size: [%f %f], frame rate: %f)", + version.c_str(), size.width(), size.height(), fps); + return nullptr; + } + + const auto anim = + sk_sp<Animation>(new Animation(res, std::move(version), size, fps, json, stats)); + const auto t2 = SkTime::GetMSecs(); + stats->fSceneParseTimeMS = t2 - t1; + stats->fTotalLoadTimeMS = t2 - t0; + + return anim; +} + +sk_sp<Animation> Animation::MakeFromFile(const char path[], const ResourceProvider* res, + Stats* stats) { + class DirectoryResourceProvider final : public ResourceProvider { + public: + explicit DirectoryResourceProvider(SkString dir) : fDir(std::move(dir)) {} + + std::unique_ptr<SkStream> openStream(const char resource[]) const override { + const auto resPath = SkOSPath::Join(fDir.c_str(), resource); + return SkStream::MakeFromFile(resPath.c_str()); + } + + private: + const SkString fDir; + }; + + const auto jsonStream = SkStream::MakeFromFile(path); + if (!jsonStream) + return nullptr; + + std::unique_ptr<ResourceProvider> defaultProvider; + if (!res) { + defaultProvider = skstd::make_unique<DirectoryResourceProvider>(SkOSPath::Dirname(path)); + } + + return Make(jsonStream.get(), res ? *res : *defaultProvider, stats); +} + +Animation::Animation(const ResourceProvider& resources, + SkString version, const SkSize& size, SkScalar fps, const json::ValueRef& json, + Stats* stats) + : fVersion(std::move(version)) + , fSize(size) + , fFrameRate(fps) + , fInPoint(json["ip"].toDefault(0.0f)) + , fOutPoint(SkTMax(json["op"].toDefault(SK_ScalarMax), fInPoint)) { + + AssetMap assets; + for (const json::ValueRef asset : json["assets"]) { + if (asset.isObject()) { + assets.set(asset["id"].toDefault(SkString()), { asset, false }); + } + } + + sksg::AnimatorList animators; + AttachContext ctx = { resources, assets, fFrameRate, animators }; + auto root = AttachComposition(json, &ctx); + + stats->fAnimatorCount = animators.size(); + + fScene = sksg::Scene::Make(std::move(root), std::move(animators)); + + // In case the client calls render before the first tick. + this->animationTick(0); +} + +Animation::~Animation() = default; + +void Animation::setShowInval(bool show) { + if (fScene) { + fScene->setShowInval(show); + } +} + +void Animation::render(SkCanvas* canvas, const SkRect* dstR) const { + if (!fScene) + return; + + SkAutoCanvasRestore restore(canvas, true); + const SkRect srcR = SkRect::MakeSize(this->size()); + if (dstR) { + canvas->concat(SkMatrix::MakeRectToRect(srcR, *dstR, SkMatrix::kCenter_ScaleToFit)); + } + canvas->clipRect(srcR); + fScene->render(canvas); +} + +void Animation::animationTick(SkMSec ms) { + if (!fScene) + return; + + // 't' in the BM model really means 'frame #' + auto t = static_cast<float>(ms) * fFrameRate / 1000; + + t = fInPoint + std::fmod(t, fOutPoint - fInPoint); + + fScene->animate(t); +} + +} // namespace skottie |