mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-02 00:17:02 +00:00
595 lines
20 KiB
C++
595 lines
20 KiB
C++
#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 <vector>
|
|
|
|
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<BezierPathContents> {
|
|
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> cgPath() const {
|
|
auto cgPath = CGPath::makePath();
|
|
|
|
std::optional<PathElement> 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<PathElement> elements;
|
|
std::optional<bool> 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<double> _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<std::shared_ptr<BezierPathContents>> 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<std::shared_ptr<BezierPathContents>> trimPathAtLengths(std::vector<BezierTrimPathPosition> const &positions) {
|
|
if (positions.empty()) {
|
|
return {};
|
|
}
|
|
auto remainingPositions = positions;
|
|
|
|
auto trim = remainingPositions[0];
|
|
remainingPositions.erase(remainingPositions.begin());
|
|
|
|
std::vector<std::shared_ptr<BezierPathContents>> paths;
|
|
|
|
double runningLength = 0.0;
|
|
bool finishedTrimming = false;
|
|
auto pathElements = elements;
|
|
|
|
auto currentPath = std::make_shared<BezierPathContents>();
|
|
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<BezierPathContents>();
|
|
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<BezierPathContents>(startPoint)) {
|
|
}
|
|
|
|
BezierPath() :
|
|
_contents(std::make_shared<BezierPathContents>()) {
|
|
}
|
|
|
|
explicit BezierPath(json11::Json const &jsonAny) noexcept(false) :
|
|
_contents(std::make_shared<BezierPathContents>(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<BezierPath> trim(double fromLength, double toLength, double offsetLength) {
|
|
std::vector<BezierPath> result;
|
|
|
|
auto resultContents = _contents->trim(fromLength, toLength, offsetLength);
|
|
for (const auto &resultContent : resultContents) {
|
|
result.emplace_back(resultContent);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// MARK: Private
|
|
|
|
std::vector<PathElement> const &elements() const {
|
|
return _contents->elements;
|
|
}
|
|
|
|
std::vector<PathElement> &mutableElements() {
|
|
return _contents->elements;
|
|
}
|
|
|
|
std::optional<bool> const &closed() const {
|
|
return _contents->closed;
|
|
}
|
|
void setClosed(std::optional<bool> const &closed) {
|
|
_contents->closed = closed;
|
|
}
|
|
|
|
std::shared_ptr<CGPath> 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<BezierPathContents> contents) :
|
|
_contents(contents) {
|
|
}
|
|
|
|
private:
|
|
std::shared_ptr<BezierPathContents> _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<BezierPath> const &paths);
|
|
CGRect bezierPathsBoundingBoxParallel(BezierPathsBoundingBoxContext &context, std::vector<BezierPath> const &paths);
|
|
CGRect calculateBoundingRectOpt(float const *pointsX, float const *pointsY, int count);
|
|
|
|
}
|
|
|
|
#endif /* BezierPath_hpp */
|