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