blob: dde866c8f400a35ef844dd7e427a2e4d52ad7609 [file] [log] [blame]
/*
* Copyright 2018 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "modules/skottie/src/SkottiePriv.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkData.h"
#include "include/core/SkFontMgr.h"
#include "include/core/SkImage.h"
#include "include/utils/SkParse.h"
#include "modules/skottie/src/SkottieAdapter.h"
#include "modules/skottie/src/SkottieJson.h"
#include "modules/skottie/src/SkottieValue.h"
#include "modules/sksg/include/SkSGClipEffect.h"
#include "modules/sksg/include/SkSGDraw.h"
#include "modules/sksg/include/SkSGGroup.h"
#include "modules/sksg/include/SkSGImage.h"
#include "modules/sksg/include/SkSGMaskEffect.h"
#include "modules/sksg/include/SkSGMerge.h"
#include "modules/sksg/include/SkSGOpacityEffect.h"
#include "modules/sksg/include/SkSGPaint.h"
#include "modules/sksg/include/SkSGPath.h"
#include "modules/sksg/include/SkSGRect.h"
#include "modules/sksg/include/SkSGRenderEffect.h"
#include "modules/sksg/include/SkSGTransform.h"
#include "src/core/SkMakeUnique.h"
#include "src/utils/SkJSON.h"
#include <algorithm>
#include <vector>
namespace skottie {
namespace internal {
namespace {
struct MaskInfo {
SkBlendMode fBlendMode; // used when masking with layers/blending
sksg::Merge::Mode fMergeMode; // used when clipping
bool fInvertGeometry;
};
const MaskInfo* GetMaskInfo(char mode) {
static constexpr MaskInfo k_add_info =
{ SkBlendMode::kSrcOver , sksg::Merge::Mode::kUnion , false };
static constexpr MaskInfo k_int_info =
{ SkBlendMode::kSrcIn , sksg::Merge::Mode::kIntersect , false };
// AE 'subtract' is the same as 'intersect' + inverted geometry
// (draws the opacity-adjusted paint *outside* the shape).
static constexpr MaskInfo k_sub_info =
{ SkBlendMode::kSrcIn , sksg::Merge::Mode::kIntersect , true };
static constexpr MaskInfo k_dif_info =
{ SkBlendMode::kDifference, sksg::Merge::Mode::kDifference, false };
switch (mode) {
case 'a': return &k_add_info;
case 'f': return &k_dif_info;
case 'i': return &k_int_info;
case 's': return &k_sub_info;
default: break;
}
return nullptr;
}
sk_sp<sksg::RenderNode> AttachMask(const skjson::ArrayValue* jmask,
const AnimationBuilder* abuilder,
AnimatorScope* ascope,
sk_sp<sksg::RenderNode> childNode) {
if (!jmask) return childNode;
struct MaskRecord {
sk_sp<sksg::Path> mask_path; // for clipping and masking
sk_sp<sksg::Color> mask_paint; // for masking
sk_sp<sksg::BlurImageFilter> mask_blur; // for masking
sksg::Merge::Mode merge_mode; // for clipping
};
SkSTArray<4, MaskRecord, true> mask_stack;
bool has_effect = false;
auto blur_effect = sksg::BlurImageFilter::Make();
for (const skjson::ObjectValue* m : *jmask) {
if (!m) continue;
const skjson::StringValue* jmode = (*m)["mode"];
if (!jmode || jmode->size() != 1) {
abuilder->log(Logger::Level::kError, &(*m)["mode"], "Invalid mask mode.");
continue;
}
const auto mode = *jmode->begin();
if (mode == 'n') {
// "None" masks have no effect.
continue;
}
const auto* mask_info = GetMaskInfo(mode);
if (!mask_info) {
abuilder->log(Logger::Level::kWarning, nullptr, "Unsupported mask mode: '%c'.", mode);
continue;
}
auto mask_path = abuilder->attachPath((*m)["pt"], ascope);
if (!mask_path) {
abuilder->log(Logger::Level::kError, m, "Could not parse mask path.");
continue;
}
// "inv" is cumulative with mask info fInvertGeometry
const auto inverted =
(mask_info->fInvertGeometry != ParseDefault<bool>((*m)["inv"], false));
mask_path->setFillType(inverted ? SkPath::kInverseWinding_FillType
: SkPath::kWinding_FillType);
auto mask_paint = sksg::Color::Make(SK_ColorBLACK);
mask_paint->setAntiAlias(true);
// First mask in the stack initializes the mask buffer.
mask_paint->setBlendMode(mask_stack.empty() ? SkBlendMode::kSrc
: mask_info->fBlendMode);
has_effect |= abuilder->bindProperty<ScalarValue>((*m)["o"], ascope,
[mask_paint](const ScalarValue& o) {
mask_paint->setOpacity(o * 0.01f);
}, 100.0f);
static const VectorValue default_feather = { 0, 0 };
if (abuilder->bindProperty<VectorValue>((*m)["f"], ascope,
[blur_effect](const VectorValue& feather) {
// Close enough to AE.
static constexpr SkScalar kFeatherToSigma = 0.38f;
auto sX = feather.size() > 0 ? feather[0] * kFeatherToSigma : 0,
sY = feather.size() > 1 ? feather[1] * kFeatherToSigma : 0;
blur_effect->setSigma({ sX, sY });
}, default_feather)) {
has_effect = true;
mask_stack.push_back({ mask_path,
mask_paint,
std::move(blur_effect),
mask_info->fMergeMode});
blur_effect = sksg::BlurImageFilter::Make();
} else {
mask_stack.push_back({mask_path, mask_paint, nullptr, mask_info->fMergeMode});
}
}
if (mask_stack.empty())
return childNode;
// If the masks are fully opaque, we can clip.
if (!has_effect) {
sk_sp<sksg::GeometryNode> clip_node;
if (mask_stack.count() == 1) {
// Single path -> just clip.
clip_node = std::move(mask_stack.front().mask_path);
} else {
// Multiple clip paths -> merge.
std::vector<sksg::Merge::Rec> merge_recs;
merge_recs.reserve(SkToSizeT(mask_stack.count()));
for (auto& mask : mask_stack) {
const auto mode = merge_recs.empty() ? sksg::Merge::Mode::kMerge : mask.merge_mode;
merge_recs.push_back({std::move(mask.mask_path), mode});
}
clip_node = sksg::Merge::Make(std::move(merge_recs));
}
return sksg::ClipEffect::Make(std::move(childNode), std::move(clip_node), true);
}
const auto make_mask = [](const MaskRecord& rec) {
auto mask = sksg::Draw::Make(std::move(rec.mask_path),
std::move(rec.mask_paint));
// Optional mask blur (feather).
return sksg::ImageFilterEffect::Make(std::move(mask), std::move(rec.mask_blur));
};
sk_sp<sksg::RenderNode> maskNode;
if (mask_stack.count() == 1) {
// no group needed for single mask
maskNode = make_mask(mask_stack.front());
} else {
std::vector<sk_sp<sksg::RenderNode>> masks;
masks.reserve(SkToSizeT(mask_stack.count()));
for (auto& rec : mask_stack) {
masks.push_back(make_mask(rec));
}
maskNode = sksg::Group::Make(std::move(masks));
}
return sksg::MaskEffect::Make(std::move(childNode), std::move(maskNode));
}
static constexpr int kCameraLayerType = 13;
} // namespace
sk_sp<sksg::RenderNode> AnimationBuilder::attachNestedAnimation(const char* name,
AnimatorScope* ascope) const {
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());
}
const RenderNode* onNodeAt(const SkPoint&) const override { return nullptr; }
void onRender(SkCanvas* canvas, const RenderContext* ctx) const override {
const auto local_scope =
ScopedRenderContext(canvas, ctx).setIsolation(this->bounds(),
canvas->getTotalMatrix(),
true);
fAnimation->render(canvas);
}
private:
const sk_sp<Animation> fAnimation;
};
class SkottieAnimatorAdapter final : public sksg::Animator {
public:
SkottieAnimatorAdapter(sk_sp<Animation> 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<Animation> fAnimation;
const float fTimeScale;
};
const auto data = fResourceProvider->load("", name);
if (!data) {
this->log(Logger::Level::kError, nullptr, "Could not load: %s.", name);
return nullptr;
}
auto animation = Animation::Builder()
.setResourceProvider(fResourceProvider)
.setFontManager(fLazyFontMgr.getMaybeNull())
.make(static_cast<const char*>(data->data()), data->size());
if (!animation) {
this->log(Logger::Level::kError, nullptr, "Could not parse nested animation: %s.", name);
return nullptr;
}
ascope->push_back(
skstd::make_unique<SkottieAnimatorAdapter>(animation, animation->duration() / fDuration));
return sk_make_sp<SkottieSGAdapter>(std::move(animation));
}
sk_sp<sksg::RenderNode> AnimationBuilder::attachAssetRef(
const skjson::ObjectValue& jlayer, AnimatorScope* ascope,
const std::function<sk_sp<sksg::RenderNode>(const skjson::ObjectValue&,
AnimatorScope*)>& func) const {
const auto refId = ParseDefault<SkString>(jlayer["refId"], SkString());
if (refId.isEmpty()) {
this->log(Logger::Level::kError, nullptr, "Layer missing refId.");
return nullptr;
}
if (refId.startsWith("$")) {
return this->attachNestedAnimation(refId.c_str() + 1, ascope);
}
const auto* asset_info = fAssets.find(refId);
if (!asset_info) {
this->log(Logger::Level::kError, nullptr, "Asset not found: '%s'.", refId.c_str());
return nullptr;
}
if (asset_info->fIsAttaching) {
this->log(Logger::Level::kError, nullptr,
"Asset cycle detected for: '%s'", refId.c_str());
return nullptr;
}
asset_info->fIsAttaching = true;
auto asset = func(*asset_info->fAsset, ascope);
asset_info->fIsAttaching = false;
return asset;
}
sk_sp<sksg::RenderNode> AnimationBuilder::attachSolidLayer(const skjson::ObjectValue& jlayer,
const LayerInfo&,
AnimatorScope*) const {
const auto size = SkSize::Make(ParseDefault<float>(jlayer["sw"], 0.0f),
ParseDefault<float>(jlayer["sh"], 0.0f));
const skjson::StringValue* hex_str = jlayer["sc"];
uint32_t c;
if (size.isEmpty() ||
!hex_str ||
*hex_str->begin() != '#' ||
!SkParse::FindHex(hex_str->begin() + 1, &c)) {
this->log(Logger::Level::kError, &jlayer, "Could not parse solid layer.");
return nullptr;
}
const SkColor color = 0xff000000 | c;
auto solid_paint = sksg::Color::Make(color);
solid_paint->setAntiAlias(true);
return sksg::Draw::Make(sksg::Rect::Make(SkRect::MakeSize(size)),
std::move(solid_paint));
}
const AnimationBuilder::ImageAssetInfo*
AnimationBuilder::loadImageAsset(const skjson::ObjectValue& jimage) const {
const skjson::StringValue* name = jimage["p"];
const skjson::StringValue* path = jimage["u"];
if (!name) {
return nullptr;
}
const auto name_cstr = name->begin(),
path_cstr = path ? path->begin() : "";
const auto res_id = SkStringPrintf("%s|%s", path_cstr, name_cstr);
if (auto* cached_info = fImageAssetCache.find(res_id)) {
return cached_info;
}
auto asset = fResourceProvider->loadImageAsset(path_cstr, name_cstr);
if (!asset) {
this->log(Logger::Level::kError, nullptr,
"Could not load image asset: %s/%s.", path_cstr, name_cstr);
return nullptr;
}
const auto size = SkISize::Make(ParseDefault<int>(jimage["w"], 0),
ParseDefault<int>(jimage["h"], 0));
return fImageAssetCache.set(res_id, { std::move(asset), size });
}
sk_sp<sksg::RenderNode> AnimationBuilder::attachImageAsset(const skjson::ObjectValue& jimage,
const LayerInfo& layer_info,
AnimatorScope* ascope) const {
const auto* asset_info = this->loadImageAsset(jimage);
if (!asset_info) {
return nullptr;
}
SkASSERT(asset_info->fAsset);
auto image = asset_info->fAsset->getFrame(0);
if (!image) {
this->log(Logger::Level::kError, nullptr, "Could not load first image asset frame.");
return nullptr;
}
auto image_node = sksg::Image::Make(image);
image_node->setQuality(kMedium_SkFilterQuality);
if (asset_info->fAsset->isMultiFrame()) {
class MultiFrameAnimator final : public sksg::Animator {
public:
MultiFrameAnimator(sk_sp<ImageAsset> asset, sk_sp<sksg::Image> image_node,
float time_bias, float time_scale)
: fAsset(std::move(asset))
, fImageNode(std::move(image_node))
, fTimeBias(time_bias)
, fTimeScale(time_scale) {}
void onTick(float t) override {
fImageNode->setImage(fAsset->getFrame((t + fTimeBias) * fTimeScale));
}
private:
sk_sp<ImageAsset> fAsset;
sk_sp<sksg::Image> fImageNode;
float fTimeBias,
fTimeScale;
};
ascope->push_back(skstd::make_unique<MultiFrameAnimator>(asset_info->fAsset,
image_node,
-layer_info.fInPoint,
1 / fFrameRate));
}
const auto asset_size = SkISize::Make(
asset_info->fSize.width() > 0 ? asset_info->fSize.width() : image->width(),
asset_info->fSize.height() > 0 ? asset_info->fSize.height() : image->height());
if (asset_size == image->bounds().size()) {
// No resize needed.
return std::move(image_node);
}
return sksg::TransformEffect::Make(std::move(image_node),
SkMatrix::MakeRectToRect(SkRect::Make(image->bounds()),
SkRect::Make(asset_size),
SkMatrix::kCenter_ScaleToFit));
}
sk_sp<sksg::RenderNode> AnimationBuilder::attachImageLayer(const skjson::ObjectValue& jlayer,
const LayerInfo& layer_info,
AnimatorScope* ascope) const {
return this->attachAssetRef(jlayer, ascope,
[this, &layer_info] (const skjson::ObjectValue& jimage, AnimatorScope* ascope) {
return this->attachImageAsset(jimage, layer_info, ascope);
});
}
sk_sp<sksg::RenderNode> AnimationBuilder::attachNullLayer(const skjson::ObjectValue& layer,
const LayerInfo&,
AnimatorScope*) const {
// Null layers are used solely to drive dependent transforms,
// but we use free-floating sksg::Matrices for that purpose.
return nullptr;
}
struct AnimationBuilder::AttachLayerContext {
AttachLayerContext(const skjson::ArrayValue& jlayers, AnimatorScope* scope)
: fLayerList(jlayers), fScope(scope) {}
const skjson::ArrayValue& fLayerList;
AnimatorScope* fScope;
SkTHashMap<int, sk_sp<sksg::Transform>> fLayerMatrixMap;
sk_sp<sksg::RenderNode> fCurrentMatte;
sk_sp<sksg::Transform> fCameraTransform;
enum class TransformType { kLayer, kCamera };
sk_sp<sksg::Transform> attachLayerTransform(const skjson::ObjectValue& jlayer,
const AnimationBuilder* abuilder,
TransformType type = TransformType::kLayer) {
const auto layer_index = ParseDefault<int>(jlayer["ind"], -1);
if (layer_index < 0)
return nullptr;
if (auto* m = fLayerMatrixMap.find(layer_index))
return *m;
return this->attachLayerTransformImpl(jlayer, abuilder, type, layer_index);
}
private:
sk_sp<sksg::Transform> attachParentLayerTransform(const skjson::ObjectValue& jlayer,
const AnimationBuilder* abuilder,
int layer_index) {
const auto parent_index = ParseDefault<int>(jlayer["parent"], -1);
if (parent_index < 0 || parent_index == layer_index)
return nullptr;
if (auto* m = fLayerMatrixMap.find(parent_index))
return *m;
for (const skjson::ObjectValue* l : fLayerList) {
if (!l) continue;
if (ParseDefault<int>((*l)["ind"], -1) == parent_index) {
const auto parent_type = ParseDefault<int>((*l)["ty"], -1) == kCameraLayerType
? TransformType::kCamera
: TransformType::kLayer;
return this->attachLayerTransformImpl(*l, abuilder, parent_type, parent_index);
}
}
return nullptr;
}
sk_sp<sksg::Transform> attachTransformNode(const skjson::ObjectValue& jlayer,
const AnimationBuilder* abuilder,
sk_sp<sksg::Transform> parent_transform,
TransformType type) const {
const skjson::ObjectValue* jtransform = jlayer["ks"];
if (!jtransform) {
return nullptr;
}
if (type == TransformType::kCamera) {
auto camera_adapter = sk_make_sp<CameraAdapter>(abuilder->fSize);
abuilder->bindProperty<ScalarValue>(jlayer["pe"], fScope,
[camera_adapter] (const ScalarValue& pe) {
// 'pe' (perspective?) corresponds to AE's "zoom" camera property.
camera_adapter->setZoom(pe);
});
// parent_transform applies to the camera itself => it pre-composes inverted to the
// camera/view/adapter transform.
//
// T_camera' = T_camera x Inv(parent_transform)
//
parent_transform = sksg::Transform::MakeInverse(std::move(parent_transform));
return abuilder->attachMatrix3D(*jtransform, fScope,
std::move(parent_transform),
std::move(camera_adapter),
true); // pre-compose parent
}
return (ParseDefault<int>(jlayer["ddd"], 0) == 0)
? abuilder->attachMatrix2D(*jtransform, fScope, std::move(parent_transform))
: abuilder->attachMatrix3D(*jtransform, fScope, std::move(parent_transform));
}
sk_sp<sksg::Transform> attachLayerTransformImpl(const skjson::ObjectValue& jlayer,
const AnimationBuilder* abuilder,
TransformType type, 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->attachParentLayerTransform(jlayer, abuilder, layer_index);
return *fLayerMatrixMap.set(layer_index, this->attachTransformNode(jlayer,
abuilder,
std::move(parent_matrix),
type));
}
};
sk_sp<sksg::RenderNode> AnimationBuilder::attachLayer(const skjson::ObjectValue* jlayer,
AttachLayerContext* layerCtx) const {
if (!jlayer || ParseDefault<bool>((*jlayer)["hd"], false)) {
// Ignore hidden layers.
return nullptr;
}
const LayerInfo layer_info = {
ParseDefault<float>((*jlayer)["ip"], 0.0f),
ParseDefault<float>((*jlayer)["op"], 0.0f)
};
if (layer_info.fInPoint >= layer_info.fOutPoint) {
this->log(Logger::Level::kError, nullptr,
"Invalid layer in/out points: %f/%f.", layer_info.fInPoint, layer_info.fOutPoint);
return nullptr;
}
const AutoPropertyTracker apt(this, *jlayer);
using LayerBuilder = sk_sp<sksg::RenderNode> (AnimationBuilder::*)(const skjson::ObjectValue&,
const LayerInfo&,
AnimatorScope*) const;
enum : uint32_t {
kTransformEffects = 1, // The layer transform applies to its effects also.
};
static constexpr struct {
LayerBuilder fBuilder;
uint32_t fFlags;
} gLayerBuildInfo[] = {
{ &AnimationBuilder::attachPrecompLayer, 0 }, // 'ty': 0 -> precomp
{ &AnimationBuilder::attachSolidLayer , kTransformEffects }, // 'ty': 1 -> solid
{ &AnimationBuilder::attachImageLayer , 0 }, // 'ty': 2 -> image
{ &AnimationBuilder::attachNullLayer , 0 }, // 'ty': 3 -> null
{ &AnimationBuilder::attachShapeLayer , 0 }, // 'ty': 4 -> shape
{ &AnimationBuilder::attachTextLayer , 0 }, // 'ty': 5 -> text
};
const auto type = ParseDefault<int>((*jlayer)["ty"], -1);
if (type == kCameraLayerType) {
// Camera layers are special: they don't build normal SG fragments, but drive a root-level
// transform.
if (layerCtx->fCameraTransform) {
this->log(Logger::Level::kWarning, jlayer, "Ignoring duplicate camera layer.");
} else {
layerCtx->fCameraTransform =
layerCtx->attachLayerTransform(*jlayer, this,
AttachLayerContext::TransformType::kCamera);
}
return nullptr;
}
if (type < 0 || type >= SkTo<int>(SK_ARRAY_COUNT(gLayerBuildInfo))) {
return nullptr;
}
const auto& build_info = gLayerBuildInfo[type];
AnimatorScope layer_animators;
// Build the layer content fragment.
auto layer = (this->*(build_info.fBuilder))(*jlayer, layer_info, &layer_animators);
// Clip layers with explicit dimensions.
float w = 0, h = 0;
if (Parse<float>((*jlayer)["w"], &w) && Parse<float>((*jlayer)["h"], &h)) {
layer = sksg::ClipEffect::Make(std::move(layer),
sksg::Rect::Make(SkRect::MakeWH(w, h)),
true);
}
// Optional layer mask.
layer = AttachMask((*jlayer)["masksProperties"], this, &layer_animators, std::move(layer));
// Optional layer transform.
auto layer_transform = layerCtx->attachLayerTransform(*jlayer, this);
// Does the transform apply to effects also?
// (AE quirk: it doesn't - except for solid layers)
const auto transform_effects = (build_info.fFlags & kTransformEffects);
// Attach the transform before effects, when needed.
if (layer_transform && !transform_effects) {
layer = sksg::TransformEffect::Make(std::move(layer), layer_transform);
}
// Optional layer effects.
if (const skjson::ArrayValue* jeffects = (*jlayer)["ef"]) {
layer = this->attachLayerEffects(*jeffects, &layer_animators, std::move(layer));
}
// Attach the transform after effects, when needed.
if (layer_transform && transform_effects) {
layer = sksg::TransformEffect::Make(std::move(layer), std::move(layer_transform));
}
// Optional layer opacity.
// TODO: de-dupe this "ks" lookup with matrix above.
if (const skjson::ObjectValue* jtransform = (*jlayer)["ks"]) {
layer = this->attachOpacity(*jtransform, &layer_animators, std::move(layer));
}
// Optional blend mode.
layer = this->attachBlendMode(*jlayer, std::move(layer));
class LayerController final : public sksg::GroupAnimator {
public:
LayerController(sksg::AnimatorList&& layer_animators,
sk_sp<sksg::OpacityEffect> controlNode,
float in, float out)
: INHERITED(std::move(layer_animators))
, fControlNode(std::move(controlNode))
, fIn(in)
, fOut(out) {}
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);
}
private:
const sk_sp<sksg::OpacityEffect> fControlNode;
const float fIn,
fOut;
using INHERITED = sksg::GroupAnimator;
};
auto controller_node = sksg::OpacityEffect::Make(std::move(layer));
if (!controller_node) {
return nullptr;
}
layerCtx->fScope->push_back(
skstd::make_unique<LayerController>(std::move(layer_animators), controller_node,
layer_info.fInPoint, layer_info.fOutPoint));
if (ParseDefault<bool>((*jlayer)["td"], 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 = ParseDefault<size_t>((*jlayer)["tt"], 1) - 1;
if (matteType < 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> AnimationBuilder::attachComposition(const skjson::ObjectValue& jcomp,
AnimatorScope* scope) const {
const skjson::ArrayValue* jlayers = jcomp["layers"];
if (!jlayers) return nullptr;
std::vector<sk_sp<sksg::RenderNode>> layers;
AttachLayerContext layerCtx(*jlayers, scope);
layers.reserve(jlayers->size());
for (const auto& l : *jlayers) {
if (auto layer = this->attachLayer(l, &layerCtx)) {
layers.push_back(std::move(layer));
}
}
if (layers.empty()) {
return nullptr;
}
sk_sp<sksg::RenderNode> comp;
if (layers.size() == 1) {
comp = std::move(layers[0]);
} else {
// Layers are painted in bottom->top order.
std::reverse(layers.begin(), layers.end());
layers.shrink_to_fit();
comp = sksg::Group::Make(std::move(layers));
}
// Optional camera.
if (layerCtx.fCameraTransform) {
comp = sksg::TransformEffect::Make(std::move(comp), std::move(layerCtx.fCameraTransform));
}
return comp;
}
} // namespace internal
} // namespace skottie