mscene.rouletteΒΆ

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