Recreating MacPaint in the Browser: The 1984 Source Code Is the Spec
PixelPaint is a working recreation of MacPaint 1.3 that runs in your browser at /paint, validated behavior-by-behavior against Bill Atkinson’s original Pascal source code, which the Computer History Museum released in 2010. Double-click the eraser and it erases the whole window, then hands you back the tool you had before, because that is what ChooseTool does at line 3651 of MacPaint.p.
{.answer-block}
In 2010, the Computer History Museum, with Apple’s permission, published the source code of MacPaint 1.3: the application Bill Atkinson shipped with the original Macintosh in January 1984.1 The release sits in the museum’s catalog as accession 102658076.2 On my machine, MacPaint.p is 5,804 lines of Apple Pascal and PaintAsm.a is 2,738 lines of 68000 assembly. Line 3 of the Pascal file reads, in its entirety:
{ BitMap Painting Program by Bill Atkinson }
That release changes what a recreation can be held to. Before it, rebuilding MacPaint meant squinting at screenshots and emulators and guessing. After it, there is ground truth. When I decided to finish PixelPaint properly, the rule I set was simple: no behavior ships on a guess when the answer is sitting in a file I can read. The license is non-commercial and the port is behavioral — I read the Pascal to learn what the program does, then implemented that in JavaScript from scratch, never translating a line of code.
This post is about what that rule cost and what it bought. The short version: the source is necessary and insufficient. The program’s behavior lives between the lines — in constants, in bitmasks, in comments, in the shape of a procedure — and extracting it is archaeology, not transcription.
The Source Is Not the Behavior
A faithful recreation needs three instruments, and I ended up using all of them:
- The source as spec. Every disputed behavior resolved by reading the procedure that implements it, cited by line.
- A running original as oracle. Infinite Mac boots real System-era Macs in the browser, with real MacPaint on the disk.5 When the source was ambiguous about feel — spray cadence, brush interpolation at speed — the emulator settled it.
- An independent implementation as cross-check. For the file format, I wrote a second decoder in Python, sharing no code with the app, and required the two to agree byte-for-byte in both directions.
The instrument you cannot use is memory. Mine, or the internet’s. Most of what “everyone knows” about MacPaint turns out to be underspecified the moment you have to make a pixel appear at an exact coordinate.
What Double-Clicking Meant in 1984
Here is a behavior no screenshot can tell you. In MacPaint, double-clicking a tool in the palette is a command. The dispatch lives in one procedure, ChooseTool, at MacPaint.p:3651–3699:
- Eraser: erase the entire window, then revert to the previously selected tool.
- Brush: open the brush-shape picker.
- Marquee: select the entire window.
- Grabber: open Show Page.
- Pencil: toggle FatBits, the pixel-level zoom.
The eraser case has a detail that only the source reveals. At line 3643, before any of this, sits the guard:
IF theTool <> eraseTool THEN prevTool := theTool;
The eraser never becomes the “previous tool.” So when a double-click erases everything, the program hands you back the brush or pencil you were actually working with — the eraser was a visitor, not a destination. Atkinson’s comment on the revert line says it plainly: { we wont need the eraser anymore }. That is interaction design expressed as a single conditional, and it is invisible from the outside until you notice that MacPaint never strands you on the eraser after a clear. PixelPaint implements all five double-click behaviors from this procedure, and an automated browser suite asserts each one end to end.
There is a second detail hiding in the marquee case. When double-click selects the entire window, the source adds one to the right and bottom of the rectangle before setting the selection. Which brings up the off-by-ones.
Two Off-by-Ones, and Who Was Right
Midway through the project, a review pass flagged two preview-versus-actual mismatches in my build:
- The marquee appeared to capture one pixel less than its rubber-band preview.
- The eraser’s stamp was one pixel larger than its cursor preview.
Both are the kind of bug you could “fix” in thirty seconds by nudging a +1 — in either direction. The whole point of having the source is that you do not get to choose. You look up which side is wrong.
The marquee was not a bug. QuickDraw rectangles are bottom/right-exclusive: a rect from (10,10) to (20,20) spans ten pixels, not eleven. The rubber band and the capture in my build already agreed under that convention — dragging 10,10 to 20,20 selects exactly 10×10. What the review had actually compared was the shape tools’ preview (which correctly includes the final pixel of an inclusive span) against the marquee’s exclusive capture. Two different conventions, both correct, sitting next to each other. Resolution: change nothing, write down why.
The eraser was a bug — mine. In the original, the eraser block equals its cursor exactly: a 16×16 square, stamped as-is (EraseSome, MacPaint.p:2210, using the tool cursor’s own mask). My stamp was computed as 2*floor(size/2)+1, which made an 8-pixel eraser erase a 9-pixel-wide hole. Fixed so the stamp spans exactly size pixels: an 8-pixel eraser now erases columns 16 through 23 and leaves 15 and 24 untouched, verified per-pixel. Inside FatBits, the eraser drops to exactly 2×2, which is also in the source (MacPaint.p:2214).
The rule that fell out of this pair became the project’s spine: when the preview and the action disagree, the original decides which one is lying.
The Page, Not the Canvas
MacPaint’s most structural idea is easy to miss because it is spatial. The document is not the window. The document is a fixed 576×720-pixel page — declared as compile-time constants at MacPaint.p:108–109 — and the drawing area on screen is a window onto it. The grabber pans the window across the page (ScrollDoc, :2778); Show Page (ShowPage, :4074) zooms out to the whole sheet and lets you drag the window rectangle to a new spot. At 72 DPI, 576×720 is exactly 8×10 inches: the document was sized for paper, not for the screen.
PixelPaint originally had a viewport-sized buffer, which meant it had a canvas where MacPaint had a document. Rebuilding it around the real model was the biggest single change in the project, and it surfaced a thoroughly 2026 constraint: iOS caps a canvas’s backing store around 16.7 megapixels. Naively allocating the full page times the FatBits zoom would demand 26.5 megapixels — a canvas that silently renders blank on the iPads I wanted this to work on. The port keeps display canvases viewport-sized and applies a document-space view transform instead; the backing store measured 0.42 megapixels at 8× zoom, and a full-page draw-plus-render frame came in at 6.4 milliseconds, under one frame at 60 Hz. Atkinson solved a 128K memory budget with hidden offscreen buffers;4 the browser port solves a hidden allocation ceiling with a transform. Same discipline, different wall.
Files a 1984 Mac Can Read
A recreation that cannot exchange documents with the original is a diorama. MacPaint’s file format is documented by the source itself: a 512-byte header, then the page as 720 scanlines of 72 bytes each, compressed with PackBits — a run-length scheme the Pascal side never implements, only declares (PackBits/UnpackBits, marked EXTERNAL at MacPaint.p:420–421; the assembly glue in MyTools.a dispatches them as system traps). The header carries the program’s pattern palette, so a document remembers the patterns it was painted with.
PixelPaint reads and writes that format. Export runs the page through Atkinson’s own error-diffusion dither to get to 1-bit — threshold at 128, each pixel’s error split into eighths and pushed to six neighbors, with two eighths deliberately discarded, which is what gives Atkinson dithering its punchy contrast. Pure black-and-white drawings pass through untouched, because their error is identically zero. There is a pleasing circularity in using Bill Atkinson’s dithering algorithm to write Bill Atkinson’s file format.
Verification is where the independent-implementation instrument earned its keep. The in-app PackBits codec round-trips fixtures byte-identically. An exported file, decoded by the separate Python implementation, produced the correct header version, intact patterns, and exactly 720 scanlines of 72 bytes with every byte consumed. A file encoded by the Python side — wrapped in MacBinary, which the importer detects by the file type at offset 65 — opened in PixelPaint with its border and diagonals landing on the computed pixels. Export, clear, re-import reproduced the packed state hash-identically. Two implementations, both directions, no shared code.
Between the Lines
The deepest cuts came from details that no feature list would ever surface — things you find only by reading.
The grid is a bitmask. MacPaint’s 8-pixel grid snap does not apply to every tool. ChooseTool decides eligibility by testing the tool index against a bare hex constant, $50BF3000, with the human-readable Pascal set left behind as a comment. The snap itself is round-to-nearest, implemented as truncate-to-8 after adding 4 (GridPoint, MacPaint.p:513). PixelPaint honors the exact tool set: marquee, text, lines, rectangles, ovals, polygons snap; freehand tools never do.
Shift-constrain is smarter than horizontal-or-vertical. Constrain (MacPaint.p:875) snaps a line to 45° by clamping both deltas to the smaller one — and additionally snaps to pure horizontal or vertical when one axis dominates the other two-to-one. Every clone I have seen implements the H/V half and skips the diagonal dominance model. The source has the full algorithm in thirty lines.
The pattern is the ink. BrushPaint’s signature takes the brush and a pattern (MacPaint.p:2024). The brush and spray can do not paint in “black”; they paint through the currently selected pattern, always. I adopted this exactly, and it changed how drawing feels — pattern selection stops being a fill option and becomes the paint itself.
Trace Edges has a hidden variant. Hold Shift and the outline offset changes from 2 to 3, annotated in the source with the comment { asymmetric shadow } (MacPaint.p:1898). A one-line easter egg from 1984, preserved.
Text is solid ink, and the source fixed my bug. Under touch testing, typed text sometimes committed zero pixels. The cause: my text commit gated glyph pixels through the fill pattern, so a sparse pattern silently swallowed the letters. The original never does this — text draws as solid foreground ink regardless of pattern (UpdateText/PatchText, MacPaint.p:992–1106). Reading the procedure was faster than debugging my own assumption, and it settled the fix beyond argument.
One more find, for the record: PaintAsm.a contains a function called Monkey — the hook for the random-input stress tester the Macintosh team used, guarded by a flag named MonkeyLives. Atkinson shipped his test harness in the same file as his blitter. Craftsmen leave their jigs on the bench.
What I Left Alone, and What I Changed
Fidelity was the design principle, so the departures are few, deliberate, and written down inside the app — the About dialog lists them, the way a facsimile edition discloses its deviations:
- A 16-color palette on top of the 1-bit engine. The dither and .mac export path give you the authentic monochrome whenever you want it.
- A 100-step undo stack. The original had exactly one level of undo, because Atkinson kept two window-sized offscreen buffers — current state and previous state — and swapped them.4 That was a heroic answer to 128K of RAM. Recreating the limitation would be cosplay; the memory model it answered no longer exists.
- Selectable eraser sizes, an optional scattered spray mode, and reference-image tracing. Additions, all off by default or clearly modern, none displacing an original behavior.
Just as deliberately, some of the original’s surface was not ported: the disk-document lifecycle (Save, Save As, Revert, Close) belongs to a floppy-based machine and is replaced by continuous autosave plus explicit exports. But File > Print survives — PrintDoc (MacPaint.p:4307) closes out the original File menu, and printing renders the artwork alone, pixel-crisp, never the browser chrome.
The strangest bug in the whole project was not a 1984 problem at all. Saving files silently failed for weeks because my own analytics script was intercepting anchor clicks — including clicks on blob: URLs — and the save code revoked the blob URL synchronously after the click, before the browser began the download. A program from 1984 does not fight its own telemetry. Recreating one in 2026 apparently does.
Go Draw Something
The tool icons you will recognize — lasso, grabber, spray can, paint bucket — were drawn by Susan Kare, whose 32×32 pixel discipline I wrote about in the design philosophy series. The behaviors under them were written by Bill Atkinson, who died in June 2025.6 The Computer History Museum’s release means his program can be studied, checked against, and rebuilt honestly instead of approximately — which is, I think, the best kind of monument for software.
PixelPaint is live at /paint, alongside the other interactive explorations on this site. It works on an iPad with a finger. Double-click the pencil to see FatBits. Draw something, save it as a .mac file, and know that a Macintosh from 1984 could open it.
FAQ
Is the original MacPaint source code available?
Yes. The Computer History Museum released the MacPaint 1.3 source code (and the QuickDraw graphics library) in July 2010 with Apple’s permission, for non-commercial use.1 The release is CHM catalog accession 1026580762 and includes the main Pascal program (MacPaint.p) plus the 68000 assembly support files. An official mirror is on GitHub under the Computer History Museum’s account.3
What is PackBits compression?
PackBits is the run-length encoding scheme MacPaint used to compress documents: each scanline is packed as literal runs and repeat runs, which works well on 1-bit images full of white space and repeating patterns. MacPaint’s Pascal declares PackBits and UnpackBits as external routines (MacPaint.p:420–421) and reaches the system’s 68000 implementation through its assembly glue. A MacPaint file is a 512-byte header followed by 720 PackBits-compressed rows of 72 bytes each — the full 576×720 page.
What is Atkinson dithering?
Atkinson dithering is the error-diffusion algorithm Bill Atkinson devised for converting grayscale images to the Macintosh’s 1-bit display. Each pixel is thresholded to black or white, and the resulting error is divided by 8 and distributed to six neighboring pixels — with the remaining two eighths intentionally discarded rather than propagated. Shedding part of the error is what gives Atkinson-dithered images their characteristic high contrast. PixelPaint uses it to convert color drawings to 1-bit for .mac export and for the live one-bit preview.
How big is a MacPaint document?
576×720 pixels, fixed — declared as constants in the source (MacPaint.p:108–109). At the Macintosh’s 72 DPI that is exactly 8×10 inches, a printable page. The screen never showed the whole document at once: the drawing window was a movable viewport onto the page, panned with the grabber or repositioned via Show Page. PixelPaint recreates the same document model, viewport and all.
Sources
-
Leonard J. Shustek, “MacPaint and QuickDraw Source Code,” Computer History Museum blog, July 18, 2010. The release announcement; documents Apple’s permission and the non-commercial license, and includes the program’s history. ↩↩
-
Computer History Museum collection catalog, “MacPaint source code,” accession 102658076. ↩↩
-
Computer History Museum, Historical Source Code: MacPaint repository, GitHub. Official mirror of the released source files. ↩
-
Andy Hertzfeld, “MacPaint Evolution,” Folklore.org. Primary source for MacPaint’s development history, including the two offscreen window-sized buffers (current and previous state) behind flicker-free drawing and one-level undo. ↩↩
-
Infinite Mac — classic Macintosh systems, including MacPaint, emulated in the browser. Used as the running-original oracle for behavior comparisons. ↩
-
Adam Engst, “Bill Atkinson Dies from Pancreatic Cancer at 74,” TidBITS, June 7, 2025. ↩