Como o Kimi K3 construiu sozinho um traço digital de caneta-tinteiro
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.

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ço | Direção | Largura 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
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.

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
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.

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.