본문 바로가기

Programming/Manim Project

계속해서 흘러가는 사인 파형 만들기

반응형

만들려는 것은, 원둘레까지의 막대를 이용해서, 사인파가 계속해서 그려지면서 왼쪽 편으로 흐르듯이 진행되는 애니메이션이다.

 

 

그냥 0 ~ 2 $\pi$까지 등 일정 범위에 대해 사인 파형을 그리는 것은 여기 코드를 참조.

 

원 막대 없이 그냥 사인파가 흐르는 애니메이션은 여기 참조.

 

기본 구상

먼저 일정 구간에 대한 사인파를 그리고, 그 사인파의 끝 지점을 원 막대에서 연장된 라인이 가리키게 한 후, 막대가 회전함에 따라 사인파를 계속 업데이트해서 마치 흐르는 것처럼 만든다.

 

 

코딩

1. 축 라인과 원을 그린다.

가로축과 세로축, 그리고 원이 놓이게 되는 좌표를 구상하자.

 

 

위 그림을 코드로 짠다.

클래스 이름은 LongSineCurve로 하자. 

class LongSineCurve(Scene):
	def construct(self):
        self.show_axis()
        self.show_circle_dot()
       
        self.wait()

    def show_axis(self):
        self.x_start = np.array([-6,0,0])
        x_axis = Line(self.x_start, np.array([6, 0, 0]))
        y_axis = Line(np.array([-4, -2, 0]), np.array([-4, 2, 0]))

        self.add(x_axis, y_axis)

        self.origin_point = np.array([-4, 0, 0])
        self.curve_start = np.array([-3, 0, 0])

        self.one_cycle_length = 2 * PI
        self.curve_end = np.array([self.curve_start[0] + self.one_cycle_length, 0, 0])

    def show_circle_dot(self):
        circle = Circle(radius=1)
        circle.move_to(self.origin_point)

        dot = Dot(radius=0.08, color=YELLOW)
        dot.move_to(circle.point_from_proportion(0))

        self.add(circle, dot)
        self.circle = circle
        self.dot = dot

 

여기까지 실행되는 모습은,

 

 

2. 점, 막대, 라인을 그리는 애니메이션 만든다.

기본 전략은,

  • 시간의 흐름에 따라 점(dot)을 원둘레로 돌게 하고: 4초에 한 바퀴
  • 점(dot)이 이동함에 따라, 원점에서 점까지의 라인이 자동 그려지게 하고, 또한 점에서 그래프의 끝에까지의 직선이 그려지게 한다.

점을 시간의 흐름에 따라 자동으로 이동하게 하는 것은, dot.add_updater(go_around_circle)에 의해 이루어지게 한다.

 

        self.t_offset=0
        rate = 0.25
        def go_around_circle(mob, dt):
            self.t_offset += dt
            # print(self.t_offset)
            mob.move_to(self.circle.point_from_proportion((self.t_offset * rate) % 1))
            
        dot.add_updater(go_around_circle)    

 

go_around_circle 함수의 파라미터에 있는 dt 값은 프레임 시간이다. 1초에 30 프레임이면 1/30이다.

따라서, self.t_offset += dt를 하게 되면 self.t_offset 변수는 실제 시간 값을 가지게 되고, 프레임이 바뀔 때마다 그 값이 경신되게 된다.

 

go_around_circle의 파라미터로 있는 mob는 add_updater를 호출한 객체 자신이되기에, mob는 dot이다. 

따라서, mob.move_to를 한 것은 dot.move_to를 한 것과 동일하다.

 

dot를 원둘레의 어떤 위치로 이동시키는데, point_from_proportion을 사용했다. 이 함수는 원 전체 둘레를 0~1까지의 값으로 보고, 주어진 값에 해당하는 원둘레 위치 값을 리턴한다. 예를 들어 circle.point_from_proportion(0.5)를 하게 되면, 원둘레의 처음 시작점에서 시작해서 절반이 되는 지점의 위치 값이 리턴된다.

 

그렇다면 self.circle.point_from_proportion((self.t_offset * rate) % 1) 은 어떤 의미일까?

 

self.t_offset은 실제 진행되는 시간에 대한 초(second) 값으로 계산되도록 해놨다. 따라서, 1초가 지났다면 self.t_offset =1이고, 여기에 rate=0.25를 곱하게 되면 0.25가 된다.

1초가 지났을 때는 원둘레의 1/4(=0.25)되는 지점을 가리키고, 2초라면 1/2 지점, 4초가 지나면 원을 한 바퀴 돌아서 출발점으로 돌아온다.  즉, 4초마다 원을 한 바퀴 돌게 하는 코드이다.

 


이제 점을 이동하게는 해 놨고, 이 점을 기준 포인트로 해서, 점의 움직임에 따라 원의 중심에서 점까지의 라인, 그리고 점에서 그래프의 끝까지의 라인을 그려보자.

 

        def get_line_to_circle():
            return Line(orgin_point, dot.get_center(), color=BLUE)

        def get_line_to_curve():
            x = self.curve_end[0]  # fixing to the end of the curve
            y = dot.get_center()[1]
            return Line(dot.get_center(), np.array([x,y,0]), color=YELLOW_A, stroke_width=2 )


	origin_to_circle_line = always_redraw(get_line_to_circle)
        dot_to_curve_line = always_redraw(get_line_to_curve)

원의 중심에서 점(dot)까지의 라인은, Line(orgin_point, dot.get_center())처럼 간단히 구현 가능.

 

점(dot)에서 사인 그래프의 끝 점까지의 라인은, 사인 그래프의 끝 점만 알아내면 되는데, 여기서는 끝 점이 되는 x 위치를 고정해 놨기에 그 고정값으로 지정하고, y 값은 점의 높이값으로 해버리면 됨 (사인 값이니깐 원 막대의 높이값에 해당)

 

그래프의 끝 점은 show_axis 함수에서 고정해 놨음

self.curve_end = np.array([self.curve_start[0] + self.one_cycle_length, 0, 0])

 

여기까지의 코드는, 

class LongSineCurve(Scene):
    def construct(self):
        self.show_axis()
        self.show_circle_dot()
        self.draw_several_cycle()

        self.wait()

    def show_axis(self):
        self.x_start = np.array([-6,0,0])
        x_axis = Line(self.x_start, np.array([6, 0, 0]))
        y_axis = Line(np.array([-4, -2, 0]), np.array([-4, 2, 0]))

        self.add(x_axis, y_axis)

        self.origin_point = np.array([-4, 0, 0])
        self.curve_start = np.array([-3, 0, 0])

        self.one_cycle_length = 2 * PI
        self.curve_end = np.array([self.curve_start[0] + self.one_cycle_length, 0, 0])

    def show_circle_dot(self):
        circle = Circle(radius=1)
        circle.move_to(self.origin_point)

        dot = Dot(radius=0.08, color=YELLOW)
        dot.move_to(circle.point_from_proportion(0))

        self.add(circle, dot)
        self.circle = circle
        self.dot = dot

    def draw_several_cycle(self):
        dot = self.dot
        orgin_point = self.origin_point

        self.t_offset = 0
        rate = 0.25
        one_cycle_time= (1 / rate) + (1/self.camera.frame_rate) * rate

        def go_around_circle(mob, dt):
            self.t_offset += dt
            # print(self.t_offset)
            mob.move_to(self.circle.point_from_proportion((self.t_offset * rate) % 1))

        def get_line_to_circle():
            return Line(orgin_point, dot.get_center(), color=BLUE)

        def get_line_to_curve():
            x = self.curve_end[0]  # fixing to the end of the curve
            y = dot.get_center()[1]
            return Line(dot.get_center(), np.array([x,y,0]), color=YELLOW_A, stroke_width=2 )

        dot.add_updater(go_around_circle)

        origin_to_circle_line = always_redraw(get_line_to_circle)
        dot_to_curve_line = always_redraw(get_line_to_curve)

        self.add(origin_to_circle_line, dot_to_curve_line)
        self.wait(one_cycle_time*4)

        dot.remove_updater(go_around_circle)

 

여기까지 코드에 대한 실행은,

 

 

3. 사인 그래프를 그리자.

먼저, 위상 차이값 dx가 주어지면, 이에 해당하는 사인 그래프를 토해내는 함수를 만든다.

즉, $\sin (x + dx)$는 $\sin x$의 그래프가 왼쪽으로 dx만큼 이동한 것

        def get_sine_curve(dx=0):
            return FunctionGraph(
                lambda x: np.sin(x-self.curve_start[0]+dx),
                x_min=self.x_start[0], x_max=self.curve_end[0],
            )

 

위 코드의 lambda 함수에서 np.sin(x+dx)가 아닌 np.sin(x-self.curve_start[x]+dx)라고 한 것은, x=self.curve_start[0]에서의 사인 그래프의 값이 0이 되도록 하게 함이다. 즉, x=self.x_start[0]=-6에서 self.curve_end[0]=-6 + 2 $ \pi$까지의 값인데, x가 self.curve_start[0]=-3에서 이 되게 하려면 np.sin(x-self.curve_start[0])이라고 해야 한다.

 

여기서 self.x_start 및 self.curve_end 등은 show_axis 함수에 정의되어 있다.

 

사인 함수가 self.curve_start[0]에서 0인 값을 가진다는 것은, 사인 그래프가 아래 그램과 같은 모양이 되길 원하는 것이다.

 

...

 

x_min=self.x_start[0]까지 한 것은, 사인 그래프의 왼쪽 끝 점을 self.x_start[0]=-6까지 한 것으로, 아래 그림과 같이, 사인 함수의 시작점보다 더 왼쪽까지 그려서, 사인 그래프의 이동되는 모습을 더 자연스럽게 보이게 하기 위함이다.

 

 

... 

 

이제, 사인 그래프가 자동으로 그려지게 하는 것은 always_redraw를 이용해서 수행되게 한다.

 

        def get_sine_curve(dx=0):
            return FunctionGraph(
                lambda x: np.sin(x-self.curve_start[0]+dx),
                x_min=self.x_start[0], x_max=self.curve_end[0],
            )

        def get_updated_sine_curve():
            return get_sine_curve(dx=self.t_offset * ( PI / 2))
       
       sine_curve = always_redraw(get_updated_sine_curve)

 

get_updated_sine_curve 함수에서는 페이즈 값 dx=self.t_offset * (PI / 2)를 가지고 get_sine_curve 함수를 호출해서 얻어낸 사인 그래프를, 계속해서 업데이트한다. 그럼으로써 사인 그래프가 왼쪽으로 이동되는 모습을 애니메이션 할 수 있다.

 

페이즈 값(위상 차 값)을 self.t_offset * (PI / 2)로 지정한 것을 생각해보자.

 

self.t_offset 값은 실제 시간을 나타내고, 사인 그래프는 4초를 한 주기가 되도록 세팅되었기에(go_around_circle 함수에서 그렇게 되도록 세팅했다), dx 값은 4초에 한 주기가 돌도록 세팅되어야 한다. 

좀 더 이해하기 쉽도록 t_offset값과 dx 값을 테이블로 나타내 보면,

 

time 0초 ... 1초 ... 2초  ... 3초   ... 4초 ...
t=t_offset 0 ... 1 ... 2 ... 3 ... 4 ...
dx 0 ... 0.25 ... 0.5 ... 0.75 ... 1 ...
x 값 0 $\frac {\pi} {2}$ $\pi$ $\frac {3 \pi} {2}$ 2 $\pi$

즉, dx 값을 비례식으로 구하면,

 

    $$4 : 2 \pi = t : x $$

    $$ x = \frac {2 \pi t} {4} = \frac {\pi t} {2}$$ 

 

...

 

이제 전체 코드가 완성되었다.

from manimlib.imports import *

class LongSineCurve(Scene):
    def construct(self):
        self.show_axis()
        self.show_circle_dot()
        self.draw_several_cycle()

        self.wait()

    def show_axis(self):
        self.x_start = np.array([-6,0,0])
        x_axis = Line(self.x_start, np.array([6, 0, 0]))
        y_axis = Line(np.array([-4, -2, 0]), np.array([-4, 2, 0]))

        self.add(x_axis, y_axis)

        self.origin_point = np.array([-4, 0, 0])
        self.curve_start = np.array([-3, 0, 0])

        self.one_cycle_length = 2 * PI
        self.curve_end = np.array([self.curve_start[0] + self.one_cycle_length, 0, 0])

    def show_circle_dot(self):
        circle = Circle(radius=1)
        circle.move_to(self.origin_point)

        dot = Dot(radius=0.08, color=YELLOW)
        dot.move_to(circle.point_from_proportion(0))

        self.add(circle, dot)
        self.circle = circle
        self.dot = dot
    def draw_several_cycle(self):
        dot = self.dot
        orgin_point = self.origin_point

        self.t_offset = 0
        rate = 0.25
        one_cycle_time= (1 / rate) + (1/self.camera.frame_rate) * rate

        def go_around_circle(mob, dt):
            self.t_offset += dt
            # print(self.t_offset)
            mob.move_to(self.circle.point_from_proportion((self.t_offset * rate) % 1))

        def get_line_to_circle():
            return Line(orgin_point, dot.get_center(), color=BLUE)

        def get_line_to_curve():
            x = self.curve_end[0]  # fixing to the end of the curve
            y = dot.get_center()[1]
            return Line(dot.get_center(), np.array([x,y,0]), color=YELLOW_A, stroke_width=2 )

        def get_sine_curve(dx=0):
            return FunctionGraph(
                lambda x: np.sin(x-self.curve_start[0]+dx),
                x_min=self.x_start[0], x_max=self.curve_end[0],
            )

        def get_updated_sine_curve():
            return get_sine_curve(dx=self.t_offset * ( PI / 2))


        dot.add_updater(go_around_circle)


        origin_to_circle_line = always_redraw(get_line_to_circle)
        dot_to_curve_line = always_redraw(get_line_to_curve)

        sine_curve = always_redraw(get_updated_sine_curve)

        self.add(origin_to_circle_line, dot_to_curve_line, sine_curve)
        self.wait(one_cycle_time*4)

        dot.remove_updater(go_around_circle)

 

실행 동영상은,

 

 

    

 

  

-끝-

반응형