Чтобы определить размер текстового предпросмотра в браузере, измерьте элемент DOM после раскладки при нужной ширине области просмотра. Для метрик однострочного текста используйте Canvas. В этом руководстве показано, как сравнить оба способа на Mac и понять, почему результаты по ширине и высоте различаются.

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

Какую ширину и высоту текста измерять?

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

Ваш вопрос Измерение Что оно показывает
Какое горизонтальное расстояние занимает одна строка без переноса? Canvas measureText(text).width Ширина продвижения текста
Каковы границы контуров глифов Canvas? Свойства Canvas actualBoundingBox… Границы глифов относительно точки выравнивания и базовой линии
Сколько места в раскладке занимает карточка? Элемент getBoundingClientRect() Его ограничивающий прямоугольник по внешним границам, включая внутренние поля и рамки
Где находятся фрагменты текста после переноса? Range getClientRects() Прямоугольники, связанные с выбранным текстом
Какой прямоугольник охватывает эти фрагменты текста? Range getBoundingClientRect() Их общий ограничивающий прямоугольник

Ширина продвижения Canvas и ширина глифов могут различаться: курсивные контуры могут выходить за пределы интервала продвижения. Чтобы получить границы глифов, рассчитайте ширину как actualBoundingBoxLeft + actualBoundingBoxRight, а высоту — как actualBoundingBoxAscent + actualBoundingBoxDescent. Эти значения измеряются в CSS-пикселях. TextMetrics — MDN

Измерение элемента включает внутренние поля и рамки, а Range, содержащий только текст, измеряет выбранный текст с использованием метрик шрифта. Ни одно из них нельзя считать точным прямоугольником закрашенных пикселей. В спецификации W3C горизонтальные прямоугольники текста Range определяются с учётом высоты над базовой линией, высоты под ней и продвижения текста. Element: getBoundingClientRect() method — MDN, CSSOM View Module — W3C

Для предпросмотра размеров в браузере используйте блок элемента, чтобы выделить место, а прямоугольники Range — чтобы изучить поток текста. Сохраняйте измерения Canvas как отдельный ориентир для строки без переноса.

Сначала задайте область просмотра и параметры текста

В Chrome на Mac откройте DevTools, включите Toggle device toolbar и выберите Responsive в разделе Dimensions. Введите нужные ширину и высоту в полях области просмотра. В документации Chrome описан ввод чисел и изменение размера области просмотра перетаскиванием. Для предпросмотра на компьютере выберите тип устройства для компьютера, если этот параметр доступен. Simulate mobile devices with device mode — Chrome for Developers

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

Например, предположим, что ширина карточки по внешним границам составляет 360px, горизонтальные внутренние поля — 16px с каждой стороны, а рамки — по 1px. Доступная ширина содержимого равна 360 − 32 − 2 = 326px. Это ширина, доступная для переноса строк. При этом ограничивающий прямоугольник элемента по-прежнему будет иметь ширину 360px. Такое поведение соответствует описанному способу измерения по внешним границам. Element: getBoundingClientRect() method — MDN

Задайте нужные семейство, насыщенность, размер и стиль шрифта, высоту строки, межбуквенный интервал и правила переноса. В частности, white-space: normal схлопывает пробельные символы и разрешает переносы, тогда как pre-wrap сохраняет пробелы и переводы строк в исходном тексте, но тоже допускает переносы. Предпросмотр с одним режимом не может надёжно представить другой. white-space — MDN

Также различайте размер шрифта и высоту строки. При горизонтальном письме line-height участвует в определении высоты строки раскладки; это не видимая высота букв. Поэтому для шрифта размером 16px с высотой строки 24px требуется расчёт высоты, отличный от расчёта границ глифов Canvas. line-height — MDN

Запустите полный предпросмотр синтетического текста

Откройте пустую вкладку, DevTools, выберите Console и один раз выполните следующий полный скрипт для браузера. Он создаст нейтральную карточку и отчёт об измерениях. Библиотеки, внешние шрифты, существующие селекторы и HTML-файлы не нужны. Перед запуском другой копии перезагрузите пустую вкладку.

Карточка подстраивается под область просмотра, пока не достигнет ширины 480px по внешним границам. Отчёт обновляется при изменении размера окна или эмулируемой области просмотра. Этот пример намеренно ограничен простым горизонтальным текстовым блоком с единообразным оформлением. Код, зависящий от браузера, был проверен, но при подготовке не запускался в браузере.

(async () => {
  const text =
    "Sample collection: round forms, soft gradients, and simple patterns.";

  document.body.style.margin = "0";

  const card = document.createElement("div");
  card.textContent = text;
  card.lang = "en";
  card.dir = "ltr";
  card.style.cssText = [
    "box-sizing: border-box",
    "width: min(480px, calc(100% - 48px))",
    "margin: 24px",
    "padding: 16px",
    "border: 1px solid #777",
    "font: normal 400 16px/24px Arial, sans-serif",
    "font-kerning: normal",
    "letter-spacing: 0px",
    "word-spacing: 0px",
    "white-space: normal",
    "overflow-wrap: normal",
    "word-break: normal",
    "hyphens: none",
    "color: #222",
    "background: #f4f4f4"
  ].join(";");

  const report = document.createElement("pre");
  report.style.cssText =
    "margin: 24px; white-space: pre-wrap; overflow-wrap: anywhere;";
  document.body.append(card, report);

  const canvas = document.createElement("canvas");
  const ctx = canvas.getContext("2d");
  if (!ctx) {
    report.textContent = "A Canvas 2D context is unavailable.";
    return;
  }

  // Establish layout before waiting for the fonts used by that layout.
  card.getBoundingClientRect();
  await document.fonts.ready;

  const format = value =>
    Number.isFinite(value) ? value.toFixed(2) : "unavailable";

  function measure() {
    const style = getComputedStyle(card);
    const box = card.getBoundingClientRect();
    const horizontalExtras =
      parseFloat(style.paddingLeft) + parseFloat(style.paddingRight) +
      parseFloat(style.borderLeftWidth) + parseFloat(style.borderRightWidth);

    const range = document.createRange();
    range.selectNodeContents(card);
    const textBox = range.getBoundingClientRect();
    const fragments = Array.from(range.getClientRects());

    ctx.font = "normal 400 16px Arial, sans-serif";
    ctx.fontKerning = "normal";
    ctx.direction = "ltr";
    ctx.textAlign = "left";
    ctx.textBaseline = "alphabetic";
    const metrics = ctx.measureText(text);
    const inkWidth =
      metrics.actualBoundingBoxLeft + metrics.actualBoundingBoxRight;
    const inkHeight =
      metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;

    report.textContent = [
      "All dimensions below are CSS pixels.",
      `Layout viewport: ${document.documentElement.clientWidth} x ${document.documentElement.clientHeight}`,
      `Card border box: ${format(box.width)} x ${format(box.height)}`,
      `Available text width: ${format(box.width - horizontalExtras)}`,
      `Range bounds: ${format(textBox.width)} x ${format(textBox.height)}`,
      `Range fragment count: ${fragments.length}`,
      `Canvas unwrapped advance: ${format(metrics.width)}`,
      `Canvas glyph bounds: ${format(inkWidth)} x ${format(inkHeight)}`,
      `Computed line height: ${style.lineHeight}`
    ].join("\n");
  }

  measure();
  window.addEventListener("resize", measure);
})().catch(error => console.error("Preview failed:", error));

Вызовы для элемента и Range используют фактическую раскладку браузера. Canvas измеряет ту же синтетическую строку как одну строку без переноса. Согласно стандарту HTML, Canvas при подготовке текста заменяет ASCII-пробелы пробелами; передача символа новой строки в measureText() не создаёт CSS-абзац из нескольких строк. HTML Standard: The canvas element — WHATWG

После настройки раскладки скрипт ожидает выполнения document.fonts.ready. Согласно MDN, это обещание выполняется после завершения загрузки и операций раскладки для используемых шрифтов. Это не означает, что загрузились все объявленные шрифты или что отображается именно изначально предпочтительный шрифт. Document: fonts property — MDN

Интерпретируйте результаты и определите, помещается ли текст

Начните с показателя Available text width, а затем сравните высоту карточки при двух размерах области просмотра. Если из-за переноса добавилась строка, высота карточки изменится, тогда как продвижение Canvas для строки без переноса может остаться прежним. Такое сочетание помогает отличить изменение переноса от смены шрифта.

Для этого примера приведённые ниже значения ширины — иллюстративные расчёты, а не результаты измерения шрифта. Предполагается, что область просмотра раскладки имеет указанную ширину, а стили скрипта не меняются.

Ширина области просмотра раскладки Ширина карточки по внешним границам Доступная ширина текста
360px 312px 278px
800px 480px 446px
1200px 480px 446px

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

В простом блоке только с единообразным текстом иллюстративная раскладка из трёх строк при высоте строки 24px занимает 72px по высоте строк раскладки. Добавьте 32px вертикальных внутренних полей и 2px рамок — получится иллюстративная высота карточки 106px. Этот расчёт помогает понять результат; когда содержимое или стили становятся сложнее, решающим значением остаётся измеренная высота DOM. line-height — MDN

Не считайте автоматически показатель Range fragment count универсальным числом строк. API Range возвращает текстовые и элементные прямоугольники согласно правилам выделения. Форматированный текст, вложенные элементы и фрагментированная раскладка могут дать более сложный набор прямоугольников. В примере выбирается только один простой текстовый узел, чтобы результат диагностики было легко понять. CSSOM View Module — W3C

Также не пытайтесь оценивать высоту текста с переносами по формуле ceil(unwrappedWidth / availableWidth). Она предполагает, что текст можно переносить в произвольных горизонтальных позициях. Переносы в браузере должны соответствовать заданным правилам работы с пробелами и разбивки текста, поэтому измеряйте блок с уже перенесённым текстом. white-space — MDN

Что проверить, если измерения расходятся?

Сначала проверьте границы измерения. Прямоугольник карточки во всю ширину и более узкий прямоугольник Range могут оба быть правильными. Карточка резервирует место в раскладке, а Range описывает выделенный текст. Высота глифов Canvas — третье значение с другой целью.

Затем проверьте готовность шрифтов. Если вы подключили объявленный веб-шрифт, document.fonts.load(fontSpecification, sampleText) может явно запросить соответствующие начертания перед измерением. Обрабатывайте отклонение промиса, а не подменяйте его успешным результатом без уведомления. Необязательный аргумент с текстом фильтрует шрифты по диапазонам Unicode, но не проверяет поддержку отдельных глифов. FontFaceSet: load() method — MDN

При расчёте ширины подписи сохраняйте границы формирования шрифта. Измеряйте строку целиком, а не складывайте ширины символов, измеренных по отдельности. Спецификация Canvas подготавливает позиционированную последовательность глифов с текущими настройками шрифта и текста; полученное значение подходит для этой строки. Ограничьте сравнение Canvas в этом примере указанными настройками шрифта и не предполагайте, что оно воспроизводит все возможности типографики CSS. HTML Standard: The canvas element — WHATWG

Для существующей страницы измеряйте сам компонент на месте. Перенос текста в универсальный скрытый блок меняет условия раскладки, если не воспроизвести соответствующие стили и контекст контейнера. Элементы без связанных блоков раскладки не дают полезных обычных измерений блоков, а CSS-трансформации влияют на прямоугольники, возвращаемые этими API. Синтетический пример позволяет избежать обеих сложностей. CSSOM View Module — W3C

Как повторить предпросмотр при нескольких размерах окна браузера?

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

Для повторных проверок Playwright поддерживает явное задание размеров области просмотра и отдельный параметр deviceScaleFactor. Документация также позволяет переопределить область просмотра предустановки устройства. Это даёт поддерживаемый способ автоматизации: задать область просмотра, загрузить страницу, дождаться шрифтов и нужного содержимого, а затем получить измерения DOM в браузере. Emulation — Playwright

Храните размеры CSS отдельно от плотности пикселей экрана. Playwright предоставляет область просмотра и коэффициент масштабирования как отдельные параметры, а текстовые метрики Canvas выражаются в CSS-пикселях. Более плотный экран сам по себе не требует вдвое увеличивать ширину CSS, выделенную под подпись. Emulation — Playwright, TextMetrics — MDN

Поэтому полезный результат предпросмотра — это краткая запись условий: браузер, область просмотра, текст, типографические параметры, ширина содержимого и измеренная высота карточки. Используйте результат измерения DOM для определения размера компонента с переносами, сохраняйте метрики Canvas для вопросов об однострочном тексте и повторяйте измерение в каждом браузере, отображение которого требуется представить.

Источники