| 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 |  |