Swiftgram/src/vector/vpath.cpp
subhransu mohanty 7501ff54a6 lottie-player : Initial draft for lottie-player library
Change-Id: I8019e13863ef635fc011f0e7b82c692eaa01201d
2018-07-11 10:51:01 +00:00

736 lines
20 KiB
C++

#include"vpath.h"
#include<vector>
#include<cassert>
#include"vdebug.h"
#include"vbezier.h"
#include "vrect.h"
struct VPathData
{
void copy(VPathData *o);
void moveTo(const VPointF &pt);
void lineTo(const VPointF &pt);
void cubicTo(const VPointF &c1, const VPointF &c2, const VPointF &e);
void close();
void reset();
void checkNewSegment();
int segments() const;
void transform(const VMatrix &m);
RefCount ref;
std::vector<VPointF> m_points;
std::vector<VPath::Element> m_elements;
int m_segments;
VPointF mStartPoint;
bool mNewSegment;
};
void VPathData::transform(const VMatrix &m)
{
for(auto &i : m_points) {
i = m.map(i);
}
}
void VPathData::checkNewSegment()
{
if (mNewSegment) {
moveTo(VPointF(0,0));
mNewSegment = false;
}
}
void VPathData::copy(VPathData *o)
{
m_points = o->m_points;
m_elements = o->m_elements;
m_segments = o->m_segments;
mStartPoint = o->mStartPoint;
}
void VPathData::moveTo(const VPointF &p)
{
mStartPoint = p;
mNewSegment = false;
m_elements.push_back(VPath::Element::MoveTo);
m_points.push_back(p);
m_segments++;
}
void VPathData::lineTo(const VPointF &p)
{
checkNewSegment();
m_elements.push_back(VPath::Element::LineTo);
m_points.push_back(p);
}
void VPathData::cubicTo(const VPointF &c1, const VPointF &c2, const VPointF &e)
{
checkNewSegment();
m_elements.push_back(VPath::Element::CubicTo);
m_points.push_back(c1);
m_points.push_back(c2);
m_points.push_back(e);
}
void VPathData::close()
{
const VPointF &lastPt = m_points.back();
if (!fuzzyCompare(mStartPoint, lastPt)) {
lineTo(mStartPoint);
}
m_elements.push_back(VPath::Element::Close);
mNewSegment = true;
}
void VPathData::reset()
{
m_elements.clear();
m_points.clear();
m_segments = 0;
}
int VPathData::segments() const
{
return m_segments;
}
static const struct VPathData shared_empty = {RefCount(-1),
std::vector<VPointF>(),
std::vector<VPath::Element>(),
0,
VPointF(),
true};
inline void VPath::cleanUp(VPathData *d)
{
delete d;
}
void VPath::detach()
{
if (d->ref.isShared())
*this = copy();
}
VPath VPath::copy() const
{
VPath other;
other.d = new VPathData(shared_empty);
other.d->m_points = d->m_points;
other.d->m_elements = d->m_elements;
other.d->m_segments = d->m_segments;
other.d->ref.setOwned();
return other;
}
VPath::~VPath()
{
if (!d->ref.deref())
cleanUp(d);
}
VPath::VPath()
: d(const_cast<VPathData*>(&shared_empty))
{
}
VPath::VPath(const VPath &other)
{
d = other.d;
d->ref.ref();
}
VPath::VPath(VPath &&other): d(other.d)
{
other.d = const_cast<VPathData*>(&shared_empty);
}
VPath &VPath::operator=(const VPath &other)
{
other.d->ref.ref();
if (!d->ref.deref())
cleanUp(d);
d = other.d;
return *this;
}
inline VPath &VPath::operator=(VPath &&other)
{
if (!d->ref.deref())
cleanUp(d);
d = other.d;
other.d = const_cast<VPathData*>(&shared_empty);
return *this;
}
bool VPath::isEmpty()const
{
return d->m_elements.empty();
}
void VPath::close()
{
if (isEmpty()) return;
detach();
d->close();
}
void VPath::reset()
{
if (isEmpty()) return;
detach();
d->reset();
}
void VPath::moveTo(const VPointF &p)
{
detach();
d->moveTo(p);
}
void VPath::lineTo(const VPointF &p)
{
detach();
d->lineTo(p);
}
void VPath::cubicTo(const VPointF &c1, const VPointF &c2, const VPointF &e)
{
detach();
d->cubicTo(c1, c2, e);
}
void VPath::reserve(int num_elm)
{
detach();
d->m_elements.reserve(num_elm);
d->m_points.reserve(num_elm);
}
const std::vector<VPath::Element> &VPath::elements() const
{
return d->m_elements;
}
const std::vector<VPointF> &VPath::points() const
{
return d->m_points;
}
int VPath::segments() const
{
return d->m_segments + 1;
}
#define PATH_KAPPA 0.5522847498
static float tForArcAngle(float angle);
void findEllipseCoords(const VRectF &r, float angle, float length,
VPointF* startPoint, VPointF *endPoint)
{
if (r.isNull()) {
if (startPoint)
*startPoint = VPointF();
if (endPoint)
*endPoint = VPointF();
return;
}
float w2 = r.width() / 2;
float h2 = r.height() / 2;
float angles[2] = { angle, angle + length };
VPointF *points[2] = { startPoint, endPoint };
for (int i = 0; i < 2; ++i) {
if (!points[i])
continue;
float theta = angles[i] - 360 * floor(angles[i] / 360);
float t = theta / 90;
// truncate
int quadrant = int(t);
t -= quadrant;
t = tForArcAngle(90 * t);
// swap x and y?
if (quadrant & 1)
t = 1 - t;
float a, b, c, d;
VBezier::coefficients(t, a, b, c, d);
VPointF p(a + b + c*PATH_KAPPA, d + c + b*PATH_KAPPA);
// left quadrants
if (quadrant == 1 || quadrant == 2)
p.rx() = -p.x();
// top quadrants
if (quadrant == 0 || quadrant == 1)
p.ry() = -p.y();
*points[i] = r.center() + VPointF(w2 * p.x(), h2 * p.y());
}
}
static float
tForArcAngle(float angle)
{
float radians, cos_angle, sin_angle, tc, ts, t;
if (floatCmp(angle,0.f)) return 0;
if (floatCmp(angle, 90.0)) return 1;
radians = (angle/180) * M_PI;
cos_angle = cos(radians);
sin_angle = sin(radians);
// initial guess
tc = angle / 90;
// do some iterations of newton's method to approximate cos_angle
// finds the zero of the function b.pointAt(tc).x() - cos_angle
tc -= ((((2-3*PATH_KAPPA) * tc + 3*(PATH_KAPPA-1)) * tc) * tc + 1 - cos_angle) // value
/ (((6-9*PATH_KAPPA) * tc + 6*(PATH_KAPPA-1)) * tc); // derivative
tc -= ((((2-3*PATH_KAPPA) * tc + 3*(PATH_KAPPA-1)) * tc) * tc + 1 - cos_angle) // value
/ (((6-9*PATH_KAPPA) * tc + 6*(PATH_KAPPA-1)) * tc); // derivative
// initial guess
ts = tc;
// do some iterations of newton's method to approximate sin_angle
// finds the zero of the function b.pointAt(tc).y() - sin_angle
ts -= ((((3*PATH_KAPPA-2) * ts - 6*PATH_KAPPA + 3) * ts + 3*PATH_KAPPA) * ts - sin_angle)
/ (((9*PATH_KAPPA-6) * ts + 12*PATH_KAPPA - 6) * ts + 3*PATH_KAPPA);
ts -= ((((3*PATH_KAPPA-2) * ts - 6*PATH_KAPPA + 3) * ts + 3*PATH_KAPPA) * ts - sin_angle)
/ (((9*PATH_KAPPA-6) * ts + 12*PATH_KAPPA - 6) * ts + 3*PATH_KAPPA);
// use the average of the t that best approximates cos_angle
// and the t that best approximates sin_angle
t = 0.5 * (tc + ts);
return t;
}
// The return value is the starting point of the arc
static VPointF
curvesForArc(const VRectF &rect, float startAngle, float sweepLength,
VPointF *curves, int *point_count)
{
if (rect.isNull()) {
return VPointF();
}
float x = rect.x();
float y = rect.y();
float w = rect.width();
float w2 = rect.width() / 2;
float w2k = w2 * PATH_KAPPA;
float h = rect.height();
float h2 = rect.height() / 2;
float h2k = h2 * PATH_KAPPA;
VPointF points[16] =
{
// start point
VPointF(x + w, y + h2),
// 0 -> 270 degrees
VPointF(x + w, y + h2 + h2k),
VPointF(x + w2 + w2k, y + h),
VPointF(x + w2, y + h),
// 270 -> 180 degrees
VPointF(x + w2 - w2k, y + h),
VPointF(x, y + h2 + h2k),
VPointF(x, y + h2),
// 180 -> 90 degrees
VPointF(x, y + h2 - h2k),
VPointF(x + w2 - w2k, y),
VPointF(x + w2, y),
// 90 -> 0 degrees
VPointF(x + w2 + w2k, y),
VPointF(x + w, y + h2 - h2k),
VPointF(x + w, y + h2)
};
if (sweepLength > 360) sweepLength = 360;
else if (sweepLength < -360) sweepLength = -360;
// Special case fast paths
if (startAngle == 0.0) {
if (sweepLength == 360.0) {
for (int i = 11; i >= 0; --i)
curves[(*point_count)++] = points[i];
return points[12];
} else if (sweepLength == -360.0) {
for (int i = 1; i <= 12; ++i)
curves[(*point_count)++] = points[i];
return points[0];
}
}
int startSegment = int(floor(startAngle / 90));
int endSegment = int(floor((startAngle + sweepLength) / 90));
float startT = (startAngle - startSegment * 90) / 90;
float endT = (startAngle + sweepLength - endSegment * 90) / 90;
int delta = sweepLength > 0 ? 1 : -1;
if (delta < 0) {
startT = 1 - startT;
endT = 1 - endT;
}
// avoid empty start segment
if (floatNull(startT - float(1))) {
startT = 0;
startSegment += delta;
}
// avoid empty end segment
if (floatNull(endT)) {
endT = 1;
endSegment -= delta;
}
startT = tForArcAngle(startT * 90);
endT = tForArcAngle(endT * 90);
const bool splitAtStart = !floatNull(startT);
const bool splitAtEnd = !floatNull(endT - float(1));
const int end = endSegment + delta;
// empty arc?
if (startSegment == end) {
const int quadrant = 3 - ((startSegment % 4) + 4) % 4;
const int j = 3 * quadrant;
return delta > 0 ? points[j + 3] : points[j];
}
VPointF startPoint, endPoint;
findEllipseCoords(rect, startAngle, sweepLength, &startPoint, &endPoint);
for (int i = startSegment; i != end; i += delta) {
const int quadrant = 3 - ((i % 4) + 4) % 4;
const int j = 3 * quadrant;
VBezier b;
if (delta > 0)
b = VBezier::fromPoints(points[j + 3], points[j + 2], points[j + 1], points[j]);
else
b = VBezier::fromPoints(points[j], points[j + 1], points[j + 2], points[j + 3]);
// empty arc?
if (startSegment == endSegment && floatCmp(startT, endT))
return startPoint;
if (i == startSegment) {
if (i == endSegment && splitAtEnd)
b = b.onInterval(startT, endT);
else if (splitAtStart)
b = b.onInterval(startT, 1);
} else if (i == endSegment && splitAtEnd) {
b = b.onInterval(0, endT);
}
// push control points
curves[(*point_count)++] = b.pt2();
curves[(*point_count)++] = b.pt3();
curves[(*point_count)++] = b.pt4();
}
curves[*(point_count)-1] = endPoint;
return startPoint;
}
void VPath::arcTo(const VRectF &rect, float startAngle, float sweepLength, bool forceMoveTo)
{
detach();
int point_count = 0;
VPointF pts[15];
VPointF curve_start = curvesForArc(rect, startAngle, sweepLength, pts, &point_count);
if (isEmpty() || forceMoveTo) {
d->moveTo(curve_start);
} else {
d->lineTo(curve_start);
}
for (int i=0; i<point_count; i+=3) {
d->cubicTo(pts[i], pts[i+1], pts[i+2]);
}
}
void VPath::addCircle(float cx, float cy, float radius, VPath::Direction dir)
{
addOval(VRectF(cx-radius, cy-radius, 2*radius, 2*radius) , dir);
}
void VPath::addOval(const VRectF &rect, VPath::Direction dir)
{
if (rect.isNull()) return;
detach();
float x = rect.x();
float y = rect.y();
float w = rect.width();
float w2 = rect.width() / 2;
float w2k = w2 * PATH_KAPPA;
float h = rect.height();
float h2 = rect.height() / 2;
float h2k = h2 * PATH_KAPPA;
if (dir == VPath::Direction::CW) {
// moveto 12 o'clock.
d->moveTo(VPointF(x+w2, y));
// 12 -> 3 o'clock
d->cubicTo(VPointF(x + w2 + w2k, y), VPointF(x + w, y + h2 - h2k), VPointF(x + w, y + h2));
// 3 -> 6 o'clock
d->cubicTo(VPointF(x + w, y + h2 + h2k), VPointF(x + w2 + w2k, y + h), VPointF(x + w2, y + h));
// 6 -> 9 o'clock
d->cubicTo(VPointF(x + w2 - w2k, y + h), VPointF(x, y + h2 + h2k), VPointF(x , y + h2));
// 9 -> 12 o'clock
d->cubicTo(VPointF(x, y + h2 - h2k), VPointF(x + w2 - w2k, y), VPointF(x + w2, y));
} else {
// moveto 12 o'clock.
d->moveTo(VPointF(x+w2, y));
// 12 -> 9 o'clock
d->cubicTo(VPointF(x + w2 - w2k, y), VPointF(x, y + h2 - h2k), VPointF(x , y + h2));
// 9 -> 6 o'clock
d->cubicTo(VPointF(x, y + h2 + h2k), VPointF(x + w2 - w2k, y + h), VPointF(x + w2, y + h));
// 6 -> 3 o'clock
d->cubicTo(VPointF(x + w2 + w2k, y + h), VPointF(x + w, y + h2 + h2k), VPointF(x + w, y + h2));
// 3 -> 12 o'clock
d->cubicTo(VPointF(x + w, y + h2 - h2k), VPointF(x + w2 + w2k, y), VPointF(x+w2, y));
}
}
void VPath::addRect(const VRectF &rect, VPath::Direction dir)
{
if (rect.isNull()) return;
detach();
float x = rect.x();
float y = rect.y();
float w = rect.width();
float h = rect.height();
if (dir == VPath::Direction::CW) {
moveTo(VPointF(x + w, y));
lineTo(VPointF(x + w, y + h));
lineTo(VPointF(x , y + h));
lineTo(VPointF(x , y));
close();
} else {
moveTo(VPointF(x + w, y));
lineTo(VPointF(x , y));
lineTo(VPointF(x , y + h));
lineTo(VPointF(x + w, y + h));
close();
}
}
void VPath::addRoundRect(const VRectF &rect, float rx, float ry, VPath::Direction dir)
{
if (floatCmp(rx, 0.f) || floatCmp(ry, 0.f)) {
addRect(rect, dir);
return;
}
float x = rect.x();
float y = rect.y();
float w = rect.width();
float h = rect.height();
// clamp the rx and ry radius value.
rx = 2*rx;
ry = 2*ry;
if (rx > w) rx = w;
if (ry > h) ry = h;
if (dir == VPath::Direction::CW) {
moveTo(VPointF(x + w, y + ry/2.f));
arcTo(VRectF(x + w - rx, y + h - ry, rx, ry), 0 , -90, false);
arcTo(VRectF(x, y + h - ry, rx, ry), -90 , -90, false);
arcTo(VRectF(x, y, rx, ry), -180 , -90, false);
arcTo(VRectF(x + w - rx, y, rx, ry), -270 , -90, false);
close();
} else {
moveTo(VPointF(x + w, y + ry/2.f));
arcTo(VRectF(x + w - rx, y , rx, ry), 0 , 90, false);
arcTo(VRectF(x, y, rx, ry), 90 , 90, false);
arcTo(VRectF(x, y + h - ry, rx, ry), 180 , 90, false);
arcTo(VRectF(x + w - rx, y + h - ry, rx, ry), 270 , 90, false);
close();
}
}
void VPath::addPolystarStar(float startAngle, float cx, float cy, float points,
float innerRadius, float outerRadius,
float innerRoundness, float outerRoundness,
VPath::Direction dir)
{
// TODO: Direction feature is missing
const static float POLYSTAR_MAGIC_NUMBER = 0.47829 / 0.28;
float currentAngle = (startAngle - 90.0) * M_PI / 180.0;
float x;
float y;
float previousX;
float previousY;
float partialPointRadius = 0;
float anglePerPoint = (float) (2.0 * M_PI / points);
float halfAnglePerPoint = anglePerPoint / 2.0;
float partialPointAmount = points - (int) points;
bool longSegment = false;
int numPoints = (int) ceil(points) * 2.0;
innerRoundness /= 100.0;
outerRoundness /= 100.0;
if (partialPointAmount != 0) {
currentAngle += halfAnglePerPoint * (1.0 - partialPointAmount);
}
if (partialPointAmount != 0) {
partialPointRadius = innerRadius + partialPointAmount * (outerRadius - innerRadius);
x = (float) (partialPointRadius * cos(currentAngle));
y = (float) (partialPointRadius * sin(currentAngle));
currentAngle += anglePerPoint * partialPointAmount / 2.0;
} else {
x = (float) (outerRadius * cos(currentAngle));
y = (float) (outerRadius * sin(currentAngle));
currentAngle += halfAnglePerPoint;
}
moveTo(VPointF(x + cx, y + cy));
for (int i = 0; i < numPoints; i++) {
float radius = longSegment ? outerRadius : innerRadius;
float dTheta = halfAnglePerPoint;
if (partialPointRadius != 0 && i == numPoints - 2) {
dTheta = anglePerPoint * partialPointAmount / 2.0;
}
if (partialPointRadius != 0 && i == numPoints - 1) {
radius = partialPointRadius;
}
previousX = x;
previousY = y;
x = (float) (radius * cos(currentAngle));
y = (float) (radius * sin(currentAngle));
if (innerRoundness == 0 && outerRoundness == 0) {
lineTo(VPointF(x + cx, y + cy));
} else {
float cp1Theta = (float) (atan2(previousY, previousX) - M_PI / 2.0);
float cp1Dx = (float) cos(cp1Theta);
float cp1Dy = (float) sin(cp1Theta);
float cp2Theta = (float) (atan2(y, x) - M_PI / 2.0);
float cp2Dx = (float) cos(cp2Theta);
float cp2Dy = (float) sin(cp2Theta);
float cp1Roundness = longSegment ? innerRoundness : outerRoundness;
float cp2Roundness = longSegment ? outerRoundness : innerRoundness;
float cp1Radius = longSegment ? innerRadius : outerRadius;
float cp2Radius = longSegment ? outerRadius : innerRadius;
float cp1x = cp1Radius * cp1Roundness * POLYSTAR_MAGIC_NUMBER * cp1Dx / points;
float cp1y = cp1Radius * cp1Roundness * POLYSTAR_MAGIC_NUMBER * cp1Dy / points;
float cp2x = cp2Radius * cp2Roundness * POLYSTAR_MAGIC_NUMBER * cp2Dx / points;
float cp2y = cp2Radius * cp2Roundness * POLYSTAR_MAGIC_NUMBER * cp2Dy / points;
if ((partialPointAmount != 0) &&
((i == 0) || (i == numPoints - 1))) {
cp1x *= partialPointAmount;
cp1y *= partialPointAmount;
cp2x *= partialPointAmount;
cp2y *= partialPointAmount;
}
cubicTo(VPointF(previousX - cp1x + cx, previousY - cp1y + cy),
VPointF(x + cp2x + cx, y + cp2y + cy),
VPointF(x + cx, y + cy));
}
currentAngle += dTheta;
longSegment = !longSegment;
}
close();
}
void VPath::addPolystarPolygon(float startAngle, float cx, float cy, float points,
float radius, float roundness,
VPath::Direction dir)
{
// TODO: Direction feature is missing
// TODO: Need to support floating point number for number of points
const static float POLYGON_MAGIC_NUMBER = 0.25;
float currentAngle = (startAngle - 90.0) * M_PI / 180.0;
float x;
float y;
float previousX;
float previousY;
float anglePerPoint = (float) (2.0 * M_PI / floor(points));
int numPoints = (int) floor(points);
roundness /= 100.0;
currentAngle = (currentAngle - 90.0) * M_PI / 180.0;
x = (float) (radius * cos(currentAngle));
y = (float) (radius * sin(currentAngle));
currentAngle += anglePerPoint;
moveTo(VPointF(x + cx, y + cy));
for (int i = 0; i < numPoints; i++) {
previousX = x;
previousY = y;
x = (float) (radius * cos(currentAngle));
y = (float) (radius * sin(currentAngle));
if (roundness != 0) {
float cp1Theta = (float) (atan2(previousY, previousX) - M_PI / 2.0);
float cp1Dx = (float) cos(cp1Theta);
float cp1Dy = (float) sin(cp1Theta);
float cp2Theta = (float) (atan2(y, x) - M_PI / 2.0);
float cp2Dx = (float) cos(cp2Theta);
float cp2Dy = (float) sin(cp2Theta);
float cp1x = radius * roundness * POLYGON_MAGIC_NUMBER * cp1Dx;
float cp1y = radius * roundness * POLYGON_MAGIC_NUMBER * cp1Dy;
float cp2x = radius * roundness * POLYGON_MAGIC_NUMBER * cp2Dx;
float cp2y = radius * roundness * POLYGON_MAGIC_NUMBER * cp2Dy;
cubicTo(VPointF(previousX - cp1x + cx, previousY - cp1y + cy),
VPointF(x + cp2x + cx, y + cp2y + cy),
VPointF(x, y));
} else {
lineTo(VPointF(x + cx, y + cy));
}
currentAngle += anglePerPoint;
}
close();
}
void VPath::transform(const VMatrix &m)
{
if (isEmpty()) return;
detach();
d->transform(m);
}