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
13namespace 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//
25enum 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
36static 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.
41static 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
51static 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
61static 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
115ShapeValue::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
166namespace internal {
167
168template <>
169bool 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