Lulucat

De ce am înlocuit trei motoare de geometrie pentru evidențiator cu amprente

Gaoge ZhangGaoge Zhang

Datele reale Apple Pencil au scos la iveală defecte ratate de testele sintetice, iar Lulucat Notes a trecut la modelul MaLiang cu amprente.

Actualizat

Lulucat Notes a început cu un renderer convențional pentru evidențiator: construia un contur geometric în jurul traseului eșantionat, apoi umplea acel contur o singură dată cu o culoare translucidă. O singură umplere evită benzile mai întunecate care apar acolo unde segmentele translucide se suprapun. De asemenea, pune fiecare îmbinare, auto-intersecție, capăt și tușă scurtă în aceeași problemă de geometrie.

Trei generații de geometrie personalizată au eliminat fiecare câte o clasă de defecte și au scos la iveală alta. Rendererul pe care îl livrăm acum folosește amprente circulare moi bazate pe MaLiang, un framework open-source de desen cu licență MIT, creat de Harley-xk. În schimbul eliminării unei întregi categorii de erori ale conturului, am acceptat auto-intersecții mai întunecate și capete conice în funcție de presiune.

Scris de mână cu evidențiator albastru, capturat pe un iPad, cu cârlige subțiri vizibile la începutul și la sfârșitul mai multor tușe.

Captura de ecran originală de pe dispozitiv, la un zoom de 228%. La ambele capete ale mai multor tușe apar cârlige mici.

Scrisul de mână real a produs cârlige pe care scenele sintetice nu le-au produs

Scenele noastre sintetice acopereau linii drepte, arce, atingeri scurte, reveniri pe traseu, schimbări de presiune și viraje bruște, iar toate erau redate curat. Scrisul real de mână chinezesc producea totuși cârlige de aproximativ 1–3 puncte la începutul și la sfârșitul multor tușe.

Prin urmare, am adăugat un buton de salvare în aplicație și am înregistrat 29 de tușe complete. Fiecare eșantion își păstra poziția, raza, presiunea, azimutul, altitudinea, marca temporală și indicatorii de estimare UIKit. Un program offline decoda acel JSON și compila aceleași fișiere Stroke.swift, StrokeRenderer.swift și HighlighterStrokeBuilder.swift pe care le folosește aplicația. Rezultatul său se potrivea cu captura de ecran de pe dispozitiv tușă cu tușă, inclusiv cârligele.

Deoarece rendererul offline reproducea defectul doar din datele de intrare înregistrate, cauza se afla în eșantioane și geometrie, nu în comportamentul afișajului. Acum puteam citi eșantioanele exacte care intraseră în renderer.

Prima noastră ipoteză punea vina pe orientarea estimată a Pencil la atingerea și desprinderea de suprafață. Datele au infirmat-o. În toate cele 29 de tușe, azimutul s-a schimbat cu cel mult 0.09 radiani, iar altitudinea cu cel mult 0.02 radiani în primele și ultimele opt eșantioane, iar toți indicatorii de estimare erau falși pe acel iPad. Orientarea a rămas stabilă, în timp ce presiunea și direcția s-au schimbat brusc.

O limită de presiune a venit dintr-un spațiu clar în date

Ultimul eșantion al fiecărei tușe înregistrate avea presiunea 0.000. Eșantioanele din timpul scrisului erau la sau peste 0.01, în timp ce eșantioanele înregistrate după ce Pencil părăsise suprafața erau la sau sub 0.002. Acele eșantioane din aer continuau adesea încă 2–7 puncte și parcurgeau 0.8–9.0 puncte, iar direcția lor se putea întoarce cu aproximativ 90 de grade față de tușa scrisă.

Tușa 2 arată mecanismul. Corpul ei se termina între 143 și 180 de grade. Coada cu presiune scăzută se întorcea apoi de la −90 până la −67 de grade și mai parcurgea 8.5 puncte. Rendererul de contur trata ultimul punct din aer ca fiind capătul de referință, așa că întindea o porțiune îngustă de geometrie către acel punct.

Presiunile măsurate lasă aproape un ordin de mărime între mișcarea de scriere și cea din aer, așa că am pus pragul în interiorul acestui spațiu:

private static let pressureFloor: CGFloat = 0.005

Sunt eliminate doar eșantioanele cu presiune scăzută de la cele două capete. Un eșantion cu presiune scăzută din mijlocul unei tușe rămâne parte din ea. Regula urmează limita măsurată dintre evenimente și lasă traseul scris nemodificat.

Imagine de aproape instrumentată înainte de tăiere: eșantioanele verzi de intrare se îndepărtează de tușă, în timp ce conturul roșu formează un cârlig îngust.

Înainte de tăiere, rendererul fixează capătul pe eșantioanele din aer. Punctele verzi sunt eșantioane brute; linia roșie este conturul generat.

Imagine de aproape instrumentată după tăiere: umplerea albastră se termină la ultimele eșantioane scrise, iar punctele verzi cu presiune scăzută rămân în afara ei.

După tăierea eșantioanelor sub 0.005, capătul se termină la ultimul punct scris. Această limită de presiune rămâne în rendererul actual bazat pe amprente.

Limita a eliminat cârligele lungi din aer. Tragerile scurte la atingere și arcele de desprindere a căror presiune a rămas peste 0.005 au supraviețuit, deoarece sunt mișcări de scriere. Tratarea acestor mișcări într-un model bazat pe contur a dus la următoarele runde de geometrie.

Trei motoare de geometrie au mutat defectele dintr-un loc în altul

Generația unu: un contur, o umplere

Primul motor sensibil la presiune urma un flux familiar. Netezea o linie mediană, calcula decalajele din stânga și din dreapta, le unea într-un singur poligon și umplea poligonul o singură dată. Vârful era o elipsă determinată de azimutul și altitudinea Apple Pencil, astfel încât secțiunea sa transversală se schimba odată cu direcția de scriere. Atingerile scurte aveau o formă separată, iar capetele plate foloseau mici asimetrii pentru a evita buzunarele cu număr de înfășurare zero.

Umplerea unică rezolva acumularea opacității în interiorul unei tușe și impunea existența unui poligon valid la fiecare viraj. Tragerile la atingere creau pene între capăt și corp. Revenirea pe traseu putea crea buzunare albe în formă de semilună. Virajele bruște puteau face conturul interior să se auto-intersecteze și să lase o crestătură neumplută.

Generația doi: repararea conturului în apropierea capetelor

Al doilea motor păstra conturul și adăuga reparații locale: tăierea revenirilor pe traseu, amortizare pentru proprietățile estimate timpurii, îndreptarea capetelor, tăierea direcției tranzitorii și o mică prelungire a capătului. Fiecare patch trata un eșec reprodus. Împreună, făceau ca comportamentul capetelor să depindă de mai multe praguri și ferestre.

Scrisul real a produs apoi cazuri în afara acelor ferestre. O fereastră fixă de îndreptare de 24 de puncte ștergea cârlige intenționate în tușele scurte. O fereastră adaptivă păstra acele cârlige, iar arcele de desprindere puteau deveni în continuare colțuri vizibile. Tăierea conștientă de direcție elimina mai multă mișcare tranzitorie, cu prețul eliminării a aproximativ 2.5 puncte dintr-un început cu adevărat curbat într-o tușă înregistrată.

Fiecare patch își rezolva cazul țintă. Modelul de dedesubt necesita în continuare un contur rigid care să rămână valid la intrare zgomotoasă și curbată.

Generația trei: reuniunea acoperirii cu capete pătrate decupate

Al treilea motor a încetat să mai construiască un singur contur. Desena un trapez pentru fiecare segment și un disc la fiecare eșantion interior într-o mască în tonuri de gri, calcula reuniunea acoperirii lor, apoi aplica o singură dată culoarea evidențiatorului. Semilunele produse de reveniri, întunecarea auto-intersecțiilor și găurile cu număr de înfășurare zero au dispărut, deoarece rendererul nu mai depindea de sensul de înfășurare al poligonului.

Reuniunea rotunjea capetele, iar noi am restaurat capetele pătrate decupând acoperirea după două semiplane de la capete. Decuparea a reparat liniile drepte cu presiune constantă și a introdus un defect mai grav: corpul unei tușe curbe poate traversa în mod legitim un semiplan de la capăt, iar decuparea capătului elimina atunci o parte din mijlocul tușei. În timpul introducerii live, direcția capătului se schimba continuu, astfel că zona decupată se deplasa și tușa pâlpâia.

O tușă albastră curbată înainte de remedierea decupării capătului, cu corpul superior tăiat de-a lungul unei linii diagonale; punctele verzi arată traseul eșantionat complet.

Constrângerea globală a capătului taie corpul unei tușe curbe.

Aceeași tușă albastră curbată după adăugarea unei limite locale pe lungimea de arc, cu ambele ramuri restaurate în jurul traseului eșantionat verde.

Limitarea decupării capătului la primitivele aflate la cel mult o jumătate de lățime de capăt restaurează această tușă.

O limită pe lungimea de arc a făcut decuparea capătului locală și a reparat fiecare caz înregistrat. Un cârlig sintetic extrem a arătat totuși o mică crestătură albă acolo unde un vârf cu presiune scăzută întâlnea capătul decupat. Și al treilea motor schimbase din nou un eșec de geometrie cu altul.

MaLiang elimină granița rigidă

MaLiang construiește o tușă din amprente de textură repetate, nu dintr-un singur contur închis. Generatorul traseului său folosește segmente Bézier pătratice care trec prin punctele mediane ale eșantioanelor alăturate, iar rendererul de linii plasează amprente la un pas regulat de-a lungul traseului. Dimensiunea, rotația, culoarea și opacitatea amprentei pot varia de-a lungul tușei.

O amprentă moale nu are un capăt, o îmbinare sau o regulă de înfășurare separată. O schimbare bruscă de direcție rămâne o succesiune de urme suprapuse. Revenirea pe traseu nu poate crea un buzunar gol în poligon, iar un semiplan de la capăt nu poate tăia mijlocul unei tușe, deoarece modelul nu conține nicio decupare la capăt.

Am adoptat modelul, nu codul. HighlighterStrokeBuilder actual efectuează patru operații:

  1. Elimină doar eșantioanele de la capete a căror presiune este sub 0.005.
  2. Netezește pozițiile cu segmente Bézier pătratice prin punctele mediane.
  3. Plasează câte o amprentă circulară pentru fiecare punct de lungime de arc.
  4. Stabilește diametrul amprentei din presiunea normalizată și compune amprentele cu alfa source-over (peste sursă).

Builderul a scăzut de la 686 de linii la 190. Azimutul, altitudinea, construcția conturului, măștile de reuniune, ferestrele direcției capătului, prelungirea capătului și decuparea capătului au ieșit toate din traseul evidențiatorului. Încă înregistrăm orientarea Pencil, deoarece modelul de intrare o acceptă, iar evidențiatorul în sine nu mai citește aceste câmpuri.

Formula de presiune MaLiang a avut nevoie de calibrarea dispozitivului

MaLiang dimensionează o amprentă cu o formulă echivalentă cu

care presupune că force folosește o parte utilă a intervalului de la 0 la 1. Datele de pe dispozitivul nostru nu făceau asta. În aproximativ 3.400 de eșantioane Apple Pencil, presiunea mediană era 0.047, iar valoarea maximă era 0.178. Introducerea directă a acestor valori în formulă făcea scrisul normal mult mai subțire decât dimensiunea nominală a pensulei.

Prin urmare, normalizăm în jurul medianei observate, apoi aplicăm exponentul și limităm rezultatul:

Presiunea mediană de scriere se mapează acum la diametrul nominal de 12 puncte. Contactul foarte ușor rămâne vizibil la 35% din dimensiunea nominală, în timp ce presiunea puternică nu mai crește peste 120%.

Această calibrare ține de dispozitivul de intrare și de comportamentul pensulei, nu de MaLiang. Copierea formulei originale fără măsurarea distribuției noastre de presiune ar fi păstrat arhitectura și ar fi produs pensula greșită.

Alfa per amprentă a venit din numărul cunoscut de suprapuneri

MaLiang compensează amprentele translucide suprapuse cu expresia empirică alpha ÷ overlapping × 2.5. Pensula noastră are un diametru nominal fix de 12 puncte și un pas între amprente de 1 punct, astfel că linia sa centrală primește aproximativ 12 straturi. În cazul nostru, numărul suprapunerilor este cunoscut, iar compensarea poate fi rezolvată, nu estimată.

Pentru o opacitate țintă a tușei și straturi egale source-over, o opacitate a amprentei produce

așadar o singură amprentă necesită

Cu și , fiecare amprentă folosește o opacitate de aproximativ . Centrul ajunge la culoarea dorită după compoziții repetate, în timp ce marginea primește mai puține amprente și păstrează o tranziție estompată de unul sau două puncte.

O planșă cu tușe albastre de evidențiator variate, redate de modelul final cu amprente moi, inclusiv bucle, cârlige, semne scurte și auto-intersecții.

Rendererul final redă din nou traseele înregistrate de pe dispozitiv. Cârligele, tăieturile diagonale și buzunarele albe lipsesc.

Cele două costuri pe care le-am acceptat

Auto-intersecțiile devin mai întunecate. Compoziția source-over numără fiecare amprentă, inclusiv amprentele depuse de o trecere anterioară prin aceeași zonă, iar motorul cu reuniunea acoperirii evitase acest lucru. Am acceptat suprapunerea mai întunecată deoarece este previzibilă și păstrează forma tușei intactă.

Capetele se îngustează odată cu presiunea. O desprindere reală reduce presiunea, ceea ce reduce diametrul amprentei, astfel că tușa se subțiază în loc să se termine cu un capăt pătrat pe toată lățimea. Am tratat acel capăt pătrat ca pe o cerință în toate cele trei motoare de geometrie. Renunțarea la el a eliminat decuparea și mecanismele capetelor din spatele mai multor eșecuri de mai sus, așa că am acceptat capătul conic.

Prin urmare, modelul pe care îl livrăm face mai puțin decât cele de dinainte. Păstrează evenimentul de scriere măsurat, răspunde la presiune după calibrarea dispozitivului și redă trasee curbe și cu auto-intersecții fără a necesita un contur valid la nivel global. Un vârf în formă de daltă, un capăt perfect pătrat și polilinia exactă dintre eșantioanele brute sunt caracteristicile la care am renunțat pentru asta.

Limita de presiune se transferă cel mai direct către alte proiecte. Valoarea 0.005 a venit din spațiul dintre două clustere din datele reale de intrare: scriere la 0.01 și peste, urmărire în aer la 0.002 și sub această valoare. Aceleași tușe înregistrate au arătat unde a eșuat fiecare reparație de geometrie. Aceste eșecuri au dus la modelul bazat pe amprente și la cele două costuri pe care le presupune.