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