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