Lulucat

Почему мы заменили три движка геометрии маркера на штампы

Gaoge ZhangGaoge Zhang

Данные Apple Pencil выявили дефекты, которые не заметили синтетические тесты, и привели Lulucat Notes от трёх контурных движков к модели штампов MaLiang.

Обновлено

Lulucat Notes начинался с обычного рендерера текстовыделителя: построить геометрический контур вокруг дискретизированного пути, а затем один раз залить этот контур полупрозрачным цветом. Одна заливка позволяет избежать более тёмных полос, появляющихся там, где полупрозрачные сегменты перекрываются. Она также сводит каждое соединение, самопересечение, концевое завершение и короткий штрих к одной и той же задаче построения геометрии.

Три поколения собственной геометрии устраняли каждый свой класс дефектов и обнаруживали следующий. В поставляемом сейчас рендерере используются мягкие круглые штампы на основе MaLiang — фреймворка рисования с открытым исходным кодом под лицензией MIT, созданного Harley-xk. В обмен на устранение целой категории сбоев контура мы приняли более тёмные самопересечения и окончания, сужающиеся под давлением.

Синие рукописные штрихи маркера, записанные на iPad; в начале и конце нескольких штрихов видны тонкие крючки.

Исходный снимок экрана устройства при увеличении 228%. На обоих концах нескольких штрихов видны небольшие крючки.

Рукописный ввод с реального устройства создавал крючки, которых не было в синтетических сценах

Наши синтетические сцены включали прямые линии, дуги, короткие касания, обратные ходы, изменения давления и резкие повороты — все они отрисовывались без дефектов. Реальный китайский рукописный ввод всё равно создавал в начале и конце многих штрихов крючки длиной примерно 1–3 точки.

Поэтому мы добавили в приложение кнопку сохранения и записали 29 полных штрихов. Для каждого образца сохранялись его положение, радиус, давление, азимут, угол наклона, временная метка и флаги оценки UIKit. Офлайн-программа декодировала этот JSON и компилировала те же файлы Stroke.swift, StrokeRenderer.swift и HighlighterStrokeBuilder.swift, которые использует приложение. Её результат совпал со снимком экрана устройства штрих к штриху, включая крючки.

Поскольку офлайн-рендерер воспроизвёл дефект только на основе записанного ввода, причина была в образцах и геометрии, а не в поведении дисплея. Теперь мы могли прочитать точные образцы, поступившие в рендерер.

Сначала мы предположили, что виновата оценка ориентации Pencil в момент касания и отрыва от поверхности. Данные это опровергли. Во всех 29 штрихах азимут менялся не более чем на 0.09 радиана, а угол наклона — не более чем на 0.02 радиана в пределах первых и последних восьми образцов; на том iPad все флаги оценки были ложными. Ориентация оставалась стабильной, пока давление и направление менялись резко.

Порог давления обнаружился в чистом разрыве данных

Последний образец каждого записанного штриха имел давление 0.000. В образцах во время письма давление было равно или выше 0.01, а в образцах, записанных после отрыва Pencil от поверхности, — равно или ниже 0.002. Такие образцы в воздухе часто продолжались ещё 2–7 точек и проходили 0.8–9.0 точки, а их направление могло отклониться примерно на 90 градусов от написанного штриха.

Штрих 2 показывает механизм. Его основная часть заканчивалась в диапазоне от 143 до 180 градусов. Затем хвост с низким давлением поворачивал от −90 до −67 градусов и проходил ещё 8.5 точки. Рендерер контура считал последнюю точку в воздухе определяющей конечной точкой, поэтому вытягивал к ней узкую часть геометрии.

Измеренные значения давления оставляют между письмом и движением в воздухе почти порядок величины, поэтому мы разместили отсечку внутри этого разрыва:

private static let pressureFloor: CGFloat = 0.005

Удаляются только образцы с низким давлением на двух концах. Образец с низким давлением в середине штриха остаётся его частью. Это правило следует измеренной границе события и не изменяет написанный путь.

Инструментированный фрагмент до обрезки: зелёные входные образцы отклоняются от штриха, а красный контур образует узкий крючок.

До обрезки рендерер привязывает окончание к образцам, записанным в воздухе. Зелёные точки — исходные образцы; красная линия — построенный контур.

Инструментированный фрагмент после обрезки: синяя заливка заканчивается на последних написанных образцах, а зелёные точки с низким давлением остаются за её пределами.

После удаления образцов ниже 0.005 окончание заканчивается в последней написанной точке. Этот порог давления сохраняется в текущем рендерере на основе штампов.

Порог устранил длинные крючки, возникавшие в воздухе. Короткие протяжки при касании и дуги при отрыве, в которых давление оставалось выше 0.005, сохранились, поскольку это написанное движение. Обработка таких движений внутри контурной модели привела к следующим раундам работы над геометрией.

Три движка геометрии перемещали дефекты с места на место

Поколение первое: один контур, одна заливка

Первый движок, учитывавший давление, следовал знакомому конвейеру. Он сглаживал осевую линию, вычислял левое и правое смещения, соединял их в один многоугольник и один раз заливал этот многоугольник. Перо было эллипсом, управляемым азимутом и углом наклона Apple Pencil, поэтому его поперечное сечение менялось вместе с направлением письма. Для коротких касаний использовалась отдельная форма, а в плоских окончаниях применялись небольшие асимметрии, чтобы избежать карманов с нулевым обходом.

Одна заливка устраняла накопление непрозрачности внутри штриха, но требовала корректного многоугольника на каждом повороте. Протяжки при касании создавали клинья между окончанием и основной частью. Обратные ходы могли создавать белые серповидные карманы. На резких поворотах внутренний контур мог самопересекаться и оставлять незаполненную выемку.

Поколение второе: исправление контура возле концов

Второй движок сохранил контур и добавил локальные исправления: обрезку обратного хода, затухание для ранних оценочных свойств, выпрямление конечной точки, обрезку переходного направления и небольшое удлинение окончания. Каждый патч устранял воспроизводившийся сбой. Вместе они сделали поведение концов зависимым от нескольких порогов и окон.

Реальный ввод затем породил случаи за пределами этих окон. Фиксированное окно выпрямления длиной 24 точки стирало намеренные крючки в коротких штрихах. Адаптивное окно сохраняло эти крючки, но дуги при отрыве всё ещё могли превращаться в заметные углы. Обрезка с учётом направления удаляла больше переходного движения, ценой потери примерно 2.5 точки действительно изогнутого начала в одном записанном штрихе.

Каждый патч исправлял свой целевой случай. Но лежащая под ними модель по-прежнему требовала жёсткого контура, остающегося корректным при зашумлённом изогнутом вводе.

Поколение третье: объединение покрытия с отсечёнными квадратными окончаниями

Третий движок перестал строить единый контур. Он рисовал трапецию для каждого сегмента и диск в каждой внутренней точке на маске в градациях серого, объединял их покрытие, а затем один раз применял цвет текстовыделителя. Серповидные карманы обратного хода, потемнение самопересечений и отверстия из-за нулевого обхода исчезли, потому что рендерер больше не зависел от обхода многоугольника.

Объединение закругляло концы, и мы вернули квадратные окончания, отсекая покрытие двумя полуплоскостями в конечных точках. Эта отсечка исправила прямые линии с постоянным давлением, но привнесла более серьёзный дефект: основная часть изогнутого штриха может закономерно пересекать полуплоскость конечной точки, а отсечка окончания затем удаляла часть середины штриха. Во время живого ввода направление конечной точки постоянно менялось, поэтому отсечённая область двигалась, а штрих мерцал.

Изогнутый синий штрих до исправления отсечки окончания: его верхняя часть срезана по диагональной линии; зелёные точки показывают полный дискретизированный путь.

Глобальное ограничение окончания проходит через основную часть изогнутого штриха.

Тот же изогнутый синий штрих после добавления локального ограничения по длине дуги: обе ветви вокруг зелёного дискретизированного пути восстановлены.

Ограничение отсечения окончания примитивами в пределах одной половины ширины от конечной точки восстанавливает этот штрих.

Ограничение по длине дуги сделало отсечение окончания локальным и исправило каждый записанный случай. В экстремальном синтетическом крючке всё ещё оставалась небольшая белая выемка там, где наконечник с низким давлением соединялся с отсечённым окончанием. Третий движок снова обменял один геометрический сбой на другой.

MaLiang устраняет жёсткую границу

MaLiang строит штрих из повторяющихся текстурных штампов, а не из одного замкнутого контура. Его генератор пути использует квадратичные сегменты Безье, проходящие через средние точки соседних образцов, а его линейный рендерер размещает штампы с регулярным шагом вдоль этого пути. Размер, поворот, цвет и непрозрачность штампа могут меняться вдоль штриха.

Мягкий штамп не имеет отдельного окончания, соединения или правила обхода. Резкое изменение направления остаётся последовательностью перекрывающихся отметок. Обратные ходы не могут создать пустой карман многоугольника, а полуплоскость конечной точки не может перерезать середину штриха, потому что в модели нет отсечения конечной точки.

Мы приняли модель, а не код. Текущий HighlighterStrokeBuilder выполняет четыре операции:

  1. Удаляет только конечные образцы, давление которых ниже 0.005.
  2. Сглаживает положения сегментами квадратичной кривой Безье через средние точки.
  3. Размещает один круглый штамп на каждую точку длины дуги.
  4. Задаёт диаметр штампа по нормализованному давлению и композитит штампы с альфа-каналом в режиме source-over.

Размер сборщика сократился с 686 до 190 строк. Азимут, угол наклона, построение контура, маски объединения, окна направления конечной точки, удлинение окончания и отсечение окончания — всё это исчезло из пути текстовыделителя. Мы по-прежнему записываем ориентацию Pencil, потому что входная модель её поддерживает, но сам текстовыделитель больше не читает эти поля.

Формуле давления MaLiang потребовалась калибровка устройства

MaLiang рассчитывает размер штампа по формуле, эквивалентной

и предполагает, что force использует полезную часть диапазона от 0 до 1. Данные нашего устройства этому не соответствовали. Среди примерно 3 400 образцов Apple Pencil медианное давление составляло 0.047, а максимальное — 0.178. Если напрямую подать эти значения в формулу, обычное письмо получается гораздо тоньше номинального размера кисти.

Поэтому мы нормализуем давление относительно наблюдаемой медианы, затем применяем показатель степени и ограничиваем результат:

Теперь медианное давление при письме отображается в номинальный диаметр 12 пунктов. При очень лёгком касании штрих остаётся видимым при 35% номинального размера, а при сильном давлении перестаёт расти после 120%.

Эта калибровка относится к устройству ввода и поведению кисти, а не к MaLiang. Если просто скопировать исходную формулу, не измерив собственное распределение давления, архитектура сохранится, но кисть получится неправильной.

Альфа-канал каждого штампа вывели из известного числа перекрытий

MaLiang компенсирует перекрытие полупрозрачных штампов эмпирическим выражением alpha ÷ overlapping × 2.5. У нашей кисти фиксированный номинальный диаметр 12 пунктов и шаг штампов 1 пункт, поэтому по её центральной линии получается примерно 12 слоёв. В нашем случае число перекрытий известно, и компенсацию можно вычислить, а не оценивать.

Для целевой непрозрачности штриха и одинаковых слоёв с композицией source-over непрозрачность штампа даёт

поэтому для одного штампа требуется

При и каждый штамп использует непрозрачность примерно . Центр достигает заданного цвета после повторной композиции, а край получает меньше штампов и сохраняет переход с растушёвкой шириной в одну-две точки.

Лист с разнообразными синими штрихами маркера, отрисованными финальной моделью мягких штампов, включая петли, крючки, короткие отметки и самопересечения.

Финальный рендерер воспроизводит записанные пути с устройства. Крючки, диагональные срезы и белые карманы отсутствуют.

Две цены, на которые мы согласились

Самопересечения становятся темнее. Композиция в режиме source-over учитывает каждый штамп, включая штампы, оставленные предыдущим проходом по той же области, а движок объединения покрытия этого избегал. Мы приняли более тёмное перекрытие, потому что оно предсказуемо и сохраняет форму штриха.

Концы сужаются вместе с давлением. При настоящем отрыве давление уменьшается, а вместе с ним уменьшается диаметр штампа, поэтому штрих сужается, вместо того чтобы заканчиваться квадратным окончанием полной ширины. Во всех трёх движках геометрии мы считали такое квадратное окончание обязательным. Отказ от него устранил отсечение и связанную с концами механику, лежавшую в основе нескольких описанных выше сбоев, поэтому мы приняли сужающийся конец.

Таким образом, поставляемая нами модель делает меньше, чем предыдущие. Она сохраняет измеренное событие письма, реагирует на давление после калибровки устройства и отрисовывает изогнутые и самопересекающиеся пути без необходимости в глобально корректном контуре. За это мы отказались от пера в форме стамески, идеально квадратного окончания и точной полилинии между исходными образцами.

Порог давления проще всего перенести в другие проекты. Значение 0.005 взялось из разрыва между двумя кластерами в реальном вводе: письмо при 0.01 и выше, отслеживание в воздухе при 0.002 и ниже. Те же записанные штрихи показали, где не сработало каждое исправление геометрии. Эти сбои привели к модели штампов и определили две связанные с ней цены.