lottie: use cubics instead of lines

This commit is contained in:
Mira Grudzinska 2025-06-18 01:38:27 +02:00
parent bd3d92f599
commit 065408b47a
3 changed files with 115 additions and 74 deletions

View file

@ -391,12 +391,71 @@ static void _repeat(LottieGroup* parent, Shape* path, RenderContext* ctx)
} }
static void _appendRect(RenderPath& path, float x, float y, float w, float h, float r, bool cw)
{
//sharp rect
if (tvg::zero(r)) {
path.cmds.grow(6);
path.cmds.push(PathCommand::MoveTo);
for (auto i = 0; i < 4; ++i) path.cmds.push(PathCommand::CubicTo);
path.cmds.push(PathCommand::Close);
path.pts.grow(13);
path.pts.push({x + w, y});
if (cw) {
path.pts.push({x + w, y}); path.pts.push({x + w, y + h}); path.pts.push({x + w, y + h});
path.pts.push({x + w, y + h}); path.pts.push({x, y + h}); path.pts.push({x, y + h});
path.pts.push({x, y + h}); path.pts.push({x, y}); path.pts.push({x, y});
path.pts.push({x, y}); path.pts.push({x + w, y}); path.pts.push({x + w, y});
} else {
path.pts.push({x + w, y}); path.pts.push({x, y}); path.pts.push({x, y});
path.pts.push({x, y}); path.pts.push({x, y + h}); path.pts.push({x, y + h});
path.pts.push({x, y + h}); path.pts.push({x + w, y + h}); path.pts.push({x + w, y + h});
path.pts.push({x + w, y + h}); path.pts.push({x + w, y}); path.pts.push({x + w, y});
}
//round rect
} else {
auto hsize = Point{w * 0.5f, h * 0.5f};
auto rx = (r > hsize.x) ? hsize.x : r;
auto ry = (r > hsize.y) ? hsize.y : r;
auto hr = Point{rx * PATH_KAPPA, ry * PATH_KAPPA};
path.cmds.grow(10);
path.cmds.push(PathCommand::MoveTo);
for (auto i = 0; i < 8; ++i) path.cmds.push(PathCommand::CubicTo);
path.cmds.push(PathCommand::Close);
path.pts.grow(25);
path.pts.push({x + w, y + ry});
if (cw) {
path.pts.push({x + w, y + ry}); path.pts.push({x + w, y + h - ry}); path.pts.push({x + w, y + h - ry});
path.pts.push({x + w, y + h - ry + hr.y}); path.pts.push({x + w - rx + hr.x, y + h}); path.pts.push({x + w - rx, y + h});
path.pts.push({x + w - rx, y + h}); path.pts.push({x + rx, y + h}); path.pts.push({x + rx, y + h});
path.pts.push({x + rx - hr.x, y + h}); path.pts.push({x, y + h - ry + hr.y}); path.pts.push({x, y + h - ry});
path.pts.push({x, y + h - ry}); path.pts.push({x, y + ry}); path.pts.push({x, y + ry});
path.pts.push({x, y + ry - hr.y}); path.pts.push({x + rx - hr.x, y}); path.pts.push({x + rx, y});
path.pts.push({x + rx, y}); path.pts.push({x + w - rx, y}); path.pts.push({x + w - rx, y});
path.pts.push({x + w - rx + hr.x, y}); path.pts.push({x + w, y + ry - hr.y}); path.pts.push({x + w, y + ry});
} else {
path.pts.push({x + w, y + ry - hr.y}); path.pts.push({x + w - rx + hr.x, y}); path.pts.push({x + w - rx, y});
path.pts.push({x + w - rx, y}); path.pts.push({x + rx, y}); path.pts.push({x + rx, y});
path.pts.push({x + rx, y}); path.pts.push({x, y + ry - hr.y}); path.pts.push({x, y + ry});
path.pts.push({x, y + ry}); path.pts.push({x, y + h - ry}); path.pts.push({x, y + h - ry});
path.pts.push({x, y + h - ry + hr.y}); path.pts.push({x + rx - hr.x, y + h}); path.pts.push({x + rx, y + h});
path.pts.push({x + rx, y + h}); path.pts.push({x + w - rx, y + h}); path.pts.push({x + w - rx, y + h});
path.pts.push({x + w - rx + hr.x, y + h}); path.pts.push({x + w, y + h - ry + hr.y}); path.pts.push({x + w, y + h - ry});
path.pts.push({x + w, y + h - ry}); path.pts.push({x + w, y + ry}); path.pts.push({x + w, y + ry});
}
}
}
void LottieBuilder::appendRect(Shape* shape, Point& pos, Point& size, float r, bool clockwise, RenderContext* ctx) void LottieBuilder::appendRect(Shape* shape, Point& pos, Point& size, float r, bool clockwise, RenderContext* ctx)
{ {
auto temp = (ctx->offset) ? Shape::gen() : shape; auto temp = (ctx->offset) ? Shape::gen() : shape;
auto cnt = SHAPE(temp)->rs.path.pts.count; auto cnt = SHAPE(temp)->rs.path.pts.count;
temp->appendRect(pos.x, pos.y, size.x, size.y, r, r, clockwise); _appendRect(SHAPE(temp)->rs.path, pos.x, pos.y, size.x, size.y, r, clockwise);
if (ctx->transform) { if (ctx->transform) {
for (auto i = cnt; i < SHAPE(temp)->rs.path.pts.count; ++i) { for (auto i = cnt; i < SHAPE(temp)->rs.path.pts.count; ++i) {
@ -506,7 +565,7 @@ void LottieBuilder::updateStar(LottiePolyStar* star, float frameNo, Matrix* tran
auto longSegment = false; auto longSegment = false;
auto numPoints = size_t(ceilf(ptsCnt) * 2); auto numPoints = size_t(ceilf(ptsCnt) * 2);
auto direction = star->clockwise ? 1.0f : -1.0f; auto direction = star->clockwise ? 1.0f : -1.0f;
auto hasRoundness = false; auto hasRoundness = !tvg::zero(innerRoundness) || !tvg::zero(outerRoundness);
bool roundedCorner = ctx->roundness && (tvg::zero(innerRoundness) || tvg::zero(outerRoundness)); bool roundedCorner = ctx->roundness && (tvg::zero(innerRoundness) || tvg::zero(outerRoundness));
Shape* shape; Shape* shape;
@ -534,14 +593,8 @@ void LottieBuilder::updateStar(LottiePolyStar* star, float frameNo, Matrix* tran
angle += halfAnglePerPoint * direction; angle += halfAnglePerPoint * direction;
} }
if (tvg::zero(innerRoundness) && tvg::zero(outerRoundness)) { SHAPE(shape)->rs.path.pts.reserve(numPoints * 3 + 2);
SHAPE(shape)->rs.path.pts.reserve(numPoints + 2); SHAPE(shape)->rs.path.cmds.reserve(numPoints + 3);
SHAPE(shape)->rs.path.cmds.reserve(numPoints + 3);
} else {
SHAPE(shape)->rs.path.pts.reserve(numPoints * 3 + 2);
SHAPE(shape)->rs.path.cmds.reserve(numPoints + 3);
hasRoundness = true;
}
auto in = Point{x, y} * transform; auto in = Point{x, y} * transform;
shape->moveTo(in.x, in.y); shape->moveTo(in.x, in.y);
@ -590,7 +643,7 @@ void LottieBuilder::updateStar(LottiePolyStar* star, float frameNo, Matrix* tran
shape->cubicTo(in2.x, in2.y, in3.x, in3.y, in4.x, in4.y); shape->cubicTo(in2.x, in2.y, in3.x, in3.y, in4.x, in4.y);
} else { } else {
auto in = Point{x, y} * transform; auto in = Point{x, y} * transform;
shape->lineTo(in.x, in.y); shape->cubicTo(SHAPE(shape)->rs.path.pts.last().x, SHAPE(shape)->rs.path.pts.last().y, in.x, in.y, in.x, in.y);
} }
angle += dTheta * direction; angle += dTheta * direction;
longSegment = !longSegment; longSegment = !longSegment;
@ -659,7 +712,7 @@ void LottieBuilder::updatePolygon(LottieGroup* parent, LottiePolyStar* star, flo
} else { } else {
Point in = {x, y}; Point in = {x, y};
if (transform) in *= *transform; if (transform) in *= *transform;
shape->lineTo(in.x, in.y); shape->cubicTo(SHAPE(shape)->rs.path.pts.last().x, SHAPE(shape)->rs.path.pts.last().y, in.x, in.y, in.x, in.y);
} }
angle += anglePerPoint * direction; angle += anglePerPoint * direction;
} }

View file

@ -33,6 +33,15 @@ static bool _colinear(const Point* p)
} }
static void _cubic(Array<PathCommand>& cmds, Array<Point>& pts, const Point& p1, const Point& p2, const Point& p3)
{
cmds.push(PathCommand::CubicTo);
pts.push(p1);
pts.push(p2);
pts.push(p3);
}
static void _roundCorner(Array<PathCommand>& cmds, Array<Point>& pts, Point& prev, Point& curr, Point& next, float r) static void _roundCorner(Array<PathCommand>& cmds, Array<Point>& pts, Point& prev, Point& curr, Point& next, float r)
{ {
auto lenPrev = length(prev - curr); auto lenPrev = length(prev - curr);
@ -43,12 +52,8 @@ static void _roundCorner(Array<PathCommand>& cmds, Array<Point>& pts, Point& pre
auto dPrev = rPrev * (curr - prev); auto dPrev = rPrev * (curr - prev);
auto dNext = rNext * (curr - next); auto dNext = rNext * (curr - next);
pts.push(curr - 2.0f * dPrev); _cubic(cmds, pts, pts.last(), curr - 2.0f * dPrev, curr - 2.0f * dPrev);
pts.push(curr - dPrev); _cubic(cmds, pts, curr - dPrev, curr - dNext, curr - 2.0f * dNext);
pts.push(curr - dNext);
pts.push(curr - 2.0f * dNext);
cmds.push(PathCommand::LineTo);
cmds.push(PathCommand::CubicTo);
} }
@ -108,42 +113,39 @@ void LottieOffsetModifier::corner(RenderPath& out, Line& line, Line& nextLine, u
Point intersect{}; Point intersect{};
if (_intersect(line, nextLine, intersect, inside)) { if (_intersect(line, nextLine, intersect, inside)) {
if (inside) { if (inside) {
if (nextClose) out.pts[movetoOutIndex] = intersect; if (nextClose) {
out.pts.push(intersect); if (movetoOutIndex + 1 < out.pts.count && tvg::zero(out.pts[movetoOutIndex] - out.pts[movetoOutIndex + 1])) out.pts[movetoOutIndex + 1] = intersect;
out.pts[movetoOutIndex] = intersect;
}
_cubic(out.cmds, out.pts, out.pts.last(), intersect, intersect);
} else { } else {
out.pts.push(line.pt2); _cubic(out.cmds, out.pts, out.pts.last(), line.pt2, line.pt2);
if (join == StrokeJoin::Round) { if (join == StrokeJoin::Round) {
out.cmds.push(PathCommand::CubicTo); _cubic(out.cmds, out.pts, (line.pt2 + intersect) * 0.5f, (nextLine.pt1 + intersect) * 0.5f, nextLine.pt1);
out.pts.push((line.pt2 + intersect) * 0.5f);
out.pts.push((nextLine.pt1 + intersect) * 0.5f);
out.pts.push(nextLine.pt1);
} else if (join == StrokeJoin::Miter) {
auto norm = normal(line.pt1, line.pt2);
auto nextNorm = normal(nextLine.pt1, nextLine.pt2);
auto miterDirection = (norm + nextNorm) / length(norm + nextNorm);
if (1.0f <= miterLimit * fabsf(miterDirection.x * norm.x + miterDirection.y * norm.y)) {
out.cmds.push(PathCommand::LineTo);
out.pts.push(intersect);
}
out.cmds.push(PathCommand::LineTo);
out.pts.push(nextLine.pt1);
} else { } else {
out.cmds.push(PathCommand::LineTo); if (join == StrokeJoin::Miter) {
out.pts.push(nextLine.pt1); auto norm = normal(line.pt1, line.pt2);
auto nextNorm = normal(nextLine.pt1, nextLine.pt2);
auto miterDirection = (norm + nextNorm) / length(norm + nextNorm);
if (1.0f <= miterLimit * fabsf(miterDirection.x * norm.x + miterDirection.y * norm.y)) {
_cubic(out.cmds, out.pts, out.pts.last(), intersect, intersect);
}
}
_cubic(out.cmds, out.pts, out.pts.last(), nextLine.pt1, nextLine.pt1);
} }
} }
} else out.pts.push(line.pt2); } else _cubic(out.cmds, out.pts, out.pts.last(), line.pt2, line.pt2);
} }
void LottieOffsetModifier::line(RenderPath& out, PathCommand* inCmds, uint32_t inCmdsCnt, Point* inPts, uint32_t& curPt, uint32_t curCmd, State& state, float offset, bool degenerated) void LottieOffsetModifier::line(RenderPath& out, PathCommand* inCmds, uint32_t inCmdsCnt, Point* inPts, uint32_t& curPt, uint32_t curCmd, State& state, float offset)
{ {
if (tvg::zero(inPts[curPt - 1] - inPts[curPt])) { if (tvg::zero(inPts[curPt - 1] - inPts[curPt])) {
++curPt; ++curPt;
return; return;
} }
if (inCmds[curCmd - 1] != PathCommand::LineTo) state.line = _offset(inPts[curPt - 1], inPts[curPt], offset); state.line = _offset(inPts[curPt - 1], inPts[curPt], offset);
if (state.moveto) { if (state.moveto) {
out.cmds.push(PathCommand::MoveTo); out.cmds.push(PathCommand::MoveTo);
@ -157,19 +159,16 @@ void LottieOffsetModifier::line(RenderPath& out, PathCommand* inCmds, uint32_t i
return inCmds[cmd] == PathCommand::CubicTo && !tvg::zero(inPts[pt] - inPts[pt + 1]) && !tvg::zero(inPts[pt + 2] - inPts[pt + 3]); return inCmds[cmd] == PathCommand::CubicTo && !tvg::zero(inPts[pt] - inPts[pt + 1]) && !tvg::zero(inPts[pt + 2] - inPts[pt + 3]);
}; };
out.cmds.push(PathCommand::LineTo); if (curCmd + 1 == inCmdsCnt || inCmds[curCmd + 1] == PathCommand::MoveTo || nonDegeneratedCubic(curCmd + 1, curPt + 1)) {
_cubic(out.cmds, out.pts, out.pts.last(), state.line.pt2, state.line.pt2);
if (curCmd + 1 == inCmdsCnt || inCmds[curCmd + 1] == PathCommand::MoveTo || nonDegeneratedCubic(curCmd + 1, curPt + degenerated)) {
out.pts.push(state.line.pt2);
++curPt; ++curPt;
return; return;
} }
Line nextLine = state.firstLine; Line nextLine = state.firstLine;
if (inCmds[curCmd + 1] == PathCommand::LineTo) nextLine = _offset(inPts[curPt + degenerated], inPts[curPt + 1 + degenerated], offset); if (inCmds[curCmd + 1] == PathCommand::CubicTo) nextLine = _offset(inPts[curPt + 2], inPts[curPt + 3], offset);
else if (inCmds[curCmd + 1] == PathCommand::CubicTo) nextLine = _offset(inPts[curPt + 1 + degenerated], inPts[curPt + 2 + degenerated], offset); else if (inCmds[curCmd + 1] == PathCommand::Close && !_zero(inPts[curPt + 1], inPts[state.movetoInIndex + 1]))
else if (inCmds[curCmd + 1] == PathCommand::Close && !_zero(inPts[curPt + degenerated], inPts[state.movetoInIndex + degenerated])) nextLine = _offset(inPts[curPt + 1], inPts[state.movetoInIndex + 1], offset);
nextLine = _offset(inPts[curPt + degenerated], inPts[state.movetoInIndex + degenerated], offset);
corner(out, state.line, nextLine, state.movetoOutIndex, inCmds[curCmd + 1] == PathCommand::Close); corner(out, state.line, nextLine, state.movetoOutIndex, inCmds[curCmd + 1] == PathCommand::Close);
@ -210,15 +209,14 @@ bool LottieRoundnessModifier::modifyPath(PathCommand* inCmds, uint32_t inCmdsCnt
break; break;
} else if (inCmds[iCmds + 1] == PathCommand::Close) { } else if (inCmds[iCmds + 1] == PathCommand::Close) {
_roundCorner(path.cmds, path.pts, prev, curr, inPts[2], r); _roundCorner(path.cmds, path.pts, prev, curr, inPts[2], r);
if (tvg::zero(path.pts[startIndex] - path.pts[startIndex + 1])) path.pts[startIndex + 1] = path.pts.last();
path.pts[startIndex] = path.pts.last(); path.pts[startIndex] = path.pts.last();
iPts += 3; iPts += 3;
break; break;
} }
} }
path.cmds.push(PathCommand::CubicTo); _cubic(path.cmds, path.pts, inPts[iPts], inPts[iPts + 1], inPts[iPts + 2]);
path.pts.push(inPts[iPts++]); iPts += 3;
path.pts.push(inPts[iPts++]);
path.pts.push(inPts[iPts++]);
break; break;
} }
case PathCommand::Close: { case PathCommand::Close: {
@ -272,14 +270,12 @@ bool LottieRoundnessModifier::modifyPolystar(RenderPath& in, RenderPath& out, fl
auto p2 = curr - dNext; auto p2 = curr - dNext;
auto p3 = curr - 2.0f * dNext; auto p3 = curr - 2.0f * dNext;
path.cmds.push(PathCommand::CubicTo); _cubic(path.cmds, path.pts, prev, p0, p0);
path.pts.push(prev); path.pts.push(p0); path.pts.push(p0); _cubic(path.cmds, path.pts, p1, p2, p3);
path.cmds.push(PathCommand::CubicTo); _cubic(path.cmds, path.pts, p3, next, nextCtrl);
path.pts.push(p1); path.pts.push(p2); path.pts.push(p3);
path.cmds.push(PathCommand::CubicTo);
path.pts.push(p3); path.pts.push(next); path.pts.push(nextCtrl);
} }
} else { } else {
//TODO - change for cubic instead of lines
path.cmds.grow(2 * in.cmds.count); path.cmds.grow(2 * in.cmds.count);
path.pts.grow(4 * in.cmds.count); path.pts.grow(4 * in.cmds.count);
@ -298,10 +294,8 @@ bool LottieRoundnessModifier::modifyPolystar(RenderPath& in, RenderPath& out, fl
auto p2 = curr - dNext; auto p2 = curr - dNext;
auto p3 = curr - 2.0f * dNext; auto p3 = curr - 2.0f * dNext;
path.cmds.push(PathCommand::LineTo); _cubic(path.cmds, path.pts, path.pts.last(), p0, p0);
path.pts.push(p0); _cubic(path.cmds, path.pts, p1, p2, p3);
path.cmds.push(PathCommand::CubicTo);
path.pts.push(p1); path.pts.push(p2); path.pts.push(p3);
dPrev = -1.0f * dNext; dPrev = -1.0f * dNext;
} }
@ -337,13 +331,11 @@ bool LottieOffsetModifier::modifyPath(PathCommand* inCmds, uint32_t inCmdsCnt, P
if (inCmds[iCmd] == PathCommand::MoveTo) { if (inCmds[iCmd] == PathCommand::MoveTo) {
state.moveto = true; state.moveto = true;
state.movetoInIndex = iPt++; state.movetoInIndex = iPt++;
} else if (inCmds[iCmd] == PathCommand::LineTo) {
line(out, inCmds, inCmdsCnt, inPts, iPt, iCmd, state, offset, false);
} else if (inCmds[iCmd] == PathCommand::CubicTo) { } else if (inCmds[iCmd] == PathCommand::CubicTo) {
//cubic degenerated to a line //cubic degenerated to a line
if (tvg::zero(inPts[iPt - 1] - inPts[iPt]) || tvg::zero(inPts[iPt + 1] - inPts[iPt + 2])) { if (tvg::zero(inPts[iPt - 1] - inPts[iPt]) || tvg::zero(inPts[iPt + 1] - inPts[iPt + 2])) {
++iPt; ++iPt;
line(out, inCmds, inCmdsCnt, inPts, iPt, iCmd, state, offset, true); line(out, inCmds, inCmdsCnt, inPts, iPt, iCmd, state, offset);
++iPt; ++iPt;
continue; continue;
} }
@ -374,20 +366,16 @@ bool LottieOffsetModifier::modifyPath(PathCommand* inCmds, uint32_t inCmdsCnt, P
} }
bool inside{}; bool inside{};
Point intersect{}; Point intersect1{}, intersect2{};
_intersect(line1, line2, intersect, inside); _intersect(line1, line2, intersect1, inside);
out.pts.push(intersect); _intersect(line2, line3, intersect2, inside);
_intersect(line2, line3, intersect, inside); _cubic(out.cmds, out.pts, intersect1, intersect2, line3.pt2);
out.pts.push(intersect);
out.pts.push(line3.pt2);
out.cmds.push(PathCommand::CubicTo);
} }
iPt += 3; iPt += 3;
} }
else { else {
if (!_zero(inPts[iPt - 1], inPts[state.movetoInIndex])) { if (!_zero(inPts[iPt - 1], inPts[state.movetoInIndex])) {
out.cmds.push(PathCommand::LineTo);
corner(out, state.line, state.firstLine, state.movetoOutIndex, true); corner(out, state.line, state.firstLine, state.movetoOutIndex, true);
} }
out.cmds.push(PathCommand::Close); out.cmds.push(PathCommand::Close);

View file

@ -102,7 +102,7 @@ private:
uint32_t movetoInIndex = 0; uint32_t movetoInIndex = 0;
}; };
void line(RenderPath& out, PathCommand* inCmds, uint32_t inCmdsCnt, Point* inPts, uint32_t& curPt, uint32_t curCmd, State& state, float offset, bool degenerated); void line(RenderPath& out, PathCommand* inCmds, uint32_t inCmdsCnt, Point* inPts, uint32_t& curPt, uint32_t curCmd, State& state, float offset);
void corner(RenderPath& out, Line& line, Line& nextLine, uint32_t movetoIndex, bool nextClose); void corner(RenderPath& out, Line& line, Line& nextLine, uint32_t movetoIndex, bool nextClose);
}; };