Как Kimi K3 самостоятельно создала цифровой штрих перьевой ручки
Мы хотели добавить в Lulucat Notes направленную перьевую ручку из Freeform. Техническим заданием был скриншот почерка. Две модели, один разграничивающий вопрос и эллиптическое перо спустя — водяная ручка пишет так, как должна. Исследование, код и валидацию выполнила Kimi K3 на Fireworks.

Результат работы конвейера Metal из Lulucat Notes, которому посвящён этот пост.
В Lulucat Notes есть простая ручка и маркер. Следующим инструментом в плане была водяная ручка — направленная перьевая ручка, знакомая по Apple Notes и Freeform: вертикальные штрихи у неё получаются толстыми, а горизонтальные — тонкими. Техническим заданием для этого инструмента был не документ. Это был скриншот: три строки рукописного текста, слова “pencilkit”, “无边记” и “这种有方向的水笔能力” — эта направленная способность водяной ручки.
Как и в случае с предыдущим конвейером Metal, работу выполнила Kimi K3, открытая модель Moonshot, работающая на Fireworks: измерения, моделирование, код и валидация. Человек предоставил скриншот, ответил на один вопрос и оценил ощущения на настоящем iPad.
Техническое задание — это скриншот
Статичное изображение не объясняет, почему штрих тонкий. Оно может показать лишь, насколько он тонкий и где именно. Поэтому первым шагом стали измерения. Мы просканировали скриншот строка за строкой и столбец за столбцом, отслеживая смещение центральной линии каждого штриха, чтобы определить его направление, а затем перевели ширину сканирования в истинную ширину через синус этого угла:
| Штрих | Направление | Истинная ширина |
|---|---|---|
| Восходящий штрих «l», «pencilkit» | ≈ 78° | 27.3 px |
| Штамб «k», «pencilkit» | ≈ 90° | 28 px |
| Левый нисходящий взмах в 力 | ≈ 66° | 25.7 px |
| Соединения курсивного письма | ≈ 8° | 11 px |
| Китайские горизонтали (横) | ≈ 0° | 6–11 px |
Отношение толстого штриха к тонкому — ≈ 2.5. Подгонка
Версия один: ширина из направления
Первая модель была очевидной. Для каждой входной точки вычислить направление, сопоставить его с шириной через подобранную кривую и в момент захвата вшить результат в радиус точки. Направление оценивалось причинно — как экспоненциально затухающее взвешенное среднее по нескольким последним точкам дуги, вычисленное в пространстве удвоенного угла, чтобы разворот движения не обнулял оценку. Используются только прошлые точки, поэтому живой и зафиксированный штрихи совпадают байт в байт.
Builder прошёл модульную проверку: синтетические горизонтальные, вертикальные, траектории под 45° и траектории с разворотом все получили теоретические ширины — 0.99, 2.52, 2.00 и 0.99 пункта. Для отрисовки вообще не понадобился новый код: штрих с заранее записанным радиусом — это просто цепочка круглых штампов, а наш конвейер point-sprite уже их рисовал.
На iPad человек отклонил модель примерно за десять секунд: «это художественная ручка, а не водяная ручка».
Две гипотезы описывают один снимок
Почему результат ощущался неправильным? Было два возможных объяснения, а скриншот не позволял их различить:
- Фиксация направления. Геометрия пера заставляет горизонтали быть тонкими, а вертикали — толстыми, что бы ни делала рука. Именно это реализовала версия один.
- Давление и скорость. Толщина ручки зависит от давления, а рисунок образца — всего лишь динамика почерка: на нисходящих штрихах естественно нажимают сильнее, а соединения естественно быстрые и лёгкие.
Оба объяснения дают снимок с тонкими горизонталями и толстыми вертикалями. Разница проявляется, когда вы сильно нажимаете на горизонтальный штрих. Версия один оставляет его тонким. Ручка, реагирующая на давление, делает его толстым. Поэтому мы задали человеку один вопрос: должен ли горизонтальный штрих, на который сильно надавили, стать толще?
«Нет. Горизонтали остаются тонкими.»
Фиксация направления подтвердилась. Но что-то ещё было не так, ведь версия один тоже фиксировала направление.
Ответ был в концах штрихов
Следующая подсказка нашлась на верхушках восходящих штрихов. Увеличение штамбов «l» и «k» на образце показало две вещи: прямой штрих сохраняет одну ширину от начала до конца, а его концы — плоские диагональные срезы, форма перьевого зубила, отрывающегося от бумаги. Не круглые точки. Не сужения от давления.

Именно это и означало «ощущение художественной ручки». Версия один моделировала вид образца — ширину как функцию оценённого направления, — но не ручку. Оценщик направления — это сенсор: на зашумлённом входе он дрожит, на поворотах запаздывает и превращает конец каждого штриха в окружность. У настоящего пера нет ни одной из этих проблем, потому что оно ничего не вычисляет. Ширина — это геометрия.
Версия два: эллиптическое перо
В итоговой модели вообще нет оценщика направления. Перо — это ориентированный эллипс: длинная ось горизонтальна, короткая зафиксирована. Штампы этого эллипса плотно размещаются вдоль траектории штриха, а всё остальное следует из геометрии:
-
Горизонтальный штрих идёт вдоль открытого края, поэтому всегда получается шириной
— постоянная тонкая линия при любом давлении. Именно это и требовала подтверждённая спецификация. -
Вертикальный штрих пересекает всю длинную ось:
, толстый конец. -
Диагональный штрих получает ширину хорды эллипса перпендикулярно направлению движения,
-
Концы штрихов — эллиптические срезы: плоские концы в форме пера из образца, без дополнительной работы.
-
Давление масштабирует только длинную ось,
, поэтому добавляет объём чернил на нисходящих штрихах и никогда не утолщает горизонталь.
По измерениям образца мы оставили
Для отрисовки понадобился один новый fragment shader и больше ничего. Формат вершины — позиция, диаметр, цвет — уже содержал всё необходимое: диаметр — это длинная ось, а короткая ось задаётся uniform на один проход. Шейдер вычисляет SDF эллипса с той же рампой покрытия в полпикселя, что и наши круглые штампы, поэтому его можно сравнивать пиксель в пиксель с эталонной реализацией Core Graphics (fillEllipse per stamp). Валидация прошла через обычные ворота: синтетический корпус с двумя штрихами перьевой ручки, отрисованными обоими способами и сравнёнными попиксельно, — ноль структурных различий, а проверенный точечно горизонтальный штрих имел 12 px в обоих рендерерах.

Версия один слева, версия два справа. Один и тот же рукописный ввод, один и тот же конвейер. Концы рассказывают историю.
На iPad новая ручка прошла проверку сразу: «好,很好» — хорошо. Очень хорошо.
Сохранить промах
Версия один не отправилась в корзину. Это интересная кисть — просто не водяная ручка. Поэтому она стала первой записью в новом меню экспериментальных кистей под названием, которое дал ей человек: 漏水的圆珠笔, подтекающая шариковая ручка. На панели инструментов появилась кнопка-колба, открывающая текстовый список экспериментов; чтобы добавить следующий, достаточно одной строки в реестре. Отвергнутая модель — не напрасная работа, если её можно выпустить как ясно обозначенный эксперимент.
Один вечер циклов
Весь путь — измерить, смоделировать, собрать, проверить на устройстве, задать вопрос, перемоделировать, пересобрать, проверить — занял один вечер. Kimi K3 на Fireworks выполнила всю техническую часть этого цикла: придумала измерительные сканы, предложила разграничивающий вопрос вместо второй догадки, удалила собственный оценщик направления, когда факты изменились, и расширила pixel-diff harness до того, как коснулась приложения. Скорость инференса Fireworks сохранила интерактивность цикла — длинные диффы Metal и Swift, скрипты анализа пикселей и инструменты для корпуса появлялись достаточно быстро, чтобы узким местом оставалось то, чем оно и должно быть: человеческое суждение.
Схема сотрудничества была той же, что и в посте о конвейере Metal, и снова сработала: модель быстрая и точная; человек отвечает за вкус и сквозную проверку. Одной фразы обратной связи об ощущении — «художественная ручка, а не водяная ручка» — хватило, чтобы модель нашла точную ошибку моделирования и заменила её более простой конструкцией.
Правильная модель оказалась меньше неправильной. В версии два было меньше подвижных частей, чем в версии один: ни оценщика, ни окна сглаживания, ни подобранного показателя степени. Перо не вычисляет ширину. Перо и есть ширина.