[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>
diff --git a/modules/skottie/src/effects/MotionTileEffect.cpp b/modules/skottie/src/effects/MotionTileEffect.cpp
new file mode 100644
index 0000000..7130ebc
--- /dev/null
+++ b/modules/skottie/src/effects/MotionTileEffect.cpp
@@ -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