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