""" Animation module for visualizing bouncing ball data. """ import numpy as np import matplotlib.pyplot as plt import matplotlib.animation as animation from matplotlib.patches import Circle import pandas as pd from IPython.display import HTML def create_ball_animation(df, ball_radius=0.5, fps=30, duration=None, title="Bouncing Ball Animation"): """ Create an animation of a bouncing ball from position data. Parameters: df (pandas.DataFrame): DataFrame with 'Time' and 'Position' columns ball_radius (float): Radius of the ball in inches fps (int): Frames per second for the animation duration (float): Duration of the animation in seconds (if None, uses the full data) title (str): Title for the animation Returns: matplotlib.animation.Animation: The animation object """ # Extract time and position data time_data = df['Time'].values position_data = df['Position'].values # Determine animation duration if duration is None: duration = time_data[-1] # Calculate the number of frames based on fps and duration num_frames = int(fps * duration) # Create figure and axis fig, ax = plt.subplots(figsize=(10, 6)) # Set axis limits max_height = np.max(position_data) + 2 * ball_radius ax.set_xlim(-5, 5) ax.set_ylim(0, max_height) # Set labels and title ax.set_xlabel('X Position (inches)') ax.set_ylabel('Y Position (inches)') ax.set_title(title) # Add a horizontal line at y=0 to represent the ground ax.axhline(y=0, color='black', linestyle='-', linewidth=2) # Create the ball (circle patch) ball = Circle((0, position_data[0]), ball_radius, color='red', zorder=2) ax.add_patch(ball) # Create a line to show the ball's path line, = ax.plot([], [], 'b-', alpha=0.3, linewidth=1) # Initialize empty lists for the path path_x = [] path_y = [] def init(): """Initialize the animation""" ball.center = (0, position_data[0]) line.set_data([], []) return ball, line def animate(i): """Update the animation for frame i""" # Calculate the current time based on the frame number current_time = i * duration / num_frames # Find the closest time index in our data idx = np.abs(time_data - current_time).argmin() # Update ball position y_pos = position_data[idx] ball.center = (0, y_pos) # Update path path_x.append(0) path_y.append(y_pos) line.set_data(path_x, path_y) return ball, line # Create the animation anim = animation.FuncAnimation( fig, animate, init_func=init, frames=num_frames, interval=1000/fps, blit=True ) plt.close() # Prevent the static plot from displaying return anim def create_ball_animation_with_model(df, peak_indices, cor, initial_height, ball_radius=0.5, fps=30, duration=None, title="Bouncing Ball Animation with Model", g=386.09): """ Create an animation of a bouncing ball from position data with an ideal model comparison. Parameters: df (pandas.DataFrame): DataFrame with 'Time' and 'Position' columns peak_indices (numpy.ndarray): Indices of detected bounces cor (float): Coefficient of Restitution initial_height (float): Initial height of the ball in inches ball_radius (float): Radius of the ball in inches fps (int): Frames per second for the animation duration (float): Duration of the animation in seconds (if None, uses the full data) title (str): Title for the animation g (float): Acceleration due to gravity (in inches/s²) Returns: matplotlib.animation.Animation: The animation object """ # Extract time and position data time_data = df['Time'].values position_data = df['Position'].values # Determine animation duration if duration is None: duration = time_data[-1] # Calculate the number of frames based on fps and duration num_frames = int(fps * duration) # Create figure and axis fig, ax = plt.subplots(figsize=(10, 6)) # Set axis limits max_height = np.max(position_data) + 2 * ball_radius ax.set_xlim(-5, 5) ax.set_ylim(0, max_height) # Set labels and title ax.set_xlabel('X Position (inches)') ax.set_ylabel('Y Position (inches)') ax.set_title(title) # Add a horizontal line at y=0 to represent the ground ax.axhline(y=0, color='black', linestyle='-', linewidth=2) # Create the real data ball (circle patch) real_ball = Circle((0, position_data[0]), ball_radius, color='red', zorder=2, label='Actual') ax.add_patch(real_ball) # Create the model ball (circle patch) model_ball = Circle((2, initial_height), ball_radius, color='blue', zorder=2, alpha=0.7, label='Model') ax.add_patch(model_ball) # Create lines to show the balls' paths real_line, = ax.plot([], [], 'r-', alpha=0.3, linewidth=1) model_line, = ax.plot([], [], 'b-', alpha=0.3, linewidth=1) # Add legend ax.legend(handles=[ plt.Line2D([0], [0], marker='o', color='w', markerfacecolor='red', markersize=10, label='Actual'), plt.Line2D([0], [0], marker='o', color='w', markerfacecolor='blue', markersize=10, label='Model') ]) # Initialize empty lists for the paths real_path_x = [] real_path_y = [] model_path_x = [] model_path_y = [] # Generate model data dt = 0.001 # Time step for simulation model_time = [] model_position = [] # Initialize simulation parameters y = initial_height v_y = 0.0 # Starting from rest t = 0.0 # Run simulation for the duration while t <= duration: model_time.append(t) model_position.append(y) # Kinematic update v_y -= g * dt y += v_y * dt t += dt # Bounce condition if y <= 0: y = 0 # Ground impact v_y = -v_y * cor # Apply COR # Convert to numpy arrays for faster indexing model_time = np.array(model_time) model_position = np.array(model_position) def init(): """Initialize the animation""" real_ball.center = (0, position_data[0]) model_ball.center = (2, initial_height) real_line.set_data([], []) model_line.set_data([], []) return real_ball, model_ball, real_line, model_line def animate(i): """Update the animation for frame i""" # Calculate the current time based on the frame number current_time = i * duration / num_frames # Find the closest time index in our data real_idx = np.abs(time_data - current_time).argmin() model_idx = np.abs(model_time - current_time).argmin() # Update ball positions real_y_pos = position_data[real_idx] model_y_pos = model_position[model_idx] real_ball.center = (0, real_y_pos) model_ball.center = (2, model_y_pos) # Update paths real_path_x.append(0) real_path_y.append(real_y_pos) model_path_x.append(2) model_path_y.append(model_y_pos) real_line.set_data(real_path_x, real_path_y) model_line.set_data(model_path_x, model_path_y) return real_ball, model_ball, real_line, model_line # Create the animation anim = animation.FuncAnimation( fig, animate, init_func=init, frames=num_frames, interval=1000/fps, blit=True ) plt.close() # Prevent the static plot from displaying return anim def display_animation(anim, html_video=True): """ Display an animation in a Jupyter notebook. Parameters: anim (matplotlib.animation.Animation): The animation to display html_video (bool): If True, convert to HTML5 video, otherwise use JavaScript Returns: IPython.display.HTML: The HTML object containing the animation """ if html_video: # Convert to HTML5 video html = anim.to_html5_video() return HTML(html) else: # Use JavaScript animation return HTML(anim.to_jshtml()) def save_animation(anim, filename, fps=30, dpi=100): """ Save an animation to a file. Parameters: anim (matplotlib.animation.Animation): The animation to save filename (str): The filename to save to (should end with .mp4, .gif, etc.) fps (int): Frames per second dpi (int): Resolution in dots per inch """ # Determine the writer based on the file extension if filename.endswith('.mp4'): writer = animation.FFMpegWriter(fps=fps) elif filename.endswith('.gif'): writer = animation.PillowWriter(fps=fps) else: raise ValueError("Unsupported file format. Use .mp4 or .gif") # Save the animation anim.save(filename, writer=writer, dpi=dpi)