From 85885575b572b8cd30294fb2b1040fa8b95e1e17 Mon Sep 17 00:00:00 2001 From: Mira Grudzinska Date: Wed, 11 Jun 2025 23:26:14 +0200 Subject: [PATCH] lottie: fix offsetting partially degenerated cubics Until now, cases where a Bezier curve had `start == ctrl1` or `ctrl2 == end` were incorrectly treated as linear segments. This led to incorrect rendering, for example when offsetting a polystar with outer roundness > 0 and inner roundness == 0. Now such cases handled as proper curves with full cubic behavior. --- src/loaders/lottie/tvgLottieModifier.cpp | 43 +++++++++++++++++------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/src/loaders/lottie/tvgLottieModifier.cpp b/src/loaders/lottie/tvgLottieModifier.cpp index ecbfa67a..78d2cee5 100644 --- a/src/loaders/lottie/tvgLottieModifier.cpp +++ b/src/loaders/lottie/tvgLottieModifier.cpp @@ -330,6 +330,10 @@ bool LottieOffsetModifier::modifyPath(PathCommand* inCmds, uint32_t inCmdsCnt, P auto offset = _clockwise(inPts, inPtsCnt) ? this->offset : -this->offset; auto threshold = 1.0f / fabsf(offset) + 1.0f; + bool inside{}; + Point intersect{}; + bool degeneratedLine1{}, degeneratedLine3{}; + for (uint32_t iCmd = 0, iPt = 0; iCmd < inCmdsCnt; ++iCmd) { if (inCmds[iCmd] == PathCommand::MoveTo) { state.moveto = true; @@ -338,7 +342,7 @@ bool LottieOffsetModifier::modifyPath(PathCommand* inCmds, uint32_t inCmdsCnt, P 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])) { + if (_colinear(inPts + iPt - 1)) { ++iPt; line(out, inCmds, inCmdsCnt, inPts, iPt, iCmd, state, offset, true); ++iPt; @@ -350,7 +354,7 @@ bool LottieOffsetModifier::modifyPath(PathCommand* inCmds, uint32_t inCmdsCnt, P auto& bezier = stack.last(); auto len = tvg::length(bezier.start - bezier.ctrl1) + tvg::length(bezier.ctrl1 - bezier.ctrl2) + tvg::length(bezier.ctrl2 - bezier.end); - if (len > threshold * bezier.length()) { + if (len > threshold * bezier.length() && len > 1.0f) { Bezier next; bezier.split(0.5f, next); stack.push(next); @@ -358,9 +362,19 @@ bool LottieOffsetModifier::modifyPath(PathCommand* inCmds, uint32_t inCmdsCnt, P } stack.pop(); - auto line1 = _offset(bezier.start, bezier.ctrl1, offset); + degeneratedLine1 = _zero(bezier.start, bezier.ctrl1); + auto line1 = degeneratedLine1 ? state.line : _offset(bezier.start, bezier.ctrl1, offset); auto line2 = _offset(bezier.ctrl1, bezier.ctrl2, offset); - auto line3 = _offset(bezier.ctrl2, bezier.end, offset); + + //line3 from the previous iteration was degenerated to a point - calculate intersection with the last valid line (state.line) + if (degeneratedLine3) { + _intersect(degeneratedLine1 ? line2 : line1, state.line, intersect, inside); + out.pts.push(intersect); + out.pts.push(intersect); + } + + degeneratedLine3 = _zero(bezier.ctrl2, bezier.end); + auto& line3 = state.line = degeneratedLine3 ? line2 : _offset(bezier.ctrl2, bezier.end, offset); if (state.moveto) { out.cmds.push(PathCommand::MoveTo); @@ -370,19 +384,22 @@ bool LottieOffsetModifier::modifyPath(PathCommand* inCmds, uint32_t inCmdsCnt, P state.moveto = false; } - 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); + if (degeneratedLine1) out.pts.push(out.pts.last()); + else { + _intersect(line1, line2, intersect, inside); + out.pts.push(intersect); + } + + if (!degeneratedLine3) { + _intersect(line2, line3, intersect, inside); + out.pts.push(intersect); + out.pts.push(line3.pt2); + } out.cmds.push(PathCommand::CubicTo); } iPt += 3; - } - else { + } else { if (!_zero(inPts[iPt - 1], inPts[state.movetoInIndex])) { out.cmds.push(PathCommand::LineTo); corner(out, state.line, state.firstLine, state.movetoOutIndex, true);