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)
{
auto temp = (ctx->offset) ? Shape::gen() : shape;
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) {
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 numPoints = size_t(ceilf(ptsCnt) * 2);
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));
Shape* shape;
@ -534,14 +593,8 @@ void LottieBuilder::updateStar(LottiePolyStar* star, float frameNo, Matrix* tran
angle += halfAnglePerPoint * direction;
}
if (tvg::zero(innerRoundness) && tvg::zero(outerRoundness)) {
SHAPE(shape)->rs.path.pts.reserve(numPoints + 2);
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;
}
SHAPE(shape)->rs.path.pts.reserve(numPoints * 3 + 2);
SHAPE(shape)->rs.path.cmds.reserve(numPoints + 3);
auto in = Point{x, y} * transform;
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);
} else {
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;
longSegment = !longSegment;
@ -659,7 +712,7 @@ void LottieBuilder::updatePolygon(LottieGroup* parent, LottiePolyStar* star, flo
} else {
Point in = {x, y};
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;
}

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)
{
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 dNext = rNext * (curr - next);
pts.push(curr - 2.0f * dPrev);
pts.push(curr - dPrev);
pts.push(curr - dNext);
pts.push(curr - 2.0f * dNext);
cmds.push(PathCommand::LineTo);
cmds.push(PathCommand::CubicTo);
_cubic(cmds, pts, pts.last(), curr - 2.0f * dPrev, curr - 2.0f * dPrev);
_cubic(cmds, pts, curr - dPrev, curr - dNext, curr - 2.0f * dNext);
}
@ -108,42 +113,39 @@ void LottieOffsetModifier::corner(RenderPath& out, Line& line, Line& nextLine, u
Point intersect{};
if (_intersect(line, nextLine, intersect, inside)) {
if (inside) {
if (nextClose) out.pts[movetoOutIndex] = intersect;
out.pts.push(intersect);
if (nextClose) {
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 {
out.pts.push(line.pt2);
_cubic(out.cmds, out.pts, out.pts.last(), line.pt2, line.pt2);
if (join == StrokeJoin::Round) {
out.cmds.push(PathCommand::CubicTo);
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);
_cubic(out.cmds, out.pts, (line.pt2 + intersect) * 0.5f, (nextLine.pt1 + intersect) * 0.5f, nextLine.pt1);
} else {
out.cmds.push(PathCommand::LineTo);
out.pts.push(nextLine.pt1);
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)) {
_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])) {
++curPt;
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) {
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]);
};
out.cmds.push(PathCommand::LineTo);
if (curCmd + 1 == inCmdsCnt || inCmds[curCmd + 1] == PathCommand::MoveTo || nonDegeneratedCubic(curCmd + 1, curPt + degenerated)) {
out.pts.push(state.line.pt2);
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);
++curPt;
return;
}
Line nextLine = state.firstLine;
if (inCmds[curCmd + 1] == PathCommand::LineTo) nextLine = _offset(inPts[curPt + degenerated], inPts[curPt + 1 + degenerated], 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 + degenerated], inPts[state.movetoInIndex + degenerated]))
nextLine = _offset(inPts[curPt + degenerated], inPts[state.movetoInIndex + degenerated], offset);
if (inCmds[curCmd + 1] == PathCommand::CubicTo) nextLine = _offset(inPts[curPt + 2], inPts[curPt + 3], offset);
else if (inCmds[curCmd + 1] == PathCommand::Close && !_zero(inPts[curPt + 1], inPts[state.movetoInIndex + 1]))
nextLine = _offset(inPts[curPt + 1], inPts[state.movetoInIndex + 1], offset);
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;
} else if (inCmds[iCmds + 1] == PathCommand::Close) {
_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();
iPts += 3;
break;
}
}
path.cmds.push(PathCommand::CubicTo);
path.pts.push(inPts[iPts++]);
path.pts.push(inPts[iPts++]);
path.pts.push(inPts[iPts++]);
_cubic(path.cmds, path.pts, inPts[iPts], inPts[iPts + 1], inPts[iPts + 2]);
iPts += 3;
break;
}
case PathCommand::Close: {
@ -272,14 +270,12 @@ bool LottieRoundnessModifier::modifyPolystar(RenderPath& in, RenderPath& out, fl
auto p2 = curr - dNext;
auto p3 = curr - 2.0f * dNext;
path.cmds.push(PathCommand::CubicTo);
path.pts.push(prev); path.pts.push(p0); path.pts.push(p0);
path.cmds.push(PathCommand::CubicTo);
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);
_cubic(path.cmds, path.pts, prev, p0, p0);
_cubic(path.cmds, path.pts, p1, p2, p3);
_cubic(path.cmds, path.pts, p3, next, nextCtrl);
}
} else {
//TODO - change for cubic instead of lines
path.cmds.grow(2 * 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 p3 = curr - 2.0f * dNext;
path.cmds.push(PathCommand::LineTo);
path.pts.push(p0);
path.cmds.push(PathCommand::CubicTo);
path.pts.push(p1); path.pts.push(p2); path.pts.push(p3);
_cubic(path.cmds, path.pts, path.pts.last(), p0, p0);
_cubic(path.cmds, path.pts, p1, p2, p3);
dPrev = -1.0f * dNext;
}
@ -337,13 +331,11 @@ bool LottieOffsetModifier::modifyPath(PathCommand* inCmds, uint32_t inCmdsCnt, P
if (inCmds[iCmd] == PathCommand::MoveTo) {
state.moveto = true;
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) {
//cubic degenerated to a line
if (tvg::zero(inPts[iPt - 1] - inPts[iPt]) || tvg::zero(inPts[iPt + 1] - inPts[iPt + 2])) {
++iPt;
line(out, inCmds, inCmdsCnt, inPts, iPt, iCmd, state, offset, true);
line(out, inCmds, inCmdsCnt, inPts, iPt, iCmd, state, offset);
++iPt;
continue;
}
@ -374,20 +366,16 @@ bool LottieOffsetModifier::modifyPath(PathCommand* inCmds, uint32_t inCmdsCnt, P
}
bool inside{};
Point intersect{};
_intersect(line1, line2, intersect, inside);
out.pts.push(intersect);
_intersect(line2, line3, intersect, inside);
out.pts.push(intersect);
out.pts.push(line3.pt2);
out.cmds.push(PathCommand::CubicTo);
Point intersect1{}, intersect2{};
_intersect(line1, line2, intersect1, inside);
_intersect(line2, line3, intersect2, inside);
_cubic(out.cmds, out.pts, intersect1, intersect2, line3.pt2);
}
iPt += 3;
}
else {
if (!_zero(inPts[iPt - 1], inPts[state.movetoInIndex])) {
out.cmds.push(PathCommand::LineTo);
corner(out, state.line, state.firstLine, state.movetoOutIndex, true);
}
out.cmds.push(PathCommand::Close);

View file

@ -102,7 +102,7 @@ private:
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);
};