Recriando o MacPaint no navegador: o código-fonte de 1984 é a especificação
O PixelPaint é uma recriação funcional do MacPaint 1.3 que roda no seu navegador em /paint, validada comportamento por comportamento contra o código-fonte Pascal original de Bill Atkinson, que o Computer History Museum liberou em 2010. Dê um duplo-clique na borracha e ela apaga a janela inteira e depois devolve a ferramenta que você usava antes, porque é isso que ChooseTool faz na linha 3651 do MacPaint.p.
{.answer-block}
Em 2010, o Computer History Museum, com a permissão da Apple, publicou o código-fonte do MacPaint 1.3: o aplicativo que Bill Atkinson entregou com o Macintosh original em janeiro de 1984.1 A liberação consta no catálogo do museu sob o número de tombo 102658076.2 Na minha máquina, o MacPaint.p tem 5.804 linhas de Apple Pascal e o PaintAsm.a tem 2.738 linhas de assembly 68000. A linha 3 do arquivo Pascal diz, na íntegra:
{ BitMap Painting Program by Bill Atkinson }
Essa liberação muda aquilo que se pode exigir de uma recriação. Antes dela, reconstruir o MacPaint significava apertar os olhos diante de capturas de tela e emuladores e ficar chutando. Depois dela, existe uma verdade de referência. Quando decidi terminar o PixelPaint direito, a regra que estabeleci foi simples: nenhum comportamento é lançado com base em um chute quando a resposta está num arquivo que posso ler. A licença é não comercial e o port é comportamental — li o Pascal para descobrir o que o programa faz e então implementei isso em JavaScript do zero, sem nunca traduzir uma linha de código.
Este post é sobre o que essa regra custou e o que ela rendeu. A versão curta: o código-fonte é necessário e insuficiente. O comportamento do programa vive nas entrelinhas — em constantes, em máscaras de bits, em comentários, no formato de um procedimento — e extraí-lo é arqueologia, não transcrição.
O código-fonte não é o comportamento
Uma recriação fiel precisa de três instrumentos, e acabei usando todos eles:
- O código-fonte como especificação. Cada comportamento em disputa resolvido pela leitura do procedimento que o implementa, citado por linha.
- Um original em execução como oráculo. O Infinite Mac inicializa Macs reais da era do System no navegador, com o MacPaint de verdade no disco.5 Quando o código-fonte era ambíguo quanto à sensação — a cadência do spray, a interpolação do pincel em velocidade — o emulador resolvia a questão.
- Uma implementação independente como verificação cruzada. Para o formato de arquivo, escrevi um segundo decodificador em Python, sem compartilhar código algum com o aplicativo, e exigi que os dois concordassem byte a byte nas duas direções.
O instrumento que você não pode usar é a memória. A minha, ou a da internet. A maior parte do que “todo mundo sabe” sobre o MacPaint se revela mal especificada no instante em que você precisa fazer um pixel aparecer numa coordenada exata.
O que o duplo-clique significava em 1984
Eis um comportamento que nenhuma captura de tela consegue lhe contar. No MacPaint, dar um duplo-clique numa ferramenta da paleta é um comando. O despacho vive num único procedimento, ChooseTool, em MacPaint.p:3651–3699:
- Borracha: apaga a janela inteira e depois volta para a ferramenta selecionada anteriormente.
- Pincel: abre o seletor de formato do pincel.
- Seleção retangular: seleciona a janela inteira.
- Grabber (a mão): abre o Show Page.
- Lápis: alterna o FatBits, o zoom no nível do pixel.
O caso da borracha tem um detalhe que só o código-fonte revela. Na linha 3643, antes de tudo isso, está a guarda:
IF theTool <> eraseTool THEN prevTool := theTool;
A borracha nunca vira a “ferramenta anterior”. Então, quando um duplo-clique apaga tudo, o programa devolve o pincel ou o lápis com que você estava realmente trabalhando — a borracha era uma visitante, não um destino. O comentário de Atkinson na linha do retorno diz isso sem rodeios: { we wont need the eraser anymore }. Isso é design de interação expresso num único condicional, e é invisível de fora até você perceber que o MacPaint nunca te deixa preso na borracha depois de uma limpeza. O PixelPaint implementa todos os cinco comportamentos de duplo-clique desse procedimento, e uma suíte automatizada de navegador verifica cada um deles de ponta a ponta.
Há um segundo detalhe escondido no caso da seleção retangular. Quando o duplo-clique seleciona a janela inteira, o código-fonte soma um à direita e à base do retângulo antes de definir a seleção. O que nos leva aos erros de off-by-one.
Dois off-by-one, e quem estava certo
No meio do projeto, uma rodada de revisão sinalizou duas divergências entre prévia e resultado real no meu build:
- A seleção retangular parecia capturar um pixel a menos do que a prévia do seu contorno elástico.
- O carimbo da borracha era um pixel maior do que a prévia do seu cursor.
Ambos são o tipo de bug que você poderia “consertar” em trinta segundos ajustando um +1 — em qualquer direção. Toda a razão de ter o código-fonte é que você não pode escolher. Você vai lá e verifica qual lado está errado.
A seleção retangular não era um bug. Os retângulos do QuickDraw são exclusivos na base e na direita: um retângulo de (10,10) a (20,20) abrange dez pixels, não onze. O contorno elástico e a captura no meu build já concordavam sob essa convenção — arrastar de 10,10 a 20,20 seleciona exatamente 10×10. O que a revisão de fato comparou foi a prévia das ferramentas de forma (que corretamente inclui o pixel final de um intervalo inclusivo) contra a captura exclusiva da seleção retangular. Duas convenções diferentes, ambas corretas, lado a lado. Resolução: não mudar nada, anotar o porquê.
A borracha era um bug — meu. No original, o bloco da borracha é idêntico ao seu cursor: um quadrado de 16×16, carimbado como está (EraseSome, MacPaint.p:2210, usando a própria máscara do cursor da ferramenta). Meu carimbo era calculado como 2*floor(size/2)+1, o que fazia uma borracha de 8 pixels apagar um buraco de 9 pixels de largura. Corrigido para que o carimbo abranja exatamente size pixels: uma borracha de 8 pixels agora apaga as colunas 16 a 23 e deixa a 15 e a 24 intactas, verificado pixel a pixel. Dentro do FatBits, a borracha cai para exatamente 2×2, o que também está no código-fonte (MacPaint.p:2214).
A regra que surgiu desse par virou a espinha dorsal do projeto: quando a prévia e a ação divergem, o original decide qual das duas está mentindo.
A página, não a tela
A ideia mais estrutural do MacPaint é fácil de passar despercebida porque é espacial. O documento não é a janela. O documento é uma página fixa de 576×720 pixels — declarada como constantes de compilação em MacPaint.p:108–109 — e a área de desenho na tela é uma janela sobre ela. O grabber desloca a janela pela página (ScrollDoc, :2778); o Show Page (ShowPage, :4074) dá um zoom out para a folha inteira e permite arrastar o retângulo da janela para um novo ponto. A 72 DPI, 576×720 dá exatamente 8×10 polegadas: o documento foi dimensionado para o papel, não para a tela.
O PixelPaint originalmente tinha um buffer do tamanho da viewport, o que significava que ele tinha uma tela onde o MacPaint tinha um documento. Reconstruí-lo em torno do modelo real foi a maior mudança isolada do projeto, e ela trouxe à tona uma restrição totalmente de 2026: o iOS limita o backing store de um canvas em torno de 16,7 megapixels. Alocar ingenuamente a página inteira multiplicada pelo zoom do FatBits exigiria 26,5 megapixels — um canvas que renderiza em branco silenciosamente nos iPads em que eu queria que isso funcionasse. O port mantém os canvas de exibição do tamanho da viewport e aplica, em vez disso, uma transformação de visualização no espaço do documento; o backing store mediu 0,42 megapixel com zoom de 8× e um quadro completo de desenho-mais-renderização da página inteira levou 6,4 milissegundos, abaixo de um quadro a 60 Hz. Atkinson resolveu um orçamento de memória de 128K com buffers ocultos fora da tela;4 o port para navegador resolve um teto de alocação oculto com uma transformação. Mesma disciplina, parede diferente.
Arquivos que um Mac de 1984 consegue ler
Uma recriação que não consegue trocar documentos com o original é um diorama. O formato de arquivo do MacPaint é documentado pelo próprio código-fonte: um cabeçalho de 512 bytes, depois a página como 720 scanlines de 72 bytes cada, comprimida com PackBits — um esquema de run-length que o lado Pascal nunca implementa, apenas declara (PackBits/UnpackBits, marcados como EXTERNAL em MacPaint.p:420–421; a cola em assembly no MyTools.a os despacha como system traps). O cabeçalho carrega a paleta de padrões do programa, então um documento lembra os padrões com que foi pintado.
O PixelPaint lê e escreve esse formato. A exportação passa a página pelo próprio dither de difusão de erro de Atkinson para chegar a 1 bit — limiar em 128, o erro de cada pixel dividido em oitavos e empurrado para seis vizinhos, com dois oitavos deliberadamente descartados, que é o que dá ao dithering de Atkinson seu contraste marcante. Desenhos em preto e branco puros passam intactos, porque seu erro é identicamente zero. Há uma circularidade agradável em usar o algoritmo de dithering de Bill Atkinson para escrever o formato de arquivo de Bill Atkinson.
A verificação é onde o instrumento da implementação independente provou seu valor. O codec PackBits interno faz round-trip das fixtures de forma byte-idêntica. Um arquivo exportado, decodificado pela implementação separada em Python, produziu a versão de cabeçalho correta, padrões intactos e exatamente 720 scanlines de 72 bytes com cada byte consumido. Um arquivo codificado pelo lado Python — embrulhado em MacBinary, que o importador detecta pelo tipo de arquivo no offset 65 — abriu no PixelPaint com sua borda e diagonais caindo nos pixels calculados. Exportar, limpar e reimportar reproduziu o estado empacotado de forma hash-idêntica. Duas implementações, as duas direções, nenhum código compartilhado.
Nas entrelinhas
Os cortes mais profundos vieram de detalhes que nenhuma lista de recursos jamais traria à tona — coisas que você só encontra lendo.
A grade é uma máscara de bits. O encaixe na grade de 8 pixels do MacPaint não se aplica a toda ferramenta. ChooseTool decide a elegibilidade testando o índice da ferramenta contra uma constante hexadecimal crua, $50BF3000, com o conjunto Pascal legível por humanos deixado para trás como comentário. O encaixe em si é arredondamento para o mais próximo, implementado como truncar-para-8 depois de somar 4 (GridPoint, MacPaint.p:513). O PixelPaint respeita o conjunto exato de ferramentas: seleção retangular, texto, linhas, retângulos, ovais e polígonos encaixam; as ferramentas de mão livre nunca.
A restrição com Shift é mais inteligente do que só horizontal-ou-vertical. Constrain (MacPaint.p:875) encaixa uma linha em 45° limitando ambos os deltas ao menor deles — e adicionalmente encaixa em horizontal ou vertical puro quando um eixo domina o outro na proporção de dois para um. Todo clone que já vi implementa a metade H/V e ignora o modelo de dominância diagonal. O código-fonte tem o algoritmo completo em trinta linhas.
O padrão é a tinta. A assinatura de BrushPaint recebe o pincel e um padrão (MacPaint.p:2024). O pincel e o spray não pintam em “preto”; eles pintam através do padrão selecionado no momento, sempre. Adotei isso exatamente, e mudou a sensação de desenhar — a seleção de padrão deixa de ser uma opção de preenchimento e passa a ser a própria tinta.
O Traçar Bordas tem uma variante oculta. Segure Shift e o deslocamento do contorno muda de 2 para 3, anotado no código-fonte com o comentário { asymmetric shadow } (MacPaint.p:1898). Um easter egg de uma linha, de 1984, preservado.
O texto é tinta sólida, e o código-fonte corrigiu meu bug. Nos testes por toque, o texto digitado às vezes gravava zero pixel. A causa: minha gravação de texto filtrava os pixels dos glifos através do padrão de preenchimento, então um padrão esparso engolia as letras silenciosamente. O original nunca faz isso — o texto é desenhado como tinta sólida de primeiro plano, independentemente do padrão (UpdateText/PatchText, MacPaint.p:992–1106). Ler o procedimento foi mais rápido do que depurar minha própria suposição, e resolveu a correção sem margem para discussão.
Mais um achado, para constar: o PaintAsm.a contém uma função chamada Monkey — o ponto de entrada para o testador de estresse por entrada aleatória que a equipe do Macintosh usava, protegido por uma flag chamada MonkeyLives. Atkinson entregou seu arcabouço de testes no mesmo arquivo que seu blitter. Artesãos deixam seus gabaritos sobre a bancada.
O que deixei intacto e o que mudei
A fidelidade era o princípio de design, então os desvios são poucos, deliberados e registrados dentro do aplicativo — a caixa de diálogo Sobre os lista, do jeito que uma edição fac-símile revela seus desvios:
- Uma paleta de 16 cores sobre o motor de 1 bit. O dither e o caminho de exportação .mac lhe entregam o monocromático autêntico sempre que você quiser.
- Uma pilha de 100 níveis de desfazer. O original tinha exatamente um nível de desfazer, porque Atkinson mantinha dois buffers fora da tela do tamanho da janela — estado atual e estado anterior — e os alternava.4 Era uma resposta heroica a 128K de RAM. Recriar a limitação seria cosplay; o modelo de memória que ela respondia não existe mais.
- Tamanhos de borracha selecionáveis, um modo opcional de spray espalhado e o decalque a partir de imagem de referência. Acréscimos, todos desativados por padrão ou claramente modernos, nenhum deles deslocando um comportamento original.
Com igual deliberação, parte da superfície do original não foi portada: o ciclo de vida de documento em disco (Save, Save As, Revert, Close) pertence a uma máquina baseada em disquete e foi substituído por autosave contínuo mais exportações explícitas. Mas o File > Print sobrevive — PrintDoc (MacPaint.p:4307) encerra o menu File original, e a impressão renderiza somente a arte, nítida no pixel, nunca o chrome do navegador.
O bug mais estranho de todo o projeto não era, de forma alguma, um problema de 1984. Salvar arquivos falhava silenciosamente havia semanas porque meu próprio script de analytics interceptava os cliques em âncoras — inclusive cliques em URLs blob: — e o código de salvamento revogava a blob URL de forma síncrona logo após o clique, antes de o navegador iniciar o download. Um programa de 1984 não briga com a própria telemetria. Recriar um em 2026, aparentemente, briga.
Vá desenhar alguma coisa
Os ícones das ferramentas que você vai reconhecer — laço, grabber, lata de spray, balde de tinta — foram desenhados por Susan Kare, cuja disciplina de 32×32 pixels comentei na série sobre filosofia de design. Os comportamentos por trás deles foram escritos por Bill Atkinson, que morreu em junho de 2025.6 A liberação do Computer History Museum significa que o programa dele pode ser estudado, conferido e reconstruído de forma honesta, em vez de aproximada — o que é, eu acho, o melhor tipo de monumento para um software.
O PixelPaint está no ar em /paint, ao lado das outras explorações interativas deste site. Funciona num iPad com o dedo. Dê um duplo-clique no lápis para ver o FatBits. Desenhe alguma coisa, salve como um arquivo .mac e saiba que um Macintosh de 1984 conseguiria abri-lo.
Perguntas frequentes
O código-fonte original do MacPaint está disponível?
Sim. O Computer History Museum liberou o código-fonte do MacPaint 1.3 (e a biblioteca gráfica QuickDraw) em julho de 2010, com a permissão da Apple, para uso não comercial.1 O acervo é o de número de tombo 1026580762 do catálogo do CHM e inclui o programa Pascal principal (MacPaint.p) mais os arquivos de apoio em assembly 68000. Um espelho oficial está no GitHub sob a conta do Computer History Museum.3
O que é a compressão PackBits?
PackBits é o esquema de codificação run-length que o MacPaint usava para comprimir documentos: cada scanline é empacotada como sequências literais e sequências de repetição, o que funciona bem em imagens de 1 bit cheias de espaço em branco e padrões repetidos. O Pascal do MacPaint declara PackBits e UnpackBits como rotinas externas (MacPaint.p:420–421) e chega à implementação 68000 do sistema por meio de sua cola em assembly. Um arquivo MacPaint é um cabeçalho de 512 bytes seguido de 720 linhas comprimidas com PackBits de 72 bytes cada — a página inteira de 576×720.
O que é o dithering de Atkinson?
O dithering de Atkinson é o algoritmo de difusão de erro que Bill Atkinson concebeu para converter imagens em tons de cinza para o display de 1 bit do Macintosh. Cada pixel é limiarizado para preto ou branco, e o erro resultante é dividido por 8 e distribuído para seis pixels vizinhos — com os dois oitavos restantes intencionalmente descartados, em vez de propagados. Descartar parte do erro é o que dá às imagens tratadas com dithering de Atkinson seu alto contraste característico. O PixelPaint o usa para converter desenhos coloridos para 1 bit na exportação .mac e para a prévia ao vivo em um bit.
Qual é o tamanho de um documento MacPaint?
576×720 pixels, fixo — declarado como constantes no código-fonte (MacPaint.p:108–109). Nos 72 DPI do Macintosh isso dá exatamente 8×10 polegadas, uma página imprimível. A tela nunca mostrava o documento inteiro de uma vez: a janela de desenho era uma viewport móvel sobre a página, deslocada com o grabber ou reposicionada pelo Show Page. O PixelPaint recria o mesmo modelo de documento, viewport e tudo.
Fontes
-
Leonard J. Shustek, “MacPaint and QuickDraw Source Code,” blog do Computer History Museum, 18 de julho de 2010. O anúncio da liberação; documenta a permissão da Apple e a licença não comercial, e inclui a história do programa. ↩↩
-
Catálogo do acervo do Computer History Museum, “MacPaint source code,” número de tombo 102658076. ↩↩
-
Computer History Museum, Historical Source Code: MacPaint repository, GitHub. Espelho oficial dos arquivos de código-fonte liberados. ↩
-
Andy Hertzfeld, “MacPaint Evolution,” Folklore.org. Fonte primária para a história do desenvolvimento do MacPaint, incluindo os dois buffers fora da tela do tamanho da janela (estado atual e estado anterior) por trás do desenho sem cintilação e do desfazer de um nível. ↩↩
-
Infinite Mac — sistemas Macintosh clássicos, incluindo o MacPaint, emulados no navegador. Usado como o oráculo do original em execução para comparações de comportamento. ↩
-
Adam Engst, “Bill Atkinson Dies from Pancreatic Cancer at 74,” TidBITS, 7 de junho de 2025. ↩