У меня нередко бывает, что я решу не увлекаться слишком сильно какой-то затеей, но в итоге всё равно в неё погружаюсь. Так было и на этот раз.
Недавно я создала на JavaScript собственный блочный шрифт, решив, что запрограммировать слитное рукописное письмо будет слишком сложно. Но не прошло и двух месяцев, как вот я здесь и готова рассказать о созданном мной механизме слитного письма. Пожалуй, в этом есть свой урок, но мы в его смысл углубляться не станем.
▍ Блочный шрифт
Предыдущая моя статья (англ.) была посвящена разработке печатной версии алфавита. Если коротко, то реализовала я его так:
- Создала код для определения ключевых точек на пути написания каждой буквы (~10 точек на букву).
- Сгладила эти пути с помощью алгоритма Чайкина.
- Преобразовала пути в контуры букв с переменной толщиной.
- Отрисовала полученные контуры с помощью библиотеки p5js.
Вот, что в итоге получилось:
Пути написания букв я определяла вручную, прописывая и подгоняя их позиции в коде, пока буквы не будут выглядеть нужным образом. Когда же дело коснулось программирования слитного письма, я упростила этот процесс.
▍ Дизайн букв
Для простоты работы я создала в редакторе p5js инструмент, упрощающий вывод ключевых точек в путях.
Он показывает образец буквы (для понимания масштаба и контекста) рядом с областью, в которой нужно построить новую букву. Само же построение происходит в несколько шагов:
- Кликами мыши отмечаются ключевые точки пути, который принимает форму кривой Чайкина.
- Нажатием
p
происходит переход в режим редактирования. - Точки поочерёдно перетаскиваются в нужные позиции.
- Нажатием «Ввод» готовый путь выводится в консоль.
Я создала для каждой буквы по 2-3 варианта.
Итоговый путь написания выглядит так:
[{x:0.7,y:22.5},{x:8.2,y:18.1},{x:8.9,y:11.2},{x:3.7,y:11.4},{x:1.7,y:18.9},{x:8.4,y:22.4},{x:17.7,y:22.0}]
Я хотела использовать в качестве ориентира собственный рукописный шрифт, поэтому записала ряд примеров прописных и заглавных букв, изображение которых загрузила напрямую в инструмент построения букв для трассировки.
Клавиши w/a/s/d используются для размещения изображения в нужной точке, а r/e — для его приближения или отдаления. Размытая e
на изображении выше выступает образцом.
Числа на этом листе бумаги представляют координаты x
и y
, обеспечивающие попадание образца в окно создания буквы.
После разметки всех путей, их выравнивания и преобразования в контуры с переменной толщиной (подробнее об этом в прошлой статье), отдельные буквы получились такими:
▍ Соединение букв
Иногда соединять между собой буквы легко — достаточно просто перейти сразу от одного пути ключевых точек к следующему, после чего алгоритм Чайкина разом их все объединит. Но некоторые пары букв связывать проблематично.
Возьмём, к примеру, пару na
. На изображении ниже красным обозначена последняя точка написания n
, а зелёным — первая точка a
. Первая находится внизу, а вторая вверху, в результате чего объединяющая линия проходит по диагонали через a
, делая её похожей на e
.
Ещё один пример. В паре ti
буква t
заканчивается как раз над базовой линией, и написание i
начинается оттуда, создавая неестественный выступ.
Чтобы исправить эти нюансы, в первом случае можно добавить в начало a
дополнительную точку, а во втором удалить две последние точки из t
.
Но такое изменение букв подойдёт не для всех сценариев.
Например, если a
находится в начале слова, дополнительная точка приведёт к появлению лишнего хвоста, а если перед a
будет идти w
, то получится линия, проходящая через a
иначе. Что касается t
, то в случае перехода в k
она деформируется.
Точки в начале и конце контуров букв должны менять своё положение в зависимости от того, какая буква идёт следующей.
Сначала я пробовала выделить конкретные «проблемные» пары и отдельно прописать для них правила, но в итоге решила этот вопрос иначе, добавив в начало и конец каждого пути одно число, определяющее один из четырёх возможных случаев:
- Невозможность соединения со следующей буквой (0).
- Соединение в районе базовой линии (1).
- Соединение чуть выше базовой линии (2).
- Соединение в районе x-height (3)
Вот несколько примеров:
Ниже показано, как теперь выглядит путь написания каждой буквы. Обратите внимание на цифры в начале и конце:
[0,{x:12.2,y:13.2},{x:13.5,y:11.0},{x:6.2,y:8.4},{x:1.1,y:13.0},{x:1.8,y:19.0},{x:7.0,y:23.4},{x:15.2,y:23.6},{x:18.4,y:22.1},1],
Возможные соединения всех пар букв я проверила таким образом:
Здесь также видны некоторые отклонения, вызванные наличием у каждой буквы нескольких путей и их редактированием в зависимости от того, какая буква идёт следующей. В идеале для каждой буквы у меня должно было получиться не менее 5-6 вариантов путей написания, но я стремилась к балансу, чтобы не раздувать размер файла.
▍ Создание слов
При создании слова:
- Для каждой буквы из 2-3 вариантов выбирается базовый путь.
- Информация о концах пути передаётся в смежные буквы (изначально должны быть выбраны все пути букв, поскольку в некоторых случаях разные их варианты для одной и той же буквы имеют разные конечные точки).
- Смежные базовые пути сонастраиваются. Например, если высота предыдущей буквы равна 2, из начала этого пути удаляется одна точка, или если стартовая высота следующей буквы равна 1, в определённое место добавляется дополнительная точка.
Функции корректировки букв порой получаются сложными. Вот пример для буквы q
:
// ip = путь
// pc = информация о конце пути предыдущей буквы
// nc = информация о начале следующей буквы
// n = индекс пути, выбранного для этой буквы
adjust: (ip, pc, nc, n) => {
// Случайным образом добавляет в конец разрыв, равный 70% этой буквы.
if (rand() < 0.7 ) ip.splice(-1, 1, 0);
// Если из 4 возможных вариантов для этого пути был выбран [2],
if (n < 2) {
// а предыдущая буква заканчивается на 3, заменить первые две точки на другую точку.
if (pc == 3) ip.splice(1, 2, {x:10,y:12});
// В противном случае, если это не 0, добавить точку в начало.
else if (pc > 0) ip.splice(1, 0, {x:10,y:20});
}
// Если между этим символом и следующим нет разрыва (0),
if (nc > 0 && ip[ip.length-1] != 0){
// заменить последние две точки другой точкой.
ip.splice(-3, 2, {x:16,y:34})
}
}
Но зачастую они откровенно короткие. Например, вот функция для буквы n
:
adjust: (ip, pc, nc) => {
// Если следующая буква начнётся с 3, на выбор создать разрыв или переместить последнюю точку.
if (nc == 3) rand() < 0.3 ? ip.splice(-1, 1, 0) : ip.splice(-2, 1, {x:17,y:23.8})
}
Затем базовые пути всех букв объединяются. При этом программа игнорирует 1,2 и 3 в их путях, но при встрече 0 создаёт разрыв, начиная новый путь.
После прорисовки этих путей, их преобразования в контуры с переменной толщиной и внесения некоторого дрожания точек с помощью шума Перлина, получился такой рукописный шрифт:
Забавы ради приведу для сравнения два текста: один, полученный программным путём на плоттере, и второй написанный мной вручную.
▍ Сколько он весит?
При создании блочного шрифта код для обработки букв составил 9,7 КБ. В случае же рукописного после прогона через минификатор сейчас он весит 26,1 КБ.
Так получилось в основном из-за присутствия у каждой буквы нескольких вариантов путей, а также функции для корректировки крайних точек соседних букв. Но мне удалось сэкономить. Уверена, что можно добиться ещё большей экономии — хоть я и не профессиональный программист, но кое-какие идеи у меня есть.
Например, сейчас буквы построены на основе предустановленного размера шрифта 20 и последующего изменения этого размера. В итоге множество точек определены как, например x: 14.5
. Если же сменить базовый размер шрифта на 200, то точку можно будет определить как 145, удалив один символ (десятичный разделитель). Это изменение нужно вносить осторожно, так что пока я его отложила.
▍ Как я всё это использую?
Основная цель — это заголовки, подписи и заметки на чертежах, с которыми я работала. Но мне также очень нравится просто играться с самим текстом.
Одно из главных преимуществ использования запрограммированных путей написания текста вместо шрифта в том, что их можно корректировать — например, изменять положение букв, толщину отдельных из них и так далее.
Следующим делом я планирую встроить этот рукописный шрифт в те самые чертежи, но ещё я определённо хочу создать что-то для самого текста, так как он мне очень нравится, и здесь есть ещё масса возможностей.
Автор: Дмитрий Брайт