Florin Malita | b97824d | 2019-06-17 11:37:02 -0400 | [diff] [blame] | 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" |
Florin Malita | 60e60df | 2019-06-17 14:18:11 -0400 | [diff] [blame] | 12 | #include "include/core/SkShader.h" |
| 13 | #include "include/effects/SkGradientShader.h" |
Florin Malita | cc982ec | 2020-01-30 10:05:02 -0500 | [diff] [blame] | 14 | #include "modules/skottie/src/Adapter.h" |
Florin Malita | b97824d | 2019-06-17 11:37:02 -0400 | [diff] [blame] | 15 | #include "modules/skottie/src/SkottieValue.h" |
| 16 | #include "modules/sksg/include/SkSGRenderNode.h" |
| 17 | #include "src/utils/SkJSON.h" |
| 18 | |
Florin Malita | 60e60df | 2019-06-17 14:18:11 -0400 | [diff] [blame] | 19 | #include <cmath> |
| 20 | |
Florin Malita | b97824d | 2019-06-17 11:37:02 -0400 | [diff] [blame] | 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 { |
Florin Malita | ccacfa0 | 2019-07-10 13:38:48 -0400 | [diff] [blame] | 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 | } |
Florin Malita | b97824d | 2019-06-17 11:37:02 -0400 | [diff] [blame] | 70 | |
Florin Malita | 60e60df | 2019-06-17 14:18:11 -0400 | [diff] [blame] | 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 | |
Florin Malita | ccacfa0 | 2019-07-10 13:38:48 -0400 | [diff] [blame] | 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); |
Florin Malita | 60e60df | 2019-06-17 14:18:11 -0400 | [diff] [blame] | 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()); |
Florin Malita | ccacfa0 | 2019-07-10 13:38:48 -0400 | [diff] [blame] | 95 | const auto phase_shift = SkVector::Make(phase_vec.fX / layerShaderMatrix.getScaleX(), |
| 96 | phase_vec.fY / layerShaderMatrix.getScaleY()) |
Florin Malita | 60e60df | 2019-06-17 14:18:11 -0400 | [diff] [blame] | 97 | * std::fmod(fPhase * (1/360.0f), 1); |
Mike Reed | 1f60733 | 2020-05-21 12:11:27 -0400 | [diff] [blame^] | 98 | const auto phase_shader_matrix = SkMatrix::Translate(phase_shift.x(), phase_shift.y()); |
Florin Malita | 60e60df | 2019-06-17 14:18:11 -0400 | [diff] [blame] | 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 | |
Florin Malita | ccacfa0 | 2019-07-10 13:38:48 -0400 | [diff] [blame] | 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. |
Kevin Lubick | 988ce04 | 2020-03-25 13:13:20 -0400 | [diff] [blame] | 116 | fPhasePassShader = SkShaders::Blend(SkBlendMode::kSrcOut, mask_shader, layer_shader) |
| 117 | ->makeWithLocalMatrix(phase_shader_matrix); |
Florin Malita | 60e60df | 2019-06-17 14:18:11 -0400 | [diff] [blame] | 118 | } else { |
Florin Malita | ccacfa0 | 2019-07-10 13:38:48 -0400 | [diff] [blame] | 119 | fMainPassShader = std::move(layer_shader); |
| 120 | fPhasePassShader = nullptr; |
Florin Malita | 60e60df | 2019-06-17 14:18:11 -0400 | [diff] [blame] | 121 | } |
| 122 | |
| 123 | // outputW and outputH also use layer size percentage units. |
Florin Malita | b97824d | 2019-06-17 11:37:02 -0400 | [diff] [blame] | 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 { |
Florin Malita | b97824d | 2019-06-17 11:37:02 -0400 | [diff] [blame] | 133 | // AE allow one of the tile dimensions to collapse, but not both. |
Florin Malita | 60e60df | 2019-06-17 14:18:11 -0400 | [diff] [blame] | 134 | if (this->bounds().isEmpty() || (fTileW <= 0 && fTileH <= 0)) { |
Florin Malita | b97824d | 2019-06-17 11:37:02 -0400 | [diff] [blame] | 135 | return; |
| 136 | } |
| 137 | |
Florin Malita | b97824d | 2019-06-17 11:37:02 -0400 | [diff] [blame] | 138 | SkPaint paint; |
| 139 | paint.setAntiAlias(true); |
Florin Malita | b97824d | 2019-06-17 11:37:02 -0400 | [diff] [blame] | 140 | |
Florin Malita | ccacfa0 | 2019-07-10 13:38:48 -0400 | [diff] [blame] | 141 | paint.setShader(fMainPassShader); |
Florin Malita | b97824d | 2019-06-17 11:37:02 -0400 | [diff] [blame] | 142 | canvas->drawRect(this->bounds(), paint); |
Florin Malita | 60e60df | 2019-06-17 14:18:11 -0400 | [diff] [blame] | 143 | |
Florin Malita | ccacfa0 | 2019-07-10 13:38:48 -0400 | [diff] [blame] | 144 | if (fPhasePassShader) { |
| 145 | paint.setShader(fPhasePassShader); |
Florin Malita | 60e60df | 2019-06-17 14:18:11 -0400 | [diff] [blame] | 146 | canvas->drawRect(this->bounds(), paint); |
| 147 | } |
Florin Malita | b97824d | 2019-06-17 11:37:02 -0400 | [diff] [blame] | 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 | |
Florin Malita | 60e60df | 2019-06-17 14:18:11 -0400 | [diff] [blame] | 162 | // These are computed/cached on revalidation. |
Florin Malita | ccacfa0 | 2019-07-10 13:38:48 -0400 | [diff] [blame] | 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) |
Florin Malita | 60e60df | 2019-06-17 14:18:11 -0400 | [diff] [blame] | 166 | |
Florin Malita | b97824d | 2019-06-17 11:37:02 -0400 | [diff] [blame] | 167 | using INHERITED = sksg::CustomRenderNode; |
| 168 | }; |
| 169 | |
Florin Malita | cc982ec | 2020-01-30 10:05:02 -0500 | [diff] [blame] | 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 | |
Florin Malita | c02e77c | 2020-03-12 09:34:41 -0400 | [diff] [blame] | 204 | tiler->setTileCenter({fTileCenter.x, fTileCenter.y}); |
Florin Malita | cc982ec | 2020-01-30 10:05:02 -0500 | [diff] [blame] | 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 | |
Florin Malita | c02e77c | 2020-03-12 09:34:41 -0400 | [diff] [blame] | 214 | Vec2Value fTileCenter = {0,0}; |
Florin Malita | cc982ec | 2020-01-30 10:05:02 -0500 | [diff] [blame] | 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 | |
Florin Malita | b97824d | 2019-06-17 11:37:02 -0400 | [diff] [blame] | 226 | } // anonymous ns |
| 227 | |
| 228 | sk_sp<sksg::RenderNode> EffectBuilder::attachMotionTileEffect(const skjson::ArrayValue& jprops, |
| 229 | sk_sp<sksg::RenderNode> layer) const { |
Florin Malita | cc982ec | 2020-01-30 10:05:02 -0500 | [diff] [blame] | 230 | return fBuilder->attachDiscardableAdapter<MotionTileAdapter>(jprops, |
| 231 | std::move(layer), |
| 232 | *fBuilder, |
| 233 | fLayerSize); |
Florin Malita | b97824d | 2019-06-17 11:37:02 -0400 | [diff] [blame] | 234 | } |
| 235 | |
| 236 | } // namespace internal |
| 237 | } // namespace skottie |