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