Lulucat

Waarom we drie geometrie-engines voor de markeerstift vervingen door stempels

Gaoge ZhangGaoge Zhang

Echte Apple Pencil-data onthulden gebreken die synthetische tests misten en brachten Lulucat Notes van drie omtrekmodellen naar MaLiang-stempels.

Bijgewerkt

Lulucat Notes begon met een conventionele renderer voor markeerstiften: bouw een geometrische omtrek rond het gesamplede pad en vul die omtrek daarna één keer met een doorschijnende kleur. Eén vulling voorkomt de donkere banden die ontstaan waar doorschijnende segmenten elkaar overlappen. Het plaatst bovendien elke verbinding, zelfsnijding, cap en korte streek binnen hetzelfde geometrische probleem.

Drie generaties aangepaste geometrie verwijderden elk één soort gebrek en brachten een ander aan het licht. De renderer die we nu uitleveren gebruikt zachte, ronde stempels op basis van MaLiang, een open-source-tekenframework met MIT-licentie van Harley-xk. In ruil voor het verwijderen van een hele categorie omtrekfouten accepteerden we donkerdere zelfsnijdingen en uiteinden die door druk taps toelopen.

Blauwe handschriftstreken met een markeerstift, vastgelegd op een iPad, met dunne haakjes zichtbaar aan het begin en einde van verschillende streken.

De oorspronkelijke schermafbeelding van het apparaat bij 228% zoom. Aan beide uiteinden van verschillende streken verschijnen kleine haakjes.

Echt handschrift leverde haakjes op die synthetische scènes niet lieten zien

Onze synthetische scènes omvatten rechte lijnen, bogen, korte tikken, terugbewegingen, drukveranderingen en scherpe bochten, en ze werden allemaal netjes gerenderd. Echt Chinees handschrift leverde toch haakjes van ongeveer 1–3 punten lang op aan het begin en einde van veel streken.

Daarom voegden we een opslagknop aan de app toe en namen we 29 volledige streken op. Elke sample behield zijn positie, straal, druk, azimut, hoogte, tijdstempel en UIKit-schattingstags. Een offlineprogramma decodeerde die JSON en compileerde dezelfde bestanden Stroke.swift, StrokeRenderer.swift en HighlighterStrokeBuilder.swift die de app gebruikt. De uitvoer kwam streek voor streek overeen met de schermafbeelding van het apparaat, inclusief de haakjes.

Omdat de offline renderer het gebrek uitsluitend vanuit de opgenomen invoer reproduceerde, lag de oorzaak bij de samples en de geometrie, niet bij het weergavegedrag. We konden nu de exacte samples lezen die de renderer waren binnengekomen.

Onze eerste hypothese gaf de geschatte Pencil-oriëntatie bij het neerzetten en optillen de schuld. De gegevens wezen die af. Over alle 29 streken veranderde de azimut in de eerste en laatste acht samples maximaal 0.09 radialen en de hoogte maximaal 0.02 radialen, en op die iPad stond elke schattingsvlag op false. De oriëntatie bleef stabiel terwijl druk en richting scherp veranderden.

Een drukdrempel kwam uit een duidelijke leemte in de data

De laatste sample van elke opgenomen streek had druk 0.000. Samples tijdens het schrijven zaten op of boven 0.01, terwijl samples die werden opgenomen nadat de Pencil het oppervlak had verlaten op of onder 0.002 zaten. Die samples in de lucht gingen vaak nog 2–7 punten door en legden 0.8–9.0 punten af; hun richting kon ongeveer 90 graden wegdraaien van de geschreven streek.

Streek 2 laat het mechanisme zien. Het lichaam eindigde tussen 143 en 180 graden. De staart met lage druk draaide daarna van −90 naar −67 graden en legde nog 8.5 punten af. De omtrekrenderer behandelde het laatste luchtpunt als het gezaghebbende eindpunt en rekte daardoor een smal stukje geometrie naar dat punt uit.

De gemeten drukken laten een verschil van bijna een orde van grootte zien tussen schrijven en bewegen in de lucht, dus we plaatsten de cutoff in die leemte:

private static let pressureFloor: CGFloat = 0.005

Alleen samples met lage druk aan de twee uiteinden worden verwijderd. Een sample met lage druk in het midden van een streek blijft er deel van uitmaken. De regel volgt de gemeten gebeurtenisgrens en laat het geschreven pad onbewerkt.

Geïnstrumenteerde close-up vóór het trimmen: groene invoersamples buigen van de streek weg terwijl de rode omtrek een smal haakje vormt.

Vóór het trimmen verankert de renderer de cap aan samples in de lucht. Groene stippen zijn ruwe samples; de rode lijn is de gegenereerde omtrek.

Geïnstrumenteerde close-up na het trimmen: de blauwe vulling eindigt bij de laatste geschreven samples en de groene samples met lage druk blijven erbuiten.

Na het trimmen van samples onder 0.005 eindigt de cap bij het laatste geschreven punt. Deze drukdrempel blijft in de huidige stempelrenderer aanwezig.

De drempel verwijderde de lange haken in de lucht. Korte sleepbewegingen bij het neerzetten en bogen bij het optillen waarvan de druk boven 0.005 bleef, overleefden de ingreep omdat het om schrijfbewegingen gaat. Het verwerken van die bewegingen in een omtrekmodel leidde tot de volgende rondes geometrie.

Drie geometrie-engines verplaatsten de gebreken alleen maar

Generatie één: één omtrek, één vulling

De eerste drukgevoelige engine volgde een bekende pipeline. Hij maakte een spine glad, berekende linker- en rechteroffsets, voegde die samen tot één polygoon en vulde die polygoon één keer. De penpunt was een ellips die werd aangestuurd door de azimut en hoogte van de Apple Pencil, waardoor de dwarsdoorsnede veranderde met de schrijfrichting. Korte tikken hadden een aparte vorm en vlakke caps gebruikten kleine asymmetrieën om pockets met winding nul te vermijden.

De enkele vulling loste de ophoping van opacity in een streek op, maar vereiste bij elke bocht een geldige polygoon. Sleepbewegingen bij het neerzetten vormden wiggen tussen de cap en het lichaam. Terugbewegingen konden witte halvemaanvormige pockets maken. Scherpe bochten konden de binnenste omtrek zichzelf laten snijden en een ongevulde inkeping achterlaten.

Generatie twee: de omtrek bij de uiteinden repareren

De tweede engine behield de omtrek en voegde lokale reparaties toe: trimmen van terugbewegingen, demping van vroeg geschatte eigenschappen, het rechtmaken van het eindpunt, trimmen van tijdelijke richtingen en een kleine verlenging van de cap. Elke patch pakte een gereproduceerd probleem aan. Samen maakten ze het gedrag van het eindpunt afhankelijk van meerdere drempels en vensters.

Echt schrijven leverde vervolgens gevallen buiten die vensters op. Een vast venster van 24 punten voor het rechtmaken wiste opzettelijke haakjes in korte streken. Een adaptief venster behield die haakjes, maar bogen bij het optillen konden nog steeds zichtbare hoeken worden. Richtingsbewust trimmen verwijderde meer tijdelijke beweging, ten koste van ongeveer 2.5 punten van een werkelijk gebogen begin in één opgenomen streek.

Elke patch repareerde zijn doelgeval. Het onderliggende model vereiste nog steeds een harde omtrek die geldig bleef bij ruisende, gebogen invoer.

Generatie drie: dekkingsunie met geknipte vierkante caps

De derde engine stopte met het construeren van één omtrek. Hij tekende voor elk segment een trapezium en bij elk intern sample een schijf in een grijswaardenmasker, nam de unie van hun dekking en paste daarna één keer de markeerstiftkleur toe. Halvemaanvormige terugbewegingen, verdonkering door zelfsnijding en gaten met winding nul verdwenen omdat de renderer niet langer afhankelijk was van de winding van de polygoon.

De unie maakte de uiteinden rond, dus herstelden we vierkante caps door de dekking tegen twee halfvlakken bij de eindpunten te knippen. Die knip repareerde rechte lijnen met constante druk, maar introduceerde een ernstiger gebrek: het lichaam van een gebogen streek kan een halfvlak bij het eindpunt legitiem kruisen, waarna de capknip een deel van het midden van de streek verwijderde. Tijdens live invoer bleef de richting van het eindpunt veranderen, waardoor het geknipte gebied verschoof en de streek flikkerde.

Een gebogen blauwe streek vóór de reparatie van de capknip, met het bovenlichaam afgesneden langs een diagonale lijn; groene stippen tonen het volledige gesamplede pad.

De globale capbeperking snijdt door het lichaam van een gebogen streek.

Dezelfde gebogen blauwe streek na het toevoegen van een lokale poort op basis van booglengte, met beide takken hersteld rond het groene gesamplede pad.

Door capknippen te beperken tot primitieven binnen een halve breedte van het eindpunt wordt deze streek hersteld.

Een poort op basis van booglengte maakte het capknippen lokaal en repareerde elk opgenomen geval. Een extreem synthetisch haakje liet nog steeds een kleine witte inkeping zien waar een punt met lage druk de geknipte cap raakte. De derde engine had opnieuw de ene geometriefout ingeruild voor een andere.

MaLiang verwijdert de harde grens

MaLiang bouwt een streek uit herhaalde textuurstempels in plaats van één gesloten omtrek. De padgenerator gebruikt kwadratische Bézier-segmenten die door de middelpunten van aangrenzende samples gaan, en de lijnrenderer plaatst stempels met een vaste stap langs dat pad. De grootte, rotatie, kleur en opacity van de stempel kunnen allemaal langs de streek variëren.

Een zachte stempel heeft geen aparte cap, verbinding of windingregel. Een scherpe richtingsverandering blijft een reeks overlappende markeringen. Terugbewegingen kunnen geen lege polygoonpocket maken en een halfvlak bij het eindpunt kan het midden van een streek niet doorsnijden, omdat het model geen eindpuntknip bevat.

We namen het model over, niet de code. De huidige HighlighterStrokeBuilder voert vier bewerkingen uit:

  1. Verwijder alleen eindpuntsamples waarvan de druk lager is dan 0.005.
  2. Maak posities glad met kwadratische Bézier-segmenten door de middelpunten.
  3. Plaats één ronde stempel per punt op basis van de booglengte.
  4. Stel de diameter van de stempel in op basis van genormaliseerde druk en combineer de stempels met source-over-alfa.

De builder kromp van 686 regels naar 190. Azimut, hoogte, omtrekconstructie, uniemaskers, richtingsvensters bij de eindpunten, capverlenging en capknippen verdwenen allemaal uit het markeerstiftpad. We registreren de Pencil-oriëntatie nog steeds omdat het invoermodel die ondersteunt, maar de markeerstift zelf leest die velden niet meer.

De drukformule van MaLiang had apparaatkalibratie nodig

MaLiang bepaalt de grootte van een stempel met een formule die gelijkwaardig is aan

die ervan uitgaat dat force een bruikbaar deel van het bereik van 0 tot 1 gebruikt. Onze apparaatmetingen voldeden daar niet aan. In ongeveer 3,400 Apple Pencil-samples was de mediane druk 0.047 en het maximum 0.178. Als we die waarden rechtstreeks in de formule invoerden, werd normaal schrijven veel dunner dan de nominale penseelgrootte.

Daarom normaliseren we rond de waargenomen mediaan, passen we daarna de exponent toe en begrenzen we het resultaat:

De mediane schrijfdruk wordt nu omgezet naar de nominale diameter van 12 punten. Heel licht contact blijft zichtbaar op 35% van de nominale grootte, terwijl zware druk niet verder groeit dan 120%.

Deze kalibratie hoort bij het invoerapparaat en het gedrag van de kwast, niet bij MaLiang. Als we de oorspronkelijke formule zonder onze eigen drukverdeling te meten hadden gekopieerd, was de architectuur behouden gebleven maar had de kwast verkeerd gewerkt.

Alfa per stempel kwam uit het bekende aantal overlappende lagen

MaLiang compenseert overlappende doorschijnende stempels met de empirische uitdrukking alpha ÷ overlapping × 2.5. Onze kwast heeft een vaste nominale diameter van 12 punten en een stempelstap van 1 punt, waardoor de middellijn ongeveer 12 lagen ontvangt. In ons geval is het aantal overlappingen bekend en kan de compensatie worden opgelost in plaats van geschat.

Voor een gewenste streekopacity en gelijke source-over-lagen levert een stempelopacity

dus één stempel vereist

Bij en gebruikt elke stempel een opacity van ongeveer . Het midden bereikt na herhaalde compositie de bedoelde kleur, terwijl de rand minder stempels ontvangt en een zachte overgang van één of twee punten behoudt.

Een vel met verschillende blauwe markeerstiftstreken, gerenderd door het uiteindelijke zachte stempelmodel, met lussen, haakjes, korte markeringen en zelfsnijdingen.

De uiteindelijke renderer speelt de opgenomen apparaatpaden opnieuw af. Haakjes, diagonale sneden en witte pockets ontbreken.

De twee kosten die we accepteerden

Zelfsnijdingen worden donkerder. Source-over-compositie telt elke stempel, ook stempels die door een eerdere passage door hetzelfde gebied zijn geplaatst; de dekkingsunie-engine had dat vermeden. We accepteerden de donkerdere overlap omdat die voorspelbaar is en de vorm van de streek intact laat.

Uiteinden worden smaller naarmate de druk afneemt. Echt optillen verlaagt de druk en daarmee de diameter van de stempel, zodat de streek taps toeloopt in plaats van te eindigen met een vierkante cap over de volle breedte. We hadden die vierkante cap in alle drie geometrie-engines als vereiste behandeld. Door die los te laten verdwenen de knip- en eindpuntmechanismen achter verschillende van de bovenstaande fouten, dus accepteerden we het taps toelopende uiteinde.

Het model dat we uitleveren doet daarom minder dan de modellen ervoor. Het bewaart de gemeten schrijfgebeurtenis, reageert op druk na apparaatkalibratie en rendert gebogen en zelfsnijdende paden zonder een globaal geldige omtrek te vereisen. Een beitelvormige penpunt, een perfect vierkante cap en de exacte polylijn tussen ruwe samples zijn de functies die we daarvoor hebben opgegeven.

De drukdrempel is het meest rechtstreeks overdraagbaar naar andere projecten. De waarde 0.005 kwam uit de leemte tussen twee clusters in echte invoer: schrijven bij 0.01 en hoger, en tracking in de lucht bij 0.002 en lager. Dezelfde opgenomen streken lieten zien waar elke geometrische reparatie faalde. Die mislukkingen leidden tot het stempelmodel en de twee kosten die het met zich meebrengt.