def animate_trajectory(
xs,
ys,
yaws,
interval=40,
max_frames=500,
triangle_size=0.1,
arrow_len=0.2,
trail=True,
figsize=(8, 6),
):
xs = xs[:max_frames]
ys = ys[:max_frames]
yaws = yaws[:max_frames]
fig, ax = plt.subplots(figsize=figsize)
# Set fixed axis limits with padding so the view doesn't jump around
pad = 0.5
ax.set_xlim(xs.min() - pad, xs.max() + pad)
ax.set_ylim(ys.min() - pad, ys.max() + pad)
ax.set_aspect("equal")
ax.grid(True, alpha=0.3)
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_title("Animated Trajectory")
# Faint full path for reference
ax.plot(xs, ys, color="lightgray", linewidth=1, zorder=1)
# Trail (path traced so far)
(trail_line,) = ax.plot([], [], "b-", linewidth=2, zorder=2)
# Heading arrow
heading_arrow = ax.quiver(
[xs[0]],
[ys[0]],
[np.cos(yaws[0])],
[np.sin(yaws[0])],
color="red",
scale=1 / arrow_len,
scale_units="xy",
angles="xy",
width=0.006,
zorder=4,
)
# Pose triangle
ts = triangle_size
base_tri = np.array([[ts, 0], [-ts / 2, ts / 2], [-ts / 2, -ts / 2]])
def rotate_translate(pts, xc, yc, theta):
c, s = np.cos(theta), np.sin(theta)
R = np.array([[c, -s], [s, c]])
return pts @ R.T + np.array([xc, yc])
xy = rotate_translate(base_tri, xs[0], ys[0], yaws[0])
pose_patch = Polygon(xy, closed=True, color="blue", alpha=0.7, zorder=3)
ax.add_patch(pose_patch)
# Step / time text
bbox = dict(boxstyle="round", facecolor="white", alpha=0.8)
txt = ax.text(0.02, 0.98, "", transform=ax.transAxes, va="top", ha="left", bbox=bbox)
def init():
trail_line.set_data([], [])
return trail_line, heading_arrow, pose_patch, txt
def update(i):
if trail:
trail_line.set_data(xs[: i + 1], ys[: i + 1])
# Update heading arrow position + direction
heading_arrow.set_offsets(np.array([[xs[i], ys[i]]]))
heading_arrow.set_UVC(np.cos(yaws[i]), np.sin(yaws[i]))
# Update pose triangle
pose_patch.set_xy(rotate_translate(base_tri, xs[i], ys[i], yaws[i]))
txt.set_text(f"step: {i + 1}/{len(xs)}\nx={xs[i]:.2f}, y={ys[i]:.2f}, yaw={np.degrees(yaws[i]):.1f}°")
return trail_line, heading_arrow, pose_patch, txt
anim = FuncAnimation(fig, update, frames=len(xs), init_func=init, interval=interval, blit=False)
plt.close(fig) # prevents duplicate static figure in Jupyter
return anim
anim = animate_trajectory(xs, ys, yaws, interval=40)
HTML(anim.to_jshtml())
# anim.save('trajectory.mp4', fps=25) # optional: save to file (needs ffmpeg)