Lulucat

Por que substituímos três mecanismos de geometria de marca-texto por carimbos

Gaoge ZhangGaoge Zhang

Dados do Apple Pencil revelaram defeitos que testes sintéticos não detectaram e fizeram o Lulucat Notes trocar três contornos por carimbos MaLiang.

Atualizado

O Lulucat Notes começou com um renderizador convencional de marca-texto: construir um contorno geométrico ao redor do caminho amostrado e depois preencher esse contorno uma vez com uma cor translúcida. Um único preenchimento evita as faixas mais escuras que aparecem onde segmentos translúcidos se sobrepõem. Ele também coloca cada junção, auto-interseção, extremidade e traço curto dentro do mesmo problema geométrico.

Três gerações de geometria personalizada removeram uma classe de defeito e expuseram outra. O renderizador que distribuímos agora usa carimbos circulares suaves baseados no MaLiang, um framework de desenho de código aberto sob a licença MIT, desenvolvido por Harley-xk. Em troca de remover toda uma categoria de falhas de contorno, aceitamos auto-interseções mais escuras e extremidades afuniladas pela pressão.

Escrita com marca-texto azul capturada em um iPad, com ganchos finos visíveis no início e no fim de vários traços.

A captura de tela original do dispositivo com zoom de 228%. Pequenos ganchos aparecem nas duas extremidades de vários traços.

A escrita manual real produziu ganchos que as cenas sintéticas não produziram

Nossas cenas sintéticas cobriam linhas retas, arcos, toques curtos, retornos, mudanças de pressão e viradas bruscas, e todas renderizavam corretamente. A escrita manual chinesa real ainda produzia ganchos de cerca de 1–3 pontos no início e no fim de muitos traços.

Por isso, adicionamos um botão de salvar ao app e registramos 29 traços completos. Cada amostra preservou sua posição, raio, pressão, azimute, altitude, marca temporal e sinalizadores de estimativa do UIKit. Um programa offline decodificou esse JSON e compilou os mesmos arquivos Stroke.swift, StrokeRenderer.swift e HighlighterStrokeBuilder.swift usados pelo app. Sua saída correspondeu à captura de tela do dispositivo traço a traço, incluindo os ganchos.

Como o renderizador offline reproduziu o defeito apenas a partir da entrada registrada, a causa estava nas amostras e na geometria, não no comportamento da tela. Agora podíamos ler as amostras exatas que haviam entrado no renderizador.

Nossa primeira hipótese culpou a orientação estimada do Pencil no toque inicial e no levantamento. Os dados a rejeitaram. Nos 29 traços, o azimute mudou no máximo 0.09 radianos e a altitude no máximo 0.02 radianos dentro das oito primeiras e últimas amostras, e todos os sinalizadores de estimativa eram falsos naquele iPad. A orientação permaneceu estável enquanto a pressão e a direção mudaram bruscamente.

Um piso de pressão veio de um intervalo limpo nos dados

A última amostra de cada traço registrado tinha pressão 0.000. As amostras de escrita ficavam em ou acima de 0.01, enquanto as amostras registradas depois que o Pencil havia deixado a superfície ficavam em ou abaixo de 0.002. Essas amostras no ar muitas vezes continuavam por 2–7 pontos e percorriam 0.8–9.0 pontos, e sua direção podia virar cerca de 90 graus para longe do traço escrito.

O traço 2 mostra o mecanismo. Seu corpo terminou entre 143 e 180 graus. A cauda de baixa pressão então virou para −90 a −67 graus e percorreu mais 8.5 pontos. O renderizador de contorno tratou o último ponto no ar como o ponto final de referência, então esticou uma faixa estreita de geometria até esse ponto.

As pressões medidas deixam quase uma ordem de magnitude entre o movimento de escrita e o movimento no ar, então colocamos o corte dentro desse intervalo:

private static let pressureFloor: CGFloat = 0.005

Somente amostras de baixa pressão nas duas extremidades são removidas. Uma amostra de baixa pressão no meio de um traço continua fazendo parte dele. A regra segue a fronteira medida entre os eventos e deixa o caminho escrito sem alterações.

Close-up instrumentado antes do corte: as amostras de entrada verdes se afastam do traço enquanto o contorno vermelho forma um gancho estreito.

Antes do corte, o renderizador ancora a extremidade nas amostras no ar. Os pontos verdes são amostras brutas; a linha vermelha é o contorno gerado.

Close-up instrumentado depois do corte: o preenchimento azul termina nas últimas amostras escritas, e os pontos verdes de baixa pressão permanecem fora dele.

Depois de cortar as amostras abaixo de 0.005, a extremidade termina no último ponto escrito. Esse piso de pressão permanece no renderizador atual baseado em carimbos.

O piso eliminou os ganchos longos no ar. Arrastos curtos de toque inicial e arcos de levantamento cuja pressão permanecia acima de 0.005 sobreviveram, porque são movimento escrito. Tratar esses movimentos dentro de um modelo de contorno levou às rodadas seguintes de geometria.

Três mecanismos de geometria deslocaram os defeitos

Geração um: um contorno, um preenchimento

O primeiro mecanismo sensível à pressão seguia um pipeline conhecido. Ele suavizava uma linha central, calculava deslocamentos à esquerda e à direita, unia-os em um único polígono e preenchia esse polígono uma vez. O bico era uma elipse orientada pelo azimute e pela altitude do Apple Pencil, então sua seção transversal mudava com a direção da escrita. Toques curtos tinham uma forma separada, e extremidades planas usavam pequenas assimetrias para evitar bolsões de winding nulo.

O preenchimento único resolvia o acúmulo de opacidade dentro de um traço e exigia um polígono válido em cada virada. Arrastos de toque inicial criavam cunhas entre a extremidade e o corpo. Retornos podiam criar crescentes brancos. Viradas bruscas podiam fazer o contorno interno se auto-intersectar e deixar um entalhe sem preenchimento.

Geração dois: reparar o contorno perto das extremidades

O segundo mecanismo manteve o contorno e adicionou reparos locais: corte de retornos, amortecimento para propriedades estimadas no início, endireitamento da extremidade, corte da direção transitória e uma pequena extensão da extremidade. Cada patch tratava de uma falha reproduzida. Juntos, eles fizeram o comportamento das extremidades depender de vários limites e janelas.

A escrita real então produziu casos fora dessas janelas. Uma janela fixa de endireitamento de 24 pontos apagava ganchos intencionais em traços curtos. Uma janela adaptativa preservava esses ganchos, e os arcos de levantamento ainda podiam se tornar cantos visíveis. O corte orientado pela direção removia mais movimento transitório, ao custo de descartar cerca de 2.5 pontos de um início genuinamente curvo em um traço registrado.

Cada patch corrigia o caso ao qual se destinava. O modelo subjacente ainda exigia um contorno rígido que permanecesse válido com uma entrada ruidosa e curva.

Geração três: união de cobertura com extremidades quadradas recortadas

O terceiro mecanismo parou de construir um contorno único. Ele desenhava um trapézio para cada segmento e um disco em cada amostra interior em uma máscara em escala de cinza, calculava a união das coberturas e então aplicava a cor do marca-texto uma vez. Crescentes de retorno, escurecimento de auto-interseções e buracos de winding nulo desapareceram, porque o renderizador já não dependia do winding de polígonos.

A união arredondava as extremidades, e restauramos extremidades quadradas recortando a cobertura contra dois semiplanos de extremidade. Esse recorte corrigia linhas retas com pressão constante e introduzia um defeito mais sério: o corpo de um traço curvo pode legitimamente atravessar um semiplano de extremidade, e o recorte da extremidade então removia parte do meio do traço. Durante a entrada ao vivo, a direção da extremidade continuava mudando, então a área recortada se movia e o traço tremulava.

Um traço curvo azul antes da correção do recorte da extremidade, com seu corpo superior cortado ao longo de uma linha diagonal; pontos verdes mostram o caminho completo amostrado.

A restrição global da extremidade corta o corpo de um traço curvo.

O mesmo traço curvo azul depois da adição de um limite local de comprimento de arco, com os dois ramos restaurados ao redor do caminho amostrado em verde.

Restringir o recorte da extremidade às primitivas dentro de uma meia largura da extremidade restaura este traço.

Um limite de comprimento de arco tornou o recorte da extremidade local e corrigiu todos os casos registrados. Um gancho sintético extremo ainda mostrou um pequeno entalhe branco onde uma ponta de baixa pressão encontrava a extremidade recortada. O terceiro mecanismo havia novamente trocado uma falha de geometria por outra.

MaLiang remove a fronteira rígida

O MaLiang constrói um traço a partir de carimbos de textura repetidos, em vez de um único contorno fechado. Seu gerador de caminhos usa segmentos quadráticos de Bézier que passam pelos pontos médios de amostras adjacentes, e seu renderizador de linhas posiciona carimbos em um passo regular ao longo desse caminho. O tamanho, a rotação, a cor e a opacidade dos carimbos podem variar ao longo do traço.

Um carimbo suave não tem uma extremidade, junção ou regra de winding separada. Uma mudança brusca de direção continua sendo uma sequência de marcas sobrepostas. Retornos não podem criar um bolsão vazio de polígono, e um semiplano de extremidade não pode cortar o meio de um traço, porque o modelo não contém um recorte de extremidade.

Adotamos o modelo, não o código. O HighlighterStrokeBuilder atual executa quatro operações:

  1. Remover somente as amostras das extremidades cuja pressão seja inferior a 0.005.
  2. Suavizar as posições com segmentos quadráticos de Bézier nos pontos médios.
  3. Colocar um carimbo circular por ponto ao longo do comprimento de arco.
  4. Definir o diâmetro do carimbo a partir da pressão normalizada e compor os carimbos com alfa source-over.

O builder caiu de 686 linhas para 190. Azimute, altitude, construção de contornos, máscaras de união, janelas de direção das extremidades, extensão das extremidades e recorte das extremidades saíram do caminho do marca-texto. Ainda registramos a orientação do Pencil, porque o modelo de entrada dá suporte a ela, e o próprio marca-texto não lê mais esses campos.

A fórmula de pressão do MaLiang precisava de calibração do dispositivo

O MaLiang dimensiona um carimbo com uma fórmula equivalente a

que pressupõe que force use uma parte útil do intervalo de 0 a 1. Os dados do nosso dispositivo não usavam. Entre cerca de 3.400 amostras do Apple Pencil, a pressão mediana era 0.047 e a máxima era 0.178. Inserir esses valores diretamente na fórmula fazia a escrita normal ficar muito mais fina que o tamanho nominal do pincel.

Por isso, normalizamos em torno da mediana observada, aplicamos o expoente e limitamos o resultado:

A pressão mediana da escrita agora corresponde ao diâmetro nominal de 12 pontos. Um contato muito leve permanece visível a 35% do tamanho nominal, enquanto uma pressão forte para de crescer em 120%.

Essa calibração pertence ao dispositivo de entrada e ao comportamento do pincel, não ao MaLiang. Copiar a fórmula original sem medir nossa própria distribuição de pressão teria preservado a arquitetura e produzido o pincel errado.

O alfa por carimbo veio da contagem conhecida de sobreposições

O MaLiang compensa carimbos translúcidos sobrepostos com a expressão empírica alpha ÷ overlapping × 2.5. Nosso pincel tem um diâmetro nominal fixo de 12 pontos e um passo de carimbo de 1 ponto, então sua linha central recebe cerca de 12 camadas. No nosso caso, a contagem de sobreposições é conhecida, e a compensação pode ser resolvida em vez de estimada.

Para uma opacidade alvo do traço e camadas iguais de source-over, uma opacidade de carimbo produz

portanto, um carimbo requer

Com e , cada carimbo usa uma opacidade de cerca de . O centro atinge a cor pretendida depois da composição repetida, enquanto a borda recebe menos carimbos e mantém uma transição esfumada de um ou dois pontos.

Uma folha com vários traços azuis de marca-texto renderizados pelo modelo final de carimbos suaves, incluindo laços, ganchos, marcas curtas e auto-interseções.

O renderizador final reproduzindo os caminhos registrados no dispositivo. Ganchos, cortes diagonais e bolsões brancos estão ausentes.

Os dois custos que aceitamos

As auto-interseções ficam mais escuras. A composição source-over conta cada carimbo, incluindo os carimbos depositados por uma passagem anterior pela mesma área, e o mecanismo de união de cobertura havia evitado isso. Aceitamos a sobreposição mais escura porque ela é previsível e mantém a forma do traço intacta.

As extremidades afinam com a pressão. Um levantamento real reduz a pressão, o que reduz o diâmetro do carimbo, então o traço afunila em vez de terminar com uma extremidade quadrada de largura total. Havíamos tratado essa extremidade quadrada como um requisito nos três mecanismos de geometria. Abandoná-la removeu o recorte e a maquinaria das extremidades por trás de várias falhas acima, então aceitamos a extremidade afunilada.

O modelo que distribuímos, portanto, faz menos que os anteriores. Ele preserva o evento de escrita medido, responde à pressão depois da calibração do dispositivo e renderiza caminhos curvos e que se cruzam sem exigir um contorno globalmente válido. Um bico em forma de cinzel, uma extremidade perfeitamente quadrada e a polilinha exata entre as amostras brutas são os recursos dos quais abrimos mão por isso.

O piso de pressão é o que se transfere mais diretamente para outros projetos. O valor 0.005 veio do intervalo entre dois agrupamentos na entrada real: escrita em 0.01 ou acima, rastreamento no ar em 0.002 ou abaixo. Os mesmos traços registrados mostraram onde cada reparo geométrico falhou. Essas falhas levaram ao modelo de carimbos e aos dois custos que ele traz.