From 065408b47a62521a9a8f55a3c3db921c72cf7758 Mon Sep 17 00:00:00 2001 From: Mira Grudzinska Date: Wed, 18 Jun 2025 01:38:27 +0200 Subject: [PATCH] lottie: use cubics instead of lines --- src/loaders/lottie/tvgLottieBuilder.cpp | 77 +++++++++++++--- src/loaders/lottie/tvgLottieModifier.cpp | 110 ++++++++++------------- src/loaders/lottie/tvgLottieModifier.h | 2 +- 3 files changed, 115 insertions(+), 74 deletions(-) diff --git a/src/loaders/lottie/tvgLottieBuilder.cpp b/src/loaders/lottie/tvgLottieBuilder.cpp index 13530042..9de24775 100644 --- a/src/loaders/lottie/tvgLottieBuilder.cpp +++ b/src/loaders/lottie/tvgLottieBuilder.cpp @@ -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; } diff --git a/src/loaders/lottie/tvgLottieModifier.cpp b/src/loaders/lottie/tvgLottieModifier.cpp index ed14d062..dd4f138e 100644 --- a/src/loaders/lottie/tvgLottieModifier.cpp +++ b/src/loaders/lottie/tvgLottieModifier.cpp @@ -33,6 +33,15 @@ static bool _colinear(const Point* p) } +static void _cubic(Array& cmds, Array& 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& cmds, Array& pts, Point& prev, Point& curr, Point& next, float r) { auto lenPrev = length(prev - curr); @@ -43,12 +52,8 @@ static void _roundCorner(Array& cmds, Array& 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); diff --git a/src/loaders/lottie/tvgLottieModifier.h b/src/loaders/lottie/tvgLottieModifier.h index b9367431..108f2730 100644 --- a/src/loaders/lottie/tvgLottieModifier.h +++ b/src/loaders/lottie/tvgLottieModifier.h @@ -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); };