| /* | |
| * Copyright 2019 Google LLC | |
| * | |
| * Use of this source code is governed by a BSD-style license that can be | |
| * found in the LICENSE file. | |
| */ | |
| #include "modules/skottie/src/Layer.h" | |
| #include "include/core/SkBlendMode.h" | |
| #include "include/core/SkColor.h" | |
| #include "include/core/SkM44.h" | |
| #include "include/core/SkPathTypes.h" | |
| #include "include/core/SkRect.h" | |
| #include "include/core/SkScalar.h" | |
| #include "include/core/SkString.h" | |
| #include "include/core/SkTileMode.h" | |
| #include "include/private/SkAssert.h" | |
| #include "include/private/SkTArray.h" | |
| #include "include/private/SkTo.h" | |
| #include "modules/jsonreader/SkJSONReader.h" | |
| #include "modules/skottie/include/Skottie.h" | |
| #include "modules/skottie/include/SkottieProperty.h" | |
| #include "modules/skottie/src/Composition.h" | |
| #include "modules/skottie/src/SkottieJson.h" | |
| #include "modules/skottie/src/SkottieValue.h" | |
| #include "modules/skottie/src/animator/Animator.h" | |
| #include "modules/skottie/src/effects/Effects.h" | |
| #include "modules/sksg/include/SkSGClipEffect.h" | |
| #include "modules/sksg/include/SkSGDraw.h" | |
| #include "modules/sksg/include/SkSGGeometryNode.h" | |
| #include "modules/sksg/include/SkSGGroup.h" | |
| #include "modules/sksg/include/SkSGMaskEffect.h" | |
| #include "modules/sksg/include/SkSGMerge.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/SkSGRenderNode.h" | |
| #include "modules/sksg/include/SkSGTransform.h" | |
| #include <array> | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| struct SkSize; | |
| using namespace skia_private; | |
| 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 }; | |
| static constexpr MaskInfo k_sub_info = | |
| { SkBlendMode::kDstOut , sksg::Merge::Mode::kDifference, true }; | |
| static constexpr MaskInfo k_dif_info = | |
| { SkBlendMode::kXor , sksg::Merge::Mode::kXOR , 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; | |
| } | |
| class MaskAdapter final : public AnimatablePropertyContainer { | |
| public: | |
| MaskAdapter(const skjson::ObjectValue& jmask, const AnimationBuilder& abuilder, SkBlendMode bm) | |
| : fMaskPaint(sksg::Color::Make(SK_ColorBLACK)) | |
| , fBlendMode(bm) | |
| { | |
| fMaskPaint->setAntiAlias(true); | |
| if (!this->requires_isolation()) { | |
| // We can mask at draw time. | |
| fMaskPaint->setBlendMode(bm); | |
| } | |
| this->bind(abuilder, jmask["o"], fOpacity); | |
| if (this->bind(abuilder, jmask["f"], fFeather)) { | |
| fMaskFilter = sksg::BlurImageFilter::Make(); | |
| // Mask feathers don't repeat edge pixels. | |
| fMaskFilter->setTileMode(SkTileMode::kDecal); | |
| } | |
| } | |
| bool hasEffect() const { | |
| return !this->isStatic() | |
| || fOpacity < 100 | |
| || fFeather != SkV2{0,0}; | |
| } | |
| sk_sp<sksg::RenderNode> makeMask(sk_sp<sksg::Path> mask_path) const { | |
| sk_sp<sksg::RenderNode> mask = sksg::Draw::Make(std::move(mask_path), fMaskPaint); | |
| // Optional mask blur (feather). | |
| mask = sksg::ImageFilterEffect::Make(std::move(mask), fMaskFilter); | |
| if (this->requires_isolation()) { | |
| mask = sksg::LayerEffect::Make(std::move(mask), fBlendMode); | |
| } | |
| return mask; | |
| } | |
| private: | |
| void onSync() override { | |
| fMaskPaint->setOpacity(fOpacity * 0.01f); | |
| if (fMaskFilter) { | |
| // Close enough to AE. | |
| static constexpr SkScalar kFeatherToSigma = 0.38f; | |
| fMaskFilter->setSigma({fFeather.x * kFeatherToSigma, | |
| fFeather.y * kFeatherToSigma}); | |
| } | |
| } | |
| bool requires_isolation() const { | |
| SkASSERT(fBlendMode == SkBlendMode::kSrc || | |
| fBlendMode == SkBlendMode::kSrcOver || | |
| fBlendMode == SkBlendMode::kSrcIn || | |
| fBlendMode == SkBlendMode::kDstOut || | |
| fBlendMode == SkBlendMode::kXor); | |
| // Some mask modes touch pixels outside the immediate draw geometry. | |
| // These require a layer. | |
| switch (fBlendMode) { | |
| case (SkBlendMode::kSrcIn): return true; | |
| default : return false; | |
| } | |
| SkUNREACHABLE; | |
| } | |
| const sk_sp<sksg::PaintNode> fMaskPaint; | |
| const SkBlendMode fBlendMode; | |
| sk_sp<sksg::BlurImageFilter> fMaskFilter; // optional "feather" | |
| Vec2Value fFeather = {0,0}; | |
| ScalarValue fOpacity = 100; | |
| }; | |
| sk_sp<sksg::RenderNode> AttachMask(const skjson::ArrayValue* jmask, | |
| const AnimationBuilder* abuilder, | |
| sk_sp<sksg::RenderNode> childNode) { | |
| if (!jmask) return childNode; | |
| struct MaskRecord { | |
| sk_sp<sksg::Path> mask_path; // for clipping and masking | |
| sk_sp<MaskAdapter> mask_adapter; // for masking | |
| sksg::Merge::Mode merge_mode; // for clipping | |
| }; | |
| STArray<4, MaskRecord, true> mask_stack; | |
| bool has_effect = false; | |
| 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"]); | |
| if (!mask_path) { | |
| abuilder->log(Logger::Level::kError, m, "Could not parse mask path."); | |
| continue; | |
| } | |
| auto mask_blend_mode = mask_info->fBlendMode; | |
| auto mask_merge_mode = mask_info->fMergeMode; | |
| auto mask_inverted = ParseDefault<bool>((*m)["inv"], false); | |
| if (mask_stack.empty()) { | |
| // First mask adjustments: | |
| // - always draw in source mode | |
| // - invert geometry if needed | |
| mask_blend_mode = SkBlendMode::kSrc; | |
| mask_merge_mode = sksg::Merge::Mode::kMerge; | |
| mask_inverted = mask_inverted != mask_info->fInvertGeometry; | |
| } | |
| mask_path->setFillType(mask_inverted ? SkPathFillType::kInverseWinding | |
| : SkPathFillType::kWinding); | |
| auto mask_adapter = sk_make_sp<MaskAdapter>(*m, *abuilder, mask_blend_mode); | |
| abuilder->attachDiscardableAdapter(mask_adapter); | |
| has_effect |= mask_adapter->hasEffect(); | |
| mask_stack.push_back({ std::move(mask_path), | |
| std::move(mask_adapter), | |
| mask_merge_mode }); | |
| } | |
| 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.size() == 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.size())); | |
| for (auto& mask : mask_stack) { | |
| merge_recs.push_back({std::move(mask.mask_path), mask.merge_mode }); | |
| } | |
| clip_node = sksg::Merge::Make(std::move(merge_recs)); | |
| } | |
| return sksg::ClipEffect::Make(std::move(childNode), std::move(clip_node), true); | |
| } | |
| // Complex masks (non-opaque or blurred) turn into a mask node stack. | |
| sk_sp<sksg::RenderNode> maskNode; | |
| if (mask_stack.size() == 1) { | |
| // no group needed for single mask | |
| const auto& rec = mask_stack.front(); | |
| maskNode = rec.mask_adapter->makeMask(std::move(rec.mask_path)); | |
| } else { | |
| std::vector<sk_sp<sksg::RenderNode>> masks; | |
| masks.reserve(SkToSizeT(mask_stack.size())); | |
| for (auto& rec : mask_stack) { | |
| masks.push_back(rec.mask_adapter->makeMask(std::move(rec.mask_path))); | |
| } | |
| maskNode = sksg::Group::Make(std::move(masks)); | |
| } | |
| return sksg::MaskEffect::Make(std::move(childNode), std::move(maskNode)); | |
| } | |
| class LayerController final : public Animator { | |
| public: | |
| LayerController(AnimatorScope&& layer_animators, | |
| sk_sp<sksg::RenderNode> layer, | |
| size_t tanim_count, float in, float out) | |
| : fLayerAnimators(std::move(layer_animators)) | |
| , fLayerNode(std::move(layer)) | |
| , fTransformAnimatorsCount(tanim_count) | |
| , fIn(in) | |
| , fOut(out) {} | |
| protected: | |
| StateChanged onSeek(float t) override { | |
| // in/out may be inverted for time-reversed layers | |
| const auto active = (t >= fIn && t < fOut) || (t > fOut && t <= fIn); | |
| bool changed = false; | |
| if (fLayerNode) { | |
| changed |= (fLayerNode->isVisible() != active); | |
| fLayerNode->setVisible(active); | |
| } | |
| // When active, dispatch ticks to all layer animators. | |
| // When inactive, we must still dispatch ticks to the layer transform animators | |
| // (active child layers depend on transforms being updated). | |
| const auto dispatch_count = active ? fLayerAnimators.size() | |
| : fTransformAnimatorsCount; | |
| for (size_t i = 0; i < dispatch_count; ++i) { | |
| changed |= fLayerAnimators[i]->seek(t); | |
| } | |
| return changed; | |
| } | |
| private: | |
| const AnimatorScope fLayerAnimators; | |
| const sk_sp<sksg::RenderNode> fLayerNode; | |
| const size_t fTransformAnimatorsCount; | |
| const float fIn, | |
| fOut; | |
| }; | |
| // AE is annoyingly inconsistent in how effects interact with layer transforms: depending on | |
| // the layer type, effects are applied before or after the content is transformed. | |
| // | |
| // Empirically, pre-rendered layers (for some loose meaning of "pre-rendered") are in the | |
| // former category (effects are subject to transformation), while the remaining types are in | |
| // the latter. | |
| enum : uint32_t { | |
| kTransformEffects = 0x01, // The layer transform also applies to its effects. | |
| kForceSeek = 0x02, // Dispatch all seek() events even when the layer is inactive. | |
| }; | |
| } // namespace | |
| LayerBuilder::LayerBuilder(const skjson::ObjectValue& jlayer, const SkSize& comp_size) | |
| : fJlayer(jlayer) | |
| , fIndex (ParseDefault<int>(jlayer["ind" ], -1)) | |
| , fParentIndex(ParseDefault<int>(jlayer["parent"], -1)) | |
| , fType (ParseDefault<int>(jlayer["ty" ], -1)) | |
| , fAutoOrient (ParseDefault<int>(jlayer["ao" ], 0)) | |
| , fInfo{ParseDefault<SkString>(jlayer["nm"], SkString()), | |
| comp_size, | |
| ParseDefault<float>(jlayer["ip"], 0.0f), | |
| ParseDefault<float>(jlayer["op"], 0.0f)} | |
| { | |
| using BuilderInfo = skottie::internal::LayerBuilder::BuilderInfo; | |
| // This array maps the 'ty' field to the appropriate layer building member function | |
| static constexpr auto gLayerBuildInfo = std::to_array<BuilderInfo>({ | |
| BuilderInfo{ &AnimationBuilder::attachPrecompLayer, kTransformEffects }, // 0 -> precomp | |
| BuilderInfo{ &AnimationBuilder::attachSolidLayer , kTransformEffects }, // 1 -> solid | |
| BuilderInfo{ &AnimationBuilder::attachFootageLayer, kTransformEffects }, // 2 -> image | |
| BuilderInfo{ &AnimationBuilder::attachNullLayer , 0 }, // 3 -> null | |
| BuilderInfo{ &AnimationBuilder::attachShapeLayer , 0 }, // 4 -> shape | |
| BuilderInfo{ &AnimationBuilder::attachTextLayer , 0 }, // 5 -> text | |
| BuilderInfo{ &AnimationBuilder::attachAudioLayer , kForceSeek }, // 6 -> audio | |
| BuilderInfo{ nullptr , 0 }, // 7 -> pholderVideo | |
| BuilderInfo{ nullptr , 0 }, // 8 -> imageSeq | |
| BuilderInfo{ &AnimationBuilder::attachFootageLayer, kTransformEffects }, // 9 -> video | |
| BuilderInfo{ nullptr , 0 }, // 10 -> pholderStill | |
| BuilderInfo{ nullptr , 0 }, // 11 -> guide | |
| BuilderInfo{ nullptr , 0 }, // 12 -> adjustment | |
| BuilderInfo{ &AnimationBuilder::attachNullLayer , 0 }, // 13 -> camera | |
| BuilderInfo{ nullptr , 0 }, // 14 -> light | |
| }); | |
| if (fType >= 0 && SkToSizeT(fType) < std::size(gLayerBuildInfo)) { | |
| fBuilderInfo = gLayerBuildInfo[fType]; | |
| } | |
| if (this->isCamera() || ParseDefault<int>(jlayer["ddd"], 0)) { | |
| fFlags |= Flags::kIs3D; | |
| } | |
| } | |
| LayerBuilder::~LayerBuilder() = default; | |
| bool LayerBuilder::isCamera() const { | |
| static constexpr int kCameraLayerType = 13; | |
| return fType == kCameraLayerType; | |
| } | |
| sk_sp<sksg::Transform> LayerBuilder::buildTransform(const AnimationBuilder& abuilder, | |
| CompositionBuilder* cbuilder) { | |
| // Depending on the leaf node type, we treat the whole transform chain as either 2D or 3D. | |
| const auto transform_chain_type = this->is3D() ? TransformType::k3D | |
| : TransformType::k2D; | |
| fLayerTransform = this->getTransform(abuilder, cbuilder, transform_chain_type); | |
| return fLayerTransform; | |
| } | |
| sk_sp<sksg::Transform> LayerBuilder::getTransform(const AnimationBuilder& abuilder, | |
| CompositionBuilder* cbuilder, | |
| TransformType ttype) { | |
| const auto cache_valid_mask = (1ul << ttype); | |
| if (!(fFlags & cache_valid_mask)) { | |
| // Set valid flag upfront to break cycles. | |
| fFlags |= cache_valid_mask; | |
| const AnimationBuilder::AutoPropertyTracker apt(&abuilder, fJlayer, PropertyObserver::NodeType::LAYER); | |
| AnimationBuilder::AutoScope ascope(&abuilder, std::move(fLayerScope)); | |
| fTransformCache[ttype] = this->doAttachTransform(abuilder, cbuilder, ttype); | |
| fLayerScope = ascope.release(); | |
| fTransformAnimatorCount = fLayerScope.size(); | |
| } | |
| return fTransformCache[ttype]; | |
| } | |
| sk_sp<sksg::Transform> LayerBuilder::getParentTransform(const AnimationBuilder& abuilder, | |
| CompositionBuilder* cbuilder, | |
| TransformType ttype) { | |
| if (auto* parent_builder = cbuilder->layerBuilder(fParentIndex)) { | |
| // Explicit parent layer. | |
| return parent_builder->getTransform(abuilder, cbuilder, ttype); | |
| } | |
| // Camera layers have no implicit parent transform, | |
| // while regular 3D transform chains are implicitly rooted onto the camera. | |
| if (ttype == TransformType::k3D && !this->isCamera()) { | |
| return cbuilder->getCameraTransform(); | |
| } | |
| return nullptr; | |
| } | |
| sk_sp<sksg::Transform> LayerBuilder::doAttachTransform(const AnimationBuilder& abuilder, | |
| CompositionBuilder* cbuilder, | |
| TransformType ttype) { | |
| const skjson::ObjectValue* jtransform = fJlayer["ks"]; | |
| if (!jtransform) { | |
| return nullptr; | |
| } | |
| auto parent_transform = this->getParentTransform(abuilder, cbuilder, ttype); | |
| if (this->isCamera()) { | |
| // 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) | |
| // | |
| return abuilder.attachCamera(fJlayer, | |
| *jtransform, | |
| sksg::Transform::MakeInverse(std::move(parent_transform)), | |
| cbuilder->fSize); | |
| } | |
| return this->is3D() | |
| ? abuilder.attachMatrix3D(*jtransform, std::move(parent_transform), fAutoOrient) | |
| : abuilder.attachMatrix2D(*jtransform, std::move(parent_transform), fAutoOrient); | |
| } | |
| const sk_sp<sksg::RenderNode>& LayerBuilder::getContentTree(const AnimationBuilder& abuilder, | |
| CompositionBuilder* cbuilder) { | |
| if (!(fFlags & kBuiltContent)) { | |
| // Set the flag first to prevent reference cycles. | |
| fFlags |= Flags::kBuiltContent; | |
| fContentTree = this->buildContentTree(abuilder, cbuilder); | |
| } | |
| return fContentTree; | |
| } | |
| sk_sp<sksg::RenderNode> LayerBuilder::buildContentTree(const AnimationBuilder& abuilder, | |
| CompositionBuilder* cbuilder) { | |
| const AnimationBuilder::AutoPropertyTracker apt(&abuilder, fJlayer, | |
| PropertyObserver::NodeType::LAYER); | |
| // Switch to the layer animator scope (which at this point holds transform-only animators). | |
| AnimationBuilder::AutoScope ascope(&abuilder, std::move(fLayerScope)); | |
| // Potentially null. | |
| sk_sp<sksg::RenderNode> layer; | |
| // Build the layer content fragment. | |
| if (fBuilderInfo.fBuilder) { | |
| layer = (abuilder.*(fBuilderInfo.fBuilder))(fJlayer, &fInfo); | |
| } | |
| // Clip layers with explicit dimensions. | |
| float w = 0, h = 0; | |
| if (::skottie::Parse<float>(fJlayer["w"], &w) && ::skottie::Parse<float>(fJlayer["h"], &h)) { | |
| layer = sksg::ClipEffect::Make(std::move(layer), | |
| sksg::Rect::Make(SkRect::MakeWH(w, h)), | |
| #ifdef SK_LEGACY_SKOTTIE_CLIPPING | |
| /*aa=*/true, /*force_clip=*/false); | |
| #else | |
| /*aa=*/true, /*force_clip=*/true); | |
| #endif | |
| } | |
| // Optional layer mask. | |
| layer = AttachMask(fJlayer["masksProperties"], &abuilder, std::move(layer)); | |
| // Does the transform apply to effects also? | |
| // (AE quirk: it doesn't - except for solid layers) | |
| const auto transform_effects = (fBuilderInfo.fFlags & kTransformEffects); | |
| // Attach the transform before effects, when needed. | |
| if (fLayerTransform && !transform_effects) { | |
| layer = sksg::TransformEffect::Make(std::move(layer), fLayerTransform); | |
| } | |
| // Optional layer effects. | |
| if (const skjson::ArrayValue* jeffects = fJlayer["ef"]) { | |
| layer = EffectBuilder(&abuilder, fInfo.fSize, cbuilder) | |
| .attachEffects(*jeffects, std::move(layer)); | |
| } | |
| // Attach the transform after effects, when needed. | |
| if (fLayerTransform && transform_effects) { | |
| layer = sksg::TransformEffect::Make(std::move(layer), std::move(fLayerTransform)); | |
| } | |
| // Optional layer styles. | |
| if (const skjson::ArrayValue* jstyles = fJlayer["sy"]) { | |
| layer = EffectBuilder(&abuilder, fInfo.fSize, cbuilder) | |
| .attachStyles(*jstyles, std::move(layer)); | |
| } | |
| // Optional layer opacity. | |
| // TODO: de-dupe this "ks" lookup with matrix above. | |
| if (const skjson::ObjectValue* jtransform = fJlayer["ks"]) { | |
| layer = abuilder.attachOpacity(*jtransform, std::move(layer)); | |
| } | |
| // Stash the layer animator scope, to be picked up later in buildRenderTree(). | |
| fLayerScope = ascope.release(); | |
| abuilder.trackLayerInfo(fInfo); | |
| return layer; | |
| } | |
| sk_sp<sksg::RenderNode> LayerBuilder::buildRenderTree(const AnimationBuilder& abuilder, | |
| CompositionBuilder* cbuilder, | |
| int prev_layer_index) { | |
| sk_sp<sksg::RenderNode> layer = this->getContentTree(abuilder, cbuilder); | |
| const auto force_seek_count = fBuilderInfo.fFlags & kForceSeek | |
| ? fLayerScope.size() | |
| : fTransformAnimatorCount; | |
| abuilder.fCurrentAnimatorScope->push_back(sk_make_sp<LayerController>(std::move(fLayerScope), | |
| layer, | |
| force_seek_count, | |
| fInfo.fInPoint, | |
| fInfo.fOutPoint)); | |
| const auto& is_hidden = [this]() { | |
| // If present, the 'hd' property controls visibility. | |
| if (const skjson::BoolValue* jhidden = fJlayer["hd"]) { | |
| return **jhidden; | |
| } | |
| // Legacy track matte flag, not supported in Lottie >= 1.0. | |
| // We only observe this in the absence of an explicit `hd` property. | |
| return ParseDefault<bool>(fJlayer["td"], false); | |
| }; | |
| if (is_hidden()) { | |
| return nullptr; | |
| } | |
| // Optional matte. | |
| if (const auto matte_mode = ParseDefault<size_t>(fJlayer["tt"], 0)) { | |
| static constexpr auto gMatteModes = std::to_array<sksg::MaskEffect::Mode>({ | |
| sksg::MaskEffect::Mode::kAlphaNormal, // tt: 1 | |
| sksg::MaskEffect::Mode::kAlphaInvert, // tt: 2 | |
| sksg::MaskEffect::Mode::kLumaNormal, // tt: 3 | |
| sksg::MaskEffect::Mode::kLumaInvert, // tt: 4 | |
| }); | |
| if (matte_mode <= std::size(gMatteModes)) { | |
| int matte_index = ParseDefault<int>(fJlayer["tp"], -1); | |
| if (matte_index < 0) { | |
| // When 'tp' is not present, assume the matte source is the previous layer | |
| // (legacy assets). | |
| matte_index = prev_layer_index; | |
| } | |
| if (matte_index >= 0) { | |
| layer = sksg::MaskEffect::Make(std::move(layer), | |
| cbuilder->layerContent(abuilder, matte_index), | |
| gMatteModes[matte_mode - 1]); | |
| } | |
| } else { | |
| abuilder.log(Logger::Level::kError, nullptr, | |
| "Unknown track matte mode: %zu\n", matte_mode); | |
| } | |
| } | |
| // Finally, attach an optional blend mode. | |
| // NB: blend modes are never applied to matte sources (layer content only). | |
| return abuilder.attachBlendMode(fJlayer, std::move(layer)); | |
| } | |
| } // namespace internal | |
| } // namespace skottie |