lottie: chaining modifiers - path

Previously, the rounding modifier was always applied
first, which led to incorrect results when a different
modifier order was expected.
This update removes that limitation and enables modifiers
to be applied in any order.

Additionally, the code now prevents an infinite loop that
occurred when the rounded corners modifier was applied
multiple times (a cycle where next was set to this).
This commit is contained in:
Mira Grudzinska 2025-06-18 18:31:03 +02:00
parent 1ac6f3d09f
commit 87af337497
4 changed files with 43 additions and 39 deletions

View file

@ -715,7 +715,7 @@ void LottieBuilder::updateRoundedCorner(TVG_UNUSED LottieGroup* parent, LottieOb
auto r = roundedCorner->radius(frameNo, tween, exps);
if (r < LottieRoundnessModifier::ROUNDNESS_EPSILON) return;
if (!ctx->roundness) ctx->roundness = new LottieRoundnessModifier(&buffer, r);
if (!ctx->roundness) ctx->roundness = new LottieRoundnessModifier(buffer, r);
else if (ctx->roundness->r < r) ctx->roundness->r = r;
ctx->update(ctx->roundness);
@ -725,7 +725,7 @@ void LottieBuilder::updateRoundedCorner(TVG_UNUSED LottieGroup* parent, LottieOb
void LottieBuilder::updateOffsetPath(TVG_UNUSED LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist<RenderContext>& contexts, RenderContext* ctx)
{
auto offset = static_cast<LottieOffsetPath*>(*child);
if (!ctx->offset) ctx->offset = new LottieOffsetModifier(offset->offset(frameNo, tween, exps), offset->miterLimit(frameNo, tween, exps), offset->join);
if (!ctx->offset) ctx->offset = new LottieOffsetModifier(buffer, offset->offset(frameNo, tween, exps), offset->miterLimit(frameNo, tween, exps), offset->join);
ctx->update(ctx->offset);
}
@ -1230,7 +1230,7 @@ void LottieBuilder::updateMasks(LottieLayer* layer, float frameNo)
//Masking with Expansion (Offset)
} else {
//TODO: Once path direction support is implemented, ensure that the direction is ignored here
auto offset = LottieOffsetModifier(expand);
auto offset = LottieOffsetModifier(buffer, expand);
mask->pathset(frameNo, SHAPE(pShape)->rs.path, nullptr, tween, exps, &offset);
}
pOpacity = opacity;

View file

@ -88,7 +88,7 @@ struct RenderContext
update(roundness);
}
if (rhs.offset) {
offset = new LottieOffsetModifier(rhs.offset->offset, rhs.offset->miterLimit, rhs.offset->join);
offset = new LottieOffsetModifier(rhs.offset->buffer, rhs.offset->offset, rhs.offset->miterLimit, rhs.offset->join);
update(offset);
}
if (rhs.transform) {
@ -174,7 +174,7 @@ private:
void updateRoundedCorner(LottieGroup* parent, LottieObject** child, float frameNo, Inlist<RenderContext>& contexts, RenderContext* ctx);
void updateOffsetPath(LottieGroup* parent, LottieObject** child, float frameNo, Inlist<RenderContext>& contexts, RenderContext* ctx);
RenderPath buffer; //resusable path
RenderPath buffer[2]; //alternating buffers used when chaining multiple modifiers; for a single modifier only buffer[0] is used
LottieExpressions* exps;
Tween tween;
};

View file

@ -176,9 +176,8 @@ void LottieOffsetModifier::line(RenderPath& out, PathCommand* inCmds, uint32_t i
bool LottieRoundnessModifier::modifyPath(PathCommand* inCmds, uint32_t inCmdsCnt, Point* inPts, uint32_t inPtsCnt, Matrix* transform, RenderPath& out)
{
buffer->clear();
auto& path = (next) ? *buffer : out;
auto& path = next ? (inCmds == buffer[0].cmds.data ? buffer[1] : buffer[0]) : out;
if (next) path.clear();
path.cmds.reserve(inCmdsCnt * 2);
path.pts.reserve((uint32_t)(inPtsCnt * 1.5));
@ -237,9 +236,8 @@ bool LottieRoundnessModifier::modifyPolystar(RenderPath& in, RenderPath& out, fl
{
constexpr auto ROUNDED_POLYSTAR_MAGIC_NUMBER = 0.47829f;
buffer->clear();
auto& path = (next) ? *buffer : out;
auto& path = next ? (&in == &buffer[0] ? buffer[1] : buffer[0]) : out;
if (next) path.clear();
auto len = length(in.pts[1] - in.pts[2]);
auto r = len > 0.0f ? ROUNDED_POLYSTAR_MAGIC_NUMBER * std::min(len * 0.5f, this->r) / len : 0.0f;
@ -316,10 +314,11 @@ bool LottieRoundnessModifier::modifyRect(Point& size, float& r)
bool LottieOffsetModifier::modifyPath(PathCommand* inCmds, uint32_t inCmdsCnt, Point* inPts, uint32_t inPtsCnt, TVG_UNUSED Matrix* transform, RenderPath& out)
{
if (next) TVGERR("LOTTIE", "Offset has a next modifier?");
auto& path = next ? (inCmds == buffer[0].cmds.data ? buffer[1] : buffer[0]) : out;
if (next) path.clear();
out.cmds.reserve(inCmdsCnt * 2);
out.pts.reserve(inPtsCnt * (join == StrokeJoin::Round ? 4 : 2));
path.cmds.reserve(inCmdsCnt * 2);
path.pts.reserve(inPtsCnt * (join == StrokeJoin::Round ? 4 : 2));
Array<Bezier> stack{5};
State state;
@ -331,12 +330,12 @@ bool LottieOffsetModifier::modifyPath(PathCommand* inCmds, uint32_t inCmdsCnt, P
state.moveto = true;
state.movetoInIndex = iPt++;
} else if (inCmds[iCmd] == PathCommand::LineTo) {
line(out, inCmds, inCmdsCnt, inPts, iPt, iCmd, state, offset, false);
line(path, 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(path, inCmds, inCmdsCnt, inPts, iPt, iCmd, state, offset, true);
++iPt;
continue;
}
@ -359,9 +358,9 @@ bool LottieOffsetModifier::modifyPath(PathCommand* inCmds, uint32_t inCmdsCnt, P
auto line3 = _offset(bezier.ctrl2, bezier.end, offset);
if (state.moveto) {
out.cmds.push(PathCommand::MoveTo);
state.movetoOutIndex = out.pts.count;
out.pts.push(line1.pt1);
path.cmds.push(PathCommand::MoveTo);
state.movetoOutIndex = path.pts.count;
path.pts.push(line1.pt1);
state.firstLine = line1;
state.moveto = false;
}
@ -369,23 +368,26 @@ bool LottieOffsetModifier::modifyPath(PathCommand* inCmds, uint32_t inCmdsCnt, P
bool inside{};
Point intersect{};
_intersect(line1, line2, intersect, inside);
out.pts.push(intersect);
path.pts.push(intersect);
_intersect(line2, line3, intersect, inside);
out.pts.push(intersect);
out.pts.push(line3.pt2);
out.cmds.push(PathCommand::CubicTo);
path.pts.push(intersect);
path.pts.push(line3.pt2);
path.cmds.push(PathCommand::CubicTo);
}
iPt += 3;
}
else {
if (!tvg::zero(inPts[iPt - 1] - inPts[state.movetoInIndex])) {
out.cmds.push(PathCommand::LineTo);
corner(out, state.line, state.firstLine, state.movetoOutIndex, true);
path.cmds.push(PathCommand::LineTo);
corner(path, state.line, state.firstLine, state.movetoOutIndex, true);
}
out.cmds.push(PathCommand::Close);
path.cmds.push(PathCommand::Close);
}
}
if (next) return next->modifyPath(path.cmds.data, path.cmds.count, path.pts.data, path.pts.count, transform, out);
return true;
}

View file

@ -34,7 +34,11 @@ struct LottieModifier
enum Type : uint8_t {Roundness = 0, Offset};
LottieModifier* next = nullptr;
RenderPath* buffer;
Type type;
bool chained = false;
LottieModifier(RenderPath* buffer) : buffer(buffer) {}
virtual ~LottieModifier() {}
@ -43,18 +47,17 @@ struct LottieModifier
LottieModifier* decorate(LottieModifier* next)
{
/* TODO: build the decorative chaining here.
currently we only have roundness and offset. */
//roundness -> offset
if (next->type == Roundness) {
next->next = this;
return next;
//TODO: resolve the possible cyclic decoration by adding support for multiple modifiers of the same type in the RenderContext
//prevent cyclic decoration: 1) self-decoration: a->decorate(a); 2) mutual decoration: a->decorate(b); b->decorate(a);
if (next->chained || next == this) {
TVGERR("LOTTIE", "Decoration skipped to prevent cyclic chain with modifier: %p", next);
return this;
}
this->chained = true;
//just in the order.
this->next = next;
return this;
//reversed order
next->next = this;
return next;
}
};
@ -62,10 +65,9 @@ struct LottieRoundnessModifier : LottieModifier
{
static constexpr float ROUNDNESS_EPSILON = 1.0f;
RenderPath* buffer;
float r;
LottieRoundnessModifier(RenderPath* buffer, float r) : buffer(buffer), r(r)
LottieRoundnessModifier(RenderPath* buffer, float r) : LottieModifier(buffer), r(r)
{
type = Roundness;
}
@ -82,7 +84,7 @@ struct LottieOffsetModifier : LottieModifier
float miterLimit;
StrokeJoin join;
LottieOffsetModifier(float offset, float miter = 4.0f, StrokeJoin join = StrokeJoin::Round) : offset(offset), miterLimit(miter), join(join)
LottieOffsetModifier(RenderPath* buffer, float offset, float miter = 4.0f, StrokeJoin join = StrokeJoin::Round) : LottieModifier(buffer), offset(offset), miterLimit(miter), join(join)
{
type = Offset;
}