Pourquoi nous avons remplacé trois moteurs de géométrie de surligneur par des tampons
Les données réelles de l’Apple Pencil ont révélé des défauts absents des tests synthétiques et fait passer Lulucat Notes de trois moteurs de contour à un modèle de tampons MaLiang.
Lulucat Notes a commencé avec un rendu classique de surligneur : construire un contour géométrique autour du tracé échantillonné, puis remplir ce contour une seule fois avec une couleur translucide. Un remplissage unique évite les bandes plus sombres qui apparaissent lorsque des segments translucides se chevauchent. Il place aussi tous les raccords, auto-intersections, terminaisons et tracés courts dans un même problème de géométrie.
Trois générations de géométrie personnalisée ont chacune supprimé une catégorie de défauts et en ont révélé une autre. Le moteur que nous livrons aujourd’hui utilise des tampons circulaires doux inspirés de MaLiang, un framework de dessin open source sous licence MIT créé par Harley-xk. En échange de la suppression de toute une catégorie de défaillances des contours, nous avons accepté des auto-intersections plus sombres et des extrémités effilées par la pression.

Capture d’écran originale de l’appareil, agrandie à 228 %. De petits crochets apparaissent aux deux extrémités de plusieurs tracés.
L’écriture manuscrite réelle produisait des crochets absents des scènes synthétiques
Nos scènes synthétiques couvraient les lignes droites, les arcs, les touches brèves, les retours en arrière, les changements de pression et les virages serrés, et tous se rendaient proprement. L’écriture chinoise réelle produisait malgré tout des crochets d’environ 1–3 points au début et à la fin de nombreux tracés.
Nous avons donc ajouté un bouton d’enregistrement à l’app et enregistré 29 tracés complets. Chaque échantillon conservait sa position, son rayon, sa pression, son azimut, son altitude, son horodatage et ses indicateurs d’estimation UIKit. Un programme hors ligne a décodé ce JSON et compilé les mêmes fichiers Stroke.swift, StrokeRenderer.swift et HighlighterStrokeBuilder.swift que ceux utilisés par l’app. Sa sortie correspondait trait par trait à la capture d’écran de l’appareil, crochets compris.
Comme le moteur hors ligne reproduisait le défaut à partir des seules données enregistrées, sa cause se trouvait dans les échantillons et la géométrie plutôt que dans le comportement de l’affichage. Nous pouvions désormais lire les échantillons exacts qui étaient entrés dans le moteur.
Notre première hypothèse accusait l’orientation estimée du Pencil au contact et au décollage. Les données l’ont réfutée. Sur les 29 tracés, l’azimut ne variait au maximum que de 0.09 radian et l’altitude de 0.02 radian dans les huit premiers et derniers échantillons, et tous les indicateurs d’estimation étaient faux sur cet iPad. L’orientation restait stable tandis que la pression et la direction changeaient fortement.
Un seuil de pression venait d’une séparation nette dans les données
Le dernier échantillon de chaque tracé enregistré avait une pression de 0.000. Les échantillons d’écriture étaient à 0.01 ou plus, tandis que ceux enregistrés après que le Pencil avait quitté la surface étaient à 0.002 ou moins. Ces échantillons en vol se poursuivaient souvent pendant 2–7 points et parcouraient 0.8–9.0 points, et leur direction pouvait tourner d’environ 90 degrés à l’opposé du tracé écrit.
Le tracé 2 montre le mécanisme. Son corps se terminait entre 143 et 180 degrés. La queue à faible pression tournait ensuite de −90 à −67 degrés et parcourait encore 8.5 points. Le moteur de contour traitait le dernier point en vol comme l’extrémité de référence et étirait donc une fine portion de géométrie vers ce point.
Les pressions mesurées montrent un écart de presque un ordre de grandeur entre l’écriture et le mouvement en vol ; nous avons donc placé le seuil dans cet intervalle :
private static let pressureFloor: CGFloat = 0.005
Seuls les échantillons à faible pression situés aux deux extrémités sont supprimés. Un échantillon à faible pression au milieu d’un tracé en reste une partie. La règle suit la limite mesurée entre les deux événements et laisse le tracé écrit intact.

Avant la suppression, le moteur ancre la terminaison sur les échantillons en vol. Les points verts sont les échantillons bruts ; la ligne rouge est le contour généré.

Après suppression des échantillons sous 0.005, la terminaison s’arrête au dernier point écrit. Ce seuil de pression reste dans le moteur actuel à base de tampons.
Le seuil a supprimé les longs crochets en vol. Les courts glissements au contact et les arcs au décollage dont la pression restait supérieure à 0.005 ont résisté au seuil, car ils correspondent à un mouvement écrit. Gérer ces mouvements dans un modèle de contour a conduit aux cycles de géométrie suivants.
Trois moteurs de géométrie ont déplacé les défauts
Première génération : un contour, un remplissage
Le premier moteur sensible à la pression suivait un pipeline familier. Il lissait une ligne centrale, calculait les décalages à gauche et à droite, les reliait en un polygone unique et remplissait ce polygone une seule fois. La pointe était une ellipse commandée par l’azimut et l’altitude de l’Apple Pencil, de sorte que sa section transversale changeait avec la direction d’écriture. Les touches brèves avaient une forme distincte, et les terminaisons plates utilisaient de petites asymétries pour éviter les poches de nombre d’enroulement nul.
Le remplissage unique résolvait l’accumulation d’opacité à l’intérieur d’un tracé et exigeait un polygone valide à chaque virage. Les glissements au contact créaient des pointes entre la terminaison et le corps. Les retours en arrière pouvaient créer des poches blanches en croissant. Les virages serrés pouvaient faire s’auto-intersecter le contour intérieur et laisser une encoche non remplie.
Deuxième génération : réparer le contour près de ses extrémités
Le deuxième moteur a conservé le contour et ajouté des réparations locales : suppression des retours en arrière, amortissement des propriétés estimées au début, redressement de l’extrémité, suppression de la direction transitoire et petite extension de la terminaison. Chaque correctif répondait à un défaut reproduit. Ensemble, ils rendaient le comportement des extrémités dépendant de plusieurs seuils et fenêtres.
L’écriture réelle a ensuite produit des cas qui sortaient de ces fenêtres. Une fenêtre fixe de redressement de 24 points effaçait les crochets intentionnels des tracés courts. Une fenêtre adaptative préservait ces crochets, et les arcs au décollage pouvaient toujours devenir des angles visibles. La suppression tenant compte de la direction éliminait davantage de mouvement transitoire, au prix de la suppression d’environ 2.5 points d’un début réellement courbé dans un tracé enregistré.
Chaque correctif corrigeait le cas auquel il était destiné. Le modèle sous-jacent exigeait toujours un contour rigide qui reste valide face à une entrée bruitée et courbe.
Troisième génération : union de couverture avec terminaisons carrées écrêtées
Le troisième moteur a cessé de construire un contour unique. Il dessinait un trapèze pour chaque segment et un disque à chaque échantillon intérieur dans un masque en niveaux de gris, calculait l’union de leurs couvertures, puis appliquait la couleur du surligneur une seule fois. Les croissants des retours en arrière, l’assombrissement des auto-intersections et les trous de nombre d’enroulement nul ont disparu, car le moteur ne dépendait plus de l’enroulement d’un polygone.
L’union arrondissait les extrémités, et nous avons restauré les terminaisons carrées en écrêtant la couverture selon deux demi-plans d’extrémité. Cet écrêtage corrigeait les lignes droites à pression constante et introduisait un défaut plus grave : le corps d’un tracé courbe peut légitimement traverser un demi-plan d’extrémité, et l’écrêtage de la terminaison supprimait alors une partie du milieu du tracé. Pendant la saisie en direct, la direction de l’extrémité changeait constamment ; la zone écrêtée se déplaçait donc et le tracé scintillait.

La contrainte globale de terminaison coupe le corps d’un tracé courbe.

Limiter l’écrêtage de la terminaison aux primitives situées à moins d’une demi-largeur de l’extrémité restaure ce tracé.
Un filtre de longueur d’arc a rendu l’écrêtage de la terminaison local et a réparé tous les cas enregistrés. Un crochet synthétique extrême montrait encore une petite encoche blanche à l’endroit où une pointe à faible pression rejoignait la terminaison écrêtée. Le troisième moteur avait une fois de plus échangé un défaut de géométrie contre un autre.
MaLiang supprime la frontière rigide
MaLiang construit un tracé à partir de tampons de texture répétés plutôt qu’à partir d’un contour fermé unique. Son générateur de tracé utilise des segments de Bézier quadratiques qui passent par les milieux d’échantillons adjacents, et son moteur de ligne place les tampons à intervalle régulier le long de ce tracé. La taille, la rotation, la couleur et l’opacité du tampon peuvent toutes varier le long du tracé.
Un tampon doux n’a ni terminaison, ni raccord, ni règle d’enroulement distincts. Une variation brusque de direction reste une suite de marques qui se chevauchent. Les retours en arrière ne peuvent pas créer de poche vide dans un polygone, et un demi-plan d’extrémité ne peut pas couper le milieu d’un tracé, car le modèle ne contient aucun écrêtage d’extrémité.
Nous avons adopté le modèle, pas le code. Le HighlighterStrokeBuilder actuel effectue quatre opérations :
- Supprimer uniquement les échantillons d’extrémité dont la pression est inférieure à
0.005. - Lisser les positions avec des segments de Bézier quadratiques passant par les milieux.
- Placer un tampon circulaire par point de longueur d’arc.
- Définir le diamètre du tampon à partir de la pression normalisée et composer les tampons avec l’alpha source-over.
Le builder est passé de 686 lignes à 190. L’azimut, l’altitude, la construction du contour, les masques d’union, les fenêtres de direction des extrémités, l’extension de terminaison et l’écrêtage de terminaison ont tous quitté le tracé du surligneur. Nous continuons à enregistrer l’orientation du Pencil, car le modèle d’entrée la prend en charge, mais le surligneur lui-même ne lit plus ces champs.
La formule de pression de MaLiang nécessitait un calibrage de l’appareil
MaLiang dimensionne un tampon avec une formule équivalente à
qui suppose que force utilise une portion utile de l’intervalle entre 0 et 1. Nos données d’appareil ne le confirmaient pas. Sur environ 3,400 échantillons Apple Pencil, la pression médiane était de 0.047 et le maximum de 0.178. Injecter directement ces valeurs dans la formule rendait l’écriture normale bien plus fine que la taille nominale du pinceau.
Nous normalisons donc autour de la médiane observée, puis appliquons l’exposant et bornons le résultat :
La pression médiane d’écriture correspond désormais au diamètre nominal de 12 points. Un contact très léger reste visible à 35 % de la taille nominale, tandis qu’une forte pression cesse d’agrandir le tracé à 120 %.
Cette calibration relève du périphérique d’entrée et du comportement du pinceau, pas de MaLiang. Copier la formule originale sans mesurer notre propre distribution de pression aurait préservé l’architecture tout en produisant le mauvais pinceau.
L’alpha par tampon venait du nombre de chevauchements connu
MaLiang compense le chevauchement des tampons translucides avec l’expression empirique alpha ÷ overlapping × 2.5. Notre pinceau a un diamètre nominal fixe de 12 points et un intervalle de 1 point entre les tampons, de sorte que sa ligne centrale reçoit environ 12 couches. Dans notre cas, le nombre de chevauchements est connu et la compensation peut être calculée plutôt qu’estimée.
Pour une opacité de tracé cible
et un tampon nécessite donc
Avec

Le moteur final rejoue les tracés enregistrés sur l’appareil. Les crochets, les coupes diagonales et les poches blanches ont disparu.
Les deux coûts que nous avons acceptés
Les auto-intersections deviennent plus sombres. La composition source-over compte chaque tampon, y compris ceux déposés lors d’un passage précédent dans la même zone, alors que le moteur d’union de couverture l’avait évité. Nous avons accepté ce chevauchement plus sombre parce qu’il est prévisible et laisse la forme du tracé intacte.
Les extrémités s’affinent avec la pression. Un véritable décollage réduit la pression, ce qui réduit le diamètre du tampon ; le tracé s’effile donc au lieu de se terminer par une terminaison carrée pleine largeur. Nous avions traité cette terminaison carrée comme une exigence dans les trois moteurs de géométrie. L’abandon de cette exigence a supprimé l’écrêtage et les mécanismes d’extrémité à l’origine de plusieurs défauts ci-dessus ; nous avons donc accepté l’extrémité effilée.
Le modèle que nous livrons fait donc moins que les précédents. Il préserve l’événement d’écriture mesuré, réagit à la pression après le calibrage de l’appareil et rend les tracés courbes et auto-sécants sans exiger un contour globalement valide. Une pointe en biseau, une terminaison parfaitement carrée et la polyligne exacte entre les échantillons bruts sont les fonctionnalités auxquelles nous avons renoncé pour cela.
Le seuil de pression est le résultat qui se transpose le plus directement à d’autres projets. La valeur 0.005 venait de l’écart entre deux groupes dans les données réelles : une écriture à 0.01 et plus, et un suivi en vol à 0.002 et moins. Les mêmes tracés enregistrés ont montré où chaque réparation géométrique échouait. Ces échecs ont conduit au modèle de tampons et aux deux coûts qu’il entraîne.