xref: /aosp_15_r20/external/skia/modules/skottie/src/animator/ShapeKeyframeAnimator.cpp (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
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/SkPath.h"
9 #include "include/core/SkPathBuilder.h"
10 #include "include/core/SkPoint.h"
11 #include "include/private/base/SkAssert.h"
12 #include "include/private/base/SkTo.h"
13 #include "modules/skottie/src/SkottieJson.h"
14 #include "modules/skottie/src/SkottieValue.h"
15 #include "modules/skottie/src/animator/Animator.h"
16 #include "modules/skottie/src/animator/VectorKeyframeAnimator.h"
17 #include "src/utils/SkJSON.h"
18 
19 #include <cstddef>
20 #include <vector>
21 
22 namespace skottie::internal {
23 class AnimationBuilder;
24 }
25 
26 namespace skottie {
27 
28 // Shapes (paths) are encoded as a vector of floats.  For each vertex, we store 6 floats:
29 //
30 //   - vertex point      (2 floats)
31 //   - in-tangent point  (2 floats)
32 //   - out-tangent point (2 floats)
33 //
34 // Additionally, we store one trailing "closed shape" flag - e.g.
35 //
36 //  [ v0.x, v0.y, v0_in.x, v0_in.y, v0_out.x, v0_out.y, ... , closed_flag ]
37 //
38 enum ShapeEncodingInfo : size_t {
39             kX_Index = 0,
40             kY_Index = 1,
41           kInX_Index = 2,
42           kInY_Index = 3,
43          kOutX_Index = 4,
44          kOutY_Index = 5,
45 
46     kFloatsPerVertex = 6
47 };
48 
shape_encoding_len(size_t vertex_count)49 static size_t shape_encoding_len(size_t vertex_count) {
50     return vertex_count * kFloatsPerVertex + 1;
51 }
52 
53 // Some versions wrap shape values as single-element arrays.
shape_root(const skjson::Value & jv)54 static const skjson::ObjectValue* shape_root(const skjson::Value& jv) {
55     if (const skjson::ArrayValue* av = jv) {
56         if (av->size() == 1) {
57             return (*av)[0];
58         }
59     }
60 
61     return jv;
62 }
63 
parse_encoding_len(const skjson::Value & jv,size_t * len)64 static bool parse_encoding_len(const skjson::Value& jv, size_t* len) {
65     if (const auto* jshape = shape_root(jv)) {
66         if (const skjson::ArrayValue* jvs = (*jshape)["v"]) {
67             *len = shape_encoding_len(jvs->size());
68             return true;
69         }
70     }
71     return false;
72 }
73 
parse_encoding_data(const skjson::Value & jv,size_t data_len,float data[])74 static bool parse_encoding_data(const skjson::Value& jv, size_t data_len, float data[]) {
75     const auto* jshape = shape_root(jv);
76     if (!jshape) {
77         return false;
78     }
79 
80     // vertices are required, in/out tangents are optional
81     const skjson::ArrayValue* jvs = (*jshape)["v"]; // vertex points
82     const skjson::ArrayValue* jis = (*jshape)["i"]; // in-tangent points
83     const skjson::ArrayValue* jos = (*jshape)["o"]; // out-tangent points
84 
85     if (!jvs || data_len != shape_encoding_len(jvs->size())) {
86         return false;
87     }
88 
89     auto parse_point = [](const skjson::ArrayValue* ja, size_t i, float* x, float* y) {
90         SkASSERT(ja);
91         const skjson::ArrayValue* jpt = (*ja)[i];
92 
93         if (!jpt || jpt->size() != 2ul) {
94             return false;
95         }
96 
97         return Parse((*jpt)[0], x) && Parse((*jpt)[1], y);
98     };
99 
100     auto parse_optional_point = [&parse_point](const skjson::ArrayValue* ja, size_t i,
101                                                float* x, float* y) {
102         if (!ja || i >= ja->size()) {
103             // default control point
104             *x = *y = 0;
105             return true;
106         }
107 
108         return parse_point(*ja, i, x, y);
109     };
110 
111     for (size_t i = 0; i < jvs->size(); ++i) {
112         float* dst = data + i * kFloatsPerVertex;
113         SkASSERT(dst + kFloatsPerVertex <= data + data_len);
114 
115         if (!parse_point         (jvs, i, dst +    kX_Index, dst +    kY_Index) ||
116             !parse_optional_point(jis, i, dst +  kInX_Index, dst +  kInY_Index) ||
117             !parse_optional_point(jos, i, dst + kOutX_Index, dst + kOutY_Index)) {
118             return false;
119         }
120     }
121 
122     // "closed" flag
123     data[data_len - 1] = ParseDefault<bool>((*jshape)["c"], false);
124 
125     return true;
126 }
127 
operator SkPath() const128 ShapeValue::operator SkPath() const {
129     const auto vertex_count = this->size() / kFloatsPerVertex;
130 
131     SkPathBuilder path;
132 
133     if (vertex_count) {
134         // conservatively assume all cubics
135         path.incReserve(1 + SkToInt(vertex_count * 3));
136 
137         // Move to first vertex.
138         path.moveTo((*this)[kX_Index], (*this)[kY_Index]);
139     }
140 
141     auto addCubic = [&](size_t from_vertex, size_t to_vertex) {
142         const auto from_index = kFloatsPerVertex * from_vertex,
143                      to_index = kFloatsPerVertex *   to_vertex;
144 
145         const SkPoint p0 = SkPoint{ (*this)[from_index +    kX_Index],
146                                     (*this)[from_index +    kY_Index] },
147                       p1 = SkPoint{ (*this)[  to_index +    kX_Index],
148                                     (*this)[  to_index +    kY_Index] },
149                       c0 = SkPoint{ (*this)[from_index + kOutX_Index],
150                                     (*this)[from_index + kOutY_Index] } + p0,
151                       c1 = SkPoint{ (*this)[  to_index +  kInX_Index],
152                                     (*this)[  to_index +  kInY_Index] } + p1;
153 
154         if (c0 == p0 && c1 == p1) {
155             // If the control points are coincident, we can power-reduce to a straight line.
156             // TODO: we could also do that when the controls are on the same line as the
157             //       vertices, but it's unclear how common that case is.
158             path.lineTo(p1);
159         } else {
160             path.cubicTo(c0, c1, p1);
161         }
162     };
163 
164     for (size_t i = 1; i < vertex_count; ++i) {
165         addCubic(i - 1, i);
166     }
167 
168     // Close the path with an extra cubic, if needed.
169     if (vertex_count && this->back() != 0) {
170         addCubic(vertex_count - 1, 0);
171         path.close();
172     }
173 
174     return path.detach();
175 }
176 
177 namespace internal {
178 
179 template <>
bind(const AnimationBuilder & abuilder,const skjson::ObjectValue * jprop,ShapeValue * v)180 bool AnimatablePropertyContainer::bind<ShapeValue>(const AnimationBuilder& abuilder,
181                                                   const skjson::ObjectValue* jprop,
182                                                   ShapeValue* v) {
183     VectorAnimatorBuilder builder(v, parse_encoding_len, parse_encoding_data);
184 
185     return this->bindImpl(abuilder, jprop, builder);
186 }
187 
188 } // namespace internal
189 
190 } // namespace skottie
191