Векторизация шрифтов и textToPath

MixSlide поддерживает преобразование текста в SVG-пути прямо в runtime виджетов. Это позволяет строить эффектные текстовые фигуры (logo-like, обводка, текст по кривой, маски).

Доступные функции

  • textToPath(text, fontRefId, _fontSize, layoutOptions)
    (_fontSize в текущей реализации runtime передаётся как legacy-параметр и не влияет на расчёт. Размер шрифта задаётся в параметрах рендера/масштабировании viewBox.)
  • textToPathAlongSVGPath(text, fontRefId, fontSize, pathData, layoutOptions)

Обе функции возвращают объект:

{
  path: string;
  bbox: { x, y, width, height };
  ready: boolean;
  topToBaseline?: number;
  unitsPerEm?: number;
  xHeight?: number;
}

Подготовка параметров

Для работы с шрифтом нужен параметр типа font:

const { text, font, fontSize } = useParams({
  text: { type: "text", default: "Sample" },
  font: { type: "font" },
  fontSize: { type: "integer", min: 1, default: 48, step: 1 },
});

Объект font обычно содержит:

  • refId — ссылка на шрифт,
  • features — OpenType features,
  • variant — оси вариативности.

Рекомендуемый паттерн рендера

ready может быть false, пока шрифт не загрузился. Поэтому лучше считать путь и bbox через useBlockMemo, а затем безопасно строить viewBox.

function Widget() {
  const { text, font, fontSize, width, height, fitWidth, fitHeight, fill, stroke, strokeWidth, letterSpacing, wordSpacing } = useParams({
    text: { type: "text", default: "Привет" },
    font: { type: "font" },
    fontSize: { type: "integer", min: 6, max: 300, default: 48, step: 1 },
    width: { type: "float" },
    height: { type: "float" },
    fitWidth: { type: "boolean", default: true },
    fitHeight: { type: "boolean", default: true },
    fill: { type: "colorGradient", default: "#000000" },
    stroke: { type: "colorGradient", default: "#000000" },
    strokeWidth: { type: "integer", min: 0, step: 1, default: 0 },
    letterSpacing: { type: "float", default: 0 },
    wordSpacing: { type: "float", default: 0 },
  });

  const {_fill, _stroke} = useBlockMemo("resolvedPaint", () => ({
    _fill: isGradient(fill) ? undefined : fill,
    _stroke: isGradient(stroke) ? undefined : stroke,
  }), [fill, stroke]);

  const { path, viewBox } = useBlockMemo("textPath", () => {
    const result = textToPath(
      text,
      font?.refId,
      fontSize,
      { letterSpacing, wordSpacing, features: font?.features, variant: font?.variant },
    );
    const bbox = result.ready && result.path ? getSvgPathBbox(result.path) : { x: 0, y: 0, width, height };
    const vWidth = fitWidth ? bbox.width : width;
    const vHeight = fitHeight ? bbox.height : height;
    return {
      path: result.path,
      viewBox: `${fitWidth ? bbox.x : 0} ${fitHeight ? bbox.y : 0} ${vWidth} ${vHeight}`,
    };
  }, [text, font?.refId, fontSize, width, height, fitWidth, fitHeight, letterSpacing, wordSpacing, font?.features, font?.variant]);

  return (
    <svg width="100%" height="100%" viewBox={viewBox} preserveAspectRatio="none">
      <path
        d={path}
        fill={_fill}
        stroke={_stroke}
        strokeWidth={strokeWidth}
        vectorEffect="non-scaling-stroke"
      />
    </svg>
  );
}

Важные моменты

  • textToPath может вернуться с ready: false до подгрузки шрифта.
  • До того момента используйте fallback bbox/плейсхолдер, чтобы не получить нулевой viewBox.
  • Для текста по кривой используйте textToPathAlongSVGPath; для него обязателен pathData.
  • font-объект лучше не кэшировать самосозданными глобальными переменными — храните зависимости через useBlockMemo.

![Пример текстового SVG с автоматическим пересчётом viewBox] []

Практические рекомендации по качеству результата

  1. Для сложных шрифтов включайте корректные features/variant только в том случае, если они действительно нужны.
  2. Ограничивайте максимальную длину текста на одном пути, чтобы не словить maxSteps.
  3. Если нужен стабильный рендер по умолчанию, используйте плоский текстовый fallback и переключайте в path только когда ready = true.
  4. Избегайте многократного пересчёта пути в каждом render — мемоизируйте через useBlockMemo.