/* * Copyright 2020 Google Inc. * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #include "include/core/SkPathBuilder.h" #include "modules/skottie/src/SkottieJson.h" #include "modules/skottie/src/SkottieValue.h" #include "modules/skottie/src/animator/Animator.h" #include "modules/skottie/src/animator/VectorKeyframeAnimator.h" namespace skottie { // Shapes (paths) are encoded as a vector of floats. For each vertex, we store 6 floats: // // - vertex point (2 floats) // - in-tangent point (2 floats) // - out-tangent point (2 floats) // // Additionally, we store one trailing "closed shape" flag - e.g. // // [ v0.x, v0.y, v0_in.x, v0_in.y, v0_out.x, v0_out.y, ... , closed_flag ] // enum ShapeEncodingInfo : size_t { kX_Index = 0, kY_Index = 1, kInX_Index = 2, kInY_Index = 3, kOutX_Index = 4, kOutY_Index = 5, kFloatsPerVertex = 6 }; static size_t shape_encoding_len(size_t vertex_count) { return vertex_count * kFloatsPerVertex + 1; } // Some versions wrap shape values as single-element arrays. static const skjson::ObjectValue* shape_root(const skjson::Value& jv) { if (const skjson::ArrayValue* av = jv) { if (av->size() == 1) { return (*av)[0]; } } return jv; } static bool parse_encoding_len(const skjson::Value& jv, size_t* len) { if (const auto* jshape = shape_root(jv)) { if (const skjson::ArrayValue* jvs = (*jshape)["v"]) { *len = shape_encoding_len(jvs->size()); return true; } } return false; } static bool parse_encoding_data(const skjson::Value& jv, size_t data_len, float data[]) { const auto* jshape = shape_root(jv); if (!jshape) { return false; } // vertices are required, in/out tangents are optional const skjson::ArrayValue* jvs = (*jshape)["v"]; // vertex points const skjson::ArrayValue* jis = (*jshape)["i"]; // in-tangent points const skjson::ArrayValue* jos = (*jshape)["o"]; // out-tangent points if (!jvs || data_len != shape_encoding_len(jvs->size())) { return false; } auto parse_point = [](const skjson::ArrayValue* ja, size_t i, float* x, float* y) { SkASSERT(ja); const skjson::ArrayValue* jpt = (*ja)[i]; if (!jpt || jpt->size() != 2ul) { return false; } return Parse((*jpt)[0], x) && Parse((*jpt)[1], y); }; auto parse_optional_point = [&parse_point](const skjson::ArrayValue* ja, size_t i, float* x, float* y) { if (!ja || i >= ja->size()) { // default control point *x = *y = 0; return true; } return parse_point(*ja, i, x, y); }; for (size_t i = 0; i < jvs->size(); ++i) { float* dst = data + i * kFloatsPerVertex; SkASSERT(dst + kFloatsPerVertex <= data + data_len); if (!parse_point (jvs, i, dst + kX_Index, dst + kY_Index) || !parse_optional_point(jis, i, dst + kInX_Index, dst + kInY_Index) || !parse_optional_point(jos, i, dst + kOutX_Index, dst + kOutY_Index)) { return false; } } // "closed" flag data[data_len - 1] = ParseDefault((*jshape)["c"], false); return true; } ShapeValue::operator SkPath() const { const auto vertex_count = this->size() / kFloatsPerVertex; SkPathBuilder path; if (vertex_count) { // conservatively assume all cubics path.incReserve(1 + SkToInt(vertex_count * 3)); // Move to first vertex. path.moveTo((*this)[kX_Index], (*this)[kY_Index]); } auto addCubic = [&](size_t from_vertex, size_t to_vertex) { const auto from_index = kFloatsPerVertex * from_vertex, to_index = kFloatsPerVertex * to_vertex; const SkPoint p0 = SkPoint{ (*this)[from_index + kX_Index], (*this)[from_index + kY_Index] }, p1 = SkPoint{ (*this)[ to_index + kX_Index], (*this)[ to_index + kY_Index] }, c0 = SkPoint{ (*this)[from_index + kOutX_Index], (*this)[from_index + kOutY_Index] } + p0, c1 = SkPoint{ (*this)[ to_index + kInX_Index], (*this)[ to_index + kInY_Index] } + p1; if (c0 == p0 && c1 == p1) { // If the control points are coincident, we can power-reduce to a straight line. // TODO: we could also do that when the controls are on the same line as the // vertices, but it's unclear how common that case is. path.lineTo(p1); } else { path.cubicTo(c0, c1, p1); } }; for (size_t i = 1; i < vertex_count; ++i) { addCubic(i - 1, i); } // Close the path with an extra cubic, if needed. if (vertex_count && this->back() != 0) { addCubic(vertex_count - 1, 0); path.close(); } return path.detach(); } namespace internal { template <> bool AnimatablePropertyContainer::bind(const AnimationBuilder& abuilder, const skjson::ObjectValue* jprop, ShapeValue* v) { VectorAnimatorBuilder builder(v, parse_encoding_len, parse_encoding_data); return this->bindImpl(abuilder, jprop, builder); } } // namespace internal } // namespace skottie