Lulucat

Warum wir drei Geometrie-Engines für Textmarker durch Stempel ersetzt haben

Gaoge ZhangGaoge Zhang

Echte Apple-Pencil-Daten enthüllten von Tests übersehene Fehler und führten Lulucat Notes von drei Kontur-Engines zu einem MaLiang-Stempelmodell.

Aktualisiert

Lulucat Notes begann mit einem konventionellen Textmarker-Renderer: eine geometrische Kontur um den abgetasteten Pfad bilden und diese Kontur anschließend einmal mit einer halbtransparenten Farbe füllen. Eine einzige Füllung vermeidet die dunkleren Bänder, die dort entstehen, wo sich halbtransparente Segmente überlagern. Außerdem landen jede Verbindung, jede Selbstüberschneidung, jede Endkappe und jeder kurze Strich im selben Geometrieproblem.

Drei Generationen eigener Geometrie entfernten jeweils eine Fehlerklasse und legten eine andere offen. Der Renderer, den wir jetzt ausliefern, verwendet weiche kreisförmige Stempel auf Basis von MaLiang, einem MIT-lizenzierten Open-Source-Zeichenframework von Harley-xk. Um eine ganze Kategorie von Konturfehlern zu beseitigen, akzeptierten wir dunklere Selbstüberschneidungen und druckabhängig verjüngte Enden.

Auf einem iPad erfasste blaue Textmarker-Handschrift, mit dünnen Häkchen an den Anfängen und Enden mehrerer Striche.

Der originale Geräte-Screenshot bei 228 % Zoom. An beiden Enden mehrerer Striche erscheinen kleine Häkchen.

Echte Handschrift erzeugte Häkchen, die synthetische Szenen nicht hervorbrachten

Unsere synthetischen Szenen umfassten gerade Linien, Bögen, kurze Taps, Rückläufe, Druckänderungen und scharfe Wendungen; alle wurden sauber gerendert. Echte chinesische Handschrift erzeugte am Anfang und Ende vieler Striche dennoch Häkchen von etwa 1–3 Punkten Länge.

Wir fügten der App daher eine Speichertaste hinzu und zeichneten 29 vollständige Striche auf. Jedes Sample behielt seine Position, seinen Radius, Druck, Azimut, Höhenwinkel, Zeitstempel und UIKit-Schätzungsflags. Ein Offline-Programm dekodierte dieses JSON und kompilierte dieselben Dateien Stroke.swift, StrokeRenderer.swift und HighlighterStrokeBuilder.swift, die die App verwendet. Seine Ausgabe stimmte Strich für Strich mit dem Geräte-Screenshot überein, einschließlich der Häkchen.

Da der Offline-Renderer den Fehler allein aus aufgezeichneten Eingabedaten reproduzierte, lag die Ursache in den Samples und der Geometrie und nicht im Anzeigeverhalten. Nun konnten wir die exakten Samples lesen, die in den Renderer gelangt waren.

Unsere erste Hypothese machte die geschätzte Pencil-Orientierung beim Aufsetzen und Abheben verantwortlich. Die Daten widersprachen ihr. Über alle 29 Striche hinweg änderte sich das Azimut in den ersten und letzten acht Samples um höchstens 0.09 Radiant und der Höhenwinkel um höchstens 0.02 Radiant; jedes Schätzungsflag war auf diesem iPad false. Die Orientierung blieb stabil, während Druck und Richtung sich stark änderten.

Eine Druckschwelle ergab sich aus einer sauberen Lücke in den Daten

Der letzte Sample jedes aufgezeichneten Strichs hatte den Druck 0.000. Schreib-Samples lagen bei oder über 0.01, während Samples, die aufgezeichnet wurden, nachdem der Pencil die Oberfläche verlassen hatte, bei oder unter 0.002 lagen. Diese in der Luft aufgezeichneten Samples liefen oft noch 2–7 Punkte weiter und legten 0.8–9.0 Punkte zurück; ihre Richtung konnte sich um ungefähr 90 Grad vom geschriebenen Strich wegdrehen.

Strich 2 zeigt den Mechanismus. Sein Körper endete zwischen 143 und 180 Grad. Der druckarme Schweif drehte sich dann auf −90 bis −67 Grad und legte weitere 8.5 Punkte zurück. Der Kontur-Renderer behandelte den letzten Sample in der Luft als maßgeblichen Endpunkt, sodass er ein schmales Stück Geometrie zu diesem Punkt hin verlängerte.

Die gemessenen Drücke lassen fast eine Größenordnung zwischen Schreibbewegung und Bewegung in der Luft klaffen, daher setzten wir die Grenze innerhalb dieser Lücke:

private static let pressureFloor: CGFloat = 0.005

Nur druckarme Samples an den beiden Enden werden entfernt. Ein druckarmes Sample in der Mitte eines Strichs bleibt Teil davon. Die Regel folgt der gemessenen Ereignisgrenze und lässt den geschriebenen Pfad unverändert.

Instrumentierte Nahaufnahme vor dem Kürzen: Grüne Eingabe-Samples wenden sich vom Strich ab, während die rote Kontur ein schmales Häkchen bildet.

Vor dem Kürzen verankert der Renderer die Endkappe an Samples in der Luft. Grüne Punkte sind Roh-Samples; die rote Linie ist die erzeugte Kontur.

Instrumentierte Nahaufnahme nach dem Kürzen: Die blaue Füllung endet an den letzten Schreib-Samples, und die druckarmen grünen Punkte bleiben außerhalb davon.

Nach dem Entfernen von Samples unter 0.005 endet die Endkappe am letzten Schreibpunkt. Diese Druckschwelle bleibt im aktuellen Stempel-Renderer erhalten.

Die Druckschwelle entfernte die langen Häkchen in der Luft. Kurze Aufsetz-Ziehbewegungen und Abhebebögen, deren Druck über 0.005 blieb, überstanden sie, weil sie geschriebene Bewegung sind. Die Behandlung dieser Bewegungen in einem Konturmodell führte zu den nächsten Geometrierunden.

Drei Geometrie-Engines verschoben die Fehler

Generation eins: eine Kontur, eine Füllung

Die erste druckabhängige Engine folgte einer vertrauten Pipeline. Sie glättete eine Mittellinie, berechnete linke und rechte Offsets, verband sie zu einem einzigen Polygon und füllte dieses Polygon einmal. Die Stiftspitze war eine Ellipse, die von Azimut und Höhenwinkel des Apple Pencil gesteuert wurde, sodass sich ihr Querschnitt mit der Schreibrichtung änderte. Kurze Taps erhielten eine eigene Form, und flache Endkappen verwendeten kleine Asymmetrien, um Lücken mit Windungszahl null zu vermeiden.

Die einzelne Füllung löste die Opazitätsakkumulation innerhalb eines Strichs und erforderte bei jeder Wendung ein gültiges Polygon. Aufsetz-Ziehbewegungen erzeugten Keile zwischen Kappe und Körper. Rückläufe konnten weiße sichelförmige Lücken erzeugen. Scharfe Wendungen konnten die innere Kontur sich selbst überschneiden lassen und eine ungefüllte Kerbe hinterlassen.

Generation zwei: die Kontur nahe ihren Enden reparieren

Die zweite Engine behielt die Kontur bei und fügte lokale Reparaturen hinzu: Kürzen von Rückläufen, Dämpfung für früh geschätzte Eigenschaften, Begradigung der Endpunkte, Kürzen vorübergehender Richtungen und eine kleine Verlängerung der Endkappe. Jeder Patch behob einen reproduzierten Fehler. Zusammen machten sie das Verhalten an den Endpunkten von mehreren Schwellenwerten und Zeitfenstern abhängig.

Reales Schreiben erzeugte dann Fälle außerhalb dieser Fenster. Ein festes Begradigungsfenster von 24 Punkten löschte beabsichtigte Häkchen in kurzen Strichen. Ein adaptives Fenster bewahrte diese Häkchen, und Abhebebögen konnten weiterhin sichtbare Ecken werden. Richtungsbewusstes Kürzen entfernte mehr vorübergehende Bewegung, auf Kosten des Verwerfens von etwa 2.5 Punkten eines tatsächlich gekrümmten Anfangs in einem aufgezeichneten Strich.

Jeder Patch behob den Fall, auf den er zielte. Das Modell darunter erforderte weiterhin eine harte Kontur, die unter verrauschter, gekrümmter Eingabe gültig blieb.

Generation drei: Abdeckungsvereinigung mit zugeschnittenen quadratischen Endkappen

Die dritte Engine hörte auf, eine einzige Kontur zu konstruieren. Sie zeichnete für jedes Segment ein Trapez und an jedem inneren Sample eine Scheibe in eine Graustufenmaske, bildete ihre Abdeckungsvereinigung und wendete anschließend die Textmarkerfarbe einmal an. Rücklaufsicheln, Verdunkelungen durch Selbstüberschneidung und Löcher mit Windungszahl null verschwanden, weil der Renderer nicht mehr von der Polygon-Windung abhing.

Die Vereinigung rundete die Enden ab, und wir stellten quadratische Endkappen wieder her, indem wir die Abdeckung an zwei Endpunkt-Halbebenen beschnitten. Diese Beschneidung behob Linien mit konstantem Druck und führte einen schwerwiegenderen Fehler ein: Der Körper eines gekrümmten Strichs kann eine Endpunkt-Halbebene legitimerweise kreuzen, und die Endkappenbeschneidung entfernte dann einen Teil der Strichmitte. Während der Live-Eingabe änderte sich die Endpunktrichtung fortlaufend, sodass sich der beschnittene Bereich bewegte und der Strich flackerte.

Ein gekrümmter blauer Strich vor der Korrektur der Endkappenbeschneidung, dessen oberer Körper entlang einer diagonalen Linie abgeschnitten ist; grüne Punkte zeigen den vollständigen abgetasteten Pfad.

Die globale Endkappenbedingung schneidet durch den Körper eines gekrümmten Strichs.

Derselbe gekrümmte blaue Strich nach dem Hinzufügen einer lokalen Bogenlängenbegrenzung; beide Äste sind um den grünen Sample-Pfad herum wiederhergestellt.

Durch die Beschränkung der Endkappenbeschneidung auf Primitive innerhalb einer halben Breite vom Endpunkt wird dieser Strich wiederhergestellt.

Eine Bogenlängenbegrenzung machte die Endkappenbeschneidung lokal und reparierte jeden aufgezeichneten Fall. Ein extremer synthetischer Haken zeigte weiterhin eine kleine weiße Kerbe, wo eine druckarme Spitze auf die beschnittene Endkappe traf. Die dritte Engine hatte erneut einen Geometriefehler gegen einen anderen eingetauscht.

MaLiang beseitigt die harte Grenze

MaLiang baut einen Strich aus wiederholten Texturstempeln statt aus einer einzigen geschlossenen Kontur auf. Sein Pfadgenerator verwendet quadratische Bézier-Segmente, die durch die Mittelpunkte benachbarter Samples verlaufen, und sein Linien-Renderer platziert entlang dieses Pfads in regelmäßigen Abständen Stempel. Stempelgröße, Rotation, Farbe und Opazität können sich entlang des Strichs verändern.

Ein weicher Stempel besitzt keine separate Endkappe, Verbindung oder Windungsregel. Eine scharfe Richtungsänderung bleibt eine Folge überlappender Markierungen. Rückläufe können keine leere Polygon-Tasche erzeugen, und eine Endpunkt-Halbebene kann nicht die Strichmitte schneiden, weil das Modell keinen Endpunkt-Clip enthält.

Wir übernahmen das Modell, nicht den Code. Der aktuelle HighlighterStrokeBuilder führt vier Operationen aus:

  1. Nur Endpunkt-Samples entfernen, deren Druck unter 0.005 liegt.
  2. Positionen mit quadratischen Bézier-Segmenten über Mittelpunkte glätten.
  3. Einen kreisförmigen Stempel pro Bogenlängenpunkt platzieren.
  4. Den Stempeldurchmesser aus dem normalisierten Druck setzen und die Stempel mit source-over-Alpha komponieren.

Der Builder schrumpfte von 686 Zeilen auf 190. Azimut, Höhenwinkel, Konturkonstruktion, Vereinigungsmasken, Endpunktrichtungsfenster, Endkappenverlängerung und Endkappenbeschneidung verschwanden aus dem Textmarker-Pfad. Wir zeichnen die Pencil-Orientierung weiterhin auf, weil das Eingabemodell sie unterstützt, und der Textmarker selbst liest diese Felder nicht mehr.

Die Druckformel von MaLiang benötigte Gerätekalibrierung

MaLiang bemisst einen Stempel mit einer Formel, die äquivalent zu

ist und voraussetzt, dass force einen nutzbaren Teil des Bereichs von 0 bis 1 verwendet. Unsere Gerätedaten taten das nicht. Unter etwa 3.400 Apple-Pencil-Samples lag der Mediandruck bei 0.047 und das Maximum bei 0.178. Wenn diese Werte direkt in die Formel eingespeist wurden, war normales Schreiben deutlich dünner als die nominale Pinselgröße.

Deshalb normalisieren wir um den beobachteten Median, wenden anschließend den Exponenten an und begrenzen das Ergebnis:

Der Median des Schreibdrucks entspricht nun dem nominalen Durchmesser von 12 Punkten. Sehr leichter Kontakt bleibt bei 35 % der nominalen Größe sichtbar, während starker Druck bei 120 % nicht weiter wächst.

Diese Kalibrierung gehört zum Eingabegerät und zum Pinselverhalten, nicht zu MaLiang. Die ursprüngliche Formel zu kopieren, ohne unsere eigene Druckverteilung zu messen, hätte die Architektur bewahrt und den falschen Pinsel erzeugt.

Die Alpha pro Stempel ergab sich aus der bekannten Überlagerungszahl

MaLiang gleicht überlappende halbtransparente Stempel mit dem empirischen Ausdruck alpha ÷ overlapping × 2.5 aus. Unser Pinsel hat einen festen nominalen Durchmesser von 12 Punkten und eine Stempel-Schrittweite von 1 Punkt, daher erhält seine Mittellinie etwa 12 Ebenen. In unserem Fall ist die Anzahl der Überlagerungen bekannt, und die Kompensation lässt sich berechnen statt schätzen.

Bei einer Ziel-Opazität eines Strichs und gleichen source-over-Schichten erzeugt eine Stempel-Opazität

also benötigt ein Stempel

Bei und verwendet jeder Stempel eine Opazität von etwa . Die Mitte erreicht durch wiederholte Komposition die beabsichtigte Farbe, während die Kante weniger Stempel erhält und einen ein- bis zweipunktigen, weich auslaufenden Übergang behält.

Ein Blatt mit verschiedenartigen blauen Textmarkerstrichen, die mit dem finalen Weichstempelmodell gerendert wurden, darunter Schleifen, Häkchen, kurze Markierungen und Selbstüberschneidungen.

Der finale Renderer spielt die aufgezeichneten Gerätepfade erneut ab. Häkchen, diagonale Schnitte und weiße Lücken fehlen.

Die zwei Kosten, die wir akzeptierten

Selbstüberschneidungen werden dunkler. Die source-over-Komposition zählt jeden Stempel, einschließlich der Stempel, die ein früherer Durchlauf durch denselben Bereich hinterlassen hat, und die Abdeckungsvereinigungs-Engine hatte das vermieden. Wir akzeptierten die dunklere Überlagerung, weil sie vorhersehbar ist und die Strichform intakt lässt.

Endpunkte werden mit dem Druck schmaler. Echtes Abheben reduziert den Druck, wodurch der Stempeldurchmesser sinkt; der Strich verjüngt sich also, statt mit einer quadratischen Endkappe voller Breite zu enden. Wir hatten diese quadratische Endkappe durch alle drei Geometrie-Engines als Anforderung behandelt. Ihr Wegfall entfernte die Beschneidung und die Endpunktmechanik hinter mehreren der obigen Fehler, daher akzeptierten wir das verjüngte Ende.

Das Modell, das wir ausliefern, leistet daher weniger als die vorherigen. Es bewahrt das gemessene Schreibereignis, reagiert nach der Gerätekalibrierung auf Druck und rendert gekrümmte sowie sich kreuzende Pfade, ohne eine global gültige Kontur zu benötigen. Eine meißelförmige Stiftspitze, eine perfekt quadratische Endkappe und die exakte Polylinie zwischen den Roh-Samples sind die Merkmale, auf die wir dafür verzichtet haben.

Von allem, was die Untersuchung hervorgebracht hat, lässt sich die Druckschwelle am direktesten auf andere Projekte übertragen. Der Wert 0.005 ergab sich aus der Lücke zwischen zwei Clustern in realen Eingabedaten: Schreiben bei 0.01 und darüber, Tracking in der Luft bei 0.002 und darunter. Dieselben aufgezeichneten Striche zeigten, wo jede Geometriereparatur scheiterte. Diese Fehler führten zum Stempelmodell und zu den beiden Kosten, die es mit sich bringt.