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 ecrã de escrita à mão. Dois modelos, uma pergunta discriminante e uma pena elíptica depois, a caneta de água escreve como deve ser. A investigação, o código e a validação foram feitos pelo Kimi K3 na Fireworks.

Atualizado

Cursiva inglesa manuscrita com as palavras "fountain pen" por cima 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 artigo trata.

O Lulucat Notes tem uma caneta simples e um marcador. A ferramenta seguinte no planeamento era uma caneta de água — a caneta-tinteiro direcional que se conhece do Apple Notes e do Freeform, em que os traços verticais saem grossos e os horizontais saem finos. A especificação desta ferramenta não era um documento. Era uma captura de ecrã: três linhas de escrita à mão, as palavras “pencilkit”, “无边记” e “这种有方向的水笔能力” — esta capacidade direcional de caneta de água.

Tal como no pipeline Metal anterior, o trabalho foi feito pelo Kimi K3, o modelo aberto da Moonshot a correr na Fireworks: as medições, a modelação, o código e a validação. O humano forneceu a captura de ecrã, respondeu a uma pergunta e avaliou a sensação num iPad real.

A especificação é uma captura de ecrã

Uma imagem estática não consegue dizer por que razão um traço é fino. Só consegue dizer quão fino e onde. Por isso, o primeiro passo foi medir. Analisámos a captura de ecrã linha a linha e coluna a coluna, seguindo o desvio da linha central de cada traço para obter a sua direção; depois convertemos a largura da varredura em largura real usando o seno desse ângulo:

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

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

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

O primeiro modelo era o óbvio. Calcular uma direção para cada ponto de entrada, mapear a direção para uma largura através da curva ajustada e incorporar o resultado no 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 duplo para que uma inversão do movimento não anulasse a estimativa. Só são usados pontos anteriores, por isso o traço ao vivo e o traço consolidado coincidem byte a byte.

O builder passou a verificação unitária: trajetórias sintéticas horizontais, verticais, a 45° e de inversão incorporaram todas 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 o nosso pipeline de point sprites já os desenhava.

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

Duas histórias cabem numa imagem

Porque é que parecia errado? Havia duas explicações candidatas, e a captura de ecrã não as conseguia separar:

  • Bloqueio de direção. A geometria da pena obriga 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 é controlada pela pressão, e o padrão da amostra é apenas a dinâmica da escrita à mão: as passadas descendentes recebem naturalmente mais pressão, e os conectores são naturalmente rápidos e leves.

Ambas produzem uma captura de ecrã com horizontais finas e verticais grossas. A diferença está no que acontece quando se pressiona com força um traço horizontal. A versão um mantém-no fino. Uma caneta controlada pela pressão torna-o grosso. Por isso fizemos uma pergunta ao humano: um traço horizontal pressionado com força deve ficar mais grosso?

“Não. As horizontais ficam finas.”

O bloqueio de direção ficou confirmado. Mas continuava a haver algo errado, porque a versão um também estava bloqueada pela direção.

A resposta estava nas pontas dos traços

A pista seguinte estava no topo das hastes ascendentes. Ao ampliar as hastes do “l” e do “k” na amostra, vimos duas coisas: um traço direito mantém a mesma largura do princípio ao fim, e as pontas dos traços são cortes diagonais planos — o formato de uma pena de talhe a levantar-se do papel. Não são pontos redondos. Não são afunilamentos causados pela pressão.

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

Era isso que a “sensação de caneta artística” significava. A versão um modelou o aspeto da amostra — a largura como função da direção estimada — mas não a caneta. Um estimador de direção é um sensor: treme com uma entrada ruidosa, atrasa-se nas curvas e arredonda cada ponta de traço até formar um círculo. Uma pena verdadeira não tem nenhum destes problemas, porque não calcula nada. A largura é geometria.

Versão dois: uma pena elíptica

O modelo final não tem qualquer estimador de direção. A pena é uma elipse orientada: eixo maior horizontal, eixo menor fixo. Os carimbos desta elipse são colocados densamente ao longo do caminho do traço, e tudo o resto resulta da geometria:

  • Um traço horizontal percorre a borda aberta, por isso tem sempre de largura — uma linha fina constante, com qualquer pressão. É exatamente a especificação confirmada.

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

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

  • As pontas dos traços são cortes elípticos — as pontas planas com o formato da pena da amostra, sem trabalho adicional.

  • A pressão escala apenas o eixo maior, , por isso aumenta o volume de tinta nas passadas descendentes e nunca pode engrossar uma horizontal.

A partir das medições da amostra, mantivemos pt e pt, a mesma proporção de 2.5. Os carimbos ficam a 0.5 pontos de distância ao longo do arco; com um raio curto de 0.55 pontos, a pior ondulação da borda dá cerca de um décimo de pixel.

A renderização precisou de um novo fragment shader e de mais nada. O formato do vértice — posição, diâmetro, cor — já transportava 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, o que o mantém comparável ao nível do 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: a da esquerda desenhada com carimbos redondos de diâmetro escalado pela direção, mostrando pontas arredondadas; a da direita com carimbos de elipse, mostrando pontas planas cortadas pela pena e uma largura mais estável.

A versão um está à esquerda e 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 legitimamente interessante — simplesmente não é uma caneta de á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 verte. A barra de ferramentas ganhou um botão em forma de frasco que abre uma lista textual de experiências; acrescentar a seguinte custa uma linha num registo. Um modelo rejeitado não é trabalho desperdiçado quando pode ser lançado como uma experiência claramente identificada.

Uma noite de ciclos

O percurso completo — medir, modelar, construir, testar no dispositivo, perguntar, remodelar, reconstruir, validar — demorou uma noite. O Kimi K3 na Fireworks tratou de todo o lado técnico do ciclo: concebeu as varreduras de medição, propôs a pergunta discriminante em vez de adivinhar uma segunda vez, apagou o seu próprio estimador de direção quando as evidências mudaram e ampliou o pixel-diff harness antes de tocar na aplicação. A velocidade de inferência da Fireworks manteve o ciclo interativo — diffs longos de Metal e Swift, scripts de análise de píxeis e ferramentas de corpus chegavam suficientemente depressa para que o gargalo continuasse onde devia: no juízo humano.

O padrão de colaboração foi o mesmo do artigo sobre o pipeline Metal e voltou a funcionar: o modelo é rápido e preciso; o humano fica 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 de água” — bastou para o modelo localizar o erro exato de modelação e substituí-lo por um design mais simples.

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