• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2019 Google Inc.
3  *
4  * Use of this source code is governed by a BSD-style license that can be
5  * found in the LICENSE file.
6  */
7 
8 #include "modules/skottie/src/Layer.h"
9 
10 #include "modules/skottie/src/Camera.h"
11 #include "modules/skottie/src/Composition.h"
12 #include "modules/skottie/src/SkottieJson.h"
13 #include "modules/skottie/src/effects/Effects.h"
14 #include "modules/skottie/src/effects/MotionBlurEffect.h"
15 #include "modules/sksg/include/SkSGClipEffect.h"
16 #include "modules/sksg/include/SkSGDraw.h"
17 #include "modules/sksg/include/SkSGGroup.h"
18 #include "modules/sksg/include/SkSGMaskEffect.h"
19 #include "modules/sksg/include/SkSGMerge.h"
20 #include "modules/sksg/include/SkSGPaint.h"
21 #include "modules/sksg/include/SkSGPath.h"
22 #include "modules/sksg/include/SkSGRect.h"
23 #include "modules/sksg/include/SkSGRenderEffect.h"
24 #include "modules/sksg/include/SkSGRenderNode.h"
25 #include "modules/sksg/include/SkSGTransform.h"
26 
27 namespace skottie {
28 namespace internal {
29 
30 namespace  {
31 
32 struct MaskInfo {
33     SkBlendMode       fBlendMode;      // used when masking with layers/blending
34     sksg::Merge::Mode fMergeMode;      // used when clipping
35     bool              fInvertGeometry;
36 };
37 
GetMaskInfo(char mode)38 const MaskInfo* GetMaskInfo(char mode) {
39     static constexpr MaskInfo k_add_info =
40         { SkBlendMode::kSrcOver   , sksg::Merge::Mode::kUnion     , false };
41     static constexpr MaskInfo k_int_info =
42         { SkBlendMode::kSrcIn     , sksg::Merge::Mode::kIntersect , false };
43     static constexpr MaskInfo k_sub_info =
44         { SkBlendMode::kDstOut    , sksg::Merge::Mode::kDifference, true  };
45     static constexpr MaskInfo k_dif_info =
46         { SkBlendMode::kXor       , sksg::Merge::Mode::kXOR       , false };
47 
48     switch (mode) {
49     case 'a': return &k_add_info;
50     case 'f': return &k_dif_info;
51     case 'i': return &k_int_info;
52     case 's': return &k_sub_info;
53     default: break;
54     }
55 
56     return nullptr;
57 }
58 
59 class MaskAdapter final : public AnimatablePropertyContainer {
60 public:
MaskAdapter(const skjson::ObjectValue & jmask,const AnimationBuilder & abuilder,SkBlendMode bm)61     MaskAdapter(const skjson::ObjectValue& jmask, const AnimationBuilder& abuilder, SkBlendMode bm)
62         : fMaskPaint(sksg::Color::Make(SK_ColorBLACK))
63         , fBlendMode(bm)
64     {
65         fMaskPaint->setAntiAlias(true);
66         if (!this->requires_isolation()) {
67             // We can mask at draw time.
68             fMaskPaint->setBlendMode(bm);
69         }
70 
71         this->bind(abuilder, jmask["o"], fOpacity);
72 
73         if (this->bind(abuilder, jmask["f"], fFeather)) {
74             fMaskFilter = sksg::BlurImageFilter::Make();
75             // Mask feathers don't repeat edge pixels.
76             fMaskFilter->setTileMode(SkTileMode::kDecal);
77         }
78     }
79 
hasEffect() const80     bool hasEffect() const {
81         return !this->isStatic()
82             || fOpacity < 100
83             || fFeather != SkV2{0,0};
84     }
85 
makeMask(sk_sp<sksg::Path> mask_path) const86     sk_sp<sksg::RenderNode> makeMask(sk_sp<sksg::Path> mask_path) const {
87         sk_sp<sksg::RenderNode> mask = sksg::Draw::Make(std::move(mask_path), fMaskPaint);
88 
89         // Optional mask blur (feather).
90         mask = sksg::ImageFilterEffect::Make(std::move(mask), fMaskFilter);
91 
92         if (this->requires_isolation()) {
93             mask = sksg::LayerEffect::Make(std::move(mask), fBlendMode);
94         }
95 
96         return mask;
97     }
98 
99 private:
onSync()100     void onSync() override {
101         fMaskPaint->setOpacity(fOpacity * 0.01f);
102         if (fMaskFilter) {
103             // Close enough to AE.
104             static constexpr SkScalar kFeatherToSigma = 0.38f;
105             fMaskFilter->setSigma({fFeather.x * kFeatherToSigma,
106                                    fFeather.y * kFeatherToSigma});
107         }
108     }
109 
requires_isolation() const110     bool requires_isolation() const {
111         SkASSERT(fBlendMode == SkBlendMode::kSrc     ||
112                  fBlendMode == SkBlendMode::kSrcOver ||
113                  fBlendMode == SkBlendMode::kSrcIn   ||
114                  fBlendMode == SkBlendMode::kDstOut  ||
115                  fBlendMode == SkBlendMode::kXor);
116 
117         // Some mask modes touch pixels outside the immediate draw geometry.
118         // These require a layer.
119         switch (fBlendMode) {
120             case (SkBlendMode::kSrcIn): return true;
121             default                   : return false;
122         }
123         SkUNREACHABLE;
124     }
125 
126     const sk_sp<sksg::PaintNode> fMaskPaint;
127     const SkBlendMode            fBlendMode;
128     sk_sp<sksg::BlurImageFilter> fMaskFilter; // optional "feather"
129 
130     Vec2Value   fFeather = {0,0};
131     ScalarValue fOpacity = 100;
132 };
133 
AttachMask(const skjson::ArrayValue * jmask,const AnimationBuilder * abuilder,sk_sp<sksg::RenderNode> childNode)134 sk_sp<sksg::RenderNode> AttachMask(const skjson::ArrayValue* jmask,
135                                    const AnimationBuilder* abuilder,
136                                    sk_sp<sksg::RenderNode> childNode) {
137     if (!jmask) return childNode;
138 
139     struct MaskRecord {
140         sk_sp<sksg::Path>  mask_path;    // for clipping and masking
141         sk_sp<MaskAdapter> mask_adapter; // for masking
142         sksg::Merge::Mode  merge_mode;   // for clipping
143     };
144 
145     SkSTArray<4, MaskRecord, true> mask_stack;
146     bool has_effect = false;
147 
148     for (const skjson::ObjectValue* m : *jmask) {
149         if (!m) continue;
150 
151         const skjson::StringValue* jmode = (*m)["mode"];
152         if (!jmode || jmode->size() != 1) {
153             abuilder->log(Logger::Level::kError, &(*m)["mode"], "Invalid mask mode.");
154             continue;
155         }
156 
157         const auto mode = *jmode->begin();
158         if (mode == 'n') {
159             // "None" masks have no effect.
160             continue;
161         }
162 
163         const auto* mask_info = GetMaskInfo(mode);
164         if (!mask_info) {
165             abuilder->log(Logger::Level::kWarning, nullptr, "Unsupported mask mode: '%c'.", mode);
166             continue;
167         }
168 
169         auto mask_path = abuilder->attachPath((*m)["pt"]);
170         if (!mask_path) {
171             abuilder->log(Logger::Level::kError, m, "Could not parse mask path.");
172             continue;
173         }
174 
175         auto mask_blend_mode = mask_info->fBlendMode;
176         auto mask_merge_mode = mask_info->fMergeMode;
177         auto mask_inverted   = ParseDefault<bool>((*m)["inv"], false);
178 
179         if (mask_stack.empty()) {
180             // First mask adjustments:
181             //   - always draw in source mode
182             //   - invert geometry if needed
183             mask_blend_mode = SkBlendMode::kSrc;
184             mask_merge_mode = sksg::Merge::Mode::kMerge;
185             mask_inverted   = mask_inverted != mask_info->fInvertGeometry;
186         }
187 
188         mask_path->setFillType(mask_inverted ? SkPathFillType::kInverseWinding
189                                              : SkPathFillType::kWinding);
190 
191         auto mask_adapter = sk_make_sp<MaskAdapter>(*m, *abuilder, mask_blend_mode);
192         abuilder->attachDiscardableAdapter(mask_adapter);
193 
194         has_effect |= mask_adapter->hasEffect();
195 
196         mask_stack.push_back({ std::move(mask_path),
197                                std::move(mask_adapter),
198                                mask_merge_mode });
199     }
200 
201 
202     if (mask_stack.empty())
203         return childNode;
204 
205     // If the masks are fully opaque, we can clip.
206     if (!has_effect) {
207         sk_sp<sksg::GeometryNode> clip_node;
208 
209         if (mask_stack.count() == 1) {
210             // Single path -> just clip.
211             clip_node = std::move(mask_stack.front().mask_path);
212         } else {
213             // Multiple clip paths -> merge.
214             std::vector<sksg::Merge::Rec> merge_recs;
215             merge_recs.reserve(SkToSizeT(mask_stack.count()));
216 
217             for (auto& mask : mask_stack) {
218                 merge_recs.push_back({std::move(mask.mask_path), mask.merge_mode });
219             }
220             clip_node = sksg::Merge::Make(std::move(merge_recs));
221         }
222 
223         return sksg::ClipEffect::Make(std::move(childNode), std::move(clip_node), true);
224     }
225 
226     // Complex masks (non-opaque or blurred) turn into a mask node stack.
227     sk_sp<sksg::RenderNode> maskNode;
228     if (mask_stack.count() == 1) {
229         // no group needed for single mask
230         const auto rec = mask_stack.front();
231         maskNode = rec.mask_adapter->makeMask(std::move(rec.mask_path));
232     } else {
233         std::vector<sk_sp<sksg::RenderNode>> masks;
234         masks.reserve(SkToSizeT(mask_stack.count()));
235         for (auto& rec : mask_stack) {
236             masks.push_back(rec.mask_adapter->makeMask(std::move(rec.mask_path)));
237         }
238 
239         maskNode = sksg::Group::Make(std::move(masks));
240     }
241 
242     return sksg::MaskEffect::Make(std::move(childNode), std::move(maskNode));
243 }
244 
245 class LayerController final : public Animator {
246 public:
LayerController(AnimatorScope && layer_animators,sk_sp<sksg::RenderNode> layer,size_t tanim_count,float in,float out)247     LayerController(AnimatorScope&& layer_animators,
248                     sk_sp<sksg::RenderNode> layer,
249                     size_t tanim_count, float in, float out)
250         : fLayerAnimators(std::move(layer_animators))
251         , fLayerNode(std::move(layer))
252         , fTransformAnimatorsCount(tanim_count)
253         , fIn(in)
254         , fOut(out) {}
255 
256 protected:
onSeek(float t)257     StateChanged onSeek(float t) override {
258         // in/out may be inverted for time-reversed layers
259         const auto active = (t >= fIn && t < fOut) || (t > fOut && t <= fIn);
260 
261         bool changed = false;
262         if (fLayerNode) {
263             changed |= (fLayerNode->isVisible() != active);
264             fLayerNode->setVisible(active);
265         }
266 
267         // When active, dispatch ticks to all layer animators.
268         // When inactive, we must still dispatch ticks to the layer transform animators
269         // (active child layers depend on transforms being updated).
270         const auto dispatch_count = active ? fLayerAnimators.size()
271                                            : fTransformAnimatorsCount;
272         for (size_t i = 0; i < dispatch_count; ++i) {
273             changed |= fLayerAnimators[i]->seek(t);
274         }
275 
276         return changed;
277     }
278 
279 private:
280     const AnimatorScope           fLayerAnimators;
281     const sk_sp<sksg::RenderNode> fLayerNode;
282     const size_t                  fTransformAnimatorsCount;
283     const float                   fIn,
284                                   fOut;
285 };
286 
287 class MotionBlurController final : public Animator {
288 public:
MotionBlurController(sk_sp<MotionBlurEffect> mbe)289     explicit MotionBlurController(sk_sp<MotionBlurEffect> mbe)
290         : fMotionBlurEffect(std::move(mbe)) {}
291 
292 protected:
293     // When motion blur is present, time ticks are not passed to layer animators
294     // but to the motion blur effect. The effect then drives the animators/scene-graph
295     // during reval and render phases.
onSeek(float t)296     StateChanged onSeek(float t) override {
297         fMotionBlurEffect->setT(t);
298         return true;
299     }
300 
301 private:
302     const sk_sp<MotionBlurEffect> fMotionBlurEffect;
303 };
304 
305 } // namespace
306 
LayerBuilder(const skjson::ObjectValue & jlayer,const SkSize & comp_size)307 LayerBuilder::LayerBuilder(const skjson::ObjectValue& jlayer, const SkSize& comp_size)
308     : fJlayer(jlayer)
309     , fIndex      (ParseDefault<int>(jlayer["ind"   ], -1))
310     , fParentIndex(ParseDefault<int>(jlayer["parent"], -1))
311     , fType       (ParseDefault<int>(jlayer["ty"    ], -1))
312     , fAutoOrient (ParseDefault<int>(jlayer["ao"    ],  0))
313     , fInfo{comp_size,
314             ParseDefault<float>(jlayer["ip"], 0.0f),
315             ParseDefault<float>(jlayer["op"], 0.0f)}
316 {
317 
318     if (this->isCamera() || ParseDefault<int>(jlayer["ddd"], 0)) {
319         fFlags |= Flags::kIs3D;
320     }
321 }
322 
323 LayerBuilder::~LayerBuilder() = default;
324 
isCamera() const325 bool LayerBuilder::isCamera() const {
326     static constexpr int kCameraLayerType = 13;
327 
328     return fType == kCameraLayerType;
329 }
330 
buildTransform(const AnimationBuilder & abuilder,CompositionBuilder * cbuilder)331 sk_sp<sksg::Transform> LayerBuilder::buildTransform(const AnimationBuilder& abuilder,
332                                                     CompositionBuilder* cbuilder) {
333     // Depending on the leaf node type, we treat the whole transform chain as either 2D or 3D.
334     const auto transform_chain_type = this->is3D() ? TransformType::k3D
335                                                    : TransformType::k2D;
336     fLayerTransform = this->getTransform(abuilder, cbuilder, transform_chain_type);
337 
338     return fLayerTransform;
339 }
340 
getTransform(const AnimationBuilder & abuilder,CompositionBuilder * cbuilder,TransformType ttype)341 sk_sp<sksg::Transform> LayerBuilder::getTransform(const AnimationBuilder& abuilder,
342                                                   CompositionBuilder* cbuilder,
343                                                   TransformType ttype) {
344     const auto cache_valid_mask = (1ul << ttype);
345     if (!(fFlags & cache_valid_mask)) {
346         // Set valid flag upfront to break cycles.
347         fFlags |= cache_valid_mask;
348 
349         const AnimationBuilder::AutoPropertyTracker apt(&abuilder, fJlayer, PropertyObserver::NodeType::LAYER);
350         AnimationBuilder::AutoScope ascope(&abuilder, std::move(fLayerScope));
351         fTransformCache[ttype] = this->doAttachTransform(abuilder, cbuilder, ttype);
352         fLayerScope = ascope.release();
353         fTransformAnimatorCount = fLayerScope.size();
354     }
355 
356     return fTransformCache[ttype];
357 }
358 
getParentTransform(const AnimationBuilder & abuilder,CompositionBuilder * cbuilder,TransformType ttype)359 sk_sp<sksg::Transform> LayerBuilder::getParentTransform(const AnimationBuilder& abuilder,
360                                                         CompositionBuilder* cbuilder,
361                                                         TransformType ttype) {
362     if (auto* parent_builder = cbuilder->layerBuilder(fParentIndex)) {
363         // Explicit parent layer.
364         return parent_builder->getTransform(abuilder, cbuilder, ttype);
365     }
366 
367     if (ttype == TransformType::k3D) {
368         // During camera transform attachment, cbuilder->getCameraTransform() is null.
369         // This prevents camera->camera transform chain cycles.
370         SkASSERT(!this->isCamera() || !cbuilder->getCameraTransform());
371 
372         // 3D transform chains are implicitly rooted onto the camera.
373         return cbuilder->getCameraTransform();
374     }
375 
376     return nullptr;
377 }
378 
doAttachTransform(const AnimationBuilder & abuilder,CompositionBuilder * cbuilder,TransformType ttype)379 sk_sp<sksg::Transform> LayerBuilder::doAttachTransform(const AnimationBuilder& abuilder,
380                                                        CompositionBuilder* cbuilder,
381                                                        TransformType ttype) {
382     const skjson::ObjectValue* jtransform = fJlayer["ks"];
383     if (!jtransform) {
384         return nullptr;
385     }
386 
387     auto parent_transform = this->getParentTransform(abuilder, cbuilder, ttype);
388 
389     if (this->isCamera()) {
390         // parent_transform applies to the camera itself => it pre-composes inverted to the
391         // camera/view/adapter transform.
392         //
393         //   T_camera' = T_camera x Inv(parent_transform)
394         //
395         return abuilder.attachCamera(fJlayer,
396                                      *jtransform,
397                                      sksg::Transform::MakeInverse(std::move(parent_transform)),
398                                      cbuilder->fSize);
399     }
400 
401     return this->is3D()
402             ? abuilder.attachMatrix3D(*jtransform, std::move(parent_transform), fAutoOrient)
403             : abuilder.attachMatrix2D(*jtransform, std::move(parent_transform), fAutoOrient);
404 }
405 
hasMotionBlur(const CompositionBuilder * cbuilder) const406 bool LayerBuilder::hasMotionBlur(const CompositionBuilder* cbuilder) const {
407     return cbuilder->fMotionBlurSamples > 1
408         && cbuilder->fMotionBlurAngle   > 0
409         && ParseDefault(fJlayer["mb"], false);
410 }
411 
buildRenderTree(const AnimationBuilder & abuilder,CompositionBuilder * cbuilder,const LayerBuilder * prev_layer)412 sk_sp<sksg::RenderNode> LayerBuilder::buildRenderTree(const AnimationBuilder& abuilder,
413                                                       CompositionBuilder* cbuilder,
414                                                       const LayerBuilder* prev_layer) {
415     const AnimationBuilder::AutoPropertyTracker apt(&abuilder, fJlayer, PropertyObserver::NodeType::LAYER);
416 
417     using LayerBuilder =
418         sk_sp<sksg::RenderNode> (AnimationBuilder::*)(const skjson::ObjectValue&,
419                                                       AnimationBuilder::LayerInfo*) const;
420 
421     // AE is annoyingly inconsistent in how effects interact with layer transforms: depending on
422     // the layer type, effects are applied before or after the content is transformed.
423     //
424     // Empirically, pre-rendered layers (for some loose meaning of "pre-rendered") are in the
425     // former category (effects are subject to transformation), while the remaining types are in
426     // the latter.
427     enum : uint32_t {
428         kTransformEffects = 0x01, // The layer transform also applies to its effects.
429         kForceSeek        = 0x02, // Dispatch all seek() events even when the layer is inactive.
430     };
431 
432     static constexpr struct {
433         LayerBuilder                      fBuilder;
434         uint32_t                          fFlags;
435     } gLayerBuildInfo[] = {
436         { &AnimationBuilder::attachPrecompLayer, kTransformEffects },  // 'ty':  0 -> precomp
437         { &AnimationBuilder::attachSolidLayer  , kTransformEffects },  // 'ty':  1 -> solid
438         { &AnimationBuilder::attachFootageLayer, kTransformEffects },  // 'ty':  2 -> image
439         { &AnimationBuilder::attachNullLayer   ,                 0 },  // 'ty':  3 -> null
440         { &AnimationBuilder::attachShapeLayer  ,                 0 },  // 'ty':  4 -> shape
441         { &AnimationBuilder::attachTextLayer   ,                 0 },  // 'ty':  5 -> text
442         { &AnimationBuilder::attachAudioLayer  ,        kForceSeek },  // 'ty':  6 -> audio
443         { nullptr                              ,                 0 },  // 'ty':  7 -> pholderVideo
444         { nullptr                              ,                 0 },  // 'ty':  8 -> imageSeq
445         { &AnimationBuilder::attachFootageLayer, kTransformEffects },  // 'ty':  9 -> video
446         { nullptr                              ,                 0 },  // 'ty': 10 -> pholderStill
447         { nullptr                              ,                 0 },  // 'ty': 11 -> guide
448         { nullptr                              ,                 0 },  // 'ty': 12 -> adjustment
449         { &AnimationBuilder::attachNullLayer   ,                 0 },  // 'ty': 13 -> camera
450         { nullptr                              ,                 0 },  // 'ty': 14 -> light
451     };
452 
453     if (fType < 0 || static_cast<size_t>(fType) >= SK_ARRAY_COUNT(gLayerBuildInfo)) {
454         return nullptr;
455     }
456 
457     const auto& build_info = gLayerBuildInfo[fType];
458 
459     // Switch to the layer animator scope (which at this point holds transform-only animators).
460     AnimationBuilder::AutoScope ascope(&abuilder, std::move(fLayerScope));
461 
462     // Potentially null.
463     sk_sp<sksg::RenderNode> layer;
464 
465     // Build the layer content fragment.
466     if (build_info.fBuilder) {
467         layer = (abuilder.*(build_info.fBuilder))(fJlayer, &fInfo);
468     }
469 
470     // Clip layers with explicit dimensions.
471     float w = 0, h = 0;
472     if (Parse<float>(fJlayer["w"], &w) && Parse<float>(fJlayer["h"], &h)) {
473         layer = sksg::ClipEffect::Make(std::move(layer),
474                                        sksg::Rect::Make(SkRect::MakeWH(w, h)),
475                                        true);
476     }
477 
478     // Optional layer mask.
479     layer = AttachMask(fJlayer["masksProperties"], &abuilder, std::move(layer));
480 
481     // Does the transform apply to effects also?
482     // (AE quirk: it doesn't - except for solid layers)
483     const auto transform_effects = (build_info.fFlags & kTransformEffects);
484 
485     // Attach the transform before effects, when needed.
486     if (fLayerTransform && !transform_effects) {
487         layer = sksg::TransformEffect::Make(std::move(layer), fLayerTransform);
488     }
489 
490     // Optional layer effects.
491     if (const skjson::ArrayValue* jeffects = fJlayer["ef"]) {
492         layer = EffectBuilder(&abuilder, fInfo.fSize, cbuilder)
493                 .attachEffects(*jeffects, std::move(layer));
494     }
495 
496     // Attach the transform after effects, when needed.
497     if (fLayerTransform && transform_effects) {
498         layer = sksg::TransformEffect::Make(std::move(layer), std::move(fLayerTransform));
499     }
500 
501     // Optional layer styles.
502     if (const skjson::ArrayValue* jstyles = fJlayer["sy"]) {
503         layer = EffectBuilder(&abuilder, fInfo.fSize, cbuilder)
504                 .attachStyles(*jstyles, std::move(layer));
505     }
506 
507     // Optional layer opacity.
508     // TODO: de-dupe this "ks" lookup with matrix above.
509     if (const skjson::ObjectValue* jtransform = fJlayer["ks"]) {
510         layer = abuilder.attachOpacity(*jtransform, std::move(layer));
511     }
512 
513     // Stash the content tree in case it is needed for later mattes.
514     fContentTree = layer;
515     if (ParseDefault<bool>(fJlayer["hd"], false)) {
516         layer = nullptr;
517     }
518 
519     const auto has_animators    = !abuilder.fCurrentAnimatorScope->empty();
520     const auto force_seek_count = build_info.fFlags & kForceSeek
521             ? abuilder.fCurrentAnimatorScope->size()
522             : fTransformAnimatorCount;
523 
524     sk_sp<Animator> controller = sk_make_sp<LayerController>(ascope.release(),
525                                                              layer,
526                                                              force_seek_count,
527                                                              fInfo.fInPoint,
528                                                              fInfo.fOutPoint);
529 
530     // Optional motion blur.
531     if (layer && has_animators && this->hasMotionBlur(cbuilder)) {
532         // Wrap both the layer node and the controller.
533         auto motion_blur = MotionBlurEffect::Make(std::move(controller), std::move(layer),
534                                                   cbuilder->fMotionBlurSamples,
535                                                   cbuilder->fMotionBlurAngle,
536                                                   cbuilder->fMotionBlurPhase);
537         controller = sk_make_sp<MotionBlurController>(motion_blur);
538         layer = std::move(motion_blur);
539     }
540 
541     abuilder.fCurrentAnimatorScope->push_back(std::move(controller));
542 
543     if (ParseDefault<bool>(fJlayer["td"], false)) {
544         // |layer| is a track matte.  We apply it as a mask to the next layer.
545         return nullptr;
546     }
547 
548     // Optional matte.
549     const auto matte_mode = prev_layer
550             ? ParseDefault<size_t>(fJlayer["tt"], 0)
551             : 0;
552     if (matte_mode > 0) {
553         static constexpr sksg::MaskEffect::Mode gMatteModes[] = {
554             sksg::MaskEffect::Mode::kAlphaNormal, // tt: 1
555             sksg::MaskEffect::Mode::kAlphaInvert, // tt: 2
556             sksg::MaskEffect::Mode::kLumaNormal,  // tt: 3
557             sksg::MaskEffect::Mode::kLumaInvert,  // tt: 4
558         };
559 
560         if (matte_mode <= SK_ARRAY_COUNT(gMatteModes)) {
561             // The current layer is masked with the previous layer *content*.
562             layer = sksg::MaskEffect::Make(std::move(layer),
563                                            prev_layer->fContentTree,
564                                            gMatteModes[matte_mode - 1]);
565         } else {
566             abuilder.log(Logger::Level::kError, nullptr,
567                          "Unknown track matte mode: %zu\n", matte_mode);
568         }
569     }
570 
571     // Finally, attach an optional blend mode.
572     // NB: blend modes are never applied to matte sources (layer content only).
573     return abuilder.attachBlendMode(fJlayer, std::move(layer));
574 }
575 
576 } // namespace internal
577 } // namespace skottie
578