← Tous les articles

Recréer MacPaint dans le navigateur : le code source de 1984 fait office de spécification

PixelPaint est une recréation fonctionnelle de MacPaint 1.3 qui s’exécute dans votre navigateur à l’adresse /paint, validée comportement par comportement face au code source Pascal original de Bill Atkinson, que le Computer History Museum a publié en 2010. Double-cliquez sur la gomme : elle efface toute la fenêtre, puis vous rend l’outil que vous utilisiez auparavant, car c’est précisément ce que fait ChooseTool à la ligne 3651 de MacPaint.p. {.answer-block}

En 2010, le Computer History Museum, avec l’autorisation d’Apple, a publié le code source de MacPaint 1.3 : l’application que Bill Atkinson a livrée avec le Macintosh original en janvier 19841. Cette publication figure au catalogue du musée sous le numéro d’acquisition 1026580762. Sur ma machine, MacPaint.p compte 5 804 lignes d’Apple Pascal et PaintAsm.a en compte 2 738 d’assembleur 68000. La ligne 3 du fichier Pascal se lit, dans son intégralité :

{  BitMap Painting Program by Bill Atkinson }

Cette publication change les exigences auxquelles une recréation peut être soumise. Avant elle, reconstruire MacPaint revenait à scruter des captures d’écran et des émulateurs, puis à deviner. Après elle, il existe une vérité de terrain. Lorsque j’ai décidé de terminer PixelPaint dans les règles, la règle que je me suis fixée était simple : aucun comportement n’est livré sur une supposition quand la réponse se trouve dans un fichier que je peux lire. La licence est non commerciale et le portage est comportemental — j’ai lu le Pascal pour comprendre ce que le programme fait, puis je l’ai implémenté en JavaScript à partir de zéro, sans jamais traduire une seule ligne de code.

Cet article traite de ce que cette règle a coûté et de ce qu’elle a rapporté. En résumé : la source est nécessaire mais insuffisante. Le comportement du programme se loge entre les lignes — dans des constantes, dans des masques de bits, dans des commentaires, dans la forme d’une procédure — et l’en extraire relève de l’archéologie, pas de la transcription.

La source n’est pas le comportement

Une recréation fidèle exige trois instruments, et j’ai fini par tous les utiliser :

  1. La source comme spécification. Chaque comportement contesté résolu en lisant la procédure qui l’implémente, citée à la ligne près.
  2. Un original en fonctionnement comme oracle. Infinite Mac démarre de véritables Mac de l’ère System dans le navigateur, avec le vrai MacPaint sur le disque5. Quand la source restait ambiguë sur le ressenti — la cadence du pistolet, l’interpolation du pinceau à grande vitesse — l’émulateur tranchait.
  3. Une implémentation indépendante comme contre-vérification. Pour le format de fichier, j’ai écrit un second décodeur en Python, sans partager la moindre ligne de code avec l’application, et j’ai exigé que les deux concordent octet par octet dans les deux sens.

L’instrument que vous ne pouvez pas utiliser, c’est la mémoire. La mienne, ou celle d’Internet. La plupart de ce que « tout le monde sait » sur MacPaint se révèle sous-spécifié dès l’instant où il faut faire apparaître un pixel à une coordonnée exacte.

Ce que signifiait double-cliquer en 1984

Voici un comportement qu’aucune capture d’écran ne peut vous révéler. Dans MacPaint, double-cliquer sur un outil de la palette est une commande. L’aiguillage réside dans une seule procédure, ChooseTool, à MacPaint.p:3651–3699 :

  • Gomme : effacer toute la fenêtre, puis revenir à l’outil précédemment sélectionné.
  • Pinceau : ouvrir le sélecteur de forme du pinceau.
  • Sélection rectangulaire : sélectionner toute la fenêtre.
  • Main (grabber) : ouvrir Afficher la page.
  • Crayon : activer ou désactiver FatBits, le zoom au niveau du pixel.

Le cas de la gomme comporte un détail que seule la source révèle. À la ligne 3643, avant tout cela, se trouve ce garde-fou :

IF theTool <> eraseTool THEN prevTool := theTool;

La gomme ne devient jamais l’« outil précédent ». Ainsi, quand un double-clic efface tout, le programme vous rend le pinceau ou le crayon avec lequel vous travailliez réellement — la gomme était de passage, pas une destination. Le commentaire d’Atkinson sur la ligne de retour le dit sans détour : { we wont need the eraser anymore }. C’est de la conception d’interaction exprimée en une seule condition, invisible de l’extérieur jusqu’à ce que vous remarquiez que MacPaint ne vous abandonne jamais sur la gomme après un effacement. PixelPaint implémente les cinq comportements de double-clic issus de cette procédure, et une suite de tests automatisée dans le navigateur vérifie chacun d’eux de bout en bout.

Un second détail se cache dans le cas de la sélection rectangulaire. Quand le double-clic sélectionne toute la fenêtre, la source ajoute un à la droite et au bas du rectangle avant d’établir la sélection. Ce qui nous amène aux décalages d’un pixel.

Deux décalages d’un pixel, et qui avait raison

À mi-parcours du projet, une passe de relecture a signalé deux écarts entre l’aperçu et le rendu réel dans ma version :

  1. La sélection rectangulaire semblait capturer un pixel de moins que son aperçu en élastique.
  2. L’empreinte de la gomme était un pixel plus large que l’aperçu de son curseur.

Ce sont tous deux le genre de bogue qu’on pourrait « corriger » en trente secondes en ajustant un +1 — dans un sens ou dans l’autre. Tout l’intérêt de disposer de la source, c’est qu’on n’a pas le choix. On va vérifier quel côté est fautif.

La sélection rectangulaire n’était pas un bogue. Les rectangles de QuickDraw excluent leurs bords bas et droit : un rectangle de (10,10) à (20,20) couvre dix pixels, pas onze. Dans ma version, l’élastique et la capture concordaient déjà selon cette convention — en faisant glisser de 10,10 à 20,20, on sélectionne exactement 10×10. Ce que la relecture avait en réalité comparé, c’était l’aperçu des outils de forme (qui inclut correctement le dernier pixel d’une plage inclusive) face à la capture exclusive de la sélection rectangulaire. Deux conventions différentes, toutes deux correctes, côte à côte. Résolution : ne rien changer, consigner pourquoi.

La gomme était un bogue — le mien. Dans l’original, le bloc de la gomme correspond exactement à son curseur : un carré de 16×16, apposé tel quel (EraseSome, MacPaint.p:2210, en utilisant le propre masque du curseur de l’outil). Mon empreinte était calculée comme 2*floor(size/2)+1, ce qui faisait qu’une gomme de 8 pixels effaçait un trou de 9 pixels de large. Corrigée pour que l’empreinte couvre exactement size pixels : une gomme de 8 pixels efface désormais les colonnes 16 à 23 et laisse intactes les colonnes 15 et 24, vérifié pixel par pixel. Dans FatBits, la gomme se réduit à exactement 2×2, ce qui figure lui aussi dans la source (MacPaint.p:2214).

La règle qui a émergé de ce duo est devenue la colonne vertébrale du projet : quand l’aperçu et l’action divergent, c’est l’original qui décide lequel des deux ment.

La page, pas le canevas

L’idée la plus structurante de MacPaint est facile à manquer parce qu’elle est spatiale. Le document n’est pas la fenêtre. Le document est une page fixe de 576×720 pixels — déclarée comme constantes à la compilation à MacPaint.p:108–109 — et la zone de dessin à l’écran est une fenêtre ouverte sur cette page. La main fait défiler la fenêtre à travers la page (ScrollDoc, :2778) ; Afficher la page (ShowPage, :4074) effectue un zoom arrière sur la feuille entière et vous permet de faire glisser le rectangle de la fenêtre vers un nouvel emplacement. À 72 DPI, 576×720 fait exactement 8×10 pouces : le document était dimensionné pour le papier, pas pour l’écran.

À l’origine, PixelPaint disposait d’un tampon à la taille de la zone d’affichage, ce qui signifie qu’il avait un canevas là où MacPaint avait un document. Le reconstruire autour du vrai modèle a été le changement le plus important du projet, et il a fait surgir une contrainte tout à fait de 2026 : iOS plafonne la mémoire de rendu d’un canevas autour de 16,7 mégapixels. Allouer naïvement la page entière multipliée par le zoom FatBits en réclamerait 26,5 — un canevas qui s’affiche silencieusement en blanc sur les iPad où je voulais que cela fonctionne. Le portage conserve des canevas d’affichage à la taille de la zone visible et applique plutôt une transformation de vue dans l’espace du document ; la mémoire de rendu mesurait 0,42 mégapixel à un zoom de 8×, et une trame complète de dessin puis de rendu de la page s’est établie à 6,4 millisecondes, sous la barre d’une image à 60 Hz. Atkinson a résolu un budget mémoire de 128 Ko avec des tampons hors écran cachés4 ; le portage navigateur résout un plafond d’allocation caché avec une transformation. Même discipline, mur différent.

Des fichiers qu’un Mac de 1984 peut lire

Une recréation incapable d’échanger des documents avec l’original n’est qu’un diorama. Le format de fichier de MacPaint est documenté par la source elle-même : un en-tête de 512 octets, puis la page sous forme de 720 lignes de balayage de 72 octets chacune, compressées avec PackBits — un schéma par plages de répétition que la partie Pascal n’implémente jamais, se contentant de le déclarer (PackBits/UnpackBits, marqués EXTERNAL à MacPaint.p:420–421 ; la glu en assembleur dans MyTools.a les aiguille comme des traps système). L’en-tête transporte la palette de motifs du programme, si bien qu’un document se souvient des motifs avec lesquels il a été peint.

PixelPaint lit et écrit ce format. À l’export, la page passe par le propre tramage par diffusion d’erreur d’Atkinson pour atteindre le 1 bit — seuil à 128, l’erreur de chaque pixel répartie en huitièmes et poussée vers six voisins, deux huitièmes étant délibérément écartés, ce qui donne au tramage d’Atkinson son contraste percutant. Les dessins en noir et blanc purs passent sans altération, car leur erreur est identiquement nulle. Il y a une agréable circularité à employer l’algorithme de tramage de Bill Atkinson pour écrire le format de fichier de Bill Atkinson.

C’est à la vérification que l’instrument de l’implémentation indépendante a prouvé son utilité. Le codec PackBits intégré à l’application effectue des allers-retours octet pour octet identiques sur les fixtures. Un fichier exporté, décodé par l’implémentation Python distincte, a produit la bonne version d’en-tête, des motifs intacts et exactement 720 lignes de balayage de 72 octets, chaque octet étant consommé. Un fichier encodé du côté Python — enveloppé en MacBinary, que l’importateur détecte grâce au type de fichier à l’offset 65 — s’est ouvert dans PixelPaint, sa bordure et ses diagonales tombant sur les pixels calculés. Exporter, effacer, réimporter a reproduit l’état compressé au hachage identique. Deux implémentations, les deux sens, aucun code partagé.

Entre les lignes

Les découvertes les plus profondes sont venues de détails qu’aucune liste de fonctionnalités ne ferait jamais remonter — des choses qu’on ne trouve qu’en lisant.

La grille est un masque de bits. L’aimantation sur la grille de 8 pixels de MacPaint ne s’applique pas à chaque outil. ChooseTool détermine l’éligibilité en testant l’index de l’outil face à une simple constante hexadécimale, $50BF3000, l’ensemble Pascal lisible par l’humain étant laissé en commentaire. L’aimantation elle-même arrondit au plus proche, implémentée comme une troncature au multiple de 8 après ajout de 4 (GridPoint, MacPaint.p:513). PixelPaint respecte l’ensemble exact d’outils : la sélection rectangulaire, le texte, les lignes, les rectangles, les ovales et les polygones s’aimantent ; les outils à main levée, jamais.

La contrainte par Maj est plus subtile qu’un simple horizontal-ou-vertical. Constrain (MacPaint.p:875) aimante une ligne à 45° en ramenant les deux deltas au plus petit des deux — et en plus la force à l’horizontale ou à la verticale pure quand un axe domine l’autre dans un rapport de deux pour un. Tous les clones que j’ai vus implémentent la moitié H/V et font l’impasse sur le modèle de dominance diagonale. La source contient l’algorithme complet en trente lignes.

Le motif est l’encre. La signature de BrushPaint prend le pinceau et un motif (MacPaint.p:2024). Le pinceau et le pistolet ne peignent pas en « noir » ; ils peignent toujours à travers le motif actuellement sélectionné. J’ai adopté cela à l’identique, et cela a changé la sensation du dessin — le choix du motif cesse d’être une option de remplissage pour devenir la peinture elle-même.

Trace Edges cache une variante. Maintenez Maj et le décalage du contour passe de 2 à 3, annoté dans la source par le commentaire { asymmetric shadow } (MacPaint.p:1898). Un easter egg d’une seule ligne datant de 1984, préservé.

Le texte est de l’encre pleine, et la source a corrigé mon bogue. Lors des tests tactiles, le texte saisi ne validait parfois aucun pixel. La cause : ma validation de texte filtrait les pixels des glyphes à travers le motif de remplissage, de sorte qu’un motif clairsemé avalait silencieusement les lettres. L’original ne fait jamais cela — le texte se dessine comme de l’encre de premier plan pleine, quel que soit le motif (UpdateText/PatchText, MacPaint.p:992–1106). Lire la procédure a été plus rapide que déboguer ma propre supposition, et cela a réglé la correction sans discussion possible.

Une dernière trouvaille, pour mémoire : PaintAsm.a contient une fonction nommée Monkey — le point d’accroche du testeur de charge à entrées aléatoires qu’utilisait l’équipe Macintosh, protégé par un drapeau nommé MonkeyLives. Atkinson a livré son harnais de test dans le même fichier que son blitter. Les artisans laissent leurs gabarits sur l’établi.

Ce que j’ai laissé intact, et ce que j’ai changé

La fidélité était le principe de conception, si bien que les écarts sont rares, délibérés et consignés dans l’application — la fenêtre À propos les énumère, à la manière dont une édition en fac-similé signale ses écarts :

  • Une palette de 16 couleurs par-dessus le moteur 1 bit. Le tramage et le chemin d’export .mac vous rendent le monochrome authentique dès que vous le souhaitez.
  • Une pile d’annulation de 100 niveaux. L’original comptait exactement un niveau d’annulation, parce qu’Atkinson conservait deux tampons hors écran à la taille de la fenêtre — l’état courant et l’état précédent — qu’il permutait4. C’était une réponse héroïque à 128 Ko de RAM. Recréer la limitation relèverait du cosplay ; le modèle mémoire auquel elle répondait n’existe plus.
  • Des tailles de gomme sélectionnables, un mode de pulvérisation dispersée optionnel et le calquage d’une image de référence. Des ajouts, tous désactivés par défaut ou clairement modernes, dont aucun ne supplante un comportement d’origine.

Tout aussi délibérément, une partie de la surface de l’original n’a pas été portée : le cycle de vie des documents sur disque (Enregistrer, Enregistrer sous, Rétablir, Fermer) appartient à une machine à disquettes et se voit remplacé par une sauvegarde automatique continue et des exports explicites. Mais Fichier > Imprimer survit — PrintDoc (MacPaint.p:4307) clôt le menu Fichier d’origine, et l’impression restitue l’œuvre seule, nette au pixel près, jamais l’habillage du navigateur.

Le bogue le plus étrange de tout le projet n’avait rien d’un problème de 1984. L’enregistrement des fichiers échouait silencieusement depuis des semaines parce que mon propre script d’analyse interceptait les clics sur les ancres — y compris les clics sur les URL blob: — et le code de sauvegarde révoquait l’URL blob de façon synchrone après le clic, avant que le navigateur n’entame le téléchargement. Un programme de 1984 ne combat pas sa propre télémétrie. En recréer un en 2026, apparemment, si.

Allez dessiner quelque chose

Les icônes d’outils que vous reconnaîtrez — le lasso, la main, le pistolet, le pot de peinture — ont été dessinées par Susan Kare, dont j’ai évoqué la discipline du pixel en 32×32 dans la série sur la philosophie du design. Les comportements qui les sous-tendent ont été écrits par Bill Atkinson, décédé en juin 20256. La publication par le Computer History Museum signifie que son programme peut être étudié, confronté et reconstruit honnêtement plutôt qu’approximativement — ce qui constitue, je crois, le meilleur des monuments pour un logiciel.

PixelPaint est en ligne à l’adresse /paint, aux côtés des autres explorations interactives de ce site. Il fonctionne sur un iPad, au doigt. Double-cliquez sur le crayon pour découvrir FatBits. Dessinez quelque chose, enregistrez-le comme fichier .mac, et sachez qu’un Macintosh de 1984 pourrait l’ouvrir.

FAQ

Le code source original de MacPaint est-il disponible ?

Oui. Le Computer History Museum a publié le code source de MacPaint 1.3 (ainsi que la bibliothèque graphique QuickDraw) en juillet 2010 avec l’autorisation d’Apple, pour un usage non commercial1. Cette publication porte le numéro d’acquisition 102658076 au catalogue du CHM2 et comprend le programme Pascal principal (MacPaint.p) ainsi que les fichiers de support en assembleur 68000. Un miroir officiel se trouve sur GitHub sous le compte du Computer History Museum3.

Qu’est-ce que la compression PackBits ?

PackBits est le codage par plages de répétition (run-length) qu’employait MacPaint pour compresser ses documents : chaque ligne de balayage est compressée sous forme de plages littérales et de plages répétées, ce qui fonctionne bien sur les images 1 bit remplies d’espaces blancs et de motifs récurrents. Le Pascal de MacPaint déclare PackBits et UnpackBits comme des routines externes (MacPaint.p:420–421) et atteint l’implémentation 68000 du système via sa glu en assembleur. Un fichier MacPaint est un en-tête de 512 octets suivi de 720 lignes compressées en PackBits de 72 octets chacune — la page complète de 576×720.

Qu’est-ce que le tramage d’Atkinson ?

Le tramage d’Atkinson est l’algorithme de diffusion d’erreur que Bill Atkinson a conçu pour convertir les images en niveaux de gris vers l’affichage 1 bit du Macintosh. Chaque pixel est ramené à noir ou blanc par seuillage, et l’erreur qui en résulte est divisée par 8 puis distribuée à six pixels voisins — les deux huitièmes restants étant intentionnellement écartés plutôt que propagés. C’est le fait d’abandonner une partie de l’erreur qui donne aux images tramées selon Atkinson leur fort contraste caractéristique. PixelPaint s’en sert pour convertir les dessins en couleur vers le 1 bit à l’export .mac et pour l’aperçu 1 bit en direct.

Quelle est la taille d’un document MacPaint ?

576×720 pixels, fixe — déclaré comme constantes dans la source (MacPaint.p:108–109). Aux 72 DPI du Macintosh, cela fait exactement 8×10 pouces, soit une page imprimable. L’écran ne montrait jamais le document entier d’un coup : la fenêtre de dessin était une zone d’affichage mobile ouverte sur la page, déplacée à la main ou repositionnée via Afficher la page. PixelPaint recrée le même modèle de document, zone d’affichage comprise.


Sources


  1. Leonard J. Shustek, « MacPaint and QuickDraw Source Code », blog du Computer History Museum, 18 juillet 2010. L’annonce de la publication ; elle atteste l’autorisation d’Apple et la licence non commerciale, et retrace l’histoire du programme. 

  2. Catalogue des collections du Computer History Museum, « MacPaint source code », numéro d’acquisition 102658076. 

  3. Computer History Museum, Historical Source Code: MacPaint repository, GitHub. Miroir officiel des fichiers source publiés. 

  4. Andy Hertzfeld, « MacPaint Evolution », Folklore.org. Source primaire sur l’histoire du développement de MacPaint, notamment les deux tampons hors écran à la taille de la fenêtre (état courant et état précédent) à l’origine du dessin sans scintillement et de l’annulation à un seul niveau. 

  5. Infinite Mac — des systèmes Macintosh classiques, dont MacPaint, émulés dans le navigateur. Utilisé comme oracle « original en fonctionnement » pour les comparaisons de comportement. 

  6. Adam Engst, « Bill Atkinson Dies from Pancreatic Cancer at 74 », TidBITS, 7 juin 2025. 

Articles connexes

GLSL pour les builders : un labo shader vraiment utilisable

Un playground GLSL pratique pour développer vite votre intuition shader : presets, contrôles live et WebGL sans framewor…

4 min de lecture

Codes de Hamming : comment les ordinateurs corrigent leurs propres erreurs

De la RAM aux QR codes : une exploration interactive de la détection et correction d’erreurs par les codes de Hamming.

9 min de lecture

The Shader Gallery That Lied: Debugging 216 WebGL Presets

A user said the shader playground looked broken. Pixel-readback testing found 30 dead presets, 11 that never compiled, a…

11 min de lecture