/* * 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 fDuration; 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 time_scale) : fAnimation(std::move(animation)) , fTimeScale(time_scale) { SkASSERT(fAnimation); } protected: void onTick(float t) { // TODO: we prolly need more sophisticated timeline mapping for nested animations. fAnimation->seek(t * fTimeScale); } private: const sk_sp fAnimation; const float fTimeScale; }; 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, animation->duration() / ctx->fDuration)); 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->fDuration, 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* provider, 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; } class NullResourceProvider final : public ResourceProvider { std::unique_ptr openStream(const char[]) const { return nullptr; } }; NullResourceProvider null_provider; const auto anim = sk_sp(new Animation(provider ? *provider : null_provider, 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.get(), 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, this->duration(), 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->seek(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::seek(SkScalar t) { if (!fScene) return; fScene->animate(fInPoint + SkTPin(t, 0.0f, 1.0f) * (fOutPoint - fInPoint)); } } // namespace skottie