작은 픽셀 캐릭터를 화면에 띄우는 것에서 BytePet을 시작했다. 고양이를 끌어 옮기고, 다시 실행해도 같은 자리에서 만나고, 보이지 않는 여백에서는 뒤쪽 앱을 클릭할 수 있게 만든 첫 과정을 기록한다.

작업하는 화면 한쪽에 작은 캐릭터를 놓아두고 싶었다. 가끔 눈을 깜빡이거나 꼬리를 움직이고, 자리가 불편하면 잡아서 옮길 수 있는 정도면 시작하기에 충분했다.
그렇게 만들기 시작한 앱이 BytePet이다. 지금은 캐릭터 옆에 집 모양 메뉴가 있고, 메모와 사진을 먹이처럼 보관하거나 개발 도구와 사용량을 확인할 수 있다. 고양이와 강아지 중 하나를 고를 수도 있고, 직접 준비한 캐릭터를 가져올 수도 있다.
하지만 첫 단계는 훨씬 작았다. 바탕화면에 고양이 한 마리를 띄우고, 다른 작업을 하면서도 그 고양이를 곁에 둘 수 있게 만드는 일이었다. 그림을 띄운 다음에는 창의 위치와 마우스 입력을 다루는 작업이 이어졌다.
처음에는 작은 투명 창이었다
첫 구현은 Electron으로 시작했다. HTML과 Canvas로 캐릭터를 그리고, 데스크톱 창의 위치와 입력 처리는 Electron에서 맡았다. 현재 앱은 Tauri로 옮겼지만, 이 글에서 다루는 것은 그 이전의 초기 구현이다.
보통 앱을 실행하면 제목 표시줄과 창 테두리가 보인다. 캐릭터 옆에 그런 틀이 남아 있으면 작은 펫보다는 그림이 들어 있는 창처럼 보일 것 같았다. 그래서 배경을 투명하게 하고, 테두리와 그림자를 없앴다.
창의 크기는 128 × 128px로 잡고, 그 가운데에 96 × 96px로 표시되는 고양이를 놓았다. 처음 실행할 때는 화면의 작업 영역을 기준으로 오른쪽 아래에 자리 잡도록 했다. 앱이 열리는 순간 다른 창의 입력을 빼앗지 않도록, 준비된 창을 포커스 없이 보여주는 방식도 사용했다.
겉으로는 고양이만 떠 있지만, 실제로는 투명한 사각형 창이 그 주변을 감싸고 있다. 이 차이는 마우스를 연결하는 단계에서 중요해졌다.
눈을 깜빡이고 꼬리를 움직이기
처음의 고양이는 이미지 파일이 아니었다. 24 × 24칸의 작은 캔버스에 색을 하나씩 채워 그렸다. 머리와 몸통은 문자로 적은 도트 배열을 색으로 바꾸고, 눈과 꼬리는 시간에 따라 위치나 모양을 조금씩 달리했다.
가만히 있는 동작에도 몇 가지 변화가 들어갔다. 숨을 쉬는 구간에서는 머리를 한 칸 올리고, 눈을 깜빡일 때는 눈의 높이를 줄였다. 꼬리 끝도 몇 개의 위치를 오갔다. 발은 같은 자리에 두어, 고양이 전체가 위아래로 떠다니는 느낌을 줄이려 했다.

캔버스를 화면에 크게 표시할 때는 픽셀이 흐려지지 않게 했다. 작은 원본을 부드럽게 확대하면 도트의 경계까지 섞이기 때문에, 각 칸의 형태가 남도록 표시하는 편이 어울렸다.
이 정도의 움직임에는 화면을 계속 높은 빈도로 다시 그릴 필요가 없었다. 초기 대기 애니메이션은 100ms마다 갱신했고, 페이지가 숨겨져 있으면 타이머를 멈췄다. 시스템의 동작 줄이기 설정을 사용하는 경우에는 정지된 모습으로 보여줬다.
이후에는 그림을 이미지 기반으로 바꾸고 동작도 늘렸다. 아래는 지금 사용하는 마멀레이드의 대기 모션이다. 캐릭터 제작 방식이 바뀐 과정은 별도의 글에서 다루려고 한다.

잡아서 옮기고, 놓아둔 자리를 기억하기
화면 오른쪽 아래에 처음 나타나는 것만으로는 부족했다. 작업하는 창이나 화면 배치에 따라 고양이가 가리는 위치도 달라진다. 원하는 곳으로 직접 옮길 수 있어야 했다.
이동은 고양이를 마우스로 잡아 끄는 방식으로 만들었다. 이때 누르자마자 창을 이동시키지는 않았다. 클릭하려는 순간에도 손이 조금 움직일 수 있기 때문이다. 처음 누른 지점에서 4px 이상 움직였을 때 드래그로 판단하고, 그때부터 포인터가 움직인 만큼 창을 옮겼다.
움직이는 동안 포인터가 고양이 밖으로 나가도 드래그는 이어져야 했다. 포인터 캡처를 사용해 시작한 동작을 유지하고, 버튼을 놓거나 입력이 취소되면 정리하도록 했다. 앱의 포커스가 사라졌을 때도 잡고 있는 상태가 남지 않게 했다.
놓은 위치는 로컬 파일에 저장했다. 앱을 다시 실행하면 저장한 좌표를 읽어 같은 자리에서 시작한다. 위치 파일이 없거나 읽을 수 없을 때는 기본 위치로 돌아오도록 했다.
저장된 좌표가 항상 유효한 것은 아니었다. 외부 모니터를 연결한 상태에서 옮겨둔 뒤 그 모니터를 빼면, 마지막 위치가 지금은 존재하지 않는 화면에 속할 수 있다. 그래서 좌표를 그대로 복원하기 전에 현재 화면의 작업 영역 안에 들어오는지 확인하고, 필요하면 보이는 위치로 보정했다.
자리를 기억하는 것과 다시 찾을 수 있는 곳에 나타나는 것은 함께 처리해야 하는 일이었다.
보이지 않는 여백이 클릭을 막았다
투명한 창은 보기에는 비어 있지만, 그 부분까지 마우스 입력을 받을 수 있다. 고양이 옆의 빈 곳에 뒤쪽 앱의 버튼이 보여도, 클릭을 펫 창이 먼저 받아버리는 식이다.
창 전체를 마우스 입력에서 제외하면 뒤쪽 앱을 사용할 수 있다. 하지만 그러면 고양이도 잡을 수 없어진다. 반대로 창 전체가 입력을 받게 두면 고양이 주변에 보이지 않는 장애물이 남는다.
필요한 것은 포인터 아래에 실제로 그려진 픽셀이 있는지 확인하는 일이었다. 캔버스에는 각 픽셀의 색과 함께 투명도를 나타내는 알파값이 있다. 포인터 위치를 캔버스 좌표로 바꾸고, 해당 픽셀이 완전히 투명하면 펫 창이 마우스 입력을 넘기도록 했다. 그림이 있는 곳에서는 다시 입력을 받는다.

여기서는 마우스가 움직였을 때만 판단해서도 안 됐다. 포인터가 가만히 있어도 고양이가 숨을 쉬거나 꼬리를 움직이면, 그 아래의 픽셀이 달라질 수 있다. 그림의 투명도 정보를 갱신하고, 창이 입력을 무시하는 동안에도 데스크톱 쪽에서 커서 위치를 확인하도록 연결했다.
드래그를 이미 시작한 뒤에는 예외가 필요했다. 이동 중 포인터가 잠깐 투명한 곳에 들어갔다고 입력을 넘기면, 잡고 있던 고양이를 놓칠 수 있다. 그래서 드래그나 우클릭 메뉴처럼 진행 중인 조작이 있을 때는 입력을 유지하고, 끝난 뒤에 다시 픽셀 기준으로 판단했다.
투명한 배경을 만드는 설정은 짧았지만, 그 위에서 클릭과 드래그가 자연스럽게 이어지게 만드는 데에는 별도의 처리가 필요했다.
화면에 떠 있는 그림에서, 곁에 둘 수 있는 앱으로
이 단계에서 할 수 있는 일은 많지 않았다. 고양이가 쉬고, 마우스로 옮길 수 있고, 다시 실행하면 놓아둔 자리를 기억했다. 고양이 주변의 빈 곳에서는 뒤쪽 앱을 사용할 수 있었다.
그 대신 캐릭터가 보이는 모양과 실제로 조작되는 범위가 조금씩 맞아가기 시작했다. 작은 그림이 화면에 있다는 것뿐 아니라, 다른 작업을 하면서도 그 그림을 곁에 둘 수 있는 상태가 된 셈이다.
이후에는 macOS의 가상 데스크톱을 바꿔도 보이게 하는 작업과 메뉴, 캐릭터 모션, 실행 구조의 변경이 이어졌다. BytePet은 지금도 Preview 단계에서 다듬고 있다. 이 시리즈에서는 현재 모습에 도달하기까지 바꾼 것들을 하나씩 남겨보려고 한다.
다음에는 이 고양이에게 메모와 파일을 먹이처럼 건네는 기능을 붙였다. 단순히 저장 버튼을 누르는 일이 캐릭터의 행동으로 이어지고, 다시 꺼내 쓸 수 있는 보관함이 생긴 과정이다.