MacPaint odtworzony w przeglądarce: kod źródłowy z 1984 roku jest specyfikacją
PixelPaint to działająca rekonstrukcja MacPainta 1.3, uruchamiana w przeglądarce pod adresem /paint i zweryfikowana zachowanie po zachowaniu względem oryginalnego kodu źródłowego Billa Atkinsona w Pascalu, opublikowanego przez Computer History Museum w 2010 roku. Dwukrotne kliknięcie gumki wymazuje całe okno, a następnie przywraca narzędzie używane wcześniej — bo dokładnie tak działa ChooseTool w wierszu 3651 pliku MacPaint.p.
{.answer-block}
W 2010 roku Computer History Museum, za zgodą Apple, opublikowało kod źródłowy MacPainta 1.3 — aplikacji, którą Bill Atkinson dostarczył wraz z pierwszym Macintoshem w styczniu 1984 roku.1 W katalogu muzeum publikacja figuruje pod numerem akcesji 102658076.2 Na moim komputerze MacPaint.p liczy 5804 wiersze w języku Apple Pascal, a PaintAsm.a — 2738 wierszy asemblera 68000. Wiersz 3 pliku Pascala brzmi, w całości:
{ BitMap Painting Program by Bill Atkinson }
Ta publikacja zmienia to, do czego można rekonstrukcję rozliczać. Wcześniej odtwarzanie MacPainta oznaczało wpatrywanie się w zrzuty ekranu i emulatory — i zgadywanie. Od tej pory istnieje źródło prawdy. Gdy postanowiłem porządnie dokończyć PixelPaint, przyjąłem prostą zasadę: żadne zachowanie nie trafia do wydania na podstawie domysłu, skoro odpowiedź leży w pliku, który mogę przeczytać. Licencja jest niekomercyjna, a port ma charakter behawioralny — czytałem Pascala, by dowiedzieć się, co program robi, a następnie implementowałem to w JavaScripcie od zera, nie tłumacząc ani jednego wiersza kodu.
Ten wpis opowiada o tym, ile ta zasada kosztowała i co dała w zamian. W skrócie: źródło jest konieczne, ale niewystarczające. Zachowanie programu żyje między wierszami — w stałych, w maskach bitowych, w komentarzach, w samym kształcie procedury — a jego wydobycie to archeologia, nie transkrypcja.
Źródło to nie zachowanie
Wierna rekonstrukcja wymaga trzech instrumentów i ostatecznie użyłem wszystkich:
- Źródło jako specyfikacja. Każde sporne zachowanie rozstrzygane lekturą procedury, która je implementuje, z cytowaniem konkretnego wiersza.
- Działający oryginał jako wyrocznia. Infinite Mac uruchamia w przeglądarce prawdziwe komputery Mac z epoki klasycznego Systemu, z prawdziwym MacPaintem na dysku.5 Gdy źródło nie rozstrzygało kwestii odczucia — rytmu sprayu, interpolacji pędzla przy szybkim ruchu — sprawę zamykał emulator.
- Niezależna implementacja jako kontrola krzyżowa. Dla formatu pliku napisałem drugi dekoder w Pythonie, niedzielący z aplikacją ani linijki kodu, i wymagałem, by obie implementacje zgadzały się bajt w bajt w obu kierunkach.
Instrumentem, którego użyć nie można, jest pamięć. Ani moja, ani internetu. Większość tego, co o MacPaincie „wszyscy wiedzą”, okazuje się niedookreślona w chwili, gdy piksel ma się pojawić w dokładnie wyznaczonej współrzędnej.
Co oznaczało dwukrotne kliknięcie w 1984 roku
Oto zachowanie, którego nie zdradzi żaden zrzut ekranu. W MacPaincie dwukrotne kliknięcie narzędzia w palecie jest poleceniem. Cała obsługa mieści się w jednej procedurze, ChooseTool, w MacPaint.p:3651–3699:
- Gumka: wymazuje całe okno, po czym wraca do poprzednio wybranego narzędzia.
- Pędzel: otwiera wybór kształtu pędzla.
- Zaznaczenie (marquee): zaznacza całe okno.
- Rączka (grabber): otwiera widok Show Page.
- Ołówek: przełącza FatBits, czyli powiększenie do poziomu pikseli.
Przypadek gumki kryje szczegół, który ujawnia dopiero źródło. W wierszu 3643, zanim cokolwiek z powyższego się wydarzy, stoi warunek ochronny:
IF theTool <> eraseTool THEN prevTool := theTool;
Gumka nigdy nie staje się „poprzednim narzędziem”. Gdy więc dwukrotne kliknięcie wymazuje wszystko, program oddaje z powrotem pędzel albo ołówek, którym faktycznie się pracowało — gumka była gościem, nie celem podróży. Komentarz Atkinsona przy wierszu przywracającym narzędzie mówi to wprost: { we wont need the eraser anymore }. To projekt interakcji wyrażony jednym warunkiem — z zewnątrz niewidoczny, dopóki nie zauważy się, że MacPaint po wyczyszczeniu ekranu nigdy nie zostawia użytkownika z gumką w ręce. PixelPaint implementuje wszystkie pięć zachowań dwukrotnego kliknięcia z tej procedury, a zautomatyzowany zestaw testów przeglądarkowych sprawdza każde z nich od początku do końca.
W przypadku zaznaczenia kryje się jeszcze drugi szczegół. Gdy dwukrotne kliknięcie zaznacza całe okno, źródło dodaje jeden do prawej i dolnej krawędzi prostokąta, zanim ustawi zaznaczenie. Co prowadzi nas do błędów o jeden.
Dwa błędy o jeden i kto miał rację
W połowie projektu przegląd kodu wykazał w mojej wersji dwie rozbieżności między podglądem a rzeczywistym działaniem:
- Zaznaczenie zdawało się obejmować o jeden piksel mniej niż jego rozciągany podgląd (rubber band).
- Stempel gumki był o piksel większy niż podgląd jej kursora.
Oba to błędy z gatunku tych, które można „naprawić” w trzydzieści sekund, przesuwając +1 — w dowolną stronę. Cały sens posiadania źródła polega na tym, że wyboru nie ma. Sprawdza się, która strona się myli.
Zaznaczenie nie było błędem. Prostokąty QuickDraw nie obejmują dolnej i prawej krawędzi: prostokąt od (10,10) do (20,20) rozpina się na dziesięciu pikselach, nie jedenastu. Rozciągany podgląd i przechwycenie w mojej wersji już były zgodne w ramach tej konwencji — przeciągnięcie z 10,10 do 20,20 zaznacza dokładnie 10×10. Przegląd tak naprawdę porównał podgląd narzędzi kształtów (który poprawnie obejmuje ostatni piksel zakresu domkniętego) z wyłączającym przechwyceniem zaznaczenia. Dwie różne konwencje, obie poprawne, tuż obok siebie. Rozstrzygnięcie: niczego nie zmieniać, zapisać dlaczego.
Gumka była błędem — moim. W oryginale blok gumki równa się dokładnie jej kursorowi: kwadrat 16×16, stemplowany bez zmian (EraseSome, MacPaint.p:2210, z użyciem maski własnego kursora narzędzia). Mój stempel był liczony jako 2*floor(size/2)+1, przez co 8-pikselowa gumka wymazywała dziurę o szerokości 9 pikseli. Poprawione tak, by stempel obejmował dokładnie size pikseli: 8-pikselowa gumka wymazuje teraz kolumny od 16 do 23, a 15 i 24 zostawia nietknięte — zweryfikowano piksel po pikselu. W trybie FatBits gumka maleje dokładnie do 2×2, co również stoi w źródle (MacPaint.p:2214).
Zasada, która wyłoniła się z tej pary, stała się kręgosłupem projektu: gdy podgląd i działanie się nie zgadzają, to oryginał rozstrzyga, które z nich kłamie.
Strona, nie płótno
Najbardziej strukturalny pomysł MacPainta łatwo przeoczyć, bo jest przestrzenny. Dokument to nie okno. Dokument to strona o stałym rozmiarze 576×720 pikseli — zadeklarowanym jako stałe czasu kompilacji w MacPaint.p:108–109 — a obszar rysowania na ekranie jest oknem na nią. Rączka przesuwa okno po stronie (ScrollDoc, :2778); Show Page (ShowPage, :4074) oddala widok do całego arkusza i pozwala przeciągnąć prostokąt okna w nowe miejsce. Przy 72 DPI 576×720 to dokładnie 8×10 cali: dokument został zwymiarowany pod papier, nie pod ekran.
PixelPaint miał pierwotnie bufor wielkości widocznego obszaru, czyli płótno tam, gdzie MacPaint miał dokument. Przebudowa wokół prawdziwego modelu była największą pojedynczą zmianą w projekcie i ujawniła ograniczenie na wskroś współczesne, rodem z 2026 roku: iOS ogranicza magazyn danych elementu canvas do około 16,7 megapiksela. Naiwna alokacja całej strony pomnożonej przez powiększenie FatBits wymagałaby 26,5 megapiksela — takie płótno na iPadach, na których miało to działać, renderuje się po cichu jako puste. Port utrzymuje płótna wyświetlania w rozmiarze widocznego obszaru i zamiast tego stosuje przekształcenie widoku w przestrzeni dokumentu; magazyn danych zmierzono na 0,42 megapiksela przy powiększeniu 8×, a klatka pełnostronicowego rysowania z renderowaniem zajęła 6,4 milisekundy — mniej niż jedna klatka przy 60 Hz. Atkinson rozwiązał budżet pamięci 128K ukrytymi buforami pozaekranowymi;4 port przeglądarkowy rozwiązuje ukryty sufit alokacji przekształceniem. Ta sama dyscyplina, inna ściana.
Pliki, które odczyta Mac z 1984 roku
Rekonstrukcja, która nie potrafi wymieniać dokumentów z oryginałem, jest dioramą. Format pliku MacPainta dokumentuje samo źródło: 512-bajtowy nagłówek, a po nim strona jako 720 linii skanowania po 72 bajty każda, skompresowanych algorytmem PackBits — schematem kodowania długości serii, którego strona pascalowa nigdy nie implementuje, a jedynie deklaruje (PackBits/UnpackBits, oznaczone EXTERNAL w MacPaint.p:420–421; asemblerowa warstwa pośrednicząca w MyTools.a wywołuje je jako pułapki systemowe). Nagłówek niesie paletę wzorków programu, więc dokument pamięta wzorki, którymi go namalowano.
PixelPaint ten format czyta i zapisuje. Eksport przepuszcza stronę przez autorski dithering dyfuzji błędu Atkinsona, by zejść do 1 bita — próg na 128, błąd każdego piksela dzielony na ósme części i rozprowadzany do sześciu sąsiadów, przy czym dwie ósme są celowo odrzucane, co właśnie nadaje ditheringowi Atkinsona jego wyrazisty kontrast. Rysunki czysto czarno-białe przechodzą nietknięte, bo ich błąd jest tożsamościowo równy zeru. Jest coś przyjemnie kolistego w tym, że format pliku Billa Atkinsona zapisuje się algorytmem ditheringu Billa Atkinsona.
Weryfikacja to moment, w którym instrument niezależnej implementacji zarobił na swoje utrzymanie. Wbudowany w aplikację kodek PackBits odtwarza dane testowe w obie strony z bajtową identycznością. Wyeksportowany plik, zdekodowany osobną implementacją w Pythonie, dał poprawną wersję nagłówka, nienaruszone wzorki i dokładnie 720 linii skanowania po 72 bajty, ze skonsumowanym każdym bajtem. Plik zakodowany po stronie Pythona — opakowany w MacBinary, który importer rozpoznaje po typie pliku pod offsetem 65 — otworzył się w PixelPaincie z ramką i przekątnymi trafiającymi w wyliczone piksele. Eksport, wyczyszczenie, ponowny import odtworzyły spakowany stan z identycznym hashem. Dwie implementacje, oba kierunki, zero wspólnego kodu.
Między wierszami
Najgłębsze odkrycia przyniosły szczegóły, których nie ujawniłaby żadna lista funkcji — rzeczy znajdowane wyłącznie podczas lektury.
Siatka jest maską bitową. Przyciąganie do 8-pikselowej siatki w MacPaincie nie dotyczy każdego narzędzia. ChooseTool rozstrzyga o kwalifikacji, testując indeks narzędzia względem gołej stałej szesnastkowej $50BF3000 — czytelny dla człowieka pascalowy zbiór został z tyłu jako komentarz. Samo przyciąganie to zaokrąglanie do najbliższej wartości, zaimplementowane jako obcięcie do wielokrotności 8 po dodaniu 4 (GridPoint, MacPaint.p:513). PixelPaint honoruje dokładnie ten zestaw narzędzi: zaznaczenie, tekst, linie, prostokąty, owale i wielokąty przyciągają; narzędzia odręczne — nigdy.
Ograniczenie z Shiftem jest mądrzejsze niż poziomo-albo-pionowo. Constrain (MacPaint.p:875) przyciąga linię do 45°, przycinając obie delty do mniejszej z nich — a dodatkowo przyciąga do czystego poziomu lub pionu, gdy jedna oś dominuje nad drugą w stosunku dwa do jednego. Każdy klon, jaki widziałem, implementuje połowę poziomo-pionową i pomija model dominacji przekątnej. Źródło mieści pełny algorytm w trzydziestu wierszach.
Wzorek jest tuszem. Sygnatura BrushPaint przyjmuje pędzel i wzorek (MacPaint.p:2024). Pędzel i spray nie malują „czernią”; malują zawsze przez aktualnie wybrany wzorek. Przejąłem to co do joty i zmieniło to odczucie rysowania — wybór wzorka przestaje być opcją wypełnienia, a staje się samą farbą.
Trace Edges ma ukryty wariant. Przytrzymanie Shifta zmienia odsunięcie obrysu z 2 na 3, co w źródle opatrzono komentarzem { asymmetric shadow } (MacPaint.p:1898). Jednowierszowy easter egg z 1984 roku — zachowany.
Tekst jest litym tuszem, a źródło naprawiło mój błąd. Podczas testów dotykowych wpisany tekst czasem nie utrwalał ani jednego piksela. Przyczyna: moje zatwierdzanie tekstu przepuszczało piksele glifów przez wzorek wypełnienia, więc rzadki wzorek po cichu połykał litery. Oryginał nigdy tego nie robi — tekst rysuje się litym tuszem pierwszego planu niezależnie od wzorka (UpdateText/PatchText, MacPaint.p:992–1106). Lektura procedury okazała się szybsza niż debugowanie własnego założenia i rozstrzygnęła poprawkę ponad wszelką dyskusję.
I jeszcze jedno znalezisko, dla porządku: PaintAsm.a zawiera funkcję o nazwie Monkey — punkt zaczepienia testera obciążeniowego z losowym wejściem, którego używał zespół Macintosha, strzeżony flagą o nazwie MonkeyLives. Atkinson dostarczył swoją aparaturę testową w tym samym pliku co swój blitter. Rzemieślnicy zostawiają przyrządy na warsztacie.
Co zostawiłem w spokoju, a co zmieniłem
Zasadą projektową była wierność, więc odstępstwa są nieliczne, celowe i spisane wewnątrz aplikacji — okno dialogowe About wymienia je tak, jak wydanie faksymilowe ujawnia swoje odchylenia:
- 16-kolorowa paleta ponad 1-bitowym silnikiem. Dithering i ścieżka eksportu .mac dają autentyczną monochromatyczność zawsze, gdy jest potrzebna.
- Stos cofania o 100 krokach. Oryginał miał dokładnie jeden poziom cofania, bo Atkinson utrzymywał dwa pozaekranowe bufory wielkości okna — stan bieżący i poprzedni — i je zamieniał.4 To była heroiczna odpowiedź na 128K RAM-u. Odtwarzanie samego ograniczenia byłoby cosplayem; model pamięci, na który odpowiadało, już nie istnieje.
- Wybieralne rozmiary gumki, opcjonalny tryb rozproszonego sprayu i odrysowywanie obrazu referencyjnego. Dodatki — wszystkie domyślnie wyłączone albo jawnie współczesne, żaden nie wypiera oryginalnego zachowania.
Równie celowo część powierzchni oryginału nie została przeniesiona: cykl życia dokumentu dyskowego (Save, Save As, Revert, Close) należy do maszyny opartej na dyskietkach i zastępuje go ciągły autozapis plus jawne eksporty. Ale File > Print przetrwał — PrintDoc (MacPaint.p:4307) domyka oryginalne menu File, a wydruk renderuje samą grafikę, z pikselową ostrością, nigdy elementy interfejsu przeglądarki.
Najdziwniejszy błąd w całym projekcie wcale nie był problemem z 1984 roku. Zapisywanie plików po cichu zawodziło tygodniami, bo mój własny skrypt analityczny przechwytywał kliknięcia w odnośniki — w tym kliknięcia w adresy blob: — a kod zapisu unieważniał adres URL bloba synchronicznie tuż po kliknięciu, zanim przeglądarka zaczęła pobieranie. Program z 1984 roku nie walczy z własną telemetrią. Rekonstruowanie go w 2026 roku — najwyraźniej tak.
Czas coś narysować
Rozpoznawalne ikony narzędzi — lasso, rączka, spray, wiadro z farbą — narysowała Susan Kare, o której dyscyplinie 32×32 pikseli pisałem w serii o filozofii projektowania. Zachowania pod nimi napisał Bill Atkinson, zmarły w czerwcu 2025 roku.6 Publikacja Computer History Museum sprawia, że jego program można studiować, można się z nim konfrontować i można go odtwarzać uczciwie zamiast w przybliżeniu — a to, jak sądzę, najlepszy rodzaj pomnika dla oprogramowania.
PixelPaint działa pod adresem /paint, obok pozostałych interaktywnych eksploracji na tej stronie. Na iPadzie da się rysować palcem. Dwukrotne kliknięcie ołówka pokazuje FatBits. Wystarczy coś narysować, zapisać jako plik .mac — i wiedzieć, że Macintosh z 1984 roku mógłby ten plik otworzyć.
FAQ
Czy oryginalny kod źródłowy MacPainta jest dostępny?
Tak. Computer History Museum opublikowało kod źródłowy MacPainta 1.3 (oraz bibliotekę graficzną QuickDraw) w lipcu 2010 roku, za zgodą Apple, do użytku niekomercyjnego.1 Publikacja figuruje w katalogu CHM pod numerem akcesji 1026580762 i obejmuje główny program w Pascalu (MacPaint.p) oraz pomocnicze pliki asemblera 68000. Oficjalna kopia lustrzana znajduje się na GitHubie, na koncie Computer History Museum.3
Czym jest kompresja PackBits?
PackBits to schemat kodowania długości serii, którym MacPaint kompresował dokumenty: każda linia skanowania jest pakowana jako serie literalne i serie powtórzeń, co dobrze sprawdza się na obrazach 1-bitowych pełnych bieli i powtarzających się wzorków. Pascal MacPainta deklaruje PackBits i UnpackBits jako procedury zewnętrzne (MacPaint.p:420–421), a do systemowej implementacji 68000 sięga przez swoją asemblerową warstwę pośredniczącą. Plik MacPainta to 512-bajtowy nagłówek, a po nim 720 skompresowanych PackBits wierszy po 72 bajty każdy — pełna strona 576×720.
Czym jest dithering Atkinsona?
Dithering Atkinsona to algorytm dyfuzji błędu, który Bill Atkinson opracował do konwersji obrazów w skali szarości na 1-bitowy wyświetlacz Macintosha. Każdy piksel jest progowany do czerni lub bieli, a powstały błąd dzielony przez 8 i rozprowadzany do sześciu sąsiednich pikseli — przy czym pozostałe dwie ósme są celowo odrzucane, a nie propagowane. To właśnie gubienie części błędu nadaje obrazom z ditheringiem Atkinsona ich charakterystyczny wysoki kontrast. PixelPaint używa go do konwersji kolorowych rysunków na 1 bit przy eksporcie .mac oraz do jednobitowego podglądu na żywo.
Jak duży jest dokument MacPainta?
576×720 pikseli, na stałe — zadeklarowane jako stałe w źródle (MacPaint.p:108–109). Przy 72 DPI Macintosha to dokładnie 8×10 cali, czyli strona nadająca się do druku. Ekran nigdy nie pokazywał całego dokumentu naraz: okno rysowania było ruchomym widocznym obszarem na stronie, przesuwanym rączką albo przestawianym przez Show Page. PixelPaint odtwarza ten sam model dokumentu, z widocznym obszarem włącznie.
Źródła
-
Leonard J. Shustek, “MacPaint and QuickDraw Source Code,” blog Computer History Museum, 18 lipca 2010. Ogłoszenie publikacji; dokumentuje zgodę Apple i licencję niekomercyjną, zawiera też historię programu. ↩↩
-
Katalog zbiorów Computer History Museum, “MacPaint source code,” numer akcesji 102658076. ↩↩
-
Computer History Museum, Historical Source Code: MacPaint repository, GitHub. Oficjalna kopia lustrzana opublikowanych plików źródłowych. ↩
-
Andy Hertzfeld, “MacPaint Evolution,” Folklore.org. Podstawowe źródło historii rozwoju MacPainta, w tym dwóch pozaekranowych buforów wielkości okna (stan bieżący i poprzedni), stojących za rysowaniem bez migotania i jednopoziomowym cofaniem. ↩↩
-
Infinite Mac — klasyczne systemy Macintosha, w tym MacPaint, emulowane w przeglądarce. Użyty jako wyrocznia działającego oryginału przy porównaniach zachowań. ↩
-
Adam Engst, “Bill Atkinson Dies from Pancreatic Cancer at 74,” TidBITS, 7 czerwca 2025. ↩