Lulucat

Como o Kimi K3 construiu sozinho um traço digital de caneta-tinteiro

Gaoge ZhangGaoge Zhang

Queríamos a caneta-tinteiro direcional do Freeform no Lulucat Notes. A especificação era uma captura de tela de uma caligrafia. Dois modelos, uma pergunta discriminante e uma pena elíptica depois, a caneta d'água escreve como deveria. A pesquisa, o código e a validação foram feitos pelo Kimi K3 no Fireworks.

Atualizado

Cursiva inglesa manuscrita com as palavras "fountain pen" acima de uma linha de caligrafia chinesa, desenhada com uma caneta-tinteiro de pena elíptica em papel creme: grossa nas verticais, fina nas horizontais.

Renderizado pelo pipeline Metal do Lulucat Notes de que este post trata.

O Lulucat Notes tem uma caneta simples e um marca-texto. A próxima ferramenta do roadmap era uma caneta d’água — a caneta-tinteiro direcional que você conhece do Apple Notes e do Freeform, em que os traços verticais saem grossos e os horizontais saem finos. A especificação dessa ferramenta não era um documento. Era uma captura de tela: três linhas de caligrafia, as palavras “pencilkit”, “无边记” e “这种有方向的水笔能力” — essa capacidade direcional de caneta d’água.

Assim como no pipeline Metal anterior, o trabalho foi feito pelo Kimi K3, o modelo aberto da Moonshot rodando no Fireworks: as medições, a modelagem, o código e a validação. O humano forneceu a captura de tela, respondeu a uma pergunta e avaliou a sensação em um iPad de verdade.

A especificação é uma captura de tela

Uma imagem estática não consegue dizer por que um traço é fino. Ela só consegue dizer quão fino e onde. Por isso, o primeiro passo foi medir. Escaneamos a captura de tela linha por linha e coluna por coluna, acompanhando o desvio da linha central de cada traço para obter sua direção; depois convertemos a largura do escaneamento em largura real usando o seno daquele ângulo:

TraçoDireçãoLargura real
Ascendente do “l”, “pencilkit”≈ 78°27.3 px
Haste do “k”, “pencilkit”≈ 90°28 px
Varredura descendente para a esquerda de 力≈ 66°25.7 px
Conectores cursivos≈ 8°11 px
Horizontais chinesas (横)≈ 0°6–11 px

A razão entre grosso e fino é ≈ 2.5. Ajustar ao ponto de 66° dá — quase linear em , com pico no eixo vertical. Na prática, uma pena itálica: uma fenda horizontal que a escrita horizontal nunca abre.

Versão um: largura a partir da direção

O primeiro modelo era o óbvio. Calcule uma direção para cada ponto de entrada, mapeie a direção para uma largura pela curva ajustada e incorpore o resultado ao raio do ponto no momento da captura. A direção era estimada causalmente — uma média ponderada com decaimento exponencial sobre os últimos pontos do arco, calculada no espaço de ângulo dobrado para que uma inversão do trajeto não anulasse a estimativa. Só pontos passados são usados, então o traço ao vivo e o traço consolidado concordam byte a byte.

O builder passou na verificação unitária: trajetórias sintéticas horizontais, verticais, a 45° e de reversão incorporaram as larguras teóricas — 0.99, 2.52, 2.00 e 0.99 pontos. A renderização não precisou de código novo: um traço com raio incorporado é apenas uma cadeia de carimbos redondos, e nosso pipeline de point sprites já os desenhava.

No iPad, o humano levou cerca de dez segundos para rejeitá-lo: “isto é uma caneta artística, não a caneta d’água”.

Duas histórias cabem na mesma imagem

Por que parecia errado? Havia duas explicações candidatas, e a captura de tela não conseguia distingui-las:

  • Travamento de direção. A geometria da pena força as horizontais a serem finas e as verticais, grossas, independentemente do que a mão faça. Foi isso que a versão um implementou.
  • Pressão e velocidade. A caneta é comandada pela pressão, e o padrão da amostra é apenas a dinâmica da escrita à mão: as passadas para baixo naturalmente recebem mais pressão, e os conectores são naturalmente rápidos e leves.

As duas explicações produzem uma captura de tela com horizontais finas e verticais grossas. A diferença aparece quando você pressiona com força um traço horizontal. A versão um o mantém fino. Uma caneta sensível à pressão o engrossa. Então fizemos uma pergunta ao humano: um traço horizontal pressionado com força deve ficar mais grosso?

“Não. As horizontais continuam finas.”

O travamento de direção estava confirmado. Mas ainda havia algo errado, porque a versão um também era travada em direção.

A resposta estava nas pontas dos traços

A próxima pista estava no topo dos ascendentes. Ao ampliar as hastes do “l” e do “k” na amostra, vimos duas coisas: um traço reto mantém a mesma largura do começo ao fim, e as pontas dos traços são cortes diagonais planos — o formato de uma pena de ponta chata se levantando do papel. Não são pontos redondos. Não são afunilamentos de pressão.

Um recorte ampliado de duas hastes ascendentes manuscritas da amostra, cada uma terminando no topo em um corte diagonal plano, com espessura uniforme ao longo da haste.

Era isso que a “sensação de caneta artística” queria dizer. A versão um modelou a aparência da amostra — largura como função da direção estimada — mas não a caneta. Um estimador de direção é um sensor: sofre jitter com uma entrada ruidosa, atrasa nas curvas e transforma cada ponta de traço em um círculo. Uma pena de verdade não tem nenhum desses problemas, porque não calcula nada. A largura é geometria.

Versão dois: uma pena elíptica

O modelo final não tem estimador de direção. A pena é uma elipse orientada: eixo maior horizontal, eixo menor fixo. Carimbos dessa elipse são colocados densamente ao longo do caminho do traço, e todo o resto vem da geometria:

  • Um traço horizontal percorre a borda aberta, então sempre sai com largura — uma linha fina constante, sob qualquer pressão. Exatamente a especificação confirmada.

  • Um traço vertical atravessa todo o eixo maior: , a extremidade grossa.

  • Um traço diagonal usa a largura da corda da elipse perpendicular ao movimento,

  • As pontas dos traços são cortes elípticos — as pontas planas em forma de pena da amostra, sem trabalho extra.

  • A pressão escala apenas o eixo maior, , então a pressão aumenta o volume de tinta nas passadas para baixo e nunca consegue engrossar uma horizontal.

A partir das medições da amostra, mantivemos pt e pt, a mesma razão de 2.5. Os carimbos ficam separados por 0.5 ponto ao longo do arco; com um raio menor de 0.55 ponto, a pior ondulação da borda fica em cerca de um décimo de pixel.

A renderização precisou de um novo fragment shader e nada mais. O formato do vértice — posição, diâmetro, cor — já carregava tudo: o diâmetro é o eixo maior, e o eixo menor é um uniform por passagem. O shader avalia um SDF de elipse com a mesma rampa de cobertura de meio pixel dos nossos carimbos redondos, mantendo a comparação pixel a pixel com a implementação de referência do Core Graphics (fillEllipse per stamp). A validação passou pelo gate habitual: um corpus sintético com dois traços de caneta-tinteiro, renderizado das duas formas e comparado pixel a pixel — zero diferenças estruturais, e um traço horizontal verificado pontualmente mediu 12 px nos dois renderers.

Duas renderizações da mesma palavra manuscrita "fountain" lado a lado: à esquerda, desenhada com carimbos redondos de diâmetro escalado pela direção, mostrando pontas arredondadas; à direita, com carimbos de elipse, mostrando pontas planas cortadas pela pena e largura mais estável.

A versão um está à esquerda, a versão dois à direita. A mesma entrada manuscrita, o mesmo pipeline. As pontas contam a história.

No iPad, a nova caneta passou imediatamente: “好,很好” — bom. Muito bom.

Ficar com o erro

A versão um não foi para o lixo. É um pincel interessante — só não é uma caneta d’água. Por isso, foi lançada como a primeira entrada de um novo menu de pincéis experimentais, com o nome que o humano lhe deu: 漏水的圆珠笔, a esferográfica que vaza. A barra de ferramentas ganhou um botão em forma de frasco que abre uma lista de texto com os experimentos; adicionar o próximo custa uma linha em um registro. Um modelo rejeitado não é trabalho desperdiçado quando pode ser lançado como um experimento claramente identificado.

Uma noite de ciclos

O arco completo — medir, modelar, construir, testar no dispositivo, perguntar, remodelar, reconstruir, validar — levou uma noite. O Kimi K3 no Fireworks conduziu todo o lado técnico do ciclo: criou os escaneamentos de medição, propôs a pergunta discriminante em vez de adivinhar uma segunda vez, apagou o próprio estimador de direção quando as evidências mudaram e ampliou o pixel-diff harness antes de tocar no app. A velocidade de inferência do Fireworks manteve o ciclo interativo — diffs longos de Metal e Swift, scripts de análise de pixels e ferramentas de corpus chegavam rápido o bastante para que o gargalo continuasse onde deveria: no julgamento humano.

O padrão de colaboração foi o mesmo do post sobre o pipeline Metal, e funcionou de novo: o modelo é rápido e preciso; o humano é responsável pelo gosto e pela verificação de ponta a ponta. Uma frase de feedback baseada na sensação — “caneta artística, não caneta d’água” — bastou para o modelo localizar o erro exato de modelagem e substituí-lo por um design mais simples.

O modelo certo acabou sendo menor que o errado. A versão dois foi lançada com menos partes móveis que a versão um — sem estimador, sem janela de suavização, sem expoente ajustado. A pena não calcula a largura. A pena é a largura.