Wireframe

asciimatics – 텍스트 기반 애니메이션 라이브러리

간단한 UI를 가진 앱을 파이썬으로 구현하는 가장 기본적인 방법은 내장되어 있는 라이브러리인 Tkinter를 사용하는 것일 것입니다. 그 외에도 wxPython이나 PyQt, PySide 같은 것들이 있습니다. 하지만 정말 단순한 UI를 구현하기만 하면 되는 상황이라면 꼭 GUI일 필요는 없을 수도 있습니다. 터미널 상에서 텍스트로 UI를 표현할 수 있는 라이브러리도 있죠. TUI 라고하는 이런 텍스트 기반 UI를 구현할 때 가장 널리 쓰이는 라이브러리로는 prompt toolkit이 있습니다. Prompt toolkit은 원래 Unix의 명령줄 제어 라이브러리인 readline의 기능을 파이썬으로 구현한 것으로, 탭 키를 입력하여 입력 중인 명령이나 파일 경로를 자동완성하거나 이전 히스토리를 네비게이션하여 다시 입력하는 기능 등을 간단하게 사용할 수 있게 해줍니다.

prompt toolkit에는 이러한 명령 프롬프트 제어 외에도 vim의 window나 buffer와 비슷한 개념이 구현되어 있어서 간단한 텍스트 편집기 같은 것을 만들 수도 있습니다. (실제로 prompt toolkit 제작자는 prompt toolkit을 사용하여 PyVim 이라는 프로젝트도 만들었죠.) 하지만 prompt toolkit으로는 편집기가 아닌 다른 종류의 TUI 애플리케이션을 만드는데는 아직까지 부족한 점이 많아서, 여전히 “간단한” UI를 가진 텍스트 기반 앱을 만드는 데는 한계가 있습니다.

그러다가 asciimatics라는 라이브러리를 알게 되었습니다. 이 라이브러리는 터미널 창을 캔버스로하여 문자를 사용하여 도형을 그리고, 애니메이션 효과를 붙여서 TUI 상에서 애니메이션을 구현하게 해주는 라이브러리입니다. 이걸 사용하면 여러가지 재미있는 걸 만들 수 있는데, “문자 기반 그림 그리기”라는 측면을 UI에 접목하여 버튼이나 텍스트 필드 같은 기본적인 폼(form) 요소들을 위젯 형태로 사용할 수 있도록 제공해주고 있습니다. 기본적인 요소들만 제공된다고는 하나, 풀스크린 TUI 앱을 구현하는데 필요한 기본 빌딩 블럭들을 웬만큼 제공하기 때문에 이 정도면 충분히 쓸만하지 않은가 라고 생각이 듭니다.

asciimatics를 사용하여 TUI를 구현하는 방법에 앞서, asciimatics가 애니메이션을 구성하는 구조에 대해서 간단하게 이해를 하면 좋을 것 같습니다. 이 구조 위에 UI를 구성하는 몇 가지 장치를 사용하여 풀스크린 TUI앱을 만들 수 있기 때문입니다. 특히 asciimatic은 실제 애니메이션 영화를 만들 때 적용하는 스토리보드 기법을 가져와서 그 컨셉을 그대로 차용하고 있습니다. 그래서 애니메이션을 구성하는 구성요소들에는 어떤 것들이 있고, 이것들이 어떻게 결합되는지를 먼저 살펴보겠습니다.

스크린, 장면 그리고 이펙트

스크린은 애니메이션이 상영되는 화면이고, asciimatics의 세계에서는 터미널 앱의 창 그 자체를 말합니다. 상영 시간의 길이에 상관 없이 하나의 애니메이션 영화는 최소 1개 이상의 장면들이 하나의 스크린에서 차례로 재생되며 만들어집니다. 이와 비슷하게 스크린은 화면을 나타내는 객체이고, 여기에 1개 이상의 ‘장면(Scene)’들을 재생할 수 있습니다.

그리고 이 ‘장면’들은 Scene 클래스의 인스턴스인데, 어떤 모티프들이 표현되고, 움직이는 무대가 됩니다. 무대 위에 올려진 캐릭터나 배경들은 저마다 각자 움직이는 효과를 갖습니다. 이것을 asciimatics에서는 ‘이펙트’라고 정의합니다. 애니메이션을 제작할 때에도 배경과 캐릭터는 별도의 레이어가 그리고, 나중에 이를 겹쳐서 촬영합니다. ‘이펙트’는 “효과”라는 단어 자체의 의미보다는 레이어의 개념으로 받아들이면 됩니다. 셀 애니메이션의 그림 한 장, 한 장과 다른 점은 ‘이펙트’ 자체가 애니메이션 효과를 포함하고 있다는 의미입니다.

스크릔

다시 스크린으로 돌아오면 스크린은 애니메이션을 재생하는 역할을 담당하기도 하지만, 화면을 제어하는 기능을 포함합니다. print_at() 메소드를 사용하면 원하는 문구를 특정한 좌표에 출력할 수 있습니다. 아래 코드는 임의의 문구를 화면 중앙에 빨간 글씨로 표시하는 예를 보여줍니다. Screen 의 정적 메소드인 wrapper(func) 를 사용하여 스크린을 제어하는 함수를 통해 화면에 특정한 텍스트 기반의 모티프를 그리는 것이 가능합니다.

from asciimatics.screen import Screen
from time import sleep

def demo(screen: Screen):
  s = "ASCIIMATICS!!!"
  x = (screen.width - len(s)) // 2
  y = screen.height // 2
  screen.print_at(s, x, y, screen.COLOUR_RED)
  screen.refresh()
  sleep(3)

Screen.wrapper(demo)

화면을 사용하는 다른 방법으로는 컨텍스트 매니저인 ManagedScreen 을 사용하는 방법이 있습니다. 이러면 별도의 함수를 미리 작성하지 않아도 with 블럭 내에서 화면을 제어할 수 있습니다. 블록에서 스크린을 제어하는지, 함수에서 제어하는지으 차이만 있을 뿐이지 두 개의 방식은 큰 차이가 없습니다.

from asciimatics import ManagedScreen

with ManagedScreen() as screen:
  s = "ASCIIMATICS!!!"
  x = (screen.width - len(s)) // 2
  y = screen.height // 2
  screen.print_at(s, x, y, screen.COLOUR_RED)
  screen.refresh()
  sleep(3)

화면에 무언가를 그리는 방법으로는 screen.move(), screen.draw()가 가장 기본적입니다. 이 메소드를 사용하면 커서를 특정 위치로 이동만 시키거나, 이동한 궤적을 따라 선을 그린 것 같은 글자를 해당 위치에 찍게 됩니다. 이 때 흥미로운 것은 단순히 색을 칠한 네모상자 모양 문자를 사용하여 벽돌 그림 같은 것을 그리는 것이 아니라, 선의 기울기에 따라 슬래시나 따옴표 같은 특정 문자의 모양을 활용하여 안티 앨리어싱이 적용된 선을 생성합니다. (물론 draw() 메소드의 옵션에 따라 고정된 문자를 사용할 수 있음) 안티앨리어싱된 선을 구성하는 문자를 찾기 위해서 브레센햄 알고리듬을 사용한다고 합니다. 그 외에도 fill_polygon() 과 같이 속이 채워진 다각형을 그릴 수 있는 기능도 제공을 합니다. 아래의 코드를 사용하여, 간단히 별 모양을 화면에 그려볼 수 있습니다.

from asciimatics.screen import ManagedScreen
from time import sleep

with ManagedScreen() as screen:
    screen.move(36, 2)
    screen.draw(18, 28)
    screen.draw(64, 11)
    screen.draw(6, 11)
    screen.draw(50, 28)
    screen.draw(36, 2)
    screen.refresh()
    sleep(13)

이펙트와 장면 사용하기

어떤 캐릭터를 그려서 이동시키는 등의 애니메이션도 가능하겠지만, 기본적으로 asciimatics에는 미리 정의되어 있는 이펙트들이 있습니다. 간단한 몇 가지 예제를 통해서 어떻게 애니메이션을 만드는지 살펴보겠습니다.

눈이 내리는 효과

‘이펙트’들은 asciimatics.effects 라는 서브 패키지에 들어있습니다. 예를 들어 Snow 이펙트는 눈이 내리는 애니메이션 효과를 구현해줍니다. 별다른 파라미터 설정이 없어도 작동이 가능하므로 예제로 만들어보겠습니다. 참고로 보든 이펙트는 생성할 때, 자신이 상영될 스크린 객체를 인자로 받게 됩니다.

# snowing.py

from asciimatics.screen import Screen
from asciimatics.scene import Scene
from asciimatics.effects import Snow

def demo(screen: Screen):
  screen.play([             # screen.play 는 Scene의 목록을 재생함
    Scene([                 # Scene을 생성할 때에는 재생할 Effect들을 포함함
           Snow(screen)     # screen에 눈이 내리는 효과
          ], duration=60*20),
  ])

Screen.wrapper(demo)

화면에 애니메이션을 재생하는 명령은 screen.play() 입니다. 스크린은 여러 개의 장면을 순차적으로 재생하므로, 장면의 목록을 인자로 넘겨주면 됩니다.

각각의 장면은 이펙트의 목록을 가지고 만들어진다고 했습니다. Snow(screen)은 현재 화면에 눈이 내리는 애니메이션 효과를 생성합니다. 이것을 목록에 넣어서 장면을 만들고, 다시 이 장면을 목록에 담아서 play() 에 전달하면 됩니다. 파일을 실행하면 콘솔 화면에 1분간 눈이 오게 됩니다. duration=은 장면의 재생 시간으로 단위는 프레임입니다. asciimatics는 내부적으로 초당 20프레임을 목표로 하기 때문에 20 * 60은 1분입니다. 1분이 지나면 애니메이션이 종료되고 자동으로 재시작합니다. 사실 이를 알아차리기는 쉽지 않지만, 이 예제에서는 놀랍게도 내린 눈이 바닥에 쌓이기 때문에 알아차릴 수 있습니다.

별이 반짝이는 효과

Stars()는 별이 반짝이는 효과입니다. 별의 개수를 지정할 수 있습니다.


from asciimatics.scene import Scene
from asciimatics.screen import Screen
from asciimatics.effects import Stars

def demo(screen: Screen):
    stars = Stars(screen, 20)
    screen.play([
        Scene([stars], duration=20*10),
        ])

Screen.wrapper(demo)

Clock() 이펙트는 현재 시간을 표현하는 아날로그 시계를 화면에 그립니다. 시계를 그리기 위해서는 중심 위치와 반경을 필요로 합니다. 그런데, 하나의 장면에는 여러 레이어처럼 여러 개의 이펙트가 들어갈 수 있습니다. 눈이 내리거나 별이 반짝이는 배경을 바탕으로 시계를 그릴 수도 있다는 이야기죠.

from asciimatics.scene import Scene
from asciimatics.screen import Screen
from asciimatics.effects import Stars, Clock

def demo(screen: Screen):
    stars = Stars(screen, 20)
    screen.play([
        Scene([stars,
               Clock(screen, screen.width // 3, screen.height // 3,
                     screen.width // 4)
              ], duration=20*10),
        
        ])

Screen.wrapper(demo)

Renderer

지금까지 소개한 이펙트들은 자체적으로 그리고자 하는 소재들을 가지고 있는 것들이었습니다. 이미 그려진 것을 가져와서 애니메이션 효과만 부여해주는 것이 실질적으로 이펙트들이 필요한 이유입니다. “그려진 것”을 요구하는 이펙트들에게 그림을 제공해주는 객체들을 ‘렌더러’라고 합니다. 렌더러는 그림을 아스키코드로 변환한다거나, 어떤 텍스트를 아스키아트로 자동으로 구현하는 것과 비슷한 것을 말합니다. 즉 어떤 그려야 하는 대상을 아스키아트처럼 만들어주는 일을 합니다.

모든 렌더러는 공통적으로 rendered_text 라는 속성을 가지고 있고, 이 속성값이 렌더링 결과입니다.

간단한 예제를 하나 보겠습니다. 먼저 FigletText는 문자열을 특정한 폰트를 적용하여 아스키아트로 만들어서 렌더링하는 렌더러입니다. 이는 어떤 단어를 아스키아트로 바꿔줍니다. Rainbow 라는 렌더러는 조금 특이한데, 다른 렌더러가 만들어놓은 결과에 무지개 색상을 적용해줍니다. 대략 아래와 같은 모양을 만들게 됩니다.

from asciimatics.effects import Print
from asciimatics.renderers import FigletText, Rainbow
from asciimatics.scene import Scene
from asciimatics.screen import Screen

def demo(screen: Screen):
    x = "ASCIIMATICS"
    text = Rainbow(screen, FigletText(x, font="basic"))
    screen.play([
            Scene([
                Print(
                    screen,
                    text,
                    (screen.height - text.max_height) // 2,
                    (screen.width - text.max_width) // 2,
                ),
            ])
    ])


if __name__ == "__main__":
    Screen.wrapper(demo)

그렇다면 이러한 애니메이션 기법과 TUI는 어떤 관련이 있다고 계속 애니메이션에 대한 내용만 소개했을까요? UI를 구현할 때 사용하는 요소들이 이러한 애니메이션을 구현하는데 사용하는 요소들의 약간의 변형입니다. 앱의 UI에서 화면 전환이 필요하다면, 이는 애니메이션의 장면의 전환으로 구현이 됩니다. 화면의 창을 표현하는 `프레임’은 ‘이펙트’의 한 종류입니다. 그래서 하나의 화면에 여러 창이 들어갈 수 있습니다. (본 화면에 팝업이나 대화상자가 추가되는 방식과 일치합니다.) 그 외에 UI를 구성하기 위해서는 창(=프레임)내에 버튼이나 텍스트 필드와 같은 UI 요소들을 배치하기 위한 레이아웃을 구성하는 과정이 필요할 수도 있습니다.

그러면 다음 글에서 UI를 구성하는 방법에 대해서 간단히 살펴보겠습니다.

Exit mobile version