Задать вопрос

Как сделать эффект текста на кривой безье?

Добрый день!
Я хочу сделать возможность добавлять приведенный ниже эффект для текста (использую html canvas для графики).
676bcaabb25df869104502.png
Писать буду сам, без использования внешних библиотек. Я научался рисовать сплайн из двух плавно соединенных кривых безье. Теперь пытаюсь разобраться какая математика стоит за таким эффектом текста на ленте.
Буду благодарен за помощь.
  • Вопрос задан
  • 1463 просмотра
Подписаться 5 Сложный 10 комментариев
Пригласить эксперта
Ответы на вопрос 3
@garbagecollected
Математика трансформаций очень простая.

Отрендерить текст на изображение можно используя
https://opentype.js.org/

Вы можете отрендерить каждую букву используя библиотеку, а далее уже отрендеренные буквы использовать на своем сайте без библиотеки.

Либо использовать эту же библиотеку для того, чтобы получить path data для каждой буквы.

Если ваша трансформация сводится к аффинным преобразованиям, то можно рассчитывать координаты не каждой точки изображения буквы, а только точки, которые участвуют в path data. Таким образом трансформируя все точки path data вы получите такую же path data, используя которую вы начертаете шрифты в любом месте на любом изображении. Это может дать очень выгодный прирост к производительности (потенциально до 1000х раз). Кроме того, линии path data "рисуется" с правильным расчетом межпиксельного пространства, и вам не надо будет делать ресамплинг, как при работе с трансформированием изображений.

Но рисовать шрифты по безье - это не всегда можно свести к аффинным преобразованиям.

На верхней иллюстрации у вас используется искажение по оси X: skewX()

Получить координаты каждой точки можно умножив матрицу с исходными точками на матрицу CTM.
https://www.w3.org/TR/SVG11/coords.html#TransformM...

Сама матрица CTM - это произведение матриц всех трансформаций. То есть, чтобы не умножать матрицу точек на матрицы каждой трансформации, сначала вычисляется произведение матриц всех трансформаций, которое называется CTM.

По ссылке выше вы найдёте матрицы с формулами аффинных преобразований в двухмерном пространстве. Матрицы имеют вид (3х3) из которых 6 чисел значимые, 3 числа - статичные (всегда [0, 0, 1]).

Для 3D-преобразований используются матрицы 4х4 из которых 12 чисел значимые, 4 числа - статичные (всегда [0, 0, 0, 1]):
https://drafts.csswg.org/css-transforms-2/#mathema...

Для расчета 3d-трансформаций с учетом перспективы используются матрицы 8х8.

На нижней иллюстрации у вас используются трехмерные искажения.

Про математику 3D искажении с перспективой можно прочитать тут
https://www.cs.cmu.edu/~ph/texfund/texfund.pdf
Там рассматриваются и формулы получения координат на плоскости и ресамплинг (чтобы, например, не было пикселизации шрифтов после трансформации).

Для перемножений матриц можно использовать функцию

const multiply = (a, b) => a.map((_, r) => b[0].map((_, c) => a[r].reduce((s,_,i) => s + a[r][i] * b[i][c], 0)));


Рассчитать координаты безье можно функциями (соответственно, квадратичная и кубическая безьё):

// [p0x, p0x] - are coordinates of origin point
// [p1x, p1y] - are coordinates of single control point
// [p2x, p2y] - are coordinates of destination point
// T - is number of points that needs to draw the curve
const quadratic = ([p0x,p0y], [p1x,p1y], [p2x,p2y], T = 60) => {
  const x = t => (1 - t)**2 * p0x + 2 * (1 - t) * t * p1x + t**2 * p2x;
  const y = t => (1 - t)**2 * p0y + 2 * (1 - t) * t * p1y + t**2 * p2y;
  return Array.from({ length: T+1 }).map((_, t) => [x(t / T), y(t / T)]);
};

// [p0x, p0x] - are coordinates of origin point
// [p1x, p1y] - are coordinates of first control point
// [p2x, p2y] - are coordinates of second control point
// [p3x, p3y] - are coordinates of destination point
// T - is number of points that needs to draw the curve
const cubic = ([p0x, p0y], [p1x, p1y], [p2x, p2y], [p3x, p3y], T = 60) => {
  const y = t => (1 - t)**3 * p0y + 3 * (1 - t)**2 * t * p1y + 3 * (1 - t) * t**2 * p2y + t**3 * p3y;
  const x = t => (1 - t)**3 * p0x + 3 * (1 - t)**2 * t * p1x + 3 * (1 - t) * t**2 * p2x + t**3 * p3x;
  return Array.from({ length: T+1 }).map((_, t) => [x(t / T), y(t / T)]);
};
Ответ написан
hint000
@hint000
у админа три руки
Нужно понять два возможных пути. Растровая деформация букв и векторная деформация. Векторная представляется мне настолько сложнее растровой (раз в сто), что даже не хочется думать о возможной её реализации (хотя результат может стоить таких усилий).

Растровый вариант довольно очевидный: кривая должна быть выражена в параметрической форме (x=x(t), y=y(t)); нарисуем букву без деформации в буфер (массив пикселей), затем будем пробегать такие значения t, чтобы (приблизительно) попадать в x(t)=n, n+1, n+2, n+3,.. Одновременно будем получать некие значения y(t).
Для t1 копируем первый столбец пикселей из буфера в позицию int(x(t1)), int(y(t1)).
Для t2 копируем второй столбец пикселей из буфера в позицию int(x(t2)), int(y(t2)).
И т.д.
Ещё раз подчеркну условие: нужно, чтобы x(t1), x(t2), и т.д. отличались приблизительно (с желаемой точностью) на единицу (в любую сторону), этого нетрудно добиться, уменьшая шаг t. Так мы будем каждый раз продвигаться на 1 пиксель по горизонтали вправо или влево.
Т.е. вы просто копируете столбики пикселей, и каждый из столбиков может быть смещён относительно предыдущего. Можно не только текст, но и любую растровую картинку так деформировать.
Ответ написан
sfi0zy
@sfi0zy
Creative frontend developer
На картинках мы видим эффект, в котором есть только вертикальные смещения букв по ходу движения. Нет никаких поворотов, растяжений, и.т.д. Только сдвиг по одной оси. Это упрощает задачу, сводя ее суть к описанному в соседнем ответе, но дальше вы там куда-то ушли в сторону трансформаций и горы масок. Это все сложно для восприятия и производительность будет оставлять желать лучшего. В контексте JS и 2D-канвасов можно пойти другим, более традиционным, путем:

  1. Определить разумную максимальную длину текста в пикселях.
  2. Сделать канвас этого размера, залить белым.
  3. Нарисовать на нем черный текст стандартным методом, чтобы он занимал 100% ширины. Подгонка текста по ширине - типовая задача, легко гуглится.
  4. Достать содержимое канваса в виде imageData. На этом этапе у нас есть текстура с нарисованным текстом.
  5. Сделать второй канвас, на котором пользователь работает. Залить белым.
  6. Достать его imageData или создать новую под его размер.
  7. Взять кривую в формате x(t), y(t). Пройти циклом по t. Разумный шаг определяется в зависимости от длины кривой на канвасе, он должен давать смещение не больше 1 по x. Для абсолютной надежности можно сделать шаг меньше, и на каждой итерации проверять, сместились ли мы. Своего рода трассировка луча по кривой.
  8. Сопоставить диапазон t и координату u в текстуре с текстом. Проход по t должен логически соответствовать проходу по горизонтали по всей текстуре.
  9. На каждой итерации цикла взять столбец из imageData текстуры с текущим u, скопировать его в imageData рабочего канваса по координатам x, y. Копируем не 1 в 1, а с учетом масштабирования, которое мы уже знаем из соотношений t и u, чтобы сохранить пропорции букв. С учетом наложений вероятно имеет смысл копировать только черное, не перекрашивая черное обратно в белое.
  10. Обновить imageData рабочего канваса.
  11. При обновлении данных повторять от п.6. Вероятно будет хорошей идеей не создавать imageData каждый раз заново, чтобы не перевыделять память под нее каждый раз.

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

Войдите, чтобы написать ответ

Похожие вопросы