Le projet a commencé dans un dossier nommé illustrator, avec une idée volontairement large : construire un éditeur vectoriel directement dans le navigateur. Il ne s’agissait pas de reproduire tous les outils d’un logiciel installé depuis vingt ans, mais de comprendre ce qui transforme une surface blanche en véritable espace de travail.
Le prototype est progressivement devenu Retrace, un éditeur capable de créer et modifier des tracés, organiser des calques, manipuler des dégradés, ouvrir plusieurs documents, importer ou exporter des fichiers et même charger ses propres plugins. Tout s’exécute côté client avec SolidJS, TypeScript et Vite.
La difficulté n’a jamais été d’afficher un rectangle. Elle commence lorsque ce rectangle doit rester sélectionnable après une rotation, devenir un tracé quand on déplace l’un de ses points, conserver son dégradé à l’export et revenir exactement à son état précédent après un annuler.
Faire du SVG le format du document#
Retrace travaille avec du SVG parce que le format fournit déjà une grande partie du vocabulaire nécessaire : formes, tracés, groupes, texte, transformations, remplissages, contours et dégradés. Un document peut être importé, affiché dans le DOM, modifié puis exporté sans inventer un format propriétaire intermédiaire.
Ce choix donne aussi une règle utile : le fichier exporté doit rester un vrai SVG, lisible ailleurs. La grille, les poignées de sélection, les repères et les autres éléments propres à l’éditeur sont donc séparés du dessin ou retirés au moment de la sérialisation.
Le modèle interne reste volontairement proche de ce format. Chaque entrée de l’historique contient un instantané du document, accompagné de métadonnées calculées à la demande. Le SVG monté dans la page reste toutefois la version la plus récente pendant une interaction : les outils et les plugins peuvent le modifier directement, puis pousser un nouvel état dans l’historique à la fin du geste.
Cette approche est moins abstraite qu’un grand modèle métier décrivant chaque objet graphique, mais elle garde une frontière claire entre trois choses : le document échangeable, son rendu et l’état temporaire de l’interface.
Une architecture autour d’un DOM vivant#
L’interface SolidJS conserve les états éphémères : outil actif, sélection, zoom, panneau ouvert ou geste en cours. Les opérations durables passent par des services TypeScript séparés pour les coordonnées, la géométrie, les documents, les calques et les styles. Ils travaillent sur le SVG monté, puis pushHistory en retire les artefacts de l’éditeur avant de créer un nouvel instantané.
SvgDocumentModel enveloppe cet instantané. Ses métadonnées et sa scène de rendu sont calculées à la demande, afin qu’une simple modification ne reparcourt pas inutilement tout le document. L’historique, le stockage local, l’export et le partage partent ensuite de cette même représentation sérialisable.
Le registre de plugins forme une seconde entrée vers le cœur. Il expose des emplacements d’interface, un petit bus d’événements, des importeurs, des exporteurs et les actions nécessaires pour lire ou modifier le document. Les plugins séparés utilisent donc les mêmes services que l’interface intégrée.
La géométrie se cache partout#
Les premiers outils donnent facilement l’impression que le problème est résolu. Tracer une ellipse ou déplacer un objet ne demande que quelques événements de pointeur. Dès que les transformations s’accumulent, les coordonnées ne parlent pourtant plus toutes du même espace.
Un point peut être exprimé dans le repère de son tracé, celui d’un groupe, celui du plan de travail ou celui de l’écran. Le zoom et les matrices de transformation s’ajoutent encore à la chaîne. Les boîtes de sélection, le magnétisme, les poignées de Bézier et les curseurs de redimensionnement doivent tous convertir ces valeurs sans dériver visuellement.
Revenir dans le bon repère#
Retrace représente une transformation affine 2D avec les six coefficients utilisés par DOMMatrix et SVG :
Un point devient donc . Une translation, une rotation, une échelle et leur composition tiennent toutes dans cette matrice. Pour appliquer deux transformations, Retrace multiplie leurs matrices ; pour revenir du repère écran au plan de travail, il utilise l’inverse de la matrice de transformation courante du SVG.
Si relie le plan de travail à l’écran et le parent d’un objet à l’écran, la conversion utilisée pour une géométrie imbriquée est . L’inversion n’existe que si le déterminant est non nul. Le code rejette les valeurs telles que , plutôt que de propager des coordonnées infinies après une mise à l’échelle presque nulle.
Les vecteurs de déplacement suivent la même transformation, sans les coefficients de translation et . Cette distinction évite qu’un déplacement de dix unités récupère accidentellement la position absolue de son groupe parent.
Le magnétisme est plus simple, mais repose sur le même principe de repère explicite. Pour une origine et un pas , chaque coordonnée devient :
Déplacer l’origine de la grille ne change ainsi pas le pas et le même calcul fonctionne dans toutes les directions.
J’ai fini par isoler les calculs de chemins et de matrices du reste de l’application. Les commandes SVG relatives sont normalisées, les transformations affines sont composées explicitement et les coordonnées du pointeur sont ramenées dans le repère utile avant chaque modification. Une matrice existante fournit aussi directement la rotation avec atan2(b, a) et l’échelle uniforme avec hypot(a, b).
La séparation entre sélection d’objet et sélection directe a également clarifié le comportement de l’éditeur. La première flèche manipule une forme complète. La seconde expose ses points et ses poignées. Une forme primitive, comme un rectangle ou une ellipse, ne devient un tracé que lorsqu’une opération exige réellement cette conversion. L’utilisateur garde ainsi un objet simple aussi longtemps que possible.
Normaliser et échantillonner les courbes#
Un attribut SVG d accepte des commandes absolues ou relatives, des lignes horizontales et verticales, des courbes cubiques ou quadratiques, des arcs et des raccourcis comme S et T. Manipuler directement toutes ces variantes multiplierait les cas dans chaque outil. Le parseur les normalise donc en coordonnées absolues, transforme H et V en lignes ordinaires et développe S et T avec leur point de contrôle réfléchi.
Une courbe de Bézier cubique est évaluée pour avec :
et sont les ancres ; et sont les poignées. Pour garder un nœud lisse, Retrace normalise la direction de la poignée déplacée autour de l’ancre , puis replace la poignée opposée avec :
Les deux tangentes restent ainsi colinéaires sans forcer une symétrie parfaite des courbures.
L’arrondi d’un angle illustre un autre compromis. La valeur demandée est limitée à la moitié de chacun des segments voisins. Deux points de coupe et sont placés de part et d’autre du sommet , puis la courbe quadratique est convertie exactement en cubique :
Le résultat rejoint les segments avec une tangente continue et reste éditable par le même code que les autres commandes C.
Pour vectoriser un contour, les courbes sont actuellement aplaties en polylignes avec seize échantillons par courbe cubique. Cela simplifie les normales, les intersections et les jointures, mais la précision dépend de la courbure et du zoom. Une subdivision adaptative fondée sur l’écart entre la corde et la courbe serait une évolution plus robuste qu’un nombre fixe d’échantillons.
Une application locale, pas une application jetable#
Retrace n’a ni compte ni API. Cela ne dispense pas de gérer un état durable. Un éditeur perd tout intérêt si un rechargement de page efface le dessin en cours.
La session active, son historique, le zoom et les réglages de grille sont enregistrés dans le stockage local du navigateur. Plusieurs documents peuvent être ouverts dans des onglets pendant une même session, chacun avec son propre historique. L’import par fichier ou glisser-déposer ouvre un nouvel onglet, tandis que l’export produit un fichier indépendant du cache de l’application.
L’annulation repose sur des instantanés SVG plutôt que sur une commande inverse écrite pour chaque action. Ce choix consomme davantage de mémoire qu’un journal d’opérations très optimisé, mais il simplifie fortement la fiabilité : restaurer un état signifie remonter exactement le document correspondant, puis reconstruire les sélections et les éléments d’interface autour de lui.
Le coût est facile à exprimer. Pour étapes contenant des SVG de tailles à , la mémoire et le temps d’un nouvel instantané sont approximativement :
Il n’y a pour l’instant ni partage structurel entre deux versions ni compactage automatique. Ce modèle convient aux dessins raisonnables, mais un historique par commandes ou des points de contrôle espacés deviendraient préférables pour des fichiers de plusieurs mégaoctets.
Pour un outil encore jeune, cette simplicité est un compromis raisonnable. Elle laisse aussi une limite nette à surveiller si les documents deviennent beaucoup plus volumineux.
Utiliser les plugins comme frontière d’architecture#
Le système de plugins est arrivé tôt dans le projet. Je voulais pouvoir ajouter une fonction sans agrandir indéfiniment le composant principal, mais surtout vérifier que le cœur exposait des primitives suffisamment cohérentes.
Un plugin peut enregistrer un outil, ajouter une interface dans le dock, le panneau latéral ou un menu contextuel, écouter les événements de l’éditeur et déclarer un importeur ou un exporteur. La vue du code SVG, la gestion des polices, l’import-export PDF, les modificateurs non destructifs et le Shape Builder utilisent cette même surface d’extension.
Le registre repose sur six slots d’interface et un bus de publication-abonnement typé. setup(api) enregistre les contributions et peut retourner une fonction de nettoyage. Désactiver un plugin exécute cette fonction, puis le réactiver rejoue son initialisation. Ce contrat évite de laisser des écouteurs, des boutons ou des overlays orphelins dans l’application.
Plusieurs de ces plugins sont compilés comme des paquets séparés puis chargés à l’exécution. Ils peuvent ainsi évoluer sans gonfler le bundle principal ni imposer leur outillage de build au cœur de l’éditeur. Les dépendances les plus lourdes, comme celles utilisées pour traiter un PDF, ne sont importées qu’au moment où la fonction correspondante est appelée.
Cette architecture oblige à répondre à des questions concrètes. Comment un plugin accède-t-il au document courant ? Comment ajoute-t-il une action à l’historique ? Comment nettoie-t-il son interface lorsqu’on le désactive ? À partir du moment où ces réponses sont stables, les fonctions intégrées et les extensions cessent d’être deux mondes séparés.
La contrepartie est assumée : un plugin chargé depuis une URL ou du code collé dispose d’un accès important à l’éditeur. L’interface prévient donc qu’il ne faut exécuter que du code de confiance. Dans une application entièrement locale, la sécurité ne disparaît pas ; elle se déplace vers ce que l’utilisateur choisit de charger.
Partager un dessin sans ajouter de serveur#
Le partage posait un problème différent. Ajouter une base de données et une API uniquement pour transmettre de petits documents aurait changé la nature du projet. Retrace place donc le dessin directement dans le fragment de l’URL, la partie située après #, qui n’est pas envoyée au serveur lors de la requête HTTP.
Un SVG brut produit rapidement un lien inutilisable. Avant d’être ajouté à l’URL, le document suit la chaîne SVG → minification → dictionnaire → DEFLATE-raw → base64url. Le dictionnaire remplace les motifs SVG fréquents avant la compression générale. Le premier caractère du jeton indique la version de l’encodage, ce qui permet de continuer à décoder les anciens liens si l’algorithme évolue.
Une protection facultative ajoute un chiffrement AES-GCM. La clé est dérivée du mot de passe dans le navigateur et le dessin chiffré reste lui aussi dans le lien. Ni le document ni le mot de passe n’ont besoin d’être enregistrés par Retrace.
Cette solution conserve une infrastructure statique, mais elle n’est pas magique : une composition complexe peut générer une URL que certaines messageries tronqueront. L’application mesure donc sa longueur et avertit lorsque le partage risque de devenir fragile. Un futur service de stockage pourrait lever cette limite, au prix d’une complexité et de responsabilités nouvelles.
Les détails qui font passer du prototype à l’outil#
Le code de géométrie est la partie la plus visible techniquement, mais beaucoup de travail se trouve dans des comportements plus ordinaires : différencier clairement les deux outils de sélection, conserver le bon curseur pendant un glissement, empêcher un calque verrouillé de réagir, faire fonctionner les raccourcis pendant l’édition d’un tracé ou garder les traductions française et anglaise synchronisées.
Une visite guidée présente les outils en demandant réellement de créer une forme, de la colorer et de manipuler ses points. Les calques peuvent être renommés, verrouillés, masqués et réorganisés. Les dégradés se règlent directement sur le plan de travail. Les documents PDF à plusieurs pages restent regroupés dans un même onglet. Pris séparément, chacun de ces détails paraît secondaire ; ensemble, ils déterminent si l’application donne confiance.
Les scripts de vérification suivent la même logique. Ils contrôlent notamment la cohérence entre les curseurs ou les icônes déclarés dans TypeScript et les fichiers réellement distribués. Des scénarios Playwright reproduisent ensuite les gestes importants dans le navigateur. Un éditeur graphique dépend trop de l’interaction pour se contenter de vérifier que le build termine sans erreur.
Ce que Retrace m’a appris#
Construire Retrace m’a surtout appris qu’un éditeur vectoriel est un système de conversions. Il convertit des gestes en géométrie, des coordonnées entre plusieurs repères, un DOM vivant en historique, un document en lien partageable et des fonctions spécialisées en extensions indépendantes.
Le navigateur fournit déjà beaucoup : SVG, matrices DOM, stockage local, compression, chiffrement, presse-papiers et chargement de modules. Le défi consiste moins à remplacer ces briques qu’à leur donner des règles communes et prévisibles.
Retrace ne cherche toujours pas à couvrir tous les usages d’un logiciel de création complet. Il me sert plutôt de terrain pour approfondir un problème après l’autre, sans perdre la propriété la plus importante du projet : on ouvre la page, on dessine, puis on repart avec un fichier standard.