[skottie] Motion tile effect

Implement support for AE's Motion Tile effect [1].

This is the first effect which needs layer size information, so the CL includes
related plumbing.

Limitations: no phase support at this point.

[1] https://helpx.adobe.com/after-effects/using/stylize-effects.html#motion_tile_effect

Change-Id: I023bf8a9d3e3d2a48458fa94218f143e6aac4c9f
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/221244
Reviewed-by: Mike Reed <reed@google.com>
Commit-Queue: Florin Malita <fmalita@chromium.org>
This commit is contained in:
Florin Malita 2019-06-17 11:37:02 -04:00 committed by Skia Commit-Bot
parent e66780dd1c
commit b97824d4d1
12 changed files with 256 additions and 32 deletions

View File

@ -34,6 +34,7 @@ skia_skottie_sources = [
"$_src/effects/GaussianBlurEffect.cpp",
"$_src/effects/GradientEffect.cpp",
"$_src/effects/LevelsEffect.cpp",
"$_src/effects/MotionTileEffect.cpp",
"$_src/effects/TintEffect.cpp",
"$_src/effects/TransformEffect.cpp",
"$_src/effects/TritoneEffect.cpp",

View File

@ -307,8 +307,7 @@ sk_sp<sksg::RenderNode> AnimationBuilder::attachAssetRef(
}
sk_sp<sksg::RenderNode> AnimationBuilder::attachSolidLayer(const skjson::ObjectValue& jlayer,
const LayerInfo&,
AnimatorScope*) 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"];
@ -358,7 +357,7 @@ AnimationBuilder::loadImageAsset(const skjson::ObjectValue& jimage) const {
}
sk_sp<sksg::RenderNode> AnimationBuilder::attachImageAsset(const skjson::ObjectValue& jimage,
const LayerInfo& layer_info,
LayerInfo* layer_info,
AnimatorScope* ascope) const {
const auto* asset_info = this->loadImageAsset(jimage);
if (!asset_info) {
@ -398,7 +397,7 @@ sk_sp<sksg::RenderNode> AnimationBuilder::attachImageAsset(const skjson::ObjectV
ascope->push_back(skstd::make_unique<MultiFrameAnimator>(asset_info->fAsset,
image_node,
-layer_info.fInPoint,
-layer_info->fInPoint,
1 / fFrameRate));
}
@ -406,6 +405,9 @@ sk_sp<sksg::RenderNode> AnimationBuilder::attachImageAsset(const skjson::ObjectV
asset_info->fSize.width() > 0 ? asset_info->fSize.width() : image->width(),
asset_info->fSize.height() > 0 ? asset_info->fSize.height() : image->height());
// Image layers are sized explicitly.
layer_info->fSize = asset_size;
if (asset_size == image->bounds().size()) {
// No resize needed.
return std::move(image_node);
@ -418,7 +420,7 @@ sk_sp<sksg::RenderNode> AnimationBuilder::attachImageAsset(const skjson::ObjectV
}
sk_sp<sksg::RenderNode> AnimationBuilder::attachImageLayer(const skjson::ObjectValue& jlayer,
const LayerInfo& layer_info,
LayerInfo* layer_info,
AnimatorScope* ascope) const {
return this->attachAssetRef(jlayer, ascope,
[this, &layer_info] (const skjson::ObjectValue& jimage, AnimatorScope* ascope) {
@ -427,8 +429,7 @@ sk_sp<sksg::RenderNode> AnimationBuilder::attachImageLayer(const skjson::ObjectV
}
sk_sp<sksg::RenderNode> AnimationBuilder::attachNullLayer(const skjson::ObjectValue& layer,
const LayerInfo&,
AnimatorScope*) 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;
@ -544,9 +545,10 @@ sk_sp<sksg::RenderNode> AnimationBuilder::attachLayer(const skjson::ObjectValue*
return nullptr;
}
const LayerInfo layer_info = {
LayerInfo layer_info = {
fSize,
ParseDefault<float>((*jlayer)["ip"], 0.0f),
ParseDefault<float>((*jlayer)["op"], 0.0f)
ParseDefault<float>((*jlayer)["op"], 0.0f),
};
if (layer_info.fInPoint >= layer_info.fOutPoint) {
this->log(Logger::Level::kError, nullptr,
@ -557,7 +559,7 @@ sk_sp<sksg::RenderNode> AnimationBuilder::attachLayer(const skjson::ObjectValue*
const AutoPropertyTracker apt(this, *jlayer);
using LayerBuilder = sk_sp<sksg::RenderNode> (AnimationBuilder::*)(const skjson::ObjectValue&,
const LayerInfo&,
LayerInfo*,
AnimatorScope*) const;
// AE is annoyingly inconsistent in how effects interact with layer transforms: depending on
@ -606,7 +608,7 @@ sk_sp<sksg::RenderNode> AnimationBuilder::attachLayer(const skjson::ObjectValue*
AnimatorScope layer_animators;
// Build the layer content fragment.
auto layer = (this->*(build_info.fBuilder))(*jlayer, layer_info, &layer_animators);
auto layer = (this->*(build_info.fBuilder))(*jlayer, &layer_info, &layer_animators);
// Clip layers with explicit dimensions.
float w = 0, h = 0;
@ -633,7 +635,8 @@ sk_sp<sksg::RenderNode> AnimationBuilder::attachLayer(const skjson::ObjectValue*
// Optional layer effects.
if (const skjson::ArrayValue* jeffects = (*jlayer)["ef"]) {
layer = EffectBuilder(this, &layer_animators).attachEffects(*jeffects, std::move(layer));
layer = EffectBuilder(this, layer_info.fSize, &layer_animators)
.attachEffects(*jeffects, std::move(layer));
}
// Attach the transform after effects, when needed.

View File

@ -19,7 +19,7 @@ namespace skottie {
namespace internal {
sk_sp<sksg::RenderNode> AnimationBuilder::attachPrecompLayer(const skjson::ObjectValue& jlayer,
const LayerInfo&,
LayerInfo* layer_info,
AnimatorScope* ascope) const {
const skjson::ObjectValue* time_remap = jlayer["tm"];
// Empirically, a time mapper supersedes start/stretch.
@ -29,6 +29,10 @@ sk_sp<sksg::RenderNode> AnimationBuilder::attachPrecompLayer(const skjson::Objec
!SkScalarNearlyEqual(stretch_time, 1) ||
time_remap;
// Precomp layers are sized explicitly.
layer_info->fSize = SkSize::Make(ParseDefault<float>(jlayer["w"], 0.0f),
ParseDefault<float>(jlayer["h"], 0.0f));
AnimatorScope local_animators;
auto precomp_layer = this->attachAssetRef(jlayer,
requires_time_mapping ? &local_animators : ascope,

View File

@ -110,22 +110,22 @@ private:
const std::function<sk_sp<sksg::RenderNode>(const skjson::ObjectValue&,
AnimatorScope* ctx)>&) const;
const ImageAssetInfo* loadImageAsset(const skjson::ObjectValue&) const;
sk_sp<sksg::RenderNode> attachImageAsset(const skjson::ObjectValue&, const LayerInfo&,
sk_sp<sksg::RenderNode> attachImageAsset(const skjson::ObjectValue&, LayerInfo*,
AnimatorScope*) const;
sk_sp<sksg::RenderNode> attachNestedAnimation(const char* name, AnimatorScope* ascope) const;
sk_sp<sksg::RenderNode> attachImageLayer (const skjson::ObjectValue&, const LayerInfo&,
sk_sp<sksg::RenderNode> attachImageLayer (const skjson::ObjectValue&, LayerInfo*,
AnimatorScope*) const;
sk_sp<sksg::RenderNode> attachNullLayer (const skjson::ObjectValue&, const LayerInfo&,
sk_sp<sksg::RenderNode> attachNullLayer (const skjson::ObjectValue&, LayerInfo*,
AnimatorScope*) const;
sk_sp<sksg::RenderNode> attachPrecompLayer(const skjson::ObjectValue&, const LayerInfo&,
sk_sp<sksg::RenderNode> attachPrecompLayer(const skjson::ObjectValue&, LayerInfo*,
AnimatorScope*) const;
sk_sp<sksg::RenderNode> attachShapeLayer (const skjson::ObjectValue&, const LayerInfo&,
sk_sp<sksg::RenderNode> attachShapeLayer (const skjson::ObjectValue&, LayerInfo*,
AnimatorScope*) const;
sk_sp<sksg::RenderNode> attachSolidLayer (const skjson::ObjectValue&, const LayerInfo&,
sk_sp<sksg::RenderNode> attachSolidLayer (const skjson::ObjectValue&, LayerInfo*,
AnimatorScope*) const;
sk_sp<sksg::RenderNode> attachTextLayer (const skjson::ObjectValue&, const LayerInfo&,
sk_sp<sksg::RenderNode> attachTextLayer (const skjson::ObjectValue&, LayerInfo*,
AnimatorScope*) const;
bool dispatchColorProperty(const sk_sp<sksg::Color>&) const;
@ -184,10 +184,10 @@ private:
mutable const char* fPropertyObserverContext;
mutable bool fHasNontrivialBlending : 1;
struct LayerInfo {
float fInPoint,
fOutPoint;
SkSize fSize;
const float fInPoint,
fOutPoint;
};
struct AssetInfo {

View File

@ -724,7 +724,7 @@ sk_sp<sksg::RenderNode> AnimationBuilder::attachShape(const skjson::ArrayValue*
}
sk_sp<sksg::RenderNode> AnimationBuilder::attachShapeLayer(const skjson::ObjectValue& layer,
const LayerInfo&,
LayerInfo*,
AnimatorScope* ascope) const {
std::vector<sk_sp<sksg::GeometryNode>> geometryStack;
std::vector<GeometryEffectRec> geometryEffectStack;

View File

@ -14,8 +14,10 @@
namespace skottie {
namespace internal {
EffectBuilder::EffectBuilder(const AnimationBuilder* abuilder, AnimatorScope* ascope)
EffectBuilder::EffectBuilder(const AnimationBuilder* abuilder, const SkSize& layer_size,
AnimatorScope* ascope)
: fBuilder(abuilder)
, fLayerSize(layer_size)
, fScope(ascope) {}
EffectBuilder::EffectBuilderT EffectBuilder::findBuilder(const skjson::ObjectValue& jeffect) const {
@ -50,6 +52,7 @@ EffectBuilder::EffectBuilderT EffectBuilder::findBuilder(const skjson::ObjectVal
static constexpr char kGradientEffectMN[] = "ADBE Ramp",
kLevelsEffectMN[] = "ADBE Easy Levels2",
kMotionTileEffectMN[] = "ADBE Tile",
kTransformEffectMN[] = "ADBE Geometry2";
if (const skjson::StringValue* mn = jeffect["mn"]) {
@ -59,6 +62,9 @@ EffectBuilder::EffectBuilderT EffectBuilder::findBuilder(const skjson::ObjectVal
if (!strcmp(mn->begin(), kLevelsEffectMN)) {
return &EffectBuilder::attachLevelsEffect;
}
if (!strcmp(mn->begin(), kMotionTileEffectMN)) {
return &EffectBuilder::attachMotionTileEffect;
}
if (!strcmp(mn->begin(), kTransformEffectMN)) {
return &EffectBuilder::attachTransformEffect;
}

View File

@ -15,7 +15,7 @@ namespace internal {
class EffectBuilder final : public SkNoncopyable {
public:
EffectBuilder(const AnimationBuilder*, AnimatorScope*);
EffectBuilder(const AnimationBuilder*, const SkSize&, AnimatorScope*);
sk_sp<sksg::RenderNode> attachEffects(const skjson::ArrayValue&,
sk_sp<sksg::RenderNode>) const;
@ -24,28 +24,31 @@ private:
using EffectBuilderT = sk_sp<sksg::RenderNode>(EffectBuilder::*)(const skjson::ArrayValue&,
sk_sp<sksg::RenderNode>) const;
sk_sp<sksg::RenderNode> attachTintEffect (const skjson::ArrayValue&,
sk_sp<sksg::RenderNode> attachDropShadowEffect (const skjson::ArrayValue&,
sk_sp<sksg::RenderNode>) const;
sk_sp<sksg::RenderNode> attachFillEffect (const skjson::ArrayValue&,
sk_sp<sksg::RenderNode>) const;
sk_sp<sksg::RenderNode> attachTritoneEffect (const skjson::ArrayValue&,
sk_sp<sksg::RenderNode>) const;
sk_sp<sksg::RenderNode> attachDropShadowEffect (const skjson::ArrayValue&,
sk_sp<sksg::RenderNode>) const;
sk_sp<sksg::RenderNode> attachGaussianBlurEffect(const skjson::ArrayValue&,
sk_sp<sksg::RenderNode>) const;
sk_sp<sksg::RenderNode> attachGradientEffect (const skjson::ArrayValue&,
sk_sp<sksg::RenderNode>) const;
sk_sp<sksg::RenderNode> attachLevelsEffect (const skjson::ArrayValue&,
sk_sp<sksg::RenderNode>) const;
sk_sp<sksg::RenderNode> attachMotionTileEffect (const skjson::ArrayValue&,
sk_sp<sksg::RenderNode>) const;
sk_sp<sksg::RenderNode> attachTintEffect (const skjson::ArrayValue&,
sk_sp<sksg::RenderNode>) const;
sk_sp<sksg::RenderNode> attachTransformEffect (const skjson::ArrayValue&,
sk_sp<sksg::RenderNode>) const;
sk_sp<sksg::RenderNode> attachTritoneEffect (const skjson::ArrayValue&,
sk_sp<sksg::RenderNode>) const;
EffectBuilderT findBuilder(const skjson::ObjectValue&) const;
static const skjson::Value& GetPropValue(const skjson::ArrayValue& jprops, size_t prop_index);
const AnimationBuilder* fBuilder;
const SkSize fLayerSize;
AnimatorScope* fScope;
};

View File

@ -0,0 +1,174 @@
/*
* 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/effects/Effects.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkPictureRecorder.h"
#include "modules/skottie/src/SkottieValue.h"
#include "modules/sksg/include/SkSGRenderNode.h"
#include "src/utils/SkJSON.h"
namespace skottie {
namespace internal {
namespace {
// AE motion tile effect semantics
// (https://helpx.adobe.com/after-effects/using/stylize-effects.html#motion_tile_effect):
//
// - the full content of the layer is mapped to a tile: tile_center, tile_width, tile_height
//
// - tiles are repeated in both dimensions to fill the output area: output_width, output_height
//
// - tiling mode is either kRepeat (default) or kMirror (when mirror_edges == true)
//
// - for a non-zero phase, alternating vertical columns (every other column) are offset by
// the specified amount
//
// - when horizontal_phase is true, the phase is applied to horizontal rows instead of columns
//
class TileRenderNode final : public sksg::CustomRenderNode {
public:
TileRenderNode(const SkSize& size, sk_sp<sksg::RenderNode> layer)
: INHERITED({std::move(layer)})
, fLayerSize(size) {}
SG_ATTRIBUTE(TileCenter , SkPoint , fTileCenter )
SG_ATTRIBUTE(TileWidth , SkScalar, fTileW )
SG_ATTRIBUTE(TileHeight , SkScalar, fTileH )
SG_ATTRIBUTE(OutputWidth , SkScalar, fOutputW )
SG_ATTRIBUTE(OutputHeight , SkScalar, fOutputH )
SG_ATTRIBUTE(Phase , SkScalar, fPhase )
SG_ATTRIBUTE(MirrorEdges , bool , fMirrorEdges )
SG_ATTRIBUTE(HorizontalPhase, bool , fHorizontalPhase)
protected:
const RenderNode* onNodeAt(const SkPoint&) const override { return nullptr; } // no hit-testing
SkRect onRevalidate(sksg::InvalidationController* ic, const SkMatrix& ctm) override {
SkASSERT(this->children().size() == 1ul);
this->children()[0]->revalidate(ic, ctm);
// outputW and outputH are layer size percentage units.
const auto outputW = fOutputW * 0.01f * fLayerSize.width(),
outputH = fOutputH * 0.01f * fLayerSize.height();
return SkRect::MakeXYWH((fLayerSize.width() - outputW) * 0.5f,
(fLayerSize.height() - outputH) * 0.5f,
outputW, outputH);
}
void onRender(SkCanvas* canvas, const RenderContext* ctx) const override {
// tileW and tileH are also layer size percentage units
const auto tileW = SkTPin(fTileW, 0.0f, 100.0f) * 0.01f * fLayerSize.width(),
tileH = SkTPin(fTileH, 0.0f, 100.0f) * 0.01f * fLayerSize.height();
// AE allow one of the tile dimensions to collapse, but not both.
if (this->bounds().isEmpty() || (!tileW && !tileH)) {
return;
}
const auto tile_size = SkSize::Make(std::max(tileW, 1.0f),
std::max(tileH, 1.0f));
const auto tile = SkRect::MakeXYWH(fTileCenter.fX - 0.5f * tile_size.width(),
fTileCenter.fY - 0.5f * tile_size.height(),
tile_size.width(),
tile_size.height());
SkASSERT(this->children().size() == 1ul);
const auto& layer = this->children()[0];
const auto layer_bounds = SkRect::MakeWH(fLayerSize.width(), fLayerSize.height());
// TODO: phase
SkPictureRecorder recorder;
layer->render(recorder.beginRecording(layer_bounds));
const auto layer_pic = recorder.finishRecordingAsPicture();
const auto shader_matrix = SkMatrix::MakeRectToRect(layer_bounds, tile,
SkMatrix::kFill_ScaleToFit);
const auto tm = fMirrorEdges ? SkTileMode::kMirror : SkTileMode::kRepeat;
SkPaint paint;
paint.setAntiAlias(true);
paint.setShader(layer_pic->makeShader(tm, tm, &shader_matrix));
canvas->drawRect(this->bounds(), paint);
}
private:
const SkSize fLayerSize;
SkPoint fTileCenter = { 0, 0 };
SkScalar fTileW = 1,
fTileH = 1,
fOutputW = 1,
fOutputH = 1,
fPhase = 0;
bool fMirrorEdges = false;
bool fHorizontalPhase = false;
using INHERITED = sksg::CustomRenderNode;
};
} // anonymous ns
sk_sp<sksg::RenderNode> EffectBuilder::attachMotionTileEffect(const skjson::ArrayValue& jprops,
sk_sp<sksg::RenderNode> layer) const {
enum : size_t {
kTileCenter_Index = 0,
kTileWidth_Index = 1,
kTileHeight_Index = 2,
kOutputWidth_Index = 3,
kOutputHeight_Index = 4,
kMirrorEdges_Index = 5,
kPhase_Index = 6,
kHorizontalPhaseShift_Index = 7,
};
auto tiler = sk_make_sp<TileRenderNode>(fLayerSize, std::move(layer));
fBuilder->bindProperty<VectorValue>(GetPropValue(jprops, kTileCenter_Index), fScope,
[tiler](const VectorValue& tc) {
tiler->setTileCenter(ValueTraits<VectorValue>::As<SkPoint>(tc));
});
fBuilder->bindProperty<ScalarValue>(GetPropValue(jprops, kTileWidth_Index), fScope,
[tiler](const ScalarValue& tw) {
tiler->setTileWidth(tw);
});
fBuilder->bindProperty<ScalarValue>(GetPropValue(jprops, kTileHeight_Index), fScope,
[tiler](const ScalarValue& th) {
tiler->setTileHeight(th);
});
fBuilder->bindProperty<ScalarValue>(GetPropValue(jprops, kOutputWidth_Index), fScope,
[tiler](const ScalarValue& ow) {
tiler->setOutputWidth(ow);
});
fBuilder->bindProperty<ScalarValue>(GetPropValue(jprops, kOutputHeight_Index), fScope,
[tiler](const ScalarValue& oh) {
tiler->setOutputHeight(oh);
});
fBuilder->bindProperty<ScalarValue>(GetPropValue(jprops, kMirrorEdges_Index), fScope,
[tiler](const ScalarValue& me) {
tiler->setMirrorEdges(SkScalarRoundToInt(me));
});
fBuilder->bindProperty<ScalarValue>(GetPropValue(jprops, kPhase_Index), fScope,
[tiler](const ScalarValue& ph) {
tiler->setPhase(ph);
});
fBuilder->bindProperty<ScalarValue>(GetPropValue(jprops, kHorizontalPhaseShift_Index), fScope,
[tiler](const ScalarValue& hp) {
tiler->setHorizontalPhase(SkScalarRoundToInt(hp));
});
return std::move(tiler);
}
} // namespace internal
} // namespace skottie

View File

@ -254,7 +254,7 @@ sk_sp<SkTypeface> AnimationBuilder::findFont(const SkString& font_name) const {
}
sk_sp<sksg::RenderNode> AnimationBuilder::attachTextLayer(const skjson::ObjectValue& layer,
const LayerInfo&,
LayerInfo*,
AnimatorScope* ascope) const {
// General text node format:
// "t": {

View File

@ -113,6 +113,24 @@ private:
typedef Node INHERITED;
};
/**
* Clients outside SkSG looking to implement custom render nodes,
* should derive from this class instead of RenderNode. It handles
* various book-keeping, and provides a controlled extension point.
*/
class CustomRenderNode : public RenderNode {
protected:
explicit CustomRenderNode(std::vector<sk_sp<RenderNode>>&& children);
~CustomRenderNode() override;
const std::vector<sk_sp<RenderNode>>& children() const { return fChildren; }
private:
std::vector<sk_sp<RenderNode>> fChildren;
using INHERITED = RenderNode;
};
} // namespace sksg
#endif // SkSGRenderNode_DEFINED

View File

@ -139,4 +139,18 @@ RenderNode::ScopedRenderContext::setFilterIsolation(const SkRect& bounds, const
return std::move(*this);
}
CustomRenderNode::CustomRenderNode(std::vector<sk_sp<RenderNode>>&& children)
: INHERITED(kOverrideDamage_Trait) // We cannot make any assumptions - override conservatively.
, fChildren(std::move(children)) {
for (const auto& child : fChildren) {
this->observeInval(child);
}
}
CustomRenderNode::~CustomRenderNode() {
for (const auto& child : fChildren) {
this->unobserveInval(child);
}
}
} // namespace sksg

File diff suppressed because one or more lines are too long