Varför vi ersatte tre geometrimotorer för överstrykningspennor med stämplar
Riktiga Apple Pencil-data avslöjade fel som syntetiska tester missade och ledde Lulucat Notes från tre konturmotorer till en MaLiang-stämpelmodell.
Lulucat Notes började med en konventionell överstrykningsrenderare: bygg en geometrisk kontur runt den samplade banan och fyll sedan konturen en gång med en halvtransparent färg. En enda fyllning undviker de mörkare band som uppstår där halvtransparenta segment överlappar varandra. Den placerar också varje skarv, självkorsning, ändavslut och kort streck i samma geometriproblem.
Tre generationer av egen geometri tog bort varsin typ av fel och blottlade en annan. Renderaren vi levererar nu använder mjuka cirkulära stämplar baserade på MaLiang, ett MIT-licensierat ramverk för ritning med öppen källkod av Harley-xk. Genom att ta bort en hel kategori av konturfel accepterade vi mörkare självkorsningar och tryckavsmalnande ändar.

Den ursprungliga skärmbilden från enheten vid 228 % zoom. Små krokar syns i båda ändarna av flera streck.
Riktig handskrift skapade krokar som syntetiska scener inte gjorde
Våra syntetiska scener täckte raka linjer, bågar, korta tryck, tillbakadragningar, tryckförändringar och skarpa svängar, och alla renderades rent. Riktig kinesisk handskrift skapade ändå krokar som var ungefär 1–3 punkter långa i början och slutet av många streck.
Därför lade vi till en spara-knapp i appen och spelade in 29 fullständiga streck. Varje sampel behöll sin position, radie, tryck, azimut, höjdvinkel, tidsstämpel och UIKit-flagga för uppskattning. Ett offlineprogram avkodade den JSON-datan och kompilerade samma filer Stroke.swift, StrokeRenderer.swift och HighlighterStrokeBuilder.swift som appen använder. Dess utdata matchade enhetens skärmbild streck för streck, inklusive krokarna.
Eftersom offline-renderaren återskapade felet enbart från inspelad indata låg orsaken i sampelen och geometrin, inte i visningsbeteendet. Nu kunde vi läsa exakt vilka sampel som hade gått in i renderaren.
Vår första hypotes skyllde på uppskattad Pencil-orientering vid nedslag och lyft från ytan. Data avvisade den. I alla 29 streck ändrades azimuten med högst 0.09 radianer och höjdvinkeln med högst 0.02 radianer inom de första och sista åtta sampelen, och varje uppskattningsflagga var falsk på den iPaden. Orienteringen förblev stabil medan tryck och riktning ändrades kraftigt.
Ett tydligt datagap gav en trycktröskel
Det sista sampeln i varje inspelat streck hade trycket 0.000. Skrivsampel låg på eller över 0.01, medan sampel som spelades in efter att Pencil lämnat ytan låg på eller under 0.002. Dessa luftburna sampel fortsatte ofta i 2–7 punkter och förflyttade sig 0.8–9.0 punkter, och deras riktning kunde svänga ungefär 90 grader bort från det skrivna strecket.
Streck 2 visar mekanismen. Dess kropp slutade mellan 143 och 180 grader. Den lågtryckta svansen svängde sedan till −90 genom −67 grader och förflyttade sig ytterligare 8.5 punkter. Konturrenderaren behandlade den sista luftburna punkten som den styrande ändpunkten och sträckte därför en smal geometridel mot den punkten.
De uppmätta trycken lämnar nästan en storleksordning mellan skrivning och luftburen rörelse, så vi placerade gränsen i det gapet:
private static let pressureFloor: CGFloat = 0.005
Endast lågtryckssampel i de två ändarna tas bort. Ett lågtryckssampel mitt i ett streck förblir en del av det. Regeln följer den uppmätta händelsegränsen och lämnar den skrivna banan oredigerad.

Före trimning förankrar renderaren ändavslutet i luftburna sampel. Gröna punkter är råsampel; den röda linjen är den genererade konturen.

Efter trimning av sampel under 0.005 slutar ändavslutet vid den sista skrivna punkten. Den här trycktröskeln finns kvar i den aktuella stämpelrenderaren.
Tröskeln tog bort de långa luftburna krokarna. Korta drag vid kontakt och bågar vid lyft där trycket låg kvar över 0.005 överlevde den, eftersom de är skrivna rörelser. Att hantera dessa rörelser i en konturmodell ledde till nästa omgångar av geometri.
Tre geometrimotorer flyttade runt felen
Generation ett: en kontur, en fyllning
Den första tryckkänsliga motorn följde ett välkänt arbetsflöde. Den jämnade ut en mittlinje, beräknade vänster- och högerförskjutningar, sammanfogade dem till en polygon och fyllde polygonen en gång. Spetsen var en ellips som styrdes av Apple Pencils azimut och höjdvinkel, så dess tvärsnitt ändrades med skrivriktningen. Korta tryck hade en separat form, och platta ändavslut använde små asymmetrier för att undvika fickor som lämnas tomma av nollvindningsregeln.
Engångsfyllningen löste ansamling av opacitet inuti ett streck och krävde en giltig polygon i varje sväng. Drag vid kontakt skapade kilar mellan ändavslutet och kroppen. Tillbakadragningar kunde skapa vita halvmåneformade fickor. Skarpa svängar kunde få den inre konturen att självkorsa och lämna ett ofyllt hack.
Generation två: reparera konturen nära ändarna
Den andra motorn behöll konturen och lade till lokala reparationer: trimning av tillbakadragningar, dämpning av tidigt uppskattade egenskaper, uträtning av ändpunkter, trimning av övergående riktning och en liten förlängning av ändavslutet. Varje korrigering åtgärdade ett återskapat fel. Tillsammans gjorde de ändpunktsbeteendet beroende av flera trösklar och fönster.
Riktig skrivning skapade sedan fall utanför dessa fönster. Ett fast uträtningsfönster på 24 punkter raderade avsiktliga krokar i korta streck. Ett adaptivt fönster bevarade krokarna, och bågar vid lyft kunde fortfarande bli synliga hörn. Riktningsmedveten trimning tog bort mer övergående rörelse, på bekostnad av att ungefär 2.5 punkter av en genuint böjd början kastades bort i ett inspelat streck.
Varje korrigering åtgärdade sitt avsedda fall. Modellen under dem krävde fortfarande en hård kontur som förblev giltig under brusig, böjd indata.
Generation tre: täckningsunion med klippta fyrkantiga ändavslut
Den tredje motorn slutade konstruera en enda kontur. Den ritade en trapets för varje segment och en disk vid varje inre sampel i en gråskalemask, tog unionen av deras täckning och applicerade sedan överstrykningsfärgen en gång. Halvmånar från tillbakadragningar, mörkare självkorsningar och hål med nollvindning försvann, eftersom renderaren inte längre var beroende av polygonens fyllningsregel.
Unionen rundade ändarna, och vi återställde fyrkantiga ändavslut genom att klippa täckningen mot två halvplan vid ändpunkterna. Den klippningen fungerade för raka linjer med konstant tryck och introducerade ett allvarligare fel: kroppen i ett böjt streck kan legitimt korsa ett halvplan vid ändpunkten, och ändavslutsklippningen tog då bort en del av streckets mitt. Under direkt inmatning fortsatte ändpunktsriktningen att ändras, så det klippta området flyttade sig och strecket flimrade.

Den globala ändavslutsbegränsningen skär genom kroppen i ett böjt streck.

Genom att begränsa ändavslutsklippningen till primitiver inom en halv bredd från ändpunkten återställs det här strecket.
Ett båglängdsvillkor gjorde ändavslutsklippningen lokal och reparerade varje inspelat fall. En extrem syntetisk krok visade fortfarande ett litet vitt hack där en lågtryckt spets mötte det klippta ändavslutet. Den tredje motorn hade återigen bytt ett geometrifel mot ett annat.
MaLiang tar bort den hårda gränsen
MaLiang bygger ett streck av upprepade texturstämplar i stället för en enda sluten kontur. Dess banagenerator använder kvadratiska Bézier-segment som går genom mittpunkterna för intilliggande sampel, och linjerenderaren placerar stämplar med ett regelbundet steg längs den banan. Stämpelstorlek, rotation, färg och opacitet kan alla variera längs strecket.
En mjuk stämpel har inget separat ändavslut, ingen skarv och ingen fyllningsregel. En skarp riktningsändring förblir en serie överlappande märken. Tillbakadragningar kan inte skapa en tom polygonficka, och ett halvplan vid ändpunkten kan inte skära av mitten av ett streck, eftersom modellen inte innehåller någon klippning vid ändpunkten.
Vi tog över modellen, inte koden. Den aktuella HighlighterStrokeBuilder utför fyra operationer:
- Ta bara bort ändsampel vars tryck är lägre än
0.005. - Jämna ut positionerna med kvadratiska Bézier-segment genom sampelmittpunkterna.
- Placera en cirkulär stämpel per båglängdspunkt.
- Sätt stämpeldiametern utifrån normaliserat tryck och kompositera stämplarna med source-over-alfa (lager-på-lager-komposition).
Byggaren minskade från 686 rader till 190. Azimut, höjdvinkel, konturkonstruktion, unionsmasker, fönster för ändpunktsriktning, förlängning av ändavslut och klippning av ändavslut försvann från överstrykningsbanan. Vi spelar fortfarande in Pencil-orientering, eftersom indatamodellen stöder den, men själva överstrykningspennan läser inte längre dessa fält.
MaLiangs tryckformel behövde enhetskalibrering
MaLiang dimensionerar en stämpel med en formel som motsvarar
och antar att force använder en användbar del av intervallet från 0 till 1. Vår enhetsdata gjorde inte det. I ungefär 3 400 Apple Pencil-sampel var mediantrycket 0.047 och maximum 0.178. När vi matade in dessa värden direkt i formeln blev normal skrivning mycket tunnare än den nominella penselstorleken.
Därför normaliserar vi kring den observerade medianen, applicerar sedan exponenten och klipper resultatet:
Det mediala skrivtrycket motsvarar nu den nominella diametern på 12 punkter. Mycket lätt kontakt förblir synlig vid 35 % av nominell storlek, medan hårt tryck slutar växa vid 120 %.
Den här kalibreringen hör till inmatningsenheten och penselbeteendet, inte till MaLiang. Att kopiera originalformeln utan att mäta vår egen tryckfördelning skulle ha bevarat arkitekturen men gett oss fel pensel.
Alfa per stämpel kom från det kända överlappningsantalet
MaLiang kompenserar för överlappande halvtransparenta stämplar med det empiriska uttrycket alpha ÷ overlapping × 2.5. Vår pensel har en fast nominell diameter på 12 punkter och ett stämpelsteg på 1 punkt, så dess mittlinje får ungefär 12 lager. I vårt fall är överlappningsantalet känt, och kompensationen kan lösas i stället för att uppskattas.
För en målopacitet
så en stämpel kräver
Med

Den slutliga renderaren spelar upp de inspelade enhetsbanorna. Krokar, diagonala snitt och vita fickor saknas.
De två kostnaderna vi accepterade
Självkorsningar blir mörkare. Source-over-komposition räknar varje stämpel, även stämplar som lagts ut av en tidigare passage genom samma område, och täckningsunionsmotorn hade undvikit det. Vi accepterade den mörkare överlappningen eftersom den är förutsägbar och låter streckformen förbli intakt.
Ändpunkterna smalnar av med trycket. Ett verkligt lyft från ytan minskar trycket, vilket minskar stämpeldiametern, så strecket smalnar av i stället för att sluta med ett fyrkantigt ändavslut i full bredd. Vi hade behandlat det fyrkantiga ändavslutet som ett krav genom alla tre geometrimotorerna. Att släppa det tog bort klippningen och ändpunktsmaskineriet bakom flera av felen ovan, så vi accepterade det avsmalnande slutet.
Modellen vi levererar gör därför mindre än de tidigare. Den bevarar den uppmätta skrivhändelsen, reagerar på tryck efter enhetskalibrering och renderar böjda och självkorsande banor utan att kräva en globalt giltig kontur. En mejselutformad spets, ett perfekt fyrkantigt ändavslut och den exakta polylinjen mellan råsampelen är de egenskaper vi gav upp för detta.
Trycktröskeln är det som enklast kan överföras till andra projekt. Värdet 0.005 kom från gapet mellan två kluster i verklig indata: skrivning vid 0.01 och över, luftburen spårning vid 0.002 och under. Samma inspelade streck visade var varje geometrikorrigering misslyckades. De felen ledde till stämpelmodellen och de två kostnader den medför.