A loop returns to its starting state after a fixed duration. A convincing loop also preserves the direction and speed of motion at that boundary.
Time belongs to the composition
In a code layer, motion.time is the current timeline time in seconds and motion.duration is the composition duration in seconds. Read these values inside the frame callback.
Calculating a frame directly from its time makes the result reproducible when you scrub the timeline. Incrementing a position on every callback instead can make the result depend on which frames ran previously.
Turn time into a phase
For one turn over the whole composition, convert time into an angle:
const phase = (motion.time / Math.max(0.001, motion.duration)) * Math.PI * 2;
At the start, the phase is zero. One duration later, it is a full turn. Sine and cosine return to the same values after a full turn, making them useful for repeating motion.
Multiplying the phase by a whole number creates that many cycles per composition. A fractional multiplier may leave the animation at a different state when playback restarts.
Position is only part of the loop
Check every changing property: size, rotation, opacity, color, and effects. A looping position cannot hide a sudden change in another property. Layer clips must also remain visible for the intended interval.
The looping animation tutorial combines rotation with two cycles of breathing motion. Both return to their initial state together.
Preview and export boundaries
A rendered video contains discrete frames. The final frame normally occurs one frame interval before the next cycle, rather than duplicating the first frame. The transition from that final frame back to the first should look like an ordinary frame step.
Whether the downloaded file repeats automatically depends on the player. Enable repeat when evaluating a loop. See Export an animation for the rendering workflow.