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