/* * Copyright 2017 Google Inc. * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #include "Skotty.h" #include "SkCanvas.h" #include "SkottyAnimator.h" #include "SkottyPriv.h" #include "SkottyProperties.h" #include "SkData.h" #include "SkImage.h" #include "SkMakeUnique.h" #include "SkOSPath.h" #include "SkPaint.h" #include "SkPath.h" #include "SkPoint.h" #include "SkSGColor.h" #include "SkSGDraw.h" #include "SkSGGroup.h" #include "SkSGImage.h" #include "SkSGInvalidationController.h" #include "SkSGMerge.h" #include "SkSGOpacityEffect.h" #include "SkSGPath.h" #include "SkSGRect.h" #include "SkSGTransform.h" #include "SkSGTrimEffect.h" #include "SkStream.h" #include "SkTArray.h" #include "SkTHash.h" #include #include #include #include "stdlib.h" namespace skotty { namespace { using AssetMap = SkTHashMap; struct AttachContext { const ResourceProvider& fResources; const AssetMap& fAssets; SkTArray>& fAnimators; }; bool LogFail(const Json::Value& json, const char* msg) { const auto dump = json.toStyledString(); LOG("!! %s: %s", msg, dump.c_str()); return false; } // This is the workhorse for binding properties: depending on whether the property is animated, // it will either apply immediately or instantiate and attach a keyframe animator. template bool BindProperty(const Json::Value& jprop, AttachContext* ctx, const sk_sp& node, typename Animator::ApplyFuncT&& apply) { if (!jprop.isObject()) return false; const auto& jpropA = jprop["a"]; const auto& jpropK = jprop["k"]; // Older Json versions don't have an "a" animation marker. // For those, we attempt to parse both ways. if (jpropA.isNull() || !ParseBool(jpropA, "false")) { ValT val; if (ValueTraits::Parse(jpropK, &val)) { // Static property. apply(node.get(), val); return true; } if (!jpropA.isNull()) { return LogFail(jprop, "Could not parse (explicit) static property"); } } // Keyframe property. using AnimatorT = Animator; auto animator = AnimatorT::Make(ParseFrames(jpropK), node, std::move(apply)); if (!animator) { return LogFail(jprop, "Could not parse keyframed property"); } ctx->fAnimators.push_back(std::move(animator)); return true; } sk_sp AttachMatrix(const Json::Value& t, AttachContext* ctx, sk_sp parentMatrix) { if (!t.isObject()) return nullptr; auto matrix = sksg::Matrix::Make(SkMatrix::I(), std::move(parentMatrix)); auto composite = sk_make_sp(matrix); auto anchor_attached = BindProperty(t["a"], ctx, composite, [](CompositeTransform* node, const VectorValue& a) { node->setAnchorPoint(ValueTraits::As(a)); }); auto position_attached = BindProperty(t["p"], ctx, composite, [](CompositeTransform* node, const VectorValue& p) { node->setPosition(ValueTraits::As(p)); }); auto scale_attached = BindProperty(t["s"], ctx, composite, [](CompositeTransform* node, const VectorValue& s) { node->setScale(ValueTraits::As(s)); }); auto rotation_attached = BindProperty(t["r"], ctx, composite, [](CompositeTransform* node, const ScalarValue& r) { node->setRotation(r); }); auto skew_attached = BindProperty(t["sk"], ctx, composite, [](CompositeTransform* node, const ScalarValue& sk) { node->setSkew(sk); }); auto skewaxis_attached = BindProperty(t["sa"], ctx, composite, [](CompositeTransform* node, const ScalarValue& sa) { node->setSkewAxis(sa); }); if (!anchor_attached && !position_attached && !scale_attached && !rotation_attached && !skew_attached && !skewaxis_attached) { LogFail(t, "Could not parse transform"); return nullptr; } return matrix; } sk_sp AttachOpacity(const Json::Value& jtransform, AttachContext* ctx, sk_sp childNode) { if (!jtransform.isObject() || !childNode) return childNode; // This is more peeky than other attachers, because we want to avoid redundant opacity // nodes for the extremely common case of static opaciy == 100. const auto& opacity = jtransform["o"]; if (opacity.isObject() && !ParseBool(opacity["a"], true) && ParseScalar(opacity["k"], -1) == 100) { // Ignoring static full opacity. return childNode; } auto opacityNode = sksg::OpacityEffect::Make(childNode); BindProperty(opacity, ctx, opacityNode, [](sksg::OpacityEffect* node, const ScalarValue& o) { // BM opacity is [0..100] node->setOpacity(o * 0.01f); }); return opacityNode; } sk_sp AttachShape(const Json::Value&, AttachContext* ctx); sk_sp AttachComposition(const Json::Value&, AttachContext* ctx); sk_sp AttachShapeGroup(const Json::Value& jgroup, AttachContext* ctx) { SkASSERT(jgroup.isObject()); return AttachShape(jgroup["it"], ctx); } sk_sp AttachPathGeometry(const Json::Value& jpath, AttachContext* ctx) { SkASSERT(jpath.isObject()); auto path_node = sksg::Path::Make(); auto path_attached = BindProperty(jpath["ks"], ctx, path_node, [](sksg::Path* node, const ShapeValue& p) { node->setPath(p); }); if (path_attached) LOG("** Attached path geometry - verbs: %d\n", path_node->getPath().countVerbs()); return path_attached ? path_node : nullptr; } sk_sp AttachRRectGeometry(const Json::Value& jrect, AttachContext* ctx) { SkASSERT(jrect.isObject()); auto rect_node = sksg::RRect::Make(); auto composite = sk_make_sp(rect_node); auto p_attached = BindProperty(jrect["p"], ctx, composite, [](CompositeRRect* node, const VectorValue& p) { node->setPosition(ValueTraits::As(p)); }); auto s_attached = BindProperty(jrect["s"], ctx, composite, [](CompositeRRect* node, const VectorValue& s) { node->setSize(ValueTraits::As(s)); }); auto r_attached = BindProperty(jrect["r"], ctx, composite, [](CompositeRRect* node, const ScalarValue& r) { node->setRadius(SkSize::Make(r, r)); }); if (!p_attached && !s_attached && !r_attached) { return nullptr; } LOG("** Attached (r)rect geometry\n"); return rect_node; } sk_sp AttachEllipseGeometry(const Json::Value& jellipse, AttachContext* ctx) { SkASSERT(jellipse.isObject()); auto rect_node = sksg::RRect::Make(); auto composite = sk_make_sp(rect_node); auto p_attached = BindProperty(jellipse["p"], ctx, composite, [](CompositeRRect* node, const VectorValue& p) { node->setPosition(ValueTraits::As(p)); }); auto s_attached = BindProperty(jellipse["s"], ctx, composite, [](CompositeRRect* node, const VectorValue& s) { const auto sz = ValueTraits::As(s); node->setSize(sz); node->setRadius(SkSize::Make(sz.width() / 2, sz.height() / 2)); }); if (!p_attached && !s_attached) { return nullptr; } LOG("** Attached ellipse geometry\n"); return rect_node; } sk_sp AttachPolystarGeometry(const Json::Value& jstar, AttachContext* ctx) { SkASSERT(jstar.isObject()); static constexpr CompositePolyStar::Type gTypes[] = { CompositePolyStar::Type::kStar, // "sy": 1 CompositePolyStar::Type::kPoly, // "sy": 2 }; const auto type = ParseInt(jstar["sy"], 0) - 1; if (type < 0 || type >= SkTo(SK_ARRAY_COUNT(gTypes))) { LogFail(jstar, "Unknown polystar type"); return nullptr; } auto path_node = sksg::Path::Make(); auto composite = sk_make_sp(path_node, gTypes[type]); BindProperty(jstar["p"], ctx, composite, [](CompositePolyStar* node, const VectorValue& p) { node->setPosition(ValueTraits::As(p)); }); BindProperty(jstar["pt"], ctx, composite, [](CompositePolyStar* node, const ScalarValue& pt) { node->setPointCount(pt); }); BindProperty(jstar["ir"], ctx, composite, [](CompositePolyStar* node, const ScalarValue& ir) { node->setInnerRadius(ir); }); BindProperty(jstar["or"], ctx, composite, [](CompositePolyStar* node, const ScalarValue& otr) { node->setOuterRadius(otr); }); BindProperty(jstar["is"], ctx, composite, [](CompositePolyStar* node, const ScalarValue& is) { node->setInnerRoundness(is); }); BindProperty(jstar["os"], ctx, composite, [](CompositePolyStar* node, const ScalarValue& os) { node->setOuterRoundness(os); }); BindProperty(jstar["r"], ctx, composite, [](CompositePolyStar* node, const ScalarValue& r) { node->setRotation(r); }); return path_node; } sk_sp AttachColorPaint(const Json::Value& obj, AttachContext* ctx) { SkASSERT(obj.isObject()); auto color_node = sksg::Color::Make(SK_ColorBLACK); color_node->setAntiAlias(true); auto color_attached = BindProperty(obj["c"], ctx, color_node, [](sksg::Color* node, const VectorValue& c) { node->setColor(ValueTraits::As(c)); }); return color_attached ? color_node : nullptr; } sk_sp AttachFillPaint(const Json::Value& jfill, AttachContext* ctx) { SkASSERT(jfill.isObject()); auto color = AttachColorPaint(jfill, ctx); if (color) { LOG("** Attached color fill: 0x%x\n", color->getColor()); } return color; } sk_sp AttachStrokePaint(const Json::Value& jstroke, AttachContext* ctx) { SkASSERT(jstroke.isObject()); auto stroke_node = AttachColorPaint(jstroke, ctx); if (!stroke_node) return nullptr; LOG("** Attached color stroke: 0x%x\n", stroke_node->getColor()); stroke_node->setStyle(SkPaint::kStroke_Style); auto width_attached = BindProperty(jstroke["w"], ctx, stroke_node, [](sksg::Color* node, const ScalarValue& w) { node->setStrokeWidth(w); }); if (!width_attached) return nullptr; stroke_node->setStrokeMiter(ParseScalar(jstroke["ml"], 4)); static constexpr SkPaint::Join gJoins[] = { SkPaint::kMiter_Join, SkPaint::kRound_Join, SkPaint::kBevel_Join, }; stroke_node->setStrokeJoin(gJoins[SkTPin(ParseInt(jstroke["lj"], 1) - 1, 0, SK_ARRAY_COUNT(gJoins) - 1)]); static constexpr SkPaint::Cap gCaps[] = { SkPaint::kButt_Cap, SkPaint::kRound_Cap, SkPaint::kSquare_Cap, }; stroke_node->setStrokeCap(gCaps[SkTPin(ParseInt(jstroke["lc"], 1) - 1, 0, SK_ARRAY_COUNT(gCaps) - 1)]); return stroke_node; } std::vector> AttachMergeGeometryEffect( const Json::Value& jmerge, AttachContext* ctx, std::vector>&& geos) { std::vector> merged; static constexpr sksg::Merge::Mode gModes[] = { sksg::Merge::Mode::kMerge, // "mm": 1 sksg::Merge::Mode::kUnion, // "mm": 2 sksg::Merge::Mode::kDifference, // "mm": 3 sksg::Merge::Mode::kIntersect, // "mm": 4 sksg::Merge::Mode::kXOR , // "mm": 5 }; const auto mode = gModes[SkTPin(ParseInt(jmerge["mm"], 1) - 1, 0, SK_ARRAY_COUNT(gModes) - 1)]; merged.push_back(sksg::Merge::Make(std::move(geos), mode)); LOG("** Attached merge path effect, mode: %d\n", mode); return merged; } std::vector> AttachTrimGeometryEffect( const Json::Value& jtrim, AttachContext* ctx, std::vector>&& geos) { enum class Mode { kMerged, // "m": 1 kSeparate, // "m": 2 } gModes[] = { Mode::kMerged, Mode::kSeparate }; const auto mode = gModes[SkTPin(ParseInt(jtrim["m"], 1) - 1, 0, SK_ARRAY_COUNT(gModes) - 1)]; std::vector> inputs; if (mode == Mode::kMerged) { inputs.push_back(sksg::Merge::Make(std::move(geos), sksg::Merge::Mode::kMerge)); } else { inputs = std::move(geos); } std::vector> trimmed; trimmed.reserve(inputs.size()); for (const auto& i : inputs) { const auto trim = sksg::TrimEffect::Make(i); trimmed.push_back(trim); BindProperty(jtrim["s"], ctx, trim, [](sksg::TrimEffect* node, const ScalarValue& s) { node->setStart(s * 0.01f); }); BindProperty(jtrim["e"], ctx, trim, [](sksg::TrimEffect* node, const ScalarValue& e) { node->setEnd(e * 0.01f); }); // TODO: "offset" doesn't currently work the same as BM - figure out what's going on. BindProperty(jtrim["o"], ctx, trim, [](sksg::TrimEffect* node, const ScalarValue& o) { node->setOffset(o * 0.01f); }); } return trimmed; } using GeometryAttacherT = sk_sp (*)(const Json::Value&, AttachContext*); static constexpr GeometryAttacherT gGeometryAttachers[] = { AttachPathGeometry, AttachRRectGeometry, AttachEllipseGeometry, AttachPolystarGeometry, }; using PaintAttacherT = sk_sp (*)(const Json::Value&, AttachContext*); static constexpr PaintAttacherT gPaintAttachers[] = { AttachFillPaint, AttachStrokePaint, }; using GroupAttacherT = sk_sp (*)(const Json::Value&, AttachContext*); static constexpr GroupAttacherT gGroupAttachers[] = { AttachShapeGroup, }; using GeometryEffectAttacherT = std::vector> (*)(const Json::Value&, AttachContext*, std::vector>&&); static constexpr GeometryEffectAttacherT gGeometryEffectAttachers[] = { AttachMergeGeometryEffect, AttachTrimGeometryEffect, }; enum class ShapeType { kGeometry, kGeometryEffect, kPaint, kGroup, kTransform, }; struct ShapeInfo { const char* fTypeString; ShapeType fShapeType; uint32_t fAttacherIndex; // index into respective attacher tables }; const ShapeInfo* FindShapeInfo(const Json::Value& shape) { static constexpr ShapeInfo gShapeInfo[] = { { "el", ShapeType::kGeometry , 2 }, // ellipse -> AttachEllipseGeometry { "fl", ShapeType::kPaint , 0 }, // fill -> AttachFillPaint { "gr", ShapeType::kGroup , 0 }, // group -> AttachShapeGroup { "mm", ShapeType::kGeometryEffect, 0 }, // merge -> AttachMergeGeometryEffect { "rc", ShapeType::kGeometry , 1 }, // rrect -> AttachRRectGeometry { "sh", ShapeType::kGeometry , 0 }, // shape -> AttachPathGeometry { "sr", ShapeType::kGeometry , 3 }, // polystar -> AttachPolyStarGeometry { "st", ShapeType::kPaint , 1 }, // stroke -> AttachStrokePaint { "tm", ShapeType::kGeometryEffect, 1 }, // trim -> AttachTrimGeometryEffect { "tr", ShapeType::kTransform , 0 }, // transform -> In-place handler }; if (!shape.isObject()) return nullptr; const auto& type = shape["ty"]; if (!type.isString()) return nullptr; const auto* info = bsearch(type.asCString(), gShapeInfo, SK_ARRAY_COUNT(gShapeInfo), sizeof(ShapeInfo), [](const void* key, const void* info) { return strcmp(static_cast(key), static_cast(info)->fTypeString); }); return static_cast(info); } sk_sp AttachShape(const Json::Value& shapeArray, AttachContext* ctx) { if (!shapeArray.isArray()) return nullptr; // (https://helpx.adobe.com/after-effects/using/overview-shape-layers-paths-vector.html#groups_and_render_order_for_shapes_and_shape_attributes) // // Render order for shapes within a shape layer // // The rules for rendering a shape layer are similar to the rules for rendering a composition // that contains nested compositions: // // * Within a group, the shape at the bottom of the Timeline panel stacking order is rendered // first. // // * All path operations within a group are performed before paint operations. This means, // for example, that the stroke follows the distortions in the path made by the Wiggle Paths // path operation. Path operations within a group are performed from top to bottom. // // * Paint operations within a group are performed from the bottom to the top in the Timeline // panel stacking order. This means, for example, that a stroke is rendered on top of // (in front of) a stroke that appears after it in the Timeline panel. // sk_sp shape_group = sksg::Group::Make(); sk_sp xformed_group = shape_group; std::vector> geos; std::vector> draws; for (const auto& s : shapeArray) { const auto* info = FindShapeInfo(s); if (!info) { LogFail(s.isObject() ? s["ty"] : s, "Unknown shape"); continue; } switch (info->fShapeType) { case ShapeType::kGeometry: { SkASSERT(info->fAttacherIndex < SK_ARRAY_COUNT(gGeometryAttachers)); if (auto geo = gGeometryAttachers[info->fAttacherIndex](s, ctx)) { geos.push_back(std::move(geo)); } } break; case ShapeType::kGeometryEffect: { SkASSERT(info->fAttacherIndex < SK_ARRAY_COUNT(gGeometryEffectAttachers)); geos = gGeometryEffectAttachers[info->fAttacherIndex](s, ctx, std::move(geos)); } break; case ShapeType::kPaint: { SkASSERT(info->fAttacherIndex < SK_ARRAY_COUNT(gPaintAttachers)); if (auto paint = gPaintAttachers[info->fAttacherIndex](s, ctx)) { for (const auto& geo : geos) { draws.push_back(sksg::Draw::Make(geo, paint)); } } } break; case ShapeType::kGroup: { SkASSERT(info->fAttacherIndex < SK_ARRAY_COUNT(gGroupAttachers)); if (auto group = gGroupAttachers[info->fAttacherIndex](s, ctx)) { draws.push_back(std::move(group)); } } break; case ShapeType::kTransform: { // TODO: BM appears to transform the geometry, not the draw op itself. if (auto matrix = AttachMatrix(s, ctx, nullptr)) { xformed_group = sksg::Transform::Make(std::move(xformed_group), std::move(matrix)); } xformed_group = AttachOpacity(s, ctx, std::move(xformed_group)); } break; } } if (draws.empty()) { return nullptr; } for (auto draw = draws.rbegin(); draw != draws.rend(); ++draw) { shape_group->addChild(std::move(*draw)); } LOG("** Attached shape: %zd draws.\n", draws.size()); return xformed_group; } sk_sp AttachCompLayer(const Json::Value& layer, AttachContext* ctx) { SkASSERT(layer.isObject()); auto refId = ParseString(layer["refId"], ""); if (refId.isEmpty()) { LOG("!! Comp layer missing refId\n"); return nullptr; } const auto* comp = ctx->fAssets.find(refId); if (!comp) { LOG("!! Pre-comp not found: '%s'\n", refId.c_str()); return nullptr; } // TODO: cycle detection return AttachComposition(**comp, ctx); } sk_sp AttachSolidLayer(const Json::Value& layer, AttachContext*) { SkASSERT(layer.isObject()); LOG("?? Solid layer stub\n"); return nullptr; } sk_sp AttachImageAsset(const Json::Value& jimage, AttachContext* ctx) { SkASSERT(jimage.isObject()); const auto name = ParseString(jimage["p"], ""), path = ParseString(jimage["u"], ""); if (name.isEmpty()) return nullptr; // TODO: plumb resource paths explicitly to ResourceProvider? const auto resName = path.isEmpty() ? name : SkOSPath::Join(path.c_str(), name.c_str()); const auto resStream = ctx->fResources.openStream(resName.c_str()); if (!resStream || !resStream->hasLength()) { LOG("!! Could not load image resource: %s\n", resName.c_str()); return nullptr; } // TODO: non-intrisic image sizing return sksg::Image::Make( SkImage::MakeFromEncoded(SkData::MakeFromStream(resStream.get(), resStream->getLength()))); } sk_sp AttachImageLayer(const Json::Value& layer, AttachContext* ctx) { SkASSERT(layer.isObject()); auto refId = ParseString(layer["refId"], ""); if (refId.isEmpty()) { LOG("!! Image layer missing refId\n"); return nullptr; } const auto* jimage = ctx->fAssets.find(refId); if (!jimage) { LOG("!! Image asset not found: '%s'\n", refId.c_str()); return nullptr; } return AttachImageAsset(**jimage, ctx); } sk_sp AttachNullLayer(const Json::Value& layer, AttachContext*) { SkASSERT(layer.isObject()); // Null layers are used solely to drive dependent transforms, // but we use free-floating sksg::Matrices for that purpose. return nullptr; } sk_sp AttachShapeLayer(const Json::Value& layer, AttachContext* ctx) { SkASSERT(layer.isObject()); LOG("** Attaching shape layer ind: %d\n", ParseInt(layer["ind"], 0)); return AttachShape(layer["shapes"], ctx); } sk_sp AttachTextLayer(const Json::Value& layer, AttachContext*) { SkASSERT(layer.isObject()); LOG("?? Text layer stub\n"); return nullptr; } struct AttachLayerContext { AttachLayerContext(const Json::Value& jlayers, AttachContext* ctx) : fLayerList(jlayers), fCtx(ctx) {} const Json::Value& fLayerList; AttachContext* fCtx; std::unordered_map> fLayerMatrixCache; std::unordered_map fLayerIndexCache; const Json::Value* findLayer(int index) { SkASSERT(fLayerList.isArray()); if (index < 0) { return nullptr; } const auto cached = fLayerIndexCache.find(index); if (cached != fLayerIndexCache.end()) { return cached->second; } for (const auto& l : fLayerList) { if (!l.isObject()) { continue; } if (ParseInt(l["ind"], -1) == index) { fLayerIndexCache.insert(std::make_pair(index, &l)); return &l; } } return nullptr; } sk_sp AttachLayerMatrix(const Json::Value& jlayer) { SkASSERT(jlayer.isObject()); const auto cached = fLayerMatrixCache.find(&jlayer); if (cached != fLayerMatrixCache.end()) { return cached->second; } const auto* parentLayer = this->findLayer(ParseInt(jlayer["parent"], -1)); // TODO: cycle detection? auto parentMatrix = (parentLayer && parentLayer != &jlayer) ? this->AttachLayerMatrix(*parentLayer) : nullptr; auto layerMatrix = AttachMatrix(jlayer["ks"], fCtx, std::move(parentMatrix)); fLayerMatrixCache.insert(std::make_pair(&jlayer, layerMatrix)); return layerMatrix; } }; sk_sp AttachLayer(const Json::Value& jlayer, AttachLayerContext* layerCtx) { if (!jlayer.isObject()) return nullptr; using LayerAttacher = sk_sp (*)(const Json::Value&, AttachContext*); static constexpr LayerAttacher gLayerAttachers[] = { AttachCompLayer, // 'ty': 0 AttachSolidLayer, // 'ty': 1 AttachImageLayer, // 'ty': 2 AttachNullLayer, // 'ty': 3 AttachShapeLayer, // 'ty': 4 AttachTextLayer, // 'ty': 5 }; int type = ParseInt(jlayer["ty"], -1); if (type < 0 || type >= SkTo(SK_ARRAY_COUNT(gLayerAttachers))) { return nullptr; } // Layer content. auto layer = gLayerAttachers[type](jlayer, layerCtx->fCtx); if (auto layerMatrix = layerCtx->AttachLayerMatrix(jlayer)) { // Optional layer transform. layer = sksg::Transform::Make(std::move(layer), std::move(layerMatrix)); } // Optional layer opacity. layer = AttachOpacity(jlayer["ks"], layerCtx->fCtx, std::move(layer)); // TODO: we should also disable related/inactive animators. class Activator final : public AnimatorBase { public: Activator(sk_sp controlNode, float in, float out) : fControlNode(std::move(controlNode)) , fIn(in) , fOut(out) {} void tick(float t) override { // Keep the layer fully transparent except for its [in..out] lifespan. // (note: opacity == 0 disables rendering, while opacity == 1 is a noop) fControlNode->setOpacity(t >= fIn && t <= fOut ? 1 : 0); } private: const sk_sp fControlNode; const float fIn, fOut; }; auto layerControl = sksg::OpacityEffect::Make(std::move(layer)); const auto in = ParseScalar(jlayer["ip"], 0), out = ParseScalar(jlayer["op"], in); if (in >= out || ! layerControl) return nullptr; layerCtx->fCtx->fAnimators.push_back(skstd::make_unique(layerControl, in, out)); return layerControl; } sk_sp AttachComposition(const Json::Value& comp, AttachContext* ctx) { if (!comp.isObject()) return nullptr; const auto& jlayers = comp["layers"]; if (!jlayers.isArray()) return nullptr; SkSTArray<16, sk_sp, true> layers; AttachLayerContext layerCtx(jlayers, ctx); for (const auto& l : jlayers) { if (auto layer_fragment = AttachLayer(l, &layerCtx)) { layers.push_back(std::move(layer_fragment)); } } if (layers.empty()) { return nullptr; } // Layers are painted in bottom->top order. auto comp_group = sksg::Group::Make(); for (int i = layers.count() - 1; i >= 0; --i) { comp_group->addChild(std::move(layers[i])); } LOG("** Attached composition '%s': %d layers.\n", ParseString(comp["id"], "").c_str(), layers.count()); return comp_group; } } // namespace std::unique_ptr Animation::Make(SkStream* stream, const ResourceProvider& res) { if (!stream->hasLength()) { // TODO: handle explicit buffering? LOG("!! cannot parse streaming content\n"); return nullptr; } Json::Value json; { auto data = SkData::MakeFromStream(stream, stream->getLength()); if (!data) { LOG("!! could not read stream\n"); return nullptr; } Json::Reader reader; auto dataStart = static_cast(data->data()); if (!reader.parse(dataStart, dataStart + data->size(), json, false) || !json.isObject()) { LOG("!! failed to parse json: %s\n", reader.getFormattedErrorMessages().c_str()); return nullptr; } } const auto version = ParseString(json["v"], ""); const auto size = SkSize::Make(ParseScalar(json["w"], -1), ParseScalar(json["h"], -1)); const auto fps = ParseScalar(json["fr"], -1); if (size.isEmpty() || version.isEmpty() || fps < 0) { LOG("!! invalid animation params (version: %s, size: [%f %f], frame rate: %f)", version.c_str(), size.width(), size.height(), fps); return nullptr; } return std::unique_ptr(new Animation(res, std::move(version), size, fps, json)); } std::unique_ptr Animation::MakeFromFile(const char path[], const ResourceProvider* res) { class DirectoryResourceProvider final : public ResourceProvider { public: explicit DirectoryResourceProvider(SkString dir) : fDir(std::move(dir)) {} std::unique_ptr openStream(const char resource[]) const override { const auto resPath = SkOSPath::Join(fDir.c_str(), resource); return SkStream::MakeFromFile(resPath.c_str()); } private: const SkString fDir; }; const auto jsonStream = SkStream::MakeFromFile(path); if (!jsonStream) return nullptr; std::unique_ptr defaultProvider; if (!res) { defaultProvider = skstd::make_unique(SkOSPath::Dirname(path)); } return Make(jsonStream.get(), res ? *res : *defaultProvider); } Animation::Animation(const ResourceProvider& resources, SkString version, const SkSize& size, SkScalar fps, const Json::Value& json) : fVersion(std::move(version)) , fSize(size) , fFrameRate(fps) , fInPoint(ParseScalar(json["ip"], 0)) , fOutPoint(SkTMax(ParseScalar(json["op"], SK_ScalarMax), fInPoint)) { AssetMap assets; for (const auto& asset : json["assets"]) { if (!asset.isObject()) { continue; } assets.set(ParseString(asset["id"], ""), &asset); } AttachContext ctx = { resources, assets, fAnimators }; fDom = AttachComposition(json, &ctx); // In case the client calls render before the first tick. this->animationTick(0); LOG("** Attached %d animators\n", fAnimators.count()); } Animation::~Animation() = default; void Animation::render(SkCanvas* canvas, const SkRect* dstR) const { if (!fDom) return; sksg::InvalidationController ic; fDom->revalidate(&ic, SkMatrix::I()); // TODO: proper inval SkAutoCanvasRestore restore(canvas, true); const SkRect srcR = SkRect::MakeSize(this->size()); if (dstR) { canvas->concat(SkMatrix::MakeRectToRect(srcR, *dstR, SkMatrix::kCenter_ScaleToFit)); } canvas->clipRect(srcR); fDom->render(canvas); if (!fShowInval) return; SkPaint fill, stroke; fill.setAntiAlias(true); fill.setColor(0x40ff0000); stroke.setAntiAlias(true); stroke.setColor(0xffff0000); stroke.setStyle(SkPaint::kStroke_Style); for (const auto& r : ic) { canvas->drawRect(r, fill); canvas->drawRect(r, stroke); } } void Animation::animationTick(SkMSec ms) { // 't' in the BM model really means 'frame #' auto t = static_cast(ms) * fFrameRate / 1000; t = fInPoint + std::fmod(t, fOutPoint - fInPoint); // TODO: this can be optimized quite a bit with some sorting/state tracking. for (const auto& a : fAnimators) { a->tick(t); } } } // namespace skotty