/*
* Copyright 2018 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "SkottiePriv.h"
#include "SkData.h"
#include "SkFontMgr.h"
#include "SkImage.h"
#include "SkJSON.h"
#include "SkMakeUnique.h"
#include "SkottieJson.h"
#include "SkottieValue.h"
#include "SkParse.h"
#include "SkSGClipEffect.h"
#include "SkSGColor.h"
#include "SkSGDraw.h"
#include "SkSGGroup.h"
#include "SkSGImage.h"
#include "SkSGMaskEffect.h"
#include "SkSGMerge.h"
#include "SkSGOpacityEffect.h"
#include "SkSGPath.h"
#include "SkSGRect.h"
#include "SkSGTransform.h"
#include <algorithm>
#include <vector>
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 };
// 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,
AnimatorScope* ascope,
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
sksg::Merge::Mode merge_mode; // for clipping
};
SkSTArray<4, MaskRecord, true> mask_stack;
bool has_opacity = 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"], ascope);
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_opacity |= abuilder->bindProperty<ScalarValue>((*m)["o"], ascope,
[mask_paint](const ScalarValue& o) {
mask_paint->setOpacity(o * 0.01f);
}, 100.0f);
mask_stack.push_back({mask_path, mask_paint, mask_info->fMergeMode});
}
if (mask_stack.empty())
return childNode;
// If the masks are fully opaque, we can clip.
if (!has_opacity) {
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);
}
sk_sp<sksg::RenderNode> maskNode;
if (mask_stack.count() == 1) {
// no group needed for single mask
maskNode = sksg::Draw::Make(std::move(mask_stack.front().mask_path),
std::move(mask_stack.front().mask_paint));
} else {
std::vector<sk_sp<sksg::RenderNode>> masks;
masks.reserve(SkToSizeT(mask_stack.count()));
for (auto& rec : mask_stack) {
masks.push_back(sksg::Draw::Make(std::move(rec.mask_path),
std::move(rec.mask_paint)));
}
maskNode = sksg::Group::Make(std::move(masks));
}
return sksg::MaskEffect::Make(std::move(childNode), std::move(maskNode));
}
} // namespace
sk_sp<sksg::RenderNode> AnimationBuilder::attachNestedAnimation(const char* name,
AnimatorScope* ascope) const {
class SkottieSGAdapter final : public sksg::RenderNode {
public:
explicit SkottieSGAdapter(sk_sp<Animation> animation)
: fAnimation(std::move(animation)) {
SkASSERT(fAnimation);
}
protected:
SkRect onRevalidate(sksg::InvalidationController*, const SkMatrix&) override {
return SkRect::MakeSize(fAnimation->size());
}
const RenderNode* onNodeAt(const SkPoint&) const override { return nullptr; }
void onRender(SkCanvas* canvas, const RenderContext* ctx) const override {
const auto local_scope =
ScopedRenderContext(canvas, ctx).setIsolation(this->bounds(), true);
fAnimation->render(canvas);
}
private:
const sk_sp<Animation> fAnimation;
};
class SkottieAnimatorAdapter final : public sksg::Animator {
public:
SkottieAnimatorAdapter(sk_sp<Animation> animation, float time_scale)
: fAnimation(std::move(animation))
, fTimeScale(time_scale) {
SkASSERT(fAnimation);
}
protected:
void onTick(float t) {
// TODO: we prolly need more sophisticated timeline mapping for nested animations.
fAnimation->seek(t * fTimeScale);
}
private:
const sk_sp<Animation> fAnimation;
const float fTimeScale;
};
const auto data = fResourceProvider->load("", name);
if (!data) {
this->log(Logger::Level::kError, nullptr, "Could not load: %s.", name);
return nullptr;
}
auto animation = Animation::Builder()
.setResourceProvider(fResourceProvider)
.setFontManager(fLazyFontMgr.getMaybeNull())
.make(static_cast<const char*>(data->data()), data->size());
if (!animation) {
this->log(Logger::Level::kError, nullptr, "Could not parse nested animation: %s.", name);
return nullptr;
}
ascope->push_back(
skstd::make_unique<SkottieAnimatorAdapter>(animation, animation->duration() / fDuration));
return sk_make_sp<SkottieSGAdapter>(std::move(animation));
}
sk_sp<sksg::RenderNode> AnimationBuilder::attachAssetRef(
const skjson::ObjectValue& jlayer, AnimatorScope* ascope,
const std::function<sk_sp<sksg::RenderNode>(const skjson::ObjectValue&,
AnimatorScope*)>& func) const {
const auto refId = ParseDefault<SkString>(jlayer["refId"], SkString());
if (refId.isEmpty()) {
this->log(Logger::Level::kError, nullptr, "Layer missing refId.");
return nullptr;
}
if (refId.startsWith("$")) {
return this->attachNestedAnimation(refId.c_str() + 1, ascope);
}
const auto* asset_info = fAssets.find(refId);
if (!asset_info) {
this->log(Logger::Level::kError, nullptr, "Asset not found: '%s'.", refId.c_str());
return nullptr;
}
if (asset_info->fIsAttaching) {
this->log(Logger::Level::kError, nullptr,
"Asset cycle detected for: '%s'", refId.c_str());
return nullptr;
}
asset_info->fIsAttaching = true;
auto asset = func(*asset_info->fAsset, ascope);
asset_info->fIsAttaching = false;
return asset;
}
sk_sp<sksg::RenderNode> AnimationBuilder::attachSolidLayer(const skjson::ObjectValue& jlayer,
const LayerInfo&,
AnimatorScope*) const {
const auto size = SkSize::Make(ParseDefault<float>(jlayer["sw"], 0.0f),
ParseDefault<float>(jlayer["sh"], 0.0f));
const skjson::StringValue* hex_str = jlayer["sc"];
uint32_t c;
if (size.isEmpty() ||
!hex_str ||
*hex_str->begin() != '#' ||
!SkParse::FindHex(hex_str->begin() + 1, &c)) {
this->log(Logger::Level::kError, &jlayer, "Could not parse solid layer.");
return nullptr;
}
const SkColor color = 0xff000000 | c;
return sksg::Draw::Make(sksg::Rect::Make(SkRect::MakeSize(size)),
sksg::Color::Make(color));
}
const AnimationBuilder::ImageAssetInfo*
AnimationBuilder::loadImageAsset(const skjson::ObjectValue& jimage) const {
const skjson::StringValue* name = jimage["p"];
const skjson::StringValue* path = jimage["u"];
if (!name) {
return nullptr;
}
const auto name_cstr = name->begin(),
path_cstr = path ? path->begin() : "";
const auto res_id = SkStringPrintf("%s|%s", path_cstr, name_cstr);
if (auto* cached_info = fImageAssetCache.find(res_id)) {
return cached_info;
}
auto asset = fResourceProvider->loadImageAsset(path_cstr, name_cstr);
if (!asset) {
this->log(Logger::Level::kError, nullptr,
"Could not load image asset: %s/%s.", path_cstr, name_cstr);
return nullptr;
}
const auto size = SkISize::Make(ParseDefault<int>(jimage["w"], 0),
ParseDefault<int>(jimage["h"], 0));
return fImageAssetCache.set(res_id, { std::move(asset), size });
}
sk_sp<sksg::RenderNode> AnimationBuilder::attachImageAsset(const skjson::ObjectValue& jimage,
const LayerInfo& layer_info,
AnimatorScope* ascope) const {
const auto* asset_info = this->loadImageAsset(jimage);
if (!asset_info) {
return nullptr;
}
SkASSERT(asset_info->fAsset);
auto image = asset_info->fAsset->getFrame(0);
if (!image) {
this->log(Logger::Level::kError, nullptr, "Could not load first image asset frame.");
return nullptr;
}
auto image_node = sksg::Image::Make(image);
if (asset_info->fAsset->isMultiFrame()) {
class MultiFrameAnimator final : public sksg::Animator {
public:
MultiFrameAnimator(sk_sp<ImageAsset> asset, sk_sp<sksg::Image> image_node,
float time_bias, float time_scale)
: fAsset(std::move(asset))
, fImageNode(std::move(image_node))
, fTimeBias(time_bias)
, fTimeScale(time_scale) {}
void onTick(float t) override {
fImageNode->setImage(fAsset->getFrame((t + fTimeBias) * fTimeScale));
}
private:
sk_sp<ImageAsset> fAsset;
sk_sp<sksg::Image> fImageNode;
float fTimeBias,
fTimeScale;
};
ascope->push_back(skstd::make_unique<MultiFrameAnimator>(asset_info->fAsset,
image_node,
layer_info.fInPoint,
1 / fFrameRate));
}
const auto asset_size = SkISize::Make(
asset_info->fSize.width() > 0 ? asset_info->fSize.width() : image->width(),
asset_info->fSize.height() > 0 ? asset_info->fSize.height() : image->height());
if (asset_size == image->bounds().size()) {
// No resize needed.
return std::move(image_node);
}
return sksg::TransformEffect::Make(std::move(image_node),
SkMatrix::MakeRectToRect(SkRect::Make(image->bounds()),
SkRect::Make(asset_size),
SkMatrix::kCenter_ScaleToFit));
}
sk_sp<sksg::RenderNode> AnimationBuilder::attachImageLayer(const skjson::ObjectValue& jlayer,
const LayerInfo& layer_info,
AnimatorScope* ascope) const {
return this->attachAssetRef(jlayer, ascope,
[this, &layer_info] (const skjson::ObjectValue& jimage, AnimatorScope* ascope) {
return this->attachImageAsset(jimage, layer_info, ascope);
});
}
sk_sp<sksg::RenderNode> AnimationBuilder::attachNullLayer(const skjson::ObjectValue& layer,
const LayerInfo&,
AnimatorScope*) const {
// Null layers are used solely to drive dependent transforms,
// but we use free-floating sksg::Matrices for that purpose.
return nullptr;
}
struct AnimationBuilder::AttachLayerContext {
AttachLayerContext(const skjson::ArrayValue& jlayers, AnimatorScope* scope)
: fLayerList(jlayers), fScope(scope) {}
const skjson::ArrayValue& fLayerList;
AnimatorScope* fScope;
SkTHashMap<int, sk_sp<sksg::Transform>> fLayerMatrixMap;
sk_sp<sksg::RenderNode> fCurrentMatte;
sk_sp<sksg::Transform> attachLayerTransform(const skjson::ObjectValue& jlayer,
const AnimationBuilder* abuilder) {
const auto layer_index = ParseDefault<int>(jlayer["ind"], -1);
if (layer_index < 0)
return nullptr;
if (auto* m = fLayerMatrixMap.find(layer_index))
return *m;
return this->attachLayerTransformImpl(jlayer, abuilder, layer_index);
}
private:
sk_sp<sksg::Transform> 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 (auto* m = fLayerMatrixMap.find(parent_index))
return *m;
for (const skjson::ObjectValue* l : fLayerList) {
if (!l) continue;
if (ParseDefault<int>((*l)["ind"], -1) == parent_index) {
return this->attachLayerTransformImpl(*l, abuilder, parent_index);
}
}
return nullptr;
}
sk_sp<sksg::Transform> attachLayerTransformImpl(const skjson::ObjectValue& jlayer,
const AnimationBuilder* abuilder,
int layer_index) {
SkASSERT(!fLayerMatrixMap.find(layer_index));
// Add a stub entry to break recursion cycles.
fLayerMatrixMap.set(layer_index, nullptr);
auto parent_matrix = this->attachParentLayerTransform(jlayer, abuilder, layer_index);
if (const skjson::ObjectValue* jtransform = jlayer["ks"]) {
auto transform_node = (ParseDefault<int>(jlayer["ddd"], 0) == 0)
? abuilder->attachMatrix2D(*jtransform, fScope, std::move(parent_matrix))
: abuilder->attachMatrix3D(*jtransform, fScope, std::move(parent_matrix));
return *fLayerMatrixMap.set(layer_index, std::move(transform_node));
}
return nullptr;
}
};
sk_sp<sksg::RenderNode> AnimationBuilder::attachLayer(const skjson::ObjectValue* jlayer,
AttachLayerContext* layerCtx) const {
if (!jlayer) return nullptr;
const LayerInfo layer_info = {
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 LayerAttacher = sk_sp<sksg::RenderNode> (AnimationBuilder::*)(const skjson::ObjectValue&,
const LayerInfo&,
AnimatorScope*) const;
static constexpr LayerAttacher gLayerAttachers[] = {
&AnimationBuilder::attachPrecompLayer, // 'ty': 0
&AnimationBuilder::attachSolidLayer, // 'ty': 1
&AnimationBuilder::attachImageLayer, // 'ty': 2
&AnimationBuilder::attachNullLayer, // 'ty': 3
&AnimationBuilder::attachShapeLayer, // 'ty': 4
&AnimationBuilder::attachTextLayer, // 'ty': 5
};
int type = ParseDefault<int>((*jlayer)["ty"], -1);
if (type < 0 || type >= SkTo<int>(SK_ARRAY_COUNT(gLayerAttachers))) {
return nullptr;
}
AnimatorScope layer_animators;
// Layer content.
auto layer = (this->*(gLayerAttachers[type]))(*jlayer, layer_info, &layer_animators);
// 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, &layer_animators, std::move(layer));
// Optional layer transform.
if (auto layer_transform = layerCtx->attachLayerTransform(*jlayer, this)) {
layer = sksg::TransformEffect::Make(std::move(layer), std::move(layer_transform));
}
// Optional layer opacity.
// TODO: de-dupe this "ks" lookup with matrix above.
if (const skjson::ObjectValue* jtransform = (*jlayer)["ks"]) {
layer = this->attachOpacity(*jtransform, &layer_animators, std::move(layer));
}
// Optional layer effects.
if (const skjson::ArrayValue* jeffects = (*jlayer)["ef"]) {
layer = this->attachLayerEffects(*jeffects, &layer_animators, std::move(layer));
}
// Optional blend mode.
layer = this->attachBlendMode(*jlayer, std::move(layer));
class LayerController final : public sksg::GroupAnimator {
public:
LayerController(sksg::AnimatorList&& layer_animators,
sk_sp<sksg::OpacityEffect> controlNode,
float in, float out)
: INHERITED(std::move(layer_animators))
, fControlNode(std::move(controlNode))
, fIn(in)
, fOut(out) {}
void onTick(float t) override {
const auto active = (t >= fIn && t <= fOut);
// Keep the layer fully transparent except for its [in..out] lifespan.
// (note: opacity == 0 disables rendering, while opacity == 1 is a noop)
fControlNode->setOpacity(active ? 1 : 0);
// Dispatch ticks only while active.
if (active) this->INHERITED::onTick(t);
}
private:
const sk_sp<sksg::OpacityEffect> fControlNode;
const float fIn,
fOut;
using INHERITED = sksg::GroupAnimator;
};
auto controller_node = sksg::OpacityEffect::Make(std::move(layer));
if (!controller_node) {
return nullptr;
}
layerCtx->fScope->push_back(
skstd::make_unique<LayerController>(std::move(layer_animators), controller_node,
layer_info.fInPoint, layer_info.fOutPoint));
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(controller_node);
return nullptr;
}
if (layerCtx->fCurrentMatte) {
// There is a pending matte. Apply and reset.
static constexpr sksg::MaskEffect::Mode gMaskModes[] = {
sksg::MaskEffect::Mode::kNormal, // tt: 1
sksg::MaskEffect::Mode::kInvert, // tt: 2
};
const auto matteType = ParseDefault<size_t>((*jlayer)["tt"], 1) - 1;
if (matteType < SK_ARRAY_COUNT(gMaskModes)) {
return sksg::MaskEffect::Make(std::move(controller_node),
std::move(layerCtx->fCurrentMatte),
gMaskModes[matteType]);
}
layerCtx->fCurrentMatte.reset();
}
return std::move(controller_node);
}
sk_sp<sksg::RenderNode> AnimationBuilder::attachComposition(const skjson::ObjectValue& comp,
AnimatorScope* scope) const {
const skjson::ArrayValue* jlayers = comp["layers"];
if (!jlayers) return nullptr;
std::vector<sk_sp<sksg::RenderNode>> layers;
AttachLayerContext layerCtx(*jlayers, scope);
layers.reserve(jlayers->size());
for (const auto& l : *jlayers) {
if (auto layer = this->attachLayer(l, &layerCtx)) {
layers.push_back(std::move(layer));
}
}
if (layers.empty()) {
return nullptr;
}
// Layers are painted in bottom->top order.
std::reverse(layers.begin(), layers.end());
layers.shrink_to_fit();
return sksg::Group::Make(std::move(layers));
}
} // namespace internal
} // namespace skottie