Lulucat

Perché abbiamo sostituito tre motori geometrici dell’evidenziatore con timbri

Gaoge ZhangGaoge Zhang

I dati reali dell’Apple Pencil hanno rivelato difetti sfuggiti ai test sintetici, portando Lulucat Notes da tre motori di contorno a un modello a timbri MaLiang.

Aggiornato

Lulucat Notes è nato con un renderer convenzionale per l’evidenziatore: costruire un contorno geometrico attorno al percorso campionato e poi riempirlo una sola volta con un colore traslucido. Un unico riempimento evita le bande più scure che compaiono quando segmenti traslucidi si sovrappongono. Allo stesso tempo porta ogni raccordo, intersezione con sé stesso, estremità e tratto breve dentro lo stesso problema geometrico.

Tre generazioni di geometria personalizzata hanno eliminato ciascuna una classe di difetti, esponendone un’altra. Il renderer che distribuiamo ora usa timbri circolari morbidi basati su MaLiang, un framework di disegno open source con licenza MIT realizzato da Harley-xk. In cambio della rimozione di un’intera categoria di problemi dei contorni, abbiamo accettato intersezioni con sé stesso più scure ed estremità rastremate dalla pressione.

Scrittura blu con evidenziatore catturata su un iPad, con sottili uncini visibili all’inizio e alla fine di diversi tratti.

Lo screenshot originale del dispositivo con zoom al 228%. Piccoli uncini compaiono su entrambe le estremità di diversi tratti.

La scrittura a mano reale produceva uncini che le scene sintetiche non mostravano

Le nostre scene sintetiche comprendevano linee rette, archi, tocchi brevi, ripassamenti, variazioni di pressione e svolte brusche, e venivano tutte renderizzate correttamente. La scrittura cinese reale produceva comunque uncini lunghi circa 1–3 punti all’inizio e alla fine di molti tratti.

Abbiamo quindi aggiunto un pulsante di salvataggio all’app e registrato 29 tratti completi. Ogni campione conservava posizione, raggio, pressione, azimut, altitudine, marca temporale e flag di stima di UIKit. Un programma offline decodificava quel JSON e compilava gli stessi file Stroke.swift, StrokeRenderer.swift e HighlighterStrokeBuilder.swift usati dall’app. L’output corrispondeva allo screenshot del dispositivo tratto per tratto, uncini inclusi.

Poiché il renderer offline riproduceva il difetto usando soltanto l’input registrato, la causa risiedeva nei campioni e nella geometria, non nel comportamento del display. Ora potevamo leggere i campioni esatti entrati nel renderer.

La nostra prima ipotesi attribuiva la causa all’orientamento stimato della Pencil durante l’appoggio e il sollevamento. I dati l’hanno smentita. Su tutti i 29 tratti, l’azimut cambiava al massimo di 0.09 radianti e l’altitudine al massimo di 0.02 radianti nei primi e negli ultimi otto campioni, e ogni flag di stima era impostato su false su quell’iPad. L’orientamento restava stabile mentre pressione e direzione cambiavano bruscamente.

Un limite di pressione nasceva da un divario netto nei dati

L’ultimo campione di ogni tratto registrato aveva pressione 0.000. I campioni durante la scrittura erano pari o superiori a 0.01, mentre quelli registrati dopo che la Pencil aveva lasciato la superficie erano pari o inferiori a 0.002. Questi campioni in aria spesso continuavano per 2–7 punti e percorrevano 0.8–9.0 punti, e la loro direzione poteva deviare di circa 90 gradi rispetto al tratto scritto.

Il tratto 2 mostra il meccanismo. Il suo corpo terminava tra 143 e 180 gradi. La coda a bassa pressione poi cambiava direzione, passando da −90 a −67 gradi, e percorreva altri 8.5 punti. Il renderer del contorno trattava l’ultimo punto in aria come estremità autorevole, allungando così una sottile porzione di geometria verso quel punto.

Le pressioni misurate lasciano quasi un ordine di grandezza tra la scrittura e il movimento in aria, quindi abbiamo collocato il cutoff all’interno di quel divario:

private static let pressureFloor: CGFloat = 0.005

Vengono rimossi solo i campioni a bassa pressione alle due estremità. Un campione a bassa pressione nel mezzo di un tratto resta parte del tratto. La regola segue il confine dell’evento misurato e lascia intatto il percorso scritto.

Primo piano strumentato prima del taglio: i campioni di input verdi si allontanano dal tratto mentre il contorno rosso forma un uncino stretto.

Prima del taglio, il renderer ancora l’estremità ai campioni in aria. I punti verdi sono campioni grezzi; la linea rossa è il contorno generato.

Primo piano strumentato dopo il taglio: il riempimento blu termina agli ultimi campioni scritti e i punti verdi a bassa pressione restano al di fuori.

Dopo aver tagliato i campioni sotto 0.005, l’estremità termina nell’ultimo punto scritto. Questo limite di pressione è ancora presente nell’attuale renderer a timbri.

Il limite ha rimosso i lunghi uncini in aria. I brevi trascinamenti durante l’appoggio e gli archi durante il sollevamento con pressione superiore a 0.005 sono sopravvissuti, perché fanno parte del movimento di scrittura. Gestire questi movimenti dentro un modello a contorno ha portato ai cicli geometrici successivi.

Tre motori geometrici spostavano i difetti

Prima generazione: un contorno, un riempimento

Il primo motore sensibile alla pressione seguiva una pipeline familiare. Levigava una linea mediana, calcolava gli offset sinistro e destro, li univa in un unico poligono e riempiva quel poligono una volta sola. Il pennino era un’ellisse guidata da azimut e altitudine dell’Apple Pencil, quindi la sua sezione trasversale cambiava con la direzione di scrittura. I tocchi brevi avevano una forma separata, mentre le estremità piatte usavano piccole asimmetrie per evitare tasche a winding nullo.

Il riempimento singolo risolveva l’accumulo di opacità all’interno di un tratto, ma richiedeva un poligono valido a ogni svolta. I trascinamenti durante l’appoggio creavano cunei tra l’estremità e il corpo. I ripassamenti potevano creare tasche bianche a mezzaluna. Le svolte brusche potevano far intersecare il contorno interno con sé stesso e lasciare una tacca non riempita.

Seconda generazione: riparare il contorno vicino alle estremità

Il secondo motore manteneva il contorno e aggiungeva correzioni locali: taglio dei ripassamenti, smorzamento delle proprietà stimate iniziali, raddrizzamento dell’estremità, taglio della direzione transitoria e una piccola estensione dell’estremità. Ogni patch affrontava un difetto riprodotto. Insieme, rendevano il comportamento delle estremità dipendente da diverse soglie e finestre.

La scrittura reale produsse poi casi al di fuori di quelle finestre. Una finestra fissa di raddrizzamento di 24 punti cancellava gli uncini intenzionali nei tratti brevi. Una finestra adattiva preservava quegli uncini, ma gli archi di sollevamento potevano ancora diventare angoli visibili. Il taglio sensibile alla direzione eliminava più movimento transitorio, al costo di scartare circa 2.5 punti di un inizio realmente curvo in un tratto registrato.

Ogni patch risolveva il caso a cui era destinata. Il modello sottostante continuava però a richiedere un contorno rigido che restasse valido con input rumorosi e curvi.

Terza generazione: unione della copertura con estremità quadrate ritagliate

Il terzo motore smise di costruire un unico contorno. Disegnava un trapezio per ogni segmento e un disco su ogni campione interno in una maschera in scala di grigi, ne prendeva l’unione di copertura e applicava il colore dell’evidenziatore una sola volta. Le mezzelune dei ripassamenti, l’oscuramento delle intersezioni con sé stesso e i buchi a winding nullo scomparivano, perché il renderer non dipendeva più dal winding del poligono.

L’unione arrotondava le estremità, quindi abbiamo ripristinato le estremità quadrate ritagliando la copertura con due semipiani terminali. Quel ritaglio sistemava le linee rette a pressione costante, ma introduceva un difetto più grave: il corpo di un tratto curvo può attraversare legittimamente un semipiano terminale, e il ritaglio dell’estremità rimuoveva allora una parte del centro del tratto. Durante l’input dal vivo la direzione dell’estremità continuava a cambiare, così l’area ritagliata si spostava e il tratto sfarfallava.

Un tratto blu curvo prima della correzione del ritaglio dell’estremità, con la parte superiore del corpo tagliata da una linea diagonale; i punti verdi mostrano l’intero percorso campionato.

Il vincolo globale dell’estremità attraversa il corpo di un tratto curvo.

Lo stesso tratto blu curvo dopo l’aggiunta di un gate locale sulla lunghezza d’arco, con entrambi i rami ripristinati intorno al percorso campionato verde.

Limitare il ritaglio dell’estremità ai primitivi entro una semilarghezza dall’estremità ripristina questo tratto.

Un gate sulla lunghezza d’arco ha reso locale il ritaglio dell’estremità e ha corretto ogni caso registrato. Un uncino sintetico estremo mostrava ancora una piccola tacca bianca nel punto in cui una punta a bassa pressione incontrava l’estremità ritagliata. Anche il terzo motore aveva quindi scambiato un difetto geometrico con un altro.

MaLiang elimina il confine rigido

MaLiang costruisce un tratto con timbri di texture ripetuti invece che con un unico contorno chiuso. Il suo generatore di percorsi usa segmenti di Bézier quadratici che passano per i punti medi di campioni adiacenti, mentre il renderer delle linee posiziona i timbri a intervalli regolari lungo quel percorso. Dimensione, rotazione, colore e opacità del timbro possono variare lungo il tratto.

Un timbro morbido non ha un’estremità, un raccordo o una regola di winding separati. Un cambio brusco di direzione resta una sequenza di segni sovrapposti. I ripassamenti non possono creare una tasca poligonale vuota e un semipiano terminale non può tagliare il centro di un tratto, perché il modello non contiene alcun ritaglio terminale.

Abbiamo adottato il modello, non il codice. L’attuale HighlighterStrokeBuilder esegue quattro operazioni:

  1. Rimuove solo i campioni terminali con pressione inferiore a 0.005.
  2. Leviga le posizioni con segmenti di Bézier quadratici sui punti medi.
  3. Posiziona un timbro circolare per ogni punto di lunghezza d’arco.
  4. Imposta il diametro del timbro a partire dalla pressione normalizzata e compone i timbri con alpha source-over.

Il builder è passato da 686 righe a 190. Azimut, altitudine, costruzione del contorno, maschere di unione, finestre direzionali delle estremità, estensione dell’estremità e ritaglio dell’estremità sono tutti usciti dal percorso dell’evidenziatore. Continuiamo a registrare l’orientamento della Pencil perché il modello di input lo supporta, ma l’evidenziatore non legge più quei campi.

La formula della pressione di MaLiang richiedeva la calibrazione del dispositivo

MaLiang dimensiona un timbro con una formula equivalente a

che presuppone che force usi una parte utile dell’intervallo da 0 a 1. I dati del nostro dispositivo non lo facevano. Su circa 3,400 campioni di Apple Pencil, la pressione mediana era 0.047 e il valore massimo 0.178. Inserire direttamente questi valori nella formula rendeva la scrittura normale molto più sottile della dimensione nominale del pennello.

Abbiamo quindi normalizzato intorno alla mediana osservata, applicato l’esponente e limitato il risultato:

La pressione mediana della scrittura ora corrisponde al diametro nominale di 12 punti. Un contatto molto leggero resta visibile al 35% della dimensione nominale, mentre una pressione forte smette di aumentare al 120%.

Questa calibrazione riguarda il dispositivo di input e il comportamento del pennello, non MaLiang. Copiare la formula originale senza misurare la nostra distribuzione della pressione avrebbe conservato l’architettura, ma prodotto il pennello sbagliato.

L’alpha per timbro deriva dal numero noto di sovrapposizioni

MaLiang compensa i timbri traslucidi sovrapposti con l’espressione empirica alpha ÷ overlapping × 2.5. Il nostro pennello ha un diametro nominale fisso di 12 punti e un passo dei timbri di 1 punto, quindi la sua linea centrale riceve circa 12 livelli. Nel nostro caso il numero di sovrapposizioni è noto e la compensazione può essere risolta invece che stimata.

Per un’opacità target del tratto e livelli source-over uguali, un’opacità del timbro produce

quindi un timbro richiede

Con e , ogni timbro usa un’opacità di circa . Il centro raggiunge il colore desiderato dopo composizioni ripetute, mentre il bordo riceve meno timbri e mantiene una transizione sfumata di uno o due punti.

Un foglio di tratti blu di evidenziatore variati, renderizzati dal modello finale a timbri morbidi, inclusi anelli, uncini, segni brevi e intersezioni con sé stessi.

Il renderer finale riproduce i percorsi registrati dal dispositivo. Uncini, tagli diagonali e tasche bianche sono assenti.

I due costi che abbiamo accettato

Le intersezioni con sé stessi diventano più scure. La composizione source-over conta ogni timbro, compresi quelli depositati da un passaggio precedente nella stessa area, mentre il motore di unione della copertura lo evitava. Abbiamo accettato la sovrapposizione più scura perché è prevedibile e mantiene intatta la forma del tratto.

Le estremità si restringono con la pressione. Un sollevamento reale riduce la pressione, che riduce il diametro del timbro, così il tratto si assottiglia invece di terminare con un’estremità quadrata a larghezza piena. Avevamo trattato quell’estremità quadrata come un requisito in tutti e tre i motori geometrici. Abbandonarla ha rimosso il ritaglio e la logica delle estremità che stavano dietro a diversi difetti descritti sopra, quindi abbiamo accettato l’estremità rastremata.

Il modello che distribuiamo fa quindi meno dei precedenti. Conserva l’evento di scrittura misurato, risponde alla pressione dopo la calibrazione del dispositivo e renderizza percorsi curvi e auto-intersecanti senza richiedere un contorno globalmente valido. Un pennino a scalpello, un’estremità perfettamente quadrata e la polilinea esatta tra i campioni grezzi sono le caratteristiche a cui abbiamo rinunciato per ottenere questo risultato.

Il limite di pressione è l’elemento più direttamente trasferibile ad altri progetti. Il valore 0.005 proviene dal divario tra due gruppi nell’input reale: scrittura a 0.01 e oltre, tracciamento in aria a 0.002 e sotto. Gli stessi tratti registrati hanno mostrato dove falliva ogni correzione geometrica. Quei fallimenti ci hanno portato al modello a timbri e ai due costi che comporta.