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