blob: 4fdcf6836889a20848a7174b077154be0372863c [file] [log] [blame]
/*
* Copyright 2019 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "modules/skottie/src/SkottiePriv.h"
#include "modules/skottie/src/SkottieAdapter.h"
#include "modules/skottie/src/SkottieJson.h"
#include "modules/skottie/src/effects/Effects.h"
#include "modules/skottie/src/effects/MotionBlurEffect.h"
#include "modules/sksg/include/SkSGClipEffect.h"
#include "modules/sksg/include/SkSGDraw.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"
namespace skottie {
namespace internal {
namespace {
static constexpr int kNullLayerType = 3;
static constexpr int kCameraLayerType = 13;
struct MaskInfo {
SkBlendMode fBlendMode; // used when masking with layers/blending
sksg::Merge::Mode fMergeMode; // used when clipping
bool fInvertGeometry;
};
const MaskInfo* GetMaskInfo(char mode) {
static constexpr MaskInfo k_add_info =
{ SkBlendMode::kSrcOver , sksg::Merge::Mode::kUnion , false };
static constexpr MaskInfo k_int_info =
{ SkBlendMode::kSrcIn , sksg::Merge::Mode::kIntersect , false };
// AE 'subtract' is the same as 'intersect' + inverted geometry
// (draws the opacity-adjusted paint *outside* the shape).
static constexpr MaskInfo k_sub_info =
{ SkBlendMode::kSrcIn , sksg::Merge::Mode::kIntersect , true };
static constexpr MaskInfo k_dif_info =
{ SkBlendMode::kDifference, sksg::Merge::Mode::kDifference, false };
switch (mode) {
case 'a': return &k_add_info;
case 'f': return &k_dif_info;
case 'i': return &k_int_info;
case 's': return &k_sub_info;
default: break;
}
return nullptr;
}
sk_sp<sksg::RenderNode> AttachMask(const skjson::ArrayValue* jmask,
const AnimationBuilder* abuilder,
sk_sp<sksg::RenderNode> childNode) {
if (!jmask) return childNode;
struct MaskRecord {
sk_sp<sksg::Path> mask_path; // for clipping and masking
sk_sp<sksg::Color> mask_paint; // for masking
sk_sp<sksg::BlurImageFilter> mask_blur; // for masking
sksg::Merge::Mode merge_mode; // for clipping
};
SkSTArray<4, MaskRecord, true> mask_stack;
bool has_effect = false;
auto blur_effect = sksg::BlurImageFilter::Make();
for (const skjson::ObjectValue* m : *jmask) {
if (!m) continue;
const skjson::StringValue* jmode = (*m)["mode"];
if (!jmode || jmode->size() != 1) {
abuilder->log(Logger::Level::kError, &(*m)["mode"], "Invalid mask mode.");
continue;
}
const auto mode = *jmode->begin();
if (mode == 'n') {
// "None" masks have no effect.
continue;
}
const auto* mask_info = GetMaskInfo(mode);
if (!mask_info) {
abuilder->log(Logger::Level::kWarning, nullptr, "Unsupported mask mode: '%c'.", mode);
continue;
}
auto mask_path = abuilder->attachPath((*m)["pt"]);
if (!mask_path) {
abuilder->log(Logger::Level::kError, m, "Could not parse mask path.");
continue;
}
// "inv" is cumulative with mask info fInvertGeometry
const auto inverted =
(mask_info->fInvertGeometry != ParseDefault<bool>((*m)["inv"], false));
mask_path->setFillType(inverted ? SkPath::kInverseWinding_FillType
: SkPath::kWinding_FillType);
auto mask_paint = sksg::Color::Make(SK_ColorBLACK);
mask_paint->setAntiAlias(true);
// First mask in the stack initializes the mask buffer.
mask_paint->setBlendMode(mask_stack.empty() ? SkBlendMode::kSrc
: mask_info->fBlendMode);
has_effect |= abuilder->bindProperty<ScalarValue>((*m)["o"],
[mask_paint](const ScalarValue& o) {
mask_paint->setOpacity(o * 0.01f);
}, 100.0f);
static const VectorValue default_feather = { 0, 0 };
if (abuilder->bindProperty<VectorValue>((*m)["f"],
[blur_effect](const VectorValue& feather) {
// Close enough to AE.
static constexpr SkScalar kFeatherToSigma = 0.38f;
auto sX = feather.size() > 0 ? feather[0] * kFeatherToSigma : 0,
sY = feather.size() > 1 ? feather[1] * kFeatherToSigma : 0;
blur_effect->setSigma({ sX, sY });
}, default_feather)) {
has_effect = true;
mask_stack.push_back({ mask_path,
mask_paint,
std::move(blur_effect),
mask_info->fMergeMode});
blur_effect = sksg::BlurImageFilter::Make();
} else {
mask_stack.push_back({mask_path, mask_paint, nullptr, mask_info->fMergeMode});
}
}
if (mask_stack.empty())
return childNode;
// If the masks are fully opaque, we can clip.
if (!has_effect) {
sk_sp<sksg::GeometryNode> clip_node;
if (mask_stack.count() == 1) {
// Single path -> just clip.
clip_node = std::move(mask_stack.front().mask_path);
} else {
// Multiple clip paths -> merge.
std::vector<sksg::Merge::Rec> merge_recs;
merge_recs.reserve(SkToSizeT(mask_stack.count()));
for (auto& mask : mask_stack) {
const auto mode = merge_recs.empty() ? sksg::Merge::Mode::kMerge : mask.merge_mode;
merge_recs.push_back({std::move(mask.mask_path), mode});
}
clip_node = sksg::Merge::Make(std::move(merge_recs));
}
return sksg::ClipEffect::Make(std::move(childNode), std::move(clip_node), true);
}
const auto make_mask = [](const MaskRecord& rec) {
auto mask = sksg::Draw::Make(std::move(rec.mask_path),
std::move(rec.mask_paint));
// Optional mask blur (feather).
return sksg::ImageFilterEffect::Make(std::move(mask), std::move(rec.mask_blur));
};
sk_sp<sksg::RenderNode> maskNode;
if (mask_stack.count() == 1) {
// no group needed for single mask
maskNode = make_mask(mask_stack.front());
} else {
std::vector<sk_sp<sksg::RenderNode>> masks;
masks.reserve(SkToSizeT(mask_stack.count()));
for (auto& rec : mask_stack) {
masks.push_back(make_mask(rec));
}
maskNode = sksg::Group::Make(std::move(masks));
}
return sksg::MaskEffect::Make(std::move(childNode), std::move(maskNode));
}
class LayerController final : public sksg::Animator {
public:
LayerController(sksg::AnimatorList&& 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:
void onTick(float t) override {
const auto active = (t >= fIn && t < fOut);
if (fLayerNode) {
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) {
fLayerAnimators[i]->tick(t);
}
}
private:
const sksg::AnimatorList fLayerAnimators;
const sk_sp<sksg::RenderNode> fLayerNode;
const size_t fTransformAnimatorsCount;
const float fIn,
fOut;
};
class MotionBlurController final : public sksg::Animator {
public:
explicit MotionBlurController(sk_sp<MotionBlurEffect> mbe)
: fMotionBlurEffect(std::move(mbe)) {}
protected:
// When motion blur is present, time ticks are not passed to layer animators
// but to the motion blur effect. The effect then drives the animators/scene-graph
// during reval and render phases.
void onTick(float t) override {
fMotionBlurEffect->setT(t);
}
private:
const sk_sp<MotionBlurEffect> fMotionBlurEffect;
};
} // namespace
AnimationBuilder::AttachLayerContext::AttachLayerContext(const skjson::ArrayValue& jlayers)
: fLayerList(jlayers) {}
AnimationBuilder::AttachLayerContext::~AttachLayerContext() = default;
AnimationBuilder::AttachLayerContext::TransformRec
AnimationBuilder::AttachLayerContext::attachLayerTransform(const skjson::ObjectValue& jlayer,
const AnimationBuilder* abuilder,
TransformType type) {
TransformRec result;
const auto layer_index = ParseDefault<int>(jlayer["ind"], -1);
if (layer_index >= 0) {
auto* rec = fLayerTransformMap.find(layer_index);
if (!rec) {
rec = this->attachLayerTransformImpl(jlayer, abuilder, type, layer_index);
}
SkASSERT(rec);
// Note: the transform animator scope is *moved* to the result, because
// we want the animators transferred to the LayerController.
//
// This is safe because a) the scope is not used internally, and
// b) there is exactly one attachLayerTransform call per layer.
// The transform node OTOH may be used at a later time for parenting.
result.fTransformNode = rec->fTransformNode;
result.fTransformScope = std::move(rec->fTransformScope);
}
return result;
}
sk_sp<sksg::Transform>
AnimationBuilder::AttachLayerContext::attachParentLayerTransform(const skjson::ObjectValue& jlayer,
const AnimationBuilder* abuilder,
int layer_index) {
const auto parent_index = ParseDefault<int>(jlayer["parent"], -1);
if (parent_index < 0 || parent_index == layer_index)
return nullptr;
if (const auto* rec = fLayerTransformMap.find(parent_index))
return rec->fTransformNode;
for (const skjson::ObjectValue* l : fLayerList) {
if (!l) continue;
if (ParseDefault<int>((*l)["ind"], -1) == parent_index) {
const auto parent_type = ParseDefault<int>((*l)["ty"], -1) == kCameraLayerType
? TransformType::kCamera
: TransformType::kLayer;
return this->attachLayerTransformImpl(*l,
abuilder,
parent_type,
parent_index)->fTransformNode;
}
}
return nullptr;
}
sk_sp<sksg::Transform>
AnimationBuilder::AttachLayerContext::attachTransformNode(const skjson::ObjectValue& jlayer,
const AnimationBuilder* abuilder,
sk_sp<sksg::Transform> parent_transform,
TransformType type) const {
const skjson::ObjectValue* jtransform = jlayer["ks"];
if (!jtransform) {
return nullptr;
}
if (type == TransformType::kCamera) {
auto camera_adapter = sk_make_sp<CameraAdapter>(abuilder->fSize);
abuilder->bindProperty<ScalarValue>(jlayer["pe"],
[camera_adapter] (const ScalarValue& pe) {
// 'pe' (perspective?) corresponds to AE's "zoom" camera property.
camera_adapter->setZoom(pe);
});
// parent_transform applies to the camera itself => it pre-composes inverted to the
// camera/view/adapter transform.
//
// T_camera' = T_camera x Inv(parent_transform)
//
parent_transform = sksg::Transform::MakeInverse(std::move(parent_transform));
return abuilder->attachMatrix3D(*jtransform,
std::move(parent_transform),
std::move(camera_adapter),
true); // pre-compose parent
}
return (ParseDefault<int>(jlayer["ddd"], 0) == 0)
? abuilder->attachMatrix2D(*jtransform, std::move(parent_transform))
: abuilder->attachMatrix3D(*jtransform, std::move(parent_transform));
}
AnimationBuilder::AttachLayerContext::TransformRec*
AnimationBuilder::AttachLayerContext::attachLayerTransformImpl(const skjson::ObjectValue& jlayer,
const AnimationBuilder* abuilder,
TransformType type,
int layer_index) {
SkASSERT(!fLayerTransformMap.find(layer_index));
// Add a stub entry to break recursion cycles.
fLayerTransformMap.set(layer_index, { nullptr, {} });
auto parent_matrix = this->attachParentLayerTransform(jlayer, abuilder, layer_index);
AutoScope ascope(abuilder);
auto transform = this->attachTransformNode(jlayer,
abuilder,
std::move(parent_matrix),
type);
return fLayerTransformMap.set(layer_index, { std::move(transform), ascope.release() });
}
bool AnimationBuilder::AttachLayerContext::hasMotionBlur(const skjson::ObjectValue& jlayer) const {
return fMotionBlurSamples > 1
&& fMotionBlurAngle > 0
&& ParseDefault(jlayer["mb"], false);
}
sk_sp<sksg::RenderNode> AnimationBuilder::attachLayer(const skjson::ObjectValue* jlayer,
AttachLayerContext* layerCtx) const {
if (!jlayer) {
return nullptr;
}
LayerInfo layer_info = {
fSize,
ParseDefault<float>((*jlayer)["ip"], 0.0f),
ParseDefault<float>((*jlayer)["op"], 0.0f),
};
if (layer_info.fInPoint >= layer_info.fOutPoint) {
this->log(Logger::Level::kError, nullptr,
"Invalid layer in/out points: %f/%f.", layer_info.fInPoint, layer_info.fOutPoint);
return nullptr;
}
const AutoPropertyTracker apt(this, *jlayer);
using LayerBuilder = sk_sp<sksg::RenderNode> (AnimationBuilder::*)(const skjson::ObjectValue&,
LayerInfo*) const;
// 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 = 1, // The layer transform also applies to its effects.
};
static constexpr struct {
LayerBuilder fBuilder;
uint32_t fFlags;
} gLayerBuildInfo[] = {
{ &AnimationBuilder::attachPrecompLayer, kTransformEffects }, // 'ty': 0 -> precomp
{ &AnimationBuilder::attachSolidLayer , kTransformEffects }, // 'ty': 1 -> solid
{ &AnimationBuilder::attachImageLayer , kTransformEffects }, // 'ty': 2 -> image
{ &AnimationBuilder::attachNullLayer , 0 }, // 'ty': 3 -> null
{ &AnimationBuilder::attachShapeLayer , 0 }, // 'ty': 4 -> shape
{ &AnimationBuilder::attachTextLayer , 0 }, // 'ty': 5 -> text
};
const auto type = ParseDefault<int>((*jlayer)["ty"], -1);
if ((type < 0) ||
(type >= SkTo<int>(SK_ARRAY_COUNT(gLayerBuildInfo)) && type != kCameraLayerType)) {
return nullptr;
}
// Optional layer transform.
const auto transform_type = (type == kCameraLayerType)
? AttachLayerContext::TransformType::kCamera
: AttachLayerContext::TransformType::kLayer;
auto layer_transform_rec = layerCtx->attachLayerTransform(*jlayer, this, transform_type);
if (type == kCameraLayerType) {
// Camera layers are special: they don't build normal SG fragments, but drive a root-level
// transform.
if (layerCtx->fCameraTransform) {
this->log(Logger::Level::kWarning, jlayer, "Ignoring duplicate camera layer.");
return nullptr;
}
layerCtx->fCameraTransform = layer_transform_rec.fTransformNode;
}
AutoScope ascope(this, std::move(layer_transform_rec.fTransformScope));
const auto transform_animator_count = fCurrentAnimatorScope->size();
const auto is_hidden = ParseDefault<bool>((*jlayer)["hd"], false) || type == kCameraLayerType;
const auto& build_info = gLayerBuildInfo[is_hidden ? kNullLayerType : type];
// Build the layer content fragment.
auto layer = (this->*(build_info.fBuilder))(*jlayer, &layer_info);
// Clip layers with explicit dimensions.
float w = 0, h = 0;
if (Parse<float>((*jlayer)["w"], &w) && Parse<float>((*jlayer)["h"], &h)) {
layer = sksg::ClipEffect::Make(std::move(layer),
sksg::Rect::Make(SkRect::MakeWH(w, h)),
true);
}
// Optional layer mask.
layer = AttachMask((*jlayer)["masksProperties"], this, std::move(layer));
// Does the transform apply to effects also?
// (AE quirk: it doesn't - except for solid layers)
const auto transform_effects = (build_info.fFlags & kTransformEffects);
// Attach the transform before effects, when needed.
if (layer_transform_rec.fTransformNode && !transform_effects) {
layer = sksg::TransformEffect::Make(std::move(layer), layer_transform_rec.fTransformNode);
}
// Optional layer effects.
if (const skjson::ArrayValue* jeffects = (*jlayer)["ef"]) {
layer = EffectBuilder(this, layer_info.fSize).attachEffects(*jeffects, std::move(layer));
}
// Attach the transform after effects, when needed.
if (layer_transform_rec.fTransformNode && transform_effects) {
layer = sksg::TransformEffect::Make(std::move(layer),
std::move(layer_transform_rec.fTransformNode));
}
// Optional layer opacity.
// TODO: de-dupe this "ks" lookup with matrix above.
if (const skjson::ObjectValue* jtransform = (*jlayer)["ks"]) {
layer = this->attachOpacity(*jtransform, std::move(layer));
}
// Optional blend mode.
layer = this->attachBlendMode(*jlayer, std::move(layer));
const auto has_animators = !fCurrentAnimatorScope->empty();
sk_sp<sksg::Animator> controller = sk_make_sp<LayerController>(ascope.release(),
layer,
transform_animator_count,
layer_info.fInPoint,
layer_info.fOutPoint);
// Optional motion blur.
if (layer && has_animators && layerCtx->hasMotionBlur(*jlayer)) {
SkASSERT(layerCtx->fMotionBlurAngle >= 0);
// Wrap both the layer node and the controller.
auto motion_blur = MotionBlurEffect::Make(std::move(controller), std::move(layer),
layerCtx->fMotionBlurSamples,
layerCtx->fMotionBlurAngle,
layerCtx->fMotionBlurPhase);
controller = sk_make_sp<MotionBlurController>(motion_blur);
layer = std::move(motion_blur);
}
fCurrentAnimatorScope->push_back(std::move(controller));
if (!layer) {
return nullptr;
}
if (ParseDefault<bool>((*jlayer)["td"], false)) {
// This layer is a matte. We apply it as a mask to the next layer.
layerCtx->fCurrentMatte = std::move(layer);
return nullptr;
}
if (layerCtx->fCurrentMatte) {
// There is a pending matte. Apply and reset.
static constexpr sksg::MaskEffect::Mode gMaskModes[] = {
sksg::MaskEffect::Mode::kAlphaNormal, // tt: 1
sksg::MaskEffect::Mode::kAlphaInvert, // tt: 2
sksg::MaskEffect::Mode::kLumaNormal, // tt: 3
sksg::MaskEffect::Mode::kLumaInvert, // tt: 4
};
const auto matteType = ParseDefault<size_t>((*jlayer)["tt"], 1) - 1;
if (matteType < SK_ARRAY_COUNT(gMaskModes)) {
return sksg::MaskEffect::Make(std::move(layer),
std::move(layerCtx->fCurrentMatte),
gMaskModes[matteType]);
}
layerCtx->fCurrentMatte.reset();
}
return layer;
}
} // namespace internal
} // namespace skottie