MacPaint im Browser nachbauen: Der Quellcode von 1984 ist die Spezifikation
PixelPaint ist ein funktionsfähiger Nachbau von MacPaint 1.3, der in Ihrem Browser unter /paint läuft und Verhalten für Verhalten gegen Bill Atkinsons originalen Pascal-Quellcode geprüft wurde, den das Computer History Museum 2010 veröffentlicht hat. Klicken Sie doppelt auf den Radiergummi, und er löscht das gesamte Fenster, um Ihnen anschließend das zuvor verwendete Werkzeug zurückzugeben — denn genau das tut ChooseTool in Zeile 3651 von MacPaint.p.
{.answer-block}
2010 veröffentlichte das Computer History Museum mit Apples Erlaubnis den Quellcode von MacPaint 1.3: der Anwendung, die Bill Atkinson im Januar 1984 mit dem ursprünglichen Macintosh auslieferte.1 Im Katalog des Museums ist die Freigabe unter der Zugangsnummer 102658076 verzeichnet.2 Auf meinem Rechner umfasst MacPaint.p 5.804 Zeilen Apple Pascal und PaintAsm.a 2.738 Zeilen 68000-Assembler. Zeile 3 der Pascal-Datei lautet vollständig:
{ BitMap Painting Program by Bill Atkinson }
Diese Freigabe verändert den Maßstab, an dem sich ein Nachbau messen lassen muss. Vorher bedeutete der Nachbau von MacPaint, an Screenshots und Emulatoren zu blinzeln und zu raten. Nachher gibt es eine gesicherte Wahrheit. Als ich beschloss, PixelPaint ordentlich fertigzustellen, war die Regel, die ich mir setzte, einfach: Kein Verhalten geht auf eine Vermutung hin in Produktion, wenn die Antwort in einer Datei steht, die ich lesen kann. Die Lizenz ist nicht-kommerziell, und die Portierung ist verhaltensbasiert — ich las den Pascal-Code, um zu verstehen, was das Programm tut, und setzte das dann von Grund auf in JavaScript um, ohne je eine Zeile Code zu übersetzen.
In diesem Beitrag geht es darum, was diese Regel gekostet und was sie eingebracht hat. Die Kurzfassung: Der Quellcode ist notwendig und unzureichend. Das Verhalten des Programms lebt zwischen den Zeilen — in Konstanten, in Bitmasken, in Kommentaren, in der Form einer Prozedur — und es herauszuarbeiten ist Archäologie, keine Abschrift.
Der Quellcode ist nicht das Verhalten
Ein originalgetreuer Nachbau braucht drei Instrumente, und am Ende habe ich alle drei genutzt:
- Der Quellcode als Spezifikation. Jedes strittige Verhalten wird geklärt, indem man die Prozedur liest, die es umsetzt — mit Zeilenangabe zitiert.
- Ein laufendes Original als Orakel. Infinite Mac startet echte Macs aus der System-Ära im Browser, mit echtem MacPaint auf der Diskette.5 Wenn der Quellcode beim Gefühl mehrdeutig war — dem Takt der Sprühdose, der Pinselinterpolation bei Tempo —, entschied der Emulator die Frage.
- Eine unabhängige Implementierung als Gegenprobe. Für das Dateiformat schrieb ich einen zweiten Decoder in Python, der keinen Code mit der App teilt, und verlangte, dass beide in beide Richtungen Byte für Byte übereinstimmen.
Das Instrument, das man nicht verwenden kann, ist die Erinnerung. Weder meine noch die des Internets. Das meiste von dem, was „jeder weiß” über MacPaint, erweist sich als unterspezifiziert, sobald man einen Pixel an einer exakten Koordinate erscheinen lassen muss.
Was ein Doppelklick 1984 bedeutete
Hier ein Verhalten, das kein Screenshot verrät. In MacPaint ist ein Doppelklick auf ein Werkzeug in der Palette ein Befehl. Die Verteilung steckt in einer einzigen Prozedur, ChooseTool, unter MacPaint.p:3651–3699:
- Radiergummi: das gesamte Fenster löschen und dann zum zuvor gewählten Werkzeug zurückkehren.
- Pinsel: die Auswahl der Pinselform öffnen.
- Auswahlrahmen: das gesamte Fenster auswählen.
- Greifhand: „Seite anzeigen” öffnen.
- Stift: FatBits umschalten, den Zoom auf Pixelebene.
Der Radiergummi-Fall hat ein Detail, das nur der Quellcode preisgibt. In Zeile 3643, noch vor alldem, steht die Absicherung:
IF theTool <> eraseTool THEN prevTool := theTool;
Der Radiergummi wird nie zum „vorherigen Werkzeug”. Wenn ein Doppelklick also alles löscht, gibt Ihnen das Programm den Pinsel oder Stift zurück, mit dem Sie tatsächlich gearbeitet haben — der Radiergummi war ein Besucher, kein Ziel. Atkinsons Kommentar an der Rücksprungzeile sagt es unverblümt: { we wont need the eraser anymore }. Das ist Interaktionsdesign, ausgedrückt als eine einzige Bedingung, und von außen unsichtbar, bis einem auffällt, dass MacPaint einen nach dem Löschen nie auf dem Radiergummi sitzen lässt. PixelPaint setzt alle fünf Doppelklick-Verhalten aus dieser Prozedur um, und eine automatisierte Browser-Testsuite prüft jedes davon von Anfang bis Ende.
Im Auswahlrahmen-Fall verbirgt sich ein zweites Detail. Wenn der Doppelklick das gesamte Fenster auswählt, addiert der Quellcode rechts und unten eine Eins auf das Rechteck, bevor er die Auswahl setzt. Was uns zu den Off-by-one-Fehlern bringt.
Zwei Off-by-one-Fehler — und wer recht hatte
Mitten im Projekt wies ein Prüfdurchgang auf zwei Abweichungen zwischen Vorschau und tatsächlichem Ergebnis in meinem Build hin:
- Der Auswahlrahmen schien einen Pixel weniger zu erfassen als seine Gummiband-Vorschau.
- Der Abdruck des Radiergummis war einen Pixel größer als seine Cursor-Vorschau.
Beides sind Fehler, die man in dreißig Sekunden „beheben” könnte, indem man ein +1 verschiebt — in die eine oder andere Richtung. Der ganze Sinn, den Quellcode zu haben, ist, dass man nicht wählen darf. Man schlägt nach, welche Seite falsch liegt.
Der Auswahlrahmen war kein Fehler. QuickDraw-Rechtecke schließen unten und rechts aus: Ein Rechteck von (10,10) bis (20,20) umspannt zehn Pixel, nicht elf. Das Gummiband und die Erfassung in meinem Build stimmten unter dieser Konvention bereits überein — beim Ziehen von 10,10 nach 20,20 wird genau 10×10 ausgewählt. Was der Prüfdurchgang tatsächlich verglichen hatte, war die Vorschau der Form-Werkzeuge (die korrekt den letzten Pixel einer einschließenden Spanne enthält) gegen die ausschließende Erfassung des Auswahlrahmens. Zwei verschiedene Konventionen, beide korrekt, direkt nebeneinander. Ergebnis: nichts ändern, aufschreiben, warum.
Der Radiergummi war ein Fehler — meiner. Im Original entspricht der Radiergummi-Block exakt seinem Cursor: ein Quadrat von 16×16, unverändert gestempelt (EraseSome, MacPaint.p:2210, unter Verwendung der eigenen Maske des Werkzeug-Cursors). Mein Abdruck wurde als 2*floor(size/2)+1 berechnet, wodurch ein 8-Pixel-Radiergummi ein 9 Pixel breites Loch löschte. Behoben, sodass der Abdruck genau size Pixel umspannt: Ein 8-Pixel-Radiergummi löscht nun die Spalten 16 bis 23 und lässt 15 und 24 unberührt, pixelweise verifiziert. In FatBits schrumpft der Radiergummi auf genau 2×2, was ebenfalls im Quellcode steht (MacPaint.p:2214).
Die Regel, die aus diesem Paar hervorging, wurde zum Rückgrat des Projekts: Wenn Vorschau und Aktion voneinander abweichen, entscheidet das Original, welche von beiden lügt.
Die Seite, nicht die Leinwand
MacPaints strukturellste Idee lässt sich leicht übersehen, weil sie räumlich ist. Das Dokument ist nicht das Fenster. Das Dokument ist eine feste Seite von 576×720 Pixeln — deklariert als Konstanten zur Kompilierzeit unter MacPaint.p:108–109 —, und der Zeichenbereich auf dem Bildschirm ist ein Fenster darauf. Die Greifhand verschiebt das Fenster über die Seite (ScrollDoc, :2778); „Seite anzeigen” (ShowPage, :4074) zoomt auf das ganze Blatt hinaus und lässt Sie das Fensterrechteck an eine neue Stelle ziehen. Bei 72 DPI sind 576×720 genau 8×10 Zoll: Das Dokument war für Papier bemessen, nicht für den Bildschirm.
PixelPaint hatte ursprünglich einen Puffer in der Größe des Ansichtsfensters, was bedeutete: Es hatte eine Leinwand, wo MacPaint ein Dokument hatte. Es um das echte Modell herum neu aufzubauen war die größte Einzeländerung im Projekt, und dabei kam eine durch und durch 2026er Einschränkung zum Vorschein: iOS begrenzt den Sicherungsspeicher einer Leinwand auf etwa 16,7 Megapixel. Naiv die volle Seite mal dem FatBits-Zoom zu belegen würde 26,5 Megapixel verlangen — eine Leinwand, die auf den iPads, auf denen das laufen sollte, stillschweigend leer rendert. Die Portierung hält die Anzeige-Leinwände in Fenstergröße und wendet stattdessen eine Ansichtstransformation im Dokumentraum an; der Sicherungsspeicher maß bei 8×-Zoom 0,42 Megapixel, und ein Frame aus Zeichnen plus Rendern der ganzen Seite lag bei 6,4 Millisekunden — unter einem Frame bei 60 Hz. Atkinson löste ein Speicherbudget von 128K mit versteckten Offscreen-Puffern;4 die Browser-Portierung löst eine versteckte Zuteilungsobergrenze mit einer Transformation. Dieselbe Disziplin, eine andere Wand.
Dateien, die ein Mac von 1984 lesen kann
Ein Nachbau, der keine Dokumente mit dem Original austauschen kann, ist ein Diorama. MacPaints Dateiformat wird vom Quellcode selbst dokumentiert: ein 512-Byte-Header, dann die Seite als 720 Scanlines zu je 72 Byte, komprimiert mit PackBits — einem Lauflängenverfahren, das die Pascal-Seite nie implementiert, sondern nur deklariert (PackBits/UnpackBits, als EXTERNAL markiert unter MacPaint.p:420–421; der Assembler-Klebecode in MyTools.a löst sie als System-Traps aus). Der Header trägt die Musterpalette des Programms, sodass sich ein Dokument die Muster merkt, mit denen es gemalt wurde.
PixelPaint liest und schreibt dieses Format. Beim Export läuft die Seite durch Atkinsons eigenes Fehlerdiffusions-Dithering, um auf 1 Bit zu kommen — Schwellenwert bei 128, der Fehler jedes Pixels in Achtel aufgeteilt und an sechs Nachbarn weitergegeben, wobei zwei Achtel bewusst verworfen werden, was dem Atkinson-Dithering seinen kräftigen Kontrast verleiht. Reine Schwarz-Weiß-Zeichnungen gehen unverändert hindurch, weil ihr Fehler identisch null ist. Es liegt eine angenehme Kreisförmigkeit darin, Bill Atkinsons Dithering-Algorithmus zu verwenden, um Bill Atkinsons Dateiformat zu schreiben.
Bei der Verifizierung zahlte sich das Instrument der unabhängigen Implementierung aus. Der PackBits-Codec in der App durchläuft Test-Fixtures byteidentisch hin und zurück. Eine exportierte Datei, dekodiert durch die separate Python-Implementierung, ergab die korrekte Header-Version, intakte Muster und genau 720 Scanlines zu 72 Byte, bei denen jedes Byte verbraucht wurde. Eine von der Python-Seite kodierte Datei — in MacBinary verpackt, das der Importer am Dateityp an Offset 65 erkennt — öffnete sich in PixelPaint mit Rand und Diagonalen, die auf den berechneten Pixeln landeten. Exportieren, Löschen, Wieder-Importieren reproduzierte den gepackten Zustand hash-identisch. Zwei Implementierungen, beide Richtungen, kein geteilter Code.
Zwischen den Zeilen
Die tiefsten Einschnitte kamen von Details, die keine Funktionsliste je zutage fördern würde — Dinge, die man nur durch Lesen findet.
Das Raster ist eine Bitmaske. MacPaints 8-Pixel-Rasterfang gilt nicht für jedes Werkzeug. ChooseTool entscheidet über die Eignung, indem es den Werkzeugindex gegen eine nackte Hex-Konstante prüft, $50BF3000, wobei die menschenlesbare Pascal-Menge als Kommentar zurückgelassen wurde. Der Fang selbst rundet auf den nächsten Wert, umgesetzt als Abschneiden auf 8 nach Addition von 4 (GridPoint, MacPaint.p:513). PixelPaint hält sich an genau diese Werkzeugmenge: Auswahlrahmen, Text, Linien, Rechtecke, Ovale und Polygone rasten ein; Freihandwerkzeuge nie.
Die Umschalt-Einschränkung ist klüger als horizontal-oder-vertikal. Constrain (MacPaint.p:875) rastet eine Linie auf 45° ein, indem es beide Deltas auf das kleinere begrenzt — und rastet zusätzlich auf rein horizontal oder vertikal ein, wenn eine Achse die andere im Verhältnis zwei zu eins dominiert. Jeder Klon, den ich gesehen habe, setzt die H/V-Hälfte um und lässt das Modell der Diagonaldominanz aus. Der Quellcode hat den vollständigen Algorithmus in dreißig Zeilen.
Das Muster ist die Tinte. Die Signatur von BrushPaint nimmt den Pinsel und ein Muster (MacPaint.p:2024). Pinsel und Sprühdose malen nicht in „Schwarz”; sie malen stets durch das aktuell ausgewählte Muster hindurch. Ich habe das genau so übernommen, und es hat verändert, wie sich das Zeichnen anfühlt — die Musterauswahl ist keine Füllungsoption mehr, sondern wird zur Farbe selbst.
„Kanten nachzeichnen” hat eine versteckte Variante. Halten Sie die Umschalttaste, und der Umriss-Versatz ändert sich von 2 auf 3, im Quellcode mit dem Kommentar { asymmetric shadow } versehen (MacPaint.p:1898). Ein einzeiliges Easter Egg von 1984, bewahrt.
Text ist volle Tinte, und der Quellcode hat meinen Fehler behoben. Beim Testen mit Touch-Eingabe setzte getippter Text manchmal null Pixel. Die Ursache: Mein Text-Commit filterte die Glyphen-Pixel durch das Füllmuster, sodass ein dünnes Muster die Buchstaben stillschweigend verschluckte. Das Original tut das nie — Text zeichnet unabhängig vom Muster als volle Vordergrundtinte (UpdateText/PatchText, MacPaint.p:992–1106). Die Prozedur zu lesen war schneller, als meine eigene Annahme zu debuggen, und es klärte die Behebung ohne jede Diskussion.
Noch ein Fund fürs Protokoll: PaintAsm.a enthält eine Funktion namens Monkey — den Einstiegspunkt für den Stresstester mit Zufallseingaben, den das Macintosh-Team nutzte, abgesichert durch ein Flag namens MonkeyLives. Atkinson lieferte sein Testgerüst in derselben Datei aus wie seinen Blitter. Handwerker lassen ihre Vorrichtungen auf der Werkbank liegen.
Was ich unangetastet ließ und was ich änderte
Originaltreue war das Gestaltungsprinzip, daher sind die Abweichungen wenige, bewusst und in der App festgehalten — der Über-Dialog listet sie auf, so wie eine Faksimile-Ausgabe ihre Abweichungen offenlegt:
- Eine 16-Farben-Palette über der 1-Bit-Engine. Der Dither- und .mac-Exportpfad liefert Ihnen das authentische Monochrom, wann immer Sie es möchten.
- Ein Rückgängig-Stapel mit 100 Schritten. Das Original hatte genau eine Rückgängig-Stufe, weil Atkinson zwei fenstergroße Offscreen-Puffer vorhielt — aktueller Zustand und vorheriger Zustand — und sie vertauschte.4 Das war eine heldenhafte Antwort auf 128K RAM. Die Beschränkung nachzubauen wäre Cosplay; das Speichermodell, auf das sie antwortete, existiert nicht mehr.
- Wählbare Radiergummi-Größen, ein optionaler gestreuter Sprühmodus und das Nachzeichnen von Referenzbildern. Ergänzungen, alle standardmäßig aus oder eindeutig modern, keine verdrängt ein Originalverhalten.
Ebenso bewusst wurde ein Teil der Oberfläche des Originals nicht portiert: Der Lebenszyklus des Disketten-Dokuments (Sichern, Sichern unter, Zurücksetzen, Schließen) gehört zu einer diskettenbasierten Maschine und wird durch fortlaufendes automatisches Sichern plus ausdrückliche Exporte ersetzt. Doch „Ablage > Drucken” überlebt — PrintDoc (MacPaint.p:4307) schließt das ursprüngliche Ablage-Menü ab, und der Druck gibt allein das Bildwerk wieder, pixelscharf, nie die Browser-Oberfläche.
Der seltsamste Fehler im ganzen Projekt war überhaupt kein Problem von 1984. Das Speichern von Dateien schlug wochenlang stillschweigend fehl, weil mein eigenes Analytics-Skript Klicks auf Anker abfing — darunter Klicks auf blob:-URLs — und der Speichercode die Blob-URL synchron nach dem Klick widerrief, bevor der Browser den Download begann. Ein Programm von 1984 kämpft nicht gegen seine eigene Telemetrie. Eines im Jahr 2026 nachzubauen offenbar schon.
Zeichnen Sie etwas
Die Werkzeugsymbole, die Sie wiedererkennen werden — Lasso, Greifhand, Sprühdose, Farbeimer —, wurden von Susan Kare gezeichnet, über deren 32×32-Pixel-Disziplin ich in der Serie zur Designphilosophie geschrieben habe. Die Verhalten darunter schrieb Bill Atkinson, der im Juni 2025 verstarb.6 Die Freigabe durch das Computer History Museum bedeutet, dass sich sein Programm studieren, überprüfen und ehrlich statt nur annähernd nachbauen lässt — was, wie ich finde, die beste Art von Denkmal für Software ist.
PixelPaint ist live unter /paint, neben den übrigen interaktiven Erkundungen auf dieser Website. Es funktioniert auf einem iPad mit dem Finger. Klicken Sie doppelt auf den Stift, um FatBits zu sehen. Zeichnen Sie etwas, speichern Sie es als .mac-Datei, und wissen Sie, dass ein Macintosh von 1984 es öffnen könnte.
FAQ
Ist der originale MacPaint-Quellcode verfügbar?
Ja. Das Computer History Museum gab den Quellcode von MacPaint 1.3 (und die QuickDraw-Grafikbibliothek) im Juli 2010 mit Apples Erlaubnis zur nicht-kommerziellen Nutzung frei.1 Die Freigabe trägt die CHM-Katalog-Zugangsnummer 1026580762 und umfasst das Pascal-Hauptprogramm (MacPaint.p) sowie die 68000-Assembler-Hilfsdateien. Ein offizieller Spiegel liegt auf GitHub unter dem Konto des Computer History Museum.3
Was ist PackBits-Kompression?
PackBits ist das Lauflängenkodierungsverfahren, mit dem MacPaint Dokumente komprimierte: Jede Scanline wird als Folge von Literal- und Wiederholungsläufen gepackt, was bei 1-Bit-Bildern voller Weißraum und sich wiederholender Muster gut funktioniert. MacPaints Pascal deklariert PackBits und UnpackBits als externe Routinen (MacPaint.p:420–421) und erreicht die 68000-Implementierung des Systems über seinen Assembler-Klebecode. Eine MacPaint-Datei besteht aus einem 512-Byte-Header, gefolgt von 720 PackBits-komprimierten Zeilen zu je 72 Byte — der vollständigen Seite von 576×720.
Was ist Atkinson-Dithering?
Atkinson-Dithering ist der Fehlerdiffusionsalgorithmus, den Bill Atkinson ersann, um Graustufenbilder für die 1-Bit-Anzeige des Macintosh umzuwandeln. Jeder Pixel wird per Schwellenwert auf Schwarz oder Weiß gesetzt, und der entstehende Fehler wird durch 8 geteilt und auf sechs benachbarte Pixel verteilt — wobei die verbleibenden zwei Achtel absichtlich verworfen statt weitergegeben werden. Einen Teil des Fehlers abzuwerfen ist es, was Atkinson-gerasterten Bildern ihren charakteristischen hohen Kontrast verleiht. PixelPaint nutzt es, um Farbzeichnungen für den .mac-Export auf 1 Bit umzuwandeln, sowie für die Live-1-Bit-Vorschau.
Wie groß ist ein MacPaint-Dokument?
576×720 Pixel, fest — als Konstanten im Quellcode deklariert (MacPaint.p:108–109). Bei den 72 DPI des Macintosh sind das genau 8×10 Zoll, eine druckbare Seite. Der Bildschirm zeigte nie das ganze Dokument auf einmal: Das Zeichenfenster war ein bewegliches Ansichtsfenster auf die Seite, verschiebbar mit der Greifhand oder neu positioniert über „Seite anzeigen”. PixelPaint bildet dasselbe Dokumentmodell nach, samt Ansichtsfenster.
Quellen
-
Leonard J. Shustek, “MacPaint and QuickDraw Source Code,” Computer History Museum Blog, 18. Juli 2010. Die Ankündigung der Freigabe; dokumentiert Apples Erlaubnis und die nicht-kommerzielle Lizenz und enthält die Geschichte des Programms. ↩↩
-
Sammlungskatalog des Computer History Museum, “MacPaint source code,” Zugangsnummer 102658076. ↩↩
-
Computer History Museum, Historical Source Code: MacPaint repository, GitHub. Offizieller Spiegel der freigegebenen Quelldateien. ↩
-
Andy Hertzfeld, “MacPaint Evolution,” Folklore.org. Primärquelle für die Entwicklungsgeschichte von MacPaint, einschließlich der zwei fenstergroßen Offscreen-Puffer (aktueller und vorheriger Zustand) hinter dem flimmerfreien Zeichnen und der einstufigen Rückgängig-Funktion. ↩↩
-
Infinite Mac — klassische Macintosh-Systeme, darunter MacPaint, im Browser emuliert. Als Orakel des laufenden Originals für Verhaltensvergleiche verwendet. ↩
-
Adam Engst, “Bill Atkinson Dies from Pancreatic Cancer at 74,” TidBITS, 7. Juni 2025. ↩