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