from manim import *
class Wheel(VGroup):
"""A class representing a rolling circle that rolls without sliding along a straight line or around another circle, with markers tracing cycloids as it moves. This allows for the simulation of rolling motion and the generation of cycloidal curves for various configurations."""
def __init__(
self,
radius: float = 1.0,
color: ManimColor = BLUE,
markers: list | None = None,
point=ORIGIN,
num_dashes=None,
angle=None,
**kwargs
):
super().__init__(**kwargs)
self.point = point
self.radius = radius
if num_dashes is None:
num_dashes = int(14 * radius)
self.circle = DashedVMobject(
Circle(
arc_center=self.point, radius=self.radius, stroke_width=5, color=color
),
num_dashes=num_dashes,
)
if angle is not None:
self.circle.rotate(angle)
self.dot = Dot(point=self.point, radius=0.08, color=color)
self.markers = VGroup()
if markers is not None:
for marker in markers:
new_marker = self._get_marker(*marker)
self.markers.add(new_marker)
self.add(self.circle, self.markers, self.dot)
def _get_point(self, r, theta):
"""Gets polar to cartesian coordinates around the center point."""
x = r * np.cos(theta)
y = r * np.sin(theta)
self.point = self.dot.get_center()
point = self.point + np.array([x, y, 0])
return point
def _get_arc_angle(self, point1, point2):
"""Gets path_arc for Transform animation in transform_markers."""
a = (point1.angle - point2.angle) % TAU
b = (point2.angle - point1.angle) % TAU
arc_angle = -a if a < b else b
return arc_angle
def _get_marker(self, r, theta, color=RED, line=True):
"""Gets marker around the center point."""
marker = VGroup()
marker.distance = r * self.radius
marker.angle = theta
point = self._get_point(marker.distance, marker.angle)
marker.dot = Dot(point, radius=0.09, color=color)
if line is not None:
if isinstance(line, ManimColor):
line_color = line
else:
line_color = average_color(self.dot.color, color)
marker.line = Line(
self.dot.get_center(), point, stroke_width=5, stroke_color=line_color
)
marker.add(marker.line)
else:
marker.line = None
marker.add(marker.dot)
return marker
def move(self, point, direction=ORIGIN):
"""Moves to the given point or Mobject."""
if isinstance(point, Mobject):
point = point.get_center()
self.point = self.dot.get_center()
target = point - self.point + self.radius * direction
self.point = point
self.shift(target)
return self
def draw_markers(self, markers, **kwargs):
"""Grows markers from the center point."""
self.point = self.dot.get_center()
new_markers = VGroup()
for marker in markers:
new_marker = self._get_marker(*marker)
new_markers.add(new_marker)
self.markers.add(new_marker)
self.dot.set_z_index(1)
anim = GrowFromPoint(new_markers, self.point, **kwargs)
return anim
def transform_markers(self, targets, idx=None, **kwargs):
"""Transforms markers into target markers."""
if idx is None:
markers = VGroup(*self.markers)
else:
markers = VGroup(*[self.markers[i] for i in idx])
anim = []
for marker, target in zip(markers, targets):
new_marker = self._get_marker(*target)
path_arc = self._get_arc_angle(marker, new_marker)
marker.distance = new_marker.distance
marker.angle = new_marker.angle
anim.append(Transform(marker, new_marker, path_arc=path_arc, **kwargs))
return anim
def undraw_markers(self, idx=None, **kwargs):
"""Shrinks markers into the center point."""
self.point = self.dot.get_center()
if idx is None:
markers = VGroup(*self.markers)
else:
markers = VGroup(*[self.markers[i] for i in idx])
self.markers.remove(*markers)
self.dot.set_z_index(1)
anim = GrowFromPoint(
markers, self.point, reverse_rate_function=True, remover=True, **kwargs
)
return anim
def trace_paths(self, idx=None, stroke_width=4, **kwargs):
"""Traces the path of markers."""
if idx is None:
markers = VGroup(*self.markers)
else:
markers = VGroup(*[self.markers[i] for i in idx])
paths = VGroup()
for marker in markers:
paths.add(
TracedPath(
marker.dot.get_center,
stroke_color=marker.dot.color,
stroke_width=stroke_width,
**kwargs
)
)
return paths
def roll(
self,
direction,
about=None,
reverse=False,
rate_func=linear,
run_time=2,
**kwargs
):
"""Rolls without sliding along a straight line or around another circle in the same plane."""
self.point = self.dot.get_center()
self.circle.angle = 0
for marker in self.markers:
marker.theta = marker.angle
if about is None:
distance = np.linalg.norm(direction)
if any(direction > ORIGIN):
distance *= -1
else:
if isinstance(about, Mobject):
length = about.width / 2
dis = self.point - about.get_center()
else:
length = 0
dis = self.point - about
radius = np.linalg.norm(dis)
theta = angle_of_vector(dis)
distance = radius * direction
if radius < length:
distance *= -1
if reverse:
distance *= -1
def update_alpha(self, alpha):
angle = (alpha * distance) / self.radius
if about is None:
point1 = self.point + alpha * direction
else:
point1_angle = alpha * direction + theta
point1 = ORIGIN + (
np.cos(point1_angle) * radius,
np.sin(point1_angle) * radius,
0.0,
)
self.circle.rotate(angle - self.circle.angle).move_to(point1)
self.circle.angle = angle
self.dot.move_to(point1)
for marker in self.markers:
point2_angle = angle + marker.theta
marker.angle = point2_angle
point2 = point1 + (
np.cos(point2_angle) * marker.distance,
np.sin(point2_angle) * marker.distance,
0.0,
)
if marker.line:
marker.line.set_points_by_ends(point1, point2)
marker.dot.move_to(point2)
anim = UpdateFromAlphaFunc(
self, update_alpha, rate_func=rate_func, run_time=run_time, **kwargs
)
return anim