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
14namespace 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//
26enum 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
37static 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.
42static 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
52static 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
62static 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
116ShapeValue::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
165namespace internal {
166
167template <>
168bool 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