Created
December 31, 2024 08:32
-
-
Save OhadRubin/27a1a032c0bf538e256c263416970d03 to your computer and use it in GitHub Desktop.
animate_svg.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# pip install svgelements | |
import io | |
from svgelements import SVG, Path | |
import matplotlib.pyplot as plt | |
from matplotlib.animation import FuncAnimation | |
import numpy as np | |
def read_svg(svg_file_path): | |
try: | |
with io.open(svg_file_path, "r", encoding="utf-8") as f: | |
svg_content = f.read() | |
try: | |
svg_bytes = svg_content.encode("utf-8") | |
except UnicodeDecodeError as e: | |
return None | |
svg_obj = SVG.parse(io.BytesIO(svg_bytes)) | |
return svg_obj | |
except Exception as e: | |
return None | |
def extract_path_points(svg_data, num_points=100): | |
paths = [] | |
for element in svg_data.elements(lambda e: isinstance(e, Path)): | |
current_segment = [] | |
all_segments = [] | |
for segment in element.segments(): | |
# Start a new segment if this is a Move command | |
if segment.__class__.__name__ == 'Move': | |
if current_segment: | |
all_segments.append(np.array(current_segment)) | |
current_segment = [] | |
point = segment.end | |
current_segment.append((point.real, point.imag)) | |
else: | |
# Sample points along each segment | |
for t in np.linspace(0, 1, num_points): | |
point = segment.point(t) | |
current_segment.append((point.real, point.imag)) | |
# Add the last segment if it exists | |
if current_segment: | |
all_segments.append(np.array(current_segment)) | |
paths.extend(all_segments) | |
return paths | |
def create_animation(paths, duration=5, fps=60): | |
fig, ax = plt.subplots() | |
# Find the bounds of all paths | |
all_points = np.concatenate(paths) | |
x_min, y_min = np.min(all_points, axis=0) | |
x_max, y_max = np.max(all_points, axis=0) | |
# Set up the plot | |
ax.set_xlim(x_min - 10, x_max + 10) | |
ax.set_ylim(y_max + 10, y_min - 10) | |
ax.set_aspect('equal') | |
# Create line objects for each path segment | |
lines = [ax.plot([], [], 'k-', linewidth=2)[0] for _ in paths] | |
def init(): | |
for line in lines: | |
line.set_data([], []) | |
return lines | |
def animate(frame): | |
progress = frame / (fps * duration) | |
total_points = sum(len(path) for path in paths) | |
points_progress = int(total_points * progress) | |
points_counted = 0 | |
for line, path in zip(lines, paths): | |
if points_counted + len(path) <= points_progress: | |
# Show full segment | |
line.set_data(path[:, 0], path[:, 1]) | |
points_counted += len(path) | |
elif points_counted < points_progress: | |
# Show partial segment | |
points_to_show = points_progress - points_counted | |
line.set_data(path[:points_to_show, 0], path[:points_to_show, 1]) | |
points_counted += len(path) | |
else: | |
# Hide segment | |
line.set_data([], []) | |
return lines | |
anim = FuncAnimation(fig, animate, | |
init_func=init, | |
frames=fps * duration, | |
interval=1000/fps, | |
blit=True) | |
return fig, anim | |
import fire | |
# usage: python3 animate_svg.py --svg_file=/Users/ohadr/Downloads/03/smoothed_image_pig.svg | |
# usage: python3 animate_svg.py --svg_file=/Users/ohadr/Downloads/03/smoothed_image_holding.svg | |
def main(svg_file: str): | |
""" | |
Create an animation from an SVG file. | |
Args: | |
svg_file: Path to the SVG file to animate | |
""" | |
svg_data = read_svg(svg_file) | |
if svg_data: | |
# Extract points from the SVG paths | |
paths = extract_path_points(svg_data) | |
# Create and display the animation | |
fig, anim = create_animation(paths) | |
# Save the animation | |
svg_file_name = svg_file.replace(".svg", ".gif") | |
anim.save(svg_file_name, writer='pillow', fps=30) | |
# Display the animation | |
# plt.show() | |
else: | |
print("Failed to load SVG file") | |
if __name__ == "__main__": | |
fire.Fire(main) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment