1/* 2 * Copyright 2020 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 "include/core/SkM44.h" 9#include "modules/skottie/src/Adapter.h" 10#include "modules/skottie/src/SkottieJson.h" 11#include "modules/skottie/src/SkottiePriv.h" 12#include "modules/skottie/src/SkottieValue.h" 13#include "modules/skottie/src/layers/shapelayer/ShapeLayer.h" 14#include "modules/sksg/include/SkSGGeometryEffect.h" 15#include "src/core/SkGeometry.h" 16 17#include <vector> 18 19namespace skottie::internal { 20 21namespace { 22 23static SkPoint lerp(const SkPoint& p0, const SkPoint& p1, SkScalar t) { 24 return p0 + (p1 - p0) * t; 25} 26 27// Operates on the cubic representation of a shape. Pulls vertices towards the shape center, 28// and cubic control points away from the center. The general shape center is the vertex average. 29class PuckerBloatEffect final : public sksg::GeometryEffect { 30public: 31 explicit PuckerBloatEffect(sk_sp<sksg::GeometryNode> geo) : INHERITED({std::move(geo)}) {} 32 33 // Fraction of the transition to center. I.e. 34 // 35 // 0 -> no effect 36 // 1 -> vertices collapsed to center 37 // 38 // Negative values are allowed (inverse direction), as are extranormal values. 39 SG_ATTRIBUTE(Amount, float, fAmount) 40 41private: 42 SkPath onRevalidateEffect(const sk_sp<GeometryNode>& geo) override { 43 struct CubicInfo { 44 SkPoint ctrl0, ctrl1, pt; // corresponding to SkPath::cubicTo() params, respectively. 45 }; 46 47 const auto input = geo->asPath(); 48 if (SkScalarNearlyZero(fAmount)) { 49 return input; 50 } 51 52 const auto input_bounds = input.computeTightBounds(); 53 const SkPoint center{input_bounds.centerX(), input_bounds.centerY()}; 54 55 SkPath path; 56 57 SkPoint contour_start = {0, 0}; 58 std::vector<CubicInfo> cubics; 59 60 auto commit_contour = [&]() { 61 path.moveTo(lerp(contour_start, center, fAmount)); 62 for (const auto& c : cubics) { 63 path.cubicTo(lerp(c.ctrl0, center, -fAmount), 64 lerp(c.ctrl1, center, -fAmount), 65 lerp(c.pt , center, fAmount)); 66 } 67 path.close(); 68 69 cubics.clear(); 70 }; 71 72 // Normalize all verbs to cubic representation. 73 SkPoint pts[4]; 74 SkPath::Verb verb; 75 SkPath::Iter iter(input, true); 76 while ((verb = iter.next(pts)) != SkPath::kDone_Verb) { 77 switch (verb) { 78 case SkPath::kMove_Verb: 79 commit_contour(); 80 contour_start = pts[0]; 81 break; 82 case SkPath::kLine_Verb: { 83 // Empirically, straight lines are treated as cubics with control points 84 // located length/100 away from extremities. 85 static constexpr float kCtrlPosFraction = 1.f / 100; 86 const auto line_start = pts[0], 87 line_end = pts[1]; 88 cubics.push_back({ 89 lerp(line_start, line_end, kCtrlPosFraction), 90 lerp(line_start, line_end, 1 - kCtrlPosFraction), 91 line_end 92 }); 93 } break; 94 case SkPath::kQuad_Verb: 95 SkConvertQuadToCubic(pts, pts); 96 cubics.push_back({pts[1], pts[2], pts[3]}); 97 break; 98 case SkPath::kConic_Verb: { 99 // We should only ever encounter conics from circles/ellipses. 100 SkASSERT(SkScalarNearlyEqual(iter.conicWeight(), SK_ScalarRoot2Over2)); 101 102 // http://spencermortensen.com/articles/bezier-circle/ 103 static constexpr float kCubicCircleCoeff = 1 - 0.551915024494f; 104 105 const auto conic_start = cubics.empty() ? contour_start 106 : cubics.back().pt, 107 conic_end = pts[2]; 108 109 cubics.push_back({ 110 lerp(pts[1], conic_start, kCubicCircleCoeff), 111 lerp(pts[1], conic_end , kCubicCircleCoeff), 112 conic_end 113 }); 114 } break; 115 case SkPath::kCubic_Verb: 116 cubics.push_back({pts[1], pts[2], pts[3]}); 117 break; 118 case SkPath::kClose_Verb: 119 commit_contour(); 120 break; 121 default: 122 break; 123 } 124 } 125 126 return path; 127 } 128 129 float fAmount = 0; 130 131 using INHERITED = sksg::GeometryEffect; 132}; 133 134class PuckerBloatAdapter final : public DiscardableAdapterBase<PuckerBloatAdapter, 135 PuckerBloatEffect> { 136public: 137 PuckerBloatAdapter(const skjson::ObjectValue& joffset, 138 const AnimationBuilder& abuilder, 139 sk_sp<sksg::GeometryNode> child) 140 : INHERITED(sk_make_sp<PuckerBloatEffect>(std::move(child))) { 141 this->bind(abuilder, joffset["a" ], fAmount); 142 } 143 144private: 145 void onSync() override { 146 // AE amount is percentage-based. 147 this->node()->setAmount(fAmount / 100); 148 } 149 150 ScalarValue fAmount = 0; 151 152 using INHERITED = DiscardableAdapterBase<PuckerBloatAdapter, PuckerBloatEffect>; 153}; 154 155} // namespace 156 157std::vector<sk_sp<sksg::GeometryNode>> ShapeBuilder::AttachPuckerBloatGeometryEffect( 158 const skjson::ObjectValue& jround, const AnimationBuilder* abuilder, 159 std::vector<sk_sp<sksg::GeometryNode>>&& geos) { 160 std::vector<sk_sp<sksg::GeometryNode>> bloated; 161 bloated.reserve(geos.size()); 162 163 for (auto& g : geos) { 164 bloated.push_back(abuilder->attachDiscardableAdapter<PuckerBloatAdapter> 165 (jround, *abuilder, std::move(g))); 166 } 167 168 return bloated; 169} 170 171} // namespace skottie::internal 172