#ifndef BezierPath_hpp #define BezierPath_hpp #include "Lottie/Private/Utility/Primitives/CurveVertex.hpp" #include "Lottie/Private/Utility/Primitives/PathElement.hpp" #include "Lottie/Private/Parsing/JsonParsing.hpp" #include "Lottie/Public/Primitives/CGPath.hpp" #include namespace lottie { struct BezierTrimPathPosition { double start; double end; explicit BezierTrimPathPosition(double start_, double end_) : start(start_), end(end_) { } }; class BezierPathContents: public std::enable_shared_from_this { public: /// Initializes a new Bezier Path. explicit BezierPathContents(CurveVertex const &startPoint) : elements({ PathElement(startPoint) }) { } BezierPathContents() : elements({}), closed(false) { } explicit BezierPathContents(json11::Json const &jsonAny) noexcept(false) : elements({}) { json11::Json::object const *json = nullptr; if (jsonAny.is_object()) { json = &jsonAny.object_items(); } else if (jsonAny.is_array()) { if (jsonAny.array_items().empty()) { throw LottieParsingException(); } if (!jsonAny.array_items()[0].is_object()) { throw LottieParsingException(); } json = &jsonAny.array_items()[0].object_items(); } if (const auto closedData = getOptionalBool(*json, "c")) { closed = closedData.value(); } auto vertexContainer = getAnyArray(*json, "v"); auto inPointsContainer = getAnyArray(*json, "i"); auto outPointsContainer = getAnyArray(*json, "o"); if (vertexContainer.size() != inPointsContainer.size() || inPointsContainer.size() != outPointsContainer.size()) { throw LottieParsingException(); } if (vertexContainer.empty()) { return; } /// Create first point Vector2D firstPoint(vertexContainer[0]); Vector2D firstInPoint(inPointsContainer[0]); Vector2D firstOutPoint(outPointsContainer[0]); CurveVertex firstVertex = CurveVertex::relative( firstPoint, firstInPoint, firstOutPoint ); PathElement previousElement(firstVertex); elements.push_back(previousElement); for (size_t i = 1; i < vertexContainer.size(); i++) { Vector2D point(vertexContainer[i]); Vector2D inPoint(inPointsContainer[i]); Vector2D outPoint(outPointsContainer[i]); CurveVertex vertex = CurveVertex::relative( point, inPoint, outPoint ); auto pathElement = previousElement.pathElementTo(vertex); elements.push_back(pathElement); previousElement = pathElement; } if (closed.value_or(false)) { auto closeElement = previousElement.pathElementTo(firstVertex); elements.push_back(closeElement); } } BezierPathContents(const BezierPathContents&) = delete; BezierPathContents& operator=(BezierPathContents&) = delete; json11::Json toJson() const { json11::Json::object result; json11::Json::array vertices; json11::Json::array inPoints; json11::Json::array outPoints; for (const auto &element : elements) { vertices.push_back(element.vertex.point.toJson()); inPoints.push_back(element.vertex.inTangentRelative().toJson()); outPoints.push_back(element.vertex.outTangentRelative().toJson()); } result.insert(std::make_pair("v", vertices)); result.insert(std::make_pair("i", inPoints)); result.insert(std::make_pair("o", outPoints)); if (closed.has_value()) { result.insert(std::make_pair("c", closed.value())); } return json11::Json(result); } std::shared_ptr cgPath() const { auto cgPath = CGPath::makePath(); std::optional previousElement; for (const auto &element : elements) { if (previousElement.has_value()) { if (previousElement->vertex.outTangentRelative().isZero() && element.vertex.inTangentRelative().isZero()) { cgPath->addLineTo(element.vertex.point); } else { cgPath->addCurveTo(element.vertex.point, previousElement->vertex.outTangent, element.vertex.inTangent); } } else { cgPath->moveTo(element.vertex.point); } previousElement = element; } if (closed.value_or(true)) { cgPath->closeSubpath(); } return cgPath; } public: std::vector elements; std::optional closed; double length() { if (_length.has_value()) { return _length.value(); } else { double result = 0.0; for (size_t i = 1; i < elements.size(); i++) { result += elements[i].length(elements[i - 1]); } _length = result; return result; } } private: std::optional _length; public: void moveToStartPoint(CurveVertex const &vertex) { elements = { PathElement(vertex) }; _length = std::nullopt; } void addVertex(CurveVertex const &vertex) { addElement(PathElement(vertex)); } void reserveCapacity(size_t capacity) { elements.reserve(capacity); } void setElementCount(size_t count) { elements.resize(count, PathElement(CurveVertex::absolute(Vector2D(0.0, 0.0), Vector2D(0.0, 0.0), Vector2D(0.0, 0.0)))); } void invalidateLength() { _length.reset(); } void addCurve(Vector2D const &toPoint, Vector2D const &outTangent, Vector2D const &inTangent) { if (elements.empty()) { return; } auto previous = elements[elements.size() - 1]; auto newVertex = CurveVertex::absolute(toPoint, inTangent, toPoint); updateVertex( CurveVertex::absolute(previous.vertex.point, previous.vertex.inTangent, outTangent), (int)elements.size() - 1, false ); addVertex(newVertex); } void addLine(Vector2D const &toPoint) { if (elements.empty()) { return; } auto previous = elements[elements.size() - 1]; auto newVertex = CurveVertex::relative(toPoint, Vector2D::Zero(), Vector2D::Zero()); updateVertex( CurveVertex::absolute(previous.vertex.point, previous.vertex.inTangent, previous.vertex.point), (int)elements.size() - 1, false ); addVertex(newVertex); } void close() { closed = true; } void addElement(PathElement const &pathElement) { elements.push_back(pathElement); } void updateVertex(CurveVertex const &vertex, int atIndex, bool remeasure) { if (remeasure) { PathElement newElement(CurveVertex::absolute(Vector2D::Zero(), Vector2D::Zero(), Vector2D::Zero())); if (atIndex > 0) { auto previousElement = elements[atIndex - 1]; newElement = previousElement.pathElementTo(vertex); } else { newElement = PathElement(vertex); } elements[atIndex] = newElement; if (atIndex + 1 < elements.size()) { auto nextElement = elements[atIndex + 1]; elements[atIndex + 1] = newElement.pathElementTo(nextElement.vertex); } } else { auto oldElement = elements[atIndex]; elements[atIndex] = oldElement.updateVertex(vertex); } } /// Trims a path fromLength toLength with an offset. /// /// Length and offset are defined in the length coordinate space. /// If any argument is outside the range of this path, then it will be looped over the path from finish to start. /// /// Cutting the curve when fromLength is less than toLength /// x x x x /// ~~~~~~~~~~~~~~~ooooooooooooooooooooooooooooooooooooooooooooooooo------------------- /// |Offset |fromLength toLength| | /// /// Cutting the curve when from Length is greater than toLength /// x x x x x /// oooooooooooooooooo--------------------~~~~~~~~~~~~~~~~ooooooooooooooooooooooooooooo /// | toLength| |Offset |fromLength | /// std::vector> trim(double fromLength, double toLength, double offsetLength) { if (elements.size() <= 1) { return {}; } if (fromLength == toLength) { return {}; } double lengthValue = length(); /// Normalize lengths to the curve length. auto start = fmod(fromLength + offsetLength, lengthValue); auto end = fmod(toLength + offsetLength, lengthValue); if (start < 0.0) { start = lengthValue + start; } if (end < 0.0) { end = lengthValue + end; } if (start == lengthValue) { start = 0.0; } if (end == 0.0) { end = lengthValue; } if ( (start == 0.0 && end == lengthValue) || start == end || (start == lengthValue && end == 0.0) ) { /// The trim encompasses the entire path. Return. return { shared_from_this() }; } if (start > end) { // Start is greater than end. Two paths are returned. return trimPathAtLengths({ BezierTrimPathPosition(0.0, end), BezierTrimPathPosition(start, lengthValue) }); } return trimPathAtLengths({ BezierTrimPathPosition(start, end) }); } // MARK: Private std::vector> trimPathAtLengths(std::vector const &positions) { if (positions.empty()) { return {}; } auto remainingPositions = positions; auto trim = remainingPositions[0]; remainingPositions.erase(remainingPositions.begin()); std::vector> paths; double runningLength = 0.0; bool finishedTrimming = false; auto pathElements = elements; auto currentPath = std::make_shared(); int i = 0; while (!finishedTrimming) { if (pathElements.size() <= i) { /// Do this for rounding errors paths.push_back(currentPath); finishedTrimming = true; continue; } /// Loop through and add elements within start->end range. /// Get current element auto element = pathElements[i]; double elementLength = 0.0; if (i != 0) { elementLength = element.length(pathElements[i - 1]); } /// Calculate new running length. auto newLength = runningLength + elementLength; if (newLength < trim.start) { /// Element is not included in the trim, continue. runningLength = newLength; i = i + 1; /// Increment index, we are done with this element. continue; } if (newLength == trim.start) { /// Current element IS the start element. /// For start we want to add a zero length element. currentPath->moveToStartPoint(element.vertex); runningLength = newLength; i = i + 1; /// Increment index, we are done with this element. continue; } if (runningLength < trim.start && trim.start < newLength && currentPath->elements.size() == 0) { /// The start of the trim is between this element and the previous, trim. /// Get previous element. auto previousElement = pathElements[i - 1]; /// Trim it auto trimLength = trim.start - runningLength; auto trimResults = element.splitElementAtPosition(previousElement, trimLength); /// Add the right span start. currentPath->moveToStartPoint(trimResults.rightSpan.start.vertex); pathElements[i] = trimResults.rightSpan.end; pathElements[i - 1] = trimResults.rightSpan.start; runningLength = runningLength + trimResults.leftSpan.end.length(trimResults.leftSpan.start); /// Dont increment index or the current length, the end of this path can be within this span. continue; } if (trim.start < newLength && newLength < trim.end) { /// Element lies within the trim span. currentPath->addElement(element); runningLength = newLength; i = i + 1; continue; } if (newLength == trim.end) { /// Element is the end element. /// The element could have a new length if it's added right after the start node. currentPath->addElement(element); /// We are done with this span. runningLength = newLength; i = i + 1; /// Allow the path to be finalized. /// Fall through to finalize path and move to next position } if (runningLength < trim.end && trim.end < newLength) { /// New element must be cut for end. /// Get previous element. auto previousElement = pathElements[i - 1]; /// Trim it auto trimLength = trim.end - runningLength; auto trimResults = element.splitElementAtPosition(previousElement, trimLength); /// Add the left span end. currentPath->updateVertex(trimResults.leftSpan.start.vertex, (int)currentPath->elements.size() - 1, false); currentPath->addElement(trimResults.leftSpan.end); pathElements[i] = trimResults.rightSpan.end; pathElements[i - 1] = trimResults.rightSpan.start; runningLength = runningLength + trimResults.leftSpan.end.length(trimResults.leftSpan.start); /// Dont increment index or the current length, the start of the next path can be within this span. /// We are done with this span. /// Allow the path to be finalized. /// Fall through to finalize path and move to next position } paths.push_back(currentPath); currentPath = std::make_shared(); if (remainingPositions.size() > 0) { trim = remainingPositions[0]; remainingPositions.erase(remainingPositions.begin()); } else { finishedTrimming = true; } } return paths; } }; class BezierPath { public: /// Initializes a new Bezier Path. explicit BezierPath(CurveVertex const &startPoint) : _contents(std::make_shared(startPoint)) { } BezierPath() : _contents(std::make_shared()) { } explicit BezierPath(json11::Json const &jsonAny) noexcept(false) : _contents(std::make_shared(jsonAny)) { } json11::Json toJson() const { return _contents->toJson(); } double length() { return _contents->length(); } void moveToStartPoint(CurveVertex const &vertex) { _contents->moveToStartPoint(vertex); } void addVertex(CurveVertex const &vertex) { _contents->addVertex(vertex); } void reserveCapacity(size_t capacity) { _contents->reserveCapacity(capacity); } void setElementCount(size_t count) { _contents->setElementCount(count); } void invalidateLength() { _contents->invalidateLength(); } void addCurve(Vector2D const &toPoint, Vector2D const &outTangent, Vector2D const &inTangent) { _contents->addCurve(toPoint, outTangent, inTangent); } void addLine(Vector2D const &toPoint) { _contents->addLine(toPoint); } void close() { _contents->close(); } void addElement(PathElement const &pathElement) { _contents->addElement(pathElement); } void updateVertex(CurveVertex const &vertex, int atIndex, bool remeasure) { _contents->updateVertex(vertex, atIndex, remeasure); } /// Trims a path fromLength toLength with an offset. /// /// Length and offset are defined in the length coordinate space. /// If any argument is outside the range of this path, then it will be looped over the path from finish to start. /// /// Cutting the curve when fromLength is less than toLength /// x x x x /// ~~~~~~~~~~~~~~~ooooooooooooooooooooooooooooooooooooooooooooooooo------------------- /// |Offset |fromLength toLength| | /// /// Cutting the curve when from Length is greater than toLength /// x x x x x /// oooooooooooooooooo--------------------~~~~~~~~~~~~~~~~ooooooooooooooooooooooooooooo /// | toLength| |Offset |fromLength | /// std::vector trim(double fromLength, double toLength, double offsetLength) { std::vector result; auto resultContents = _contents->trim(fromLength, toLength, offsetLength); for (const auto &resultContent : resultContents) { result.emplace_back(resultContent); } return result; } // MARK: Private std::vector const &elements() const { return _contents->elements; } std::vector &mutableElements() { return _contents->elements; } std::optional const &closed() const { return _contents->closed; } void setClosed(std::optional const &closed) { _contents->closed = closed; } std::shared_ptr cgPath() const { return _contents->cgPath(); } BezierPath copyUsingTransform(CATransform3D const &transform) const { if (transform == CATransform3D::identity()) { return (*this); } BezierPath result; result._contents->closed = _contents->closed; result.reserveCapacity(_contents->elements.size()); for (const auto &element : _contents->elements) { result._contents->elements.emplace_back(element.vertex.transformed(transform)); } return result; } public: BezierPath(std::shared_ptr contents) : _contents(contents) { } private: std::shared_ptr _contents; }; class BezierPathsBoundingBoxContext { public: BezierPathsBoundingBoxContext() : pointsX((float *)malloc(1024 * 4)), pointsY((float *)malloc(1024 * 4)), pointsSize(1024) { } ~BezierPathsBoundingBoxContext() { free(pointsX); free(pointsY); } public: float *pointsX = nullptr; float *pointsY = nullptr; int pointsSize = 0; }; CGRect bezierPathsBoundingBox(std::vector const &paths); CGRect bezierPathsBoundingBoxParallel(BezierPathsBoundingBoxContext &context, std::vector const &paths); CGRect calculateBoundingRectOpt(float const *pointsX, float const *pointsY, int count); } #endif /* BezierPath_hpp */