브라우저에서 MacPaint 되살리기: 1984년 소스 코드가 곧 명세입니다
PixelPaint는 브라우저의 /paint에서 실제로 동작하는 MacPaint 1.3 재현작으로, Computer History Museum이 2010년에 공개한 Bill Atkinson의 원본 Pascal 소스 코드와 동작 하나하나를 대조해 검증했습니다. 지우개를 더블 클릭하면 창 전체가 지워지고, 그런 다음 직전에 쓰던 도구가 손에 다시 쥐어집니다. ChooseTool이 MacPaint.p의 3651번째 줄에서 하는 일이 바로 그것이기 때문입니다.
{.answer-block}
2010년, Computer History Museum은 Apple의 허가를 받아 MacPaint 1.3의 소스 코드를 공개했습니다. Bill Atkinson이 1984년 1월 초기 Macintosh와 함께 출시한 바로 그 애플리케이션입니다.1 이 공개본은 박물관 목록에 소장 번호 102658076으로 등재되어 있습니다.2 제 컴퓨터에서 MacPaint.p는 5,804줄의 Apple Pascal이고, PaintAsm.a는 2,738줄의 68000 어셈블리입니다. Pascal 파일의 3번째 줄은 전체가 이렇게 되어 있습니다:
{ BitMap Painting Program by Bill Atkinson }
이 공개본은 재현작에 요구할 수 있는 기준 자체를 바꿔 놓습니다. 그전까지 MacPaint를 다시 만드는 일은 스크린샷과 에뮬레이터를 실눈 뜨고 들여다보며 추측하는 작업이었습니다. 그 이후로는 확실한 근거가 존재합니다. PixelPaint를 제대로 완성하기로 마음먹었을 때 제가 세운 규칙은 단순했습니다. 답이 제가 읽을 수 있는 파일 안에 놓여 있는 한, 어떤 동작도 추측으로 내보내지 않는다. 라이선스는 비상업용이고 이 이식은 동작 기반입니다. 저는 프로그램이 무엇을 하는지 알아내려고 Pascal을 읽었고, 그렇게 파악한 것을 JavaScript로 처음부터 새로 구현했을 뿐, 코드를 단 한 줄도 옮겨 적지 않았습니다.
이 글은 그 규칙이 무엇을 치르게 했고 무엇을 가져다주었는지에 관한 것입니다. 짧게 말하면, 소스는 필요하지만 충분하지는 않습니다. 프로그램의 동작은 행간에 살아 있습니다. 상수 속에, 비트마스크 속에, 주석 속에, 프로시저의 형태 속에 말이죠. 그리고 그것을 끄집어내는 일은 옮겨 적기가 아니라 고고학입니다.
소스는 동작이 아닙니다
충실한 재현에는 세 가지 도구가 필요했고, 결국 저는 그 셋을 모두 동원했습니다:
- 명세로서의 소스. 논란이 되는 동작은 하나같이 그것을 구현한 프로시저를 읽어서 해결했고, 줄 번호까지 인용했습니다.
- 정답지로서의 실제 구동 원본. Infinite Mac은 System 시절의 진짜 Mac을 브라우저에서 부팅하며, 디스크에는 진짜 MacPaint가 들어 있습니다.5 소스가 감각에 대해 모호할 때 — 스프레이가 뿌려지는 리듬, 빠르게 움직일 때의 브러시 보간 — 에뮬레이터가 판정을 내려 주었습니다.
- 교차 검증으로서의 독립 구현. 파일 형식을 위해 저는 앱과 코드를 전혀 공유하지 않는 두 번째 디코더를 Python으로 작성했고, 두 구현이 양방향으로 바이트 단위까지 일치하도록 요구했습니다.
쓸 수 없는 도구는 기억입니다. 제 기억이든, 인터넷의 기억이든 말이죠. MacPaint에 대해 “누구나 아는” 것 대부분은, 정확한 좌표에 픽셀 하나를 찍어야 하는 순간 명세가 턱없이 부족했다는 사실이 드러납니다.
1984년에 더블 클릭이 뜻했던 것
스크린샷으로는 결코 알 수 없는 동작이 하나 있습니다. MacPaint에서 팔레트의 도구를 더블 클릭하는 것은 하나의 명령입니다. 그 분기 처리는 ChooseTool이라는 단일 프로시저 안에, MacPaint.p:3651–3699에 자리 잡고 있습니다:
- 지우개: 창 전체를 지운 다음, 직전에 선택했던 도구로 되돌아갑니다.
- 브러시: 브러시 모양 선택기를 엽니다.
- 선택 영역(Marquee): 창 전체를 선택합니다.
- 손(Grabber): Show Page를 엽니다.
- 연필: 픽셀 단위 확대 기능인 FatBits를 토글합니다.
지우개의 경우, 소스만이 드러내 주는 세부가 하나 있습니다. 이 모든 처리에 앞서 3643번째 줄에는 다음과 같은 가드가 놓여 있습니다:
IF theTool <> eraseTool THEN prevTool := theTool;
지우개는 결코 “직전 도구”가 되지 않습니다. 그래서 더블 클릭이 모든 것을 지우고 나면, 프로그램은 실제로 작업하던 브러시나 연필을 다시 손에 쥐어 줍니다. 지우개는 목적지가 아니라 잠깐 다녀가는 손님이었던 셈입니다. 되돌리는 줄에 남긴 Atkinson의 주석이 이를 분명하게 말해 줍니다. { we wont need the eraser anymore }. 이것은 단 하나의 조건문으로 표현된 인터랙션 디자인이며, MacPaint가 화면을 지운 뒤 결코 지우개에 사용자를 붙잡아 두지 않는다는 사실을 알아차리기 전까지는 바깥에서 보이지 않습니다. PixelPaint는 이 프로시저에 담긴 다섯 가지 더블 클릭 동작을 모두 구현했으며, 자동화된 브라우저 테스트 스위트가 각각을 처음부터 끝까지 검증합니다.
선택 영역 처리에도 두 번째 세부가 숨어 있습니다. 더블 클릭이 창 전체를 선택할 때, 소스는 선택을 설정하기 전에 사각형의 오른쪽과 아래쪽에 1을 더합니다. 여기서 off-by-one 문제가 등장합니다.
두 개의 off-by-one, 그리고 누가 옳았는가
프로젝트 중반, 검토 과정에서 제 빌드의 미리보기와 실제 동작이 어긋나는 두 가지 불일치가 지적되었습니다:
- 선택 영역이 고무줄 미리보기보다 픽셀 하나를 덜 잡아내는 것처럼 보였습니다.
- 지우개가 찍는 자국이 커서 미리보기보다 픽셀 하나만큼 컸습니다.
둘 다 +1을 이쪽이든 저쪽이든 살짝 밀어 30초 만에 “고칠” 수 있는 종류의 버그입니다. 하지만 소스를 갖고 있다는 것의 핵심은, 어느 쪽으로 고칠지 마음대로 고를 수 없다는 데 있습니다. 어느 쪽이 틀렸는지를 찾아봐야 합니다.
선택 영역은 버그가 아니었습니다. QuickDraw의 사각형은 아래쪽과 오른쪽 경계를 포함하지 않습니다. (10,10)에서 (20,20)까지의 사각형은 열한 픽셀이 아니라 열 픽셀에 걸칩니다. 제 빌드의 고무줄과 실제 캡처는 이미 그 규약에 따라 일치하고 있었습니다. 10,10에서 20,20까지 드래그하면 정확히 10×10이 선택됩니다. 검토가 실제로 비교한 것은 도형 도구의 미리보기(포함 구간의 마지막 픽셀까지 올바르게 포함하는)와 선택 영역의 경계 비포함 캡처였습니다. 서로 다른 두 규약이, 둘 다 옳은 채로, 나란히 놓여 있었던 것입니다. 결론: 아무것도 바꾸지 않되, 그 이유를 적어 둔다.
지우개는 버그였습니다. 제 버그요. 원본에서 지우개 블록은 커서와 정확히 일치합니다. 16×16 정사각형을 있는 그대로 찍습니다(EraseSome, MacPaint.p:2210, 도구 커서 자체의 마스크를 사용). 제 자국은 2*floor(size/2)+1로 계산되어, 8픽셀 지우개가 9픽셀 폭의 구멍을 지우게 만들었습니다. 자국이 정확히 size 픽셀에 걸치도록 수정했습니다. 이제 8픽셀 지우개는 16번부터 23번까지의 열을 지우고 15번과 24번은 건드리지 않으며, 이는 픽셀 단위로 검증했습니다. FatBits 안에서 지우개는 정확히 2×2로 줄어드는데, 이 역시 소스에 있습니다(MacPaint.p:2214).
이 한 쌍에서 도출된 규칙이 프로젝트의 척추가 되었습니다. 미리보기와 실제 동작이 어긋날 때, 어느 쪽이 거짓말을 하는지는 원본이 판정한다.
캔버스가 아니라 페이지
MacPaint의 가장 구조적인 아이디어는 공간적이기 때문에 놓치기 쉽습니다. 문서는 창이 아닙니다. 문서는 고정된 576×720 픽셀 페이지이며 — MacPaint.p:108–109에 컴파일 타임 상수로 선언되어 있습니다 — 화면상의 그림 그리는 영역은 그 페이지를 들여다보는 창입니다. 손 도구는 창을 페이지 위에서 이동시키고(ScrollDoc, :2778), Show Page(ShowPage, :4074)는 전체 시트가 보이도록 축소해 창 사각형을 새 위치로 드래그할 수 있게 해 줍니다. 72 DPI에서 576×720은 정확히 8×10인치입니다. 문서는 화면이 아니라 종이에 맞춰 크기가 정해진 것입니다.
PixelPaint는 원래 뷰포트 크기의 버퍼를 갖고 있었는데, 이는 MacPaint에 문서가 있던 자리에 캔버스가 있었다는 뜻입니다. 진짜 모델을 중심으로 이를 다시 만드는 일이 프로젝트에서 가장 큰 단일 변경이었고, 그 과정에서 지극히 2026년다운 제약이 드러났습니다. iOS는 캔버스의 백킹 스토어를 약 16.7메가픽셀로 제한합니다. 전체 페이지에 FatBits 확대 배율을 곱해 단순하게 할당하면 26.5메가픽셀이 필요한데, 이는 제가 동작하기를 바랐던 iPad에서 아무 말 없이 빈 화면으로 렌더링되는 캔버스입니다. 이 이식본은 표시용 캔버스를 뷰포트 크기로 유지하고, 대신 문서 공간 뷰 변환을 적용합니다. 백킹 스토어는 8× 확대에서 0.42메가픽셀로 측정되었고, 전체 페이지를 그리고 렌더링하는 한 프레임이 6.4밀리초로, 60Hz에서 한 프레임 이내에 들어왔습니다. Atkinson은 128K 메모리 예산을 화면 밖 숨은 버퍼로 해결했고,4 브라우저 이식본은 숨은 할당 상한을 변환으로 해결합니다. 같은 규율, 다른 벽입니다.
1984년 Mac이 읽을 수 있는 파일
원본과 문서를 주고받지 못하는 재현작은 디오라마일 뿐입니다. MacPaint의 파일 형식은 소스 자체가 문서화하고 있습니다. 512바이트 헤더, 그다음 각 72바이트짜리 720개의 스캔라인으로 이루어진 페이지이며, PackBits로 압축됩니다. PackBits는 Pascal 쪽이 결코 구현하지 않고 선언만 하는 런 렝스 방식입니다(PackBits/UnpackBits, MacPaint.p:420–421에서 EXTERNAL로 표시되며, MyTools.a의 어셈블리 접착 코드가 이를 시스템 트랩으로 분기합니다). 헤더는 프로그램의 패턴 팔레트를 담고 있어, 문서는 자신이 어떤 패턴으로 그려졌는지를 기억합니다.
PixelPaint는 그 형식을 읽고 씁니다. 내보내기는 페이지를 Atkinson 자신의 오차 확산 디더링으로 통과시켜 1비트로 만듭니다. 임계값은 128이고, 각 픽셀의 오차를 8분의 1 단위로 나눠 여섯 개의 이웃으로 밀어내되, 8분의 2는 일부러 버립니다. 바로 이것이 Atkinson 디더링 특유의 강렬한 대비를 만들어 냅니다. 순수한 흑백 그림은 오차가 완전히 0이기 때문에 손대지 않은 채로 통과합니다. Bill Atkinson의 파일 형식을 쓰기 위해 Bill Atkinson의 디더링 알고리즘을 사용한다는 데에는 기분 좋은 순환성이 있습니다.
검증이야말로 독립 구현이라는 도구가 제 몫을 해낸 지점입니다. 앱 내부의 PackBits 코덱은 픽스처를 바이트 단위로 동일하게 왕복시킵니다. 내보낸 파일을 별도의 Python 구현으로 디코딩하니 올바른 헤더 버전, 온전한 패턴, 그리고 모든 바이트가 소진된 정확히 72바이트짜리 720개의 스캔라인이 나왔습니다. Python 쪽에서 인코딩한 파일은 — MacBinary로 감싸여 있고, 임포터는 이를 오프셋 65의 파일 타입으로 감지합니다 — 테두리와 대각선이 계산된 픽셀에 정확히 안착한 채로 PixelPaint에서 열렸습니다. 내보내기, 지우기, 다시 가져오기를 거치니 패킹된 상태가 해시까지 동일하게 재현되었습니다. 두 개의 구현, 양방향, 공유 코드 없음.
행간에서
가장 깊은 통찰은 어떤 기능 목록으로도 결코 드러나지 않을 세부에서 나왔습니다. 오직 읽어야만 발견되는 것들 말입니다.
그리드는 비트마스크입니다. MacPaint의 8픽셀 그리드 스냅은 모든 도구에 적용되지 않습니다. ChooseTool은 도구 인덱스를 벌거벗은 16진 상수 $50BF3000과 대조해 적용 여부를 결정하며, 사람이 읽을 수 있는 Pascal 집합 표기는 주석으로 남겨져 있습니다. 스냅 자체는 가장 가까운 값으로 반올림하는 방식으로, 4를 더한 뒤 8 단위로 버림하여 구현됩니다(GridPoint, MacPaint.p:513). PixelPaint는 이 정확한 도구 집합을 지킵니다. 선택 영역, 텍스트, 선, 사각형, 타원, 다각형은 스냅되고, 자유곡선 도구는 결코 스냅되지 않습니다.
Shift 제약은 수평-또는-수직보다 영리합니다. Constrain(MacPaint.p:875)은 두 델타를 더 작은 쪽에 맞춰 잘라 냄으로써 선을 45°로 스냅하며, 덧붙여 한 축이 다른 축을 2대 1로 압도할 때는 순수한 수평 또는 수직으로 스냅합니다. 제가 본 모든 복제품은 수평/수직 절반만 구현하고 대각선 우세 모델은 건너뜁니다. 소스에는 서른 줄 안에 전체 알고리즘이 담겨 있습니다.
패턴이 곧 잉크입니다. BrushPaint의 시그니처는 브러시 그리고 패턴을 인자로 받습니다(MacPaint.p:2024). 브러시와 스프레이 캔은 “검정”으로 칠하지 않습니다. 언제나 현재 선택된 패턴을 통해 칠합니다. 저는 이를 그대로 채택했고, 그러자 그림 그리는 감각이 달라졌습니다. 패턴 선택은 더 이상 채우기 옵션에 머무르지 않고 물감 그 자체가 됩니다.
가장자리 추적 기능에는 숨은 변형이 있습니다. Shift를 누르고 있으면 윤곽선 오프셋이 2에서 3으로 바뀌는데, 소스에는 { asymmetric shadow }라는 주석으로 표시되어 있습니다(MacPaint.p:1898). 1984년에서 온 한 줄짜리 이스터에그를, 그대로 보존했습니다.
텍스트는 꽉 찬 잉크이며, 소스가 제 버그를 고쳐 주었습니다. 터치 테스트 중에 입력한 텍스트가 이따금 픽셀을 하나도 남기지 않았습니다. 원인은 이렇습니다. 제 텍스트 확정 코드가 글리프 픽셀을 채우기 패턴을 통해 걸러 냈던 탓에, 성긴 패턴이 아무 말 없이 글자를 삼켜 버린 것입니다. 원본은 결코 이렇게 하지 않습니다. 텍스트는 패턴과 무관하게 꽉 찬 전경 잉크로 그려집니다(UpdateText/PatchText, MacPaint.p:992–1106). 프로시저를 읽는 편이 제 잘못된 가정을 디버깅하는 것보다 빨랐고, 논쟁의 여지 없이 수정을 매듭지어 주었습니다.
기록을 위해 하나 더 말하자면, PaintAsm.a에는 Monkey라는 함수가 들어 있습니다. Macintosh 팀이 사용하던 무작위 입력 스트레스 테스터를 걸어 두는 지점으로, MonkeyLives라는 플래그로 보호되어 있습니다. Atkinson은 자신의 테스트 하니스를 블리터와 같은 파일에 담아 출시했습니다. 장인은 자신의 작업 지그를 작업대 위에 그대로 둡니다.
그대로 둔 것, 그리고 바꾼 것
충실함이 설계 원칙이었으므로, 원본에서 벗어난 지점은 적고, 의도적이며, 앱 안에 기록되어 있습니다. 영인본이 자신의 이탈을 밝히듯, About 대화상자가 그것들을 나열합니다:
- 16색 팔레트를 1비트 엔진 위에 얹었습니다. 디더링과 .mac 내보내기 경로가 원할 때면 언제든 진본 그대로의 흑백을 돌려줍니다.
- 100단계 실행 취소 스택. 원본은 정확히 한 단계의 실행 취소만 지원했습니다. Atkinson이 창 크기의 화면 밖 버퍼 두 개 — 현재 상태와 이전 상태 — 를 두고 서로 맞바꿨기 때문입니다.4 그것은 128K RAM에 대한 영웅적인 답이었습니다. 그 제약을 재현하는 것은 코스프레일 뿐입니다. 그 제약이 답하던 메모리 모델은 더 이상 존재하지 않습니다.
- 선택 가능한 지우개 크기, 선택적으로 켤 수 있는 흩뿌리기 스프레이 모드, 그리고 참조 이미지 따라 그리기. 모두 추가 기능으로, 기본적으로 꺼져 있거나 명백히 현대적이며, 어느 것도 원본 동작을 밀어내지 않습니다.
그만큼 의도적으로, 원본의 어떤 표면은 이식하지 않았습니다. 디스크 문서 생명 주기(Save, Save As, Revert, Close)는 플로피 기반 기계에 속한 것이라, 연속 자동 저장과 명시적 내보내기로 대체했습니다. 하지만 File > Print는 살아남았습니다. PrintDoc(MacPaint.p:4307)이 원본 File 메뉴를 마무리하며, 인쇄는 브라우저 크롬이 아니라 그림만을 픽셀까지 또렷하게 렌더링합니다.
프로젝트 전체에서 가장 이상했던 버그는 1984년의 문제가 전혀 아니었습니다. 파일 저장이 몇 주 동안 아무 말 없이 실패했는데, 제 자신의 분석 스크립트가 앵커 클릭을 — blob: URL에 대한 클릭까지 — 가로채고 있었고, 저장 코드가 클릭 직후 브라우저가 다운로드를 시작하기도 전에 blob URL을 동기적으로 해제해 버렸기 때문입니다. 1984년의 프로그램은 자신의 텔레메트리와 싸우지 않습니다. 2026년에 그것을 재현한 프로그램은 아무래도 그렇게 하는 모양입니다.
무언가 그려 보세요
여러분이 알아볼 도구 아이콘들 — 올가미, 손, 스프레이 캔, 페인트 통 — 은 Susan Kare가 그렸습니다. 그의 32×32 픽셀 규율에 대해서는 디자인 철학 시리즈에서 다룬 바 있습니다. 그 아이콘 아래의 동작들은 Bill Atkinson이 작성했으며, 그는 2025년 6월에 세상을 떠났습니다.6 Computer History Museum의 공개는, 그의 프로그램이 어림짐작이 아니라 정직하게 연구되고, 대조되고, 다시 만들어질 수 있게 되었다는 뜻입니다. 이것이야말로 소프트웨어에 바칠 수 있는 가장 좋은 종류의 기념비라고, 저는 생각합니다.
PixelPaint는 이 사이트의 다른 인터랙티브 실험들과 함께 /paint에서 만나볼 수 있습니다. iPad에서 손가락으로도 동작합니다. 연필을 더블 클릭해 FatBits를 확인해 보세요. 무언가 그린 다음 .mac 파일로 저장하고, 1984년의 Macintosh가 그 파일을 열 수 있다는 사실을 떠올려 보세요.
자주 묻는 질문
원본 MacPaint 소스 코드를 구할 수 있나요?
네. Computer History Museum은 2010년 7월 Apple의 허가를 받아 비상업적 용도로 MacPaint 1.3 소스 코드(그리고 QuickDraw 그래픽 라이브러리)를 공개했습니다.1 이 공개본은 CHM 목록 소장 번호 102658076이며2, 메인 Pascal 프로그램(MacPaint.p)과 68000 어셈블리 지원 파일들을 포함합니다. 공식 미러는 Computer History Museum 계정으로 GitHub에 올라와 있습니다.3
PackBits 압축이란 무엇인가요?
PackBits는 MacPaint가 문서를 압축할 때 사용한 런 렝스 인코딩 방식입니다. 각 스캔라인은 리터럴 런과 반복 런으로 패킹되며, 이는 흰 여백과 반복 패턴으로 가득한 1비트 이미지에서 잘 작동합니다. MacPaint의 Pascal은 PackBits와 UnpackBits를 외부 루틴으로 선언하고(MacPaint.p:420–421), 어셈블리 접착 코드를 통해 시스템의 68000 구현에 도달합니다. MacPaint 파일은 512바이트 헤더에 이어 각 72바이트짜리 720개의 PackBits 압축 행으로 이루어집니다. 바로 전체 576×720 페이지입니다.
Atkinson 디더링이란 무엇인가요?
Atkinson 디더링은 Bill Atkinson이 그레이스케일 이미지를 Macintosh의 1비트 디스플레이로 변환하기 위해 고안한 오차 확산 알고리즘입니다. 각 픽셀은 임계값을 기준으로 검정 또는 흰색으로 정해지고, 그 결과 생긴 오차는 8로 나뉘어 여섯 개의 이웃 픽셀로 분배되며, 남은 8분의 2는 전파하지 않고 일부러 버립니다. 오차의 일부를 덜어 내는 것이 바로 Atkinson 디더링을 거친 이미지 특유의 높은 대비를 만들어 냅니다. PixelPaint는 이를 사용해 .mac 내보내기와 실시간 1비트 미리보기를 위해 컬러 그림을 1비트로 변환합니다.
MacPaint 문서의 크기는 얼마인가요?
576×720 픽셀로 고정되어 있으며, 소스에 상수로 선언되어 있습니다(MacPaint.p:108–109). Macintosh의 72 DPI에서 이는 정확히 8×10인치, 곧 인쇄 가능한 한 페이지입니다. 화면은 문서 전체를 한 번에 보여 준 적이 없습니다. 그림 창은 페이지를 들여다보는 움직일 수 있는 뷰포트였고, 손 도구로 이동하거나 Show Page로 위치를 다시 잡았습니다. PixelPaint는 뷰포트를 포함한 동일한 문서 모델을 그대로 재현합니다.
출처
-
Leonard J. Shustek, “MacPaint and QuickDraw Source Code,” Computer History Museum 블로그, 2010년 7월 18일. 공개 발표문으로, Apple의 허가와 비상업용 라이선스를 문서화하고 프로그램의 역사를 담고 있습니다. ↩↩
-
Computer History Museum 소장품 목록, “MacPaint source code,” 소장 번호 102658076. ↩↩
-
Computer History Museum, Historical Source Code: MacPaint repository, GitHub. 공개된 소스 파일의 공식 미러. ↩
-
Andy Hertzfeld, “MacPaint Evolution,” Folklore.org. MacPaint 개발사에 관한 1차 자료로, 깜박임 없는 그리기와 한 단계 실행 취소를 뒷받침한 창 크기의 화면 밖 버퍼 두 개(현재 상태와 이전 상태)를 다룹니다. ↩↩
-
Infinite Mac — MacPaint를 비롯한 고전 Macintosh 시스템을 브라우저에서 에뮬레이션합니다. 동작 비교를 위한 실제 구동 원본 정답지로 사용했습니다. ↩
-
Adam Engst, “Bill Atkinson Dies from Pancreatic Cancer at 74,” TidBITS, 2025년 6월 7일. ↩