MacPaintをブラウザで再現する——1984年のソースコードこそが仕様書
PixelPaintは、ブラウザで動くMacPaint 1.3の再現アプリで、/paintで公開しています。Computer History Museumが2010年に公開したBill AtkinsonのオリジナルPascalソースコードと、動作をひとつずつ照合して検証しました。消しゴムをダブルクリックするとウィンドウ全体を消去し、その後は直前に使っていたツールに戻ります——MacPaint.pの3651行目にあるChooseToolがそう動くからです。
{.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でゼロから実装しました。コードは1行も翻訳していません。
この記事は、そのルールに何を支払い、何を得たかについての話です。要約すると、ソースは必要条件であって十分条件ではありません。プログラムの動作は行間に宿っています——定数に、ビットマスクに、コメントに、そしてプロシージャの「かたち」に。それを取り出す作業は書き写しではなく、考古学なのです。
ソースは動作そのものではない
忠実な再現には3つの計器が必要で、結局そのすべてを使うことになりました。
- 仕様書としてのソース。 議論の余地がある動作はすべて、それを実装しているプロシージャを読み、行番号を引いて解決しました。
- オラクルとしての動くオリジナル。 Infinite Macは、System時代の本物のMacをブラウザ上で起動でき、ディスクには本物のMacPaintが入っています。5 ソースが「感触」について曖昧な場合——スプレーの吐出リズム、高速時のブラシ補間——は、エミュレータが決着をつけました。
- クロスチェックとしての独立実装。 ファイルフォーマットについては、アプリとコードを一切共有しないデコーダをPythonでもう1つ書き、双方向でバイト単位の一致を要求しました。
使ってはいけない計器は記憶です。私の記憶も、インターネットの記憶も。MacPaintについて「誰もが知っている」ことの大半は、正確な座標にピクセルを打つ段になると、仕様が足りないことが判明します。
1984年、ダブルクリックは何を意味したか
スクリーンショットからは決してわからない動作があります。MacPaintでは、パレット上のツールをダブルクリックすることがコマンドになっています。そのディスパッチはMacPaint.pの3651〜3699行、ChooseToolという1つのプロシージャに収まっています。
- 消しゴム:ウィンドウ全体を消去し、直前に選択していたツールに戻る。
- ブラシ:ブラシ形状ピッカーを開く。
- マーキー(選択枠):ウィンドウ全体を選択する。
- グラバー(手のひらツール):Show Pageを開く。
- 鉛筆:ピクセル単位のズームであるFatBitsを切り替える。
消しゴムのケースには、ソースを読んで初めてわかるディテールがあります。3643行目、これらの処理より前に、次のガードが置かれています。
IF theTool <> eraseTool THEN prevTool := theTool;
消しゴムは決して「直前のツール」になりません。だからダブルクリックですべてを消したあと、プログラムは実際に作業していたブラシや鉛筆を返してくれます——消しゴムは訪問者であって、目的地ではないのです。復帰処理の行に添えられたAtkinsonのコメントは端的です。{ we wont need the eraser anymore }。これはたった1つの条件分岐として表現されたインタラクションデザインであり、消去のあとMacPaintが消しゴムを握らせたまま放置しないことに気づくまで、外側からは見えません。PixelPaintはこのプロシージャにある5つのダブルクリック動作をすべて実装し、自動ブラウザテストスイートがそれぞれをエンドツーエンドで検証しています。
マーキーのケースには、もう1つのディテールが隠れています。ダブルクリックでウィンドウ全体を選択するとき、ソースは選択範囲を設定する前に、矩形の右端と下端に「1を加えて」います。ここで、オフバイワンの話になります。
2つのオフバイワン、正しかったのはどちらか
プロジェクトの中盤、レビューで私のビルドにプレビューと実動作の不一致が2件見つかりました。
- マーキーが、ラバーバンドのプレビューより1ピクセル小さい範囲をキャプチャしているように見える。
- 消しゴムのスタンプが、カーソルのプレビューより1ピクセル大きい。
どちらも、+1をどちらかの方向にずらせば30秒で「直せて」しまう類のバグです。ソースを持っていることの意味は、まさに自分で選ばなくてよい点にあります。どちら側が間違っているかを調べればよいのです。
マーキーはバグではありませんでした。QuickDrawの矩形は下端・右端を含みません。(10,10)から(20,20)までの矩形は11ピクセルではなく10ピクセルの幅を持ちます。私のビルドのラバーバンドとキャプチャは、すでにこの規約のもとで一致していました——10,10から20,20へドラッグすれば、ちょうど10×10が選択されます。レビューが実際に比較していたのは、図形ツールのプレビュー(包含的な範囲の最終ピクセルを正しく含みます)と、マーキーの排他的なキャプチャでした。2つの異なる規約が、どちらも正しいまま隣り合っていたのです。結論:何も変えず、理由を書き残す。
消しゴムはバグでした——私のバグです。オリジナルでは、消しゴムのブロックはカーソルと正確に一致します。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)。
この2件から導かれたルールが、プロジェクトの背骨になりました。プレビューと実動作が食い違ったら、どちらが嘘をついているかはオリジナルが決める。
キャンバスではなくページ
MacPaintの最も構造的なアイデアは、空間に関わるものであるだけに見落とされがちです。ドキュメントはウィンドウではありません。ドキュメントは576×720ピクセルの固定ページで——MacPaint.pの108〜109行目にコンパイル時定数として宣言されています——画面上の描画領域は、そのページを覗く「窓」にすぎません。グラバーはページの上でウィンドウをパンし(ScrollDoc、:2778)、Show Page(ShowPage、:4074)はシート全体までズームアウトして、ウィンドウ矩形を新しい位置へドラッグさせてくれます。72 DPIでの576×720はちょうど8×10インチ。ドキュメントは画面ではなく、紙に合わせてサイズが決められていたのです。
PixelPaintは元々ビューポートサイズのバッファを持っていました。つまり、MacPaintがドキュメントを持つところに、キャンバスを持っていたわけです。本来のモデルを中心に作り直すことがプロジェクト最大の変更となり、そこで極めて2026年的な制約が浮かび上がりました。iOSはcanvasのバッキングストアを約16.7メガピクセルに制限しています。ページ全体にFatBitsのズーム倍率を掛けたサイズを素朴に確保すると26.5メガピクセルが必要になります——動かしたかったiPadの上では、何も言わず空白のまま描画されるcanvasです。移植版では表示用canvasをビューポートサイズのまま保ち、代わりにドキュメント空間のビュー変換を適用しています。バッキングストアは8倍ズームで実測0.42メガピクセル、ページ全体の描画+レンダリングの1フレームは6.4ミリ秒と、60 Hzの1フレーム以内に収まりました。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分割して6つの近傍に配り、残りの2/8は意図的に捨てます。この「捨て」こそが、Atkinsonディザリング特有のパンチの効いたコントラストを生みます。純粋な白黒の絵は誤差が常にゼロなので、そのまま素通りします。Bill Atkinsonのディザリングアルゴリズムを使ってBill Atkinsonのファイルフォーマットを書き出すことには、心地よい円環があります。
検証こそ、独立実装という計器が真価を発揮した場面です。アプリ内のPackBitsコーデックは、フィクスチャをバイト単位で完全にラウンドトリップします。エクスポートしたファイルを別系統のPython実装でデコードすると、正しいヘッダバージョン、無傷のパターン、そして全バイトを消費し切った72バイト×720本のスキャンラインが得られました。Python側で「エンコードした」ファイル——MacBinaryでラップされており、インポータはオフセット65のファイルタイプで検出します——をPixelPaintで開くと、枠線と対角線が計算どおりのピクセルに載りました。エクスポート、消去、再インポートを行うと、パック済み状態がハッシュ一致で再現されます。2つの実装、双方向、共有コードなし、です。
行間にあるもの
最も深い発見は、どんな機能一覧にも決して現れないディテールから来ました。読むことでしか見つからないものです。
グリッドはビットマスクです。MacPaintの8ピクセルグリッドスナップは、すべてのツールに適用されるわけではありません。ChooseToolは、ツールのインデックスを生の16進定数$50BF3000と突き合わせて適用可否を判定します。人間が読めるPascalの集合表記は、コメントとして残されているだけです。スナップ自体は最近接への丸めで、4を足してから8単位に切り捨てる実装になっています(GridPoint、MacPaint.p:513)。PixelPaintはこのツール集合を正確に踏襲しています。マーキー、テキスト、直線、矩形、楕円、多角形はスナップし、フリーハンド系のツールは決してスナップしません。
Shift制約は「水平か垂直か」より賢い。Constrain(MacPaint.p:875)は、両軸の差分を小さい方に揃えることで線を45°にスナップし——「さらに」一方の軸がもう一方の2倍以上になると、純粋な水平または垂直にスナップします。私が見てきたクローンはどれも水平・垂直の半分だけを実装し、対角優勢のモデルを省いています。ソースには完全なアルゴリズムが30行で書かれています。
パターンこそがインクです。BrushPaintのシグネチャは、ブラシ「とパターン」を受け取ります(MacPaint.p:2024)。ブラシとスプレー缶は「黒」で塗るのではありません。常に、現在選択中のパターンを通して塗ります。私はこれをそのまま採用し、描く感触が変わりました。パターン選択は塗りつぶしのオプションであることをやめ、絵の具そのものになるのです。
Trace Edgesには隠しバリアントがあります。Shiftを押しながら実行すると、輪郭のオフセットが2から3に変わります。ソースには{ asymmetric shadow }というコメントが添えられています(MacPaint.p:1898)。1984年の1行のイースターエッグを、そのまま保存しました。
テキストはベタ塗りのインクであり、ソースが私のバグを直しました。タッチ操作のテストで、入力したテキストが1ピクセルも確定されないことがありました。原因は、私のテキスト確定処理がグリフのピクセルを塗りパターンに通していたことで、まばらなパターンが文字を静かに飲み込んでいたのです。オリジナルは決してそうしません。テキストはパターンに関係なく、常に前景色のベタで描かれます(UpdateText/PatchText、MacPaint.p:992〜1106)。プロシージャを読むほうが自分の思い込みをデバッグするより速く、修正の根拠は議論の余地なく確定しました。
もう1つ、記録のために。PaintAsm.aにはMonkeyという関数があります。Macintoshチームが使っていたランダム入力ストレステスターのフックで、MonkeyLivesという名前のフラグでガードされています。Atkinsonは自分のテストハーネスを、ブリッタと同じファイルに載せて出荷していたのです。職人は治具を作業台に残していくものです。
手を付けなかったもの、変えたもの
忠実さが設計原理だったので、逸脱は少数で、意図的で、アプリの中に明記してあります。Aboutダイアログに一覧を載せました。ファクシミリ版が逸脱箇所を開示するのと同じやり方です。
- 16色パレットを1ビットエンジンの上に載せました。ディザと.macエクスポート経路により、いつでも本来のモノクロに戻れます。
- 100段階のアンドゥスタック。オリジナルのアンドゥはちょうど1段階でした。Atkinsonがウィンドウサイズのオフスクリーンバッファを2枚——現在の状態と直前の状態——確保して入れ替えていたからです。4 それは128KのRAMに対する英雄的な答えでした。この「制限」まで再現するのはコスプレです。それが答えていたメモリモデルは、もう存在しません。
- 消しゴムのサイズ選択、オプションの飛散スプレーモード、参照画像トレース。いずれも追加機能で、デフォルトでは無効か明らかにモダンなものであり、オリジナルの動作を置き換えるものはありません。
同じく意図的に、オリジナルの表層の一部は移植「しません」でした。ディスクドキュメントのライフサイクル(Save、Save As、Revert、Close)はフロッピーベースのマシンのものであり、継続的な自動保存と明示的なエクスポートに置き換えています。ただしFile > Printは生き残りました。PrintDoc(MacPaint.p:4307)はオリジナルのFileメニューを締めくくるプロシージャであり、印刷はブラウザのUIではなく、アートワークだけをピクセルシャープに描画します。
プロジェクト全体で最も奇妙なバグは、1984年の問題ではまったくありませんでした。ファイル保存が数週間にわたり静かに失敗していたのです。原因は、私自身のアナリティクススクリプトがアンカークリックを——blob: URLへのクリックまで含めて——横取りしていたこと。そして保存コードが、ブラウザがダウンロードを始める前に、クリック直後へ同期的にblob URLを失効させていたことでした。1984年のプログラムは自分のテレメトリと戦ったりしません。2026年に再現すると、どうやら戦うことになるようです。
さあ、何か描いてみましょう
見覚えのあるツールアイコン——投げ縄、グラバー、スプレー缶、ペンキバケツ——を描いたのはSusan Kareです。彼女の32×32ピクセルの規律については、デザイン哲学シリーズに書きました。その下で動く挙動を書いたのはBill Atkinsonで、2025年6月に亡くなりました。6 Computer History Museumの公開のおかげで、彼のプログラムは研究され、照合され、近似ではなく誠実に再構築できるようになりました。ソフトウェアにとって、これ以上の記念碑はないと思います。
PixelPaintは/paintで公開中で、このサイトの他のインタラクティブ探索と並んでいます。iPadなら指1本で動きます。鉛筆をダブルクリックしてFatBitsを見てください。何か描いて.macファイルとして保存し、1984年のMacintoshがそのファイルを開けるのだと噛みしめてください。
FAQ
オリジナルの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ディザリングは、グレースケール画像をMacintoshの1ビットディスプレイ向けに変換するためにBill Atkinsonが考案した誤差拡散アルゴリズムです。各ピクセルをしきい値で白か黒に振り分け、生じた誤差を8で割って6つの近傍ピクセルに分配します。残りの2/8は伝播させず、意図的に捨てます。誤差の一部を捨てることこそが、Atkinsonディザ特有の高いコントラストを生むのです。PixelPaintでは、カラーの絵を.macエクスポート用に1ビット化する処理と、ライブの1ビットプレビューに使っています。
MacPaintのドキュメントはどのくらいの大きさですか?
576×720ピクセルの固定サイズで、ソースに定数として宣言されています(MacPaint.p:108〜109)。Macintoshの72 DPIではちょうど8×10インチ、つまり印刷できる1ページ分です。画面にドキュメント全体が一度に表示されることはありませんでした。描画ウィンドウはページを覗く可動ビューポートで、グラバーでパンするか、Show Pageで位置を変えます。PixelPaintは、ビューポートも含めて同じドキュメントモデルを再現しています。
出典
-
Leonard J. Shustek, “MacPaint and QuickDraw Source Code,” Computer History Museum blog、2010年7月18日。公開の告知記事。Appleの許可と非商用ライセンスを記録し、プログラムの歴史も収めています。 ↩↩
-
Computer History Museum collection catalog, “MacPaint source code,” 収蔵番号102658076。 ↩↩
-
Computer History Museum, Historical Source Code: MacPaint repository, GitHub。公開されたソースファイルの公式ミラーです。 ↩
-
Andy Hertzfeld, “MacPaint Evolution,” Folklore.org。MacPaintの開発史に関する一次資料。ちらつきのない描画と1段階アンドゥを支えた、ウィンドウサイズのオフスクリーンバッファ2枚(現在の状態と直前の状態)についての記述を含みます。 ↩↩
-
Infinite Mac——MacPaintを含むクラシックMacintoshシステムをブラウザ上でエミュレートしています。動作比較における「動くオリジナル」のオラクルとして使用しました。 ↩
-
Adam Engst, “Bill Atkinson Dies from Pancreatic Cancer at 74,” TidBITS、2025年6月7日。 ↩