Completion compares the pass counter with strict equality, so an overshot counter loops forever
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 itThe 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.
Source: airbnb/lottie-web