advanceTime decides completion with:
if (!this.loop || this.playCount === this.loop) {
Because the comparison is exact equality, any state where playCount is already above loop can never complete, and the animation loops forever.
The counter overshoots through the public API alone: with loop: true, every pass runs this.playCount += 1 unbounded. Calling setLoop(2) after four passes then asks a counter sitting at 4 to equal 2:
const anim = lottie.loadAnimation({
container,
renderer: "svg",
autoplay: true,
loop: true,
animationData: shortAnimation,
});
// after four loopComplete events:
anim.setLoop(2);
// never completes: playCount is 4, and only playCount === 2 would stop it
The reverse path has the same shape with the mirrored guard (this.playCount-- counts down and the check is against 0), so a counter pushed past either bound is permanently unstoppable.
Comparing with >= in the forward branch (and the symmetric fix in the reverse one) makes an overshot counter complete at the next boundary instead.
Related: #3214, where an untyped setLoop value reaches the same comparison.
advanceTimedecides completion with:Because the comparison is exact equality, any state where
playCountis already aboveloopcan never complete, and the animation loops forever.The counter overshoots through the public API alone: with
loop: true, every pass runsthis.playCount += 1unbounded. CallingsetLoop(2)after four passes then asks a counter sitting at 4 to equal 2:The reverse path has the same shape with the mirrored guard (
this.playCount--counts down and the check is against 0), so a counter pushed past either bound is permanently unstoppable.Comparing with
>=in the forward branch (and the symmetric fix in the reverse one) makes an overshot counter complete at the next boundary instead.Related: #3214, where an untyped
setLoopvalue reaches the same comparison.