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/effects/Effects.h"
9
10 #include "include/core/SkCanvas.h"
11 #include "include/core/SkPictureRecorder.h"
12 #include "include/core/SkShader.h"
13 #include "include/effects/SkGradientShader.h"
14 #include "modules/skottie/src/SkottieValue.h"
15 #include "modules/sksg/include/SkSGRenderNode.h"
16 #include "src/utils/SkJSON.h"
17
18 #include <cmath>
19
20 namespace skottie {
21 namespace internal {
22
23 namespace {
24
25 // AE motion tile effect semantics
26 // (https://helpx.adobe.com/after-effects/using/stylize-effects.html#motion_tile_effect):
27 //
28 // - the full content of the layer is mapped to a tile: tile_center, tile_width, tile_height
29 //
30 // - tiles are repeated in both dimensions to fill the output area: output_width, output_height
31 //
32 // - tiling mode is either kRepeat (default) or kMirror (when mirror_edges == true)
33 //
34 // - for a non-zero phase, alternating vertical columns (every other column) are offset by
35 // the specified amount
36 //
37 // - when horizontal_phase is true, the phase is applied to horizontal rows instead of columns
38 //
39 class TileRenderNode final : public sksg::CustomRenderNode {
40 public:
TileRenderNode(const SkSize & size,sk_sp<sksg::RenderNode> layer)41 TileRenderNode(const SkSize& size, sk_sp<sksg::RenderNode> layer)
42 : INHERITED({std::move(layer)})
43 , fLayerSize(size) {}
44
45 SG_ATTRIBUTE(TileCenter , SkPoint , fTileCenter )
46 SG_ATTRIBUTE(TileWidth , SkScalar, fTileW )
47 SG_ATTRIBUTE(TileHeight , SkScalar, fTileH )
48 SG_ATTRIBUTE(OutputWidth , SkScalar, fOutputW )
49 SG_ATTRIBUTE(OutputHeight , SkScalar, fOutputH )
50 SG_ATTRIBUTE(Phase , SkScalar, fPhase )
51 SG_ATTRIBUTE(MirrorEdges , bool , fMirrorEdges )
52 SG_ATTRIBUTE(HorizontalPhase, bool , fHorizontalPhase)
53
54 protected:
onNodeAt(const SkPoint &) const55 const RenderNode* onNodeAt(const SkPoint&) const override { return nullptr; } // no hit-testing
56
onRevalidate(sksg::InvalidationController * ic,const SkMatrix & ctm)57 SkRect onRevalidate(sksg::InvalidationController* ic, const SkMatrix& ctm) override {
58 // Re-record the layer picture if needed.
59 if (!fLayerPicture || this->hasChildrenInval()) {
60 SkASSERT(this->children().size() == 1ul);
61 const auto& layer = this->children()[0];
62
63 layer->revalidate(ic, ctm);
64
65 SkPictureRecorder recorder;
66 layer->render(recorder.beginRecording(fLayerSize.width(), fLayerSize.height()));
67 fLayerPicture = recorder.finishRecordingAsPicture();
68 }
69
70 // tileW and tileH use layer size percentage units.
71 const auto tileW = SkTPin(fTileW, 0.0f, 100.0f) * 0.01f * fLayerSize.width(),
72 tileH = SkTPin(fTileH, 0.0f, 100.0f) * 0.01f * fLayerSize.height();
73 const auto tile_size = SkSize::Make(std::max(tileW, 1.0f),
74 std::max(tileH, 1.0f));
75 const auto tile = SkRect::MakeXYWH(fTileCenter.fX - 0.5f * tile_size.width(),
76 fTileCenter.fY - 0.5f * tile_size.height(),
77 tile_size.width(),
78 tile_size.height());
79
80 const auto layerShaderMatrix = SkMatrix::MakeRectToRect(
81 SkRect::MakeWH(fLayerSize.width(), fLayerSize.height()),
82 tile, SkMatrix::kFill_ScaleToFit);
83
84 const auto tm = fMirrorEdges ? SkTileMode::kMirror : SkTileMode::kRepeat;
85 auto layer_shader = fLayerPicture->makeShader(tm, tm, &layerShaderMatrix);
86
87 if (fPhase) {
88 // To implement AE phase semantics, we construct a mask shader for the pass-through
89 // rows/columns. We then draw the layer content through this mask, and then again
90 // through the inverse mask with a phase shift.
91 const auto phase_vec = fHorizontalPhase
92 ? SkVector::Make(tile.width(), 0)
93 : SkVector::Make(0, tile.height());
94 const auto phase_shift = SkVector::Make(phase_vec.fX / layerShaderMatrix.getScaleX(),
95 phase_vec.fY / layerShaderMatrix.getScaleY())
96 * std::fmod(fPhase * (1/360.0f), 1);
97 const auto phase_shader_matrix = SkMatrix::MakeTrans(phase_shift.x(), phase_shift.y());
98
99 // The mask is generated using a step gradient shader, spanning 2 x tile width/height,
100 // and perpendicular to the phase vector.
101 static constexpr SkColor colors[] = { 0xffffffff, 0x00000000 };
102 static constexpr SkScalar pos[] = { 0.5f, 0.5f };
103
104 const SkPoint pts[] = {{ tile.x(), tile.y() },
105 { tile.x() + 2 * (tile.width() - phase_vec.fX),
106 tile.y() + 2 * (tile.height() - phase_vec.fY) }};
107
108 auto mask_shader = SkGradientShader::MakeLinear(pts, colors, pos,
109 SK_ARRAY_COUNT(colors),
110 SkTileMode::kRepeat);
111
112 // First drawing pass: in-place masked layer content.
113 fMainPassShader = SkShaders::Blend(SkBlendMode::kSrcIn , mask_shader, layer_shader);
114 // Second pass: phased-shifted layer content, with an inverse mask.
115 fPhasePassShader = SkShaders::Blend(SkBlendMode::kSrcOut, mask_shader, layer_shader,
116 &phase_shader_matrix);
117 } else {
118 fMainPassShader = std::move(layer_shader);
119 fPhasePassShader = nullptr;
120 }
121
122 // outputW and outputH also use layer size percentage units.
123 const auto outputW = fOutputW * 0.01f * fLayerSize.width(),
124 outputH = fOutputH * 0.01f * fLayerSize.height();
125
126 return SkRect::MakeXYWH((fLayerSize.width() - outputW) * 0.5f,
127 (fLayerSize.height() - outputH) * 0.5f,
128 outputW, outputH);
129 }
130
onRender(SkCanvas * canvas,const RenderContext * ctx) const131 void onRender(SkCanvas* canvas, const RenderContext* ctx) const override {
132 // AE allow one of the tile dimensions to collapse, but not both.
133 if (this->bounds().isEmpty() || (fTileW <= 0 && fTileH <= 0)) {
134 return;
135 }
136
137 SkPaint paint;
138 paint.setAntiAlias(true);
139
140 paint.setShader(fMainPassShader);
141 canvas->drawRect(this->bounds(), paint);
142
143 if (fPhasePassShader) {
144 paint.setShader(fPhasePassShader);
145 canvas->drawRect(this->bounds(), paint);
146 }
147 }
148
149 private:
150 const SkSize fLayerSize;
151
152 SkPoint fTileCenter = { 0, 0 };
153 SkScalar fTileW = 1,
154 fTileH = 1,
155 fOutputW = 1,
156 fOutputH = 1,
157 fPhase = 0;
158 bool fMirrorEdges = false;
159 bool fHorizontalPhase = false;
160
161 // These are computed/cached on revalidation.
162 sk_sp<SkPicture> fLayerPicture; // cached picture for layer content
163 sk_sp<SkShader> fMainPassShader, // shader for the main tile(s)
164 fPhasePassShader; // shader for the phased tile(s)
165
166 using INHERITED = sksg::CustomRenderNode;
167 };
168
169 } // anonymous ns
170
attachMotionTileEffect(const skjson::ArrayValue & jprops,sk_sp<sksg::RenderNode> layer) const171 sk_sp<sksg::RenderNode> EffectBuilder::attachMotionTileEffect(const skjson::ArrayValue& jprops,
172 sk_sp<sksg::RenderNode> layer) const {
173 enum : size_t {
174 kTileCenter_Index = 0,
175 kTileWidth_Index = 1,
176 kTileHeight_Index = 2,
177 kOutputWidth_Index = 3,
178 kOutputHeight_Index = 4,
179 kMirrorEdges_Index = 5,
180 kPhase_Index = 6,
181 kHorizontalPhaseShift_Index = 7,
182 };
183
184 auto tiler = sk_make_sp<TileRenderNode>(fLayerSize, std::move(layer));
185
186 fBuilder->bindProperty<VectorValue>(GetPropValue(jprops, kTileCenter_Index),
187 [tiler](const VectorValue& tc) {
188 tiler->setTileCenter(ValueTraits<VectorValue>::As<SkPoint>(tc));
189 });
190 fBuilder->bindProperty<ScalarValue>(GetPropValue(jprops, kTileWidth_Index),
191 [tiler](const ScalarValue& tw) {
192 tiler->setTileWidth(tw);
193 });
194 fBuilder->bindProperty<ScalarValue>(GetPropValue(jprops, kTileHeight_Index),
195 [tiler](const ScalarValue& th) {
196 tiler->setTileHeight(th);
197 });
198 fBuilder->bindProperty<ScalarValue>(GetPropValue(jprops, kOutputWidth_Index),
199 [tiler](const ScalarValue& ow) {
200 tiler->setOutputWidth(ow);
201 });
202 fBuilder->bindProperty<ScalarValue>(GetPropValue(jprops, kOutputHeight_Index),
203 [tiler](const ScalarValue& oh) {
204 tiler->setOutputHeight(oh);
205 });
206 fBuilder->bindProperty<ScalarValue>(GetPropValue(jprops, kMirrorEdges_Index),
207 [tiler](const ScalarValue& me) {
208 tiler->setMirrorEdges(SkScalarRoundToInt(me));
209 });
210 fBuilder->bindProperty<ScalarValue>(GetPropValue(jprops, kPhase_Index),
211 [tiler](const ScalarValue& ph) {
212 tiler->setPhase(ph);
213 });
214 fBuilder->bindProperty<ScalarValue>(GetPropValue(jprops, kHorizontalPhaseShift_Index),
215 [tiler](const ScalarValue& hp) {
216 tiler->setHorizontalPhase(SkScalarRoundToInt(hp));
217 });
218
219 return tiler;
220 }
221
222 } // namespace internal
223 } // namespace skottie
224